mirror of
https://github.com/home-assistant/core.git
synced 2025-07-13 00:07:10 +00:00
Ensure camera scaling always produces an image of at least the requested width and height (#55033)
This commit is contained in:
parent
ccaf0d5c75
commit
dc851b9dd5
@ -1,4 +1,5 @@
|
|||||||
"""Image processing for cameras."""
|
"""Image processing for cameras."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
@ -15,7 +16,28 @@ if TYPE_CHECKING:
|
|||||||
from . import Image
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
def scale_jpeg_camera_image(cam_image: "Image", width: int, height: int) -> bytes:
|
def find_supported_scaling_factor(
|
||||||
|
current_width: int, current_height: int, target_width: int, target_height: int
|
||||||
|
) -> tuple[int, int] | None:
|
||||||
|
"""Find a supported scaling factor to scale the image.
|
||||||
|
|
||||||
|
If there is no exact match, we use one size up to ensure
|
||||||
|
the image remains crisp.
|
||||||
|
"""
|
||||||
|
for idx, supported_sf in enumerate(SUPPORTED_SCALING_FACTORS):
|
||||||
|
ratio = supported_sf[0] / supported_sf[1]
|
||||||
|
width_after_scale = current_width * ratio
|
||||||
|
height_after_scale = current_height * ratio
|
||||||
|
if width_after_scale == target_width and height_after_scale == target_height:
|
||||||
|
return supported_sf
|
||||||
|
if width_after_scale < target_width or height_after_scale < target_height:
|
||||||
|
return None if idx == 0 else SUPPORTED_SCALING_FACTORS[idx - 1]
|
||||||
|
|
||||||
|
# Giant image, the most we can reduce by is 1/8
|
||||||
|
return SUPPORTED_SCALING_FACTORS[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def scale_jpeg_camera_image(cam_image: Image, width: int, height: int) -> bytes:
|
||||||
"""Scale a camera image as close as possible to one of the supported scaling factors."""
|
"""Scale a camera image as close as possible to one of the supported scaling factors."""
|
||||||
turbo_jpeg = TurboJPEGSingleton.instance()
|
turbo_jpeg = TurboJPEGSingleton.instance()
|
||||||
if not turbo_jpeg:
|
if not turbo_jpeg:
|
||||||
@ -28,17 +50,12 @@ def scale_jpeg_camera_image(cam_image: "Image", width: int, height: int) -> byte
|
|||||||
except OSError:
|
except OSError:
|
||||||
return cam_image.content
|
return cam_image.content
|
||||||
|
|
||||||
if current_width <= width or current_height <= height:
|
scaling_factor = find_supported_scaling_factor(
|
||||||
|
current_width, current_height, width, height
|
||||||
|
)
|
||||||
|
if scaling_factor is None:
|
||||||
return cam_image.content
|
return cam_image.content
|
||||||
|
|
||||||
ratio = width / current_width
|
|
||||||
|
|
||||||
scaling_factor = SUPPORTED_SCALING_FACTORS[-1]
|
|
||||||
for supported_sf in SUPPORTED_SCALING_FACTORS:
|
|
||||||
if ratio >= (supported_sf[0] / supported_sf[1]):
|
|
||||||
scaling_factor = supported_sf
|
|
||||||
break
|
|
||||||
|
|
||||||
return cast(
|
return cast(
|
||||||
bytes,
|
bytes,
|
||||||
turbo_jpeg.scale_with_quality(
|
turbo_jpeg.scale_with_quality(
|
||||||
@ -61,7 +78,7 @@ class TurboJPEGSingleton:
|
|||||||
__instance = None
|
__instance = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def instance() -> "TurboJPEG":
|
def instance() -> TurboJPEG:
|
||||||
"""Singleton for TurboJPEG."""
|
"""Singleton for TurboJPEG."""
|
||||||
if TurboJPEGSingleton.__instance is None:
|
if TurboJPEGSingleton.__instance is None:
|
||||||
TurboJPEGSingleton()
|
TurboJPEGSingleton()
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
"""Test img_util module."""
|
"""Test img_util module."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
from turbojpeg import TurboJPEG
|
from turbojpeg import TurboJPEG
|
||||||
|
|
||||||
from homeassistant.components.camera import Image
|
from homeassistant.components.camera import Image
|
||||||
from homeassistant.components.camera.img_util import (
|
from homeassistant.components.camera.img_util import (
|
||||||
TurboJPEGSingleton,
|
TurboJPEGSingleton,
|
||||||
|
find_supported_scaling_factor,
|
||||||
scale_jpeg_camera_image,
|
scale_jpeg_camera_image,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -59,6 +61,15 @@ def test_scale_jpeg_camera_image():
|
|||||||
|
|
||||||
assert jpeg_bytes == EMPTY_8_6_JPEG
|
assert jpeg_bytes == EMPTY_8_6_JPEG
|
||||||
|
|
||||||
|
turbo_jpeg = mock_turbo_jpeg(
|
||||||
|
first_width=640, first_height=480, second_width=640, second_height=480
|
||||||
|
)
|
||||||
|
with patch("turbojpeg.TurboJPEG", return_value=turbo_jpeg):
|
||||||
|
TurboJPEGSingleton()
|
||||||
|
jpeg_bytes = scale_jpeg_camera_image(camera_image, 320, 480)
|
||||||
|
|
||||||
|
assert jpeg_bytes == EMPTY_16_12_JPEG
|
||||||
|
|
||||||
|
|
||||||
def test_turbojpeg_load_failure():
|
def test_turbojpeg_load_failure():
|
||||||
"""Handle libjpegturbo not being installed."""
|
"""Handle libjpegturbo not being installed."""
|
||||||
@ -70,3 +81,38 @@ def test_turbojpeg_load_failure():
|
|||||||
_clear_turbojpeg_singleton()
|
_clear_turbojpeg_singleton()
|
||||||
TurboJPEGSingleton()
|
TurboJPEGSingleton()
|
||||||
assert TurboJPEGSingleton.instance() is not None
|
assert TurboJPEGSingleton.instance() is not None
|
||||||
|
|
||||||
|
|
||||||
|
SCALE_TEST_EXPECTED = [
|
||||||
|
(5782, 3946, 640, 480, (1, 8)), # Maximum scale
|
||||||
|
(1600, 1200, 640, 480, (1, 2)), # Equal scale for width and height
|
||||||
|
(1600, 1200, 1400, 1050, (7, 8)), # Equal scale for width and height
|
||||||
|
(1600, 1200, 1200, 900, (3, 4)), # Equal scale for width and height
|
||||||
|
(1600, 1200, 1000, 750, (5, 8)), # Equal scale for width and height
|
||||||
|
(1600, 1200, 600, 450, (3, 8)), # Equal scale for width and height
|
||||||
|
(1600, 1200, 400, 300, (1, 4)), # Equal scale for width and height
|
||||||
|
(1600, 1200, 401, 300, (3, 8)), # Width is just a little to big, next size up
|
||||||
|
(640, 480, 330, 200, (5, 8)), # Preserve width clarity
|
||||||
|
(640, 480, 300, 260, (5, 8)), # Preserve height clarity
|
||||||
|
(640, 480, 1200, 480, None), # Request larger width - no scaling
|
||||||
|
(640, 480, 640, 480, None), # Request same - no scaling
|
||||||
|
(640, 480, 640, 270, None), # Request smaller height - no scaling
|
||||||
|
(640, 480, 320, 480, None), # Request smaller width - no scaling
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"image_width, image_height, input_width, input_height, scaling_factor",
|
||||||
|
SCALE_TEST_EXPECTED,
|
||||||
|
)
|
||||||
|
def test_find_supported_scaling_factor(
|
||||||
|
image_width, image_height, input_width, input_height, scaling_factor
|
||||||
|
):
|
||||||
|
"""Test we always get an image of at least the size we ask if its big enough."""
|
||||||
|
|
||||||
|
assert (
|
||||||
|
find_supported_scaling_factor(
|
||||||
|
image_width, image_height, input_width, input_height
|
||||||
|
)
|
||||||
|
== scaling_factor
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user