2Image annotation from a pixel-to-primitive map.
4These functions turn a per-pixel record of which Context primitive is visible, together with
5labels stored as primitive or object data, into bounding boxes (Ultralytics YOLO format) and
6segmentation masks (COCO JSON format). They do not depend on how the image was produced, so any
7renderer that can say which primitive is in each pixel writes the same annotations for the same
8labeled Context. The radiation and visualizer plug-ins use the same routines for their own
9annotation methods (:meth:`pyhelios.RadiationModel.writeImageBoundingBoxes`,
10:meth:`pyhelios.Visualizer.writeImageBoundingBoxes`, and their segmentation-mask counterparts).
12**The pixel-to-primitive map** is a sequence of ``width * height`` unsigned integers, row-major,
13with row 0 at the TOP of the image. Each element is the UUID of the primitive visible in that
14pixel **plus one**; 0 means no primitive is visible. A nested list, a flat list or a NumPy array
15of either shape is accepted. :meth:`pyhelios.Visualizer.getPixelUUIDs` returns a map in exactly
18**Labels** are primitive (or, with ``use_object_data=True``, object) data of type ``uint`` or
19``int``. Every primitive carrying the same value of the same data label belongs to one object.
20Primitives without the label are not annotated.
23 >>> from pyhelios import Context, annotation
24 >>> from pyhelios.types import int2
25 >>> # pixel_uuids, width, height = visualizer.getPixelUUIDs()
26 >>> boxes = annotation.labelBoundingBoxes(context, pixel_uuids, int2(width, height),
28 >>> annotation.writeLabelBoundingBoxes(context, pixel_uuids, int2(width, height),
29 ... "leaf_id", 0, image_file="scene.jpeg")
33from dataclasses
import dataclass
34from typing
import Dict, List, Optional, Sequence, Tuple, Union
38from .Context
import Context
39from .wrappers
import UContextWrapper
as context_wrapper
40from .wrappers.DataTypes
import int2, vec2
45 "writeLabelBoundingBoxes",
47 "writeLabelSegmentationMasks",
53 """A bounding box in the normalized form of the YOLO annotation format.
56 class_id: Class index of the object.
57 center: Center of the box, normalized to [0,1] by the image dimensions, origin at the top-left.
58 size: Width and height of the box, normalized to [0,1] by the image dimensions.
66 if not isinstance(context, Context):
67 raise ValueError(f
"{func_name}: context must be a Context, got {type(context).__name__}")
68 context._check_context_available()
72 if not isinstance(resolution, int2):
73 raise ValueError(f
"{func_name}: resolution must be an int2, got {type(resolution).__name__}")
74 if resolution.x <= 0
or resolution.y <= 0:
75 raise ValueError(f
"{func_name}: resolution must be positive, got ({resolution.x}, {resolution.y})")
76 return int(resolution.x), int(resolution.y)
79def _check_pixel_map(pixel_uuids, width: int, height: int, func_name: str) -> np.ndarray:
80 if isinstance(pixel_uuids, (str, bytes))
or not isinstance(pixel_uuids, (Sequence, np.ndarray)):
81 raise TypeError(f
"{func_name}: pixel_uuids must be a list or NumPy array of integers, "
82 f
"got {type(pixel_uuids).__name__}")
83 arr = np.asarray(pixel_uuids)
84 if arr.size > 0
and arr.dtype.kind
not in "iu":
85 raise TypeError(f
"{func_name}: pixel_uuids must hold integers (UUID + 1 per pixel, 0 for none), "
86 f
"got dtype {arr.dtype}")
87 if arr.size != width * height:
88 raise ValueError(f
"{func_name}: the pixel-to-primitive map has {arr.size} elements, which does not "
89 f
"match the image resolution of {width} x {height} ({width * height} pixels)")
90 if arr.size > 0
and (arr.min() < 0
or arr.max() > 0xFFFFFFFF):
91 raise ValueError(f
"{func_name}: pixel_uuids values must be in [0, 2^32 - 1] (UUID + 1 per pixel, 0 for none)")
92 return arr.reshape(-1)
95def _check_labels(data_labels, class_ids, func_name: str) -> Tuple[List[str], List[int]]:
96 if isinstance(data_labels, str):
97 if not isinstance(class_ids, numbers.Integral)
or isinstance(class_ids, bool):
98 raise TypeError(f
"{func_name}: for a single data label, class_ids must be an integer, "
99 f
"got {type(class_ids).__name__}")
100 data_labels, class_ids = [data_labels], [class_ids]
101 elif isinstance(data_labels, (list, tuple)):
102 if not isinstance(class_ids, (list, tuple)):
103 raise TypeError(f
"{func_name}: for a list of data labels, class_ids must be a list of integers, "
104 f
"got {type(class_ids).__name__}")
105 if len(data_labels) != len(class_ids):
106 raise ValueError(f
"{func_name}: data_labels and class_ids must have the same length "
107 f
"({len(data_labels)} != {len(class_ids)})")
108 if len(data_labels) == 0:
109 raise ValueError(f
"{func_name}: at least one data label is required")
111 raise TypeError(f
"{func_name}: data_labels must be a string or a list of strings, "
112 f
"got {type(data_labels).__name__}")
113 for label
in data_labels:
114 if not isinstance(label, str)
or not label.strip():
115 raise TypeError(f
"{func_name}: data labels must be non-empty strings, got {label!r}")
116 for cid
in class_ids:
117 if not isinstance(cid, numbers.Integral)
or isinstance(cid, bool)
or cid < 0:
118 raise ValueError(f
"{func_name}: class IDs must be non-negative integers, got {cid!r}")
119 return list(data_labels), [int(c)
for c
in class_ids]
123 if not isinstance(value, str)
or not value.strip():
124 raise TypeError(f
"{func_name}: {name} must be a non-empty string")
129 data_labels: Union[str, List[str]], class_ids: Union[int, List[int]],
130 use_object_data: bool =
False) -> List[YOLOBox]:
132 Compute a bounding box for each labeled object visible in a pixel-to-primitive map.
134 One box is produced per distinct pair of class index and label value. A box encloses whole
135 every pixel in which the object is visible: it runs from the left edge of the first pixel
136 column to the right edge of the last, and from the top edge of the first pixel row to the
137 bottom edge of the last, so an object visible in a single pixel has a box one pixel in size.
140 context: Context holding the primitives named by the map and their label data.
141 pixel_uuids: Pixel-to-primitive map (UUID + 1 per pixel, 0 for none), row-major with
142 row 0 at the top of the image.
143 resolution: Image dimensions in pixels, as ``int2(width, height)``.
144 data_labels: Name of a primitive or object data label, or a list of them.
145 class_ids: Class index for the objects of each data label (an int for a single label, or a
146 list in the same order as ``data_labels``).
147 use_object_data: If True, labels are read from the data of each primitive's parent
148 object; if False (default), from primitive data.
151 List of :class:`YOLOBox`, ordered by class index and then label value.
154 ValueError: If ``context`` is not a Context, ``resolution`` is not an int2, or the map
155 does not have ``width * height`` elements.
156 TypeError: If the labels, class IDs or map have the wrong type.
157 NotImplementedError: If the native library predates helios-core v1.3.91.
159 func_name =
"labelBoundingBoxes"
163 labels, ids =
_check_labels(data_labels, class_ids, func_name)
164 boxes = context_wrapper.annotationLabelBoundingBoxesWrapper(
165 context.getNativePtr(), pixels, width, height, labels, ids, bool(use_object_data))
166 return [
YOLOBox(class_id=cid, center=
vec2(cx, cy), size=
vec2(w, h))
for cid, cx, cy, w, h
in boxes]
170 data_labels: Union[str, List[str]], class_ids: Union[int, List[int]],
171 image_file: str, classes_txt_file: str =
"classes.txt",
172 image_path: str =
"./", use_object_data: bool =
False) ->
None:
174 Write YOLO bounding boxes and a class file for the labeled objects visible in a pixel-to-primitive map.
176 The boxes (see :func:`labelBoundingBoxes`) are written to a file named after the image, with
177 its extension replaced by ``.txt``, in the directory ``image_path``. The class file holds one
178 ``class_id data_label`` pair per line.
181 context: Context holding the primitives named by the map and their label data.
182 pixel_uuids: Pixel-to-primitive map (UUID + 1 per pixel, 0 for none), row-major with
183 row 0 at the top of the image.
184 resolution: Image dimensions in pixels, as ``int2(width, height)``.
185 data_labels: Name of a primitive or object data label, or a list of them.
186 class_ids: Class index for the objects of each data label.
187 image_file: Name of the image these boxes annotate. Only its stem is used, to name the
188 output file; the image itself need not exist.
189 classes_txt_file: Name of the class file to write within ``image_path`` (default "classes.txt").
190 image_path: Directory to write the output files to (default: current directory).
191 use_object_data: If True, labels are read from object data; if False (default), from primitive data.
194 ValueError: On invalid arguments (see :func:`labelBoundingBoxes`).
195 HeliosRuntimeError: If ``image_path`` is not a writable directory or a file cannot be written.
196 NotImplementedError: If the native library predates helios-core v1.3.91.
198 func_name =
"writeLabelBoundingBoxes"
202 labels, ids =
_check_labels(data_labels, class_ids, func_name)
205 if not isinstance(image_path, str):
206 raise TypeError(f
"{func_name}: image_path must be a string")
207 context_wrapper.annotationWriteLabelBoundingBoxesWrapper(
208 context.getNativePtr(), pixels, width, height, labels, ids, image_file, classes_txt_file,
209 image_path, bool(use_object_data))
212def labelMasks(context: Context, pixel_uuids, resolution: int2, data_label: str,
213 use_object_data: bool =
False) -> Dict[int, np.ndarray]:
215 Build a binary mask for each distinct value of a data label visible in a pixel-to-primitive map.
218 context: Context holding the primitives named by the map and their label data.
219 pixel_uuids: Pixel-to-primitive map (UUID + 1 per pixel, 0 for none), row-major with
220 row 0 at the top of the image.
221 resolution: Image dimensions in pixels, as ``int2(width, height)``.
222 data_label: Name of the primitive or object data label, of type ``uint`` or ``int``.
223 use_object_data: If True, the label is read from object data; if False (default), from primitive data.
226 Dict mapping each label value to a boolean NumPy array of shape ``(height, width)``,
227 indexed ``[row, column]`` with row 0 at the top of the image.
230 ValueError: On invalid arguments (see :func:`labelBoundingBoxes`).
231 NotImplementedError: If the native library predates helios-core v1.3.91.
233 func_name =
"labelMasks"
237 if not isinstance(data_label, str)
or not data_label.strip():
238 raise TypeError(f
"{func_name}: data_label must be a non-empty string")
239 values, masks = context_wrapper.annotationLabelMasksWrapper(
240 context.getNativePtr(), pixels, width, height, data_label, bool(use_object_data))
241 return {value: masks[i]
for i, value
in enumerate(values)}
245 data_labels: Union[str, List[str]], class_ids: Union[int, List[int]],
246 json_filename: str, image_file: str,
247 data_attribute_labels: Optional[List[str]] =
None,
248 append_file: bool =
False, use_object_data: bool =
False) ->
None:
250 Write COCO JSON segmentation masks for the labeled objects visible in a pixel-to-primitive map.
252 Each connected region of pixels sharing one value of a data label becomes one annotation, so
253 an object split in two by an occluder yields two annotations. Pixels touching only at a corner
254 are connected. The ``bbox`` of an annotation is (x, y, width, height) in pixels, enclosing
255 whole every pixel of the region. A region of only one or two pixels is too small to outline
256 and is not written. Each data label becomes a COCO category.
259 context: Context holding the primitives named by the map and their label data.
260 pixel_uuids: Pixel-to-primitive map (UUID + 1 per pixel, 0 for none), row-major with
261 row 0 at the top of the image.
262 resolution: Image dimensions in pixels, as ``int2(width, height)``.
263 data_labels: Name of a primitive or object data label, or a list of them.
264 class_ids: COCO category ID for each data label.
265 json_filename: Path of the COCO JSON file to write. ".json" is appended if missing.
266 image_file: Path of the image these masks annotate, which must exist.
267 data_attribute_labels: Optional primitive or object data labels of type int, uint, float
268 or double whose mean over the pixels of each mask is written to that annotation's
269 "attributes". A label that exists as both is read from primitive data; labels that do
270 not exist are ignored.
271 append_file: If True, annotations are added to an existing file at ``json_filename``;
272 if False (default), a new file is written.
273 use_object_data: If True, labels are read from object data; if False (default), from primitive data.
276 ValueError: On invalid arguments (see :func:`labelBoundingBoxes`).
277 HeliosRuntimeError: If ``image_file`` does not exist or the JSON file cannot be written.
278 NotImplementedError: If the native library predates helios-core v1.3.91.
280 func_name =
"writeLabelSegmentationMasks"
284 labels, ids =
_check_labels(data_labels, class_ids, func_name)
287 if data_attribute_labels
is None:
288 data_attribute_labels = []
289 elif isinstance(data_attribute_labels, str)
or not isinstance(data_attribute_labels, (list, tuple)):
290 raise TypeError(f
"{func_name}: data_attribute_labels must be a list of strings")
291 if not all(isinstance(label, str)
for label
in data_attribute_labels):
292 raise TypeError(f
"{func_name}: data_attribute_labels must be a list of strings")
293 context_wrapper.annotationWriteLabelSegmentationMasksWrapper(
294 context.getNativePtr(), pixels, width, height, labels, ids, json_filename, image_file,
295 list(data_attribute_labels), bool(append_file), bool(use_object_data))
A bounding box in the normalized form of the YOLO annotation format.
None writeLabelBoundingBoxes(Context context, pixel_uuids, int2 resolution, Union[str, List[str]] data_labels, Union[int, List[int]] class_ids, str image_file, str classes_txt_file="classes.txt", str image_path="./", bool use_object_data=False)
Write YOLO bounding boxes and a class file for the labeled objects visible in a pixel-to-primitive ma...
Tuple[int, int] _check_resolution(resolution, str func_name)
str _check_filename(value, str name, str func_name)
Tuple[List[str], List[int]] _check_labels(data_labels, class_ids, str func_name)
Dict[int, np.ndarray] labelMasks(Context context, pixel_uuids, int2 resolution, str data_label, bool use_object_data=False)
Build a binary mask for each distinct value of a data label visible in a pixel-to-primitive map.
np.ndarray _check_pixel_map(pixel_uuids, int width, int height, str func_name)
List[YOLOBox] labelBoundingBoxes(Context context, pixel_uuids, int2 resolution, Union[str, List[str]] data_labels, Union[int, List[int]] class_ids, bool use_object_data=False)
Compute a bounding box for each labeled object visible in a pixel-to-primitive map.
None writeLabelSegmentationMasks(Context context, pixel_uuids, int2 resolution, Union[str, List[str]] data_labels, Union[int, List[int]] class_ids, str json_filename, str image_file, Optional[List[str]] data_attribute_labels=None, bool append_file=False, bool use_object_data=False)
Write COCO JSON segmentation masks for the labeled objects visible in a pixel-to-primitive map.
None _check_context(context, str func_name)