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.
Files changed (19) hide show
  1. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/PKG-INFO +4 -6
  2. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/__init__.py +1 -1
  3. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/complete_structure.py +12 -16
  4. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/decimer_segmentation.py +242 -136
  5. {decimer_segmentation-1.4.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.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/PKG-INFO +4 -6
  8. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/SOURCES.txt +1 -0
  9. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/requires.txt +4 -2
  10. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/setup.py +10 -10
  11. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/LICENSE +0 -0
  12. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/README.md +0 -0
  13. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/config.py +0 -0
  14. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/moldetect.py +0 -0
  15. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/utils.py +0 -0
  16. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation/mrcnn/visualize.py +0 -0
  17. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/dependency_links.txt +0 -0
  18. {decimer_segmentation-1.4.0 → decimer_segmentation-1.5.0}/decimer_segmentation.egg-info/top_level.txt +0 -0
  19. {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.4.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",
@@ -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(image,
184
- 1,
185
- np.pi / 180,
186
- threshold=5,
187
- minLineLength=int(max(max_depiction_size)/4),
188
- maxLineGap=10)
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.bool)
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
- * 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
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
- from pdf2image import convert_from_path
16
- from pdf2image.exceptions import PDFInfoNotInstalledError
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 .complete_structure import complete_structure_mask
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: Windows users need to specify the path of their
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
- try:
63
- images = convert_from_path(file_path, 300, poppler_path=poppler_path)
64
- except PDFInfoNotInstalledError:
65
- poppler_path = "C:\\Program Files (x86)\\poppler-0.68.0\\bin"
66
- images = convert_from_path(file_path, 300, poppler_path=poppler_path)
67
- images = [np.array(image) for image in 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
- images = [cv2.imread(file_path)]
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
- with Pool(4) as pool:
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
- segments = [su for li in segments for su in li]
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
- ) -> List[np.array]:
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
- List[np.array]: expanded segments sorted in top->bottom, left->right order with
97
- a certain tolerance for grouping into "lines"(shape: (h, w, num_masks))
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
- segments = [segment for segment in segments
119
- if segment.shape[0] > 0
120
- if segment.shape[1] > 0]
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
- return segments
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
- heights = []
140
- widths = []
141
- for bbox in bboxes:
142
- height = bbox[2] - bbox[0]
143
- width = bbox[3] - bbox[1]
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 (top-to-bottom reading order)
170
- sorted_bboxes = sorted(bboxes, key=lambda bbox: bbox[0])
205
+ # Sort indices by y-coordinate
206
+ indices.sort(key=lambda i: bboxes[i][0])
171
207
 
172
- # Group bounding boxes by rows based on y-coordinate
208
+ # Group bounding boxes by rows
173
209
  rows = []
174
- current_row = [sorted_bboxes[0]]
175
- for bbox in sorted_bboxes[1:]:
176
- if (
177
- abs(bbox[0] - current_row[-1][0]) < same_row_pixel_threshold
178
- ): # You can adjust this threshold as needed
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
- rows.append(
182
- sorted(current_row, key=lambda x: x[1])
183
- ) # Sort by x-coordinate within each row
184
- current_row = [bbox]
185
- rows.append(sorted(current_row, key=lambda x: x[1])) # Sort the last row
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
- # Flatten the list of rows and return
188
- sorted_bboxes = [bbox for row in rows for bbox in row]
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 = "https://zenodo.org/record/10663579/files/mask_rcnn_molecule.h5?download=1"
209
- req = requests.get(url, allow_redirects=True)
210
- with open(model_path, "wb") as model_file:
211
- model_file.write(req.content)
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(image: np.array, masks: np.array) -> List[np.array]:
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
- masks = [masks[:, :, i] for i in range(masks.shape[2])]
287
- if len(masks) == 0:
339
+ if masks.shape[2] == 0:
288
340
  return [], []
289
- segmented_images_bboxes = map(apply_mask, cycle([image]), masks)
290
- segmented_images, bboxes = list(zip(*list(segmented_images_bboxes)))
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(image: np.array, mask: np.array) -> np.array:
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 depictions (np.array)
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
- # TODO: Further cleanup
308
- im = deepcopy(image)
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
- # Removal of transparent layer and generation of segment
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
- def get_masked_image(image: np.array, mask: np.array) -> np.array:
327
- """
328
- This function takes an image and a masks for this image
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
- Args:
334
- image (np.array): image of a page from a scientific publication
335
- mask (np.array): masks (shape: (h, w, num_masks))
394
+ # Set transparent pixels to white
395
+ trans_mask = rgba[:, :, 3] == 0
396
+ rgba[trans_mask] = [255, 255, 255, 255]
336
397
 
337
- Returns:
338
- List[np.array]: segmented chemical structure depictions
339
- List[int]: bounding box of segmented object
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
- for channel in range(image.shape[2]):
342
- image[:, :, channel] = image[:, :, channel] * mask[:, :]
343
- # Remove unwanted background
344
- grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
345
- _, thresholded = cv2.threshold(grayscale, 0, 255, cv2.THRESH_OTSU)
346
- bbox = cv2.boundingRect(thresholded)
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 = np.zeros(image.shape).astype(np.uint8)
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
- if not os.path.exists(path):
367
- os.makedirs(path, exist_ok=True)
368
- for index in range(len(images)):
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, images[index])
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
- grayscale_im = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
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
- image = Image.fromarray(image)
405
- old_size = image.size
406
- grayscale_image = image.convert("L")
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
- new_size = old_size
413
- resized_image = Image.new("L", (desired_size, desired_size), "white")
486
+ grayscale = image
414
487
 
415
- resized_image.paste(
416
- grayscale_image,
417
- ((desired_size - new_size[0]) // 2, (desired_size - new_size[1]) // 2),
418
- )
419
- return np.array(resized_image)
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
- model = load_model()
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
- # Segment chemical structure depictions
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])
@@ -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"] = "3"
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(nms_keep_map, unique_pre_nms_class_ids, dtype=tf.int64)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: decimer-segmentation
3
- Version: 1.4.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
 
@@ -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
@@ -1,9 +1,11 @@
1
- tensorflow==2.12.0
1
+ tensorflow<=2.15.1,>=2.12.0
2
2
  numpy>=1.2.0
3
3
  scikit-image>=0.2.0
4
4
  pillow
5
5
  opencv-python
6
6
  matplotlib
7
7
  IPython
8
- pdf2image
8
+ PyMuPDF
9
+ numba
9
10
  scipy
11
+ requests
@@ -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-macos==2.10.0"
9
+ tensorflow_os = "tensorflow>=2.14.0,<=2.15.1"
10
10
  else:
11
- tensorflow_os = "tensorflow==2.12.0"
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.4.0",
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
- "pdf2image",
38
- "scipy"
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.5",
44
- "Programming Language :: Python :: 3.6",
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.5",
51
+ python_requires=">=3.10",
52
52
  )