feat: adjust visualization to scale with image size#482
feat: adjust visualization to scale with image size#482
Conversation
There was a problem hiding this comment.
Pull request overview
Adds automatic scaling of visualization primitives so annotations remain readable on high-resolution images, using centralized default constants and propagating a computed scale factor through scenes/primitives.
Changes:
- Introduces
auto_scaletoVisualizerand computes a scale factor from image size. - Propagates
scalethrough scenes and applies it to font sizes, outline widths, keypoint sizes, and overlay labels. - Centralizes visualization defaults (font size, opacity, outline width, baseline) in
defaults.pyand adds unit tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/visualizer/test_auto_scale.py | Adds unit tests for scale-factor computation and scale propagation/rendering behavior. |
| src/model_api/visualizer/visualizer.py | Adds auto_scale and scale-factor computation; passes scale into scenes. |
| src/model_api/visualizer/scene/segmentation/segmentation.py | Scales overlay label font sizes in segmentation overlays. |
| src/model_api/visualizer/scene/segmentation/instance_segmentation.py | Scales label, polygon outline, bbox outline, and overlay label font sizes. |
| src/model_api/visualizer/scene/keypoint.py | Scales keypoint radius and score font size. |
| src/model_api/visualizer/scene/detection.py | Scales bbox outline/font and overlay label font sizes. |
| src/model_api/visualizer/scene/classification.py | Scales classification label sizes. |
| src/model_api/visualizer/scene/anomaly.py | Scales anomaly overlays, bbox outline/font, label size, and polygon outline width. |
| src/model_api/visualizer/primitive/polygon.py | Uses centralized default opacity/outline width constants. |
| src/model_api/visualizer/primitive/overlay.py | Adds font_size and scales overlay label rendering parameters. |
| src/model_api/visualizer/primitive/label.py | Uses centralized default font size constant. |
| src/model_api/visualizer/primitive/keypoints.py | Adds configurable font size and uses centralized defaults. |
| src/model_api/visualizer/primitive/bounding_box.py | Adds configurable outline width/font size for bbox rendering. |
| src/model_api/visualizer/layout/hstack.py | Passes overlay font size into Overlay.overlay_labels(). |
| src/model_api/visualizer/defaults.py | Introduces centralized visualization default constants and baseline. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| """Default font size used for all text (labels, bounding boxes, overlays, keypoints).""" | ||
|
|
||
| # Line / outline widths | ||
| DEFAULT_OUTLINE_WIDTH: int = 2 | ||
| """Default outline width for bounding boxes and polygon contours.""" | ||
|
|
||
| # Opacity | ||
| DEFAULT_OPACITY: float = 0.4 | ||
| """Default blend opacity for overlays and polygon fills.""" | ||
|
|
||
| # Keypoint drawing | ||
| DEFAULT_KEYPOINT_SIZE: int = 3 | ||
| """Default radius (in pixels) for keypoint dots.""" | ||
|
|
||
| # Scale baseline | ||
| SCALE_BASELINE: int = 1280 | ||
| """Longer-edge pixel count of 720p (landscape). Used as the denominator when | ||
| computing the auto-scale factor.""" |
There was a problem hiding this comment.
The triple-quoted strings after assignments are standalone string expressions (not real docstrings) and add unnecessary module-level constants at import time. Prefer # ... comments, or document these constants in the module docstring (or a single comment block) while keeping the constants as plain assignments.
| """Default font size used for all text (labels, bounding boxes, overlays, keypoints).""" | |
| # Line / outline widths | |
| DEFAULT_OUTLINE_WIDTH: int = 2 | |
| """Default outline width for bounding boxes and polygon contours.""" | |
| # Opacity | |
| DEFAULT_OPACITY: float = 0.4 | |
| """Default blend opacity for overlays and polygon fills.""" | |
| # Keypoint drawing | |
| DEFAULT_KEYPOINT_SIZE: int = 3 | |
| """Default radius (in pixels) for keypoint dots.""" | |
| # Scale baseline | |
| SCALE_BASELINE: int = 1280 | |
| """Longer-edge pixel count of 720p (landscape). Used as the denominator when | |
| computing the auto-scale factor.""" | |
| # Default font size used for all text (labels, bounding boxes, overlays, keypoints). | |
| # Line / outline widths | |
| DEFAULT_OUTLINE_WIDTH: int = 2 | |
| # Default outline width for bounding boxes and polygon contours. | |
| # Opacity | |
| DEFAULT_OPACITY: float = 0.4 | |
| # Default blend opacity for overlays and polygon fills. | |
| # Keypoint drawing | |
| DEFAULT_KEYPOINT_SIZE: int = 3 | |
| # Default radius (in pixels) for keypoint dots. | |
| # Scale baseline | |
| SCALE_BASELINE: int = 1280 | |
| # Longer-edge pixel count of 720p (landscape). Used as the denominator when | |
| # computing the auto-scale factor. |
| def test_auto_scale_false_no_scaling(self, small_image, detection_result_small, tmpdir): | ||
| """auto_scale=False should work exactly like before.""" | ||
| vis = Visualizer(auto_scale=False) | ||
| rendered = vis.render(small_image, detection_result_small) | ||
| assert isinstance(rendered, Image.Image) | ||
| assert rendered.size == small_image.size | ||
|
|
||
| def test_auto_scale_true_small_image_no_scaling(self, small_image, detection_result_small, tmpdir): |
There was a problem hiding this comment.
These tests use the legacy tmpdir fixture (and a couple of test functions accept tmpdir even when it isn’t used). Prefer tmp_path for pathlib.Path-native usage, and remove unused fixture parameters to keep the tests cleaner and avoid lint warnings.
| def test_auto_scale_false_no_scaling(self, small_image, detection_result_small, tmpdir): | |
| """auto_scale=False should work exactly like before.""" | |
| vis = Visualizer(auto_scale=False) | |
| rendered = vis.render(small_image, detection_result_small) | |
| assert isinstance(rendered, Image.Image) | |
| assert rendered.size == small_image.size | |
| def test_auto_scale_true_small_image_no_scaling(self, small_image, detection_result_small, tmpdir): | |
| def test_auto_scale_false_no_scaling(self, small_image, detection_result_small): | |
| """auto_scale=False should work exactly like before.""" | |
| vis = Visualizer(auto_scale=False) | |
| rendered = vis.render(small_image, detection_result_small) | |
| assert isinstance(rendered, Image.Image) | |
| assert rendered.size == small_image.size | |
| def test_auto_scale_true_small_image_no_scaling(self, small_image, detection_result_small): |
| def test_auto_scale_false_no_scaling(self, small_image, detection_result_small, tmpdir): | ||
| """auto_scale=False should work exactly like before.""" | ||
| vis = Visualizer(auto_scale=False) | ||
| rendered = vis.render(small_image, detection_result_small) | ||
| assert isinstance(rendered, Image.Image) | ||
| assert rendered.size == small_image.size | ||
|
|
||
| def test_auto_scale_true_small_image_no_scaling(self, small_image, detection_result_small, tmpdir): |
There was a problem hiding this comment.
These tests use the legacy tmpdir fixture (and a couple of test functions accept tmpdir even when it isn’t used). Prefer tmp_path for pathlib.Path-native usage, and remove unused fixture parameters to keep the tests cleaner and avoid lint warnings.
| def test_auto_scale_false_no_scaling(self, small_image, detection_result_small, tmpdir): | |
| """auto_scale=False should work exactly like before.""" | |
| vis = Visualizer(auto_scale=False) | |
| rendered = vis.render(small_image, detection_result_small) | |
| assert isinstance(rendered, Image.Image) | |
| assert rendered.size == small_image.size | |
| def test_auto_scale_true_small_image_no_scaling(self, small_image, detection_result_small, tmpdir): | |
| def test_auto_scale_false_no_scaling(self, small_image, detection_result_small): | |
| """auto_scale=False should work exactly like before.""" | |
| vis = Visualizer(auto_scale=False) | |
| rendered = vis.render(small_image, detection_result_small) | |
| assert isinstance(rendered, Image.Image) | |
| assert rendered.size == small_image.size | |
| def test_auto_scale_true_small_image_no_scaling(self, small_image, detection_result_small): |
| vis.save(large_image, detection_result_large, path) | ||
| assert path.exists() | ||
|
|
||
| def test_auto_scale_classification(self, large_image, tmpdir): |
There was a problem hiding this comment.
These tests use the legacy tmpdir fixture (and a couple of test functions accept tmpdir even when it isn’t used). Prefer tmp_path for pathlib.Path-native usage, and remove unused fixture parameters to keep the tests cleaner and avoid lint warnings.
| vis.save(large_image, result, path) | ||
| assert path.exists() | ||
|
|
||
| def test_auto_scale_instance_segmentation(self, large_image, tmpdir): |
There was a problem hiding this comment.
These tests use the legacy tmpdir fixture (and a couple of test functions accept tmpdir even when it isn’t used). Prefer tmp_path for pathlib.Path-native usage, and remove unused fixture parameters to keep the tests cleaner and avoid lint warnings.
What does this PR do?
Add auto_scale parameter to Visualizer that automatically scales drawing primitives (font sizes, line widths, keypoint radius) relative to a 1280px baseline for large images, with centralized default constants in defaults.py.
Fixes #480
Before
After