Skip to content

Commit f107181

Browse files
committed
added roi-images filter for extracting sub-images based on bboxes of objdet annotations
1 parent 5160f47 commit f107181

6 files changed

Lines changed: 201 additions & 1 deletion

File tree

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
-------------------
66

77
- switched from `imgaug3` to `imaug` for numpy 2 support
8+
- added `roi-images` filter for extracting sub-images based on object-detection bounding boxes
89

910

1011
0.0.9 (2025-04-03)

plugins/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* [linear-contrast](linear-contrast.md)
1313
* [meta-sub-images](meta-sub-images.md)
1414
* [resize](resize.md)
15+
* [roi-images](roi-images.md)
1516
* [rotate](rotate.md)
1617
* [scale](scale.md)
1718
* [sub-images](sub-images.md)

plugins/roi-images.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# roi-images
2+
3+
* accepts: idc.api.ObjectDetectionData
4+
* generates: idc.api.ObjectDetectionData
5+
6+
Extracts sub-images using the bbox of all the object detection annotations that have matching labels. If no labels are specified, all annotations are extracted.
7+
8+
```
9+
usage: roi-images [-h] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
10+
[-N LOGGER_NAME] [--labels [LABELS ...]] [-S SUFFIX]
11+
12+
Extracts sub-images using the bbox of all the object detection annotations
13+
that have matching labels. If no labels are specified, all annotations are
14+
extracted.
15+
16+
options:
17+
-h, --help show this help message and exit
18+
-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --logging_level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
19+
The logging level to use. (default: WARN)
20+
-N LOGGER_NAME, --logger_name LOGGER_NAME
21+
The custom name to use for the logger, uses the plugin
22+
name by default (default: None)
23+
--labels [LABELS ...]
24+
The label(s) of the annotations to forward as sub-
25+
images, uses all annotations if not specified.
26+
(default: None)
27+
-S SUFFIX, --suffix SUFFIX
28+
The suffix pattern to use for the generated sub-
29+
images, available placeholders:
30+
{X}|{Y}|{W}|{H}|{X0}|{Y0}|{X1}|{Y1}|{INDEX} (default:
31+
-{INDEX})
32+
```

