You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
6.5 KiB
Python
200 lines
6.5 KiB
Python
import socket
|
|
from pathlib import Path
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
|
|
RAW_FRAME_CHANNELS = {
|
|
"bgr24": 3,
|
|
"rgb24": 3,
|
|
"gray8": 1,
|
|
"gray16": 2,
|
|
"yuyv422": 2,
|
|
}
|
|
|
|
|
|
def raw_frame_size(encoding, width, height):
|
|
channels = RAW_FRAME_CHANNELS.get(str(encoding or "").lower())
|
|
return max(1, int(width)) * max(1, int(height)) * channels if channels else 0
|
|
|
|
|
|
def decode_frame_data(data, encoding, width, height):
|
|
encoding = str(encoding or "auto").lower()
|
|
width = max(1, int(width))
|
|
height = max(1, int(height))
|
|
raw = np.frombuffer(data, dtype=np.uint8)
|
|
if encoding == "auto":
|
|
frame = cv2.imdecode(raw, cv2.IMREAD_COLOR)
|
|
if frame is None:
|
|
raise ValueError("frame is not JPEG/PNG")
|
|
return frame
|
|
|
|
shapes = {
|
|
"bgr24": (height, width, 3),
|
|
"rgb24": (height, width, 3),
|
|
"gray8": (height, width),
|
|
"gray16": (height, width),
|
|
"yuyv422": (height, width, 2),
|
|
}
|
|
if encoding not in shapes:
|
|
raise ValueError(f"unsupported frame encoding: {encoding}")
|
|
dtype = np.dtype("<u2") if encoding == "gray16" else np.dtype(np.uint8)
|
|
expected = int(np.prod(shapes[encoding]) * dtype.itemsize)
|
|
if len(data) != expected:
|
|
raise ValueError(f"raw frame size {len(data)} != {expected}")
|
|
pixels = np.frombuffer(data, dtype=dtype).reshape(shapes[encoding])
|
|
if encoding == "bgr24":
|
|
return pixels.copy()
|
|
if encoding == "rgb24":
|
|
return cv2.cvtColor(pixels, cv2.COLOR_RGB2BGR)
|
|
if encoding == "yuyv422":
|
|
return cv2.cvtColor(pixels, cv2.COLOR_YUV2BGR_YUY2)
|
|
if encoding == "gray16":
|
|
pixels = (pixels >> 8).astype(np.uint8)
|
|
return cv2.cvtColor(pixels, cv2.COLOR_GRAY2BGR)
|
|
|
|
|
|
class DelimitedFrameCapture:
|
|
"""Reads encoded or raw frames separated by one byte."""
|
|
|
|
MAX_FRAME_SIZE = 64 * 1024 * 1024
|
|
|
|
def __init__(
|
|
self,
|
|
source=None,
|
|
host="0.0.0.0",
|
|
port=59005,
|
|
separator=0,
|
|
encoding="auto",
|
|
width=1280,
|
|
height=720,
|
|
fps=30.0,
|
|
):
|
|
self.source = Path(source) if source is not None else None
|
|
self.host = str(host)
|
|
self.port = int(port)
|
|
self.separator = bytes((int(separator) & 0xFF,))
|
|
self.encoding = str(encoding or "auto").lower()
|
|
self.width = max(1, int(width))
|
|
self.height = max(1, int(height))
|
|
self.fps = max(1.0, float(fps))
|
|
self.frames_read = 0
|
|
self.dropped_frames = 0
|
|
self.last_error = ""
|
|
self._buffer = bytearray()
|
|
self._frames = []
|
|
self._file = None
|
|
self._socket = None
|
|
try:
|
|
if self.source is not None:
|
|
self._file = self.source.open("rb", buffering=8 * 1024 * 1024)
|
|
else:
|
|
self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
self._socket.bind((self.host, self.port))
|
|
self.port = int(self._socket.getsockname()[1])
|
|
self._socket.settimeout(0.5)
|
|
except OSError as exc:
|
|
self.last_error = str(exc)
|
|
self.release()
|
|
|
|
def isOpened(self):
|
|
return self._file is not None or self._socket is not None
|
|
|
|
def _next_chunk(self):
|
|
if self._file is not None:
|
|
return self._file.read(1024 * 1024)
|
|
while self._socket is not None:
|
|
try:
|
|
return self._socket.recvfrom(65535)[0]
|
|
except socket.timeout:
|
|
continue
|
|
except OSError as exc:
|
|
self.last_error = str(exc)
|
|
return b""
|
|
return b""
|
|
|
|
def _split(self, chunk):
|
|
expected = self._raw_frame_size()
|
|
if self._socket is not None and expected:
|
|
if chunk == self.separator:
|
|
if self._buffer:
|
|
self._buffer.clear()
|
|
self.dropped_frames += 1
|
|
return
|
|
self._buffer.extend(chunk)
|
|
while len(self._buffer) >= expected:
|
|
self._frames.append(bytes(self._buffer[:expected]))
|
|
del self._buffer[:expected]
|
|
if self._buffer.startswith(self.separator):
|
|
del self._buffer[:1]
|
|
if len(self._buffer) > self.MAX_FRAME_SIZE:
|
|
self._buffer.clear()
|
|
self.dropped_frames += 1
|
|
return
|
|
|
|
self._buffer.extend(chunk)
|
|
parts = self._buffer.split(self.separator)
|
|
self._buffer = bytearray(parts.pop())
|
|
self._frames.extend(part for part in parts if part)
|
|
if len(self._buffer) > self.MAX_FRAME_SIZE:
|
|
self._buffer.clear()
|
|
self.dropped_frames += 1
|
|
|
|
def _raw_frame_size(self):
|
|
return raw_frame_size(self.encoding, self.width, self.height)
|
|
|
|
def _decode(self, data):
|
|
frame = decode_frame_data(data, self.encoding, self.width, self.height)
|
|
self.height, self.width = frame.shape[:2]
|
|
return frame
|
|
|
|
def read(self):
|
|
while self.isOpened():
|
|
if not self._frames:
|
|
chunk = self._next_chunk()
|
|
if not chunk:
|
|
if self._file is not None and self._buffer:
|
|
self._frames.append(bytes(self._buffer))
|
|
self._buffer.clear()
|
|
else:
|
|
return False, None
|
|
else:
|
|
self._split(chunk)
|
|
continue
|
|
data = self._frames.pop(0)
|
|
try:
|
|
frame = self._decode(data)
|
|
except ValueError as exc:
|
|
self.last_error = str(exc)
|
|
self.dropped_frames += 1
|
|
continue
|
|
self.frames_read += 1
|
|
return True, frame
|
|
return False, None
|
|
|
|
def get(self, prop):
|
|
if prop == cv2.CAP_PROP_FRAME_WIDTH:
|
|
return float(self.width)
|
|
if prop == cv2.CAP_PROP_FRAME_HEIGHT:
|
|
return float(self.height)
|
|
if prop == cv2.CAP_PROP_FPS:
|
|
return self.fps
|
|
if prop == cv2.CAP_PROP_POS_FRAMES:
|
|
return float(self.frames_read)
|
|
if prop == cv2.CAP_PROP_POS_MSEC:
|
|
return 1000.0 * self.frames_read / self.fps
|
|
return 0.0
|
|
|
|
def set(self, _prop, _value):
|
|
return False
|
|
|
|
def release(self):
|
|
if self._file is not None:
|
|
self._file.close()
|
|
self._file = None
|
|
sock, self._socket = self._socket, None
|
|
if sock is not None:
|
|
sock.close()
|