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.
Files changed (19) hide show
  1. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/PKG-INFO +4 -6
  2. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/__init__.py +1 -1
  3. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/complete_structure.py +130 -25
  4. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/decimer_segmentation.py +243 -136
  5. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/model.py +6 -2
  6. decimer_segmentation-1.5.0/decimer_segmentation/optimized_complete_structure.py +377 -0
  7. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/PKG-INFO +4 -6
  8. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/SOURCES.txt +1 -0
  9. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/requires.txt +4 -2
  10. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/setup.py +10 -10
  11. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/LICENSE +0 -0
  12. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/README.md +0 -0
  13. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/config.py +0 -0
  14. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/moldetect.py +0 -0
  15. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/utils.py +0 -0
  16. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/visualize.py +0 -0
  17. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/dependency_links.txt +0 -0
  18. {decimer_segmentation-1.3.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/top_level.txt +0 -0
  19. {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.0
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.5
13
- Classifier: Programming Language :: Python :: 3.6
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.5
17
+ Requires-Python: >=3.10
20
18
  Description-Content-Type: text/markdown
21
19
  License-File: LICENSE
22
20
 
@@ -10,7 +10,7 @@ For comments, bug reports or feature requests,
10
10
  please raise an issue on our GitHub repository.
11
11
  """
12
12
 
13
- __version__ = "1.3.0"
13
+ __version__ = "1.5.0"
14
14
 
15
15
  __all__ = [
16
16
  "decimer_segmentation",
@@ -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. [Definition of long: 1/5 of the
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
- image_with_exclusion = np.invert(image_array) * np.invert(exclusion_mask)
156
- labeled_array, _ = label(image_with_exclusion)
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] or exclusion_mask[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
- mask_array,
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): More verbose if True. Defaults to False.
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
- exclusion_mask = detect_horizontal_and_vertical_lines(
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
- # Run expansion the expansion
231
- image_repeat = itertools.repeat(blurred_image_array, mask_array.shape[2])
232
- exclusion_mask_repeat = itertools.repeat(exclusion_mask, mask_array.shape[2])
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
- exclusion_mask_repeat,
321
+ exclusion_repeat,
239
322
  )
240
- # Stack mask arrays to give the desired output format
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