Download custom_nodes/dolphin_nodes/dataset_builder/scene_split.py from bjooo/tutorials: direct link, hf CLI and curl.
- Browser
- Download file 6.47 kB
-
https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/dataset_builder/scene_split.py
- Command line
-
hf download hf://bjooo/tutorials/custom_nodes/dolphin_nodes/dataset_builder/scene_split.py
-
curl -L -o scene_split.py https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/dataset_builder/scene_split.py
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)) | |