diff --git a/src/model_api/visualizer/primitive/bounding_box.py b/src/model_api/visualizer/primitive/bounding_box.py index 3816f889..36ba4280 100644 --- a/src/model_api/visualizer/primitive/bounding_box.py +++ b/src/model_api/visualizer/primitive/bounding_box.py @@ -5,9 +5,10 @@ from __future__ import annotations -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw from model_api.visualizer.defaults import DEFAULT_FONT_SIZE, DEFAULT_OUTLINE_WIDTH +from model_api.visualizer.utils import default_font from .primitive import Primitive @@ -49,7 +50,7 @@ def __init__( self.color = color self.outline_width = outline_width self.font_size = font_size - self.font = ImageFont.load_default(size=self.font_size) + self.font = default_font(size=self.font_size) self.y_buffer = max(3, font_size // 3) # Text at the bottom of the text box is clipped. This prevents that. def compute(self, image: Image) -> Image: diff --git a/src/model_api/visualizer/primitive/keypoints.py b/src/model_api/visualizer/primitive/keypoints.py index 01d07e4d..df3b35e0 100644 --- a/src/model_api/visualizer/primitive/keypoints.py +++ b/src/model_api/visualizer/primitive/keypoints.py @@ -6,9 +6,10 @@ from typing import Union import numpy as np -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw from model_api.visualizer.defaults import DEFAULT_FONT_SIZE, DEFAULT_KEYPOINT_SIZE +from model_api.visualizer.utils import default_font from .primitive import Primitive @@ -51,7 +52,7 @@ def compute(self, image: Image) -> Image: ) if self.scores is not None: - font = ImageFont.load_default(size=self.font_size) + font = default_font(size=self.font_size) for score, keypoint in zip(self.scores, self.keypoints): textbox = draw.textbbox((0, 0), f"{score:.2f}", font=font) draw.text( diff --git a/src/model_api/visualizer/primitive/label.py b/src/model_api/visualizer/primitive/label.py index 3c1845ff..a99c4010 100644 --- a/src/model_api/visualizer/primitive/label.py +++ b/src/model_api/visualizer/primitive/label.py @@ -6,9 +6,10 @@ from io import BytesIO from typing import Union -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw from model_api.visualizer.defaults import DEFAULT_FONT_SIZE +from model_api.visualizer.utils import default_font, truetype_font from .primitive import Primitive @@ -53,7 +54,7 @@ def __init__( self.label = f"{label} ({score:.2f})" if score is not None else label self.fg_color = fg_color self.bg_color = bg_color - self.font = ImageFont.load_default(size=size) if font_path is None else ImageFont.truetype(font_path, size) + self.font = default_font(size=size) if font_path is None else truetype_font(font_path, size) def compute(self, image: Image, buffer_y: int = 5) -> Image: """Generate label on top of the image. diff --git a/src/model_api/visualizer/primitive/overlay.py b/src/model_api/visualizer/primitive/overlay.py index c596e495..72bbcbdc 100644 --- a/src/model_api/visualizer/primitive/overlay.py +++ b/src/model_api/visualizer/primitive/overlay.py @@ -9,9 +9,9 @@ import numpy as np import PIL -from PIL import ImageFont from model_api.visualizer.defaults import DEFAULT_FONT_SIZE, DEFAULT_OPACITY +from model_api.visualizer.utils import default_font from .primitive import Primitive @@ -66,7 +66,7 @@ def overlay_labels( """ if labels is not None: labels = [labels] if isinstance(labels, str) else labels - font = ImageFont.load_default(size=font_size) + font = default_font(size=font_size) buffer_y = max(3, font_size // 3) dummy_image = PIL.Image.new("RGB", (1, 1)) draw = PIL.ImageDraw.Draw(dummy_image) diff --git a/src/model_api/visualizer/scene/detection.py b/src/model_api/visualizer/scene/detection.py index 4ec15055..fd2a98b5 100644 --- a/src/model_api/visualizer/scene/detection.py +++ b/src/model_api/visualizer/scene/detection.py @@ -12,9 +12,9 @@ from model_api.visualizer.defaults import DEFAULT_FONT_SIZE, DEFAULT_OUTLINE_WIDTH from model_api.visualizer.layout import Flatten, HStack, Layout from model_api.visualizer.primitive import BoundingBox, Label, Overlay +from model_api.visualizer.utils import get_label_color_mapping from .scene import Scene -from .utils import get_label_color_mapping class DetectionScene(Scene): diff --git a/src/model_api/visualizer/scene/segmentation/instance_segmentation.py b/src/model_api/visualizer/scene/segmentation/instance_segmentation.py index fdfe8a59..3c0044b4 100644 --- a/src/model_api/visualizer/scene/segmentation/instance_segmentation.py +++ b/src/model_api/visualizer/scene/segmentation/instance_segmentation.py @@ -13,7 +13,7 @@ from model_api.visualizer.layout import Flatten, HStack, Layout from model_api.visualizer.primitive import BoundingBox, Label, Overlay, Polygon from model_api.visualizer.scene import Scene -from model_api.visualizer.scene.utils import get_label_color_mapping +from model_api.visualizer.utils import get_label_color_mapping class InstanceSegmentationScene(Scene): diff --git a/src/model_api/visualizer/scene/utils.py b/src/model_api/visualizer/utils.py similarity index 60% rename from src/model_api/visualizer/scene/utils.py rename to src/model_api/visualizer/utils.py index 9ad97723..e15e1453 100644 --- a/src/model_api/visualizer/scene/utils.py +++ b/src/model_api/visualizer/utils.py @@ -1,5 +1,9 @@ """Visualizer utilities.""" +from functools import lru_cache + +from PIL import ImageFont + # Copyright (C) 2026 Intel Corporation # SPDX-License-Identifier: Apache-2.0 @@ -38,3 +42,28 @@ def get_label_color_mapping(labels: list[str]) -> dict[str, str]: """ unique_labels = sorted(set(labels)) return {label: COLOR_PALETTE[i % len(COLOR_PALETTE)] for i, label in enumerate(unique_labels)} + + +@lru_cache(maxsize=5) +def default_font(size: int = 10): + """Get the default font with the specified size using cache to store the object. + + Args: + size: Font size. + + Returns: + A PIL ImageFont instance with the default font and specified size. + """ + return ImageFont.load_default(size=size) + + +@lru_cache(maxsize=5) +def truetype_font(font_path: str, size: int = 10): + """Get a TrueType font from the specified path and size using cache to store the object. + + Args: + font_path: Path to the .ttf font file. + size: Font size. + """ + + return ImageFont.truetype(font_path, size)