decimer-segmentation 1.3.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.3.0 → decimer_segmentation-1.5.0}/PKG-INFO +4 -6
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/__init__.py +1 -1
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/complete_structure.py +130 -25
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/decimer_segmentation.py +243 -136
- {decimer_segmentation-1.3.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.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/PKG-INFO +4 -6
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/SOURCES.txt +1 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/requires.txt +4 -2
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/setup.py +10 -10
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/LICENSE +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/README.md +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/config.py +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/moldetect.py +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/utils.py +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/visualize.py +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/dependency_links.txt +0 -0
- {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/top_level.txt +0 -0
- {decimer_segmentation-1.3.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.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/complete_structure.py
RENAMED
|
@@ -4,7 +4,7 @@ import matplotlib.pyplot as plt
|
|
|
4
4
|
import itertools
|
|
5
5
|
from skimage.color import rgb2gray
|
|
6
6
|
from skimage.filters import threshold_otsu
|
|
7
|
-
from skimage.morphology import binary_erosion
|
|
7
|
+
from skimage.morphology import binary_erosion, binary_dilation
|
|
8
8
|
from typing import List, Tuple
|
|
9
9
|
from scipy.ndimage import label
|
|
10
10
|
|
|
@@ -98,8 +98,7 @@ def detect_horizontal_and_vertical_lines(
|
|
|
98
98
|
) -> np.ndarray:
|
|
99
99
|
"""
|
|
100
100
|
This function takes an image and returns a binary mask that labels the pixels that
|
|
101
|
-
are part of long horizontal or vertical lines.
|
|
102
|
-
width/height of the image].
|
|
101
|
+
are part of long horizontal or vertical lines.
|
|
103
102
|
|
|
104
103
|
Args:
|
|
105
104
|
image (np.ndarray): binarised image (np.array; type bool) as it is returned by
|
|
@@ -130,11 +129,84 @@ def detect_horizontal_and_vertical_lines(
|
|
|
130
129
|
return horizontal_mask + vertical_mask
|
|
131
130
|
|
|
132
131
|
|
|
132
|
+
def find_equidistant_points(
|
|
133
|
+
x1: int, y1: int, x2: int, y2: int, num_points: int = 5
|
|
134
|
+
) -> List[Tuple[int, int]]:
|
|
135
|
+
"""
|
|
136
|
+
Finds equidistant points between two points.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
x1 (int): x coordinate of first point
|
|
140
|
+
y1 (int): y coordinate of first point
|
|
141
|
+
x2 (int): x coordinate of second point
|
|
142
|
+
y2 (int): y coordinate of second point
|
|
143
|
+
num_points (int, optional): Number of points to return. Defaults to 5.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List[Tuple[int, int]]: Equidistant points on the given line
|
|
147
|
+
"""
|
|
148
|
+
points = []
|
|
149
|
+
for i in range(num_points + 1):
|
|
150
|
+
t = i / num_points
|
|
151
|
+
x = x1 * (1 - t) + x2 * t
|
|
152
|
+
y = y1 * (1 - t) + y2 * t
|
|
153
|
+
points.append((x, y))
|
|
154
|
+
return points
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def detect_lines(
|
|
158
|
+
image: np.ndarray,
|
|
159
|
+
max_depiction_size: Tuple[int, int],
|
|
160
|
+
segmentation_mask: np.ndarray,
|
|
161
|
+
) -> np.ndarray:
|
|
162
|
+
"""
|
|
163
|
+
This function takes an image and returns a binary mask that labels the pixels that
|
|
164
|
+
are part of lines that are not part of chemical structures (like arrays, tables).
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
image (np.ndarray): binarised image (np.array; type bool) as it is returned by
|
|
168
|
+
binary_erosion() in complete_structure_mask()
|
|
169
|
+
max_depiction_size (Tuple[int, int]): height, width; used for thresholds
|
|
170
|
+
segmentation_mask (np.ndarray): Indicates whether or not a pixel is part of a
|
|
171
|
+
chemical structure depiction (shape: (height, width))
|
|
172
|
+
Returns:
|
|
173
|
+
np.ndarray: Exclusion mask that contains indices of pixels that are part of
|
|
174
|
+
horizontal or vertical lines
|
|
175
|
+
"""
|
|
176
|
+
image = ~image * 255
|
|
177
|
+
image = image.astype("uint8")
|
|
178
|
+
# Detect lines using the Hough Transform
|
|
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
|
+
)
|
|
187
|
+
# Generate exclusion mask based on detected lines
|
|
188
|
+
exclusion_mask = np.zeros_like(image)
|
|
189
|
+
if lines is None:
|
|
190
|
+
return exclusion_mask
|
|
191
|
+
for line in lines:
|
|
192
|
+
x1, y1, x2, y2 = line[0]
|
|
193
|
+
# Check if any of the lines is in a chemical structure depiction
|
|
194
|
+
points = find_equidistant_points(x1, y1, x2, y2, num_points=7)
|
|
195
|
+
points_in_structure = False
|
|
196
|
+
for x, y in points[1:-1]:
|
|
197
|
+
if segmentation_mask[int(y), int(x)]:
|
|
198
|
+
points_in_structure = True
|
|
199
|
+
break
|
|
200
|
+
if points_in_structure:
|
|
201
|
+
continue
|
|
202
|
+
cv2.line(exclusion_mask, (x1, y1), (x2, y2), 255, 2)
|
|
203
|
+
return exclusion_mask
|
|
204
|
+
|
|
205
|
+
|
|
133
206
|
def expand_masks(
|
|
134
207
|
image_array: np.array,
|
|
135
208
|
seed_pixels: List[Tuple[int, int]],
|
|
136
209
|
mask_array: np.array,
|
|
137
|
-
exclusion_mask: np.array,
|
|
138
210
|
) -> np.array:
|
|
139
211
|
"""
|
|
140
212
|
This function generates a mask array where the given masks have been
|
|
@@ -144,20 +216,16 @@ def expand_masks(
|
|
|
144
216
|
image_array (np.array): array that represents an image (float values)
|
|
145
217
|
seed_pixels (List[Tuple[int, int]]): [(x, y), ...]
|
|
146
218
|
mask_array (np.array): MRCNN output; shape: (y, x, mask_index)
|
|
147
|
-
exclusion_mask (np.array]: indicates whether or not a pixel is excluded from
|
|
148
|
-
expansion
|
|
149
|
-
contour_expansion (bool, optional): Indicates whether or not to expand
|
|
150
|
-
from contours. Defaults to False.
|
|
151
219
|
|
|
152
220
|
Returns:
|
|
153
221
|
np.array: Expanded masks
|
|
154
222
|
"""
|
|
155
|
-
|
|
156
|
-
labeled_array, _ = label(
|
|
223
|
+
image_array = np.invert(image_array)
|
|
224
|
+
labeled_array, _ = label(image_array)
|
|
157
225
|
mask_array = np.zeros_like(image_array)
|
|
158
226
|
for seed_pixel in seed_pixels:
|
|
159
227
|
x, y = seed_pixel
|
|
160
|
-
if mask_array[y, x]
|
|
228
|
+
if mask_array[y, x]:
|
|
161
229
|
continue
|
|
162
230
|
label_value = labeled_array[y, x]
|
|
163
231
|
if label_value > 0:
|
|
@@ -173,10 +241,8 @@ def expansion_coordination(
|
|
|
173
241
|
the mask expansion. It returns the expanded mask. The purpose of this function is
|
|
174
242
|
wrapping up the expansion procedure in a map function.
|
|
175
243
|
"""
|
|
176
|
-
seed_pixels = get_seeds(image_array,
|
|
177
|
-
|
|
178
|
-
exclusion_mask)
|
|
179
|
-
mask_array = expand_masks(image_array, seed_pixels, mask_array, exclusion_mask)
|
|
244
|
+
seed_pixels = get_seeds(image_array, mask_array, exclusion_mask)
|
|
245
|
+
mask_array = expand_masks(image_array, seed_pixels, mask_array)
|
|
180
246
|
return mask_array
|
|
181
247
|
|
|
182
248
|
|
|
@@ -200,7 +266,8 @@ def complete_structure_mask(
|
|
|
200
266
|
image_array (np.array): input image
|
|
201
267
|
mask_array (np.array): shape: y, x, n where n is the amount of masks
|
|
202
268
|
max_depiction_size (Tuple[int, int]): height, width
|
|
203
|
-
debug (bool, optional):
|
|
269
|
+
debug (bool, optional): You get visualisations in a Jupyter Notebook if True.
|
|
270
|
+
Defaults to False.
|
|
204
271
|
|
|
205
272
|
Returns:
|
|
206
273
|
np.array: expanded mask array
|
|
@@ -209,13 +276,12 @@ def complete_structure_mask(
|
|
|
209
276
|
if mask_array.size != 0:
|
|
210
277
|
# Binarization of input image
|
|
211
278
|
binarized_image_array = binarize_image(image_array, threshold=0.72)
|
|
279
|
+
if debug:
|
|
280
|
+
plot_it(binarized_image_array)
|
|
212
281
|
# Apply dilation with a resolution-dependent kernel to the image
|
|
213
282
|
blur_factor = (
|
|
214
283
|
int(image_array.shape[1] / 185) if image_array.shape[1] / 185 >= 2 else 2
|
|
215
284
|
)
|
|
216
|
-
if debug:
|
|
217
|
-
plot_it(binarized_image_array)
|
|
218
|
-
# Define kernel and apply
|
|
219
285
|
kernel = np.ones((blur_factor, blur_factor))
|
|
220
286
|
blurred_image_array = binary_erosion(binarized_image_array, footprint=kernel)
|
|
221
287
|
if debug:
|
|
@@ -224,22 +290,61 @@ def complete_structure_mask(
|
|
|
224
290
|
split_mask_arrays = np.array(
|
|
225
291
|
[mask_array[:, :, index] for index in range(mask_array.shape[2])]
|
|
226
292
|
)
|
|
227
|
-
|
|
293
|
+
# Detect horizontal and vertical lines
|
|
294
|
+
horizontal_vertical_lines = detect_horizontal_and_vertical_lines(
|
|
228
295
|
blurred_image_array, max_depiction_size
|
|
229
296
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
297
|
+
|
|
298
|
+
hough_lines = detect_lines(
|
|
299
|
+
binarized_image_array,
|
|
300
|
+
max_depiction_size,
|
|
301
|
+
segmentation_mask=np.any(mask_array, axis=2).astype(np.bool_),
|
|
302
|
+
)
|
|
303
|
+
hough_lines = binary_dilation(hough_lines, footprint=kernel)
|
|
304
|
+
exclusion_mask = horizontal_vertical_lines + hough_lines
|
|
305
|
+
image_with_exclusion = np.invert(
|
|
306
|
+
np.invert(blurred_image_array) * np.invert(exclusion_mask)
|
|
307
|
+
)
|
|
308
|
+
if debug:
|
|
309
|
+
plot_it(horizontal_vertical_lines)
|
|
310
|
+
plot_it(hough_lines)
|
|
311
|
+
plot_it(exclusion_mask)
|
|
312
|
+
plot_it(image_with_exclusion)
|
|
313
|
+
# Run expansion
|
|
314
|
+
image_repeat = itertools.repeat(image_with_exclusion, mask_array.shape[2])
|
|
315
|
+
exclusion_repeat = itertools.repeat(exclusion_mask, mask_array.shape[2])
|
|
233
316
|
# Faster with map function
|
|
234
317
|
expanded_split_mask_arrays = map(
|
|
235
318
|
expansion_coordination,
|
|
236
319
|
split_mask_arrays,
|
|
237
320
|
image_repeat,
|
|
238
|
-
|
|
321
|
+
exclusion_repeat,
|
|
239
322
|
)
|
|
240
|
-
#
|
|
323
|
+
# Filter duplicates and stack mask arrays to give the desired output format
|
|
324
|
+
expanded_split_mask_arrays = filter_duplicate_masks(expanded_split_mask_arrays)
|
|
241
325
|
mask_array = np.stack(expanded_split_mask_arrays, -1)
|
|
242
326
|
return mask_array
|
|
243
327
|
else:
|
|
244
328
|
print("No masks found.")
|
|
245
329
|
return mask_array
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def filter_duplicate_masks(array_list: List[np.array]) -> List[np.array]:
|
|
333
|
+
"""
|
|
334
|
+
This function takes a list of arrays and returns a list of unique arrays.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
array_list (List[np.array]): Masks
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
List[np.array]: Unique masks
|
|
341
|
+
"""
|
|
342
|
+
seen = set()
|
|
343
|
+
unique_list = []
|
|
344
|
+
for arr in array_list:
|
|
345
|
+
# Convert the array to a hashable tuple
|
|
346
|
+
arr_tuple = tuple(arr.ravel())
|
|
347
|
+
if arr_tuple not in seen:
|
|
348
|
+
seen.add(arr_tuple)
|
|
349
|
+
unique_list.append(arr)
|
|
350
|
+
return unique_list
|