77from pathlib import Path
88import packaging .version
99
10+ import cv2
1011import numpy as np
1112import pandas as pd
1213
1516from ibllib .misc import check_nvidia_driver
1617from ibllib .ephys import ephysqc , spikes , sync_probes
1718from ibllib .io import ffmpeg , spikeglx
18- from ibllib .io .video import label_from_path
19+ from ibllib .io .video import label_from_path , assert_valid_label
1920from ibllib .io .extractors import ephys_fpga , ephys_passive , camera
2021from ibllib .pipes import tasks
2122from ibllib .pipes .training_preprocessing import TrainingRegisterRaw as EphysRegisterRaw
@@ -893,14 +894,159 @@ def get_signatures(self, **kwargs):
893894
894895
895896class EphysDLC (tasks .Task ):
897+ """
898+ This task relies on a correctly installed dlc environment as per
899+ https://docs.google.com/document/d/1g0scP6_3EmaXCU4SsDNZWwDTaD9MG0es_grLA-d0gh0/edit#
900+
901+ If your environment is set up otherwise, make sure that you set the respective attributes:
902+ t = EphysDLC(session_path)
903+ t.dlcenv = Path('/path/to/your/dlcenv/bin/activate')
904+ t.scripts = Path('/path/to/your/iblscripts/deploy/serverpc/dlc')
905+ """
896906 gpu = 1
897907 cpu = 4
898908 io_charge = 90
899909 level = 2
910+ force = True
900911
901- def _run (self ):
902- """empty placeholder for job creation only"""
903- pass
912+ dlcenv = Path .home ().joinpath ('Documents' , 'PYTHON' , 'envs' , 'dlcenv' , 'bin' , 'activate' )
913+ scripts = Path .home ().joinpath ('Documents' , 'PYTHON' , 'iblscripts' , 'deploy' , 'serverpc' , 'dlc' )
914+ signature = {
915+ 'input_files' : [
916+ ('_iblrig_leftCamera.raw.mp4' , 'raw_video_data' , True ),
917+ ('_iblrig_rightCamera.raw.mp4' , 'raw_video_data' , True ),
918+ ('_iblrig_bodyCamera.raw.mp4' , 'raw_video_data' , True ),
919+ ],
920+ 'output_files' : [
921+ ('_ibl_leftCamera.dlc.pqt' , 'alf' , True ),
922+ ('_ibl_rightCamera.dlc.pqt' , 'alf' , True ),
923+ ('_ibl_bodyCamera.dlc.pqt' , 'alf' , True ),
924+ ('leftCamera.ROIMotionEnergy.npy' , 'alf' , True ),
925+ ('rightCamera.ROIMotionEnergy.npy' , 'alf' , True ),
926+ ('bodyCamera.ROIMotionEnergy.npy' , 'alf' , True ),
927+ ('leftROIMotionEnergy.position.npy' , 'alf' , True ),
928+ ('rightROIMotionEnergy.position.npy' , 'alf' , True ),
929+ ('bodyROIMotionEnergy.position.npy' , 'alf' , True ),
930+ ],
931+ }
932+
933+ def _check_dlcenv (self ):
934+ """Check that scripts are present, dlcenv can be activated and get iblvideo version"""
935+ assert len (list (self .scripts .rglob ('run_dlc.*' ))) == 2 , \
936+ f'Scripts run_dlc.sh and run_dlc.py do not exist in { self .scripts } '
937+ assert len (list (self .scripts .rglob ('run_motion.*' ))) == 2 , \
938+ f'Scripts run_motion.sh and run_motion.py do not exist in { self .scripts } '
939+ assert self .dlcenv .exists (), f"DLC environment does not exist in assumed location { self .dlcenv } "
940+ command2run = f"source { self .dlcenv } ; python -c 'import iblvideo; print(iblvideo.__version__)'"
941+ process = subprocess .Popen (
942+ command2run ,
943+ shell = True ,
944+ stdout = subprocess .PIPE ,
945+ stderr = subprocess .PIPE ,
946+ executable = "/bin/bash"
947+ )
948+ info , error = process .communicate ()
949+ if process .returncode != 0 :
950+ raise AssertionError (f"DLC environment check failed\n { error .decode ('utf-8' )} " )
951+ version = info .decode ("utf-8" ).strip ().split ('\n ' )[- 1 ]
952+ return version
953+
954+ @staticmethod
955+ def _video_intact (file_mp4 ):
956+ """Checks that the downloaded video can be opened and is not empty"""
957+ cap = cv2 .VideoCapture (str (file_mp4 ))
958+ frame_count = cap .get (cv2 .CAP_PROP_FRAME_COUNT )
959+ intact = True if frame_count > 0 else False
960+ cap .release ()
961+ return intact
962+
963+ def _run (self , cams = None , overwrite = False ):
964+ # Default to all three cams
965+ cams = cams or ['left' , 'right' , 'body' ]
966+ cams = assert_valid_label (cams )
967+ # Set up
968+ self .session_id = self .one .path2eid (self .session_path )
969+ actual_outputs = []
970+
971+ # Loop through cams
972+ for cam in cams :
973+ # Catch exceptions so that following cameras can still run
974+ try :
975+ # If all results exist and overwrite is False, skip computation
976+ expected_outputs_present , expected_outputs = self .assert_expected (self .output_files , silent = True )
977+ if overwrite is False and expected_outputs_present is True :
978+ actual_outputs .extend (expected_outputs )
979+ continue
980+ else :
981+ file_mp4 = next (self .session_path .joinpath ('raw_video_data' ).glob (f'_iblrig_{ cam } Camera.raw*.mp4' ))
982+ if not file_mp4 .exists ():
983+ # In this case we set the status to Incomplete.
984+ _logger .error (f"No raw video file available for { cam } , skipping." )
985+ self .status = - 3
986+ continue
987+ if not self ._video_intact (file_mp4 ):
988+ _logger .error (f"Corrupt raw video file { file_mp4 } " )
989+ self .status = - 1
990+ continue
991+ # Check that dlc environment is ok, shell scripts exists, and get iblvideo version, GPU addressable
992+ self .version = self ._check_dlcenv ()
993+ _logger .info (f'iblvideo version { self .version } ' )
994+ check_nvidia_driver ()
995+
996+ _logger .info (f'Running DLC on { cam } Camera.' )
997+ command2run = f"{ self .scripts .joinpath ('run_dlc.sh' )} { str (self .dlcenv )} { file_mp4 } { overwrite } "
998+ _logger .info (command2run )
999+ process = subprocess .Popen (
1000+ command2run ,
1001+ shell = True ,
1002+ stdout = subprocess .PIPE ,
1003+ stderr = subprocess .PIPE ,
1004+ executable = "/bin/bash" ,
1005+ )
1006+ info , error = process .communicate ()
1007+ info_str = info .decode ("utf-8" ).strip ()
1008+ _logger .info (info_str )
1009+ if process .returncode != 0 :
1010+ error_str = error .decode ("utf-8" ).strip ()
1011+ _logger .error (f'DLC failed for { cam } Camera\n { error_str } ' )
1012+ self .status = - 1
1013+ # We dont' run motion energy, or add any files if dlc failed to run
1014+ continue
1015+ dlc_result = next (self .session_path .joinpath ('alf' ).glob (f'_ibl_{ cam } Camera.dlc*.pqt' ))
1016+ actual_outputs .append (dlc_result )
1017+
1018+ _logger .info (f'Computing motion energy for { cam } Camera' )
1019+ command2run = f"{ self .scripts .joinpath ('run_motion.sh' )} { str (self .dlcenv )} { file_mp4 } { dlc_result } "
1020+ _logger .info (command2run )
1021+ process = subprocess .Popen (
1022+ command2run ,
1023+ shell = True ,
1024+ stdout = subprocess .PIPE ,
1025+ stderr = subprocess .PIPE ,
1026+ executable = "/bin/bash" ,
1027+ )
1028+ info , error = process .communicate ()
1029+ info_str = info .decode ("utf-8" ).strip ()
1030+ _logger .info (info_str )
1031+ if process .returncode != 0 :
1032+ error_str = error .decode ("utf-8" ).strip ()
1033+ _logger .error (f'Motion energy failed for { cam } Camera \n { error_str } ' )
1034+ self .status = - 1
1035+ continue
1036+ actual_outputs .append (next (self .session_path .joinpath ('alf' ).glob (
1037+ f'{ cam } Camera.ROIMotionEnergy*.npy' )))
1038+ actual_outputs .append (next (self .session_path .joinpath ('alf' ).glob (
1039+ f'{ cam } ROIMotionEnergy.position*.npy' )))
1040+ except BaseException :
1041+ _logger .error (traceback .format_exc ())
1042+ self .status = - 1
1043+ continue
1044+ # If status is Incomplete, check that there is at least one output.
1045+ # # Otherwise make sure it gets set to Empty (outputs = None), and set status to -1 to make sure it doesn't slip
1046+ if self .status == - 3 and len (actual_outputs ) == 0 :
1047+ actual_outputs = None
1048+ self .status = - 1
1049+ return actual_outputs
9041050
9051051
9061052class EphysPostDLC (tasks .Task ):
@@ -965,15 +1111,22 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
9651111 f'Computations using camera.times will be skipped' )
9661112 self .status = - 1
9671113 times = False
1114+ elif dlc_t .shape [0 ] < len (dlc_thresh ):
1115+ _logger .error (f'Camera times shorter than DLC traces for { cam } camera. '
1116+ f'Computations using camera.times will be skipped' )
1117+ self .status = - 1
1118+ times = 'short'
9681119 # These features are only computed from left and right cam
9691120 if cam in ('left' , 'right' ):
9701121 features = pd .DataFrame ()
9711122 # If camera times are available, get the lick time stamps for combined array
972- if times :
1123+ if times is True :
9731124 _logger .info (f"Computing lick times for { cam } camera." )
9741125 combined_licks .append (get_licks (dlc_thresh , dlc_t ))
975- else :
976- _logger .warning (f"Skipping lick times for { cam } camera as no camera.times available." )
1126+ elif times is False :
1127+ _logger .warning (f"Skipping lick times for { cam } camera as no camera.times available" )
1128+ elif times == 'short' :
1129+ _logger .warning (f"Skipping lick times for { cam } camera as camera.times are too short" )
9771130 # Compute pupil diameter, raw and smoothed
9781131 _logger .info (f"Computing raw pupil diameter for { cam } camera." )
9791132 features ['pupilDiameter_raw' ] = get_pupil_diameter (dlc_thresh )
@@ -983,19 +1136,20 @@ def _run(self, overwrite=False, run_qc=True, plot_qc=True):
9831136 cam )
9841137 except BaseException :
9851138 _logger .error (f"Computing smooth pupil diameter for { cam } camera failed, saving all NaNs." )
1139+ _logger .error (traceback .format_exc ())
9861140 features ['pupilDiameter_smooth' ] = np .nan
9871141 # Safe to pqt
9881142 features_file = Path (self .session_path ).joinpath ('alf' , f'_ibl_{ cam } Camera.features.pqt' )
9891143 features .to_parquet (features_file )
9901144 output_files .append (features_file )
9911145
9921146 # For all cams, compute DLC qc if times available
993- if times and run_qc :
1147+ if times is True or times == 'short' and run_qc :
9941148 # Setting download_data to False because at this point the data should be there
9951149 qc = DlcQC (self .session_path , side = cam , one = self .one , download_data = False )
9961150 qc .run (update = True )
9971151 else :
998- if not times :
1152+ if times is False :
9991153 _logger .warning (f"Skipping QC for { cam } camera as no camera.times available" )
10001154 if not run_qc :
10011155 _logger .warning (f"Skipping QC for { cam } camera as run_qc=False" )
0 commit comments