Skip to content

Commit 8e44f3e

Browse files
committed
added find-contours filter (imgseg -> objdet)
1 parent 54fa1c7 commit 8e44f3e

6 files changed

Lines changed: 207 additions & 0 deletions

File tree

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
0.0.3 (????-??-??)
5+
------------------
6+
7+
- added `find-contours` filter for turning blobs in image segmentation annotations into object detection polygons.
8+
9+
410
0.0.2 (2024-06-13)
511
------------------
612

plugins/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* [clip-grayscale](clip-grayscale.md)
55
* [crop](crop.md)
66
* [enhance](enhance.md)
7+
* [find-contours](find-contours.md)
78
* [flip](flip.md)
89
* [gaussian-blur](gaussian-blur.md)
910
* [hsl-grayscale](hsl-grayscale.md)

plugins/find-contours.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# find-contours
2+
3+
* accepts: idc.api.ImageSegmentationData
4+
* generates: idc.api.ObjectDetectionData
5+
6+
Detects blobs in the annotations of the image segmentation data and turns them into object detection polygons.
7+
8+
```
9+
usage: find-contours [-h] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
10+
[-N LOGGER_NAME] [-t MASK_THRESHOLD] [-n MASK_NTH]
11+
[-m VIEW_MARGIN] [-f {low,high}]
12+
13+
Detects blobs in the annotations of the image segmentation data and turns them
14+
into object detection polygons.
15+
16+
optional arguments:
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+
-t MASK_THRESHOLD, --mask_threshold MASK_THRESHOLD
24+
The (lower) probability threshold for mask values in
25+
order to be considered part of the object (0-1).
26+
(default: 0.1)
27+
-n MASK_NTH, --mask_nth MASK_NTH
28+
The contour tracing can be slow for large masks, by
29+
using only every nth row/col, this can be sped up
30+
dramatically. (default: 1)
31+
-m VIEW_MARGIN, --view_margin VIEW_MARGIN
32+
The margin in pixels to enlarge the view with in each
33+
direction. (default: 5)
34+
-f {low,high}, --fully_connected {low,high}
35+
Whether regions of high or low values should be fully-
36+
connected at isthmuses. (default: low)
37+
```

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def _read(f):
3535
"imgaug>=0.4.0",
3636
"matplotlib",
3737
"Shapely",
38+
"simple-mask-utils==0.0.1"
3839
],
3940
version="0.0.2",
4041
author='Peter Reutemann',

