Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
607d093
extract-compute-emit pattern for classificationparser
dtronmans Jun 29, 2026
0f4bd7e
extract-compute-emit for all parsers in dai nodes
dtronmans Jun 30, 2026
a34c3f0
XFeatStereoParser preserve old fallback behavior, restore the RF-DETR…
dtronmans Jul 1, 2026
0ab544b
pre-commit fix
dtronmans Jul 1, 2026
3f928e4
Address pr-relevant coderabbit concerns
dtronmans Jul 2, 2026
91b0c6f
Address pr-relevant coderabbit concerns
dtronmans Jul 2, 2026
6cd7bf3
fixes for unnecessary defensive behavior, mention arguments explicitly
dtronmans Jul 6, 2026
0b6c383
YOLOComputeInputs dataclass, compute() no longer takes **kwargs
dtronmans Jul 6, 2026
dea817d
merge conflict fix
dtronmans Jul 6, 2026
692acdb
stride resolving behavior re-implemented after merge conflict for non…
dtronmans Jul 8, 2026
977a346
smart merge of 304 into 302
dtronmans Jul 8, 2026
26b22c6
fix incorrect shift from new semantic segmentation changes
dtronmans Jul 8, 2026
07eb5f7
CodeRabbitAI + miscellaneous fixes
dtronmans Jul 9, 2026
78c578a
normalized xyxy corners before converting to xywh
dtronmans Jul 13, 2026
72864ec
remove unnecessarily defensive step: ppdet is explicitly extracted as…
dtronmans Jul 15, 2026
207653b
fix merge conflicts: favor depthai 3.8.0 and integrate segmentation l…
dtronmans Jul 15, 2026
21ea2a5
Merge branch 'feat/extract-compute-emit' into feat/misc-fixes
dtronmans Jul 15, 2026
5552b7b
precommit lint
dtronmans Jul 15, 2026
2e20798
precommit fix
dtronmans Jul 15, 2026
b6b396f
Merge branch 'feat/extract-compute-emit' into feat/misc-fixes
dtronmans Jul 15, 2026
422a14f
add instance_masks, fix merge conflcits
dtronmans Jul 15, 2026
f7230af
remove no longer needed instance_masks
dtronmans Jul 15, 2026
802ac04
accurate number of maximum instance masks
dtronmans Jul 15, 2026
bfa49c5
correctly account for [1,H,W,1] output format
dtronmans Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions depthai_nodes/node/parsers/fastsam.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ def run(self):
protos_output,
protos_len,
) = self.extract(output)
results_masks = self.compute(
results_masks, mask_count = self.compute(
outputs_values,
masks_outputs_values,
protos_output,
Expand All @@ -321,7 +321,7 @@ def run(self):
point_label=self.point_label,
bbox=self.bbox,
)
self.emit(output, results_masks)
self.emit(output, results_masks, mask_count)

def extract(
self, output: dai.NNData
Expand Down Expand Up @@ -357,7 +357,7 @@ def compute(
points: tuple[int, int] | None,
point_label: int | None,
bbox: tuple[int, int, int, int] | None,
) -> np.ndarray:
) -> tuple[np.ndarray, int]:
return compute_fastsam_mask(
outputs_values,
masks_outputs_values,
Expand All @@ -373,16 +373,16 @@ def compute(
bbox=bbox,
)

def emit(self, output: dai.NNData, results_masks: np.ndarray) -> None:
def emit(
self, output: dai.NNData, results_masks: np.ndarray, mask_count: int
) -> None:
segmentation_message = create_segmentation_message(results_masks)
transformation = output.getTransformation()
if transformation is not None:
segmentation_message.setTransformation(transformation)
segmentation_message.setTimestamp(output.getTimestamp())
segmentation_message.setSequenceNum(output.getSequenceNum())
segmentation_message.setTimestampDevice(output.getTimestampDevice())
self._logger.debug(
f"Created segmentation message with {len(results_masks)} masks"
)
self._logger.debug(f"Created segmentation message with {mask_count} masks")
self.out.send(segmentation_message)
self._logger.debug("Segmentation message sent successfully")
16 changes: 10 additions & 6 deletions depthai_nodes/node/parsers/scrfd.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,16 @@ def __init__(
self.num_anchors = num_anchors
self.input_size = input_size
self.label_names = ["Face"]
self._cached_anchors = compute_anchor_centers(
self.feat_stride_fpn, self.input_size, self.num_anchors
)
self._refresh_cached_anchors()
self._logger.debug(
f"SCRFDParser initialized with output_layer_names={output_layer_names}, conf_threshold={conf_threshold}, iou_threshold={iou_threshold}, max_det={max_det}, input_size={input_size}, feat_stride_fpn={feat_stride_fpn}, num_anchors={num_anchors}"
)

def _refresh_cached_anchors(self) -> None:
self._cached_anchors = compute_anchor_centers(
self.feat_stride_fpn, self.input_size, self.num_anchors
)

def setOutputLayerNames(self, output_layer_names: list[str]) -> None:
"""Sets the output layer name(s) for the parser.

Expand All @@ -105,6 +108,7 @@ def setInputSize(self, input_size: tuple[int, int]) -> None:
if not all(isinstance(size, int) for size in input_size):
raise ValueError("Input size must be a tuple of integers.")
self.input_size = input_size
self._refresh_cached_anchors()
self._logger.debug(f"Input size set to {self.input_size}")

def setFeatStrideFPN(self, feat_stride_fpn: list[int]) -> None:
Expand All @@ -118,6 +122,7 @@ def setFeatStrideFPN(self, feat_stride_fpn: list[int]) -> None:
if not all(isinstance(stride, int) for stride in feat_stride_fpn):
raise ValueError("Feature stride must be a list of integers.")
self.feat_stride_fpn = feat_stride_fpn
self._refresh_cached_anchors()
self._logger.debug(f"Feature stride set to {self.feat_stride_fpn}")

def setNumAnchors(self, num_anchors: int) -> None:
Expand All @@ -129,6 +134,7 @@ def setNumAnchors(self, num_anchors: int) -> None:
if not isinstance(num_anchors, int):
raise ValueError("Number of anchors must be an integer.")
self.num_anchors = num_anchors
self._refresh_cached_anchors()
self._logger.debug(f"Number of anchors set to {self.num_anchors}")

def build(
Expand Down Expand Up @@ -160,9 +166,7 @@ def build(
self.output_layer_names = output_layers
self.feat_stride_fpn = head_config.get("feat_stride_fpn", self.feat_stride_fpn)
self.num_anchors = head_config.get("num_anchors", self.num_anchors)
self._cached_anchors = compute_anchor_centers(
self.feat_stride_fpn, self.input_size, self.num_anchors
)
self._refresh_cached_anchors()

self._logger.debug(
f"SCRFDParser built with output_layer_names={self.output_layer_names}, feat_stride_fpn={self.feat_stride_fpn}, num_anchors={self.num_anchors}"
Expand Down
15 changes: 14 additions & 1 deletion depthai_nodes/node/parsers/segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ class segmentation model. Default is False. If True, the parser will use
self.output_layer_name = output_layer_name
self.classes_in_one_layer = classes_in_one_layer
self.background_class = background_class
self.class_names = None
self.n_classes = 0
self._background_class_ignored_warning_sent = False
self._logger.debug(
"SegmentationParser initialized with "
Expand Down Expand Up @@ -106,6 +108,13 @@ def _warn_if_background_class_ignored(self) -> None:
)
self._background_class_ignored_warning_sent = True

def _get_logged_class_count(self, class_map: np.ndarray) -> int:
if self.class_names is not None:
return len(self.class_names)
if self.n_classes > 0:
return self.n_classes
return int(np.unique(class_map[class_map != 255]).size)

def build(
self,
head_config: dict[str, Any],
Expand All @@ -130,6 +139,10 @@ def build(
self.background_class = head_config.get(
"background_class", self.background_class
)
self.class_names = head_config.get("classes", self.class_names)
self.n_classes = head_config.get("n_classes", self.n_classes)
if self.n_classes == 0 and self.class_names is not None:
self.n_classes = len(self.class_names)

self._logger.debug(
"SegmentationParser built with "
Expand Down Expand Up @@ -196,7 +209,7 @@ def emit(self, output: dai.NNData, class_map: np.ndarray) -> None:
mask_message.setTransformation(transformation)

self._logger.debug(
f"Created segmentation message with {class_map.shape[0]} classes"
f"Created segmentation message with {self._get_logged_class_count(class_map)} classes"
)
self.out.send(mask_message)
self._logger.debug("Segmentation message sent successfully")
7 changes: 5 additions & 2 deletions depthai_nodes/node/parsers/utils/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ def compute_detection_outputs(
max_det: int,
) -> tuple[np.ndarray, np.ndarray]:
"""Filter detection outputs and convert boxes to center-width-height format."""
indices = nms_cv2(bboxes, scores, conf_threshold, iou_threshold, max_det)
nms_bboxes = np.column_stack((bboxes[:, :2], bboxes[:, 2:] - bboxes[:, :2]))
indices = np.asarray(
nms_cv2(nms_bboxes, scores, conf_threshold, iou_threshold, max_det)
).reshape(-1)
Comment thread
klemen1999 marked this conversation as resolved.

if len(indices) == 0:
if indices.size == 0:
return np.array([]), np.array([])

filtered_bboxes = xyxy_to_xywh(bboxes[indices])
Expand Down
9 changes: 6 additions & 3 deletions depthai_nodes/node/parsers/utils/fastsam.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,8 @@ def compute_fastsam_mask(
points: tuple[int, int] | None,
point_label: int | None,
bbox: tuple[int, int, int, int] | None,
) -> np.ndarray:
"""Decode FastSAM outputs into a merged segmentation mask."""
) -> tuple[np.ndarray, int]:
"""Decode FastSAM outputs into a merged segmentation mask and mask count."""
width = outputs_values[0].shape[3] * 8
height = outputs_values[0].shape[2] * 8
input_shape = (width, height)
Expand Down Expand Up @@ -462,5 +462,8 @@ def compute_fastsam_mask(

if len(results_masks) == 0:
results_masks = np.full((1, height, width), -1, dtype=np.int16)
results_mask_count = 0
else:
results_mask_count = len(results_masks)

return merge_masks(results_masks)
return merge_masks(results_masks), results_mask_count
2 changes: 1 addition & 1 deletion depthai_nodes/node/parsers/utils/image_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def compute_image_output(output_image: np.ndarray) -> np.ndarray:
"""Convert a model image output tensor into an image array."""
image = np.asarray(output_image)

if image.shape[0] == 1:
if image.ndim == 4 and image.shape[0] == 1:
image = image[0]

if image.ndim != 3:
Expand Down
14 changes: 12 additions & 2 deletions depthai_nodes/node/parsers/utils/map_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
def compute_map_output(map_tensor: np.ndarray) -> np.ndarray:
"""Return the model map output without the batch dimension."""
map_output = np.asarray(map_tensor)
if map_output.shape[0] == 1:

while map_output.ndim > 2 and map_output.shape[0] == 1:
map_output = map_output[0]
return map_output

if map_output.ndim == 2:
return map_output

if map_output.ndim == 3 and map_output.shape[-1] == 1:
return map_output[:, :, 0]

raise ValueError(
f"Expected HW, NHW, or HWN with singleton N; got {map_output.shape}."
)
9 changes: 8 additions & 1 deletion depthai_nodes/node/parsers/utils/yolo.py
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,12 @@ def decode_yolo_output(
@return: NMS output.
@rtype: np.ndarray
"""
if len(strides) != len(yolo_outputs):
raise ValueError(
"Number of `strides` must match number of YOLO outputs. "
f"Got {len(strides)} strides for {len(yolo_outputs)} outputs."
)

num_outputs = num_classes + 5

# 1. Parse and concatenate all head outputs efficiently
Expand Down Expand Up @@ -698,7 +704,8 @@ def compute_yolo_detections(
max_instance_id = np.iinfo(np.int16).max
if results.shape[0] > max_instance_id + 1:
raise ValueError(
"YOLO segmentation can encode at most 32768 instances in SegmentationMask."
"YOLO segmentation can encode at most "
f"{max_instance_id + 1} instance masks in SegmentationMask."
)
final_mask = np.full(input_shape, -1, dtype=np.int16)

Expand Down
Loading