Skip to content

Commit 5f53374

Browse files
committed
Implemented Aspect of takam's suggestions
- Black Formatting with line length = 120 - Added FPS indicator color for preview tab - Added updates to valid exposure times when fps is modified - Implemented proper deinitialisation of the camera api from the preview window. This was causing conflicts of which camera pointer was getting the frames i wanted. - Removed unused libraries from requirements.txt - Added default config values for missing values in the .json camera settings
1 parent afdfb51 commit 5f53374

13 files changed

+110
-57
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
*.pyc
33
vscode/
44
.vscode/*
5+
pyproject.toml
6+
57
ErrorLog.txt
68
todo.md
79
test_notebook.ipynb
810
_installation/SDK/*
911
_installation/miniconda.exe
1012
_installation/spinview/spinnakerSDK.exe
11-
config/camera_configs.json
13+
config/camera_configs.json

GUI/GUI_main.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ def __init__(self, parsed_args):
4949
self.show()
5050

5151
def on_tab_change(self):
52-
"""Function that is run on tab change"""
52+
"""Function that is run on tab change: Deselect the tab you are in before selecting a new tab"""
5353
if self.tab_widget.currentIndex() == 0:
54-
self.video_capture_tab.tab_selected()
5554
self.camera_setup_tab.tab_deselected()
55+
self.video_capture_tab.tab_selected()
5656
else:
5757
self.video_capture_tab.tab_deselected()
5858
self.camera_setup_tab.tab_selected()

GUI/camera_setup_tab.py

+51-15
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@
1515
from PyQt6.QtCore import QTimer
1616
from dataclasses import asdict
1717

18-
from config.config import ffmpeg_config, paths_config
19-
from .utility import CameraSettingsConfig, find_all_cameras, load_saved_setups, load_camera_dict, init_camera_api, get_valid_supported_encoder_formats
20-
from .preview_dialog import CameraPreviewDialog
18+
from config.config import ffmpeg_config, paths_config, default_camera_config
19+
from .utility import (
20+
CameraSettingsConfig,
21+
find_all_cameras,
22+
load_saved_setups,
23+
load_camera_dict,
24+
init_camera_api,
25+
get_valid_supported_encoder_formats,
26+
)
27+
from .preview_dialog import CameraPreviewWidget
2128

2229

2330
class CamerasTab(QWidget):
@@ -61,6 +68,11 @@ def tab_selected(self):
6168
def tab_deselected(self):
6269
"""Called when tab deselected."""
6370
self.refresh_timer.stop()
71+
# Deinitialise all camera apis on tab being deselected
72+
for unique_id in self.setups:
73+
if self.GUI.preview_showing:
74+
self.setups[unique_id].close_preview_camera()
75+
self.setups[unique_id].camera_api.stop_capturing()
6476

6577
def get_saved_setups(self, unique_id: str = None, name: str = None) -> CameraSettingsConfig:
6678
"""Get a saved Setup_info object from a name or unique_id from self.saved_setups."""
@@ -117,11 +129,11 @@ def refresh(self):
117129
setups_table=self.camera_table,
118130
name=None,
119131
unique_id=unique_id,
120-
fps="30",
121-
pxl_fmt="Mono8",
122-
downsampling_factor=1,
123-
exposure_time=15000,
124-
gain=0,
132+
fps=default_camera_config["fps"],
133+
pxl_fmt=default_camera_config["pxl_fmt"],
134+
downsampling_factor=default_camera_config["downsampling_factor"],
135+
exposure_time=default_camera_config["exposure_time"],
136+
gain=default_camera_config["gain"],
125137
)
126138

127139
self.update_saved_setups(self.setups[unique_id])
@@ -196,7 +208,17 @@ def remove(self, unique_id):
196208
class Camera_table_item:
197209
"""Class representing single camera in the Camera Tab table."""
198210

199-
def __init__(self, setups_table, name, unique_id, fps, exposure_time, gain, pxl_fmt, downsampling_factor):
211+
def __init__(
212+
self,
213+
setups_table,
214+
name,
215+
unique_id,
216+
fps,
217+
exposure_time,
218+
gain,
219+
pxl_fmt,
220+
downsampling_factor,
221+
):
200222
self.setups_table = setups_table
201223
self.setups_tab = setups_table.setups_tab
202224
self.GUI = self.setups_tab.GUI
@@ -211,6 +233,7 @@ def __init__(self, setups_table, name, unique_id, fps, exposure_time, gain, pxl_
211233
self.label = self.label if self.label is not None else self.unique_id
212234
self.GUI.preview_showing = False
213235
self.camera_api = init_camera_api(_id=self.unique_id)
236+
214237
# Name edit
215238
self.name_edit = QLineEdit()
216239
if self.label:
@@ -232,6 +255,7 @@ def __init__(self, setups_table, name, unique_id, fps, exposure_time, gain, pxl_
232255
self.fps = str(self.fps[0])
233256
self.fps_edit.setValue(int(self.fps))
234257
self.fps_edit.valueChanged.connect(self.camera_fps_changed)
258+
self.fps_edit.setEnabled(False)
235259

236260
# Exposure time edit
237261
self.exposure_time_edit = QSpinBox()
@@ -241,6 +265,8 @@ def __init__(self, setups_table, name, unique_id, fps, exposure_time, gain, pxl_
241265
if self.exposure_time:
242266
self.exposure_time_edit.setValue(int(self.exposure_time))
243267
self.exposure_time_edit.valueChanged.connect(self.camera_exposure_time_changed)
268+
self.exposure_time_edit.setEnabled(False)
269+
244270
# Gain edit
245271
self.gain_edit = QSpinBox()
246272
self.gain_edit.setRange(*self.camera_api.get_gain_range())
@@ -290,7 +316,7 @@ def camera_fps_changed(self):
290316
self.setups_tab.update_saved_setups(setup=self)
291317
if self.GUI.preview_showing is True:
292318
self.setups_tab.camera_preview.camera_api.set_frame_rate(self.fps)
293-
# reset the range of the exposure time
319+
294320
self.exposure_time_edit.setRange(*self.camera_api.get_exposure_time_range())
295321

296322
def camera_pxl_fmt_changed(self):
@@ -325,13 +351,23 @@ def camera_downsampling_factor(self):
325351
def open_preview_camera(self):
326352
"""Button to preview the camera in the row"""
327353
if self.GUI.preview_showing:
328-
self.setups_tab.camera_preview.close()
329-
self.GUI.preview_showing = False
330-
self.setups_tab.camera_preview = CameraPreviewDialog(gui=self.GUI, unique_id=self.unique_id)
354+
self.close_preview_camera()
355+
self.setups_tab.camera_preview = CameraPreviewWidget(
356+
gui=self.GUI, camera_table_item=self, unique_id=self.unique_id
357+
) # camera_api=self.camera_api)
331358
self.setups_tab.page_layout.addWidget(self.setups_tab.camera_preview)
332359
self.GUI.preview_showing = True
333-
# refresh timer off
334-
360+
361+
self.fps_edit.setEnabled(self.GUI.preview_showing)
362+
self.exposure_time_edit.setEnabled(self.GUI.preview_showing)
363+
364+
def close_preview_camera(self):
365+
self.setups_tab.camera_preview.close()
366+
self.GUI.preview_showing = False
367+
368+
self.fps_edit.setEnabled(self.GUI.preview_showing)
369+
self.exposure_time_edit.setEnabled(self.GUI.preview_showing)
370+
335371
def getCameraSettingsConfig(self):
336372
"""Get the camera settings config datastruct from the setups table."""
337373
return CameraSettingsConfig(

GUI/camera_widget.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
QWidget,
2121
)
2222

23-
from .utility import cbox_update_options, CameraSetupConfig, init_camera_api, validate_ffmpeg_path
23+
from .utility import (
24+
cbox_update_options,
25+
CameraSetupConfig,
26+
init_camera_api,
27+
validate_ffmpeg_path,
28+
)
2429
from .message_dialogs import show_warning_message
2530
from config.config import ffmpeg_config, paths_config
2631

@@ -39,7 +44,9 @@ def __init__(self, parent, label, subject_id):
3944
self.paths["FFMPEG"]
4045
if validate_ffmpeg_path(self.paths["FFMPEG"])
4146
else show_warning_message(
42-
f"FFMPEG path {self.paths['FFMPEG']} not valid", okayButtonPresent=False, ignoreButtonPresent=True
47+
f"FFMPEG path {self.paths['FFMPEG']} not valid",
48+
okayButtonPresent=False,
49+
ignoreButtonPresent=True,
4350
)
4451
)
4552

GUI/message_dialogs.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from PyQt6.QtWidgets import QMessageBox
22

3+
34
def show_info_message(input_text: str):
45
"""
56
Display an information message dialog box.
@@ -13,9 +14,7 @@ def show_info_message(input_text: str):
1314
info_dialog.exec()
1415

1516

16-
def show_warning_message(
17-
input_text: str, okayButtonPresent: bool, ignoreButtonPresent: bool
18-
) -> bool:
17+
def show_warning_message(input_text: str, okayButtonPresent: bool, ignoreButtonPresent: bool) -> bool:
1918
"""
2019
Displays a warning message with Ignore button.
2120
Returns True if ignore button clicked else False.

GUI/preview_dialog.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
from collections import deque
3-
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton
3+
from PyQt6.QtWidgets import QVBoxLayout, QPushButton, QWidget
44
from PyQt6.QtCore import QTimer
55
from PyQt6.QtGui import QIcon, QFont
66
import pyqtgraph as pg
@@ -10,21 +10,23 @@
1010
from config.config import paths_config, gui_config
1111

1212

13-
class CameraPreviewDialog(QDialog):
13+
class CameraPreviewWidget(QWidget):
1414
"""Dialog for previewing the video feed from the setups tab"""
1515

16-
def __init__(self, gui, unique_id: str):
16+
def __init__(self, gui, camera_table_item, unique_id): # camera_api):
1717
super().__init__()
1818
# self.setups_tab = parent
19-
self.main_gui = gui
20-
self.window_title = f"Camera {unique_id}"
19+
self.GUI = gui
20+
self.camera_table_item = camera_table_item
2121
self.unique_id = unique_id
22+
# self.unique_id = self.camera_api.unique_id
23+
self.window_title = f"Camera {self.unique_id}"
2224
self.paths = paths_config
2325

2426
self.setWindowTitle(self.window_title)
2527
icon = QIcon(os.path.join(self.paths["assets_dir"], "logo.svg"))
2628
self.setWindowIcon(icon)
27-
main_gui_geometry = self.main_gui.geometry()
29+
main_gui_geometry = self.GUI.geometry()
2830
self.setGeometry(
2931
main_gui_geometry.x() + main_gui_geometry.width() + 10,
3032
main_gui_geometry.y(),
@@ -84,8 +86,8 @@ def display_data(self):
8486
self.frame_timestamps.extend(self.buffered_data["timestamps"])
8587
avg_time_diff = (self.frame_timestamps[-1] - self.frame_timestamps[0]) / (self.frame_timestamps.maxlen - 1)
8688
calculated_framerate = 1e9 / avg_time_diff
87-
# Display timestamps data
88-
self.frame_rate_text.setText(f"FPS: {calculated_framerate:.2f}", color="r")
89+
color = "r" if (abs(calculated_framerate - int(self.camera_table_item.fps)) > 1) else "g"
90+
self.frame_rate_text.setText(f"FPS: {calculated_framerate:.2f}", color=color)
8991
self.exposure_time_text.setText(
9092
f"Exposure Time (us) : {self.camera_api.get_exposure_time():.2f}",
9193
color="magenta",
@@ -102,14 +104,18 @@ def start_timer(self):
102104

103105
def closeEvent(self, event):
104106
"""Handle the close event to stop the timer and release resources"""
105-
if hasattr(self, "timer"):
106-
self.display_update_timer.stop()
107107
self.camera_api.stop_capturing()
108-
self.main_gui.preview_showing = False
108+
self.display_update_timer.stop()
109+
self.GUI.preview_showing = False
110+
self.camera_table_item.exposure_time_edit.setEnabled(False)
111+
self.camera_table_item.fps_edit.setEnabled(False)
112+
self.close()
113+
super().closeEvent(event)
109114
event.accept()
110115

111116
def resizeEvent(self, event, scale_factor=0.02):
112117
"""Scale the font size to the window"""
118+
super().resizeEvent(event)
113119
font_size = int(min(self.width(), self.height()) * scale_factor)
114120
self.frame_rate_text.setFont(QFont("Arial", font_size))
115121
self.exposure_time_text.setFont(QFont("Arial", font_size))

GUI/utility.py

+10-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Dict, Any
88
from dataclasses import dataclass
99

10-
from config.config import ffmpeg_config
10+
from config.config import ffmpeg_config, default_camera_config
1111

1212
# Custom data classes -----------------------------------------------------------------
1313

@@ -84,6 +84,7 @@ def get_valid_ffmpeg_encoders() -> list:
8484
valid_encoders_keys.append(key)
8585
return valid_encoders_keys
8686

87+
8788
def get_valid_supported_encoder_formats(camera_formats: list[str]) -> list[str]:
8889
"""Get the list of supported encoder formats by comparing the camera formats to the config pxl_fmt keys"""
8990
supported_formats = []
@@ -92,6 +93,7 @@ def get_valid_supported_encoder_formats(camera_formats: list[str]) -> list[str]:
9293
supported_formats.append(fmt)
9394
return supported_formats
9495

96+
9597
def validate_ffmpeg_path(ffmpeg_path):
9698
"""Validate the provided ffmpeg path."""
9799
if type(ffmpeg_path) is type(None):
@@ -194,13 +196,13 @@ def load_saved_setups(camera_data) -> list[CameraSettingsConfig]:
194196
for cam in camera_data:
195197
setups_from_database.append(
196198
CameraSettingsConfig(
197-
name=cam["name"],
198-
unique_id=cam["unique_id"],
199-
fps=cam["fps"],
200-
pxl_fmt=cam["pxl_fmt"],
201-
downsample_factor=cam["downsample_factor"],
202-
exposure_time=cam["exposure_time"],
203-
gain=cam["gain"],
199+
name=cam.get("name", None),
200+
unique_id=cam.get("unique_id"),
201+
fps=cam.get("fps", default_camera_config["fps"]),
202+
pxl_fmt=cam.get("pxl_fmt", default_camera_config["pxl_fmt"]),
203+
downsample_factor=cam.get("downsample_factor", default_camera_config["downsample_factor"]),
204+
exposure_time=cam.get("exposure_time", default_camera_config["exposure_time"]),
205+
gain=cam.get("gain", default_camera_config["gain"]),
204206
)
205207
)
206208
return setups_from_database

camera_api/spinnaker.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ def __init__(self, unique_id: str, CameraConfig=None):
1616
self.serial_number, self.api = unique_id.split("-")
1717
self.cam = self.system.GetCameras().GetBySerial(self.serial_number)
1818
self.camera_model = self.cam.TLDevice.DeviceModelName.GetValue()[:10]
19-
if self.cam.IsInitialized():
20-
self.cam.DeInit()
2119
self.cam.Init()
2220
self.nodemap = self.cam.GetNodeMap()
2321
self.stream_nodemap = self.cam.GetTLStreamNodeMap()
@@ -131,7 +129,7 @@ def get_available_pixel_fmt(self) -> list[str]:
131129

132130
# Functions to set camera paramteters.
133131

134-
def set_frame_rate(self, frame_rate: isinstance) -> None:
132+
def set_frame_rate(self, frame_rate: float) -> None:
135133
"""Set the frame rate of the camera in Hz."""
136134
PySpin.CFloatPtr(self.nodemap.GetNode("AcquisitionFrameRate")).SetValue(int(frame_rate))
137135

config/config.py

+11-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,7 @@
2222
"CPU (H264)": "libx264",
2323
"CPU (H265)": "libx265",
2424
},
25-
"pxl_fmt": {
26-
"Mono8": "yuv420p",
27-
"Mono16": "yuv420p"
28-
}
25+
"pxl_fmt": {"Mono8": "yuv420p", "Mono16": "yuv420p"},
2926
},
3027
}
3128

@@ -42,3 +39,13 @@
4239
"config_dir": os.path.join(ROOT, "config"),
4340
"assets_dir": os.path.join(ROOT, "assets", "icons"),
4441
}
42+
43+
# Default Camera Settings -------------------------------------------------------------
44+
45+
default_camera_config = {
46+
"fps": "60",
47+
"pxl_fmt": "Mono8",
48+
"downsample_factor": 1,
49+
"exposure_time": 15000,
50+
"gain": 0,
51+
}

pyMultiVideo_GUI.pyw

-3
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ def check_module(module_name):
2424

2525
check_module("PyQt6")
2626
check_module("pyqtgraph")
27-
# check_module("ffmpeg")
2827
check_module("PySpin")
29-
check_module("cv2_enumerate_cameras")
30-
check_module("cv2")
3128

3229
# Import GUI now that dependancies are verified.
3330
import PyQt6.QtWidgets as QtWidgets

readme.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
# pyMultiVideo
22

3-
See description in docs
4-
5-
I have added a change
3+
See description in docs

requirements.txt

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
cv2_enumerate_cameras==1.1.17
21
ffmpeg_python==0.2.0
32
numpy==1.26.4
4-
opencv_python==4.10.0.84
53
PyQt6==6.7.1
64
PyQt6_sip==13.8.0
75
pyqtgraph==0.13.7

test/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Ignore all contents of folder except .gitignore needed to ensure folder is version controlled.
2+
*
3+
!.gitignore

0 commit comments

Comments
 (0)