|
| 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