55
66"""
77
8- import cv2
98import logging
10- import numpy as np
119
1210from typing import Any
1311from time import sleep , perf_counter
14- from transitions import Machine , State
1512
13+ import cv2
14+ import numpy as np
1615from PyQt5 .QtCore import QObject , pyqtSignal , pyqtSlot
16+ from transitions import Machine , State
1717
1818from ulc_mm_package .hardware .scope import MalariaScope , GPIOEdge
1919from ulc_mm_package .hardware .scope_routines import Routines
3838 LEDNoPower ,
3939)
4040
41- from ulc_mm_package .neural_nets .neural_network_constants import IMG_RESIZED_DIMS
41+ from ulc_mm_package .neural_nets .neural_network_constants import (
42+ IMG_RESIZED_DIMS ,
43+ QC_GOODNESS_THRESHOLD ,
44+ QC_STATUS ,
45+ PERC_OF_IMAGES_GOOD ,
46+ )
4247from ulc_mm_package .neural_nets .YOGOInference import YOGO , ClassCountResult
4348from ulc_mm_package .neural_nets .neural_network_constants import (
4449 YOGO_CLASS_LIST ,
@@ -84,7 +89,7 @@ class NamedMachine(Machine):
8489
8590class ScopeOp (QObject , NamedMachine ):
8691 setup_done = pyqtSignal ()
87- experiment_done = pyqtSignal (str , str )
92+ experiment_done = pyqtSignal (str , str , int )
8893 reset_done = pyqtSignal ()
8994
9095 yield_mscope = pyqtSignal (MalariaScope )
@@ -301,6 +306,34 @@ def update_thumbnails(self):
301306 )
302307 )
303308
309+ def run_status_from_qc_results (self , qc_results : np .ndarray ) -> QC_STATUS :
310+ """Logic for determining whether a run was decent based on the QC results at the end of a run.
311+
312+ Parameters
313+ ----------
314+ qc_results : np.ndarray
315+ Array of QC results from the QC model.
316+
317+ Returns
318+ -------
319+ QC_STATUS
320+ Status of the run based on the QC results.
321+ - "good" if X% of results are below the QC_GOODNESS_THRESHOLD (i.e considered 'good')
322+ - "poor" if Y% of results are above the threshold
323+ """
324+
325+ num_good = (qc_results <= QC_GOODNESS_THRESHOLD ).sum ()
326+ num_total = len (qc_results )
327+
328+ if num_total == 0 :
329+ self .logger .warning ("No QC results available. Cannot determine run status." )
330+ raise ValueError ("Run status cannot be determined without QC results." )
331+
332+ if num_good / num_total >= PERC_OF_IMAGES_GOOD :
333+ return QC_STATUS .GOOD
334+ else :
335+ return QC_STATUS .POOR
336+
304337 def setup (self ):
305338 self .create_timers .emit ()
306339
@@ -559,6 +592,48 @@ def _end_experiment(self, *args):
559592
560593 self .mscope .reset_for_end_experiment ()
561594
595+ # Run the QC model a small partition of the data
596+ self .logger .info ("Running QC on images." )
597+ qc_results = None
598+ try :
599+ zf = self .mscope .data_storage .get_read_only_zarr ()
600+
601+ try :
602+ img_indices = np .linspace (0 , zf .initialized - 1 , 50 ).astype (int )
603+ for idx in img_indices :
604+ img = zf [:, :, idx ]
605+ self .mscope .qc .asyn (img )
606+ qc_results = self .mscope .qc .get_asyn_results (timeout = None )
607+ qc_results = [self .mscope .qc ._sigmoid (x .result ) for x in qc_results ]
608+ except Exception as e :
609+ self .logger .error (
610+ f"Unexpected error while submitting images to QC model: { e } . Skipping QC..."
611+ )
612+ except Exception as e :
613+ self .logger .error (f"Failed to get zarr data for QC: { e } .\n Skipping QC..." )
614+
615+ # Log QC results
616+ if qc_results :
617+ self .did_run_pass_qc = None
618+ qc_results_np = np .array ([x [0 ][0 ] for x in qc_results ])
619+ num_qc_results_good = (qc_results_np <= QC_GOODNESS_THRESHOLD ).sum ()
620+ num_imgs_qc = len (qc_results_np )
621+ self .logger .info (
622+ f"QC all results: { qc_results_np } \n "
623+ f"QC mean: { qc_results_np .mean ():.3f} , "
624+ f"QC stdev: { qc_results_np .std ():.3f} , "
625+ f"QC best score: { qc_results_np .min ():.3f} , "
626+ f"QC worst score: { qc_results_np .max ():.3f} , "
627+ f"QC number of images good: { num_qc_results_good } /{ num_imgs_qc } ({ num_qc_results_good / num_imgs_qc :.2%} )%"
628+ )
629+ self .did_run_pass_qc = self .run_status_from_qc_results (qc_results_np ).value
630+ else :
631+ self .logger .warning ("No QC results available. Skipping QC..." )
632+
633+ # Save qc results
634+ self .mscope .data_storage .save_qc_data (img_indices , qc_results_np )
635+ self .finishing_experiment .emit (80 )
636+
562637 # Turn camera back on
563638 self .mscope .camera .startAcquisition ()
564639
@@ -577,12 +652,15 @@ def _end_experiment(self, *args):
577652 def _start_intermission (self , msg ):
578653 parasitemia_vis_path = self .mscope .data_storage .get_parasitemia_vis_filename ()
579654
655+ # Display parasitemia visualization if it exists
580656 if parasitemia_vis_path .exists ():
581657 self .experiment_done .emit (
582- msg + PARASITEMIA_VIS_MSG , str (parasitemia_vis_path )
658+ msg + PARASITEMIA_VIS_MSG ,
659+ str (parasitemia_vis_path ),
660+ self .did_run_pass_qc ,
583661 )
584662 else :
585- self .experiment_done .emit (msg , "" )
663+ self .experiment_done .emit (msg , "" , self . did_run_pass_qc )
586664
587665 @pyqtSlot (np .ndarray , float )
588666 def run_autobrightness (self , img , _timestamp ):
@@ -701,10 +779,7 @@ def run_autofocus(self, img, _timestamp):
701779
702780 if not self .autofocus_done :
703781 if len (self .autofocus_batch ) < AF_BATCH_SIZE :
704- resized_img = cv2 .resize (
705- img , IMG_RESIZED_DIMS , interpolation = cv2 .INTER_CUBIC
706- )
707- self .autofocus_batch .append (resized_img )
782+ self .autofocus_batch .append (img )
708783
709784 if self .running :
710785 self .img_signal .connect (self .run_autofocus )
@@ -931,7 +1006,7 @@ def run_experiment(self, img, timestamp) -> None:
9311006 raw_focus_err ,
9321007 filtered_focus_err ,
9331008 focus_adjustment ,
934- ) = self .PSSAF_routine .send (resized_img )
1009+ ) = self .PSSAF_routine .send (img )
9351010 except MotorControllerError as e :
9361011 if not SIMULATION :
9371012 self .logger .error (
0 commit comments