Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 17 additions & 9 deletions dimos/perception/detection/type/detection2d/bbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,15 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember investigating this on my end, and concluded that weirdly, ROS vision messages does define class_id as string

https://github.com/ros-perception/vision_msgs/blob/ros2/vision_msgs/msg/ObjectHypothesis.msg

https://docs.ros.org/en/rolling/p/vision_msgs/msg/ObjectHypothesis.html

class_id = 0
confidence = 0.0
if ros_det.results:
hypothesis = ros_det.results[0].hypothesis
class_id = hypothesis.class_id
try:
class_id = int(hypothesis.class_id)
except (ValueError, TypeError):
class_id = 0
confidence = hypothesis.score

# Extract track_id
Expand All @@ -393,16 +397,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),
)
19 changes: 11 additions & 8 deletions dimos/perception/detection/type/detection2d/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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),
)

Expand Down
110 changes: 110 additions & 0 deletions dimos/perception/detection/type/detection2d/test_imageDetections2D.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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