Skip to content

Commit

Permalink
refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
mgineer85 committed Nov 14, 2024
1 parent ef11f9f commit 5cc4e08
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 96 deletions.
63 changes: 35 additions & 28 deletions wigglecam/services/backends/cameras/abstractbackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from threading import Barrier, BrokenBarrierError, Condition, Event
from queue import Full, Queue
from threading import Barrier, BrokenBarrierError, Condition, Event, current_thread
from typing import Literal

from ....utils.stoppablethread import StoppableThread
Expand Down Expand Up @@ -43,26 +44,20 @@ def __init__(self):
self._nominal_framerate: int = None
self._started_evt: Event = None
self._camera_thread: StoppableThread = None
self._align_thread: StoppableThread = None
self._ticker_thread: StoppableThread = None
self._barrier: Barrier = None
self._current_timestamp_reference_in_queue: Queue[int] = None
self._current_timestampset: TimestampSet = None
self._align_timestampset: TimestampSet = None

# init
self._started_evt = Event()

def __repr__(self):
return f"{self.__class__}"

def get_timestamps_to_align(self) -> TimestampSet:
assert self._current_timestampset.reference is not None
assert self._current_timestampset.camera is not None

# shift reference to align with camera cycle
_current_timestampset_reference = self._current_timestampset.reference
# _current_timestampset_reference -= (1.0 / self._nominal_framerate) * 1e9

self._align_timestampset = TimestampSet(reference=_current_timestampset_reference, camera=self._current_timestampset.camera)
@abstractmethod
def _backend_align():
pass

@abstractmethod
def start(self, nominal_framerate: int = None):
Expand All @@ -74,15 +69,15 @@ def start(self, nominal_framerate: int = None):

# init common abstract props
self._nominal_framerate = nominal_framerate
self._barrier = Barrier(3, action=self.get_timestamps_to_align)
self._barrier = Barrier(2, action=self._backend_align)
self._current_timestamp_reference_in_queue: Queue[int] = Queue(maxsize=1)
self._current_timestampset = TimestampSet(None, None)
self._align_timestampset = TimestampSet(None, None)

self._camera_thread = StoppableThread(name="_camera_thread", target=self._camera_fun, args=(), daemon=True)
self._camera_thread.start()

self._align_thread = StoppableThread(name="_align_thread", target=self._align_fun, args=(), daemon=True)
self._align_thread.start()
self._ticker_thread = StoppableThread(name="_ticker_thread", target=self._ticker_fun, args=(), daemon=True)
self._ticker_thread.start()

@abstractmethod
def stop(self):
Expand All @@ -92,9 +87,9 @@ def stop(self):
if self._barrier:
self._barrier.abort()

if self._align_thread and self._align_thread.is_alive():
self._align_thread.stop()
self._align_thread.join()
if self._ticker_thread and self._ticker_thread.is_alive():
self._ticker_thread.stop()
self._ticker_thread.join()

if self._camera_thread and self._camera_thread.is_alive():
self._camera_thread.stop()
Expand All @@ -103,16 +98,17 @@ def stop(self):
@abstractmethod
def camera_alive(self) -> bool:
camera_alive = self._camera_thread and self._camera_thread.is_alive()
align_alive = self._align_thread and self._align_thread.is_alive()
ticker_alive = self._ticker_thread and self._ticker_thread.is_alive()

return camera_alive and align_alive
return camera_alive and ticker_alive

def sync_tick(self, timestamp_ns: int):
self._current_timestampset.reference = timestamp_ns
# use a queue maxlen=1 to decouple the thread calling sync_tick and the consuming thread
try:
self._barrier.wait()
except BrokenBarrierError:
logger.debug("sync barrier broke")
self._current_timestamp_reference_in_queue.put(timestamp_ns, block=True, timeout=0.5 / self._nominal_framerate)
except Full:
# this happens if the reference and camera are totally out of sync. It should recover from this state or maybe need to remove old timestamp and place new always?
print("queue full, could not place updated ref time, skip and continue...")

@abstractmethod
def start_stream(self):
Expand Down Expand Up @@ -148,6 +144,17 @@ def encode_frame_to_image(self, frame, format: Formats) -> bytes:
def _camera_fun(self):
pass

@abstractmethod
def _align_fun(self):
pass
def _ticker_fun(self):
# TODO: somewhere else:
logger.debug("starting _ticker_fun")

while not current_thread().stopped():
# - 0.2 * 1e9 # TODO: this one improved continuous capture on pi3! why?
self._current_timestampset.reference = self._current_timestamp_reference_in_queue.get(block=True, timeout=1.0)

try:
self._barrier.wait()
except BrokenBarrierError:
logger.debug("sync barrier broke")

logger.debug("left _ticker_fun")
82 changes: 31 additions & 51 deletions wigglecam/services/backends/cameras/picamera2.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,59 +234,39 @@ def recover(self):

logger.info(f"recovered, time taken: {round((time.time() - tms)*1.0e3, 0)}ms")

def _align_fun(self):
logger.debug("starting _align_fun")
timestamp_delta_ns = 0
adjust_cycle_counter = 0
adjust_amount_us = 0
# capture_time_assigned_timestamp_ns = None
nominal_frame_duration = 1.0 / self._nominal_framerate
adjust_cycle_counter = 0

# wait until all threads are actually started before process anything. mostly relevent for the _fun's defined in abstract
self._started_evt.wait(timeout=10) # we wait very long, it would usually not time out except there is a bug and this unstalls

self.recover()

while not current_thread().stopped():
# if self._capture_in_progress:
# adjust_cycle_counter = 0 # keep counter 0 until something is in progress and wait X_CYCLES until adjustment is done afterwards

