когда мы считаем здесь MAE для всего видеоряда пос...
创建于:2025年9月17日
创建于:2025年9月17日
когда мы считаем здесь MAE для всего видеоряда после синхронизации, между чем усредняется MAE? нужно чтобы усреднялось между всеми ключевыми точками на всех кадразfrom pathlib import Path
import cv2
import numpy as np
import pandas as pd
from tqdm import tqdm
from detectors.sift_detector import SIFTDetector
from detectors.orb_detector import ORBDetector
from detectors.akaze_detector import AKAZEDetector
from video_sync.metrics import compute_matches_and_inliers
from video_sync.plot_sync import (
plot_distance_histogram,
plot_inliers_heatmap_normalized
)
def get_safe_frame_idxs(video1: Path, video2: Path,
offset_min: int, offset_max: int,
frame_skip: int) -> list[int]:
"""
Возвращает список индексов кадров, которые есть в обоих видео
при любом смещении в [offset_min, offset_max].
"""
cap1 = cv2.VideoCapture(str(video1))
cap2 = cv2.VideoCapture(str(video2))
total1 = int(cap1.get(cv2.CAP_PROP_FRAME_COUNT))
total2 = int(cap2.get(cv2.CAP_PROP_FRAME_COUNT))
cap1.release()
cap2.release()
textstart = abs(offset_min) end = min(total1, total2) - abs(offset_max) return list(range(start, end, frame_skip))
def compute_metrics_for_offset(video1: Path, video2: Path,
offset: int, frame_idxs: list[int],
ransac_thresh: float,
det) -> tuple[float,float,list[float]]:
"""
Для заданного offset и заранее заданного набора frame_idxs
вычисляет MAE, MSE и список всех inlier-расстояний.
"""
cap1 = cv2.VideoCapture(str(video1))
cap2 = cv2.VideoCapture(str(video2))
fps1 = cap1.get(cv2.CAP_PROP_FPS)
fps2 = cap2.get(cv2.CAP_PROP_FPS)
textmae_list: list[float] = [] mse_list: list[float] = [] all_dists: list[float] = [] for frame_idx in tqdm(frame_idxs, desc=f"[{det.__class__.__name__}] offset={offset}", leave=False): cap1.set(cv2.CAP_PROP_POS_FRAMES, frame_idx) ret1, f1 = cap1.read() if not ret1: continue t2 = frame_idx/fps1 + offset/fps1 idx2 = int(round(t2 * fps2)) cap2.set(cv2.CAP_PROP_POS_FRAMES, idx2) ret2, f2 = cap2.read() if not ret2: continue g1 = cv2.cvtColor(f1, cv2.COLOR_BGR2GRAY) g2 = cv2.cvtColor(f2, cv2.COLOR_BGR2GRAY) # quick inlier check _, inliers = compute_matches_and_inliers(det, g1, g2, ransac_thresh) if inliers < 4: continue # compute distances for inliers kp1, des1 = det.detect_and_compute(g1) kp2, des2 = det.detect_and_compute(g2) matches = det.match_features(des1, des2) pts1 = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1,1,2) pts2 = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1,1,2) _, mask = cv2.findHomography(pts1, pts2, cv2.RANSAC, ransac_thresh) if mask is None: continue mask = mask.ravel().astype(bool) p1 = pts1[mask].reshape(-1,2) p2 = pts2[mask].reshape(-1,2) if p1.size == 0: continue d = np.linalg.norm(p1 - p2, axis=1) mae_list.append(d.mean()) mse_list.append((d**2).mean()) all_dists.extend(d.tolist()) cap1.release() cap2.release() overall_mae = float(np.nanmean(mae_list)) if mae_list else float('nan') overall_mse = float(np.nanmean(mse_list)) if mse_list else float('nan') return overall_mae, overall_mse, all_dists
OFFSET_MIN = -30
OFFSET_MAX = 30
def main():
video1 = Path(r"D:\Downloads\samples\samples\sony\C0430.MP4")
video2 = Path(r"D:\Downloads\samples\samples\huawei\VID_20250715_170534.mp4")
offsets_csv = Path("output/video_sync/offset_scores.csv")
stats_csv = Path("output/video_sync/video_sync_stats.csv")
out_dir = Path("output/video_sync/metrics")
frame_skip = 2
ransac_thresh = 5.0
textout_dir.mkdir(parents=True, exist_ok=True) df_off = pd.read_csv(offsets_csv) df_stats = pd.read_csv(stats_csv) # выбираем лучший offset по max inliers best_offsets = { alg: int(sub.sort_values("sum_inliers", ascending=False) .iloc[0]["offset"]) for alg, sub in df_off.groupby("alg") } print("Best offsets:", best_offsets) # формируем безопасные индексы кадров frame_idxs = get_safe_frame_idxs( video1, video2, OFFSET_MIN, OFFSET_MAX, frame_skip ) # детекторы detectors = { "SIFT": SIFTDetector(nfeatures=300), "ORB": ORBDetector(nfeatures=300), "AKAZE": AKAZEDetector(), } # подсчёт MAE/MSE по фиксированным кадрам records = [] distances_map = {} for alg, offset in best_offsets.items(): det = detectors[alg] mae, mse, dists = compute_metrics_for_offset( video1, video2, offset, frame_idxs, ransac_thresh, det ) records.append({ "alg": alg, "offset": offset, "MAE": mae, "MSE": mse }) distances_map[alg] = dists df_metrics = pd.DataFrame(records) df_metrics.to_csv(out_dir/"fixed_offset_metrics.csv", index=False, encoding="utf-8-sig") print("Saved fixed_offset_metrics.csv:\n", df_metrics) # строим heatmap inliers over time plot_inliers_heatmap_normalized(df_stats, out_dir) print("Saved heatmap → inliers_heatmap.png") # строим гистограммы расстояний for alg, dists in distances_map.items(): plot_distance_histogram(np.array(dists), out_dir, alg) print(f"Saved {alg}_distance_histogram.png") print("Готово. Результаты в", out_dir)
if name == "main":
main()
В данном коде MAE усредняется следующим образом:
Пошагово объясню, между чем именно усредняется MAE:
Сначала для каждого кадра, который есть в обоих видеорядах (с учетом лучшего вычисленного offset
), вычисляются координаты всех ключевых точек, совпавших по алгоритму (SIFT, ORB, AKAZE).
Затем для этих пар совпавших точек из кадров двух видео рассчитывается расстояние (евклидово расстояние между точками на кадре 1 и точками на кадре 2).
Затем рассчитывается среднее расстояние (MAE) для всех ключевых точек в этом конкретном кадре.
После этого происходит усреднение всех MAE, полученных на каждом кадре, чтобы получить общий итоговый MAE для всего видеоряда.
Если требуется строгое усреднение между всеми ключевыми точками на всех кадрах (без промежуточного усреднения по кадрам), код нужно изменить так:
Изменение:
python# Вместо этого: overall_mae = float(np.nanmean(mae_list)) if mae_list else float('nan') # Используйте это: overall_mae = float(np.nanmean(all_dists)) if all_dists else float('nan')
Тогда итоговое MAE будет точно отражать среднее расстояние всех ключевых точек за все кадры.
poetry run python -m scripts.metrics
Best offsets: {'AKAZE': 12, 'ORB': 4, 'SIFT': 6}
Saved fixed_offset_metrics.csv:
alg offset MAE MSE
0 AKAZE 12 NaN NaN
1 ORB 4 NaN NaN
2 SIFT 6 NaN NaN
Saved heatmap → inliers_heatmap.png
Saved AKAZE_distance_histogram.png
Saved ORB_distance_histogram.png
Saved SIFT_distance_histogram.png
Готово. Результаты в output\video_sync(frame_skip=5)\metrics
PS C:\Users\01\Documents\GitHub\temporal_alignment>
Причина того, что у тебя получился NaN в MAE и MSE:
Что именно происходит?
В коде есть проверка, когда недостаточно inliers (совпавших точек) на кадре (меньше 4), кадр пропускается:
python_, inliers = compute_matches_and_inliers(det, g1, g2, ransac_thresh) if inliers < 4: continue
Также есть дополнительная проверка:
pythonmask = mask.ravel().astype(bool) p1 = pts1[mask].reshape(-1,2) p2 = pts2[mask].reshape(-1,2) if p1.size == 0: continue
Если по всем кадрам не набирается ни одного валидного случая с количеством точек >4, то список расстояний остаётся пустым (mae_list
и mse_list
), что приводит к NaN
.
frame_skip
) с 5 до 1 или 2.pythonframe_skip = 1 # или 2
pythonransac_thresh = 10.0
{'AKAZE': 12, 'ORB': 4, 'SIFT': 6}
) слишком большой, кадры могут быть просто не похожи друг на друга.offset=0
или offset=1
, и проверить.pythonret1, f1 = cap1.read() ret2, f2 = cap2.read() if not (ret1 and ret2): print(f"Failed to read frames: {frame_idx}, {idx2}")
Измени в scripts.metrics
эти строки и перезапусти:
pythonframe_skip = 1 # <–– важно уменьшить ransac_thresh = 10.0 # <–– увеличить порог
Затем снова запусти:
bashpoetry run python -m scripts.metrics
После этого проверь, исчезли ли NaN.