Home Assistant Unofficial Reference 2024.12.1
img_util.py
Go to the documentation of this file.
1 """Image processing for cameras."""
2 
3 from __future__ import annotations
4 
5 from contextlib import suppress
6 import logging
7 from typing import TYPE_CHECKING, Literal, cast
8 
9 with suppress(Exception):
10  # TurboJPEG imports numpy which may or may not work so
11  # we have to guard the import here. We still want
12  # to import it at top level so it gets loaded
13  # in the import executor and not in the event loop.
14  from turbojpeg import TurboJPEG
15 
16 
17 if TYPE_CHECKING:
18  from . import Image
19 
20 SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)]
21 
22 _LOGGER = logging.getLogger(__name__)
23 
24 JPEG_QUALITY = 75
25 
26 
28  current_width: int, current_height: int, target_width: int, target_height: int
29 ) -> tuple[int, int] | None:
30  """Find a supported scaling factor to scale the image.
31 
32  If there is no exact match, we use one size up to ensure
33  the image remains crisp.
34  """
35  for idx, supported_sf in enumerate(SUPPORTED_SCALING_FACTORS):
36  ratio = supported_sf[0] / supported_sf[1]
37  width_after_scale = current_width * ratio
38  height_after_scale = current_height * ratio
39  if width_after_scale == target_width and height_after_scale == target_height:
40  return supported_sf
41  if width_after_scale < target_width or height_after_scale < target_height:
42  return None if idx == 0 else SUPPORTED_SCALING_FACTORS[idx - 1]
43 
44  # Giant image, the most we can reduce by is 1/8
45  return SUPPORTED_SCALING_FACTORS[-1]
46 
47 
48 def scale_jpeg_camera_image(cam_image: Image, width: int, height: int) -> bytes:
49  """Scale a camera image.
50 
51  Scale as close as possible to one of the supported scaling factors.
52  """
53  turbo_jpeg = TurboJPEGSingleton.instance()
54  if not turbo_jpeg:
55  return cam_image.content
56 
57  try:
58  (current_width, current_height, _, _) = turbo_jpeg.decode_header(
59  cam_image.content
60  )
61  except OSError:
62  return cam_image.content
63 
64  scaling_factor = find_supported_scaling_factor(
65  current_width, current_height, width, height
66  )
67  if scaling_factor is None:
68  return cam_image.content
69 
70  return cast(
71  bytes,
72  turbo_jpeg.scale_with_quality(
73  cam_image.content,
74  scaling_factor=scaling_factor,
75  quality=JPEG_QUALITY,
76  ),
77  )
78 
79 
81  """Load TurboJPEG only once.
82 
83  Ensures we do not log load failures each snapshot
84  since camera image fetches happen every few
85  seconds.
86  """
87 
88  __instance: TurboJPEG | Literal[False] | None = None
89 
90  @staticmethod
91  def instance() -> TurboJPEG | Literal[False] | None:
92  """Singleton for TurboJPEG."""
93  if TurboJPEGSingleton.__instance is None:
95  return TurboJPEGSingleton.__instance
96 
97  def __init__(self) -> None:
98  """Try to create TurboJPEG only once."""
99  try:
100  TurboJPEGSingleton.__instance = TurboJPEG()
101  except Exception:
102  _LOGGER.exception(
103  "Error loading libturbojpeg; Camera snapshot performance will be sub-optimal"
104  )
105  TurboJPEGSingleton.__instance = False
106 
107 
108 # TurboJPEG loads libraries that do blocking I/O.
109 # Initialize TurboJPEGSingleton in the executor to avoid
110 # blocking the event loop.
111 TurboJPEGSingleton.instance()
tuple[int, int]|None find_supported_scaling_factor(int current_width, int current_height, int target_width, int target_height)
Definition: img_util.py:29
bytes scale_jpeg_camera_image(Image cam_image, int width, int height)
Definition: img_util.py:48