deepdoctection 0.39.6__py3-none-any.whl → 0.40.0__py3-none-any.whl

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.

Potentially problematic release.


This version of deepdoctection might be problematic. Click here for more details.

@@ -25,7 +25,7 @@ from .utils.logger import LoggingRecord, logger
25
25
 
26
26
  # pylint: enable=wrong-import-position
27
27
 
28
- __version__ = "0.39.6"
28
+ __version__ = "0.40.0"
29
29
 
30
30
  _IMPORT_STRUCTURE = {
31
31
  "analyzer": ["config_sanity_checks", "get_dd_analyzer", "ServiceFactory"],
@@ -260,6 +260,7 @@ _IMPORT_STRUCTURE = {
260
260
  "ImageCroppingService",
261
261
  "IntersectionMatcher",
262
262
  "NeighbourMatcher",
263
+ "FamilyCompound",
263
264
  "MatchingService",
264
265
  "PageParsingService",
265
266
  "AnnotationNmsService",
@@ -72,7 +72,6 @@ cfg.SEGMENTATION.THRESHOLD_COLS = 0.4
72
72
  cfg.SEGMENTATION.FULL_TABLE_TILING = True
73
73
  cfg.SEGMENTATION.REMOVE_IOU_THRESHOLD_ROWS = 0.001
74
74
  cfg.SEGMENTATION.REMOVE_IOU_THRESHOLD_COLS = 0.001
75
- cfg.SEGMENTATION.CELL_CATEGORY_ID = 12
76
75
  cfg.SEGMENTATION.TABLE_NAME = LayoutType.TABLE
77
76
  cfg.SEGMENTATION.PUBTABLES_CELL_NAMES = [
78
77
  CellType.SPANNING,
@@ -35,13 +35,14 @@ from ..extern.tpdetect import TPFrcnnDetector
35
35
  from ..pipe.base import PipelineComponent
36
36
  from ..pipe.common import (
37
37
  AnnotationNmsService,
38
+ FamilyCompound,
38
39
  IntersectionMatcher,
39
40
  MatchingService,
40
41
  NeighbourMatcher,
41
42
  PageParsingService,
42
43
  )
43
44
  from ..pipe.doctectionpipe import DoctectionPipe
44
- from ..pipe.layout import ImageLayoutService
45
+ from ..pipe.layout import ImageLayoutService, skip_if_category_or_service_extracted
45
46
  from ..pipe.order import TextOrderService
46
47
  from ..pipe.refine import TableSegmentationRefinementService
47
48
  from ..pipe.segment import PubtablesSegmentationService, TableSegmentationService
@@ -284,7 +285,6 @@ class ServiceFactory:
284
285
  return SubImageLayoutService(
285
286
  sub_image_detector=detector,
286
287
  sub_image_names=[LayoutType.TABLE, LayoutType.TABLE_ROTATED],
287
- category_id_mapping=None,
288
288
  detect_result_generator=detect_result_generator,
289
289
  padder=padder,
290
290
  )
@@ -405,7 +405,6 @@ class ServiceFactory:
405
405
  tile_table_with_items=config.SEGMENTATION.FULL_TABLE_TILING,
406
406
  remove_iou_threshold_rows=config.SEGMENTATION.REMOVE_IOU_THRESHOLD_ROWS,
407
407
  remove_iou_threshold_cols=config.SEGMENTATION.REMOVE_IOU_THRESHOLD_COLS,
408
- cell_class_id=config.SEGMENTATION.CELL_CATEGORY_ID,
409
408
  table_name=config.SEGMENTATION.TABLE_NAME,
410
409
  cell_names=config.SEGMENTATION.PUBTABLES_CELL_NAMES,
411
410
  spanning_cell_names=config.SEGMENTATION.PUBTABLES_SPANNING_CELL_NAMES,
@@ -516,6 +515,15 @@ class ServiceFactory:
516
515
  """
517
516
  return ServiceFactory._build_pdf_miner_text_service(detector)
518
517
 
518
+ @staticmethod
519
+ def _build_doctr_word_detector_service(detector: DoctrTextlineDetector) -> ImageLayoutService:
520
+ """Building a Doctr word detector service
521
+
522
+ :param detector: DoctrTextlineDetector
523
+ :return: ImageLayoutService
524
+ """
525
+ return ImageLayoutService(layout_detector=detector, to_image=True, crop_image=True)
526
+
519
527
  @staticmethod
520
528
  def build_doctr_word_detector_service(detector: DoctrTextlineDetector) -> ImageLayoutService:
521
529
  """Building a Doctr word detector service
@@ -523,9 +531,7 @@ class ServiceFactory:
523
531
  :param detector: DoctrTextlineDetector
524
532
  :return: ImageLayoutService
525
533
  """
526
- return ImageLayoutService(
527
- layout_detector=detector, to_image=True, crop_image=True, skip_if_layout_extracted=True
528
- )
534
+ return ServiceFactory._build_doctr_word_detector_service(detector)
529
535
 
530
536
  @staticmethod
531
537
  def _build_text_extraction_service(
@@ -539,7 +545,6 @@ class ServiceFactory:
539
545
  """
540
546
  return TextExtractionService(
541
547
  detector,
542
- skip_if_text_extracted=config.USE_PDF_MINER,
543
548
  extract_from_roi=config.TEXT_CONTAINER if config.OCR.USE_DOCTR else None,
544
549
  )
545
550
 
@@ -567,11 +572,16 @@ class ServiceFactory:
567
572
  threshold=config.WORD_MATCHING.THRESHOLD,
568
573
  max_parent_only=config.WORD_MATCHING.MAX_PARENT_ONLY,
569
574
  )
575
+ family_compounds = [
576
+ FamilyCompound(
577
+ parent_categories=config.WORD_MATCHING.PARENTAL_CATEGORIES,
578
+ child_categories=config.TEXT_CONTAINER,
579
+ relationship_key=Relationships.CHILD,
580
+ )
581
+ ]
570
582
  return MatchingService(
571
- parent_categories=config.WORD_MATCHING.PARENTAL_CATEGORIES,
572
- child_categories=config.TEXT_CONTAINER,
583
+ family_compounds=family_compounds,
573
584
  matcher=matcher,
574
- relationship_key=Relationships.CHILD,
575
585
  )
576
586
 
577
587
  @staticmethod
@@ -591,11 +601,16 @@ class ServiceFactory:
591
601
  :return: MatchingService
592
602
  """
593
603
  neighbor_matcher = NeighbourMatcher()
604
+ family_compounds = [
605
+ FamilyCompound(
606
+ parent_categories=config.LAYOUT_LINK.PARENTAL_CATEGORIES,
607
+ child_categories=config.LAYOUT_LINK.CHILD_CATEGORIES,
608
+ relationship_key=Relationships.LAYOUT_LINK,
609
+ )
610
+ ]
594
611
  return MatchingService(
595
- parent_categories=config.LAYOUT_LINK.PARENTAL_CATEGORIES,
596
- child_categories=config.LAYOUT_LINK.CHILD_CATEGORIES,
612
+ family_compounds=family_compounds,
597
613
  matcher=neighbor_matcher,
598
- relationship_key=Relationships.LAYOUT_LINK,
599
614
  )
600
615
 
601
616
  @staticmethod
@@ -699,9 +714,11 @@ class ServiceFactory:
699
714
  table_refinement_service = ServiceFactory.build_table_refinement_service(config)
700
715
  pipe_component_list.append(table_refinement_service)
701
716
 
717
+ d_text_service_id = ""
702
718
  if config.USE_PDF_MINER:
703
719
  pdf_miner = ServiceFactory.build_pdf_text_detector(config)
704
720
  d_text = ServiceFactory.build_pdf_miner_text_service(pdf_miner)
721
+ d_text_service_id = d_text.service_id
705
722
  pipe_component_list.append(d_text)
706
723
 
707
724
  # setup ocr
@@ -710,10 +727,14 @@ class ServiceFactory:
710
727
  if config.OCR.USE_DOCTR:
711
728
  word_detector = ServiceFactory.build_doctr_word_detector(config)
712
729
  word_service = ServiceFactory.build_doctr_word_detector_service(word_detector)
730
+ word_service.set_inbound_filter(skip_if_category_or_service_extracted(service_ids=d_text_service_id))
713
731
  pipe_component_list.append(word_service)
714
732
 
715
733
  ocr_detector = ServiceFactory.build_ocr_detector(config)
716
734
  text_extraction_service = ServiceFactory.build_text_extraction_service(config, ocr_detector)
735
+ text_extraction_service.set_inbound_filter(
736
+ skip_if_category_or_service_extracted(service_ids=d_text_service_id)
737
+ )
717
738
  pipe_component_list.append(text_extraction_service)
718
739
 
719
740
  if config.USE_PDF_MINER or config.USE_OCR:
@@ -18,10 +18,11 @@
18
18
  """
19
19
  Implementation of BoundingBox class and related methods
20
20
  """
21
+ from __future__ import annotations
21
22
 
22
23
  from dataclasses import dataclass
23
24
  from math import ceil, floor
24
- from typing import Optional, Sequence, no_type_check
25
+ from typing import Optional, Sequence, Union, no_type_check
25
26
 
26
27
  import numpy as np
27
28
  import numpy.typing as npt
@@ -31,7 +32,7 @@ from numpy import float32
31
32
  from ..utils.error import BoundingBoxError
32
33
  from ..utils.file_utils import cocotools_available
33
34
  from ..utils.logger import LoggingRecord, logger
34
- from ..utils.types import PixelValues
35
+ from ..utils.types import BoxCoordinate, PixelValues
35
36
 
36
37
  with try_import() as import_guard:
37
38
  import pycocotools.mask as coco_mask
@@ -142,96 +143,213 @@ def iou(boxes1: npt.NDArray[float32], boxes2: npt.NDArray[float32]) -> npt.NDArr
142
143
  return np_iou(boxes1, boxes2)
143
144
 
144
145
 
146
+ RELATIVE_COORD_CONVERTER = 10**8
147
+
148
+
145
149
  @dataclass
146
150
  class BoundingBox:
147
151
  """
148
- Rectangular bounding box dataclass for object detection. Store coordinates and allows several
149
- representations. You can define an instance by passing the upper left point along with either height
150
- and width or along with the lower right point. Pass absolute_coords = 'True' if you work with image
151
- pixel coordinates. If you work with coordinates in the range between (0,1) then pass absolute_coords
152
- ='False'. A bounding box is a disposable object. Do not change the coordinates once the have been set but define
153
- a new box.
154
-
155
- `absolute_coords` indicates, whether given coordinates are in absolute or in relative terms
156
-
157
- `ulx`: upper left x
152
+ Rectangular bounding box that stores coordinates and allows different representations.
158
153
 
159
- `uly`: upper left y
154
+ This implementation differs from the previous version by using internal integer storage with precision scaling
155
+ for both absolute and relative coordinates. Coordinates are stored internally as integers (_ulx, _uly, etc.)
156
+ with relative coordinates multiplied by RELATIVE_COORD_CONVERTER (10^8) for precision. Properties (ulx, uly, etc.)
157
+ handle the conversion between internal storage and exposed values.
160
158
 
161
- `lrx`: lower right x
159
+ You can define an instance by passing:
160
+ - Upper left point (ulx, uly) + width and height, OR
161
+ - Upper left point (ulx, uly) + lower right point (lrx, lry)
162
162
 
163
- `lry`: lower right y
164
-
165
- `height`: height
166
-
167
- `width`: width
163
+ Notes:
164
+ - When absolute_coords=True, coordinates will be rounded to integers
165
+ - When absolute_coords=False, coordinates must be between 0 and 1
166
+ - The box is validated on initialization to ensure coordinates are valid
168
167
  """
169
168
 
170
169
  absolute_coords: bool
171
- ulx: float
172
- uly: float
173
- lrx: float = 0.0
174
- lry: float = 0.0
175
- height: float = 0.0
176
- width: float = 0.0
170
+ _ulx: int = 0
171
+ _uly: int = 0
172
+ _lrx: int = 0
173
+ _lry: int = 0
174
+ _height: int = 0
175
+ _width: int = 0
176
+
177
+ def __init__(
178
+ self,
179
+ absolute_coords: bool,
180
+ ulx: BoxCoordinate,
181
+ uly: BoxCoordinate,
182
+ lrx: BoxCoordinate = 0,
183
+ lry: BoxCoordinate = 0,
184
+ width: BoxCoordinate = 0,
185
+ height: BoxCoordinate = 0,
186
+ ):
187
+ """
188
+ Initialize a BoundingBox instance with the specified coordinates.
189
+
190
+ This initializer supports two ways of defining a bounding box:
191
+ 1. Using upper-left coordinates (ulx, uly) with width and height
192
+ 2. Using upper-left (ulx, uly) and lower-right (lrx, lry) coordinates
193
+
194
+ When absolute_coords is True, coordinates are stored as integers.
195
+ When absolute_coords is False, coordinates are stored as scaled integers
196
+ (original float values * RELATIVE_COORD_CONVERTER) for precision.
197
+
198
+ :param absolute_coords: Whether coordinates are absolute pixels (True) or normalized [0,1] values (False)
199
+ :param ulx: Upper-left x-coordinate (float or int)
200
+ :param uly: Upper-left y-coordinate (float or int)
201
+ :param lrx: Lower-right x-coordinate (float or int), default 0
202
+ :param lry: Lower-right y-coordinate (float or int), default 0
203
+ :param width: Width of the bounding box (float or int), default 0
204
+ :param height: Height of the bounding box (float or int), default 0
205
+ """
206
+ self.absolute_coords = absolute_coords
207
+ if absolute_coords:
208
+ self._ulx = round(ulx)
209
+ self._uly = round(uly)
210
+ if lrx and lry:
211
+ self._lrx = round(lrx)
212
+ self._lry = round(lry)
213
+ if width and height:
214
+ self._width = round(width)
215
+ self._height = round(height)
216
+ else:
217
+ self._ulx = round(ulx * RELATIVE_COORD_CONVERTER)
218
+ self._uly = round(uly * RELATIVE_COORD_CONVERTER)
219
+ if lrx and lry:
220
+ self._lrx = round(lrx * RELATIVE_COORD_CONVERTER)
221
+ self._lry = round(lry * RELATIVE_COORD_CONVERTER)
222
+ if width and height:
223
+ self._width = round(width * RELATIVE_COORD_CONVERTER)
224
+ self._height = round(height * RELATIVE_COORD_CONVERTER)
225
+ if not self._width and not self._height:
226
+ self._width = self._lrx - self._ulx
227
+ self._height = self._lry - self._uly
228
+ if not self._lrx and not self._lry:
229
+ self._lrx = self._ulx + self._width
230
+ self._lry = self._uly + self._height
231
+ self.__post_init__()
177
232
 
178
233
  def __post_init__(self) -> None:
179
- if self.width == 0.0:
180
- if self.lrx is None:
234
+ if self._width == 0:
235
+ if self._lrx is None:
181
236
  raise BoundingBoxError("Bounding box not fully initialized")
182
- self.width = self.lrx - self.ulx
183
- if self.height == 0.0:
184
- if self.lry is None:
237
+ self._width = self._lrx - self._ulx
238
+ if self._height == 0:
239
+ if self._lry is None:
185
240
  raise BoundingBoxError("Bounding box not fully initialized")
186
- self.height = self.lry - self.uly
241
+ self._height = self._lry - self._uly
187
242
 
188
- if self.lrx == 0.0:
189
- if self.width is None:
243
+ if self._lrx == 0:
244
+ if self._width is None:
190
245
  raise BoundingBoxError("Bounding box not fully initialized")
191
- self.lrx = self.ulx + self.width
192
- if self.lry == 0.0:
193
- if self.height is None:
246
+ self._lrx = self._ulx + self._width
247
+ if self._lry == 0:
248
+ if self._height is None:
194
249
  raise BoundingBoxError("Bounding box not fully initialized")
195
- self.lry = self.uly + self.height
250
+ self._lry = self._uly + self._height
196
251
 
197
- if not (self.ulx >= 0.0 and self.uly >= 0.0):
198
- raise BoundingBoxError("Bounding box ul must be >= (0.,0.)")
199
- if not (self.height > 0.0 and self.width > 0.0):
252
+ if not (self._ulx >= 0 and self._uly >= 0):
253
+ raise BoundingBoxError("Bounding box ul must be >= (0,0)")
254
+ if not (self._height > 0 and self._width > 0):
200
255
  raise BoundingBoxError(
201
256
  f"bounding box must have height and width >0. Check coords "
202
257
  f"ulx: {self.ulx}, uly: {self.uly}, lrx: {self.lrx}, "
203
258
  f"lry: {self.lry}."
204
259
  )
205
- if not self.absolute_coords:
206
- if not (self.ulx <= 1.0 and self.uly <= 1.0 and self.lrx <= 1.0 and self.lry <= 1.0):
207
- raise BoundingBoxError("coordinates must be between 0 and 1")
260
+ if not self.absolute_coords and not (
261
+ 0 <= self.ulx <= 1 and 0 <= self.uly <= 1 and 0 <= self.lrx <= 1 and 0 <= self.lry <= 1
262
+ ):
263
+ raise BoundingBoxError("coordinates must be between 0 and 1")
208
264
 
209
265
  @property
210
- def cx(self) -> float:
211
- """
212
- Bounding box center x coordinate
213
- """
266
+ def ulx(self) -> BoxCoordinate:
267
+ """ulx property"""
268
+ return self._ulx / RELATIVE_COORD_CONVERTER if not self.absolute_coords else self._ulx
269
+
270
+ @ulx.setter
271
+ def ulx(self, value: BoxCoordinate) -> None:
272
+ """ulx setter"""
273
+ self._ulx = round(value * RELATIVE_COORD_CONVERTER) if not self.absolute_coords else round(value)
274
+ self._width = self._lrx - self._ulx
275
+
276
+ @property
277
+ def uly(self) -> BoxCoordinate:
278
+ """uly property"""
279
+ return self._uly / RELATIVE_COORD_CONVERTER if not self.absolute_coords else self._uly
280
+
281
+ @uly.setter
282
+ def uly(self, value: BoxCoordinate) -> None:
283
+ """uly setter"""
284
+ self._uly = round(value * RELATIVE_COORD_CONVERTER) if not self.absolute_coords else round(value)
285
+ self._height = self._lry - self._uly
286
+
287
+ @property
288
+ def lrx(self) -> BoxCoordinate:
289
+ """lrx property"""
290
+ return self._lrx / RELATIVE_COORD_CONVERTER if not self.absolute_coords else self._lrx
291
+
292
+ @lrx.setter
293
+ def lrx(self, value: BoxCoordinate) -> None:
294
+ """lrx setter"""
295
+ self._lrx = round(value * RELATIVE_COORD_CONVERTER) if not self.absolute_coords else round(value)
296
+ self._width = self._lrx - self._ulx
297
+
298
+ @property
299
+ def lry(self) -> BoxCoordinate:
300
+ """lry property"""
301
+ return self._lry / RELATIVE_COORD_CONVERTER if not self.absolute_coords else self._lry
302
+
303
+ @lry.setter
304
+ def lry(self, value: BoxCoordinate) -> None:
305
+ """lry setter"""
306
+ self._lry = round(value * RELATIVE_COORD_CONVERTER) if not self.absolute_coords else round(value)
307
+ self._height = self._lry - self._uly
308
+
309
+ @property
310
+ def width(self) -> BoxCoordinate:
311
+ """width property"""
312
+ return self._width / RELATIVE_COORD_CONVERTER if not self.absolute_coords else self._width
313
+
314
+ @width.setter
315
+ def width(self, value: BoxCoordinate) -> None:
316
+ """width setter"""
317
+ self._width = round(value * RELATIVE_COORD_CONVERTER) if not self.absolute_coords else round(value)
318
+ self._lrx = self._ulx + self._width
319
+
320
+ @property
321
+ def height(self) -> BoxCoordinate:
322
+ """height property"""
323
+ return self._height / RELATIVE_COORD_CONVERTER if not self.absolute_coords else self._height
324
+
325
+ @height.setter
326
+ def height(self, value: BoxCoordinate) -> None:
327
+ """height setter"""
328
+ self._height = round(value * RELATIVE_COORD_CONVERTER) if not self.absolute_coords else round(value)
329
+ self._lry = self._uly + self._height
330
+
331
+ @property
332
+ def cx(self) -> BoxCoordinate:
333
+ """cx property"""
334
+ if self.absolute_coords:
335
+ return round(self.ulx + 0.5 * self.width)
214
336
  return self.ulx + 0.5 * self.width
215
337
 
216
338
  @property
217
- def cy(self) -> float:
218
- """
219
- Bounding box center y coordinate
220
- """
339
+ def cy(self) -> BoxCoordinate:
340
+ """cy property"""
341
+ if self.absolute_coords:
342
+ return round(self.uly + 0.5 * self.height)
221
343
  return self.uly + 0.5 * self.height
222
344
 
223
345
  @property
224
- def center(self) -> list[float]:
225
- """
226
- Bounding box center [x,y]
227
- """
228
- return [self.cx, self.cy]
346
+ def center(self) -> tuple[BoxCoordinate, BoxCoordinate]:
347
+ """center property"""
348
+ return (self.cx, self.cy)
229
349
 
230
350
  @property
231
- def area(self) -> float:
232
- """
233
- Bounding box area
234
- """
351
+ def area(self) -> Union[int, float]:
352
+ """area property"""
235
353
  if self.absolute_coords:
236
354
  return self.width * self.height
237
355
  raise ValueError("Cannot calculate area, when bounding box coords are relative")
@@ -264,7 +382,7 @@ class BoundingBox:
264
382
  * np_poly_scale
265
383
  )
266
384
 
267
- def to_list(self, mode: str, scale_x: float = 1.0, scale_y: float = 1.0) -> list[float]:
385
+ def to_list(self, mode: str, scale_x: float = 1.0, scale_y: float = 1.0) -> list[BoxCoordinate]:
268
386
  """
269
387
  Returns the coordinates as list
270
388
 
@@ -280,36 +398,62 @@ class BoundingBox:
280
398
  """
281
399
  assert mode in ("xyxy", "xywh", "poly"), "Not a valid mode"
282
400
  if mode == "xyxy":
283
- return [
401
+ return (
402
+ [
403
+ round(self.ulx * scale_x),
404
+ round(self.uly * scale_y),
405
+ round(self.lrx * scale_x),
406
+ round(self.lry * scale_y),
407
+ ]
408
+ if self.absolute_coords
409
+ else [
410
+ self.ulx * scale_x,
411
+ self.uly * scale_y,
412
+ self.lrx * scale_x,
413
+ self.lry * scale_y,
414
+ ]
415
+ )
416
+ if mode == "xywh":
417
+ return (
418
+ [
419
+ round(self.ulx * scale_x),
420
+ round(self.uly * scale_y),
421
+ round(self.width * scale_x),
422
+ round(self.height * scale_y),
423
+ ]
424
+ if self.absolute_coords
425
+ else [self.ulx * scale_x, self.uly * scale_y, self.width * scale_x, self.height * scale_y]
426
+ )
427
+ return (
428
+ [
429
+ round(self.ulx * scale_x),
430
+ round(self.uly * scale_y),
431
+ round(self.lrx * scale_x),
432
+ round(self.uly * scale_y),
433
+ round(self.lrx * scale_x),
434
+ round(self.lry * scale_y),
435
+ round(self.ulx * scale_x),
436
+ round(self.lry * scale_y),
437
+ ]
438
+ if self.absolute_coords
439
+ else [
284
440
  self.ulx * scale_x,
285
441
  self.uly * scale_y,
286
442
  self.lrx * scale_x,
443
+ self.uly * scale_y,
444
+ self.lrx * scale_x,
287
445
  self.lry * scale_y,
288
- ]
289
- if mode == "xywh":
290
- return [
291
446
  self.ulx * scale_x,
292
- self.uly * scale_y,
293
- self.width * scale_x,
294
- self.height * scale_y,
447
+ self.lry * scale_y,
295
448
  ]
296
- return [
297
- self.ulx * scale_x,
298
- self.uly * scale_y,
299
- self.lrx * scale_x,
300
- self.uly * scale_y,
301
- self.lrx * scale_x,
302
- self.lry * scale_y,
303
- self.ulx * scale_x,
304
- self.lry * scale_y,
305
- ]
449
+ )
306
450
 
307
451
  def transform(
308
452
  self,
309
453
  image_width: float,
310
454
  image_height: float,
311
455
  absolute_coords: bool = False,
312
- ) -> "BoundingBox":
456
+ ) -> BoundingBox:
313
457
  """
314
458
  Transforms bounding box coordinates into absolute or relative coords. Internally, a new bounding box will be
315
459
  created. Changing coordinates requires width and height of the whole image.
@@ -320,8 +464,7 @@ class BoundingBox:
320
464
 
321
465
  :return: Either a list or np.array.
322
466
  """
323
-
324
- if absolute_coords != self.absolute_coords: # only transforming in this case
467
+ if absolute_coords != self.absolute_coords:
325
468
  if self.absolute_coords:
326
469
  transformed_box = BoundingBox(
327
470
  absolute_coords=not self.absolute_coords,
@@ -344,22 +487,26 @@ class BoundingBox:
344
487
  def __str__(self) -> str:
345
488
  return f"Bounding Box ulx: {self.ulx}, uly: {self.uly}, lrx: {self.lrx}, lry: {self.lry}"
346
489
 
490
+ def __repr__(self) -> str:
491
+ return (
492
+ f"BoundingBox(absolute_coords={self.absolute_coords}, ulx={self.ulx}, uly={self.uly}, lrx={self.lrx},"
493
+ f" lry={self.lry}, width={self.width}, height={self.height})"
494
+ )
495
+
347
496
  @staticmethod
348
497
  def remove_keys() -> list[str]:
349
- """
350
- A list of attributes to suspend from as_dict creation.
351
- """
352
- return ["height", "width"]
498
+ """Removing keys when converting the dataclass object to a dict"""
499
+ return ["_height", "_width"]
500
+
501
+ @staticmethod
502
+ def replace_keys() -> dict[str, str]:
503
+ """Replacing keys when converting the dataclass object to a dict. Useful for backward compatibility"""
504
+ return {"_ulx": "ulx", "_uly": "uly", "_lrx": "lrx", "_lry": "lry"}
353
505
 
354
506
  @classmethod
355
507
  @no_type_check
356
- def from_dict(cls, **kwargs) -> "BoundingBox":
357
- """
358
- Create `BoundingBox` instance from dict
359
-
360
- :param kwargs: dict with `BoundingBox` attributes
361
- :return: Initialized BoundingBox
362
- """
508
+ def from_dict(cls, **kwargs) -> BoundingBox:
509
+ """from dict"""
363
510
  return cls(**kwargs)
364
511
 
365
512
 
@@ -65,6 +65,10 @@ def as_dict(obj: Any, dict_factory) -> Union[Any]: # type: ignore
65
65
  if hasattr(obj, "remove_keys"):
66
66
  if attribute.name in obj.remove_keys():
67
67
  continue
68
+ if hasattr(obj, "replace_keys"):
69
+ old_to_new_keys = obj.replace_keys()
70
+ if attribute.name in old_to_new_keys:
71
+ attribute.name = obj.replace_keys()[attribute.name]
68
72
  result.append((attribute.name, value))
69
73
  return dict_factory(result)
70
74
  if isinstance(obj, (list, tuple)):
@@ -342,7 +342,7 @@ class Image:
342
342
  self,
343
343
  category_names: Optional[Union[str, ObjectTypes, Sequence[Union[str, ObjectTypes]]]] = None,
344
344
  annotation_ids: Optional[Union[str, Sequence[str]]] = None,
345
- service_id: Optional[Union[str, Sequence[str]]] = None,
345
+ service_ids: Optional[Union[str, Sequence[str]]] = None,
346
346
  model_id: Optional[Union[str, Sequence[str]]] = None,
347
347
  session_ids: Optional[Union[str, Sequence[str]]] = None,
348
348
  ignore_inactive: bool = True,
@@ -356,7 +356,7 @@ class Image:
356
356
 
357
357
  :param category_names: A single name or list of names
358
358
  :param annotation_ids: A single id or list of ids
359
- :param service_id: A single service name or list of service names
359
+ :param service_ids: A single service name or list of service names
360
360
  :param model_id: A single model name or list of model names
361
361
  :param session_ids: A single session id or list of session ids
362
362
  :param ignore_inactive: If set to `True` only active annotations are returned.
@@ -372,7 +372,7 @@ class Image:
372
372
  )
373
373
 
374
374
  ann_ids = [annotation_ids] if isinstance(annotation_ids, str) else annotation_ids
375
- service_id = [service_id] if isinstance(service_id, str) else service_id
375
+ service_ids = [service_ids] if isinstance(service_ids, str) else service_ids
376
376
  model_id = [model_id] if isinstance(model_id, str) else model_id
377
377
  session_id = [session_ids] if isinstance(session_ids, str) else session_ids
378
378
 
@@ -387,8 +387,8 @@ class Image:
387
387
  if ann_ids is not None:
388
388
  anns = filter(lambda x: x.annotation_id in ann_ids, anns)
389
389
 
390
- if service_id is not None:
391
- anns = filter(lambda x: x.service_id in service_id, anns)
390
+ if service_ids is not None:
391
+ anns = filter(lambda x: x.service_id in service_ids, anns)
392
392
 
393
393
  if model_id is not None:
394
394
  anns = filter(lambda x: x.model_id in model_id, anns)
@@ -659,7 +659,7 @@ class Page(Image):
659
659
  self,
660
660
  category_names: Optional[Union[str, ObjectTypes, Sequence[Union[str, ObjectTypes]]]] = None,
661
661
  annotation_ids: Optional[Union[str, Sequence[str]]] = None,
662
- service_id: Optional[Union[str, Sequence[str]]] = None,
662
+ service_ids: Optional[Union[str, Sequence[str]]] = None,
663
663
  model_id: Optional[Union[str, Sequence[str]]] = None,
664
664
  session_ids: Optional[Union[str, Sequence[str]]] = None,
665
665
  ignore_inactive: bool = True,
@@ -676,7 +676,7 @@ class Page(Image):
676
676
 
677
677
  :param category_names: A single name or list of names
678
678
  :param annotation_ids: A single id or list of ids
679
- :param service_id: A single service name or list of service names
679
+ :param service_ids: A single service name or list of service names
680
680
  :param model_id: A single model name or list of model names
681
681
  :param session_ids: A single session id or list of session ids
682
682
  :param ignore_inactive: If set to `True` only active annotations are returned.
@@ -691,7 +691,7 @@ class Page(Image):
691
691
  else tuple(get_type(cat_name) for cat_name in category_names)
692
692
  )
693
693
  ann_ids = [annotation_ids] if isinstance(annotation_ids, str) else annotation_ids
694
- service_id = [service_id] if isinstance(service_id, str) else service_id
694
+ service_ids = [service_ids] if isinstance(service_ids, str) else service_ids
695
695
  model_id = [model_id] if isinstance(model_id, str) else model_id
696
696
  session_id = [session_ids] if isinstance(session_ids, str) else session_ids
697
697
 
@@ -706,8 +706,8 @@ class Page(Image):
706
706
  if ann_ids is not None:
707
707
  anns = filter(lambda x: x.annotation_id in ann_ids, anns)
708
708
 
709
- if service_id is not None:
710
- anns = filter(lambda x: x.generating_service in service_id, anns)
709
+ if service_ids is not None:
710
+ anns = filter(lambda x: x.generating_service in service_ids, anns)
711
711
 
712
712
  if model_id is not None:
713
713
  anns = filter(lambda x: x.generating_model in model_id, anns)