try:
self._barrier.wait()
# at this point we got an updated self._align_timestampset set in barriers action.
except BrokenBarrierError:
logger.debug("sync barrier broke")
break

timestamp_delta_ns = self._align_timestampset.camera - self._align_timestampset.reference # in ns
def _backend_align(self):
global adjust_cycle_counter
# in picamera2 backend the current time ref is mapped to the frame captured in prior cycle
self._current_timestampset.reference -= int((1.0 / self._nominal_framerate) * 1e9)
timestamp_delta_ns = self._current_timestampset.camera - self._current_timestampset.reference # in ns

if adjust_cycle_counter >= ADJUST_EVERY_X_CYCLE:
adjust_cycle_counter = 0
adjust_amount_us = -timestamp_delta_ns / 1.0e3
else:
adjust_cycle_counter += 1
adjust_amount_us = 0

with self._picamera2.controls as ctrl:
fixed_frame_duration = int(nominal_frame_duration * 1e6 + adjust_amount_us)
ctrl.FrameDurationLimits = (fixed_frame_duration,) * 2

THRESHOLD_LOG = 0
if abs(timestamp_delta_ns / 1.0e6) > THRESHOLD_LOG:
# even in debug reduce verbosity a bit if all is fine and within 2ms tolerance
logger.debug(
f"🕑 clk/cam/Δ/adjust=( "
f"{self._align_timestampset.reference/1e6:.1f} / "
f"{self._align_timestampset.camera/1e6:.1f} / "
f"{timestamp_delta_ns/1e6:5.1f} / "
f"{adjust_amount_us/1e3:5.1f}) ms"
# f"FrameDuration={round(picam_metadata['FrameDuration']/1e3,1)} ms "
)
else:
pass
# silent

logger.info("_align_fun left")
if adjust_cycle_counter >= ADJUST_EVERY_X_CYCLE:
adjust_cycle_counter = 0
adjust_amount_us = -timestamp_delta_ns / 1.0e3
else:
adjust_cycle_counter += 1
adjust_amount_us = 0

with self._picamera2.controls as ctrl:
fixed_frame_duration = int(1.0 / self._nominal_framerate * 1e6 + adjust_amount_us)
ctrl.FrameDurationLimits = (fixed_frame_duration,) * 2

THRESHOLD_LOG = 0
if abs(timestamp_delta_ns / 1.0e6) > THRESHOLD_LOG:
# even in debug reduce verbosity a bit if all is fine and within 2ms tolerance
logger.debug(
f"🕑 clk/cam/Δ/adjust=( "
f"{self._current_timestampset.reference/1e6:.1f} / "
f"{self._current_timestampset.camera/1e6:.1f} / "
f"{timestamp_delta_ns/1e6:5.1f} / "
f"{adjust_amount_us/1e3:5.1f}) ms"
# f"FrameDuration={round(picam_metadata['FrameDuration']/1e3,1)} ms "
)
else:
pass
# silent

def _camera_fun(self):
logger.debug("starting _camera_fun")
Expand Down
47 changes: 30 additions & 17 deletions wigglecam/services/backends/cameras/virtualcamera.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ def __init__(self, config: ConfigBackendVirtualCamera):
self._data_condition: Condition = None
self._offset_x: int = None
self._offset_y: int = None
self._adjust_amount: float = None

# initializiation
self._data_bytes: bytes = None
self._data_condition: Condition = Condition()
self._adjust_amount: float = 0

def start(self, nominal_framerate: int = None):
super().start(nominal_framerate=nominal_framerate)
Expand Down Expand Up @@ -98,31 +100,42 @@ def wait_for_lores_image(self) -> bytes:

return self._data_bytes

def _align_fun(self):
logger.debug("starting _align_fun")

while not current_thread().stopped():
try:
self._barrier.wait()
except BrokenBarrierError:
logger.debug("sync barrier broke")
break

# simulate some processing and lower cpu use
time.sleep(0.01)

logger.info("_align_fun left")
def _backend_align(self):
# called after barrier is waiting for all as action.
# since a thread is calling this, delay in this function lead to constant offset for virtual camera
timestamp_delta_ns = self._current_timestampset.camera - self._current_timestampset.reference # in ns

self._adjust_amount = -timestamp_delta_ns / 1.0e9

THRESHOLD_LOG = 0
if abs(timestamp_delta_ns / 1.0e6) > THRESHOLD_LOG:
# even in debug reduce verbosity a bit if all is fine and within 2ms tolerance
logger.debug(
f"🕑 clk/cam/Δ/adjust=( "
f"{self._current_timestampset.reference/1e6:.1f} / "
f"{self._current_timestampset.camera/1e6:.1f} / "
f"{timestamp_delta_ns/1e6:5.1f} / "
f"{self._adjust_amount*1e3:5.1f}) ms"
)
else:
pass
# silent

def _camera_fun(self):
logger.debug("starting _camera_fun")

while not current_thread().stopped():
regular_sleep = 1.0 / self._nominal_framerate
adjust_amount_clamped = max(min(0.5 / self._nominal_framerate, self._adjust_amount), -0.5 / self._nominal_framerate)

time.sleep(regular_sleep + adjust_amount_clamped)

with self._data_condition:
self._current_timestampset.camera = time.monotonic_ns() # need to set as a backend would do so the align functions work fine
# because image is produced in same thread that is responsible for timing, it's jittery but ok for virtual cam
self._data_bytes = self._produce_dummy_image()
self._data_condition.notify_all()

time.sleep(1.0 / self._nominal_framerate)
self._current_timestampset.camera = time.monotonic_ns() # need to set as a backend would do so the align functions work fine
self._data_condition.notify_all()

# part of the alignment functions - that is not implemented, but barrier is needed
try:
Expand Down

0 comments on commit 5cc4e08

Please sign in to comment.