aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/user_guide/gaze_analysis_pipeline/logging.md31
-rw-r--r--docs/user_guide/gaze_analysis_pipeline/visualisation.md41
-rw-r--r--src/argaze/utils/UtilsFeatures.py72
-rw-r--r--src/argaze/utils/demo_data/demo_frame_logger.py17
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)
+ }
+