bjooo's picture
Upload folder using huggingface_hub
9c98083 verified
Raw History Blame Contribute Delete
6.47 kB
"""Video scene-cut detection, clip extraction, and subject-centered crop/resize.
Uses plain (non-relative) imports of sibling modules so this file works whether
invoked as ``python dataset_builder/build.py`` or ``python -m dataset_builder.build``
(build.py inserts its own directory onto sys.path before importing).
"""
import math
import statistics
import cv2
import imageio
import numpy as np
from PIL import Image
import crop_resize
from detect import detect_subject_bbox, subdivide_bbox
def _frame_hist(frame_bgr):
hsv = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2HSV)
hist = cv2.calcHist([hsv], [0, 1], None, [50, 60], [0, 180, 0, 256])
cv2.normalize(hist, hist)
return hist
def find_scene_cuts(video_path, diff_thresh=0.45, sample_stride=1):
"""Returns a sorted list of frame indices where a scene cut is detected,
via Bhattacharyya distance between consecutive HSV histograms."""
cap = cv2.VideoCapture(video_path)
cuts = []
prev_hist = None
idx = 0
while True:
ok, frame = cap.read()
if not ok:
break
if idx % sample_stride == 0:
hist = _frame_hist(frame)
if prev_hist is not None:
score = cv2.compareHist(prev_hist, hist, cv2.HISTCMP_BHATTACHARYYA)
if score > diff_thresh:
cuts.append(idx)
prev_hist = hist
idx += 1
cap.release()
return cuts
def build_clip_ranges(total_frames, fps, cuts, clip_min_sec=2.0, clip_max_sec=6.0):
"""Turns scene-cut frame indices into (start, end) frame ranges honoring
clip_min/max seconds: too-short segments are merged forward, segments
longer than clip_max are chopped into roughly-equal sub-segments."""
min_frames = max(1, int(clip_min_sec * fps))
max_frames = max(min_frames, int(clip_max_sec * fps))
boundaries = [0] + sorted(set(c for c in cuts if 0 < c < total_frames)) + [total_frames]
raw_ranges = []
start = boundaries[0]
for b in boundaries[1:]:
if b - start >= min_frames:
raw_ranges.append((start, b))
start = b
if start < total_frames:
if raw_ranges and (total_frames - start) < min_frames:
s, _ = raw_ranges[-1]
raw_ranges[-1] = (s, total_frames)
else:
raw_ranges.append((start, total_frames))
ranges = []
for s, e in raw_ranges:
length = e - s
if length <= max_frames:
ranges.append((s, e))
continue
n_chunks = math.ceil(length / max_frames)
chunk_len = length // n_chunks
for i in range(n_chunks):
cs = s + i * chunk_len
ce = e if i == n_chunks - 1 else cs + chunk_len
ranges.append((cs, ce))
return [(s, e) for s, e in ranges if e - s >= max(1, min_frames // 2)]
def _sample_indices(start, end, n=3):
if end - start <= n:
return list(range(start, end))
step = (end - start) / (n + 1)
return [int(start + step * (i + 1)) for i in range(n)]
def _median_bbox(bboxes):
return (
statistics.median(b[0] for b in bboxes),
statistics.median(b[1] for b in bboxes),
statistics.median(b[2] for b in bboxes),
statistics.median(b[3] for b in bboxes),
)
def _motion_tag(frames_gray):
if len(frames_gray) < 2:
return "static, minimal motion"
mags = []
for a, b in zip(frames_gray, frames_gray[1:]):
flow = cv2.calcOpticalFlowFarneback(a, b, None, 0.5, 3, 15, 3, 5, 1.2, 0)
mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
mags.append(float(np.mean(mag)))
avg = sum(mags) / len(mags)
if avg < 0.5:
return "static, minimal motion"
if avg < 2.0:
return "subtle motion"
if avg < 5.0:
return "moderate motion"
return "fast, dynamic motion"
def extract_clip(video_path, start, end, target_fps, max_tier="1080p", pad_overrides=None,
body_region="auto", body_region_frac=0.45):
"""Reads frames [start, end) from video_path, computes one stable subject
crop for the whole clip (median bbox over sampled frames), resamples to
target_fps, trims to a 4n+1 frame count, and returns
(frames_bgr, (target_w, target_h), representative_pil_image, motion_tag),
or None if no frames could be read."""
cap = cv2.VideoCapture(video_path)
src_fps = cap.get(cv2.CAP_PROP_FPS) or target_fps
img_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
img_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
bboxes, kinds = [], []
for idx in _sample_indices(start, end, n=3):
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
ok, frame = cap.read()
if not ok:
continue
bbox, kind = detect_subject_bbox(frame)
bboxes.append(bbox)
kinds.append(kind)
if not bboxes:
cap.release()
return None
bbox = _median_bbox(bboxes)
kind = max(set(kinds), key=kinds.count)
if kind == "person" and body_region != "auto":
bbox = subdivide_bbox(bbox, body_region, body_region_frac)
kind = f"person_{body_region}"
crop_box, (tw, th), _bucket_name, _tier = crop_resize.compute_crop(
img_w, img_h, bbox, kind, pad_overrides, max_tier
)
step = src_fps / target_fps
raw_indices = []
f = float(start)
while f < end:
raw_indices.append(int(round(f)))
f += step
if len(raw_indices) >= 5:
n = ((len(raw_indices) - 1) // 4) * 4 + 1
raw_indices = raw_indices[:n]
frames_out, frames_gray = [], []
for idx in raw_indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
ok, frame = cap.read()
if not ok:
continue
cropped = crop_resize.crop_resize_np(frame, crop_box, tw, th)
frames_out.append(cropped)
if len(frames_gray) < 6:
frames_gray.append(cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY))
cap.release()
if not frames_out:
return None
motion_tag = _motion_tag(frames_gray)
mid = frames_out[len(frames_out) // 2]
representative = Image.fromarray(cv2.cvtColor(mid, cv2.COLOR_BGR2RGB))
return frames_out, (tw, th), representative, motion_tag
def write_clip_mp4(frames_bgr, out_path, fps):
with imageio.get_writer(str(out_path), fps=fps, codec="libx264", quality=8) as writer:
for frame in frames_bgr:
writer.append_data(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))