A bunch of changes...
[python_utils.git] / camera_utils.py
1 #!/usr/bin/env python3
2
3 """Utilities for dealing with webcam images."""
4
5 import logging
6 import platform
7 import subprocess
8 from typing import NamedTuple, Optional
9
10 import cv2  # type: ignore
11 import numpy as np
12 import requests
13
14 import decorator_utils
15 import exceptions
16
17 logger = logging.getLogger(__name__)
18
19
20 class RawJpgHsv(NamedTuple):
21     """Raw image bytes, the jpeg image and the HSV (hue saturation value) image."""
22     raw: Optional[bytes]
23     jpg: Optional[np.ndarray]
24     hsv: Optional[np.ndarray]
25
26
27 class SanityCheckImageMetadata(NamedTuple):
28     """Is a Blue Iris image bad (big grey borders around it) or infrared?"""
29     is_bad_image: bool
30     is_infrared_image: bool
31
32
33 def sanity_check_image(hsv: np.ndarray) -> SanityCheckImageMetadata:
34     """See if a Blue Iris image is bad and infrared."""
35     def is_near(a, b) -> bool:
36         return abs(a - b) < 3
37
38     rows, cols, _ = hsv.shape
39     num_pixels = rows * cols
40     weird_orange_count = 0
41     hs_zero_count = 0
42     for r in range(rows):
43         for c in range(cols):
44             pixel = hsv[(r, c)]
45             if (
46                     is_near(pixel[0], 16) and
47                     is_near(pixel[1], 117) and
48                     is_near(pixel[2], 196)
49             ):
50                 weird_orange_count += 1
51             elif (is_near(pixel[0], 0) and is_near(pixel[1], 0)):
52                 hs_zero_count += 1
53     logger.debug(f"hszero#={hs_zero_count}, weird_orange={weird_orange_count}")
54     return SanityCheckImageMetadata(
55         hs_zero_count > (num_pixels * 0.75), weird_orange_count > (num_pixels * 0.75)
56     )
57
58
59 @decorator_utils.retry_if_none(tries=2, delay_sec=1, backoff=1.1)
60 def fetch_camera_image_from_video_server(
61     camera_name: str, *, width: int = 256, quality: int = 70
62 ) -> Optional[bytes]:
63     """Fetch the raw webcam image from the video server."""
64     camera_name = camera_name.replace(".house", "")
65     camera_name = camera_name.replace(".cabin", "")
66     url = f"http://10.0.0.226:8080/Umtxxf1uKMBniFblqeQ9KRbb6DDzN4/jpeg/GKlT2FfiSQ/{camera_name}/s.jpg"
67     try:
68         response = requests.get(url, stream=False, timeout=10.0)
69         if response.ok:
70             raw = response.content
71             logger.debug(f'Read {len(response.content)} byte image from HTTP server')
72             tmp = np.frombuffer(raw, dtype="uint8")
73             logger.debug(f'Translated raw content into {tmp.shape} {type(tmp)} with element type {type(tmp[0])}.')
74             jpg = cv2.imdecode(tmp, cv2.IMREAD_COLOR)
75             logger.debug(f'Decoded into {jpg.shape} jpeg {type(jpg)} with element type {type(jpg[0][0])}')
76             hsv = cv2.cvtColor(jpg, cv2.COLOR_BGR2HSV)
77             logger.debug(f'Converted JPG into HSV {hsv.shape} HSV {type(hsv)} with element type {type(hsv[0][0])}')
78             (_, is_bad_image) = sanity_check_image(hsv)
79             if not is_bad_image:
80                 logger.debug(f"Got a good image from {url}")
81                 return raw
82     except Exception as e:
83         logger.exception(e)
84     logger.warning(f"Got a bad image or HTTP error from {url}")
85     return None
86
87
88 def blue_iris_camera_name_to_hostname(camera_name: str) -> str:
89     mapping = {
90         "driveway": "driveway.house",
91         "backyard": "backyard.house",
92         "frontdoor": "frontdoor.house",
93         "cabin_driveway": "driveway.cabin",
94     }
95     camera_name = mapping.get(camera_name, camera_name)
96     if "." not in camera_name:
97         hostname = platform.node()
98         suffix = hostname.split(".")[-1]
99         camera_name += f".{suffix}"
100     return camera_name
101
102
103 @decorator_utils.retry_if_none(tries=2, delay_sec=1, backoff=1.1)
104 def fetch_camera_image_from_rtsp_stream(
105     camera_name: str, *, width: int = 256
106 ) -> Optional[bytes]:
107     """Fetch the raw webcam image straight from the webcam's RTSP stream."""
108     hostname = blue_iris_camera_name_to_hostname(camera_name)
109     try:
110         cmd = [
111             "/usr/bin/timeout",
112             "-k 9s",
113             "8s",
114             "/usr/local/bin/ffmpeg",
115             "-y",
116             "-i",
117             f"rtsp://camera:IaLaIok@{hostname}:554/live",
118             "-f",
119             "singlejpeg",
120             "-vframes",
121             "1",
122             "-vf",
123             f"scale={width}:-1",
124             "-",
125         ]
126         with subprocess.Popen(
127             cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
128         ) as proc:
129             out, _ = proc.communicate(timeout=10)
130             return out
131     except Exception as e:
132         logger.exception(e)
133     logger.warning("Failed to retrieve image from RTSP stream")
134     return None
135
136
137 @decorator_utils.timeout(seconds=30, use_signals=False)
138 def _fetch_camera_image(
139     camera_name: str, *, width: int = 256, quality: int = 70
140 ) -> RawJpgHsv:
141     """Fetch a webcam image given the camera name."""
142     logger.debug("Trying to fetch camera image from video server")
143     raw = fetch_camera_image_from_video_server(
144         camera_name, width=width, quality=quality
145     )
146     if raw is None:
147         logger.debug(
148             "Reading from video server failed; trying direct RTSP stream"
149         )
150         raw = fetch_camera_image_from_rtsp_stream(camera_name, width=width)
151     if raw is not None and len(raw) > 0:
152         tmp = np.frombuffer(raw, dtype="uint8")
153         jpg = cv2.imdecode(tmp, cv2.IMREAD_COLOR)
154         hsv = cv2.cvtColor(jpg, cv2.COLOR_BGR2HSV)
155         return RawJpgHsv(
156             raw=raw,
157             jpg=jpg,
158             hsv=hsv,
159         )
160     logger.warning(
161         "Failed to retieve image from both video server and direct RTSP stream"
162     )
163     return RawJpgHsv(None, None, None)
164
165
166 def fetch_camera_image(
167     camera_name: str, *, width: int = 256, quality: int = 70
168 ) -> RawJpgHsv:
169     try:
170         return _fetch_camera_image(camera_name, width=width, quality=quality)
171     except exceptions.TimeoutError:
172         return RawJpgHsv(None, None, None)