diff --git a/node/OverlayNode/draw_util/draw_util.py b/node/OverlayNode/draw_util/draw_util.py index b794cec5..f271f0a4 100644 --- a/node/OverlayNode/draw_util/draw_util.py +++ b/node/OverlayNode/draw_util/draw_util.py @@ -673,10 +673,19 @@ def draw_multi_object_tracking_info( # トラックID、スコア score = '%.2f' % score text = 'TID:%s(%s)' % (str(int(track_id_dict[id])), str(score)) + + # Position text above bbox if there's space, otherwise inside/below + # Use margin of 5 pixels to avoid text being cut off at image edge + margin = 5 + tid_y_pos = y1 - vertical_offset_1 + if tid_y_pos < margin: + # Not enough space above, put it inside the box + tid_y_pos = y1 + vertical_offset_1 + image = cv2.putText( image, text, - (x1, y1 - vertical_offset_1), + (x1, tid_y_pos), cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, @@ -685,10 +694,17 @@ def draw_multi_object_tracking_info( # クラスID text = 'CID:%s(%s)' % (str(int(class_id)), class_names[int(class_id)]) + + # Position text above bbox if there's space, otherwise inside/below + cid_y_pos = y1 - vertical_offset_2 + if cid_y_pos < margin: + # Not enough space above, put it below TID text + cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2 + image = cv2.putText( image, text, - (x1, y1 - vertical_offset_2), + (x1, cid_y_pos), cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, diff --git a/node/basenode.py b/node/basenode.py index f8474729..57861063 100644 --- a/node/basenode.py +++ b/node/basenode.py @@ -959,10 +959,19 @@ def draw_multi_object_tracking_info( score = "%.2f" % score text = "TID:%s(%s)" % (str(int(track_id_dict[id])), str(score)) + + # Position text above bbox if there's space, otherwise inside/below + # Use margin of 5 pixels to avoid text being cut off at image edge + margin = 5 + tid_y_pos = y1 - vertical_offset_1 + if tid_y_pos < margin: + # Not enough space above, put it inside the box + tid_y_pos = y1 + vertical_offset_1 + image = cv2.putText( image, text, - (x1, y1 - vertical_offset_1), + (x1, tid_y_pos), cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, @@ -971,10 +980,17 @@ def draw_multi_object_tracking_info( class_name = self.get_class_name(class_id, class_names) text = "CID:%s(%s)" % (str(int(class_id)), class_name) + + # Position text above bbox if there's space, otherwise inside/below + cid_y_pos = y1 - vertical_offset_2 + if cid_y_pos < margin: + # Not enough space above, put it below TID text + cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2 + image = cv2.putText( image, text, - (x1, y1 - vertical_offset_2), + (x1, cid_y_pos), cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, diff --git a/tests/test_mot_label_visibility.py b/tests/test_mot_label_visibility.py new file mode 100644 index 00000000..1d854627 --- /dev/null +++ b/tests/test_mot_label_visibility.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Test to verify that MOT tracking labels are always visible, +even when bounding boxes are at the top of the image. + +This is a simplified unit test that tests the positioning logic directly. +""" +import unittest +import sys +import os + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +class TestMOTLabelPositioningLogic(unittest.TestCase): + """Test the label positioning logic to ensure visibility""" + + def test_label_positioning_at_top_of_image(self): + """Test that labels are repositioned when bbox is at the top""" + # Simulate the positioning logic from the fix + image_height = 480 + font_scale = max(0.3, min(1.0, (image_height / 720.0) * 0.5)) + vertical_offset_1 = int(36 * (font_scale / 0.5)) + vertical_offset_2 = int(12 * (font_scale / 0.5)) + margin = 5 + + # Test case 1: Bbox at y1 = 0 (very top) + y1 = 0 + tid_y_pos = y1 - vertical_offset_1 + + # Should be negative, so we reposition inside + self.assertLess(tid_y_pos, 0, "Original position should be negative") + + # Apply fix logic + if tid_y_pos < margin: + tid_y_pos = y1 + vertical_offset_1 + + self.assertGreater(tid_y_pos, 0, "Fixed TID position should be positive") + self.assertLess(tid_y_pos, image_height, "Fixed TID position should be within image") + + # Test CID position + cid_y_pos = y1 - vertical_offset_2 + self.assertLess(cid_y_pos, 0, "Original CID position should be negative") + + if cid_y_pos < margin: + cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2 + + self.assertGreater(cid_y_pos, 0, "Fixed CID position should be positive") + self.assertLess(cid_y_pos, image_height, "Fixed CID position should be within image") + self.assertGreater(cid_y_pos, tid_y_pos, "CID should be below TID when repositioned") + + def test_label_positioning_with_small_y1(self): + """Test label positioning when bbox is close to top (y1 < offset)""" + image_height = 720 + font_scale = max(0.3, min(1.0, (image_height / 720.0) * 0.5)) + vertical_offset_1 = int(36 * (font_scale / 0.5)) + vertical_offset_2 = int(12 * (font_scale / 0.5)) + margin = 5 + + # Small y1 value + y1 = 10 + tid_y_pos = y1 - vertical_offset_1 + + # Should be negative since y1 < vertical_offset_1 + self.assertLess(tid_y_pos, 0, "Original position should be negative") + + # Apply fix + if tid_y_pos < margin: + tid_y_pos = y1 + vertical_offset_1 + + self.assertGreater(tid_y_pos, 0, "Fixed position should be positive") + self.assertEqual(tid_y_pos, y1 + vertical_offset_1, "Should be positioned inside bbox") + + def test_label_positioning_with_sufficient_space(self): + """Test that labels stay above bbox when there's sufficient space""" + image_height = 720 + font_scale = max(0.3, min(1.0, (image_height / 720.0) * 0.5)) + vertical_offset_1 = int(36 * (font_scale / 0.5)) + vertical_offset_2 = int(12 * (font_scale / 0.5)) + margin = 5 + + # Bbox with enough space above (y1 = 100) + y1 = 100 + tid_y_pos = y1 - vertical_offset_1 + + # Should be positive (no fix needed) + self.assertGreater(tid_y_pos, 0, "Position should be positive with sufficient space") + + # No fix should be applied + if tid_y_pos < margin: + tid_y_pos = y1 + vertical_offset_1 + + # Should remain above the bbox + self.assertEqual(tid_y_pos, y1 - vertical_offset_1, "Should stay above bbox") + + # Same for CID + cid_y_pos = y1 - vertical_offset_2 + self.assertGreater(cid_y_pos, 0, "CID position should be positive") + + if cid_y_pos < margin: + cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2 + + self.assertEqual(cid_y_pos, y1 - vertical_offset_2, "CID should stay above bbox") + + def test_label_positioning_at_exact_threshold(self): + """Test label positioning when y1 equals vertical_offset""" + image_height = 720 + font_scale = max(0.3, min(1.0, (image_height / 720.0) * 0.5)) + vertical_offset_1 = int(36 * (font_scale / 0.5)) + margin = 5 + + # y1 exactly at threshold + y1 = vertical_offset_1 + tid_y_pos = y1 - vertical_offset_1 + + # Should be exactly 0 + self.assertEqual(tid_y_pos, 0, "Position should be 0 at threshold") + + # Fix should be applied since 0 < margin + if tid_y_pos < margin: + tid_y_pos = y1 + vertical_offset_1 + + # Position should now be inside the box + self.assertEqual(tid_y_pos, y1 + vertical_offset_1, + "Position should be repositioned inside box when at margin threshold") + + def test_multiple_image_sizes(self): + """Test label positioning logic across different image sizes""" + test_heights = [360, 480, 720, 1080, 1440] + margin = 5 + + for image_height in test_heights: + font_scale = max(0.3, min(1.0, (image_height / 720.0) * 0.5)) + vertical_offset_1 = int(36 * (font_scale / 0.5)) + vertical_offset_2 = int(12 * (font_scale / 0.5)) + + # Test at top (y1 = 0) + y1 = 0 + tid_y_pos = y1 - vertical_offset_1 + cid_y_pos = y1 - vertical_offset_2 + + # Apply fix + if tid_y_pos < margin: + tid_y_pos = y1 + vertical_offset_1 + if cid_y_pos < margin: + cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2 + + # Verify fix works for all sizes + self.assertGreater(tid_y_pos, 0, + f"TID should be visible for height {image_height}") + self.assertGreater(cid_y_pos, 0, + f"CID should be visible for height {image_height}") + self.assertLess(tid_y_pos, image_height, + f"TID should be within image for height {image_height}") + self.assertLess(cid_y_pos, image_height, + f"CID should be within image for height {image_height}") + + def test_label_ordering_when_repositioned(self): + """Test that TID and CID maintain proper ordering when repositioned""" + image_height = 720 + font_scale = max(0.3, min(1.0, (image_height / 720.0) * 0.5)) + vertical_offset_1 = int(36 * (font_scale / 0.5)) + vertical_offset_2 = int(12 * (font_scale / 0.5)) + margin = 5 + + # Bbox at top where both labels need repositioning + y1 = 0 + + # Original positions (both negative) + tid_y_pos = y1 - vertical_offset_1 + cid_y_pos = y1 - vertical_offset_2 + + # Apply fix + if tid_y_pos < margin: + tid_y_pos = y1 + vertical_offset_1 + if cid_y_pos < margin: + cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2 + + # When repositioned inside, CID should be below TID + self.assertGreater(cid_y_pos, tid_y_pos, + "When repositioned, CID should be below TID") + + # The spacing should be approximately vertical_offset_2 + spacing = cid_y_pos - tid_y_pos + self.assertAlmostEqual(spacing, vertical_offset_2, delta=1, + msg="Spacing between TID and CID should be maintained") + + +class TestImplementationInFiles(unittest.TestCase): + """Test that the implementation is present in the actual files""" + + def test_basenode_has_visibility_fix(self): + """Test that basenode.py has the visibility fix""" + basenode_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'node', + 'basenode.py' + ) + + with open(basenode_path, 'r') as f: + content = f.read() + + # Check for the fix logic + self.assertIn('margin = 5', content, + "Should define margin for edge detection") + self.assertIn('tid_y_pos = y1 - vertical_offset_1', content, + "Should calculate tid_y_pos") + self.assertIn('if tid_y_pos < margin:', content, + "Should check if tid_y_pos is less than margin") + self.assertIn('tid_y_pos = y1 + vertical_offset_1', content, + "Should reposition tid_y_pos when too close to edge") + + self.assertIn('cid_y_pos = y1 - vertical_offset_2', content, + "Should calculate cid_y_pos") + self.assertIn('if cid_y_pos < margin:', content, + "Should check if cid_y_pos is less than margin") + self.assertIn('cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2', content, + "Should reposition cid_y_pos when too close to edge") + + # Check that positioning uses the new variables + self.assertIn('(x1, tid_y_pos)', content, + "Should use tid_y_pos for positioning") + self.assertIn('(x1, cid_y_pos)', content, + "Should use cid_y_pos for positioning") + + def test_draw_util_has_visibility_fix(self): + """Test that draw_util.py has the visibility fix""" + draw_util_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + 'node', + 'OverlayNode', + 'draw_util', + 'draw_util.py' + ) + + with open(draw_util_path, 'r') as f: + content = f.read() + + # Check for the fix logic + self.assertIn('margin = 5', content, + "Should define margin for edge detection") + self.assertIn('tid_y_pos = y1 - vertical_offset_1', content, + "Should calculate tid_y_pos") + self.assertIn('if tid_y_pos < margin:', content, + "Should check if tid_y_pos is less than margin") + self.assertIn('tid_y_pos = y1 + vertical_offset_1', content, + "Should reposition tid_y_pos when too close to edge") + + self.assertIn('cid_y_pos = y1 - vertical_offset_2', content, + "Should calculate cid_y_pos") + self.assertIn('if cid_y_pos < margin:', content, + "Should check if cid_y_pos is less than margin") + self.assertIn('cid_y_pos = y1 + vertical_offset_1 + vertical_offset_2', content, + "Should reposition cid_y_pos when too close to edge") + + # Check that positioning uses the new variables + self.assertIn('(x1, tid_y_pos)', content, + "Should use tid_y_pos for positioning") + self.assertIn('(x1, cid_y_pos)', content, + "Should use cid_y_pos for positioning") + + +if __name__ == '__main__': + unittest.main()