Skip to content

Commit d75be73

Browse files
Add Anomaly scene
Signed-off-by: Ashwin Vaidya <ashwinnitinvaidya@gmail.com>
1 parent 46b74db commit d75be73

File tree

8 files changed

+96
-17
lines changed

8 files changed

+96
-17
lines changed

src/python/model_api/visualizer/layout/flatten.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ def _compute_on_primitive(self, primitive: Type[Primitive], image: PIL.Image, sc
4343
return None
4444

4545
def __call__(self, scene: Scene) -> PIL.Image:
46-
image_: PIL.Image = scene.base.copy()
46+
image: PIL.Image = scene.base.copy()
4747
for child in self.children:
48-
image_ = child(scene) if isinstance(child, Layout) else self._compute_on_primitive(child, image_, scene)
49-
return image_
48+
image_ = child(scene) if isinstance(child, Layout) else self._compute_on_primitive(child, image, scene)
49+
if image_ is not None:
50+
image = image_
51+
return image

src/python/model_api/visualizer/primitive/label.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ class Label(Primitive):
4040

4141
def __init__(
4242
self,
43-
label: str,
43+
label: Union[str, float],
4444
fg_color: Union[str, tuple[int, int, int]] = "black",
4545
bg_color: Union[str, tuple[int, int, int]] = "yellow",
4646
font_path: Union[str, BytesIO, None] = None,
4747
size: int = 16,
4848
) -> None:
49-
self.label = label
49+
self.label = str(label)
5050
self.fg_color = fg_color
5151
self.bg_color = bg_color
5252
self.font = ImageFont.load_default(size=size) if font_path is None else ImageFont.truetype(font_path, size)

src/python/model_api/visualizer/scene/anomaly.py

+39-5
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,56 @@
33
# Copyright (C) 2024 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0
55

6+
from itertools import starmap
7+
from typing import Union
8+
9+
import cv2
610
from PIL import Image
711

812
from model_api.models.result import AnomalyResult
913
from model_api.visualizer.layout import Flatten, Layout
10-
from model_api.visualizer.primitive import Overlay
14+
from model_api.visualizer.primitive import BoundingBox, Label, Overlay, Polygon
1115

1216
from .scene import Scene
1317

1418

1519
class AnomalyScene(Scene):
1620
"""Anomaly Scene."""
1721

18-
def __init__(self, image: Image, result: AnomalyResult) -> None:
19-
self.image = image
20-
self.result = result
22+
def __init__(self, image: Image, result: AnomalyResult, layout: Union[Layout, None] = None) -> None:
23+
super().__init__(
24+
base=image,
25+
overlay=self._get_overlays(result),
26+
bounding_box=self._get_bounding_boxes(result),
27+
label=self._get_labels(result),
28+
polygon=self._get_polygons(result),
29+
layout=layout,
30+
)
31+
32+
def _get_overlays(self, result: AnomalyResult) -> list[Overlay]:
33+
if result.anomaly_map is not None:
34+
anomaly_map = cv2.cvtColor(result.anomaly_map, cv2.COLOR_BGR2RGB)
35+
return [Overlay(anomaly_map)]
36+
return []
37+
38+
def _get_bounding_boxes(self, result: AnomalyResult) -> list[BoundingBox]:
39+
if result.pred_boxes is not None:
40+
return list(starmap(BoundingBox, result.pred_boxes))
41+
return []
42+
43+
def _get_labels(self, result: AnomalyResult) -> list[Label]:
44+
labels = []
45+
if result.pred_label is not None:
46+
labels.append(Label(result.pred_label))
47+
if result.pred_score is not None:
48+
labels.append(Label(result.pred_score))
49+
return labels
50+
51+
def _get_polygons(self, result: AnomalyResult) -> list[Polygon]:
52+
if result.pred_mask is not None:
53+
return [Polygon(result.pred_mask)]
54+
return []
2155

2256
@property
2357
def default_layout(self) -> Layout:
24-
return Flatten(Overlay)
58+
return Flatten(Overlay, BoundingBox, Label, Polygon)

src/python/model_api/visualizer/scene/classification.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Copyright (C) 2024 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0
55

6+
from typing import Union
7+
68
from PIL import Image
79

810
from model_api.models.result import ClassificationResult
@@ -15,7 +17,7 @@
1517
class ClassificationScene(Scene):
1618
"""Classification Scene."""
1719

18-
def __init__(self, image: Image, result: ClassificationResult) -> None:
20+
def __init__(self, image: Image, result: ClassificationResult, layout: Union[Layout, None] = None) -> None:
1921
self.image = image
2022
self.result = result
2123

src/python/model_api/visualizer/scene/detection.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
# Copyright (C) 2024 Intel Corporation
44
# SPDX-License-Identifier: Apache-2.0
55

6+
from typing import Union
7+
68
from PIL import Image
79

810
from model_api.models.result import DetectionResult
11+
from model_api.visualizer.layout import Layout
912

1013
from .scene import Scene
1114

1215

1316
class DetectionScene(Scene):
1417
"""Detection Scene."""
1518

16-
def __init__(self, image: Image, result: DetectionResult) -> None:
19+
def __init__(self, image: Image, result: DetectionResult, layout: Union[Layout, None] = None) -> None:
1720
self.image = image
1821
self.result = result

src/python/model_api/visualizer/scene/scene.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ def __init__(
4040
self.polygon = self._to_polygon(polygon)
4141
self.layout = layout
4242

43-
def show(self) -> Image: ...
43+
def show(self) -> None:
44+
self.render().show()
4445

4546
def save(self, path: Path) -> None:
4647
self.render().save(path)

src/python/model_api/visualizer/visualizer.py

+7-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# SPDX-License-Identifier: Apache-2.0
55

66
from pathlib import Path
7+
from typing import Union
78

89
from PIL import Image
910

@@ -19,7 +20,9 @@
1920

2021

2122
class Visualizer:
22-
def __init__(self, layout: Layout) -> None:
23+
"""Utility class to automatically select the correct scene and render/show it."""
24+
25+
def __init__(self, layout: Union[Layout, None] = None) -> None:
2326
self.layout = layout
2427

2528
def show(self, image: Image, result: Result) -> Image:
@@ -33,11 +36,11 @@ def save(self, image: Image, result: Result, path: Path) -> None:
3336
def _scene_from_result(self, image: Image, result: Result) -> Scene:
3437
scene: Scene
3538
if isinstance(result, AnomalyResult):
36-
scene = AnomalyScene(image, result)
39+
scene = AnomalyScene(image, result, self.layout)
3740
elif isinstance(result, ClassificationResult):
38-
scene = ClassificationScene(image, result)
41+
scene = ClassificationScene(image, result, self.layout)
3942
elif isinstance(result, DetectionResult):
40-
scene = DetectionScene(image, result)
43+
scene = DetectionScene(image, result, self.layout)
4144
else:
4245
msg = f"Unsupported result type: {type(result)}"
4346
raise ValueError(msg)
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Tests for scene."""
2+
3+
# Copyright (C) 2025 Intel Corporation
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
from pathlib import Path
7+
8+
import numpy as np
9+
from PIL import Image
10+
11+
from model_api.models.result import AnomalyResult
12+
from model_api.visualizer import Visualizer
13+
14+
15+
def test_anomaly_scene(mock_image: Image, tmpdir: Path):
16+
"""Test if the anomaly scene is created."""
17+
heatmap = np.ones(mock_image.size, dtype=np.uint8)
18+
heatmap *= 255
19+
20+
mask = np.zeros(mock_image.size, dtype=np.uint8)
21+
mask[32:96, 32:96] = 255
22+
mask[40:80, 0:128] = 255
23+
24+
anomaly_result = AnomalyResult(
25+
anomaly_map=heatmap,
26+
pred_boxes=np.array([[0, 0, 128, 128], [32, 32, 96, 96]]),
27+
pred_label="Anomaly",
28+
pred_mask=mask,
29+
pred_score=0.85,
30+
)
31+
32+
visualizer = Visualizer()
33+
visualizer.save(mock_image, anomaly_result, tmpdir / "anomaly_scene.jpg")
34+
assert Path(tmpdir / "anomaly_scene.jpg").exists()

0 commit comments

Comments
 (0)