src/idc/imgaug/filter/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
from ._linear_contrast import LinearContrast
1414
from ._meta_sub_images import MetaSubImages
1515
from ._resize import Resize
16+
from ._roi_images import RegionOfInterestImages
1617
from ._rotate import Rotate
1718
from ._scale import Scale
1819
from ._sub_images import SubImages
1920
from ._sub_images_utils import (parse_regions, new_from_template, extract_regions, transfer_region, prune_annotations, region_filename,
2021
PLACEHOLDERS, REGION_SORTING, REGION_SORTING_XY, REGION_SORTING_YX, REGION_SORTING_NONE, DEFAULT_SUFFIX,
21-
Region, generate_regions, regions_to_string)
22+
Region, generate_regions, regions_to_string, locatedobject_to_region, locatedobject_to_xyxy)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import argparse
2+
from typing import List
3+
4+
from idc.api import ObjectDetectionData, flatten_list, make_list
5+
from idc.imgaug.filter._sub_images_utils import PLACEHOLDERS, DEFAULT_SUFFIX, \
6+
extract_regions, locatedobject_to_xyxy
7+
from seppl.io import Filter
8+
from wai.logging import LOGGING_WARNING
9+
10+
11+
class RegionOfInterestImages(Filter):
12+
"""
13+
Extracts sub-images using the bbox of all the annotations that have matching labels.
14+
"""
15+
16+
def __init__(self, labels: List[str] = None, suffix: str = DEFAULT_SUFFIX,
17+
logger_name: str = None, logging_level: str = LOGGING_WARNING):
18+
"""
19+
Initializes the filter.
20+
21+
:param labels: the label(s) to extract, extracts all if None or the list is empty
22+
:type labels: list
23+
:param suffix: the suffix pattern to use for the generated sub-images (with placeholders)
24+
:type suffix: str
25+
:param logger_name: the name to use for the logger
26+
:type logger_name: str
27+
:param logging_level: the logging level to use
28+
:type logging_level: str
29+
"""
30+
super().__init__(logger_name=logger_name, logging_level=logging_level)
31+
self.labels = labels
32+
self.suffix = suffix
33+
34+
def name(self) -> str:
35+
"""
36+
Returns the name of the handler, used as sub-command.
37+
38+
:return: the name
39+
:rtype: str
40+
"""
41+
return "roi-images"
42+
43+
def description(self) -> str:
44+
"""
45+
Returns a description of the filter.
46+
47+
:return: the description
48+
:rtype: str
49+
"""
50+
return "Extracts sub-images using the bbox of all the object detection annotations that have matching labels. If no labels are specified, all annotations are extracted."
51+
52+
def accepts(self) -> List:
53+
"""
54+
Returns the list of classes that are accepted.
55+
56+
:return: the list of classes
57+
:rtype: list
58+
"""
59+
return [ObjectDetectionData]
60+
61+
def generates(self) -> List:
62+
"""
63+
Returns the list of classes that get produced.
64+
65+
:return: the list of classes
66+
:rtype: list
67+
"""
68+
return [ObjectDetectionData]
69+
70+
def _create_argparser(self) -> argparse.ArgumentParser:
71+
"""
72+
Creates an argument parser. Derived classes need to fill in the options.
73+
74+
:return: the parser
75+
:rtype: argparse.ArgumentParser
76+
"""
77+
parser = super()._create_argparser()
78+
parser.add_argument("--labels", type=str, default=None, help="The label(s) of the annotations to forward as sub-images, uses all annotations if not specified.", required=False, nargs="*")
79+
parser.add_argument("-S", "--suffix", type=str, default=DEFAULT_SUFFIX, help="The suffix pattern to use for the generated sub-images, available placeholders: " + "|".join(PLACEHOLDERS), required=False)
80+
return parser
81+
82+
def _apply_args(self, ns: argparse.Namespace):
83+
"""
84+
Initializes the object with the arguments of the parsed namespace.
85+
86+
:param ns: the parsed arguments
87+
:type ns: argparse.Namespace
88+
"""
89+
super()._apply_args(ns)
90+
self.labels = ns.labels
91+
self.suffix = ns.suffix
92+
93+
def initialize(self):
94+
"""
95+
Initializes the processing, e.g., for opening files or databases.
96+
"""
97+
super().initialize()
98+
if (self.labels is not None) and (len(self.labels) == 0):
99+
self.labels = None
100+
if self.suffix is None:
101+
self.suffix = DEFAULT_SUFFIX
102+
103+
def _do_process(self, data):
104+
"""
105+
Processes the data record(s).
106+
107+
:param data: the record(s) to process
108+
:return: the potentially updated record(s)
109+
"""
110+
result = []
111+
labels = None
112+
if self.labels is not None:
113+
labels = set(self.labels)
114+
115+
for item in make_list(data):
116+
ann = item.get_absolute()
117+
if ann is None:
118+
self.logger().warning("Failed to obtain absolute annotations!")
119+
result.append(item)
120+
continue
121+
122+
# determines bboxes to extract
123+
lobjs = []
124+
xyxys = []
125+
for o in ann:
126+
extract = (labels is None) or (("type" in o.metadata) and (o.metadata["type"] in labels))
127+
if not extract:
128+
continue
129+
lobjs.append(o)
130+
xyxys.append(locatedobject_to_xyxy(o))
131+
132+
sub_items = extract_regions(item, lobjs, xyxys, self.suffix,
133+
False, False, self.logger())
134+
# failed to process?
135+
if sub_items is None:
136+
result.append(item)
137+
else:
138+
for _, sub_item, _ in sub_items:
139+
result.append(sub_item)
140+
141+
return flatten_list(result)

src/idc/imgaug/filter/_sub_images_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,3 +553,27 @@ def regions_to_string(regions: List[Region], one_based: bool = False, logger=Non
553553
else:
554554
regions_str += " %d,%d,%d,%d" % (region.x, region.y, region.w, region.h)
555555
return regions_str.strip()
556+
557+
558+
def locatedobject_to_region(lobj: LocatedObject) -> Region:
559+
"""
560+
Converts a LocatedObject instance into a Region object.
561+
562+
:param lobj: the LocatedObject instance to convert
563+
:type lobj: LocatedObject
564+
:return: the Region object
565+
:rtype: Region
566+
"""
567+
return Region(x=lobj.x, y=lobj.y, w=lobj.width, h=lobj.height)
568+
569+
570+
def locatedobject_to_xyxy(lobj: LocatedObject) -> Tuple[int, int, int, int]:
571+
"""
572+
Converts a LocatedObject instance into a tuple of x0,y0,x1,y1.
573+
574+
:param lobj: the LocatedObject instance to convert
575+
:type lobj: LocatedObject
576+
:return: the tuple of the coordinates
577+
:rtype: tuple
578+
"""
579+
return lobj.x, lobj.y, lobj.x + lobj.width - 1, lobj.y + lobj.height - 1

0 commit comments

Comments
 (0)