From 02c931780e08bd21bc6b48136a5430a405478047 Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Thu, 1 Feb 2024 02:04:36 +0100 Subject: Adding VideoWriter class. Using it in demo. Documenting its use. --- docs/user_guide/gaze_analysis_pipeline/logging.md | 31 ++++++++-- .../gaze_analysis_pipeline/visualisation.md | 41 ++---------- src/argaze/utils/UtilsFeatures.py | 72 +++++++++++++++++++--- src/argaze/utils/demo_data/demo_frame_logger.py | 17 +++-- 4 files changed, 109 insertions(+), 52 deletions(-) diff --git a/docs/user_guide/gaze_analysis_pipeline/logging.md b/docs/user_guide/gaze_analysis_pipeline/logging.md index 617690f..8afb511 100644 --- a/docs/user_guide/gaze_analysis_pipeline/logging.md +++ b/docs/user_guide/gaze_analysis_pipeline/logging.md @@ -3,7 +3,7 @@ Log gaze analysis [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) and [ArLayer](../../argaze.md/#argaze.ArFeatures.ArLayer) analysis can be logged by registering observers to their **look** method. -## Enable ArFrame and ArLayer analysis logging +## Export gaze analysis to CSV file [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) and [ArLayer](../../argaze.md/#argaze.ArFeatures.ArLayer) have an observers attribute to enable pipeline execution logging. @@ -36,7 +36,7 @@ from argaze.utils import UtilsFeatures class ScanPathAnalysisLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.FileWriter): def on_look(self, timestamp, ar_frame): - """Log scan path metrics""" + """Log scan path metrics.ar_frame""" if ar_frame.analysis_available: @@ -76,7 +76,7 @@ from argaze.utils import UtilsFeatures class AOIScanPathAnalysisLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.FileWriter): def on_look(self, timestamp, ar_layer): - """Log aoi scan path metrics""" + """Log aoi scan path metrics.""" if ar_layer.analysis_available: @@ -104,4 +104,27 @@ Assuming that [ArGaze.GazeAnalysis.NGram](../../argaze.md/#argaze.GazeAnalysis.N !!! note "" - Learn to [script the pipeline](./advanced_topics/scripting.md) to know more about [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) and [ArLayers](../../argaze.md/#argaze.ArFeatures.ArLayer) attributes. \ No newline at end of file + Learn to [script the pipeline](./advanced_topics/scripting.md) to know more about [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) and [ArLayers](../../argaze.md/#argaze.ArFeatures.ArLayer) attributes. + +### Export gaze analysis to video file + +As explained in [pipeline steps visualisation chapter](visualisation.md), it is possible to get [ArFrame.image](../../argaze.md/#argaze.ArFeatures.ArFrame.image) once timestamped gaze positions have been processed by [ArFrame.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method. + +```python +from argaze import DataFeatures +from argaze.utils import UtilsFeatures + +class VideoRecorder(DataFeatures.PipelineStepObserver, UtilsFeatures.VideoWriter): + + def on_look(self, timestamp, ar_frame): + """Record frame image into video file.""" + + self.write(ar_frame.image()) + +# Export recorder as observer +__observers__ = { + "Video recorder": VideoRecorder(path="./video.mp4", width=1920, height=1080, fps=15) + } +``` + +Assuming that [ArFrame.image_parameters](../../argaze.md/#argaze.ArFeatures.ArFrame.image_parameters) are provided, ***video.mp4*** file would be created. \ No newline at end of file diff --git a/docs/user_guide/gaze_analysis_pipeline/visualisation.md b/docs/user_guide/gaze_analysis_pipeline/visualisation.md index 5f06fac..e046ddf 100644 --- a/docs/user_guide/gaze_analysis_pipeline/visualisation.md +++ b/docs/user_guide/gaze_analysis_pipeline/visualisation.md @@ -82,43 +82,6 @@ Here is an extract from the JSON ArFrame configuration file with a sample where Then, [ArFrame.image](../../argaze.md/#argaze.ArFeatures.ArFrame.image) method can be called in various situations. -## Export to PNG file - -Once timestamped gaze positions have been processed by [ArFrame.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method, it is possible to write [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) image into a file thanks to [OpenCV package](https://pypi.org/project/opencv-python/). - -```python -import cv2 - -# Assuming that timestamped gaze positions have been processed by ArFrame.look method -... - -# Export ArFrame image -cv2.imwrite('./ar_frame.png', ar_frame.image()) -``` - -## Export to MP4 file - -While timestamped gaze positions are processed by [ArFrame.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method, it is possible to write [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) image into a video file thanks to [OpenCV package](https://pypi.org/project/opencv-python/). - -```python -import cv2 - -# Assuming ArFrame is loaded -... - -# Create a video file to save ArFrame -video = cv2.VideoWriter('ar_frame.avi', cv2.VideoWriter_fourcc(*'MJPG'), 10, ar_frame.size) - -# Assuming that timestamped gaze positions are being processed by ArFrame.look method -... - - # Write ArFrame image into video file - video.write(ar_frame.image()) - -# Close video file -video.release() -``` - ## Live window display While timestamped gaze positions are processed by [ArFrame.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method, it is possible to display [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) image thanks to [OpenCV package](https://pypi.org/project/opencv-python/). @@ -147,3 +110,7 @@ if __name__ == '__main__': main() ``` + +!!! note "Export to video file" + + Video exportation is detailed in [gaze analysis logging chapter](logging.md). \ No newline at end of file diff --git a/src/argaze/utils/UtilsFeatures.py b/src/argaze/utils/UtilsFeatures.py index 0cb6d77..76d5f59 100644 --- a/src/argaze/utils/UtilsFeatures.py +++ b/src/argaze/utils/UtilsFeatures.py @@ -10,6 +10,8 @@ __license__ = "BSD" from typing import Tuple import time import types +import numpy +import cv2 def printProgressBar (iteration:int, total:int, prefix:str = '', suffix:str = '', decimals:int = 1, length:int = 100, fill:str = '█', printEnd:str = "\r"): """ @@ -152,7 +154,7 @@ def tuple_to_string(t: tuple, separator: str = ", ") -> str: return separator.join(f'\"{e}\"' for e in t) class FileWriter(): - """Write into a file line by line. + """Write data into a file line by line. Parameters: path: File path where to write data. @@ -185,11 +187,6 @@ class FileWriter(): print(header, file=self._file, flush=True) - def __del__(self): - """Close file.""" - - self._file.close() - def write(self, log: str|tuple): """Write log as a new line into file. @@ -203,4 +200,65 @@ class FileWriter(): log = tuple_to_string(log, self.separator) # Write into file - print(log, file=self._file, flush=True) \ No newline at end of file + print(log, file=self._file, flush=True) + + def __del__(self): + """Close file.""" + + self._file.close() + +class VideoWriter(): + """Write images into a file using ffmpeg. + + Parameters: + path: File path where to write images. + width: video horizontal resolution. + height: video vertical resolution. + fps: frame per second. + """ + + def __init__(self, path: str, width: int, height: int, fps: int): + """Open ffmpeg application as sub-process. + FFmpeg input PIPE: RAW images in BGR color format + FFmpeg output MP4 file encoded with HEVC codec. + + Arguments list: + -y Overwrite output file without asking + -s {width}x{height} Input resolution width x height (1344x756) + -pixel_format bgr24 Input frame color format is BGR with 8 bits per color component + -f rawvideo Input format: raw video + -r {fps} Frame rate: fps + -i pipe: ffmpeg input is a PIPE + -vcodec libx265 Video codec: H.265 (HEVC) + -pix_fmt yuv420p Output video color space YUV420 (saving space compared to YUV444) + -crf 24 Constant quality encoding (lower value for higher quality and larger output file). + {output_filename} Output file name: output_filename (output.mp4) + """ + + import subprocess as sp + import shlex + + self.__width = width + self.__height = height + + self.__process = sp.Popen(shlex.split(f'ffmpeg -hide_banner -loglevel error -y -s {width}x{height} -pixel_format bgr24 -f rawvideo -r {fps} -i pipe: -vcodec libx265 -x265-params log-level=error -pix_fmt yuv420p -crf 24 {path}'), stdin=sp.PIPE) + + def write(self, image: numpy.array): + """Write raw video frame to input stream of ffmpeg sub-process.""" + + # Resize image to adapt to video resolution + output = cv2.resize(image, dsize=(self.__width, self.__height), interpolation=cv2.INTER_LINEAR) + + self.__process.stdin.write(output.tobytes()) + + def __del__(self): + + # Close and flush stdin + self.__process.stdin.close() + + # Wait for sub-process to finish + self.__process.wait() + + # Terminate the sub-process + # Note: We don't have to terminate the sub-process (after process.wait(), the sub-process is supposed to be closed). + self.__process.terminate() \ No newline at end of file diff --git a/src/argaze/utils/demo_data/demo_frame_logger.py b/src/argaze/utils/demo_data/demo_frame_logger.py index 1c8046a..3448492 100644 --- a/src/argaze/utils/demo_data/demo_frame_logger.py +++ b/src/argaze/utils/demo_data/demo_frame_logger.py @@ -13,7 +13,7 @@ from argaze.utils import UtilsFeatures class FixationLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.FileWriter): def on_look(self, timestamp, frame): - """Log fixations""" + """Log fixations.""" # Log fixations if GazeFeatures.is_fixation(frame.last_gaze_movement) and frame.last_gaze_movement.finished: @@ -30,7 +30,7 @@ class FixationLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.FileWriter class ScanPathAnalysisLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.FileWriter): def on_look(self, timestamp, frame): - """Log scan path metrics""" + """Log scan path metrics.""" if frame.analysis_available: @@ -45,8 +45,17 @@ class ScanPathAnalysisLogger(DataFeatures.PipelineStepObserver, UtilsFeatures.Fi self.write(log) +class VideoRecorder(DataFeatures.PipelineStepObserver, UtilsFeatures.VideoWriter): + + def on_look(self, timestamp, frame): + """Write frame image.""" + + self.write(frame.image()) + # Export loggers instances to register them as pipeline step object observers __observers__ = { "Fixation logger": FixationLogger(path="_export/logs/fixations.csv", header="Timestamp (ms), Focus (px), Duration (ms), AOI"), - "Scan path analysis logger": ScanPathAnalysisLogger(path="_export/logs/scan_path_metrics.csv", header="Timestamp (ms), Duration (ms), Step, K, NNI, XXR") - } \ No newline at end of file + "Scan path analysis logger": ScanPathAnalysisLogger(path="_export/logs/scan_path_metrics.csv", header="Timestamp (ms), Duration (ms), Step, K, NNI, XXR"), + "Video recorder": VideoRecorder(path="_export/logs/video.mp4", width=1920, height=1080, fps=15) + } + -- cgit v1.1