Repository: tryolabs/norfair

Issue:
Absolute paths drawing with centroids
We only allow drawing the absolute paths of bounding boxes.

To draw an absolute path with centroids points I need to do a trick simulating a bounding box duplicating the centroid coordinate.

Example:
```
centroid = np.array(
                [
                    [detection_as_xywh[0].item(), detection_as_xywh[1].item()],
                    [detection_as_xywh[0].item(), detection_as_xywh[1].item()],
                ]
            )
```

Discussion:

Patch:
diff --git a/norfair/tracker.py b/norfair/tracker.py
index 1a26b07..0a80b97 100644
--- a/norfair/tracker.py
+++ b/norfair/tracker.py
@@ -438,18 +438,14 @@ class TrackedObject:
         reid_hit_counter_max: Optional[int],
         abs_to_rel: Callable[[np.array], np.array],
     ):
-        try:
-            initial_detection_points = validate_points(
-                initial_detection.absolute_points
-            )
-        except AttributeError:
+        if not isinstance(initial_detection, Detection):
             print(
                 f"\n[red]ERROR[/red]: The detection list fed into `tracker.update()` should be composed of {Detection} objects not {type(initial_detection)}.\n"
             )
             exit()
 
-        self.dim_points = initial_detection_points.shape[1]
-        self.num_points = initial_detection_points.shape[0]
+        self.dim_points = initial_detection.absolute_points.shape[1]
+        self.num_points = initial_detection.absolute_points.shape[0]
         self.hit_counter_max: int = hit_counter_max
         self.pointwise_hit_counter_max: int = pointwise_hit_counter_max
         self.initialization_delay = initialization_delay
@@ -487,7 +483,7 @@ class TrackedObject:
             self.past_detections: Sequence["Detection"] = []
 
         # Create Kalman Filter
-        self.filter = filter_factory.create_filter(initial_detection_points)
+        self.filter = filter_factory.create_filter(initial_detection.absolute_points)
         self.dim_z = self.dim_points * self.num_points
         self.label = initial_detection.label
         self.abs_to_rel = abs_to_rel
@@ -550,7 +546,6 @@ class TrackedObject:
         return self.point_hit_counter > 0
 
     def hit(self, detection: "Detection", period: int = 1):
-        points = validate_points(detection.absolute_points)
         self._conditionally_add_to_past_detections(detection)
 
         self.last_detection = detection
@@ -580,7 +575,9 @@ class TrackedObject:
         self.point_hit_counter[self.point_hit_counter < 0] = 0
         H_vel = np.zeros(H_pos.shape)  # But we don't directly measure velocity
         H = np.hstack([H_pos, H_vel])
-        self.filter.update(np.expand_dims(points.flatten(), 0).T, None, H)
+        self.filter.update(
+            np.expand_dims(detection.absolute_points.flatten(), 0).T, None, H
+        )
 
         # Force points being detected for the first time to have velocity = 0
         # This is needed because some detectors (like OpenPose) set points with
@@ -600,7 +597,7 @@ class TrackedObject:
         )
 
         self.filter.x[: self.dim_z][first_detection_mask] = np.expand_dims(
-            points.flatten(), 0
+            detection.absolute_points.flatten(), 0
         ).T[first_detection_mask]
 
         self.filter.x[self.dim_z :][np.logical_not(detected_at_least_once_mask)] = 0
@@ -690,9 +687,9 @@ class Detection:
         label: Hashable = None,
         embedding=None,
     ):
-        self.points = points
+        self.points = validate_points(points)
         self.scores = scores
         self.data = data
         self.label = label
-        self.absolute_points = points.copy()
+        self.absolute_points = self.points.copy()
         self.embedding = embedding


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index e4162cd..45c5526 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,8 @@
 import numpy as np
 import pytest
 
+from norfair.utils import validate_points
+
 
 @pytest.fixture
 def mock_det():
@@ -30,3 +32,23 @@ def mock_obj(mock_det):
             self.last_detection = mock_det(points, scores=scores)
 
     return FakeTrackedObject
+
+
+@pytest.fixture
+def mock_coordinate_transformation():
+
+    # simple mock to return abs or relative positions
+    class TransformMock:
+        def __init__(self, relative_points, absolute_points) -> None:
+            self.absolute_points = validate_points(absolute_points)
+            self.relative_points = validate_points(relative_points)
+
+        def abs_to_rel(self, points):
+            np.testing.assert_equal(points, self.absolute_points)
+            return self.relative_points
+
+        def rel_to_abs(self, points):
+            np.testing.assert_equal(points, self.relative_points)
+            return self.absolute_points
+
+    return TransformMock
diff --git a/tests/test_tracker.py b/tests/test_tracker.py
index ffdeb45..bb6da11 100644
--- a/tests/test_tracker.py
+++ b/tests/test_tracker.py
@@ -9,6 +9,7 @@ from norfair import (
     OptimizedKalmanFilterFactory,
     Tracker,
 )
+from norfair.utils import validate_points
 
 
 def test_params():
@@ -154,6 +155,68 @@ def test_distance_t(filter_factory):
     assert 4 < tracked_objects[0].estimate[0][1] <= 4.5
 
 
+@pytest.mark.parametrize(
+    "filter_factory", [FilterPyKalmanFilterFactory(), OptimizedKalmanFilterFactory()]
+)
+def test_1d_points(filter_factory, mock_coordinate_transformation):
+    #
+    # Test a detection with rank 1
+    #
+    tracker = Tracker(
+        "frobenius",
+        initialization_delay=0,
+        distance_threshold=1,
+        filter_factory=filter_factory,
+    )
+    detection = Detection(points=np.array([1, 1]))
+    assert detection.points.shape == (1, 2)
+    assert detection.absolute_points.shape == (1, 2)
+    tracked_objects = tracker.update([detection])
+    assert len(tracked_objects) == 1
+    tracked_object = tracked_objects[0]
+    assert tracked_object.estimate.shape == (1, 2)
+
+
+def test_camera_motion(mock_coordinate_transformation):
+    #
+    # Simple test for camera motion
+    #
+    for one_d in [True, False]:
+        tracker = Tracker("frobenius", 1, initialization_delay=0)
+        if one_d:
+            absolute_points = np.array([1, 1])
+        else:
+            absolute_points = np.array([[1, 1]])
+
+        relative_points = absolute_points + 1
+
+        coord_transformation_mock = mock_coordinate_transformation(
+            relative_points=relative_points, absolute_points=absolute_points
+        )
+
+        detection = Detection(relative_points)
+        tracked_objects = tracker.update(
+            [detection], coord_transformations=coord_transformation_mock
+        )
+
+        # assert that the detection was correctly updated
+        np.testing.assert_equal(
+            detection.absolute_points, validate_points(absolute_points)
+        )
+        np.testing.assert_equal(detection.points, validate_points(relative_points))
+
+        # check the tracked_object
+        assert len(tracked_objects) == 1
+        obj = tracked_objects[0]
+        np.testing.assert_almost_equal(
+            obj.get_estimate(absolute=False), validate_points(relative_points)
+        )
+        np.testing.assert_almost_equal(
+            obj.get_estimate(absolute=True), validate_points(absolute_points)
+        )
+        np.testing.assert_almost_equal(obj.estimate, validate_points(relative_points))
+
+
 # TODO tests list:
 #   - detections with different labels
 #   - partial matches where some points are missing