src/idc/imgaug/filter/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ._clip_grayscale import ClipGrayscale
66
from ._crop import Crop
77
from ._enhance import Enhance, ENHANCEMENTS, ENHANCEMENT_CONTRAST, ENHANCEMENT_COLOR, ENHANCEMENT_SHARPNESS, ENHANCEMENT_BRIGHTNESS
8+
from ._find_contours import FindContours, CONNECTIVITY, CONNECTIVITY_LOW, CONNECTIVITY_HIGH
89
from ._flip import Flip
910
from ._gaussian_blur import GaussianBlur
1011
from ._hsl_grayscale import HSLGrayScale
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import argparse
2+
import numpy as np
3+
from typing import List
4+
5+
from seppl.io import Filter
6+
from wai.logging import LOGGING_WARNING
7+
from wai.common.geometry import Polygon, Point
8+
from wai.common.adams.imaging.locateobjects import LocatedObject, LocatedObjects
9+
from idc.api import ObjectDetectionData, ImageSegmentationData, make_list, flatten_list, LABEL_KEY
10+
from smu import mask_to_polygon, polygon_to_lists
11+
12+
13+
CONNECTIVITY_LOW = "low"
14+
CONNECTIVITY_HIGH = "high"
15+
CONNECTIVITY = [
16+
CONNECTIVITY_LOW,
17+
CONNECTIVITY_HIGH,
18+
]
19+
20+
21+
class FindContours(Filter):
22+
"""
23+
Detects blobs in the annotations of the image segmentation data and turns them into object detection polygons.
24+
"""
25+
26+
def __init__(self, mask_threshold: float = 0.1, mask_nth: int = 1,
27+
view_margin: int = 5, fully_connected: str = "low",
28+
logger_name: str = None, logging_level: str = LOGGING_WARNING):
29+
"""
30+
Initializes the filter.
31+
32+
:param mask_threshold: the (lower) probability threshold for mask values in order to be considered part of the object (0-1)
33+
:type mask_threshold: float
34+
:param mask_nth: the contour tracing can be slow for large masks, by using only every nth row/col, this can be sped up dramatically
35+
:type mask_nth: int
36+
:param view_margin: the margin in pixels to enlarge the view with in each direction
37+
:type view_margin: int
38+
:param fully_connected: whether regions of high or low values should be fully-connected at isthmuses
39+
:type fully_connected: str
40+
:param logger_name: the name to use for the logger
41+
:type logger_name: str
42+
:param logging_level: the logging level to use
43+
:type logging_level: str
44+
"""
45+
super().__init__(logger_name=logger_name, logging_level=logging_level)
46+
self.mask_threshold = mask_threshold
47+
self.mask_nth = mask_nth
48+
self.view_margin = view_margin
49+
self.fully_connected = fully_connected
50+
51+
def name(self) -> str:
52+
"""
53+
Returns the name of the handler, used as sub-command.
54+
55+
:return: the name
56+
:rtype: str
57+
"""
58+
return "find-contours"
59+
60+
def description(self) -> str:
61+
"""
62+
Returns a description of the filter.
63+
64+
:return: the description
65+
:rtype: str
66+
"""
67+
return "Detects blobs in the annotations of the image segmentation data and turns them into object detection polygons."
68+
69+
def accepts(self) -> List:
70+
"""
71+
Returns the list of classes that are accepted.
72+
73+
:return: the list of classes
74+
:rtype: list
75+
"""
76+
return [ImageSegmentationData]
77+
78+
def generates(self) -> List:
79+
"""
80+
Returns the list of classes that get produced.
81+
82+
:return: the list of classes
83+
:rtype: list
84+
"""
85+
return [ObjectDetectionData]
86+
87+
def _create_argparser(self) -> argparse.ArgumentParser:
88+
"""
89+
Creates an argument parser. Derived classes need to fill in the options.
90+
91+
:return: the parser
92+
:rtype: argparse.ArgumentParser
93+
"""
94+
parser = super()._create_argparser()
95+
parser.add_argument("-t", "--mask_threshold", type=float, help="The (lower) probability threshold for mask values in order to be considered part of the object (0-1).", default=0.1, required=False)
96+
parser.add_argument("-n", "--mask_nth", type=float, help="The contour tracing can be slow for large masks, by using only every nth row/col, this can be sped up dramatically.", default=1, required=False)
97+
parser.add_argument("-m", "--view_margin", type=int, help="The margin in pixels to enlarge the view with in each direction.", default=5, required=False)
98+
parser.add_argument("-f", "--fully_connected", choices=CONNECTIVITY, help="Whether regions of high or low values should be fully-connected at isthmuses.", default=CONNECTIVITY_LOW, required=False)
99+
return parser
100+
101+
def _apply_args(self, ns: argparse.Namespace):
102+
"""
103+
Initializes the object with the arguments of the parsed namespace.
104+
105+
:param ns: the parsed arguments
106+
:type ns: argparse.Namespace
107+
"""
108+
super()._apply_args(ns)
109+
self.mask_threshold = ns.mask_threshold
110+
self.mask_nth = ns.mask_nth
111+
self.view_margin = ns.view_margin
112+
self.fully_connected = ns.fully_connected
113+
114+
def initialize(self):
115+
"""
116+
Initializes the processing, e.g., for opening files or databases.
117+
"""
118+
super().initialize()
119+
if self.mask_threshold is None:
120+
self.mask_threshold = 0.1
121+
if self.mask_nth is None:
122+
self.mask_nth = 1
123+
if self.view_margin is None:
124+
self.view_margin = 5
125+
if self.fully_connected is None:
126+
self.fully_connected = CONNECTIVITY_LOW
127+
128+
def _do_process(self, data):
129+
"""
130+
Processes the data record(s).
131+
132+
:param data: the record(s) to process
133+
:return: the potentially updated record(s)
134+
"""
135+
result = []
136+
for item in make_list(data):
137+
objs = LocatedObjects()
138+
for i, label in enumerate(item.annotation.labels):
139+
if label not in item.annotation.layers:
140+
continue
141+
layer = item.annotation.layers[label]
142+
layer = np.where(layer == 255, 1, 0)
143+
polys = mask_to_polygon(layer, mask_threshold=self.mask_threshold, mask_nth=self.mask_nth,
144+
fully_connected=self.fully_connected)
145+
for poly in polys:
146+
px, py = polygon_to_lists(poly, swap_x_y=True, as_type="int")
147+
left = min(px)
148+
right = max(px)
149+
top = min(py)
150+
bottom = max(py)
151+
points = [Point(x, y) for x, y in zip(px, py)]
152+
polygon = Polygon(*points)
153+
obj = LocatedObject(left, top, right - left + 1, bottom - top + 1)
154+
obj.metadata[LABEL_KEY] = label
155+
obj.set_polygon(polygon)
156+
objs.append(obj)
157+
item_new = ObjectDetectionData(source=item.source, image_name=item.image_name, data=item._data,
158+
annotation=objs, metadata=item.get_metadata())
159+
result.append(item_new)
160+
161+
return flatten_list(result)

0 commit comments

Comments
 (0)