-
Notifications
You must be signed in to change notification settings - Fork 0
/
ArtieLabUI.py
2478 lines (2278 loc) · 109 KB
/
ArtieLabUI.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import sys
from collections import deque
from datetime import datetime
from pathlib import Path
import pandas as pd
import tifffile
from PyQt5 import uic
import cv2
import os
import pyqtgraph as pg
from SweeperUIs import AnalyserSweepDialog, FieldSweepDialog
os.add_dll_directory(r"C:\Program Files\JetBrains\CLion 2024.1.1\bin\mingw\bin")
from CImageProcessing import integer_mean
from WrapperClasses import *
import os.path
from os import listdir
from os.path import isfile, join
import sys
import numpy as np
from PyQt5 import QtCore, QtWidgets, uic, QtGui
import logging
from logging.handlers import RotatingFileHandler
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
class ArtieLabUI(QtWidgets.QMainWindow):
"""
The main GUI class for ArtieLab which encompasses all working functionality of the MOKE system.
"""
def __init__(self):
# Loads the UI file and sets it to full screen
super(ArtieLabUI, self).__init__()
uic.loadUi('res/ArtieLab.ui', self)
self.__prepare_logging()
right_monitor = QtWidgets.QDesktopWidget().screenGeometry(1)
self.move(right_monitor.left(), right_monitor.top())
# Define variables
self.mutex = QtCore.QMutex()
self.binning = 2
self.BUFFER_SIZE = 2
self.frame_buffer = deque(maxlen=self.BUFFER_SIZE)
self.item_semaphore = QtCore.QSemaphore(0)
self.spaces_semaphore = QtCore.QSemaphore(self.BUFFER_SIZE)
self.plot_timer = QtCore.QTimer(self)
self.magnetic_field_timer = QtCore.QTimer(self)
self.image_timer = QtCore.QTimer(self)
self.enabled_leds_spi = {
"left1": False,
"left2": False,
"right1": False,
"right2": False,
"up1": False,
"up2": False,
"down1": False,
"down2": False
}
self.LED_brightnesses = {
"left1": 180,
"left2": 180,
"right1": 180,
"right2": 180,
"up1": 180,
"up2": 180,
"down1": 180,
"down2": 180
}
self.enabled_led_pairs = {
"left": False,
"right": False,
"up": False,
"down": False
}
self.led_binary_enum = {
"left1": 2,
"left2": 1,
"right1": 8,
"right2": 4,
"up1": 32,
"up2": 16,
"down1": 128,
"down2": 64
}
self.led_id_enum = {
"left1": 2,
"left2": 1,
"right1": 4,
"right2": 3,
"up1": 6,
"up2": 5,
"down1": 8,
"down2": 7
}
# Create controller objects and threads
self.camera_grabber = CameraGrabber(self)
self.camera_thread = QtCore.QThread()
self.camera_grabber.moveToThread(self.camera_thread)
self.frame_processor = FrameProcessor(self)
self.frame_processor_thread = QtCore.QThread()
self.frame_processor.moveToThread(self.frame_processor_thread)
self.lamp_controller = LampController(reset=True)
self.magnet_controller = MagnetController()
self.analyser_controller = AnalyserController()
self.lamp_controller.disable_all()
self.frame_processor_thread.start()
self.camera_thread.start()
self.height, self.width = self.camera_grabber.get_data_dims()
# Program flow control
self.flickering = False
self.paused = False
self.close_event = None
self.get_background = False
self.LED_control_all = False
self.exposure_time = 0.05
self.roi = (0, 0, 0, 0)
self.latest_processed_frame = np.zeros((1024, 1024), dtype=np.uint16)
self.recording = False
self.recording_store = None
self.recording_meta_data = None
self.recording_contents = []
self.recording_fields = []
self.recording_angles = []
self.recording_frame_index = 0
self.__populate_calibration_combobox()
self.__populate_analyser_position()
self.__connect_signals()
self.__prepare_views()
# Actually display the window
self.showMaximized()
self.show()
self.activateWindow()
# Start image acquisition and update loops
QtCore.QMetaObject.invokeMethod(self.camera_grabber, "start",
QtCore.Qt.ConnectionType.QueuedConnection)
QtCore.QMetaObject.invokeMethod(self.frame_processor, "start_processing",
QtCore.Qt.ConnectionType.QueuedConnection)
self.start_time = time.time()
# Assigned so that restarting these timers can use the same rate. All in ms
self.image_timer_rate = 33 # Maxfps is 30 anyway
self.plot_timer_rate = 200 # Doesn't need to be so often
self.magnetic_field_timer_rate = 50 # max frequency of 10Hz detectable but this is performance limited.
self.image_timer.start(self.image_timer_rate)
self.plot_timer.start(self.plot_timer_rate)
self.magnetic_field_timer.start(self.magnetic_field_timer_rate)
self.button_long_pol.setChecked(True)
self.__on_long_pol(True)
self.__on_image_processing_mode_change(3)
def __connect_signals(self):
"""
Connects all signals between buttons and pyqt signals enabling communication between threads.
:return None:
"""
# LED controls
self.button_left_led1.clicked.connect(self.__on_individual_led)
self.button_right_led1.clicked.connect(self.__on_individual_led)
self.button_up_led1.clicked.connect(self.__on_individual_led)
self.button_down_led1.clicked.connect(self.__on_individual_led)
self.button_left_led2.clicked.connect(self.__on_individual_led)
self.button_right_led2.clicked.connect(self.__on_individual_led)
self.button_up_led2.clicked.connect(self.__on_individual_led)
self.button_down_led2.clicked.connect(self.__on_individual_led)
self.button_leds_off.clicked.connect(self.__disable_all_leds)
# LED Modes
self.button_long_pol.clicked.connect(self.__on_long_pol)
self.button_trans_pol.clicked.connect(self.__on_trans_pol)
self.button_polar.clicked.connect(self.__on_polar)
self.button_long_trans.clicked.connect(self.__on_long_trans)
self.button_pure_long.clicked.connect(self.__on_pure_long)
self.button_pure_trans.clicked.connect(self.__on_pure_trans)
# LED Brightness
self.button_LED_control_all.clicked.connect(self.__on_control_change)
self.button_LED_reset_all.clicked.connect(self.__reset_brightness)
self.scroll_LED_brightness.valueChanged.connect(self.__on_brightness_slider)
self.scroll_blocker = QtCore.QSignalBlocker(self.scroll_LED_brightness)
self.scroll_blocker.unblock()
# LED equalisation
self.button_eq_selected.clicked.connect(self.__equalise_selected_leds)
self.button_eq_vertical.clicked.connect(self.__equalise_vertical_leds)
self.button_eq_horizontal.clicked.connect(self.__equalise_horizontal_leds)
self.button_eq_pairs.clicked.connect(self.__equalise_pairs)
# Image Processing Controls
self.combo_normalisation_selector.currentIndexChanged.connect(self.__on_image_processing_mode_change)
self.spin_percentile_lower.editingFinished.connect(self.__on_image_processing_spin_box_change)
self.spin_percentile_upper.editingFinished.connect(self.__on_image_processing_spin_box_change)
self.spin_clip.editingFinished.connect(self.__on_image_processing_spin_box_change)
self.button_ROI_select.clicked.connect(self.__select_roi)
self.button_draw_line.clicked.connect(self.__draw_line)
self.button_flip_line.clicked.connect(self.__on_flip_line)
self.button_clear_roi.clicked.connect(self.__on_clear_roi)
self.button_clear_line.clicked.connect(self.__on_clear_line)
self.frame_processor.frame_processor_ready.connect(self.__on_frame_processor_ready)
self.frame_processor.new_raw_frame_signal.connect(self.__on_frame_processor_new_raw_frame)
self.frame_processor.new_processed_frame_signal.connect(self.__on_frame_processor_new_processed_frame)
# Averaging controls
self.button_measure_background.clicked.connect(self.__on_get_new_background)
self.button_toggle_averaging.clicked.connect(self.__on_averaging)
self.spin_foreground_averages = SpinBox(self.spin_foreground_averages_old)
self.AVERAGESELECTGRID.replaceWidget(self.spin_foreground_averages_old, self.spin_foreground_averages)
self.spin_foreground_averages_old.close()
self.spin_foreground_averages.editingFinished.connect(self.__on_average_changed)
# Camera Controls
# self.combo_targetfps.currentIndexChanged.connect(self.__on_exposure_time_changed)
self.spin_exposure_time = DoubleSpinBox(self.spin_exposure_time_old)
self.METADATAGRID.replaceWidget(self.spin_exposure_time_old, self.spin_exposure_time)
self.spin_exposure_time_old.close()
self.spin_exposure_time.editingFinished.connect(self.__on_exposure_time_changed)
self.combo_binning.currentIndexChanged.connect(self.__on_binning_mode_changed)
self.button_pause_camera.clicked.connect(self.__on_pause_button)
self.button_display_subtraction.clicked.connect(self.__on_show_subtraction)
self.button_record.clicked.connect(self.__on_record_button)
# Data Streams and Signals
self.camera_grabber.camera_ready.connect(self.__on_camera_ready)
self.camera_grabber.quit_ready.connect(self.__on_quit_ready)
# saving GUI
self.button_save_package.clicked.connect(self.__on_save)
self.button_save_single.clicked.connect(self.__on_save_single)
self.button_dir_browse.clicked.connect(self.__on_browse)
# Magnetic Field Control
self.combo_calib_file.currentIndexChanged.connect(self.__on_change_calibration)
self.calib_file_blocker = QtCore.QSignalBlocker(self.combo_calib_file)
self.calib_file_blocker.unblock()
# Replace spin boxes with improved spinboxes. Inherits settings from qtDesigner file
self.spin_mag_offset = DoubleSpinBox(self.spin_mag_offset_old)
self.spin_mag_amplitude = DoubleSpinBox(self.spin_mag_amplitude_old)
self.spin_mag_freq = DoubleSpinBox(self.spin_mag_freq_old)
self.spin_mag_decay = DoubleSpinBox(self.spin_mag_decay_old)
self.MAGSPINFORM.replaceWidget(self.spin_mag_offset_old, self.spin_mag_offset)
self.MAGSPINFORM.replaceWidget(self.spin_mag_amplitude_old, self.spin_mag_amplitude)
self.MAGSPINFORM.replaceWidget(self.spin_mag_freq_old, self.spin_mag_freq)
self.MAGSPINFORM.replaceWidget(self.spin_mag_decay_old, self.spin_mag_decay)
self.spin_mag_offset_old.close()
self.spin_mag_amplitude_old.close()
self.spin_mag_freq_old.close()
self.spin_mag_decay_old.close()
self.spin_mag_amplitude.editingFinished.connect(self.__on_change_field_amplitude)
self.spin_mag_offset.editingFinished.connect(self.__on_change_field_offset)
self.spin_mag_freq.editingFinished.connect(self.__on_change_mag_freq)
self.spin_mag_decay.editingFinished.connect(self.__on_change_decay_time)
self.spin_dc_step.valueChanged.connect(self.__on_change_field_offset_step)
self.spin_ac_step.valueChanged.connect(self.__on_change_field_amp_step)
self.spin_freq_step.valueChanged.connect(self.__on_change_field_freq_step)
self.spin_decay_step.valueChanged.connect(self.__on_change_field_decay_time_step)
self.button_zero_field.clicked.connect(self.__set_zero_field)
self.button_DC_field.clicked.connect(self.__on_DC_field)
self.button_AC_field.clicked.connect(self.__on_AC_field)
self.button_invert_field.clicked.connect(self.__on_invert_field)
self.button_calibration_directory.clicked.connect(self.__on_browse_mag_calib)
self.button_decay_field.clicked.connect(self.__on_decay)
# Analyser Controls
self.button_move_analyser_back.clicked.connect(self.__rotate_analyser_backward)
self.button_move_analyser_for.clicked.connect(self.__rotate_analyser_forward)
self.button_minimise_analyser.clicked.connect(self.__on_find_minimum)
# Special Function Controls
self.button_analy_sweep.clicked.connect(self.__on_analyser_sweep)
self.button_hyst_sweep.clicked.connect(self.__on_hysteresis_sweep)
# Plot controls
self.magnetic_field_timer.timeout.connect(self.__update_field_measurement)
self.plot_timer.timeout.connect(self.__update_plots)
self.image_timer.timeout.connect(self.__update_images)
self.spin_number_of_points = SpinBox(self.spin_number_of_points_old)
self.spin_mag_point_count = SpinBox(self.spin_mag_point_count_old)
self.layout_plot_header.replaceWidget(self.spin_number_of_points_old, self.spin_number_of_points)
self.layout_magmeas.replaceWidget(self.spin_mag_point_count_old, self.spin_mag_point_count)
self.spin_number_of_points_old.close()
self.spin_mag_point_count_old.close()
self.spin_number_of_points.editingFinished.connect(self.__on_change_plot_count)
self.spin_mag_point_count.editingFinished.connect(self.__on_change_mag_plot_count)
self.button_reset_plots.clicked.connect(self.__on_reset_plots)
def __prepare_logging(self):
"""
The multicolour logging box in the GUI and the log file are both set up here.
:return None:
"""
self.log_text_box = HTMLBasedColorLogger(self)
# self.log_text_box.setFormatter(
# logging.Formatter('%(asctime)s %(levelname)s %(module)s - %(message)s', "%H:%M:%S"))
# logging.Formatter(CustomLoggingFormatter())
logging.getLogger().addHandler(self.log_text_box)
self.log_text_box.setFormatter(CustomLoggingFormatter())
logging.getLogger().setLevel(logging.INFO)
self.layout_logging.addWidget(self.log_text_box.widget)
fh = RotatingFileHandler('ArtieLabUI.log',
mode='a',
maxBytes=1024 * 1024,
backupCount=1,
encoding=None,
delay=False,
errors=None
)
fh.setLevel(logging.DEBUG)
fh.setFormatter(
logging.Formatter(
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s'))
logging.getLogger().addHandler(fh)
def __prepare_views(self):
"""
Prepares the empty camera view(s) and the graph axes
:return:
"""
self.stream_window = 'HamamatsuView'
window_width = self.width
window_height = self.height
cv2.namedWindow(
self.stream_window,
flags=(cv2.WINDOW_NORMAL | cv2.WINDOW_GUI_NORMAL | cv2.WINDOW_FREERATIO))
cv2.setWindowProperty(self.stream_window, cv2.WND_PROP_TOPMOST, 1.0)
cv2.setWindowProperty(self.stream_window, cv2.WND_PROP_FULLSCREEN, 1.0)
cv2.resizeWindow(
self.stream_window,
window_width,
window_height)
cv2.moveWindow(
self.stream_window,
0,
0)
self.plots_canvas = pg.GraphicsLayoutWidget()
self.layout_plots.addWidget(self.plots_canvas)
self.intensity_plot = self.plots_canvas.addPlot(
row=0,
col=0,
title="Intensity",
left="mean intensity",
bottom="time (s)"
)
self.intensity_line = self.intensity_plot.plot(list(self.frame_processor.frame_times),
list(self.frame_processor.intensities_y), pen="k")
self.hist_plot = self.plots_canvas.addPlot(
row=1,
col=0,
title="Histogram as seen",
left="counts",
bottom="intensity"
)
self.hist_line = self.hist_plot.plot(self.frame_processor.latest_hist_bins,
self.frame_processor.latest_hist_data, pen="k")
self.roi_plot = self.plots_canvas.addPlot(
row=2,
col=0,
title="ROI Intensity",
left="mean intensity",
bottom="time (s)"
)
self.roi_line = self.roi_plot.plot([], pen="k")
self.roi_plot.hide()
self.line_profile_plot = self.plots_canvas.addPlot(
row=3,
col=0,
title="Line Profile",
left="intensity",
bottom="pixel index"
)
self.line_profile_line = self.line_profile_plot.plot([], [], pen="k")
self.line_profile_plot.hide()
self.mag_plot_canvas = pg.GraphicsLayoutWidget()
self.layout_mag_plot.addWidget(self.mag_plot_canvas)
self.mag_plot = self.mag_plot_canvas.addPlot(
row=0,
col=0,
title="Magnetic Field",
left="Field (mT)",
bottom="time (s)"
)
self.mag_y = deque([0.], maxlen=10000)
self.mag_t = deque([0.], maxlen=10000)
self.mag_line = self.mag_plot.plot(self.mag_t, self.mag_y, pen="k")
def __populate_calibration_combobox(self):
"""
Loads the last used calibration file if possible else searches in the local Coil Calibrations directory else
asks for user input.
:return None: """
# Magnetic field calibration stuff
if os.path.isfile('res/last_calibration_location.txt'):
with open('res/last_calibration_location.txt', 'r') as file:
calib_dir = file.readline()
logging.info(f"Previous calibration file directory found.")
elif os.path.isdir("Coil Calibrations\\"):
calib_dir = "Coil Calibrations\\"
logging.warning("No calibration location found, trying: " + str(calib_dir))
else:
logging.warning("Default calib file location not found. Asking for user input.")
self.calib_dir = QtWidgets.QFileDialog.getExistingDirectory(
None,
'Choose Calibration File Directory',
QtWidgets.QFileDialog.ShowDirsOnly
)
file_names = [f for f in listdir(calib_dir) if isfile(join(calib_dir, f)) and ".txt" in f]
if file_names:
self.calib_file_dir = calib_dir
logging.info(f"Loading calibration files from {calib_dir}")
self.calibration_dictionary = {i + 1: name for i, name in enumerate(file_names)}
# +1 because 0 is "None"
strings = [name.replace('.txt', '') for name in file_names]
strings = [name.replace('_fit', '') for name in strings]
self.combo_calib_file.clear()
self.combo_calib_file.addItem("None")
self.combo_calib_file.addItems(strings)
else:
logging.warning(" No magnet calibration files found.")
def __populate_analyser_position(self):
if os.path.isfile('res/last_analyser_position.txt'):
with open('res/last_analyser_position.txt', 'r') as file:
val = file.readline()
logging.info(f"Previous analyser position loaded: {val}")
self.line_current_angle.setText(str(round(float(val), 3)))
def __update_plots(self):
"""
Updates all the graph axes. Is called by a continuously running timer: self.plot_timer
:return None:
"""
# Sometimes, the update can be called between the appending to frame times and to intensities
length = min([self.frame_processor.frame_times.__len__(), self.frame_processor.intensities_y.__len__()])
if length > 0:
self.intensity_line.setData(
np.array(self.frame_processor.frame_times)[-length:] - np.min(self.frame_processor.frame_times),
list(self.frame_processor.intensities_y)[-length:]
)
self.hist_line.setData(
self.frame_processor.latest_hist_bins,
self.frame_processor.latest_hist_data
)
# The time taken to measure a frame is calculated from the difference in frame times so must be 2 or more frames.
if len(self.frame_processor.frame_times) > 1:
n_to_avg = min(10, self.frame_processor.frame_times.__len__())
self.line_FPSdisplay.setText(
"%.3f" % (1 / (np.mean(np.diff(np.array(self.frame_processor.frame_times)[-n_to_avg:]))))
)
# After starting a ROI measurement, these deques will have different lengths so must take the last values
# from the frame times until they are both fully populated.
length = min([self.frame_processor.frame_times.__len__(), self.frame_processor.roi_int_y.__len__()])
if sum(self.frame_processor.roi) > 0 and length > 0:
self.roi_line.setData(
np.array(self.frame_processor.frame_times)[-length:] - np.min(self.frame_processor.frame_times),
list(self.frame_processor.roi_int_y)[-length:]
)
if self.frame_processor.line_coords is not None and len(self.frame_processor.latest_profile) > 0:
self.line_profile_line.setData(
self.frame_processor.latest_profile
)
self.mag_line.setData(self.mag_t, self.mag_y)
if self.frame_processor.averaging:
if self.flickering:
progress = (self.frame_processor.diff_frame_stack_a.shape[0] /
self.spin_foreground_averages.value() * 100)
else:
progress = (self.frame_processor.raw_frame_stack.shape[0] /
self.spin_foreground_averages.value() * 100)
self.bar_averaging.setValue(int(progress))
else:
self.bar_averaging.setValue(0)
def __update_images(self):
"""
Updates the CV2 display(s) with latest frame data.
:return None:
"""
frame = self.latest_processed_frame
if sum(self.frame_processor.roi) > 0:
x, y, w, h = self.frame_processor.roi
frame = cv2.rectangle(
frame,
(x, y),
(x + w, y + h),
color=(0, 0, 0),
thickness=2
)
if self.frame_processor.line_coords is not None:
start, end = self.frame_processor.line_coords
frame = cv2.arrowedLine(
frame,
start[::-1],
end[::-1],
color=(0, 0, 0),
thickness=2
)
cv2.imshow(self.stream_window, frame)
cv2.waitKey(1)
def __update_field_measurement(self):
"""
Updates the data used in the field plots ready for plotting. Is called by a continuously running timer:
self.magnetic_field_timer. This updates more often than the plot is updated to improve performance
:return None:
"""
fields, voltages = self.magnet_controller.get_amplitude_values()
if len(fields) > 0:
self.line_measured_field.setText("{:0.4f}".format(fields[-1]))
self.line_measured_voltage.setText("{:0.4f}".format(voltages[-1]))
[self.mag_y.append(field) for field in fields]
last_time = self.mag_t[-1]
times = [(val / 1000) + last_time for val in list(range(len(voltages)))]
[self.mag_t.append(time) for time in times]
def __on_reset_plots(self):
"""
Resets all the data used to plot graphs that have a time/frame number axis.
:return :
"""
self.mutex.lock()
self.frame_processor.frame_times = deque(maxlen=self.spin_number_of_points.value())
self.frame_processor.intensities_y = deque(maxlen=self.spin_number_of_points.value())
self.frame_processor.roi_int_y = deque(maxlen=self.spin_number_of_points.value())
self.mag_y = deque(self.mag_y, maxlen=self.spin_mag_point_count.value())
self.mag_t = deque(self.mag_t, maxlen=self.spin_mag_point_count.value())
self.mutex.unlock()
def __on_change_plot_count(self):
"""
Changes the maxmimum length of the data used to plot the data on the right hand panel
:param value: The new number of points
:return:
"""
value = self.spin_number_of_points.value()
self.mutex.lock()
self.frame_processor.frame_times = deque(self.frame_processor.frame_times, maxlen=value)
self.frame_processor.intensities_y = deque(self.frame_processor.intensities_y, maxlen=value)
self.frame_processor.roi_int_y = deque(self.frame_processor.roi_int_y, maxlen=value)
self.mutex.unlock()
def __on_change_mag_plot_count(self):
"""
Changes the maxmimum length of the data used to plot the magnetic field vs time.
:param value: The new number of points
:return None:
"""
value = self.spin_mag_point_count.value()
self.mag_y = deque(self.mag_y, maxlen=value)
self.mag_t = deque(self.mag_t, maxlen=value)
def __reset_pairs(self):
"""
Reset the local store of all enabled pairs of LEDs to False
:return:
"""
self.enabled_led_pairs.update(
{"left": False,
"right": False,
"up": False,
"down": False})
def __reset_led_spis(self):
"""
Reset the local store of all enabled individual LEDs to False
:return:
"""
self.enabled_leds_spi.update(
{"left1": False,
"left2": False,
"right1": False,
"right2": False,
"up1": False,
"up2": False,
"down1": False,
"down2": False}
)
def __reset_brightness(self):
"""
Sets all the LED brightnesses to maximum
:return:
"""
self.LED_brightnesses.update(
{"left1": 180,
"left2": 180,
"right1": 180,
"right2": 180,
"up1": 180,
"up2": 180,
"down1": 180,
"down2": 180}
)
self.lamp_controller.set_all_brightness(180)
def get_lighting_configuration(self):
"""
Finds the current lighting state, basically which mode is enabled. If no mode is selected then it returns
all enabled LEDs as a boolean.
:return str/list: str of currently selected lighting mode or list of bools for each LED's enabled state.
"""
if self.button_long_pol.isChecked():
return "longitudinal and polar"
elif self.button_trans_pol.isChecked():
return "transpose and polar"
elif self.button_polar.isChecked():
return "polar"
elif self.button_long_trans.isChecked():
return "longitudinal and transpose and polar"
elif self.button_pure_long.isChecked():
return "pure longitudinal"
elif self.button_pure_trans.isChecked():
return "pure transpose"
else:
return [self.button_up_led1.isChecked(),
self.button_up_led2.isChecked(),
self.button_down_led1.isChecked(),
self.button_down_led2.isChecked(),
self.button_left_led1.isChecked(),
self.button_left_led2.isChecked(),
self.button_right_led1.isChecked(),
self.button_right_led2.isChecked()]
def __on_image_processing_spin_box_change(self):
"""
Updates the frame processor with all variables for the various image processing modes.
:return None:
"""
if self.spin_percentile_lower.value() < self.frame_processor.p_high:
self.frame_processor.p_low = self.spin_percentile_lower.value()
if self.spin_percentile_upper.value() > self.frame_processor.p_low:
self.frame_processor.p_high = self.spin_percentile_upper.value()
self.frame_processor.clip = self.spin_clip.value()
def __on_image_processing_mode_change(self, mode):
"""
Changes the currently used image processing mode based on the user selection.
:param mode: 0 - none, 1 - basic (just divides by max), 2 - contrast stretching (percentile based stretching of
histogram), 3 - whole image histogram equalisation to linearise the cumulative distribution 4 - local version
of 3, very computationally demanding.
:return None:
"""
self.frame_processor.mode = mode
match mode:
case 0: # None
self.spin_percentile_lower.setEnabled(False)
self.spin_percentile_upper.setEnabled(False)
self.spin_clip.setEnabled(False)
case 1: # Basic
self.spin_percentile_lower.setEnabled(False)
self.spin_percentile_upper.setEnabled(False)
self.spin_clip.setEnabled(False)
case 2: # Contrast stretching
self.spin_percentile_lower.setEnabled(True)
self.spin_percentile_upper.setEnabled(True)
self.spin_clip.setEnabled(False)
if self.spin_percentile_lower.value() < self.frame_processor.p_high:
self.frame_processor.p_low = self.spin_percentile_lower.value()
if self.spin_percentile_upper.value() > self.frame_processor.p_low:
self.frame_processor.p_high = self.spin_percentile_upper.value()
# This is contrast stretching and needs min and max percentiles
case 3: # Histrogram eq
self.spin_percentile_lower.setEnabled(False)
self.spin_percentile_upper.setEnabled(False)
self.spin_clip.setEnabled(False)
# this is auto hist and so no other settings are needed
case 4: # Adaptive eq
self.spin_percentile_lower.setEnabled(False)
self.spin_percentile_upper.setEnabled(False)
self.spin_clip.setEnabled(True)
self.frame_processor.clip = self.spin_clip.value()
# this is Adaptive EQ and needs a clip limit
case _:
logging.error("Unsupported image processing mode")
def __select_roi(self):
"""
Asks the user to select a region of interest and then, if one is selected, updates the frame processor and plots
such that the ROI based information is accessible.
:return None:
"""
logging.log(
ATTENTION_LEVEL,
"Select a ROI and then press SPACE or ENTER button! \n" +
" Cancel the selection process by pressing c button")
self.image_timer.stop()
# Seleting using the raw frame means that the scaling is handled automatically.
roi = cv2.selectROI(self.stream_window, self.frame_processor.latest_processed_frame.astype(np.uint16),
showCrosshair=True, printNotice=False)
if sum(roi) > 0:
# self.frame_processor.roi = tuple([int(value * (2 / self.binning)) for value in roi])
self.frame_processor.roi = roi
self.roi_plot.show()
logging.info("ROI set to " + str(roi))
self.button_clear_roi.setEnabled(True)
logging.info(f'Binning mode: {self.binning}, roi: {self.frame_processor.roi}')
else:
logging.info('Failed to set ROI')
self.__on_clear_roi()
self.image_timer.start(self.image_timer_rate)
def __draw_line(self):
"""
Asks the user to draw a rectangle containing the two ends of a line and then, if one is selected, updates the
frame processor and plots such that the line profile is plotted for each frame.
:return None:
"""
logging.log(
ATTENTION_LEVEL,
"Select a bounding box and then press SPACE or ENTER button! \n" +
" Cancel the selection process by pressing c button")
self.image_timer.stop()
roi = cv2.selectROI(self.stream_window, self.frame_processor.latest_processed_frame.astype(np.uint16),
showCrosshair=True, printNotice=False)
if sum(roi) > 0:
x, y, w, h = roi
self.frame_processor.line_coords = ((y, x), (y + h, x + w))
self.line_profile_plot.show()
self.button_clear_line.setEnabled(True)
self.button_flip_line.setEnabled(True)
logging.info(
f'Binning mode: {self.binning}, line between: {self.frame_processor.line_coords[0]}' +
f' and {self.frame_processor.line_coords[1]}')
else:
logging.warning('Failed to set line profile')
self.__on_clear_line()
self.image_timer.start(self.image_timer_rate)
self.button_clear_line.setEnabled(True)
# cv2.line
def __on_flip_line(self):
"""
Because the drawn line is taken from the corners of an ROI box, the line can be flipped such that the other
pair of corners is used instead. This is that.
:return None:
"""
(x1, y1), (x2, y2) = self.frame_processor.line_coords
self.frame_processor.line_coords = ((x1, y2), (x2, y1))
logging.info(
f'Flipped line. Line now between: {self.frame_processor.line_coords[0]}' +
f' and {self.frame_processor.line_coords[1]}')
def __on_clear_roi(self):
"""
Clear the ROI and disable now-irrelevant plots buttons.
:return None:
"""
self.button_clear_roi.setEnabled(False)
self.frame_processor.roi = (0, 0, 0, 0)
self.frame_processor.roi_int_y = deque(maxlen=self.spin_number_of_points.value())
self.roi_plot.hide()
logging.info("Cleared ROI")
def __on_clear_line(self):
"""
Clear the points used for line profiling and disable now-irrelevant plots buttons.
:return None:
"""
self.button_clear_line.setEnabled(False)
self.button_flip_line.setEnabled(False)
self.line_profile_plot.hide()
self.frame_processor.line_coords = None
logging.info("Cleared Line")
def __disable_all_leds(self):
"""
Called when the user clicks the red cross button to disable all LEDs. Disables an enabled LED modes and turns
all the lights off
:return None:
"""
logging.info("Disabling all LEDs")
self.__reset_led_spis()
self.__reset_pairs()
if self.flickering:
self.__reset_after_flicker_mode()
self.button_long_pol.setChecked(False)
self.button_trans_pol.setChecked(False)
self.button_polar.setChecked(False)
self.button_long_trans.setChecked(False)
self.button_pure_long.setChecked(False)
self.button_pure_trans.setChecked(False)
self.button_up_led1.setChecked(False)
self.button_up_led2.setChecked(False)
self.button_down_led1.setChecked(False)
self.button_down_led2.setChecked(False)
self.button_left_led1.setChecked(False)
self.button_left_led2.setChecked(False)
self.button_right_led1.setChecked(False)
self.button_right_led2.setChecked(False)
self.__update_controller_pairs()
def __on_individual_led(self):
"""
This is called whenever the user clicked to enable or disable an individual LED. It disables all active modes
and enables the enabled LEDs.
:return None:
"""
logging.info("Individual LED being called")
if not self.flickering:
if self.check_for_any_active_LED_mode():
self.button_long_pol.setChecked(False)
self.button_trans_pol.setChecked(False)
self.button_polar.setChecked(False)
self.button_long_trans.setChecked(False)
self.button_pure_long.setChecked(False)
self.button_pure_trans.setChecked(False)
self.__populate_spis()
self.__update_controller_spi()
def __populate_spis(self):
self.enabled_leds_spi["up1"] = self.button_up_led1.isChecked()
self.enabled_leds_spi["up2"] = self.button_up_led2.isChecked()
self.enabled_leds_spi["down1"] = self.button_down_led1.isChecked()
self.enabled_leds_spi["down2"] = self.button_down_led2.isChecked()
self.enabled_leds_spi["left1"] = self.button_left_led1.isChecked()
self.enabled_leds_spi["left2"] = self.button_left_led2.isChecked()
self.enabled_leds_spi["right1"] = self.button_right_led1.isChecked()
self.enabled_leds_spi["right2"] = self.button_right_led2.isChecked()
def __on_long_pol(self, checked):
"""
Called when the user clicks the long and pol button. Enables top pair.
:param bool checked: The state of button_long_pol after clicking.
:return None:
"""
if checked:
if self.flickering:
self.__reset_after_flicker_mode()
self.enabled_led_pairs.update(
{"left": False,
"right": False,
"up": True,
"down": False})
self.__reset_led_spis()
self.button_up_led1.setChecked(True)
self.button_up_led2.setChecked(True)
self.button_down_led1.setChecked(False)
self.button_down_led2.setChecked(False)
self.button_left_led1.setChecked(False)
self.button_left_led2.setChecked(False)
self.button_right_led1.setChecked(False)
self.button_right_led2.setChecked(False)
self.button_trans_pol.setChecked(False)
self.button_polar.setChecked(False)
self.button_long_trans.setChecked(False)
self.button_pure_long.setChecked(False)
self.button_pure_trans.setChecked(False)
self.__populate_spis()
self.__update_controller_pairs()
else:
if not self.check_for_any_active_LED_mode():
self.__disable_all_leds()
def __on_trans_pol(self, checked):
"""
Called when the user clicks the trans and pol button. Enables left pair.
:param bool checked: The state of button_trans_pol after clicking.
:return None:
"""
if checked:
if self.flickering:
self.__reset_after_flicker_mode()
self.enabled_led_pairs.update({"left": True,
"right": False,
"up": False,
"down": False})
self.__reset_led_spis()
self.button_up_led1.setChecked(False)
self.button_up_led2.setChecked(False)
self.button_down_led1.setChecked(False)
self.button_down_led2.setChecked(False)
self.button_left_led1.setChecked(True)
self.button_left_led2.setChecked(True)
self.button_right_led1.setChecked(False)
self.button_right_led2.setChecked(False)
self.button_long_pol.setChecked(False)
self.button_polar.setChecked(False)
self.button_long_trans.setChecked(False)
self.button_pure_long.setChecked(False)
self.button_pure_trans.setChecked(False)
self.__populate_spis()
self.__update_controller_pairs()
else:
if not self.check_for_any_active_LED_mode():
self.__disable_all_leds()
def __on_polar(self, checked):
"""
Called when the user clicks the polar button. Enables top middle and bottom middle.
Cancels longitudinal component.
:param bool checked: The state of button_pol after clicking.
:return None:
"""
if checked:
if self.flickering:
self.__reset_after_flicker_mode()
self.__reset_pairs()
self.enabled_leds_spi.update(
{"left1": False,
"left2": False,
"right1": False,
"right2": False,
"up1": True,
"up2": False,
"down1": True,
"down2": False})
self.button_up_led1.setChecked(True)
self.button_up_led2.setChecked(False)
self.button_down_led1.setChecked(True)
self.button_down_led2.setChecked(False)
self.button_left_led1.setChecked(False)
self.button_left_led2.setChecked(False)
self.button_right_led1.setChecked(False)
self.button_right_led2.setChecked(False)
self.button_long_pol.setChecked(False)
self.button_trans_pol.setChecked(False)
self.button_long_trans.setChecked(False)
self.button_pure_long.setChecked(False)
self.button_pure_trans.setChecked(False)
self.__populate_spis()
self.__update_controller_spi()
else:
if not self.check_for_any_active_LED_mode():
self.__disable_all_leds()
def __on_long_trans(self, checked):
"""
Called when the user clicks the long and trans and pol button. Flickers between the left and top pairs,
taking a difference image.
:param bool checked: The state of button_long_trans after clicking.
:return None:
"""
# There is a difference in operation between turning off all modes and switching back to non-flicker modes.
if checked:
if not self.flickering:
self.__prepare_for_flicker_mode()
else:
self.lamp_controller.stop_flicker()
self.button_long_pol.setChecked(False)
self.button_trans_pol.setChecked(False)
self.button_polar.setChecked(False)
self.button_pure_long.setChecked(False)
self.button_pure_trans.setChecked(False)
self.button_up_led1.setChecked(True)