decimer-segmentation 1.4.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/PKG-INFO +4 -6
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/__init__.py +1 -1
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/complete_structure.py +12 -16
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/decimer_segmentation.py +242 -136
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/model.py +6 -2
- decimer_segmentation-1.5.0/decimer_segmentation/optimized_complete_structure.py +377 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/PKG-INFO +4 -6
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/SOURCES.txt +1 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/requires.txt +4 -2
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/setup.py +10 -10
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/LICENSE +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/README.md +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/config.py +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/moldetect.py +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/utils.py +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/visualize.py +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/dependency_links.txt +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/top_level.txt +0 -0
- {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: decimer_segmentation
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: DECIMER Segmentation - Extraction of chemical structure depictions from scientific literature
|
|
5
5
|
Home-page: https://github.com/Kohulan/DECIMER-Image-Segmentation
|
|
6
6
|
Author: Kohulan Rajan
|
|
@@ -9,14 +9,12 @@ Maintainer: Kohulan Rajan
|
|
|
9
9
|
Maintainer-email: kohulan.rajan@uni-jena.de
|
|
10
10
|
License: MIT
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
14
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
18
16
|
Classifier: Operating System :: OS Independent
|
|
19
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.10
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
21
19
|
License-File: LICENSE
|
|
22
20
|
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/complete_structure.py
RENAMED
|
@@ -130,11 +130,7 @@ def detect_horizontal_and_vertical_lines(
|
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
def find_equidistant_points(
|
|
133
|
-
x1: int,
|
|
134
|
-
y1: int,
|
|
135
|
-
x2: int,
|
|
136
|
-
y2: int,
|
|
137
|
-
num_points: int = 5
|
|
133
|
+
x1: int, y1: int, x2: int, y2: int, num_points: int = 5
|
|
138
134
|
) -> List[Tuple[int, int]]:
|
|
139
135
|
"""
|
|
140
136
|
Finds equidistant points between two points.
|
|
@@ -161,7 +157,7 @@ def find_equidistant_points(
|
|
|
161
157
|
def detect_lines(
|
|
162
158
|
image: np.ndarray,
|
|
163
159
|
max_depiction_size: Tuple[int, int],
|
|
164
|
-
segmentation_mask: np.ndarray
|
|
160
|
+
segmentation_mask: np.ndarray,
|
|
165
161
|
) -> np.ndarray:
|
|
166
162
|
"""
|
|
167
163
|
This function takes an image and returns a binary mask that labels the pixels that
|
|
@@ -180,12 +176,14 @@ def detect_lines(
|
|
|
180
176
|
image = ~image * 255
|
|
181
177
|
image = image.astype("uint8")
|
|
182
178
|
# Detect lines using the Hough Transform
|
|
183
|
-
lines = cv2.HoughLinesP(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
lines = cv2.HoughLinesP(
|
|
180
|
+
image,
|
|
181
|
+
1,
|
|
182
|
+
np.pi / 180,
|
|
183
|
+
threshold=5,
|
|
184
|
+
minLineLength=int(max(max_depiction_size) / 4),
|
|
185
|
+
maxLineGap=10,
|
|
186
|
+
)
|
|
189
187
|
# Generate exclusion mask based on detected lines
|
|
190
188
|
exclusion_mask = np.zeros_like(image)
|
|
191
189
|
if lines is None:
|
|
@@ -243,9 +241,7 @@ def expansion_coordination(
|
|
|
243
241
|
the mask expansion. It returns the expanded mask. The purpose of this function is
|
|
244
242
|
wrapping up the expansion procedure in a map function.
|
|
245
243
|
"""
|
|
246
|
-
seed_pixels = get_seeds(image_array,
|
|
247
|
-
mask_array,
|
|
248
|
-
exclusion_mask)
|
|
244
|
+
seed_pixels = get_seeds(image_array, mask_array, exclusion_mask)
|
|
249
245
|
mask_array = expand_masks(image_array, seed_pixels, mask_array)
|
|
250
246
|
return mask_array
|
|
251
247
|
|
|
@@ -302,7 +298,7 @@ def complete_structure_mask(
|
|
|
302
298
|
hough_lines = detect_lines(
|
|
303
299
|
binarized_image_array,
|
|
304
300
|
max_depiction_size,
|
|
305
|
-
segmentation_mask=np.any(mask_array, axis=2).astype(np.
|
|
301
|
+
segmentation_mask=np.any(mask_array, axis=2).astype(np.bool_),
|
|
306
302
|
)
|
|
307
303
|
hough_lines = binary_dilation(hough_lines, footprint=kernel)
|
|
308
304
|
exclusion_mask = horizontal_vertical_lines + hough_lines
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
* This Software is under the MIT License
|
|
3
|
+
* Refer to LICENSE or https://opensource.org/licenses/MIT for more information
|
|
4
|
+
* Written by ©Kohulan Rajan 2020
|
|
5
|
+
* Optimized for performance
|
|
5
6
|
"""
|
|
7
|
+
|
|
6
8
|
import os
|
|
7
9
|
import requests
|
|
8
10
|
import cv2
|
|
9
11
|
import argparse
|
|
10
|
-
import warnings
|
|
11
12
|
import numpy as np
|
|
12
|
-
from copy import deepcopy
|
|
13
|
-
from itertools import cycle
|
|
14
13
|
from multiprocessing import Pool
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
from typing import List, Tuple
|
|
14
|
+
import pymupdf # PyMuPDF
|
|
15
|
+
from typing import List, Tuple, Union
|
|
18
16
|
from PIL import Image
|
|
19
|
-
from
|
|
17
|
+
from functools import lru_cache
|
|
18
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
19
|
+
from .optimized_complete_structure import complete_structure_mask
|
|
20
20
|
from .mrcnn import model as modellib
|
|
21
21
|
from .mrcnn import visualize
|
|
22
22
|
from .mrcnn import moldetect
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
warnings.filterwarnings("ignore")
|
|
26
|
-
|
|
27
24
|
# Root directory of the project
|
|
28
25
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.getcwd()))
|
|
29
26
|
|
|
27
|
+
# Global model instance (lazy loading)
|
|
28
|
+
_model = None
|
|
29
|
+
|
|
30
30
|
|
|
31
31
|
class InferenceConfig(moldetect.MolDetectConfig):
|
|
32
32
|
"""
|
|
@@ -40,9 +40,7 @@ class InferenceConfig(moldetect.MolDetectConfig):
|
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
def segment_chemical_structures_from_file(
|
|
43
|
-
file_path: str,
|
|
44
|
-
expand: bool = True,
|
|
45
|
-
poppler_path=None,
|
|
43
|
+
file_path: str, expand: bool = True
|
|
46
44
|
) -> List[np.array]:
|
|
47
45
|
"""
|
|
48
46
|
This function runs the segmentation model as well as the mask expansion
|
|
@@ -52,28 +50,58 @@ def segment_chemical_structures_from_file(
|
|
|
52
50
|
Args:
|
|
53
51
|
file_path (str): image of a page from a scientific publication
|
|
54
52
|
expand (bool): indicates whether or not to use mask expansion
|
|
55
|
-
poppler_path:
|
|
56
|
-
Poppler installation
|
|
53
|
+
poppler_path: Deprecated parameter - no longer needed with PyMuPDF
|
|
57
54
|
|
|
58
55
|
Returns:
|
|
59
56
|
List[np.array]: expanded segments (shape: (h, w, num_masks))
|
|
60
57
|
"""
|
|
61
58
|
if file_path[-3:].lower() == "pdf":
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
images = [
|
|
59
|
+
# Convert PDF to images using PyMuPDF with optimized settings
|
|
60
|
+
pdf_document = pymupdf.open(file_path)
|
|
61
|
+
images = []
|
|
62
|
+
|
|
63
|
+
# Pre-allocate list for known size
|
|
64
|
+
images = [None] * pdf_document.page_count
|
|
65
|
+
|
|
66
|
+
# Use thread pool for parallel page rendering
|
|
67
|
+
def render_page(page_num):
|
|
68
|
+
page = pdf_document[page_num]
|
|
69
|
+
# Render page to image with 300 DPI
|
|
70
|
+
matrix = pymupdf.Matrix(300 / 72, 300 / 72)
|
|
71
|
+
pix = page.get_pixmap(matrix=matrix, alpha=False) # Skip alpha channel
|
|
72
|
+
# Direct conversion to numpy array
|
|
73
|
+
img_array = np.frombuffer(pix.samples, dtype=np.uint8).reshape(
|
|
74
|
+
pix.h, pix.w, pix.n
|
|
75
|
+
)
|
|
76
|
+
return page_num, img_array
|
|
77
|
+
|
|
78
|
+
# Use thread pool for I/O bound operations
|
|
79
|
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
|
80
|
+
futures = [
|
|
81
|
+
executor.submit(render_page, i) for i in range(pdf_document.page_count)
|
|
82
|
+
]
|
|
83
|
+
for future in futures:
|
|
84
|
+
page_num, img_array = future.result()
|
|
85
|
+
images[page_num] = img_array
|
|
86
|
+
|
|
87
|
+
pdf_document.close()
|
|
88
|
+
|
|
89
|
+
# Filter out any None values
|
|
90
|
+
images = [img for img in images if img is not None]
|
|
68
91
|
else:
|
|
69
|
-
|
|
92
|
+
# Use faster image reading with proper flags
|
|
93
|
+
images = [cv2.imread(file_path, cv2.IMREAD_COLOR)]
|
|
94
|
+
|
|
70
95
|
if len(images) > 1:
|
|
71
|
-
|
|
96
|
+
# Use optimized multiprocessing
|
|
97
|
+
with Pool(min(4, len(images))) as pool:
|
|
72
98
|
starmap_args = [(im, expand) for im in images]
|
|
73
99
|
segments = pool.starmap(segment_chemical_structures, starmap_args)
|
|
74
|
-
|
|
100
|
+
# More efficient flattening
|
|
101
|
+
segments = [seg for sublist in segments for seg in sublist]
|
|
75
102
|
else:
|
|
76
|
-
segments = segment_chemical_structures(images[0])
|
|
103
|
+
segments = segment_chemical_structures(images[0], expand)
|
|
104
|
+
|
|
77
105
|
return segments
|
|
78
106
|
|
|
79
107
|
|
|
@@ -81,7 +109,8 @@ def segment_chemical_structures(
|
|
|
81
109
|
image: np.array,
|
|
82
110
|
expand: bool = True,
|
|
83
111
|
visualization: bool = False,
|
|
84
|
-
|
|
112
|
+
return_bboxes: bool = False,
|
|
113
|
+
) -> Union[List[np.array], Tuple[List[np.array], List[Tuple[int, int, int, int]]]]:
|
|
85
114
|
"""
|
|
86
115
|
This function runs the segmentation model as well as the mask expansion
|
|
87
116
|
-> returns a List of segmented chemical structure depictions (np.array)
|
|
@@ -91,10 +120,13 @@ def segment_chemical_structures(
|
|
|
91
120
|
expand (bool): indicates whether or not to use mask expansion
|
|
92
121
|
visualization (bool): indicates whether or not to visualize the
|
|
93
122
|
results (only works in Jupyter notebook)
|
|
123
|
+
return_bboxes (bool): indicates whether to return bounding boxes along with segments
|
|
94
124
|
|
|
95
125
|
Returns:
|
|
96
|
-
|
|
97
|
-
|
|
126
|
+
If return_bboxes is False:
|
|
127
|
+
List[np.array]: expanded segments sorted in top->bottom, left->right order
|
|
128
|
+
If return_bboxes is True:
|
|
129
|
+
Tuple[List[np.array], List[Tuple]]: segments and bounding boxes
|
|
98
130
|
"""
|
|
99
131
|
if not expand:
|
|
100
132
|
masks, bboxes, _ = get_mrcnn_results(image)
|
|
@@ -115,15 +147,19 @@ def segment_chemical_structures(
|
|
|
115
147
|
if len(segments) > 0:
|
|
116
148
|
segments, bboxes = sort_segments_bboxes(segments, bboxes)
|
|
117
149
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
150
|
+
# Vectorized filtering for valid segments
|
|
151
|
+
segments = [
|
|
152
|
+
segment for segment in segments if segment.shape[0] > 0 and segment.shape[1] > 0
|
|
153
|
+
]
|
|
121
154
|
|
|
122
|
-
|
|
155
|
+
if return_bboxes:
|
|
156
|
+
return segments, bboxes
|
|
157
|
+
else:
|
|
158
|
+
return segments
|
|
123
159
|
|
|
124
160
|
|
|
125
161
|
def determine_depiction_size_with_buffer(
|
|
126
|
-
bboxes: List[Tuple[int, int, int, int]]
|
|
162
|
+
bboxes: List[Tuple[int, int, int, int]],
|
|
127
163
|
) -> Tuple[int, int]:
|
|
128
164
|
"""
|
|
129
165
|
This function takes a list of bounding boxes and returns 1.1 * the maximal
|
|
@@ -136,13 +172,11 @@ def determine_depiction_size_with_buffer(
|
|
|
136
172
|
Returns:
|
|
137
173
|
Tuple [int, int]: average depiction size (height, width)
|
|
138
174
|
"""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
heights.append(height)
|
|
145
|
-
widths.append(width)
|
|
175
|
+
# Vectorized computation for better performance
|
|
176
|
+
bboxes_array = np.array(bboxes)
|
|
177
|
+
heights = bboxes_array[:, 2] - bboxes_array[:, 0]
|
|
178
|
+
widths = bboxes_array[:, 3] - bboxes_array[:, 1]
|
|
179
|
+
|
|
146
180
|
height = int(1.1 * np.max(heights))
|
|
147
181
|
width = int(1.1 * np.max(widths))
|
|
148
182
|
return height, width
|
|
@@ -152,7 +186,7 @@ def sort_segments_bboxes(
|
|
|
152
186
|
segments: List[np.array],
|
|
153
187
|
bboxes: List[Tuple[int, int, int, int]], # (y0, x0, y1, x1)
|
|
154
188
|
same_row_pixel_threshold=50,
|
|
155
|
-
) -> Tuple[np.array, List[Tuple[int, int, int, int]]]:
|
|
189
|
+
) -> Tuple[List[np.array], List[Tuple[int, int, int, int]]]:
|
|
156
190
|
"""
|
|
157
191
|
Sorts segments and bounding boxes in "reading order"
|
|
158
192
|
|
|
@@ -165,36 +199,44 @@ def sort_segments_bboxes(
|
|
|
165
199
|
Returns:
|
|
166
200
|
segments and bboxes in reading order
|
|
167
201
|
"""
|
|
202
|
+
# Create index array for efficient sorting
|
|
203
|
+
indices = list(range(len(bboxes)))
|
|
168
204
|
|
|
169
|
-
# Sort by y-coordinate
|
|
170
|
-
|
|
205
|
+
# Sort indices by y-coordinate
|
|
206
|
+
indices.sort(key=lambda i: bboxes[i][0])
|
|
171
207
|
|
|
172
|
-
# Group bounding boxes by rows
|
|
208
|
+
# Group bounding boxes by rows
|
|
173
209
|
rows = []
|
|
174
|
-
current_row = [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
current_row.append(bbox)
|
|
210
|
+
current_row = [indices[0]]
|
|
211
|
+
|
|
212
|
+
for i in indices[1:]:
|
|
213
|
+
if abs(bboxes[i][0] - bboxes[current_row[-1]][0]) < same_row_pixel_threshold:
|
|
214
|
+
current_row.append(i)
|
|
180
215
|
else:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
184
|
-
current_row = [
|
|
185
|
-
|
|
216
|
+
# Sort current row by x-coordinate
|
|
217
|
+
current_row.sort(key=lambda idx: bboxes[idx][1])
|
|
218
|
+
rows.append(current_row)
|
|
219
|
+
current_row = [i]
|
|
220
|
+
|
|
221
|
+
# Don't forget the last row
|
|
222
|
+
current_row.sort(key=lambda idx: bboxes[idx][1])
|
|
223
|
+
rows.append(current_row)
|
|
224
|
+
|
|
225
|
+
# Flatten the sorted indices
|
|
226
|
+
sorted_indices = [idx for row in rows for idx in row]
|
|
186
227
|
|
|
187
|
-
#
|
|
188
|
-
|
|
228
|
+
# Apply sorting to segments and bboxes
|
|
229
|
+
sorted_segments = [segments[i] for i in sorted_indices]
|
|
230
|
+
sorted_bboxes = [bboxes[i] for i in sorted_indices]
|
|
189
231
|
|
|
190
|
-
sorted_segments = [segments[bboxes.index(bbox)] for bbox in sorted_bboxes]
|
|
191
232
|
return sorted_segments, sorted_bboxes
|
|
192
233
|
|
|
193
234
|
|
|
235
|
+
@lru_cache(maxsize=1)
|
|
194
236
|
def load_model() -> modellib.MaskRCNN:
|
|
195
237
|
"""
|
|
196
238
|
This function loads the segmentation model and returns it. The weights
|
|
197
|
-
are downloaded if necessary.
|
|
239
|
+
are downloaded if necessary. Cached to avoid reloading.
|
|
198
240
|
|
|
199
241
|
Returns:
|
|
200
242
|
modellib.MaskRCNN: MRCNN model with trained weights
|
|
@@ -202,14 +244,21 @@ def load_model() -> modellib.MaskRCNN:
|
|
|
202
244
|
# Define directory with trained model weights
|
|
203
245
|
root_dir = os.path.split(__file__)[0]
|
|
204
246
|
model_path = os.path.join(root_dir, "mask_rcnn_molecule.h5")
|
|
247
|
+
|
|
205
248
|
# Download trained weights if needed
|
|
206
249
|
if not os.path.exists(model_path):
|
|
207
250
|
print("Downloading model weights...")
|
|
208
|
-
url =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
251
|
+
url = (
|
|
252
|
+
"https://zenodo.org/record/10663579/files/mask_rcnn_molecule.h5?download=1"
|
|
253
|
+
)
|
|
254
|
+
# Use streaming download for large files
|
|
255
|
+
with requests.get(url, stream=True) as req:
|
|
256
|
+
req.raise_for_status()
|
|
257
|
+
with open(model_path, "wb") as model_file:
|
|
258
|
+
for chunk in req.iter_content(chunk_size=8192):
|
|
259
|
+
model_file.write(chunk)
|
|
212
260
|
print("Successfully downloaded the segmentation model weights!")
|
|
261
|
+
|
|
213
262
|
# Create model object in inference mode.
|
|
214
263
|
model = modellib.MaskRCNN(mode="inference", model_dir=".", config=InferenceConfig())
|
|
215
264
|
# Load weights
|
|
@@ -234,13 +283,11 @@ def get_expanded_masks(image: np.array) -> np.array:
|
|
|
234
283
|
masks, bboxes, _ = get_mrcnn_results(image)
|
|
235
284
|
if len(bboxes) == 0:
|
|
236
285
|
return masks
|
|
286
|
+
|
|
237
287
|
size = determine_depiction_size_with_buffer(bboxes)
|
|
238
288
|
# Mask expansion
|
|
239
289
|
expanded_masks = complete_structure_mask(
|
|
240
|
-
image_array=image,
|
|
241
|
-
mask_array=masks,
|
|
242
|
-
max_depiction_size=size,
|
|
243
|
-
debug=False
|
|
290
|
+
image_array=image, mask_array=masks, max_depiction_size=size, debug=False
|
|
244
291
|
)
|
|
245
292
|
return expanded_masks
|
|
246
293
|
|
|
@@ -263,6 +310,9 @@ def get_mrcnn_results(
|
|
|
263
310
|
List[Tuple[int]]: bounding boxes [(y0, x0, y1, x1), ...]
|
|
264
311
|
List[float]: confidence scores
|
|
265
312
|
"""
|
|
313
|
+
# Ensure model is loaded
|
|
314
|
+
model = get_model()
|
|
315
|
+
|
|
266
316
|
results = model.detect([image], verbose=1)
|
|
267
317
|
scores = results[0]["scores"]
|
|
268
318
|
bboxes = results[0]["rois"]
|
|
@@ -270,11 +320,13 @@ def get_mrcnn_results(
|
|
|
270
320
|
return masks, bboxes, scores
|
|
271
321
|
|
|
272
322
|
|
|
273
|
-
def apply_masks(
|
|
323
|
+
def apply_masks(
|
|
324
|
+
image: np.array, masks: np.array
|
|
325
|
+
) -> Tuple[List[np.array], List[Tuple[int, int, int, int]]]:
|
|
274
326
|
"""
|
|
275
327
|
This function takes an image and the masks for this image
|
|
276
328
|
(shape: (h, w, num_structures)) and returns a list of segmented
|
|
277
|
-
chemical structure depictions (np.array)
|
|
329
|
+
chemical structure depictions (np.array) and their bounding boxes
|
|
278
330
|
|
|
279
331
|
Args:
|
|
280
332
|
image (np.array): image of a page from a scientific publication
|
|
@@ -282,19 +334,36 @@ def apply_masks(image: np.array, masks: np.array) -> List[np.array]:
|
|
|
282
334
|
|
|
283
335
|
Returns:
|
|
284
336
|
List[np.array]: segmented chemical structure depictions
|
|
337
|
+
List[Tuple[int, int, int, int]]: bounding boxes for each segment (y0, x0, y1, x1)
|
|
285
338
|
"""
|
|
286
|
-
|
|
287
|
-
if len(masks) == 0:
|
|
339
|
+
if masks.shape[2] == 0:
|
|
288
340
|
return [], []
|
|
289
|
-
|
|
290
|
-
|
|
341
|
+
|
|
342
|
+
# Pre-allocate lists for better performance
|
|
343
|
+
num_masks = masks.shape[2]
|
|
344
|
+
segmented_images = [None] * num_masks
|
|
345
|
+
bboxes = [None] * num_masks
|
|
346
|
+
|
|
347
|
+
# Process masks in parallel for better performance
|
|
348
|
+
def process_mask(i):
|
|
349
|
+
mask = masks[:, :, i]
|
|
350
|
+
return apply_mask(image, mask)
|
|
351
|
+
|
|
352
|
+
# Use thread pool for I/O bound operations
|
|
353
|
+
with ThreadPoolExecutor(max_workers=min(4, num_masks)) as executor:
|
|
354
|
+
futures = [executor.submit(process_mask, i) for i in range(num_masks)]
|
|
355
|
+
for i, future in enumerate(futures):
|
|
356
|
+
segmented_images[i], bboxes[i] = future.result()
|
|
357
|
+
|
|
291
358
|
return segmented_images, bboxes
|
|
292
359
|
|
|
293
360
|
|
|
294
|
-
def apply_mask(
|
|
361
|
+
def apply_mask(
|
|
362
|
+
image: np.array, mask: np.array
|
|
363
|
+
) -> Tuple[np.array, Tuple[int, int, int, int]]:
|
|
295
364
|
"""
|
|
296
365
|
This function takes an image and a mask for this image (shape: (h, w))
|
|
297
|
-
and returns a segmented chemical structure
|
|
366
|
+
and returns a segmented chemical structure depiction (np.array)
|
|
298
367
|
|
|
299
368
|
Args:
|
|
300
369
|
image (np.array): image of a page from a scientific publication
|
|
@@ -304,52 +373,51 @@ def apply_mask(image: np.array, mask: np.array) -> np.array:
|
|
|
304
373
|
np.array: segmented chemical structure depiction
|
|
305
374
|
Tuple[int]: (y0, x0, y1, x1)
|
|
306
375
|
"""
|
|
307
|
-
#
|
|
308
|
-
|
|
309
|
-
for channel in range(image.shape[2]):
|
|
310
|
-
im[:, :, channel] = im[:, :, channel] * mask
|
|
311
|
-
masked_image, bbox = get_masked_image(deepcopy(image), mask)
|
|
376
|
+
# Get masked image and bbox more efficiently
|
|
377
|
+
masked_image, bbox = get_masked_image_optimized(image, mask)
|
|
312
378
|
x, y, w, h = bbox
|
|
379
|
+
|
|
380
|
+
# Convert to grayscale more efficiently
|
|
313
381
|
im_gray = cv2.cvtColor(masked_image, cv2.COLOR_RGB2GRAY)
|
|
314
382
|
_, im_bw = cv2.threshold(im_gray, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)
|
|
315
|
-
|
|
383
|
+
|
|
384
|
+
# Create alpha channel
|
|
316
385
|
_, alpha = cv2.threshold(im_bw, 0, 255, cv2.THRESH_BINARY)
|
|
317
|
-
b, g, r = cv2.split(image)
|
|
318
|
-
rgba = [b, g, r, alpha]
|
|
319
|
-
dst = cv2.merge(rgba, 4)
|
|
320
|
-
background = dst[y : y + h, x : x + w]
|
|
321
|
-
trans_mask = background[:, :, 3] == 0
|
|
322
|
-
background[trans_mask] = [255, 255, 255, 255]
|
|
323
|
-
return background, (y, x, y + h, x + w)
|
|
324
386
|
|
|
387
|
+
# Extract region of interest first to reduce processing
|
|
388
|
+
roi = image[y : y + h, x : x + w]
|
|
325
389
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
(shape: (h, w)) and returns the masked image where only the
|
|
330
|
-
masked area is not completely white and a bounding box of the
|
|
331
|
-
segmented object
|
|
390
|
+
# Split channels and merge with alpha
|
|
391
|
+
b, g, r = cv2.split(roi)
|
|
392
|
+
rgba = cv2.merge([b, g, r, alpha[y : y + h, x : x + w]])
|
|
332
393
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
394
|
+
# Set transparent pixels to white
|
|
395
|
+
trans_mask = rgba[:, :, 3] == 0
|
|
396
|
+
rgba[trans_mask] = [255, 255, 255, 255]
|
|
336
397
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
398
|
+
return rgba, (y, x, y + h, x + w)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def get_masked_image_optimized(
|
|
402
|
+
image: np.array, mask: np.array
|
|
403
|
+
) -> Tuple[np.array, Tuple[int, int, int, int]]:
|
|
404
|
+
"""
|
|
405
|
+
Optimized version of get_masked_image using vectorized operations
|
|
340
406
|
"""
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
bbox =
|
|
407
|
+
# Find bounding box more efficiently
|
|
408
|
+
rows = np.any(mask, axis=1)
|
|
409
|
+
cols = np.any(mask, axis=0)
|
|
410
|
+
rmin, rmax = np.where(rows)[0][[0, -1]]
|
|
411
|
+
cmin, cmax = np.where(cols)[0][[0, -1]]
|
|
412
|
+
bbox = (cmin, rmin, cmax - cmin + 1, rmax - rmin + 1)
|
|
413
|
+
|
|
414
|
+
# Create output image
|
|
415
|
+
masked_image = np.zeros_like(image, dtype=np.uint8)
|
|
416
|
+
# Apply mask using vectorized operation
|
|
417
|
+
for c in range(3):
|
|
418
|
+
masked_image[:, :, c] = mask * 255
|
|
347
419
|
|
|
348
|
-
masked_image
|
|
349
|
-
masked_image = visualize.apply_mask(masked_image, mask, [1, 1, 1])
|
|
350
|
-
masked_image = Image.fromarray(masked_image)
|
|
351
|
-
masked_image = masked_image.convert("RGB")
|
|
352
|
-
return np.array(masked_image), bbox
|
|
420
|
+
return masked_image, bbox
|
|
353
421
|
|
|
354
422
|
|
|
355
423
|
def save_images(images: List[np.array], path: str, name: str) -> None:
|
|
@@ -363,17 +431,22 @@ def save_images(images: List[np.array], path: str, name: str) -> None:
|
|
|
363
431
|
path (str): Output directory
|
|
364
432
|
name (str): name for filename generation
|
|
365
433
|
"""
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
for
|
|
434
|
+
os.makedirs(path, exist_ok=True)
|
|
435
|
+
|
|
436
|
+
# Use thread pool for parallel I/O operations
|
|
437
|
+
def save_single_image(args):
|
|
438
|
+
index, image = args
|
|
369
439
|
filename = f"{name}_{index}.png"
|
|
370
440
|
file_path = os.path.join(path, filename)
|
|
371
|
-
cv2.imwrite(file_path,
|
|
441
|
+
cv2.imwrite(file_path, image)
|
|
442
|
+
|
|
443
|
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
|
444
|
+
executor.map(save_single_image, enumerate(images))
|
|
372
445
|
|
|
373
446
|
|
|
374
447
|
def get_bnw_image(image: np.array) -> np.array:
|
|
375
448
|
"""
|
|
376
|
-
This function takes an image and returns
|
|
449
|
+
This function takes an image and returns a binarized version
|
|
377
450
|
|
|
378
451
|
Args:
|
|
379
452
|
image (np.array): input image
|
|
@@ -381,7 +454,12 @@ def get_bnw_image(image: np.array) -> np.array:
|
|
|
381
454
|
Returns:
|
|
382
455
|
np.array: binarized input image
|
|
383
456
|
"""
|
|
384
|
-
|
|
457
|
+
# Check if already grayscale
|
|
458
|
+
if len(image.shape) == 2:
|
|
459
|
+
grayscale_im = image
|
|
460
|
+
else:
|
|
461
|
+
grayscale_im = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
462
|
+
|
|
385
463
|
_, im_bw = cv2.threshold(
|
|
386
464
|
grayscale_im, 128, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU
|
|
387
465
|
)
|
|
@@ -401,25 +479,47 @@ def get_square_image(image: np.array, desired_size: int) -> np.array:
|
|
|
401
479
|
Returns:
|
|
402
480
|
np.array: resized output image
|
|
403
481
|
"""
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if old_size[0] or old_size[1] != desired_size:
|
|
408
|
-
ratio = float(desired_size) / max(old_size)
|
|
409
|
-
new_size = tuple([int(x * ratio) for x in old_size])
|
|
410
|
-
grayscale_image = grayscale_image.resize(new_size, Image.ANTIALIAS)
|
|
482
|
+
# Convert to grayscale if needed
|
|
483
|
+
if len(image.shape) == 3:
|
|
484
|
+
grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
411
485
|
else:
|
|
412
|
-
|
|
413
|
-
resized_image = Image.new("L", (desired_size, desired_size), "white")
|
|
486
|
+
grayscale = image
|
|
414
487
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
488
|
+
old_height, old_width = grayscale.shape
|
|
489
|
+
|
|
490
|
+
# Calculate new size
|
|
491
|
+
if old_height != desired_size or old_width != desired_size:
|
|
492
|
+
ratio = float(desired_size) / max(old_height, old_width)
|
|
493
|
+
new_width = int(old_width * ratio)
|
|
494
|
+
new_height = int(old_height * ratio)
|
|
495
|
+
|
|
496
|
+
# Resize using OpenCV (faster than PIL)
|
|
497
|
+
resized = cv2.resize(
|
|
498
|
+
grayscale, (new_width, new_height), interpolation=cv2.INTER_LANCZOS4
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
new_width, new_height = old_width, old_height
|
|
502
|
+
resized = grayscale
|
|
503
|
+
|
|
504
|
+
# Create output image
|
|
505
|
+
output = np.full((desired_size, desired_size), 255, dtype=np.uint8)
|
|
506
|
+
|
|
507
|
+
# Calculate padding
|
|
508
|
+
y_offset = (desired_size - new_height) // 2
|
|
509
|
+
x_offset = (desired_size - new_width) // 2
|
|
510
|
+
|
|
511
|
+
# Place resized image in center
|
|
512
|
+
output[y_offset : y_offset + new_height, x_offset : x_offset + new_width] = resized
|
|
513
|
+
|
|
514
|
+
return output
|
|
420
515
|
|
|
421
516
|
|
|
422
|
-
|
|
517
|
+
def get_model():
|
|
518
|
+
"""Get or create the global model instance"""
|
|
519
|
+
global _model
|
|
520
|
+
if _model is None:
|
|
521
|
+
_model = load_model()
|
|
522
|
+
return _model
|
|
423
523
|
|
|
424
524
|
|
|
425
525
|
def main():
|
|
@@ -434,12 +534,18 @@ def main():
|
|
|
434
534
|
"--input", help="Enter the input filename (pdf or image)", required=True
|
|
435
535
|
)
|
|
436
536
|
args = parser.parse_args()
|
|
537
|
+
|
|
437
538
|
# Define image path and output path
|
|
438
539
|
input_path = os.path.normpath(args.input)
|
|
439
|
-
|
|
540
|
+
|
|
541
|
+
# Pre-load model before segmentation
|
|
440
542
|
print("Loading model...")
|
|
543
|
+
get_model() # Pre-load the model
|
|
544
|
+
|
|
545
|
+
# Segment chemical structure depictions
|
|
441
546
|
print("Segmenting structures...")
|
|
442
547
|
segments = segment_chemical_structures_from_file(input_path)
|
|
548
|
+
|
|
443
549
|
# Save segments
|
|
444
550
|
segment_dir = os.path.join(f"{input_path}_output", "segments")
|
|
445
551
|
save_images(segments, segment_dir, os.path.split(input_path)[1][:-4])
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/model.py
RENAMED
|
@@ -29,7 +29,7 @@ import tensorflow.keras.models as KM
|
|
|
29
29
|
from . import utils
|
|
30
30
|
from distutils.version import LooseVersion
|
|
31
31
|
|
|
32
|
-
os.environ["CUDA_VISIBLE_DEVICES"] = "
|
|
32
|
+
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
|
|
33
33
|
|
|
34
34
|
gpus = tf.config.experimental.list_physical_devices("GPU")
|
|
35
35
|
for gpu in gpus:
|
|
@@ -871,7 +871,11 @@ def refine_detections_graph(rois, probs, deltas, window, config):
|
|
|
871
871
|
return class_keep
|
|
872
872
|
|
|
873
873
|
# 2. Map over class IDs
|
|
874
|
-
nms_keep = tf.map_fn(
|
|
874
|
+
nms_keep = tf.map_fn(
|
|
875
|
+
nms_keep_map,
|
|
876
|
+
unique_pre_nms_class_ids,
|
|
877
|
+
fn_output_signature=tf.TensorSpec(shape=None, dtype=tf.int64),
|
|
878
|
+
)
|
|
875
879
|
# 3. Merge results into one list, and remove -1 padding
|
|
876
880
|
nms_keep = tf.reshape(nms_keep, [-1])
|
|
877
881
|
nms_keep = tf.gather(nms_keep, tf.compat.v1.where(nms_keep > -1)[:, 0])
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import cv2
|
|
2
|
+
import numpy as np
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import itertools
|
|
5
|
+
from skimage.color import rgb2gray
|
|
6
|
+
from skimage.filters import threshold_otsu
|
|
7
|
+
from skimage.morphology import binary_erosion, binary_dilation
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
from scipy.ndimage import label
|
|
10
|
+
from numba import jit, prange
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
# Suppress numba warnings for cleaner output
|
|
14
|
+
warnings.filterwarnings("ignore", category=np.VisibleDeprecationWarning)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plot_it(image_array: np.array) -> None:
|
|
18
|
+
"""
|
|
19
|
+
This function shows the plot of a given image (np.array)
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
image_array (np.array): Image
|
|
23
|
+
"""
|
|
24
|
+
plt.rcParams["figure.figsize"] = (20, 15)
|
|
25
|
+
_, ax = plt.subplots(1)
|
|
26
|
+
ax.imshow(image_array)
|
|
27
|
+
plt.show()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def binarize_image(image_array: np.array, threshold="otsu") -> np.array:
|
|
31
|
+
"""
|
|
32
|
+
This function takes a np.array that represents an RGB image and returns
|
|
33
|
+
the binarized image (np.array) by applying the otsu threshold.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
image_array (np.array): image
|
|
37
|
+
threshold (str, optional): "otsu" or a float. Defaults to "otsu".
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
np.array: binarized image
|
|
41
|
+
"""
|
|
42
|
+
grayscale = rgb2gray(image_array)
|
|
43
|
+
if threshold == "otsu":
|
|
44
|
+
threshold = threshold_otsu(grayscale)
|
|
45
|
+
return grayscale > threshold
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@jit(nopython=True, cache=True)
|
|
49
|
+
def _get_seeds_fast(
|
|
50
|
+
mask_indices,
|
|
51
|
+
image_indices,
|
|
52
|
+
exclusion_indices,
|
|
53
|
+
x_min_limit,
|
|
54
|
+
x_max_limit,
|
|
55
|
+
y_min_limit,
|
|
56
|
+
y_max_limit,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
Fast numba-compiled function for seed pixel detection.
|
|
60
|
+
"""
|
|
61
|
+
# Convert to sets for fast intersection
|
|
62
|
+
mask_set = set()
|
|
63
|
+
for i in range(len(mask_indices[0])):
|
|
64
|
+
mask_set.add((mask_indices[0][i], mask_indices[1][i]))
|
|
65
|
+
|
|
66
|
+
image_set = set()
|
|
67
|
+
for i in range(len(image_indices[0])):
|
|
68
|
+
image_set.add((image_indices[0][i], image_indices[1][i]))
|
|
69
|
+
|
|
70
|
+
exclusion_set = set()
|
|
71
|
+
for i in range(len(exclusion_indices[0])):
|
|
72
|
+
exclusion_set.add((exclusion_indices[0][i], exclusion_indices[1][i]))
|
|
73
|
+
|
|
74
|
+
# Find intersection and filter
|
|
75
|
+
seed_pixels = []
|
|
76
|
+
for coord in mask_set:
|
|
77
|
+
if coord in image_set:
|
|
78
|
+
y_coord, x_coord = coord
|
|
79
|
+
if (
|
|
80
|
+
x_coord >= x_min_limit
|
|
81
|
+
and x_coord <= x_max_limit
|
|
82
|
+
and y_coord >= y_min_limit
|
|
83
|
+
and y_coord <= y_max_limit
|
|
84
|
+
and coord not in exclusion_set
|
|
85
|
+
):
|
|
86
|
+
seed_pixels.append((x_coord, y_coord))
|
|
87
|
+
|
|
88
|
+
return seed_pixels
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_seeds(
|
|
92
|
+
image_array: np.array,
|
|
93
|
+
mask_array: np.array,
|
|
94
|
+
exclusion_mask: np.array,
|
|
95
|
+
) -> List[Tuple[int, int]]:
|
|
96
|
+
"""
|
|
97
|
+
Optimized version of get_seeds function.
|
|
98
|
+
"""
|
|
99
|
+
mask_indices = np.where(mask_array)
|
|
100
|
+
if len(mask_indices[0]) == 0:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
# Calculate boundaries once
|
|
104
|
+
mask_y_diff = mask_indices[0].max() - mask_indices[0].min()
|
|
105
|
+
mask_x_diff = mask_indices[1].max() - mask_indices[1].min()
|
|
106
|
+
x_min_limit = mask_indices[1].min() + mask_x_diff / 10
|
|
107
|
+
x_max_limit = mask_indices[1].max() - mask_x_diff / 10
|
|
108
|
+
y_min_limit = mask_indices[0].min() + mask_y_diff / 10
|
|
109
|
+
y_max_limit = mask_indices[0].max() - mask_y_diff / 10
|
|
110
|
+
|
|
111
|
+
image_indices = np.where(~image_array)
|
|
112
|
+
exclusion_indices = np.where(exclusion_mask)
|
|
113
|
+
|
|
114
|
+
return _get_seeds_fast(
|
|
115
|
+
mask_indices,
|
|
116
|
+
image_indices,
|
|
117
|
+
exclusion_indices,
|
|
118
|
+
x_min_limit,
|
|
119
|
+
x_max_limit,
|
|
120
|
+
y_min_limit,
|
|
121
|
+
y_max_limit,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def detect_horizontal_and_vertical_lines(
|
|
126
|
+
image: np.ndarray, max_depiction_size: Tuple[int, int]
|
|
127
|
+
) -> np.ndarray:
|
|
128
|
+
"""
|
|
129
|
+
Optimized version with pre-allocated arrays and combined operations.
|
|
130
|
+
"""
|
|
131
|
+
# Convert to uint8 once
|
|
132
|
+
binarised_im = (~image).astype(np.uint8) * 255
|
|
133
|
+
structure_height, structure_width = max_depiction_size
|
|
134
|
+
|
|
135
|
+
# Use optimized kernel creation and operations
|
|
136
|
+
horizontal_kernel = np.ones((1, structure_width), dtype=np.uint8)
|
|
137
|
+
vertical_kernel = np.ones((structure_height, 1), dtype=np.uint8)
|
|
138
|
+
|
|
139
|
+
# Perform morphological operations
|
|
140
|
+
horizontal_mask = (
|
|
141
|
+
cv2.morphologyEx(binarised_im, cv2.MORPH_OPEN, horizontal_kernel, iterations=2)
|
|
142
|
+
== 255
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
vertical_mask = (
|
|
146
|
+
cv2.morphologyEx(binarised_im, cv2.MORPH_OPEN, vertical_kernel, iterations=2)
|
|
147
|
+
== 255
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return horizontal_mask | vertical_mask # Use bitwise OR instead of addition
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@jit(nopython=True, cache=True)
|
|
154
|
+
def _find_equidistant_points_fast(x1, y1, x2, y2, num_points):
|
|
155
|
+
"""
|
|
156
|
+
Vectorized version of equidistant points calculation.
|
|
157
|
+
"""
|
|
158
|
+
points = np.empty((num_points + 1, 2), dtype=np.float64)
|
|
159
|
+
for i in range(num_points + 1):
|
|
160
|
+
t = i / num_points
|
|
161
|
+
points[i, 0] = x1 * (1 - t) + x2 * t
|
|
162
|
+
points[i, 1] = y1 * (1 - t) + y2 * t
|
|
163
|
+
return points
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def find_equidistant_points(
|
|
167
|
+
x1: int, y1: int, x2: int, y2: int, num_points: int = 5
|
|
168
|
+
) -> np.ndarray:
|
|
169
|
+
"""
|
|
170
|
+
Optimized version using numba compilation.
|
|
171
|
+
"""
|
|
172
|
+
return _find_equidistant_points_fast(x1, y1, x2, y2, num_points)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def detect_lines(
|
|
176
|
+
image: np.ndarray,
|
|
177
|
+
max_depiction_size: Tuple[int, int],
|
|
178
|
+
segmentation_mask: np.ndarray,
|
|
179
|
+
) -> np.ndarray:
|
|
180
|
+
"""
|
|
181
|
+
Optimized line detection with vectorized operations.
|
|
182
|
+
"""
|
|
183
|
+
# Convert to uint8 once
|
|
184
|
+
image_uint8 = (~image).astype(np.uint8) * 255
|
|
185
|
+
|
|
186
|
+
# Detect lines using the Hough Transform
|
|
187
|
+
lines = cv2.HoughLinesP(
|
|
188
|
+
image_uint8,
|
|
189
|
+
1,
|
|
190
|
+
np.pi / 180,
|
|
191
|
+
threshold=5,
|
|
192
|
+
minLineLength=int(max(max_depiction_size) / 4),
|
|
193
|
+
maxLineGap=10,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if lines is None:
|
|
197
|
+
return np.zeros_like(image_uint8, dtype=np.uint8)
|
|
198
|
+
|
|
199
|
+
# Pre-allocate exclusion mask
|
|
200
|
+
exclusion_mask = np.zeros_like(image_uint8, dtype=np.uint8)
|
|
201
|
+
|
|
202
|
+
# Vectorized line processing
|
|
203
|
+
for line in lines:
|
|
204
|
+
x1, y1, x2, y2 = line[0]
|
|
205
|
+
points = find_equidistant_points(x1, y1, x2, y2, num_points=7)
|
|
206
|
+
|
|
207
|
+
# Check if points are in structure (vectorized)
|
|
208
|
+
valid_points = points[1:-1] # Exclude endpoints
|
|
209
|
+
coords = np.clip(
|
|
210
|
+
valid_points.astype(int),
|
|
211
|
+
0,
|
|
212
|
+
[segmentation_mask.shape[1] - 1, segmentation_mask.shape[0] - 1],
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if not segmentation_mask[coords[:, 1], coords[:, 0]].any():
|
|
216
|
+
cv2.line(exclusion_mask, (x1, y1), (x2, y2), 255, 2)
|
|
217
|
+
|
|
218
|
+
return exclusion_mask
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def expand_masks(
|
|
222
|
+
image_array: np.array,
|
|
223
|
+
seed_pixels: List[Tuple[int, int]],
|
|
224
|
+
mask_array: np.array,
|
|
225
|
+
) -> np.array:
|
|
226
|
+
"""
|
|
227
|
+
Optimized mask expansion with reduced memory allocations.
|
|
228
|
+
"""
|
|
229
|
+
if not seed_pixels:
|
|
230
|
+
return np.zeros_like(image_array, dtype=bool)
|
|
231
|
+
|
|
232
|
+
inverted_image = ~image_array
|
|
233
|
+
labeled_array, _ = label(inverted_image)
|
|
234
|
+
result_mask = np.zeros_like(image_array, dtype=bool)
|
|
235
|
+
|
|
236
|
+
# Process all seed pixels in vectorized manner where possible
|
|
237
|
+
processed_labels = set()
|
|
238
|
+
for x, y in seed_pixels:
|
|
239
|
+
if result_mask[y, x]:
|
|
240
|
+
continue
|
|
241
|
+
label_value = labeled_array[y, x]
|
|
242
|
+
if label_value > 0 and label_value not in processed_labels:
|
|
243
|
+
result_mask[labeled_array == label_value] = True
|
|
244
|
+
processed_labels.add(label_value)
|
|
245
|
+
|
|
246
|
+
return result_mask
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def expansion_coordination(
|
|
250
|
+
mask_array: np.array, image_array: np.array, exclusion_mask: np.array
|
|
251
|
+
) -> np.array:
|
|
252
|
+
"""
|
|
253
|
+
Optimized coordination function.
|
|
254
|
+
"""
|
|
255
|
+
seed_pixels = get_seeds(image_array, mask_array, exclusion_mask)
|
|
256
|
+
return expand_masks(image_array, seed_pixels, mask_array)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def complete_structure_mask(
|
|
260
|
+
image_array: np.array,
|
|
261
|
+
mask_array: np.array,
|
|
262
|
+
max_depiction_size: Tuple[int, int],
|
|
263
|
+
debug=False,
|
|
264
|
+
) -> np.array:
|
|
265
|
+
"""
|
|
266
|
+
Heavily optimized version of complete_structure_mask.
|
|
267
|
+
"""
|
|
268
|
+
if mask_array.size == 0:
|
|
269
|
+
print("No masks found.")
|
|
270
|
+
return mask_array
|
|
271
|
+
|
|
272
|
+
# Optimize binarization
|
|
273
|
+
binarized_image_array = binarize_image(image_array, threshold=0.72)
|
|
274
|
+
if debug:
|
|
275
|
+
plot_it(binarized_image_array)
|
|
276
|
+
|
|
277
|
+
# Calculate blur factor and kernel once
|
|
278
|
+
blur_factor = max(2, int(image_array.shape[1] / 185))
|
|
279
|
+
kernel = np.ones((blur_factor, blur_factor), dtype=np.uint8)
|
|
280
|
+
|
|
281
|
+
# Apply erosion
|
|
282
|
+
blurred_image_array = binary_erosion(binarized_image_array, footprint=kernel)
|
|
283
|
+
if debug:
|
|
284
|
+
plot_it(blurred_image_array)
|
|
285
|
+
|
|
286
|
+
# Optimized mask splitting - use moveaxis instead of list comprehension
|
|
287
|
+
split_mask_arrays = np.moveaxis(mask_array, 2, 0)
|
|
288
|
+
|
|
289
|
+
# Detect lines with optimized functions
|
|
290
|
+
horizontal_vertical_lines = detect_horizontal_and_vertical_lines(
|
|
291
|
+
blurred_image_array, max_depiction_size
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# Create segmentation mask more efficiently
|
|
295
|
+
segmentation_mask = mask_array.any(axis=2)
|
|
296
|
+
|
|
297
|
+
hough_lines = detect_lines(
|
|
298
|
+
binarized_image_array,
|
|
299
|
+
max_depiction_size,
|
|
300
|
+
segmentation_mask=segmentation_mask,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Combine masks efficiently
|
|
304
|
+
hough_lines_dilated = binary_dilation(hough_lines, footprint=kernel)
|
|
305
|
+
exclusion_mask = horizontal_vertical_lines | hough_lines_dilated
|
|
306
|
+
|
|
307
|
+
# Optimize image processing
|
|
308
|
+
image_with_exclusion = ~((~blurred_image_array) & (~exclusion_mask))
|
|
309
|
+
|
|
310
|
+
if debug:
|
|
311
|
+
plot_it(horizontal_vertical_lines)
|
|
312
|
+
plot_it(hough_lines_dilated)
|
|
313
|
+
plot_it(exclusion_mask)
|
|
314
|
+
plot_it(image_with_exclusion)
|
|
315
|
+
|
|
316
|
+
# Optimized expansion using list comprehension instead of map
|
|
317
|
+
expanded_masks = [
|
|
318
|
+
expansion_coordination(mask, image_with_exclusion, exclusion_mask)
|
|
319
|
+
for mask in split_mask_arrays
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
# Optimized duplicate filtering
|
|
323
|
+
unique_masks = filter_duplicate_masks_fast(expanded_masks)
|
|
324
|
+
|
|
325
|
+
if not unique_masks:
|
|
326
|
+
return np.empty((image_array.shape[0], image_array.shape[1], 0), dtype=bool)
|
|
327
|
+
|
|
328
|
+
return np.stack(unique_masks, axis=-1)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def filter_duplicate_masks_fast(array_list: List[np.array]) -> List[np.array]:
|
|
332
|
+
"""
|
|
333
|
+
Highly optimized duplicate filtering using hash-based comparison.
|
|
334
|
+
"""
|
|
335
|
+
if not array_list:
|
|
336
|
+
return []
|
|
337
|
+
|
|
338
|
+
seen_hashes = set()
|
|
339
|
+
unique_list = []
|
|
340
|
+
|
|
341
|
+
for arr in array_list:
|
|
342
|
+
# Use hash of array for faster comparison
|
|
343
|
+
if arr.size > 0:
|
|
344
|
+
# Use a more efficient hash method
|
|
345
|
+
arr_hash = hash(arr.tobytes())
|
|
346
|
+
if arr_hash not in seen_hashes:
|
|
347
|
+
seen_hashes.add(arr_hash)
|
|
348
|
+
unique_list.append(arr)
|
|
349
|
+
elif not seen_hashes: # Handle empty arrays
|
|
350
|
+
seen_hashes.add(0) # Placeholder for empty array
|
|
351
|
+
unique_list.append(arr)
|
|
352
|
+
|
|
353
|
+
return unique_list
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# Alternative filter method for very large arrays
|
|
357
|
+
def filter_duplicate_masks_memory_efficient(
|
|
358
|
+
array_list: List[np.array],
|
|
359
|
+
) -> List[np.array]:
|
|
360
|
+
"""
|
|
361
|
+
Memory-efficient version for very large arrays.
|
|
362
|
+
"""
|
|
363
|
+
if not array_list:
|
|
364
|
+
return []
|
|
365
|
+
|
|
366
|
+
unique_list = []
|
|
367
|
+
|
|
368
|
+
for i, arr1 in enumerate(array_list):
|
|
369
|
+
is_duplicate = False
|
|
370
|
+
for j in range(i):
|
|
371
|
+
if np.array_equal(arr1, array_list[j]):
|
|
372
|
+
is_duplicate = True
|
|
373
|
+
break
|
|
374
|
+
if not is_duplicate:
|
|
375
|
+
unique_list.append(arr1)
|
|
376
|
+
|
|
377
|
+
return unique_list
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: decimer-segmentation
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: DECIMER Segmentation - Extraction of chemical structure depictions from scientific literature
|
|
5
5
|
Home-page: https://github.com/Kohulan/DECIMER-Image-Segmentation
|
|
6
6
|
Author: Kohulan Rajan
|
|
@@ -9,14 +9,12 @@ Maintainer: Kohulan Rajan
|
|
|
9
9
|
Maintainer-email: kohulan.rajan@uni-jena.de
|
|
10
10
|
License: MIT
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
14
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
18
16
|
Classifier: Operating System :: OS Independent
|
|
19
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.10
|
|
20
18
|
Description-Content-Type: text/markdown
|
|
21
19
|
License-File: LICENSE
|
|
22
20
|
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/SOURCES.txt
RENAMED
|
@@ -4,6 +4,7 @@ setup.py
|
|
|
4
4
|
decimer_segmentation/__init__.py
|
|
5
5
|
decimer_segmentation/complete_structure.py
|
|
6
6
|
decimer_segmentation/decimer_segmentation.py
|
|
7
|
+
decimer_segmentation/optimized_complete_structure.py
|
|
7
8
|
decimer_segmentation.egg-info/PKG-INFO
|
|
8
9
|
decimer_segmentation.egg-info/SOURCES.txt
|
|
9
10
|
decimer_segmentation.egg-info/dependency_links.txt
|
|
@@ -6,16 +6,16 @@ import platform
|
|
|
6
6
|
if (
|
|
7
7
|
platform.processor() == "arm" or platform.processor() == "i386"
|
|
8
8
|
) and platform.system() == "Darwin":
|
|
9
|
-
tensorflow_os = "tensorflow
|
|
9
|
+
tensorflow_os = "tensorflow>=2.14.0,<=2.15.1"
|
|
10
10
|
else:
|
|
11
|
-
tensorflow_os = "tensorflow
|
|
11
|
+
tensorflow_os = "tensorflow>=2.12.0,<=2.15.1"
|
|
12
12
|
|
|
13
13
|
with open("README.md", "r") as fh:
|
|
14
14
|
long_description = fh.read()
|
|
15
15
|
|
|
16
16
|
setuptools.setup(
|
|
17
17
|
name="decimer_segmentation",
|
|
18
|
-
version="1.
|
|
18
|
+
version="1.5.0",
|
|
19
19
|
author="Kohulan Rajan",
|
|
20
20
|
author_email="kohulan.rajan@uni-jena.de",
|
|
21
21
|
maintainer="Kohulan Rajan",
|
|
@@ -34,19 +34,19 @@ setuptools.setup(
|
|
|
34
34
|
"opencv-python",
|
|
35
35
|
"matplotlib",
|
|
36
36
|
"IPython",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"PyMuPDF",
|
|
38
|
+
"numba",
|
|
39
|
+
"scipy",
|
|
40
|
+
"requests",
|
|
39
41
|
],
|
|
40
42
|
package_data={"decimer_segmentation": ["mrcnn/*.*"]},
|
|
41
43
|
classifiers=[
|
|
42
44
|
"Programming Language :: Python :: 3",
|
|
43
|
-
"Programming Language :: Python :: 3.
|
|
44
|
-
"Programming Language :: Python :: 3.
|
|
45
|
-
"Programming Language :: Python :: 3.7",
|
|
46
|
-
"Programming Language :: Python :: 3.8",
|
|
45
|
+
"Programming Language :: Python :: 3.10",
|
|
46
|
+
"Programming Language :: Python :: 3.11",
|
|
47
47
|
"Programming Language :: Python :: 3 :: Only",
|
|
48
48
|
"License :: OSI Approved :: MIT License",
|
|
49
49
|
"Operating System :: OS Independent",
|
|
50
50
|
],
|
|
51
|
-
python_requires=">=3.
|
|
51
|
+
python_requires=">=3.10",
|
|
52
52
|
)
|
|
File without changes
|
|
File without changes
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/config.py
RENAMED
|
File without changes
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/moldetect.py
RENAMED
|
File without changes
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/utils.py
RENAMED
|
File without changes
|
{decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/visualize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|