From 59480cfbf20b509a25260894c6d7ac6aa83b796a Mon Sep 17 00:00:00 2001 From: Lev Date: Wed, 25 Feb 2026 23:56:29 +0900 Subject: [PATCH 1/3] fix(vision): handle class_id as string in LCM serialization --- .../detection/type/detection2d/bbox.py | 25 ++++++++++++------- .../detection/type/detection2d/point.py | 19 ++++++++------ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/dimos/perception/detection/type/detection2d/bbox.py b/dimos/perception/detection/type/detection2d/bbox.py index 32109dffd3..ec96a47fb8 100644 --- a/dimos/perception/detection/type/detection2d/bbox.py +++ b/dimos/perception/detection/type/detection2d/bbox.py @@ -367,11 +367,14 @@ def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Self: # typ bbox = (x1, y1, x2, y2) # Extract hypothesis info + # Note: LCM decodes class_id as str (LCM string type), convert back to int class_id = 0 confidence = 0.0 if ros_det.results: hypothesis = ros_det.results[0].hypothesis - class_id = hypothesis.class_id + class_id = ( + int(hypothesis.class_id) if str(hypothesis.class_id).lstrip("-").isdigit() else 0 + ) confidence = hypothesis.score # Extract track_id @@ -393,16 +396,20 @@ def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Self: # typ ) def to_ros_detection2d(self) -> ROSDetection2D: + # LCM ObjectHypothesis.class_id is a *string* type (see dimos_lcm/vision_msgs/ObjectHypothesis.py). + # Passing an int causes AttributeError: 'int' object has no attribute 'encode'. + results = [ + ObjectHypothesisWithPose( + ObjectHypothesis( + class_id=str(self.class_id), + score=self.confidence, + ) + ) + ] return ROSDetection2D( header=Header(self.ts, "camera_link"), bbox=self.to_ros_bbox(), - results=[ - ObjectHypothesisWithPose( - ObjectHypothesis( - class_id=self.class_id, - score=self.confidence, - ) - ) - ], + results=results, + results_length=len(results), id=str(self.track_id), ) diff --git a/dimos/perception/detection/type/detection2d/point.py b/dimos/perception/detection/type/detection2d/point.py index 216ec57b82..46fe72eca3 100644 --- a/dimos/perception/detection/type/detection2d/point.py +++ b/dimos/perception/detection/type/detection2d/point.py @@ -152,6 +152,15 @@ def to_image_annotations(self) -> ImageAnnotations: def to_ros_detection2d(self) -> ROSDetection2D: """Convert point to ROS Detection2D message (as zero-size bbox at point).""" + # LCM ObjectHypothesis.class_id is a *string* type. + results = [ + ObjectHypothesisWithPose( + ObjectHypothesis( + class_id=str(self.class_id), + score=self.confidence, + ) + ) + ] return ROSDetection2D( header=Header(self.ts, "camera_link"), bbox=BoundingBox2D( @@ -162,14 +171,8 @@ def to_ros_detection2d(self) -> ROSDetection2D: size_x=0.0, size_y=0.0, ), - results=[ - ObjectHypothesisWithPose( - ObjectHypothesis( - class_id=self.class_id, - score=self.confidence, - ) - ) - ], + results=results, + results_length=len(results), id=str(self.track_id), ) From 1c0ff7df5af065ac684a061104bf8c09977021bd Mon Sep 17 00:00:00 2001 From: Lev Date: Wed, 25 Feb 2026 23:56:36 +0900 Subject: [PATCH 2/3] test(vision): add unit tests for LCM class_id roundtrip serialization --- .../detection2d/test_imageDetections2D.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py index 83487d2c25..a4becdbab0 100644 --- a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py +++ b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py @@ -11,9 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import time +from unittest.mock import MagicMock + +from dimos_lcm.vision_msgs import Detection2DArray as LCMArray import pytest from dimos.perception.detection.type import ImageDetections2D +from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox def test_from_ros_detection2d_array(get_moment_2d) -> None: @@ -50,3 +55,108 @@ def test_from_ros_detection2d_array(get_moment_2d) -> None: print(f" Recovered bbox: {recovered_det.bbox}") print(f" Track ID: {recovered_det.track_id}") print(f" Confidence: {recovered_det.confidence:.3f}") + + +def _make_detection( + class_id: int = 0, + confidence: float = 0.9, + track_id: int = 1, + bbox: tuple[float, float, float, float] = (10.0, 20.0, 100.0, 200.0), +) -> Detection2DBBox: + """Create a Detection2DBBox with given attributes, using a mock Image.""" + img = MagicMock() + img.ts = time.time() + img.width = 640 + img.height = 480 + img.shape = (480, 640, 3) + img.crop.return_value = img + + return Detection2DBBox( + bbox=bbox, + track_id=track_id, + class_id=class_id, + confidence=confidence, + name=f"class_{class_id}", + ts=img.ts, + image=img, + ) + + +@pytest.mark.parametrize("class_id", [0, 1, 15, 79]) +def test_to_ros_detection2d_class_id_is_str_in_lcm(class_id: int) -> None: + """ + LCM ObjectHypothesis.class_id type is string, so + to_ros_detection2d() must encode class_id to string. + If int is passed, AttributeError: 'int' object has no attribute 'encode' will occur. + """ + det = _make_detection(class_id=class_id) + ros_det = det.to_ros_detection2d() + + assert ros_det.results_length == 1, "results_length must equal len(results)" + assert len(ros_det.results) == 1 + + lcm_class_id = ros_det.results[0].hypothesis.class_id + assert isinstance(lcm_class_id, str), ( + f"LCM class_id must be str, got {type(lcm_class_id).__name__}. " + "Passing int causes AttributeError in dimos_lcm _encode_one." + ) + assert lcm_class_id == str(class_id) + + +@pytest.mark.parametrize("class_id", [0, 1, 15, 79]) +def test_to_ros_detection2d_lcm_encode_does_not_crash(class_id: int) -> None: + """ + to_ros_detection2d() → Detection2DArray → lcm_encode() entire pipeline must not crash. + """ + det = _make_detection(class_id=class_id) + + ros_det = det.to_ros_detection2d() + array = LCMArray( + detections_length=1, + header=ros_det.header, + detections=[ros_det], + ) + + encoded = array.lcm_encode() + assert isinstance(encoded, bytes) + assert len(encoded) > 0 + + +@pytest.mark.parametrize("class_id", [0, 1, 15, 79]) +def test_lcm_roundtrip_class_id_preserved_as_int(class_id: int) -> None: + """ + Detection2DBBox → LCM serialization → LCM deserialization → from_ros_detection2d() restoration + class_id must be restored as the original int value. + + After LCM decoding, hypothesis.class_id is str, + so it must be converted to int() inside from_ros_detection2d(). + """ + det = _make_detection(class_id=class_id, confidence=0.87, track_id=42) + img = det.image + + # Encode + ros_det = det.to_ros_detection2d() + array = LCMArray( + detections_length=1, + header=ros_det.header, + detections=[ros_det], + ) + encoded = array.lcm_encode() + + # Decode + decoded_array = LCMArray.lcm_decode(encoded) + assert decoded_array.detections_length == 1 + decoded_det = decoded_array.detections[0] + + # After LCM decoding, class_id is str + assert isinstance(decoded_det.results[0].hypothesis.class_id, str) + + # from_ros_detection2d restoration → class_id must be int + recovered = Detection2DBBox.from_ros_detection2d(decoded_det, image=img) + assert isinstance(recovered.class_id, int), ( + "Recovered class_id must be int (Detection2DBBox.class_id: int). " + "from_ros_detection2d must convert str -> int." + ) + assert recovered.class_id == class_id + assert recovered.confidence == pytest.approx(0.87, abs=0.01) + assert recovered.track_id == 42 From 53677b084b8955569ff2912a38936bb7e445565c Mon Sep 17 00:00:00 2001 From: jongmoon-konglabs Date: Thu, 26 Feb 2026 00:41:25 +0900 Subject: [PATCH 3/3] refactor(vision): simplify class_id parsing with try-except in LCM decoding --- dimos/perception/detection/type/detection2d/bbox.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dimos/perception/detection/type/detection2d/bbox.py b/dimos/perception/detection/type/detection2d/bbox.py index ec96a47fb8..2168dd4f4c 100644 --- a/dimos/perception/detection/type/detection2d/bbox.py +++ b/dimos/perception/detection/type/detection2d/bbox.py @@ -372,9 +372,10 @@ def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Self: # typ confidence = 0.0 if ros_det.results: hypothesis = ros_det.results[0].hypothesis - class_id = ( - int(hypothesis.class_id) if str(hypothesis.class_id).lstrip("-").isdigit() else 0 - ) + try: + class_id = int(hypothesis.class_id) + except (ValueError, TypeError): + class_id = 0 confidence = hypothesis.score # Extract track_id