Skip to content

Commit

Permalink
first version of live view (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
irkri committed Oct 27, 2024
1 parent ca34eb9 commit fc5c66b
Show file tree
Hide file tree
Showing 7 changed files with 646 additions and 458 deletions.
83 changes: 83 additions & 0 deletions blitz/data/live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import cv2
import numpy as np
from PyQt5.QtCore import QObject, QThread, pyqtSignal

from ..tools import log
from .load import DataLoader


class CamWatcher(QObject):

on_next_image = pyqtSignal(object)

def __init__(
self,
cam: int,
buffer: int,
frame_rate: int,
grayscale: bool,
) -> None:
super().__init__()
self.cam = cv2.VideoCapture(cam)
if self.cam is None or not self.cam.isOpened():
self.is_available = False
log(f"Error: camera {cam} not found", color="red")
else:
self.is_available = True
width = int(self.cam.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(self.cam.get(cv2.CAP_PROP_FRAME_HEIGHT))
self._index = 0
self.grayscale = grayscale
self.frame_rate = frame_rate
if self.grayscale:
self.output = np.zeros((buffer, width, height))
else:
self.output = np.zeros((buffer, width, height, 3))
self.watching = True

def watch(self) -> None:
while self.watching:
_, frame = self.cam.read()
self.output[:-1] = self.output[1:]
if self.grayscale:
frame: np.ndarray = np.sum(
frame * np.array([0.2989, 0.5870, 0.1140]),
axis=-1,
)
self.output[-1] = frame.swapaxes(0, 1)
self.on_next_image.emit(DataLoader.from_array(self.output))
QThread.msleep(self.frame_rate)


class LiveView(QObject):

on_next_image = pyqtSignal(object)

def __init__(
self,
cam: int,
buffer: int,
frame_rate: int,
grayscale: bool,
) -> None:
super().__init__()
self._watcher = CamWatcher(cam, buffer, frame_rate, grayscale)
self._reader_thread = QThread()

@property
def available(self) -> bool:
return self._watcher.is_available

def send(self, img: np.ndarray) -> None:
self.on_next_image.emit(img)

def start(self) -> None:
self._watcher.moveToThread(self._reader_thread)
self._reader_thread.started.connect(self._watcher.watch)
self._watcher.on_next_image.connect(self.send)
self._reader_thread.start()

def end(self) -> None:
self._watcher.watching = False
self._reader_thread.quit()
self._reader_thread.wait()
20 changes: 20 additions & 0 deletions blitz/data/load.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,26 @@ def _load_single_array(
)
return array, metadata

@staticmethod
def from_array(
array: np.ndarray,
name: str = "",
) -> ImageData:
if array.ndim == 2:
array = array[np.newaxis, ...]
metadata = [MetaData(
file_name=f"{name}-{i}",
file_size_MB=array.nbytes/2**20,
size=(array.shape[0], array.shape[1]),
dtype=array.dtype,
bit_depth=8*array.dtype.itemsize,
color_model=(
"rgb" if (array.ndim == 4 and array.shape[-1] == 3)
else "grayscale"
),
) for i in range(array.shape[0])]
return ImageData(array, metadata)

@staticmethod
def from_text(
text: str,
Expand Down
2 changes: 1 addition & 1 deletion blitz/data/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _finish_connect(self, file_name: str | None) -> None:
self._connect_thread.quit()
self._connect_thread.wait()

def _start_download(self, file_name: str):
def _start_download(self, file_name: str) -> None:
target = self._target
if not target.endswith("/"):
target += "/"
Expand Down
31 changes: 31 additions & 0 deletions blitz/layout/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .. import __version__, settings
from ..data.image import ImageData
from ..data.web import WebDataLoader
from ..data.live import LiveView
from ..tools import LoadingManager, get_available_ram, log
from .rosee import ROSEEAdapter
from .tof import TOFAdapter
Expand Down Expand Up @@ -43,6 +44,7 @@ def __init__(self) -> None:
(self.ui.textbox_rosee_slope_h,
self.ui.textbox_rosee_slope_v),
)
self._live_view: None | LiveView = None
self.setup_connections()
self.reset_options()

Expand Down Expand Up @@ -107,6 +109,8 @@ def setup_connections(self) -> None:
self.ui.button_disconnect.pressed.connect(
lambda: self.end_web_connection(None)
)
self.ui.button_watch_start.pressed.connect(self.start_live_view)
self.ui.button_watch_stop.pressed.connect(self.end_live_view)
self.ui.checkbox_flipx.clicked.connect(
lambda: self.ui.image_viewer.manipulate("flip_x")
)
Expand Down Expand Up @@ -226,6 +230,8 @@ def setup_connections(self) -> None:
def reset_options(self) -> None:
self.ui.spinbox_max_ram.setRange(.1, .8 * get_available_ram())
self.ui.spinbox_max_ram.setValue(settings.get("default/max_ram"))
self.ui.spinbox_camera.setValue(0)
self.ui.spinbox_frame_pause.setValue(500)
self.ui.button_apply_mask.setChecked(False)
self.ui.checkbox_flipx.setChecked(settings.get("data/flipped_x"))
self.ui.checkbox_flipy.setChecked(settings.get("data/flipped_y"))
Expand Down Expand Up @@ -607,6 +613,31 @@ def end_web_connection(self, img: ImageData | None) -> None:
self.ui.button_disconnect.setEnabled(False)
self._web_connection.deleteLater()

def start_live_view(self) -> None:
if self._live_view is not None:
return
self._live_view = LiveView(
cam=self.ui.spinbox_camera.value(),
buffer=self.ui.spinbox_buffer_size.value(),
frame_rate=self.ui.spinbox_frame_pause.value(),
grayscale=self.ui.checkbox_load_grayscale.isChecked(),
)
if not self._live_view.available:
self._live_view = None
return
self._live_view.on_next_image.connect(self.ui.image_viewer.set_image)
self._live_view.start()

def end_live_view(self) -> None:
if self._live_view is None:
return
self._live_view.end()
self._live_view.on_next_image.disconnect(
self.ui.image_viewer.set_image
)
self._live_view.deleteLater()
self._live_view = None

def load_images_adapter(
self,
file_path: Optional[Path | str] = None,
Expand Down
31 changes: 31 additions & 0 deletions blitz/layout/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ def setup_option_dock(self) -> None:
self.spinbox_max_ram.setSingleStep(0.1)
self.spinbox_max_ram.setPrefix("Max. RAM: ")
file_layout.addWidget(self.spinbox_max_ram)
self.checkbox_sync_file = QCheckBox("Save project file")
self.checkbox_sync_file.setChecked(True)
file_layout.addWidget(self.checkbox_sync_file)
load_btn_lay = QHBoxLayout()
self.button_open_file = QPushButton("Open File")
load_btn_lay.addWidget(self.button_open_file)
Expand All @@ -296,6 +299,34 @@ def setup_option_dock(self) -> None:
connect_lay.addWidget(self.button_connect, 2, 0, 2, 1)
connect_lay.addWidget(self.button_disconnect, 2, 1, 2, 1)
file_layout.addLayout(connect_lay)
live_label = QLabel("Live view")
live_label.setStyleSheet(style_heading)
file_layout.addWidget(live_label)
label_camera = QLabel("Camera")
self.spinbox_camera = QSpinBox()
self.spinbox_frame_pause = QSpinBox()
self.spinbox_frame_pause.setSuffix(" ms")
self.spinbox_frame_pause.setMinimum(10)
self.spinbox_frame_pause.setMaximum(10000)
label_rate = QLabel("Frame rate")
label_buffer = QLabel("Buffer size")
self.spinbox_buffer_size = QSpinBox()
self.spinbox_buffer_size.setMinimum(1)
self.spinbox_buffer_size.setMaximum(1000)
gridlay_camera = QGridLayout()
gridlay_camera.addWidget(label_camera, 0, 0, 1, 1)
gridlay_camera.addWidget(self.spinbox_camera, 0, 1, 1, 1)
gridlay_camera.addWidget(label_rate, 1, 0, 1, 1)
gridlay_camera.addWidget(self.spinbox_frame_pause, 1, 1, 1, 1)
gridlay_camera.addWidget(label_buffer, 2, 0, 1, 1)
gridlay_camera.addWidget(self.spinbox_buffer_size, 2, 1, 1, 1)
file_layout.addLayout(gridlay_camera)
self.button_watch_start = QPushButton("start")
self.button_watch_stop = QPushButton("stop")
watch_lay = QHBoxLayout()
watch_lay.addWidget(self.button_watch_start)
watch_lay.addWidget(self.button_watch_stop)
file_layout.addLayout(watch_lay)
file_layout.addStretch()
self.create_option_tab(file_layout, "File")

Expand Down
Loading

0 comments on commit fc5c66b

Please sign in to comment.