112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
"""OpenFlexure Microscope OpenCV Camera
|
|
|
|
This module defines a Thing that is responsible for using the stage and
|
|
camera together to perform an autofocus routine.
|
|
|
|
See repository root for licensing information.
|
|
"""
|
|
from __future__ import annotations
|
|
import io
|
|
import json
|
|
import logging
|
|
from typing import Literal, Optional
|
|
from threading import Thread
|
|
|
|
import cv2
|
|
import piexif
|
|
|
|
from labthings_fastapi.utilities import get_blocking_portal
|
|
from labthings_fastapi.decorators import thing_action, thing_property
|
|
from labthings_fastapi.dependencies.metadata import GetThingStates
|
|
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
|
from labthings_fastapi.outputs.blob import BlobOutput
|
|
from labthings_fastapi.types.numpy import NDArray
|
|
|
|
from . import Camera
|
|
|
|
|
|
class JPEGBlob(BlobOutput):
|
|
media_type = "image/jpeg"
|
|
|
|
|
|
class OpenCVCamera(Camera):
|
|
"""A Thing representing an OpenCV camera"""
|
|
def __init__(self, camera_index: int=0):
|
|
self.camera_index = camera_index
|
|
self._capture_thread: Optional[Thread] = None
|
|
self._capture_enabled = False
|
|
|
|
def __enter__(self):
|
|
self.cap = cv2.VideoCapture(self.camera_index)
|
|
self._capture_enabled = True
|
|
self._capture_thread = Thread(target=self._capture_frames)
|
|
self._capture_thread.start()
|
|
return self
|
|
|
|
def __exit__(self, _exc_type, _exc_value, _traceback):
|
|
if self.stream_active:
|
|
self._capture_enabled = False
|
|
self._capture_thread.join()
|
|
self.cap.release()
|
|
|
|
@thing_property
|
|
def stream_active(self) -> bool:
|
|
"Whether the MJPEG stream is active"
|
|
if self._capture_enabled and self._capture_thread:
|
|
return self._capture_thread.is_alive()
|
|
return False
|
|
mjpeg_stream = MJPEGStreamDescriptor()
|
|
lores_mjpeg_stream = MJPEGStreamDescriptor()
|
|
|
|
def _capture_frames(self):
|
|
portal = get_blocking_portal(self)
|
|
while self._capture_enabled:
|
|
ret, frame = self.cap.read()
|
|
if not ret:
|
|
logging.error(f"Failed to capture frame from camera {self.camera_index}")
|
|
break
|
|
jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
|
|
self.mjpeg_stream.add_frame(jpeg, portal)
|
|
jpeg_lores = cv2.imencode(".jpg", cv2.resize(frame, (320, 240)))[1].tobytes()
|
|
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
|
|
|
|
@thing_action
|
|
def capture_array(
|
|
self,
|
|
resolution: Literal["main", "full"] = "full",
|
|
) -> NDArray:
|
|
"""Acquire one image from the camera and return as an array
|
|
|
|
This function will produce a nested list containing an uncompressed RGB image.
|
|
It's likely to be highly inefficient - raw and/or uncompressed captures using
|
|
binary image formats will be added in due course.
|
|
"""
|
|
ret, frame = self.cap.read()
|
|
if not ret:
|
|
raise RuntimeError(f"Failed to capture frame from camera {self.camera_index}")
|
|
return frame
|
|
|
|
@thing_action
|
|
def capture_jpeg(
|
|
self,
|
|
metadata_getter: GetThingStates,
|
|
resolution: Literal["main", "full"] = "main",
|
|
) -> JPEGBlob:
|
|
"""Acquire one image from the camera and return as a JPEG blob
|
|
|
|
This function will produce a JPEG image.
|
|
"""
|
|
frame = self.capture_array()
|
|
jpeg = cv2.imencode(".jpg", frame)[1].tobytes()
|
|
exif_dict = {
|
|
"Exif": {
|
|
piexif.ExifIFD.UserComment: json.dumps(metadata_getter()).encode("utf-8")
|
|
},
|
|
"GPS": {},
|
|
"Interop": {},
|
|
"1st": {},
|
|
"thumbnail": None,
|
|
}
|
|
output = io.BytesIO()
|
|
piexif.insert(piexif.dump(exif_dict), jpeg, output)
|
|
return JPEGBlob.from_bytes(output.getvalue())
|