From 354fb9b6e763f0834a917fc01b0f63eccd382c42 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Tue, 23 Apr 2024 14:29:40 +0200 Subject: Making LiveStream as LiveProcessingContext. --- src/argaze/utils/contexts/TobiiProGlasses2.py | 220 ++++++++++++-------------- 1 file changed, 101 insertions(+), 119 deletions(-) diff --git a/src/argaze/utils/contexts/TobiiProGlasses2.py b/src/argaze/utils/contexts/TobiiProGlasses2.py index 4491c34..b7db70b 100644 --- a/src/argaze/utils/contexts/TobiiProGlasses2.py +++ b/src/argaze/utils/contexts/TobiiProGlasses2.py @@ -71,12 +71,6 @@ TOBII_SEGMENT_INFO_FILENAME = "segment.json" TOBII_SEGMENT_VIDEO_FILENAME = "fullstream.mp4" TOBII_SEGMENT_DATA_FILENAME = "livedata.json.gz" -# Define default Tobii image_parameters values -DEFAULT_TOBII_IMAGE_PARAMETERS = { - "draw_something": False -} - - # Define extra classes to support Tobii data parsing @dataclass class DirSig(): @@ -335,12 +329,12 @@ class TobiiJsonDataParser(): return MarkerPosition(data['marker3d'], data['marker2d']) -class LiveStream(ArFeatures.ArContext): +class LiveStream(ArFeatures.LiveProcessingContext): @DataFeatures.PipelineStepInit def __init__(self, **kwargs): - # Init ArContext class + # Init LiveProcessingContext class super().__init__() # Init private attributes @@ -353,13 +347,15 @@ class LiveStream(ArFeatures.ArContext): self.__participant_name = None self.__participant_id = None + self.__calibration_status = None + self.__calibration_id = None + + self.__recording_id = None + self.__configuration = {} self.__parser = TobiiJsonDataParser() - # Init protected attributes - self._image_parameters = {**ArFeatures.DEFAULT_ARCONTEXT_IMAGE_PARAMETERS, **DEFAULT_TOBII_IMAGE_PARAMETERS} - @property def address(self) -> str: """Network address where to find the device.""" @@ -603,23 +599,6 @@ class LiveStream(ArFeatures.ArContext): # Stop video streaming threading.Thread.join(self.__video_thread) - @DataFeatures.PipelineStepImage - def image(self, draw_something: bool = None, **kwargs: dict) -> numpy.array: - """Get Tobii visualization. - - Parameters: - draw_something: example - kwargs: ArContext.image parameters - """ - - # Get context image - image = super().image(**kwargs) - - if draw_something: - cv2.putText(image, 'SOMETHING', (512, 512), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) - - return image - def __make_socket(self): """Create a socket to enable network communication.""" @@ -892,135 +871,107 @@ class LiveStream(ArFeatures.ArContext): return datetime.datetime.now().replace(microsecond=0).strftime(timeformat) - # CALIBRATION - - def calibrate(self, project_name, participant_name): + def calibrate(self): """Handle whole Tobii glasses calibration process.""" - # Start calibration - self.calibration_start(project_name, participant_name) - - # While calibrating... - status = self.calibration_status() - - while status == 'calibrating': - time.sleep(1) - status = self.calibration_status() - - if status == 'uncalibrated' or status == 'stale' or status == 'failed': - raise Exception(f'Calibration {status}') - - # CALIBRATION - - def calibration_start(self, project_name, participant_name): - """Start calibration process for project and participant.""" - - project_id = self.__get_project_id(project_name) - participant_id = self.get_participant_id(participant_name) - - # Init calibration id - self.__calibration_id = None + self.__calibration_status = None # Calibration have to be done for a project and a participant - if project_id is None or participant_id is None: - raise Exception(f'Setup project and participant before') + if self.__project_id is None or self.__participant_id is None: + raise Exception(f'Set project and participant names') + + # Request calibration id data = { - 'ca_project': project_id, + 'ca_project': self.__project_id, 'ca_type': 'default', - 'ca_participant': participant_id, + 'ca_participant': self.__participant_id, 'ca_created': self.__get_current_datetime() } - # Request calibration - json_data = self.__post_request('/api/calibrations', data) - # noinspection PyAttributeOutsideInit - self.__calibration_id = json_data['ca_id'] + self.__calibration_id = self.__post_request('/api/calibrations', data)['ca_id'] # Start calibration self.__post_request('/api/calibrations/' + self.__calibration_id + '/start') - def calibration_status(self) -> str: - """Ask for calibration status: calibrating, calibrated, stale, uncalibrated or failed.""" + # Check calibration status + self.__calibration_status_thread = threading.Thread(target=self.__check_calibration_status) + self.__calibration_status_thread.start() - if self.__calibration_id is not None: + def __check_calibration_status(self): - status = self.__wait_for_status('/api/calibrations/' + self.__calibration_id + '/status', 'ca_state', ['calibrating', 'calibrated', 'stale', 'uncalibrated', 'failed']) + while True: - # Forget calibration id - if status != 'calibrating': - # noinspection PyAttributeOutsideInit - self.__calibration_id = None + # Wait one second + time.sleep(1) - return status + # Check new calibration status + self.__calibration_status = self.__wait_for_status('/api/calibrations/' + self.__calibration_id + '/status', 'ca_state', ['calibrating', 'calibrated', 'stale', 'uncalibrated', 'failed']) - else: + # Exit once it is not calibrating + if self.__calibration_status != 'calibrating': - raise Exception(f'Start calibration before') + break - # RECORDING FEATURES + # Forget calibration id + self.__calibration_id = None - def __wait_for_recording_status(self, recording_id, status_array=['init', 'starting', 'recording', 'pausing', 'paused', 'stopping', 'stopped', 'done', 'stale', 'failed']): + def get_calibration_status(self) -> str: - return self.__wait_for_status('/api/recordings/' + recording_id + '/status', 'rec_state', status_array) + return self.__calibration_status - def create_recording(self, participant_name, recording_name='', recording_notes='') -> str: - """Create a new recording. + # RECORDING FEATURES - Returns: - recording id - """ + def create_recording(self, name: str = '', notes: str = ''): + """Create a new recording on the Tobii interface's SD Card.""" - participant_id = self.get_participant_id(participant_name) + # Reset recording id + self.__recording_id = None - if participant_id is None: - raise NameError(f'{participant_name} participant doesn\'t exist') + # Recording have to be done for a participant + if self.__participant_id is None: + + raise Exception(f'Set participant name') + # Request record id data = { - 'rec_participant': participant_id, + 'rec_participant': self.__participant_id, 'rec_info': { - 'EagleId': str(uuid.uuid5(uuid.NAMESPACE_DNS, participant_name)), - 'Name': recording_name, - 'Notes': recording_notes + 'EagleId': str(uuid.uuid5(uuid.NAMESPACE_DNS, self.participant)), + 'Name': name, + 'Notes': notes }, 'rec_created': self.__get_current_datetime() } - json_data = self.__post_request('/api/recordings', data) - - return json_data['rec_id'] - - def start_recording(self, recording_id) -> bool: - """Start recording on the Tobii interface's SD Card.""" + self.__recording_id = self.__post_request('/api/recordings', data)['rec_id'] - self.__post_request('/api/recordings/' + recording_id + '/start') - return self.__wait_for_recording_status(recording_id, ['recording']) == 'recording' - - def stop_recording(self, recording_id) -> bool: - """Stop recording on the Tobii interface's SD Card.""" + def __wait_for_recording_status(self, recording_id, status_array=['init', 'starting', 'recording', 'pausing', 'paused', 'stopping', 'stopped', 'done', 'stale', 'failed']): + + return self.__wait_for_status('/api/recordings/' + recording_id + '/status', 'rec_state', status_array) - self.__post_request('/api/recordings/' + recording_id + '/stop') - return self.__wait_for_recording_status(recording_id, ['done']) == "done" + def start_recording(self) -> bool: + """Start recording.""" - def pause_recording(self, recording_id) -> bool: - """Pause recording on the Tobii interface's SD Card.""" + self.__post_request('/api/recordings/' + self.__recording_id + '/start') + return self.__wait_for_recording_status(self.__recording_id, ['recording']) == 'recording' - self.__post_request('/api/recordings/' + recording_id + '/pause') - return self.__wait_for_recording_status(recording_id, ['paused']) == "paused" + def stop_recording(self) -> bool: + """Stop recording.""" - def __get_recording_status(self): - return self.get_status()['sys_recording'] + self.__post_request('/api/recordings/' + self.__recording_id + '/stop') + return self.__wait_for_recording_status(self.__recording_id, ['done']) == "done" - def get_current_recording_id(self) -> str: - """Get current recording id.""" + def pause_recording(self) -> bool: + """Pause recording.""" - return self.__get_recording_status()['rec_id'] + self.__post_request('/api/recordings/' + self.__recording_id + '/pause') + return self.__wait_for_recording_status(self.__recording_id, ['paused']) == "paused" - @property - def recording(self) -> bool: + def is_recording(self) -> bool: """Is it recording?""" - rec_status = self.__get_recording_status() + rec_status = self.get_status()['sys_recording'] if rec_status != {}: if rec_status['rec_state'] == "recording": @@ -1035,15 +986,15 @@ class LiveStream(ArFeatures.ArContext): # EVENTS AND EXPERIMENTAL VARIABLES - def __post_recording_data(self, event_type: str, event_tag=''): + def __post_data(self, event_type: str, event_tag=''): data = {'type': event_type, 'tag': event_tag} self.__post_request('/api/events', data, wait_for_response=False) def send_event(self, event_type: str, event_value=None): - self.__post_recording_data('JsonEvent', "{'event_type': '%s','event_value': '%s'}" % (event_type, event_value)) + self.__post_data('JsonEvent', "{'event_type': '%s','event_value': '%s'}" % (event_type, event_value)) def send_variable(self, variable_name: str, variable_value=None): - self.__post_recording_data(str(variable_name), str(variable_value)) + self.__post_data(str(variable_name), str(variable_value)) # MISC @@ -1123,6 +1074,40 @@ class LiveStream(ArFeatures.ArContext): data = {'sys_sc_fps': 50} json_data = self.__post_request('/api/system/conf/', data) + @DataFeatures.PipelineStepImage + def image(self, **kwargs): + """ + Get pipeline image with live processing information. + """ + logging.debug('LiveProcessingContext.image %s', self.name) + + image = super().image(**kwargs) + height, width, _ = image.shape + + # Display calibration status + calibration_panel = ((int(width/2), 0), (width, 50)) + + if self.__calibration_status is None: + + cv2.rectangle(image, calibration_panel[0], calibration_panel[1], (0, 0, 0), -1) + cv2.putText(image, 'Calibration required', (calibration_panel[0][0]+20, calibration_panel[0][1]+40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) + + elif self.__calibration_status == 'calibrating': + + cv2.rectangle(image, calibration_panel[0], calibration_panel[1], (127, 127, 127), -1) + cv2.putText(image, f'Calibrating...', (calibration_panel[0][0]+20, calibration_panel[0][1]+40), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) + + elif self.__calibration_status != 'calibrated': + + cv2.rectangle(image, calibration_panel[0], calibration_panel[1], (0, 0, 127), -1) + cv2.putText(image, f'Calibration {calibration_status}', (calibration_panel[0][0]+20, calibration_panel[0][1]+40), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) + + else: + + cv2.rectangle(image, calibration_panel[0], calibration_panel[1], (0, 127, 0), -1) + cv2.putText(image, f'Calibration succeeded', (calibration_panel[0][0]+20, calibration_panel[0][1]+40), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) + + return image class PostProcessing(ArFeatures.PostProcessingContext): @@ -1176,9 +1161,6 @@ class PostProcessing(ArFeatures.PostProcessingContext): # Initialize inconsistent timestamp monitoring self.__last_data_ts = None - # Init protected attributes - self._image_parameters = {**ArFeatures.DEFAULT_ARCONTEXT_IMAGE_PARAMETERS, **DEFAULT_TOBII_IMAGE_PARAMETERS} - @property def segment(self) -> str: """Path to segment folder.""" -- cgit v1.1