@@ -248,6 +248,40 @@ def init_ui(self):
248248 self .export_resolution_combo .setCurrentText ("4x pixel scale" )
249249 export_layout .addWidget (self .export_resolution_combo )
250250
251+ # Frame range controls (only for non-mode shape videos)
252+ if not self .is_mode_shape :
253+ self .frame_range_label = QtWidgets .QLabel ("Frame Range:" )
254+ export_layout .addWidget (self .frame_range_label )
255+
256+ frame_range_layout = QtWidgets .QHBoxLayout ()
257+
258+ # Start frame
259+ self .start_frame_spin = QtWidgets .QSpinBox ()
260+ self .start_frame_spin .setRange (0 , self .video .shape [0 ] - 1 )
261+ self .start_frame_spin .setValue (0 )
262+ self .start_frame_spin .setFixedWidth (80 )
263+ self .start_frame_spin .valueChanged .connect (self .on_start_frame_changed )
264+ frame_range_layout .addWidget (self .start_frame_spin )
265+
266+ # Stop frame
267+ self .stop_frame_spin = QtWidgets .QSpinBox ()
268+ self .stop_frame_spin .setRange (0 , self .video .shape [0 ] - 1 )
269+ self .stop_frame_spin .setValue (self .video .shape [0 ] - 1 )
270+ self .stop_frame_spin .setFixedWidth (80 )
271+ self .stop_frame_spin .valueChanged .connect (self .on_stop_frame_changed )
272+ frame_range_layout .addWidget (self .stop_frame_spin )
273+
274+ export_layout .addLayout (frame_range_layout )
275+
276+ # Update the label with initial frame count
277+ self .update_frame_range_label ()
278+
279+ # Full range checkbox
280+ self .full_range_checkbox = QtWidgets .QCheckBox ("Full Range" )
281+ self .full_range_checkbox .setChecked (True ) # Initially checked since defaults are full range
282+ self .full_range_checkbox .stateChanged .connect (self .on_full_range_checkbox_changed )
283+ export_layout .addWidget (self .full_range_checkbox )
284+
251285 # Duration for mode shapes
252286 if self .is_mode_shape :
253287 export_layout .addWidget (QtWidgets .QLabel ("Duration (seconds):" ))
@@ -409,6 +443,68 @@ def update_point_size(self):
409443 size = self .point_size_spin .value ()
410444 self .scatter .setSize (size )
411445
446+ def on_start_frame_changed (self , value ):
447+ # Ensure start frame is not greater than stop frame
448+ if hasattr (self , 'stop_frame_spin' ) and value > self .stop_frame_spin .value ():
449+ self .stop_frame_spin .setValue (value )
450+
451+ # Update the frame range label
452+ self .update_frame_range_label ()
453+
454+ # Update checkbox state based on whether we have full range
455+ self .update_full_range_checkbox_state ()
456+
457+ def on_stop_frame_changed (self , value ):
458+ # Ensure stop frame is not less than start frame
459+ if hasattr (self , 'start_frame_spin' ) and value < self .start_frame_spin .value ():
460+ self .start_frame_spin .setValue (value )
461+
462+ # Update the frame range label
463+ self .update_frame_range_label ()
464+
465+ # Update checkbox state based on whether we have full range
466+ self .update_full_range_checkbox_state ()
467+
468+ def update_frame_range_label (self ):
469+ """Update the frame range label with current frame count."""
470+ if not self .is_mode_shape and hasattr (self , 'frame_range_label' ):
471+ start_frame = self .start_frame_spin .value ()
472+ stop_frame = self .stop_frame_spin .value ()
473+ total_frames = stop_frame - start_frame + 1
474+ self .frame_range_label .setText (f"Frame Range: ({ total_frames } frames)" )
475+
476+ def on_full_range_checkbox_changed (self , state ):
477+ """Handle full range checkbox state changes."""
478+ if not self .is_mode_shape :
479+ if state == QtCore .Qt .CheckState .Checked .value :
480+ # Set to full range
481+ self .start_frame_spin .blockSignals (True )
482+ self .stop_frame_spin .blockSignals (True )
483+ self .start_frame_spin .setValue (0 )
484+ self .stop_frame_spin .setValue (self .video .shape [0 ] - 1 )
485+ self .start_frame_spin .blockSignals (False )
486+ self .stop_frame_spin .blockSignals (False )
487+
488+ # Update the frame range label
489+ self .update_frame_range_label ()
490+
491+ def update_full_range_checkbox_state (self ):
492+ """Update the checkbox state based on current spinbox values."""
493+ if not self .is_mode_shape and hasattr (self , 'full_range_checkbox' ):
494+ is_full_range = (self .start_frame_spin .value () == 0 and
495+ self .stop_frame_spin .value () == self .video .shape [0 ] - 1 )
496+
497+ # Block signals to prevent recursive calls
498+ self .full_range_checkbox .blockSignals (True )
499+ self .full_range_checkbox .setChecked (is_full_range )
500+ self .full_range_checkbox .blockSignals (False )
501+
502+ def set_full_range (self ):
503+ """Set the frame range to cover the full video."""
504+ if not self .is_mode_shape :
505+ self .start_frame_spin .setValue (0 )
506+ self .stop_frame_spin .setValue (self .video .shape [0 ] - 1 )
507+
412508 def next_frame (self ):
413509 if self .is_mode_shape :
414510 self .current_frame = (self .current_frame + 1 ) % int (self .fps * self .time_per_period )
@@ -537,8 +633,12 @@ def export_video(self):
537633 if self .is_mode_shape :
538634 duration = self .duration_spin .value ()
539635 total_frames = int (export_fps * duration )
636+ start_frame = 0
637+ stop_frame = total_frames - 1
540638 else :
541- total_frames = self .video .shape [0 ]
639+ start_frame = self .start_frame_spin .value ()
640+ stop_frame = self .stop_frame_spin .value ()
641+ total_frames = stop_frame - start_frame + 1
542642
543643 # Initialize video writer
544644 writer = cv2 .VideoWriter (file_path , fourcc , export_fps , (export_width , export_height ))
@@ -578,9 +678,11 @@ def export_video(self):
578678 phase = np .angle (displ_raw )
579679 displ = scale * amplitude * np .sin (2 * np .pi * t - phase )
580680 else :
581- self .current_frame = frame_idx
582- base_frame = self .video [self .current_frame ]
583- displ = self .displacements [:, self .current_frame , :] * scale
681+ # For regular videos, use the actual frame index within the specified range
682+ actual_frame_idx = start_frame + frame_idx
683+ self .current_frame = actual_frame_idx
684+ base_frame = self .video [actual_frame_idx ]
685+ displ = self .displacements [:, actual_frame_idx , :] * scale
584686
585687 # Create the export frame by scaling the video frame pixel-perfectly
586688 # Convert to RGB for proper color handling
@@ -638,10 +740,17 @@ def export_video(self):
638740
639741 writer .release ()
640742
743+ # Create success message with frame range info
744+ if self .is_mode_shape :
745+ frame_info = f"Duration: { self .duration_spin .value ():.1f} s"
746+ else :
747+ frame_info = f"Frames: { start_frame } to { stop_frame } ({ total_frames } total)"
748+
641749 QtWidgets .QMessageBox .information (self , "Export Complete" ,
642750 f"Video exported successfully to:\n { file_path } \n "
643751 f"Resolution: { export_width } x{ export_height } "
644- f"(pixel scale: { pixel_scale } x)" )
752+ f"(pixel scale: { pixel_scale } x)\n "
753+ f"{ frame_info } " )
645754
646755 except Exception as e :
647756 import traceback
0 commit comments