Compare commits
No commits in common. "59f8f0120218c466660d5be859ef811e95275c09" and "d26b93e1be1093e9d696b634dd1f7dde3bb7142a" have entirely different histories.
59f8f01202
...
d26b93e1be
10 changed files with 48 additions and 139 deletions
Binary file not shown.
|
|
@ -69,7 +69,7 @@ class StackParams(BaseModel):
|
|||
# Using docstrings under variables as this is how pdoc would expect
|
||||
# attributed to be documented
|
||||
|
||||
settling_time: float = Field(default=0.05, ge=0)
|
||||
settling_time: float = Field(default=0.3, ge=0)
|
||||
"""Time (in seconds) between moving and capturing an image"""
|
||||
|
||||
origin: StackOrigin = StackOrigin.START
|
||||
|
|
@ -345,14 +345,12 @@ class JPEGSharpnessMonitor:
|
|||
async def monitor_sharpness(self) -> None:
|
||||
"""Start monitoring sharpness metrics."""
|
||||
self.running = True
|
||||
while self.running:
|
||||
i = await self.camera.lores_mjpeg_stream.next_frame()
|
||||
entry = await self.camera.lores_mjpeg_stream.ringbuffer_entry(i)
|
||||
self._jpeg_times.append(entry.timestamp.timestamp())
|
||||
async for frame in self.camera.lores_mjpeg_stream.frame_async_generator():
|
||||
self._jpeg_times.append(time.time())
|
||||
|
||||
# JPEG sharpness metric
|
||||
if self.record & SharpnessMethod.JPEG:
|
||||
self._jpeg_sizes.append(len(entry.frame))
|
||||
self._jpeg_sizes.append(len(frame))
|
||||
|
||||
# FocusFoM metric
|
||||
if self.record & SharpnessMethod.FOCUS_FOM:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from abc import ABC, abstractmethod
|
|||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
from types import TracebackType
|
||||
from typing import Any, Literal, Mapping, Optional, Self, Union, overload
|
||||
from typing import Any, Literal, Mapping, Optional, Self
|
||||
|
||||
import numpy as np
|
||||
import piexif
|
||||
|
|
@ -216,70 +216,6 @@ class CaptureGalleryInfo(BaseModel):
|
|||
card_type: Literal["Capture"] = "Capture"
|
||||
|
||||
|
||||
class MJPEGStreamWithTimestamp(lt.outputs.MJPEGStream):
|
||||
"""An MJPEG stream where the frame time can be recorded with the frame.
|
||||
|
||||
This can be used to capture when the image was taken by the sensor if known
|
||||
rather than take the time it is added into the ring buffer.
|
||||
"""
|
||||
|
||||
def add_frame(self, frame: bytes, timestamp: Optional[datetime] = None) -> None:
|
||||
"""Add a JPEG to the MJPEG stream.
|
||||
|
||||
Modify the standard function to have an option to send in the capture time.
|
||||
|
||||
:param frame: The frame to add
|
||||
:param timestamp: The time the frame was captured.
|
||||
|
||||
:raise ValueError: if the supplied frame does not start with the JPEG
|
||||
start bytes and end with the end bytes.
|
||||
"""
|
||||
if not (
|
||||
frame[0] == 0xFF
|
||||
and frame[1] == 0xD8
|
||||
and frame[-2] == 0xFF
|
||||
and frame[-1] == 0xD9
|
||||
):
|
||||
raise ValueError("Invalid JPEG")
|
||||
with self._lock:
|
||||
entry = self._ringbuffer[(self.last_frame_i + 1) % len(self._ringbuffer)]
|
||||
entry.timestamp = timestamp if timestamp is not None else datetime.now()
|
||||
entry.frame = frame
|
||||
entry.index = self.last_frame_i + 1
|
||||
self._thing_server_interface.start_async_task_soon(
|
||||
self.notify_new_frame, entry.index
|
||||
)
|
||||
|
||||
|
||||
class MJPEGStreamDescriptorWithTimestamp(lt.outputs.MJPEGStreamDescriptor):
|
||||
"""Subclass the Labthings descriptor to return our own class."""
|
||||
|
||||
@overload
|
||||
def __get__(self, obj: Literal[None], type: type | None = None) -> Self: ...
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, obj: lt.Thing, type: type | None = None
|
||||
) -> MJPEGStreamWithTimestamp: ...
|
||||
|
||||
def __get__(
|
||||
self,
|
||||
obj: Optional[lt.Thing],
|
||||
type: type[lt.Thing] | None = None, # noqa: A002
|
||||
) -> Union[MJPEGStreamWithTimestamp | Self]:
|
||||
"""Return our own MJPEG Stream with timestamp."""
|
||||
if obj is None:
|
||||
return self
|
||||
try:
|
||||
return obj.__dict__[self.name]
|
||||
except KeyError:
|
||||
obj.__dict__[self.name] = MJPEGStreamWithTimestamp(
|
||||
**self._kwargs,
|
||||
thing_server_interface=obj._thing_server_interface,
|
||||
)
|
||||
return obj.__dict__[self.name]
|
||||
|
||||
|
||||
class BaseCamera(OFMThing, ABC):
|
||||
"""The base class for all cameras. All cameras must directly inherit from this class.
|
||||
|
||||
|
|
@ -291,8 +227,8 @@ class BaseCamera(OFMThing, ABC):
|
|||
|
||||
_all_background_detectors: Mapping[str, BackgroundDetectAlgorithm] = lt.thing_slot()
|
||||
|
||||
mjpeg_stream = MJPEGStreamDescriptorWithTimestamp()
|
||||
lores_mjpeg_stream = MJPEGStreamDescriptorWithTimestamp()
|
||||
mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
|
||||
lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
|
||||
_memory_buffer = CameraMemoryBuffer()
|
||||
supports_focus_fom: bool = False
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import tempfile
|
|||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from threading import RLock
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
|
|
@ -55,7 +54,7 @@ from openflexure_microscope_server.ui import (
|
|||
property_control_for,
|
||||
)
|
||||
|
||||
from . import BaseCamera, CaptureMode, MJPEGStreamWithTimestamp, StreamingMode
|
||||
from . import BaseCamera, CaptureMode, StreamingMode
|
||||
from . import picamera_recalibrate_utils as recalibrate_utils
|
||||
from . import picamera_tuning_file_utils as tf_utils
|
||||
|
||||
|
|
@ -76,37 +75,24 @@ class MissingCalibrationError(RuntimeError):
|
|||
class PicameraStreamOutput(Output):
|
||||
"""An Output class that sends frames to a stream."""
|
||||
|
||||
def __init__(self, stream: MJPEGStreamWithTimestamp, encoder: MJPEGEncoder) -> None:
|
||||
def __init__(self, stream: lt.outputs.MJPEGStream) -> None:
|
||||
"""Create an output that puts frames in an MJPEGStream.
|
||||
|
||||
:param stream: The labthings MJPEGStream to send frames to.
|
||||
:param encoder: The encoder that is encoding the frames. This is needed to get
|
||||
the full timestamp.
|
||||
"""
|
||||
Output.__init__(self)
|
||||
self.stream = stream
|
||||
self.encoder = encoder
|
||||
|
||||
def outputframe(
|
||||
self,
|
||||
frame: bytes,
|
||||
_keyframe: Optional[bool] = True,
|
||||
timestamp: Optional[int] = None,
|
||||
_timestamp: Optional[int] = None,
|
||||
_packet: Any = None,
|
||||
_audio: bool = False,
|
||||
) -> None:
|
||||
"""Add a frame to the stream's ringbuffer."""
|
||||
if timestamp is None or self.encoder.firsttimestamp is None:
|
||||
frame_timestamp = None
|
||||
else:
|
||||
# The difference between unix time and the CPU time. Do not cache this
|
||||
# number as it can change with clock slew or other time adjustments.
|
||||
cpu_dt = time.time() - time.monotonic()
|
||||
# Reconstruct full CPU time of the frame in us
|
||||
timestamp += self.encoder.firsttimestamp
|
||||
# Convert to datetime
|
||||
frame_timestamp = datetime.fromtimestamp(cpu_dt + timestamp / 1e6)
|
||||
self.stream.add_frame(frame, frame_timestamp)
|
||||
self.stream.add_frame(frame)
|
||||
|
||||
|
||||
class PiCamera2StreamingMode(StreamingMode):
|
||||
|
|
@ -501,16 +487,14 @@ class StreamingPiCamera2(BaseCamera, ABC):
|
|||
picam.configure(stream_config)
|
||||
LOGGER.info("Starting picamera MJPEG stream...")
|
||||
stream_name = "lores" if mode_info.use_lores_as_preview else "main"
|
||||
encoder = MJPEGEncoder(self.mjpeg_bitrate)
|
||||
picam.start_recording(
|
||||
encoder,
|
||||
PicameraStreamOutput(self.mjpeg_stream, encoder),
|
||||
MJPEGEncoder(self.mjpeg_bitrate),
|
||||
PicameraStreamOutput(self.mjpeg_stream),
|
||||
name=stream_name,
|
||||
)
|
||||
encoder = MJPEGEncoder(100000000)
|
||||
picam.start_encoder(
|
||||
encoder,
|
||||
PicameraStreamOutput(self.lores_mjpeg_stream, encoder),
|
||||
MJPEGEncoder(100000000),
|
||||
PicameraStreamOutput(self.lores_mjpeg_stream),
|
||||
name="lores",
|
||||
)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import io
|
|||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from types import TracebackType
|
||||
from typing import Optional, Self, overload
|
||||
|
|
@ -468,10 +467,9 @@ class SimulatedCamera(BaseCamera):
|
|||
last_frame_t = time.time()
|
||||
|
||||
frame = self.generate_frame()
|
||||
timestamp = datetime.fromtimestamp(last_frame_t)
|
||||
self.mjpeg_stream.add_frame(_frame2bytes(frame), timestamp)
|
||||
self.mjpeg_stream.add_frame(_frame2bytes(frame))
|
||||
ds_frame = frame.resize((320, 240), resample=Image.Resampling.NEAREST)
|
||||
self.lores_mjpeg_stream.add_frame(_frame2bytes(ds_frame), timestamp)
|
||||
self.lores_mjpeg_stream.add_frame(_frame2bytes(ds_frame))
|
||||
|
||||
@lt.action
|
||||
def discard_frames(self) -> None:
|
||||
|
|
|
|||
|
|
@ -228,11 +228,3 @@ a:hover {
|
|||
.uk-pagination > .uk-active > * {
|
||||
color: @global-primary-background;
|
||||
}
|
||||
|
||||
/*
|
||||
* Checkboxes
|
||||
*/
|
||||
|
||||
.uk-checkbox {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
|
@ -20,7 +20,8 @@
|
|||
:value="option"
|
||||
:checked="modelValue.includes(option)"
|
||||
@change="toggleOption(option)"
|
||||
/>{{ option }}
|
||||
/>
|
||||
{{ option }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -198,9 +198,7 @@ export default {
|
|||
.log-summary-text {
|
||||
text-align: center;
|
||||
font-size: large;
|
||||
|
||||
/* Reserve two lines even when message is short. line-clamp handles truncation */
|
||||
min-height: calc(2 * 1.4em);
|
||||
height: calc(2 * 1.4em); /* height of element is 2 lines */
|
||||
line-height: 1.4em;
|
||||
padding: 0 5px; /* Give some padding so there's room for ... to not expand too far */
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ and zero position buttons. It also includes the d-pad.
|
|||
<li class="uk-closed">
|
||||
<a class="uk-accordion-title" href="#">Position</a>
|
||||
<div class="uk-accordion-content">
|
||||
<form>
|
||||
<!-- Text boxes to set and view position -->
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
|
|
@ -36,6 +37,7 @@ and zero position buttons. It also includes the d-pad.
|
|||
@error="modalError"
|
||||
/>
|
||||
</p>
|
||||
</form>
|
||||
<action-button
|
||||
thing="stage"
|
||||
action="set_zero_position"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue