diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..355164c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*/ diff --git a/cut_to_length.py b/cut_to_length.py index b337146..50f1221 100644 --- a/cut_to_length.py +++ b/cut_to_length.py @@ -1,16 +1,66 @@ #!/usr/bin/env python3 import subprocess -import json -import sys -import shutil from pathlib import Path +import cv2 +import numpy as np +from scipy import signal def run(cmd): - return subprocess.run(cmd, capture_output=True, text=True, check=True) + """Run a subprocess command and raise on error.""" + subprocess.run(cmd, check=True) +def get_video_signal(path): + cap = cv2.VideoCapture(path) + motion_signal = [] + prev_gray = None + + while True: + ret, frame = cap.read() + if not ret: + break + + # 1. Shrink the frame (makes it faster and removes noise) + small_frame = cv2.resize(frame, (160, 90)) + gray = cv2.cvtColor(small_frame, cv2.COLOR_BGR2GRAY) + + if prev_gray is not None: + # 2. Calculate the difference between this frame and the last + # This detects the "movement" or "cuts" + diff = cv2.absdiff(gray, prev_gray) + motion_signal.append(np.sum(diff)) + else: + motion_signal.append(0) + + prev_gray = gray + + cap.release() + return np.array(motion_signal) + +def find_offset(video_a, video_b): + # print("Extracting signals (this may take a moment)...") + sig_a = get_video_signal(video_a) + # print("sig_a finished") + sig_b = get_video_signal(video_b) + # print("sig_b finished") + + # Normalize signals (subtract mean, divide by std dev) to improve correlation + sig_a = (sig_a - np.mean(sig_a)) / (np.std(sig_a) + 1e-5) + sig_b = (sig_b - np.mean(sig_b)) / (np.std(sig_b) + 1e-5) + + # Perform Cross-Correlation + correlation = signal.correlate(sig_a, sig_b, mode='full') + lags = signal.correlation_lags(len(sig_a), len(sig_b), mode='full') + # plt.plot(lags) + + # The peak of the correlation is our offset in frames + best_lag = lags[np.argmax(correlation)] + + return best_lag def probe_video(path): + """Get video FPS, duration, codec, and stream start time.""" + import json cmd = [ "ffprobe", "-v", "error", @@ -19,11 +69,9 @@ def probe_video(path): "-show_format", path ] - - data = json.loads(run(cmd).stdout) + data = json.loads(subprocess.run(cmd, capture_output=True, text=True, check=True).stdout) duration = float(data["format"]["duration"]) - for s in data["streams"]: if s["codec_type"] == "video": num, den = s["r_frame_rate"].split("/") @@ -34,95 +82,67 @@ def probe_video(path): raise RuntimeError("No video stream found") -def vaapi_cut(inp, outp, start, end, codec="h264_vaapi"): - encoder = codec - - cmd = [ +def trim_audio(input_mkv, start_time, end_time, out_audio): + """Trim only the first (and only) audio track from input_mkv.""" + run([ "ffmpeg", "-y", - "-vaapi_device", "/dev/dri/renderD128", - - "-ss", str(start), - "-to", str(end), - - "-i", inp, - "-map", "0", - - "-vf", "format=nv12,hwupload", - - "-c:v", encoder, - "-qp", "20", - - "-c:a", "copy", - "-c:s", "copy", - "-c:d", "copy", - "-c:t", "copy", - - outp - ] - - subprocess.run(cmd, check=True) - -def extract_audio(output_mkv, out_audio): - # Extract the first (and only) audio track - run([ - "mkvextract", - "tracks", - output_mkv, - f"1:{out_audio}" # track IDs are usually 0 for video, 1 for audio + "-i", input_mkv, + "-ss", str(start_time), + "-to", str(end_time), + "-map", "0:a:0", # first audio track + "-c:a", "copy", # no re-encode + out_audio ]) -def mux_audio_into_copy(inp2, audio_file, output_dir): +def mux_audio_into_output(inp2, audio_file, output_dir): + """Mux trimmed audio into inp2 and place final file in output_dir.""" output_dir = Path(output_dir) output_dir.mkdir(exist_ok=True) - copied_video = output_dir / Path(inp2).name - shutil.copy2(inp2, copied_video) - - final_output = output_dir / "final_with_audio.mkv" + final_output = output_dir / f"{Path(inp2).stem}_ger.mkv" run([ "mkvmerge", "-o", str(final_output), - str(copied_video), + str(inp2), "--language", "0:deu", # assign German language + "--track-name", "0:German (2.0 aac)", str(audio_file) ]) return final_output def main(): - inp = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E001.mkv" - inp2 = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E01 (BD 1080p HEVC Opus) [Dual Audio].mkv" - - - outp = "./output.mkv" - n1 = 24 + for i in range(1, 12): + # --- Inputs --- + inp = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv" + inp2 = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv" + n1 = find_offset(inp, inp2) # first frame to keep + output_dir = "./output_dir" - fps, _, _, vstart = probe_video(inp) - _, duration, _, _ = probe_video(inp2) + # --- Probe video --- + fps, _, _, vstart = probe_video(inp) + _, duration2, _, _ = probe_video(inp2) - start = vstart + (n1 / fps) - end = duration + start + # --- Compute timestamps --- + start = vstart + (n1 / fps) + end = start + duration2 - if end <= start: - raise ValueError("n2 must be greater than n1") + if end <= start: + raise ValueError("n2 must be greater than n1") - print(f"fps: {fps}") - print(f"cutting from frame {n1} to {end*fps}") - print(f"time range: {start:.6f}s -> {end:.6f}s") + print(f"fps: {fps}") + print(f"Cutting audio from frame {n1} (timestamp {start:.6f}s) to timestamp {end:.6f}s") - print("Trying stream copy...") - vaapi_cut(inp, outp, start, end) + # --- Trim only audio --- + audio_file = Path(output_dir) / f"{Path(inp2).stem}_german_audio.aac" + trim_audio(inp, start, end, audio_file) - output_dir = "./output_dir" - audio_file = Path(output_dir) / "german_audio.aac" + # --- Mux trimmed audio --- + final_file = mux_audio_into_output(inp2, audio_file, output_dir) - extract_audio(outp, audio_file) - - final_file = mux_audio_into_copy(inp2, audio_file, output_dir) - - print(f"Final file created at: {final_file}") + print(f"Final file with trimmed audio created at: {final_file}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/extract_frame.py b/extract_frame.py new file mode 100644 index 0000000..d035e35 --- /dev/null +++ b/extract_frame.py @@ -0,0 +1,72 @@ +import cv2 +import numpy as np + +def create_comparison_collage(video_a_path, video_b_path, time_a, offset_b, radius=1, output_name="collage.jpg"): + caps = { + "A": cv2.VideoCapture(video_a_path), + "B": cv2.VideoCapture(video_b_path) + } + + results = {"A": [], "B": []} + + for label, cap in caps.items(): + if not cap.isOpened(): + print(f"Error: Could not open Video {label}") + return + + fps = cap.get(cv2.CAP_PROP_FPS) + # Determine base time for this specific video + base_time = time_a if label == "A" else (time_a + offset_b) + center_frame = int(round(base_time * fps)) + + # Extract range: from (center - radius) to (center + radius) + for i in range(-radius, radius + 1): + target_idx = center_frame + i + cap.set(cv2.CAP_PROP_POS_FRAMES, target_idx) + success, frame = cap.read() + + if success: + # Add overlay text (Timestamp and Frame Index) + current_time = target_idx / fps + text = f"Vid {label} | F: {target_idx} | {current_time:.4f}s" + + # Draw black background for text for readability + cv2.putText(frame, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0,0,0), 4, cv2.LINE_AA) + cv2.putText(frame, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255,255,255), 2, cv2.LINE_AA) + + results[label].append(frame) + else: + # Placeholder if frame is missing + blank = np.zeros((720, 1280, 3), dtype=np.uint8) + cv2.putText(blank, "FRAME MISSING", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2) + results[label].append(blank) + + cap.release() + + # Ensure all frames are the same size for the collage (using Video A's dimensions) + h, w, _ = results["A"][0].shape + + rows = [] + for i in range(len(results["A"])): + img_a = cv2.resize(results["A"][i], (w, h)) + img_b = cv2.resize(results["B"][i], (w, h)) + # Combine Left (A) and Right (B) + combined_row = np.hstack((img_a, img_b)) + rows.append(combined_row) + + # Combine all rows Top to Bottom + final_collage = np.vstack(rows) + + # Save and show + cv2.imwrite(output_name, final_collage) + print(f"Collage saved as {output_name}") + +# --- SETTINGS --- +video_a = "/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E02 (BD 1080p HEVC Opus) [Dual Audio].mkv" +video_b = "/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E002.mkv" + +timestamp_a = 60 * 20#1/(24000/1001) * 250 # Center point for Video A 75 +offset_for_b = 1/(24000/1001) * 24 +frames_around = 10 # 2 before, 2 after (5 frames total per video) + +create_comparison_collage(video_a, video_b, timestamp_a, offset_for_b, frames_around) \ No newline at end of file diff --git a/opencv_cross_correlation.py b/opencv_cross_correlation.py index 77cd574..52d93f2 100644 --- a/opencv_cross_correlation.py +++ b/opencv_cross_correlation.py @@ -2,6 +2,7 @@ import cv2 import numpy as np from scipy import signal import matplotlib.pyplot as plt +import sys def get_video_signal(path): cap = cv2.VideoCapture(path) @@ -52,17 +53,18 @@ def find_offset(video_a, video_b): return best_lag # Usage -# for i in range(1, 12): -# # print(f"{i:02}") -# video_a = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv" -# video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E{i:02} (BD 1080p HEVC Opus) [Dual Audio].mkv" +for i in range(1, 12): + # print(f"{i:02}") + video_a = f"/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv" + video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E0{i:02}.mkv" -# offset_frames = find_offset(video_a, video_b) -# print(f"{i}: Video B is offset by {offset_frames} frames relative to Video A.") + offset_frames = find_offset(video_a, video_b) + print(f"{i}: Video B is offset by {offset_frames} frames relative to Video A.") +# sys.exit() -# print(f"{i:02}") -video_a = f"/home/honney/projects/mkv/output.mkv" -video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E01 (BD 1080p HEVC Opus) [Dual Audio].mkv" +# video_a = f"/home/honney/projects/mkv/output_dir/AnoHana The Flower We Saw That Day S01E001_ger.mkv" +# video_b = f"/home/honney/projects/mkv/Anohana - The Flower We Saw That Day/S01/AnoHana The Flower We Saw That Day S01E001.mkv" -offset_frames = find_offset(video_a, video_b) -print(f"Video B is offset by {offset_frames} frames relative to Video A.") \ No newline at end of file +# offset_frames = find_offset(video_a, video_b) +# print(f"Video B is offset by {offset_frames} frames relative to Video A.") +# sys.exit() \ No newline at end of file diff --git a/temporal_correlation.py b/temporal_correlation.py index 60d2417..9b9af6f 100644 --- a/temporal_correlation.py +++ b/temporal_correlation.py @@ -1,104 +1,95 @@ import subprocess import numpy as np import matplotlib.pyplot as plt -import sys +from scipy.interpolate import interp1d # --- Setup --- -video_a = "/home/honney/projects/mkv/Saga of Tanya the Evil/Season 1/Saga Of Tanya The Evil - S01E01 - The Devil Of The Rhine.mkv" -video_b = "/home/honney/projects/mkv/Saga of Tanya the Evil (2017-2017) [imdbid-tt6455986]/Season 01/Saga of Tanya the Evil S01E001.mkv" +video_a = "/home/honney/projects/mkv/Anohana - The Flower We Saw That Day - S01 (BD 1080p HEVC Opus) [derpie]/[derpie] Anohana - The Flower We Saw That Day - S01E01 (BD 1080p HEVC Opus) [Dual Audio].mkv" +video_b = "/home/honney/projects/mkv/AnoHana The Flower We Saw That Day (2011-2011) [imdbid-tt1913273]/Season 01/AnoHana The Flower We Saw That Day S01E001.mkv" +FPS_NATIVE = 24000/1001 WIDTH = 160 HEIGHT = int((WIDTH/16)*9) -FPS = 24000/1001 -MAX_OFFSET_SECONDS = 2 # ±2 Sekunden Suche +MAX_OFFSET_SECONDS = 1 +# FAKTOR FÜR PRÄZISION: +# 2 = Doppelte FPS für die Rechnung (Sub-Frame) +# 10 = Zehnfache Präzision (reicht für Mikrosekunden-Sync) +UPSAMPLE_FACTOR = 10 +FPS_VIRTUAL = FPS_NATIVE * UPSAMPLE_FACTOR def extract_hist_signal(video): - """Extrahiert pro Frame einen Histogrammvektor unter Nutzung von VA-API (AMD GPU).""" - # Wir nutzen vaapi für Hardware-Dekodierung und Scaling proc = subprocess.Popen([ - "ffmpeg", - "-loglevel", "error", - "-hwaccel", "vaapi", # Nutze VA-API Hardware-Beschleunigung - "-hwaccel_device", "/dev/dri/renderD128", # Pfad zur RX 7900 XTX - "-hwaccel_output_format", "vaapi", - "-i", video, - "-vf", ( - f"fps={FPS}," # Framerate Anpassung - f"scale_vaapi=w={WIDTH}:h={HEIGHT}:format=gray8," # GPU-Scaling + Grayscale - "hwdownload,format=gray8" # Zurück in den RAM schieben - ), - "-f", "rawvideo", - "-" + "ffmpeg", "-loglevel", "error", "-i", video, + "-vf", f"scale={WIDTH}:{HEIGHT},format=gray", + "-f", "rawvideo", "-" ], stdout=subprocess.PIPE) frame_size = WIDTH * HEIGHT hist_signal = [] - - # Der restliche Loop bleibt gleich, da die Histogramm-Berechnung - # (np.histogram) weiterhin auf der CPU via NumPy erfolgt. while True: data = proc.stdout.read(frame_size) - if not data or len(data) < frame_size: - break + if not data or len(data) < frame_size: break frame = np.frombuffer(data, dtype=np.uint8) - hist, _ = np.histogram(frame, bins=32, range=(0, 255)) - hist_signal.append(hist / np.sum(hist)) - + hist, _ = np.histogram(frame, bins=64, range=(0, 255)) + hist_signal.append(hist / (np.sum(hist) + 1e-6)) proc.wait() return np.array(hist_signal) +print(f"Extracting signals (Native FPS: {FPS_NATIVE:.4f})...") +a_raw = extract_hist_signal(video_a) +b_raw = extract_hist_signal(video_b) -print("Extracting signal of video A") -a = extract_hist_signal(video_a) +# 1. In 1D-Signal umwandeln (Helligkeits-Schnitt) +sig_a_raw = np.mean(a_raw, axis=1) +sig_b_raw = np.mean(b_raw, axis=1) -print("Extracting signal of video B") -b = extract_hist_signal(video_b) +# 2. UPSAMPLING (Die "doppelte FPS" Logik) +# Wir erstellen eine glatte Kurve durch die Frames und tasten sie feiner ab +def upsample_signal(sig, factor): + x = np.arange(len(sig)) + f = interp1d(x, sig, kind='cubic') # Kubische Interpolation für weiche Übergänge + x_new = np.linspace(0, len(sig) - 1, len(sig) * factor) + return f(x_new) -print("Calculating cross correlation") +print(f"Upsampling signals by factor {UPSAMPLE_FACTOR}...") +sig_a = upsample_signal(sig_a_raw, UPSAMPLE_FACTOR) +sig_b = upsample_signal(sig_b_raw, UPSAMPLE_FACTOR) -max_lag_frames = int(MAX_OFFSET_SECONDS * FPS) -n_a, n_bins = a.shape -n_b, _ = b.shape +# 3. Zentrieren für bessere Korrelation +sig_a -= np.mean(sig_a) +sig_b -= np.mean(sig_b) -lags = np.arange(-max_lag_frames, max_lag_frames + 1) -corr = [] +print("Calculating high-precision cross correlation...") +corr = np.correlate(sig_a, sig_b, mode='full') +lags = np.arange(-len(sig_b) + 1, len(sig_a)) -for lag in lags: - if lag < 0: - # Offset negativ → Video B vor Video A - sub_a = a[:lag] # bis lag Frames von Video A - sub_b = b[-lag:] # von -lag bis Ende von Video B - else: - sub_a = a[lag:] # ab lag bis Ende von Video A - sub_b = b[:n_b - lag] # bis n_b - lag von Video B +# Suchbereich einschränken (jetzt basierend auf virtueller FPS) +max_lag_v = int(MAX_OFFSET_SECONDS * FPS_VIRTUAL) +search_range = (lags >= -max_lag_v) & (lags <= max_lag_v) - # Frames angleichen - min_len = min(len(sub_a), len(sub_b)) - if min_len == 0: - corr.append(0) - continue - sub_a = sub_a[:min_len] - sub_b = sub_b[:min_len] +filtered_corr = corr[search_range] +filtered_lags = lags[search_range] - corr.append(np.sum(sub_a * sub_b)) +best_index = np.argmax(filtered_corr) +best_lag_v = filtered_lags[best_index] -corr = np.array(corr) -best_index = np.argmax(corr) -best_lag = lags[best_index] +# Der Offset berechnet sich nun aus dem virtuellen Lag und der virtuellen FPS +best_offset_seconds = best_lag_v / FPS_VIRTUAL -print("Best offset frames:", best_lag) -print("Best offset seconds:", best_lag / FPS) - -sys.exit() +print("-" * 30) +print(f"Best virtual lag: {best_lag_v} units") +print(f"Resulting frames: {best_lag_v / UPSAMPLE_FACTOR:.3f}") +print(f"Best offset seconds: {best_offset_seconds:.12f}") +print("-" * 30) # Plot -plt.figure(figsize=(10, 5)) -plt.plot(lags / FPS, corr, marker='o') -plt.axvline(best_lag / FPS, color='r', linestyle='--', label=f"Best offset = {best_lag / FPS:.3f}s") +plt.figure(figsize=(12, 5)) +plt.plot(filtered_lags / FPS_VIRTUAL, filtered_corr) +plt.axvline(best_offset_seconds, color='r', linestyle='--', label=f"Peak: {best_offset_seconds:.6f}s") +plt.title(f"High-Precision Sync (Upsampled x{UPSAMPLE_FACTOR})") plt.xlabel("Offset (seconds)") -plt.ylabel("Cross correlation") -plt.title("Video cross correlation (Histogram-based)") +plt.ylabel("Correlation") plt.legend() plt.grid(True) plt.show() \ No newline at end of file