feat: add relax option and improve debug mode output
- Add --relax option to include N frames before/after threshold frames - Implement two-pass approach for relax feature - Generate PowerShell command in debug mode to test all suggested thresholds - Place debug mode outputs in results/debug folder by default
This commit is contained in:
parent
8da27a1ead
commit
c8e5a299d6
121
main.py
121
main.py
@ -148,17 +148,26 @@ def analyze_changes_only(video_path, x_column=None, y_row=None, debug_output=Non
|
|||||||
|
|
||||||
# Suggest thresholds
|
# Suggest thresholds
|
||||||
percentiles = [50, 75, 90, 95, 99]
|
percentiles = [50, 75, 90, 95, 99]
|
||||||
|
threshold_values = []
|
||||||
print(f"\nSuggested threshold values:")
|
print(f"\nSuggested threshold values:")
|
||||||
for p in percentiles:
|
for p in percentiles:
|
||||||
thresh = np.percentile(changes, p)
|
thresh = np.percentile(changes, p)
|
||||||
|
threshold_values.append(thresh)
|
||||||
frames_above = np.sum(np.array(changes) >= thresh)
|
frames_above = np.sum(np.array(changes) >= thresh)
|
||||||
compression = (len(changes) - frames_above) / len(changes) * 100
|
compression = (len(changes) - frames_above) / len(changes) * 100
|
||||||
print(f" {p}th percentile: {thresh:.4f} (keeps {frames_above} frames, {compression:.1f}% compression)")
|
print(f" {p}th percentile: {thresh:.4f} (keeps {frames_above} frames, {compression:.1f}% compression)")
|
||||||
|
|
||||||
|
# Generate PowerShell command to test all suggested thresholds
|
||||||
|
threshold_list = ",".join([f"{t:.4f}" for t in threshold_values])
|
||||||
|
video_path_str = str(video_path.absolute())
|
||||||
|
pwsh_cmd = f"{threshold_list} | %{{uv run .\\main.py {video_path_str} --threshold $_}}"
|
||||||
|
print(f"\nPowerShell command to test all thresholds:")
|
||||||
|
print(f" {pwsh_cmd}")
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
|
|
||||||
|
|
||||||
def extract_column_strip(video_path, x_column, output_path, change_threshold=0.005):
|
def extract_column_strip(video_path, x_column, output_path, change_threshold=0.005, relax=0):
|
||||||
"""
|
"""
|
||||||
Extract vertical strip at x_column from each frame of the video.
|
Extract vertical strip at x_column from each frame of the video.
|
||||||
Only include frames where the change exceeds the threshold.
|
Only include frames where the change exceeds the threshold.
|
||||||
@ -168,6 +177,7 @@ def extract_column_strip(video_path, x_column, output_path, change_threshold=0.0
|
|||||||
x_column: X-coordinate of the column to extract
|
x_column: X-coordinate of the column to extract
|
||||||
output_path: Path for output image
|
output_path: Path for output image
|
||||||
change_threshold: Minimum change threshold (0-1) to include frame
|
change_threshold: Minimum change threshold (0-1) to include frame
|
||||||
|
relax: Number of extra frames to include before/after threshold frames
|
||||||
"""
|
"""
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
cap = cv2.VideoCapture(str(video_path))
|
||||||
|
|
||||||
@ -185,12 +195,13 @@ def extract_column_strip(video_path, x_column, output_path, change_threshold=0.0
|
|||||||
print(f"Processing {total_frames} frames...")
|
print(f"Processing {total_frames} frames...")
|
||||||
print(f"Extracting column {x_column} from {frame_width}x{frame_height} frames")
|
print(f"Extracting column {x_column} from {frame_width}x{frame_height} frames")
|
||||||
print(f"Change threshold: {change_threshold}")
|
print(f"Change threshold: {change_threshold}")
|
||||||
|
if relax > 0:
|
||||||
|
print(f"Relax: including {relax} frames before/after threshold frames")
|
||||||
|
|
||||||
# Collect significant columns
|
# First pass: collect all columns and identify significant frames
|
||||||
significant_columns = []
|
all_columns = []
|
||||||
|
changes = []
|
||||||
previous_column = None
|
previous_column = None
|
||||||
included_frames = 0
|
|
||||||
skipped_frames = 0
|
|
||||||
|
|
||||||
frame_idx = 0
|
frame_idx = 0
|
||||||
while True:
|
while True:
|
||||||
@ -200,29 +211,39 @@ def extract_column_strip(video_path, x_column, output_path, change_threshold=0.0
|
|||||||
|
|
||||||
# Extract current column
|
# Extract current column
|
||||||
current_column = frame[:, x_column, :].copy()
|
current_column = frame[:, x_column, :].copy()
|
||||||
|
all_columns.append(current_column)
|
||||||
|
|
||||||
# Check if this is the first frame or if change is significant
|
# Calculate change from previous frame
|
||||||
include_frame = False
|
if previous_column is not None:
|
||||||
if previous_column is None:
|
|
||||||
include_frame = True # Always include first frame
|
|
||||||
else:
|
|
||||||
change = calculate_line_difference(current_column, previous_column)
|
change = calculate_line_difference(current_column, previous_column)
|
||||||
if change >= change_threshold:
|
changes.append(change)
|
||||||
include_frame = True
|
|
||||||
|
|
||||||
if include_frame:
|
|
||||||
significant_columns.append(current_column)
|
|
||||||
previous_column = current_column
|
|
||||||
included_frames += 1
|
|
||||||
else:
|
else:
|
||||||
skipped_frames += 1
|
changes.append(0) # First frame has no change
|
||||||
|
|
||||||
|
previous_column = current_column
|
||||||
frame_idx += 1
|
frame_idx += 1
|
||||||
|
|
||||||
if frame_idx % 100 == 0:
|
if frame_idx % 100 == 0:
|
||||||
print(f"Processed {frame_idx}/{total_frames} frames")
|
print(f"Processed {frame_idx}/{total_frames} frames")
|
||||||
|
|
||||||
cap.release()
|
cap.release()
|
||||||
|
|
||||||
|
# Second pass: determine which frames to include
|
||||||
|
include_mask = [False] * len(all_columns)
|
||||||
|
|
||||||
|
for i, change in enumerate(changes):
|
||||||
|
if i == 0 or change >= change_threshold:
|
||||||
|
# Mark this frame and surrounding frames
|
||||||
|
start = max(0, i - relax)
|
||||||
|
end = min(len(all_columns), i + relax + 1)
|
||||||
|
for j in range(start, end):
|
||||||
|
include_mask[j] = True
|
||||||
|
|
||||||
|
# Collect significant columns
|
||||||
|
significant_columns = [col for i, col in enumerate(all_columns) if include_mask[i]]
|
||||||
|
included_frames = sum(include_mask)
|
||||||
|
skipped_frames = len(all_columns) - included_frames
|
||||||
|
|
||||||
if not significant_columns:
|
if not significant_columns:
|
||||||
raise ValueError("No significant changes detected. Try lowering the threshold.")
|
raise ValueError("No significant changes detected. Try lowering the threshold.")
|
||||||
|
|
||||||
@ -240,7 +261,7 @@ def extract_column_strip(video_path, x_column, output_path, change_threshold=0.0
|
|||||||
cv2.imwrite(str(output_path), strip_image)
|
cv2.imwrite(str(output_path), strip_image)
|
||||||
|
|
||||||
|
|
||||||
def extract_row_strip(video_path, y_row, output_path, change_threshold=0.01):
|
def extract_row_strip(video_path, y_row, output_path, change_threshold=0.01, relax=0):
|
||||||
"""
|
"""
|
||||||
Extract horizontal strip at y_row from each frame of the video.
|
Extract horizontal strip at y_row from each frame of the video.
|
||||||
Only include frames where the change exceeds the threshold.
|
Only include frames where the change exceeds the threshold.
|
||||||
@ -250,6 +271,7 @@ def extract_row_strip(video_path, y_row, output_path, change_threshold=0.01):
|
|||||||
y_row: Y-coordinate of the row to extract
|
y_row: Y-coordinate of the row to extract
|
||||||
output_path: Path for output image
|
output_path: Path for output image
|
||||||
change_threshold: Minimum change threshold (0-1) to include frame
|
change_threshold: Minimum change threshold (0-1) to include frame
|
||||||
|
relax: Number of extra frames to include before/after threshold frames
|
||||||
"""
|
"""
|
||||||
cap = cv2.VideoCapture(str(video_path))
|
cap = cv2.VideoCapture(str(video_path))
|
||||||
|
|
||||||
@ -267,12 +289,13 @@ def extract_row_strip(video_path, y_row, output_path, change_threshold=0.01):
|
|||||||
print(f"Processing {total_frames} frames...")
|
print(f"Processing {total_frames} frames...")
|
||||||
print(f"Extracting row {y_row} from {frame_width}x{frame_height} frames")
|
print(f"Extracting row {y_row} from {frame_width}x{frame_height} frames")
|
||||||
print(f"Change threshold: {change_threshold}")
|
print(f"Change threshold: {change_threshold}")
|
||||||
|
if relax > 0:
|
||||||
|
print(f"Relax: including {relax} frames before/after threshold frames")
|
||||||
|
|
||||||
# Collect significant rows
|
# First pass: collect all rows and identify significant frames
|
||||||
significant_rows = []
|
all_rows = []
|
||||||
|
changes = []
|
||||||
previous_row = None
|
previous_row = None
|
||||||
included_frames = 0
|
|
||||||
skipped_frames = 0
|
|
||||||
|
|
||||||
frame_idx = 0
|
frame_idx = 0
|
||||||
while True:
|
while True:
|
||||||
@ -282,29 +305,39 @@ def extract_row_strip(video_path, y_row, output_path, change_threshold=0.01):
|
|||||||
|
|
||||||
# Extract current row
|
# Extract current row
|
||||||
current_row = frame[y_row, :, :].copy()
|
current_row = frame[y_row, :, :].copy()
|
||||||
|
all_rows.append(current_row)
|
||||||
|
|
||||||
# Check if this is the first frame or if change is significant
|
# Calculate change from previous frame
|
||||||
include_frame = False
|
if previous_row is not None:
|
||||||
if previous_row is None:
|
|
||||||
include_frame = True # Always include first frame
|
|
||||||
else:
|
|
||||||
change = calculate_line_difference(current_row, previous_row)
|
change = calculate_line_difference(current_row, previous_row)
|
||||||
if change >= change_threshold:
|
changes.append(change)
|
||||||
include_frame = True
|
|
||||||
|
|
||||||
if include_frame:
|
|
||||||
significant_rows.append(current_row)
|
|
||||||
previous_row = current_row
|
|
||||||
included_frames += 1
|
|
||||||
else:
|
else:
|
||||||
skipped_frames += 1
|
changes.append(0) # First frame has no change
|
||||||
|
|
||||||
|
previous_row = current_row
|
||||||
frame_idx += 1
|
frame_idx += 1
|
||||||
|
|
||||||
if frame_idx % 100 == 0:
|
if frame_idx % 100 == 0:
|
||||||
print(f"Processed {frame_idx}/{total_frames} frames")
|
print(f"Processed {frame_idx}/{total_frames} frames")
|
||||||
|
|
||||||
cap.release()
|
cap.release()
|
||||||
|
|
||||||
|
# Second pass: determine which frames to include
|
||||||
|
include_mask = [False] * len(all_rows)
|
||||||
|
|
||||||
|
for i, change in enumerate(changes):
|
||||||
|
if i == 0 or change >= change_threshold:
|
||||||
|
# Mark this frame and surrounding frames
|
||||||
|
start = max(0, i - relax)
|
||||||
|
end = min(len(all_rows), i + relax + 1)
|
||||||
|
for j in range(start, end):
|
||||||
|
include_mask[j] = True
|
||||||
|
|
||||||
|
# Collect significant rows
|
||||||
|
significant_rows = [row for i, row in enumerate(all_rows) if include_mask[i]]
|
||||||
|
included_frames = sum(include_mask)
|
||||||
|
skipped_frames = len(all_rows) - included_frames
|
||||||
|
|
||||||
if not significant_rows:
|
if not significant_rows:
|
||||||
raise ValueError("No significant changes detected. Try lowering the threshold.")
|
raise ValueError("No significant changes detected. Try lowering the threshold.")
|
||||||
|
|
||||||
@ -360,6 +393,13 @@ def main():
|
|||||||
help="Change threshold (0-1) for including frames (default: 0.01)"
|
help="Change threshold (0-1) for including frames (default: 0.01)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--relax",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Include N extra frames before/after frames exceeding threshold (default: 0)"
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--debug",
|
"--debug",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@ -407,8 +447,11 @@ def main():
|
|||||||
print(f"No extension specified, using: {output_path}")
|
print(f"No extension specified, using: {output_path}")
|
||||||
else:
|
else:
|
||||||
# Auto-generate output path in results folder with UUID
|
# Auto-generate output path in results folder with UUID
|
||||||
|
if args.debug:
|
||||||
|
results_dir = Path("results/debug")
|
||||||
|
else:
|
||||||
results_dir = Path("results")
|
results_dir = Path("results")
|
||||||
results_dir.mkdir(exist_ok=True)
|
results_dir.mkdir(parents=True, exist_ok=True)
|
||||||
# Generate 4-character UUID prefix
|
# Generate 4-character UUID prefix
|
||||||
uuid_prefix = uuid.uuid4().hex[:4]
|
uuid_prefix = uuid.uuid4().hex[:4]
|
||||||
# Include threshold in filename
|
# Include threshold in filename
|
||||||
@ -434,10 +477,10 @@ def main():
|
|||||||
# Normal mode: extract strip photography
|
# Normal mode: extract strip photography
|
||||||
if args.xcolumn is not None:
|
if args.xcolumn is not None:
|
||||||
print(f"Column mode: Extracting vertical line at x={args.xcolumn}")
|
print(f"Column mode: Extracting vertical line at x={args.xcolumn}")
|
||||||
extract_column_strip(video_path, args.xcolumn, output_path, args.threshold)
|
extract_column_strip(video_path, args.xcolumn, output_path, args.threshold, args.relax)
|
||||||
else:
|
else:
|
||||||
print(f"Row mode: Extracting horizontal line at y={args.yrow}")
|
print(f"Row mode: Extracting horizontal line at y={args.yrow}")
|
||||||
extract_row_strip(video_path, args.yrow, output_path, args.threshold)
|
extract_row_strip(video_path, args.yrow, output_path, args.threshold, args.relax)
|
||||||
|
|
||||||
print("Strip photography extraction completed successfully!")
|
print("Strip photography extraction completed successfully!")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user