@@ -39,6 +39,14 @@ class InferenceContext:
3939
4040class FrameBuffer :
4141 def __init__ (self , buffer_id : str , roi : vc .Rect , max_capacity : int , timestamp : float ):
42+ """Initialize a thread-safe frame buffer for a specific region of interest.
43+
44+ Args:
45+ buffer_id: Unique identifier for the buffer.
46+ roi: The region of interest (ROI) associated with this buffer.
47+ max_capacity: The maximum number of frames the buffer can hold.
48+ timestamp: The creation timestamp of the buffer.
49+ """
4250 self .id = buffer_id
4351 self .roi = roi
4452 self .created_at = timestamp
@@ -51,13 +59,27 @@ def count(self) -> int:
5159 return len (self .buffer )
5260
5361 def append (self , frame : np .ndarray , context : InferenceContext ):
62+ """Add a frame and its metadata to the buffer, maintaining max capacity.
63+
64+ Args:
65+ frame: The parsed RGB image data.
66+ context: Metadata including timestamp and ROI.
67+ """
5468 self .buffer .append ((frame , context ))
5569 self .last_seen = context .timestamp
5670 if len (self .buffer ) > self .max_capacity :
5771 overflow = len (self .buffer ) - self .max_capacity
5872 self .buffer = self .buffer [overflow :]
5973
6074 def execute (self , take_count : int , keep_count : int ) -> list :
75+ """Extract frames for inference and manage buffer overlap.
76+
77+ Args:
78+ take_count: Number of frames to extract for the current batch.
79+ keep_count: Number of frames to retain for the next sliding window.
80+ Returns:
81+ payload: A list of (frame, context) tuples, or None if insufficient frames.
82+ """
6183 if take_count <= 0 or len (self .buffer ) < take_count :
6284 return None
6385 payload = self .buffer [:take_count ]
@@ -66,8 +88,13 @@ def execute(self, take_count: int, keep_count: int) -> list:
6688 return payload \
6789
6890class BufferManager :
69- """Thread-safe buffer manager."""
91+ """Thread-safe manager for multiple streaming frame buffers ."""
7092 def __init__ (self , buffer_config : vc .BufferConfig ):
93+ """Initialize the manager with specific buffering and overlap constraints.
94+
95+ Args:
96+ buffer_config: Configuration for stream window sizes and overlaps.
97+ """
7198 self .buffer_planner = vc .BufferPlanner (buffer_config )
7299 self .buffer_config = buffer_config
73100 self .buffers = {}
@@ -84,6 +111,14 @@ def _get_active_metadata(self):
84111 ]
85112
86113 def register_target (self , target_roi : vc .Rect , timestamp : float ):
114+ """Register or update a target ROI in the buffer planner.
115+
116+ Args:
117+ target_roi: The latest detected face ROI to track.
118+ timestamp: The current frame timestamp.
119+ Returns:
120+ buffer_id: The ID of the buffer assigned to this target, or None.
121+ """
87122 with self .lock :
88123 self .current_timestamp = max (self .current_timestamp , timestamp )
89124 action = self .buffer_planner .evaluate_target (target_roi , timestamp , self ._get_active_metadata ())
@@ -100,12 +135,26 @@ def register_target(self, target_roi: vc.Rect, timestamp: float):
100135 return None
101136
102137 def append (self , buffer_id : str , frame : np .ndarray , context : InferenceContext ):
138+ """Route a frame to a specific active buffer.
139+
140+ Args:
141+ buffer_id: The identifier for the target buffer.
142+ frame: The parsed RGB image data.
143+ context: Metadata including timestamp and ROI.
144+ """
103145 with self .lock :
104146 self .current_timestamp = max (self .current_timestamp , context .timestamp )
105147 if buffer_id in self .buffers :
106148 self .buffers [buffer_id ].append (frame , context )
107149
108150 def poll (self , flush : bool = False ):
151+ """Poll the planner for the next required inference command.
152+
153+ Args:
154+ flush: If True, forces the planner to return commands for remaining data.
155+ Returns:
156+ command: A vc.InferenceCommand containing the buffer ID and frame counts.
157+ """
109158 with self .lock :
110159 has_state = self .state is not None
111160 plan = self .buffer_planner .poll (
@@ -121,30 +170,62 @@ def poll(self, flush: bool = False):
121170 return plan .command
122171
123172 def execute (self , command : vc .InferenceCommand ) -> list :
173+ """Execute a specific inference command by extracting data from the relevant buffer.
174+
175+ Args:
176+ command: The command containing buffer ID and frame counts.
177+ Returns:
178+ window: A list of frames and contexts ready for inference.
179+ """
124180 with self .lock :
125181 if command .buffer_id in self .buffers :
126182 return self .buffers [command .buffer_id ].execute (command .take_count , command .keep_count )
127183 return None
128184
129185 def get_state (self ):
186+ """Retrieve the current physiological state of the rPPG model.
187+
188+ Returns:
189+ state: The model's internal state vector or None.
190+ """
130191 with self .lock :
131192 return self .state
132193
133194 def update_state (self , new_state ):
195+ """Update the internal state of the rPPG model after an inference step.
196+
197+ Args:
198+ new_state: The updated state vector returned by the API.
199+ """
134200 with self .lock :
135201 self .state = new_state
136202
137203 def get_all_buffers (self ):
204+ """Get a list of all currently active buffers.
205+
206+ Returns:
207+ buffers: A list of (buffer_id, roi) tuples.
208+ """
138209 with self .lock :
139210 return [(buf .id , buf .roi ) for buf in self .buffers .values ()]
140211
141212 def reset (self ):
213+ """Clear all active buffers and reset the model state."""
142214 with self .lock :
143215 self .buffers .clear ()
144216 self .state = None
145217
146218class StreamSession :
219+ """Context manager handling background inference and buffer synchronization for live streams."""
147220 def __init__ (self , rppg_method , face_detector = None , fdet_fs = 1.0 , on_result : Optional [Callable ] = None ):
221+ """Initialize the streaming session and start the background inference thread.
222+
223+ Args:
224+ rppg_method: The method instance (e.g., VitalLensRPPGMethod) to use.
225+ face_detector: Optional instance of FaceDetector for automatic tracking.
226+ fdet_fs: Frequency [Hz] at which to run automatic face detection.
227+ on_result: Optional callback for asynchronous result handling.
228+ """
148229 self .rppg = rppg_method
149230 self .face_detector = face_detector
150231 self .fdet_fs = fdet_fs
@@ -167,12 +248,19 @@ def __exit__(self, exc_type, exc_val, exc_tb):
167248 self .close ()
168249
169250 def close (self ):
251+ """Stop the background thread and shut down the streaming session."""
170252 self .running = False
171253 if self .thread .is_alive ():
172254 self .thread .join (timeout = 2.0 )
173255
174256 def push (self , frame : np .ndarray , timestamp : float , face : np .ndarray = None ):
175- """Pushes a single frame to the ingestion pipeline on the main thread."""
257+ """Push a single frame and its timestamp into the streaming pipeline.
258+
259+ Args:
260+ frame: The input video frame of shape (h, w, 3) in RGB format.
261+ timestamp: The absolute timestamp of the frame in seconds.
262+ face: Optional pre-detected face box [x0, y0, x1, y1].
263+ """
176264 if not self .running : return
177265 # Throttled face detection
178266 min_interval = 1.0 / self .rppg .fps_target
@@ -235,14 +323,21 @@ def push(self, frame: np.ndarray, timestamp: float, face: np.ndarray = None):
235323 self .buffer_manager .append (buf_id , payload , ctx )
236324
237325 def get_result (self , block = False , timeout = None ):
238- """Pulls the latest result from the queue."""
326+ """Pull the latest inference results from the session queue.
327+
328+ Args:
329+ block: Whether to block until a result is available.
330+ timeout: Maximum time to block if block is True.
331+ Returns:
332+ results: A list of analysis results for detected faces.
333+ """
239334 try :
240335 return self .result_queue .get (block = block , timeout = timeout )
241336 except queue .Empty :
242337 return None
243338
244339 def _inference_loop (self ):
245- """Background thread executing inference without blocking the webcam ."""
340+ """Background loop that polls for ready buffers and executes model inference ."""
246341 while self .running :
247342 command = self .buffer_manager .poll ()
248343 if not command :
@@ -279,7 +374,13 @@ def _inference_loop(self):
279374 self .vc_session .reset ()
280375
281376 def _format_result (self , session_result ):
282- # TODO: Is there a smarter way to do this?
377+ """Format the raw session result into the standard vitallens-python result dictionary.
378+
379+ Args:
380+ session_result: The vc.SessionResult object from vitallens-core.
381+ Returns:
382+ res_dict: A list containing a formatted results dictionary for the face.
383+ """
283384 face_dict = {
284385 'coordinates' : [],
285386 'confidence' : [],
0 commit comments