3. ROV视频抖动与运动模糊的判读可用性筛查
为什么先做质量筛查,再谈缺陷判读
ROV 在流水中的姿态扰动、推进器振动、云台补偿滞后,都会让视频出现两类典型退化:抖动(帧间整体位移大、画面来回跳)和运动模糊(单帧内沿运动方向拖影,边缘被抹平)。这两类退化对判读的伤害不同:抖动破坏的是帧间可比性(同一缺陷在相邻帧位置乱跳,难以确认形态与尺寸),运动模糊破坏的是单帧可辨性(裂纹、点蚀边缘糊成一片,容易被误判为附着物或漏判)。因此判读前必须建立一道质量门槛:把视频切成短段,逐段量化抖动与模糊,超阈值的帧段直接标记为“不可判读”,并区分是重判读(换帧、换视角仍可用)还是补拍(本段信息已丢失)。
筛查对象与分段口径
以例 2 建立的“帧号—时间戳—里程”对应表为基础,按固定里程窗分段,而不是按固定时长分段——因为判读关注的是管体单位长度上的信息量。建议窗长取 0.5 m(对应巡检速度 0.3 m/s 时约 1.7 s、约 50 帧@30fps),窗间不重叠,便于逐段给出结论并回填到里程表。每段计算两个核心指标:
- 抖动幅度 J:段内相邻帧的全局平移量(像素),取中位数与 95 分位。中位数反映常态抖动,95 分位捕捉突发冲击。
- 模糊程度 B:段内各帧的梯度能量(如 Tenengrad 响应)或拉普拉斯方差,取中位数;数值越低越模糊。为消除光照影响,用同段内相对值而非绝对值。
阈值必须结合相机分辨率与巡检速度标定,不能照搬。下面给出一组教学示例阈值(假设 1920×1080、巡检速度 0.3 m/s、云台增稳开启):J_med ≤ 6 px 且 J_p95 ≤ 18 px 视为抖动可接受;B_med ≥ 该相机清晰基准的 60% 视为模糊可接受。两者同时满足才判“可判读”。
逐段筛查的实现
下面代码对一段已抽帧的视频目录逐窗计算 J 与 B,输出每段结论与建议。全局平移用相位相关(FFT)估计,避免特征点匹配在模糊帧上失效;模糊用拉普拉斯方差。代码自洽可运行,输入为按帧号命名的灰度图序列。
import os, glob
import numpy as np
import cv2
FRAME_DIR = "frames" # 例2抽出的帧,命名如 000123.png
WIN_FRAMES = 50 # 0.5 m 窗 ≈ 50 帧
J_MED_TH, J_P95_TH = 6.0, 18.0
B_RATIO_TH = 0.60 # 相对清晰基准的比例
def load_gray(p):
return cv2.imread(p, cv2.IMREAD_GRAYSCALE).astype(np.float32)
def shift_px(a, b):
# 相位相关估计 b 相对 a 的全局平移(像素)
(dx, dy), _ = cv2.phaseCorrelate(a, b)
return float(np.hypot(dx, dy))
def blur_score(img):
return float(cv2.Laplacian(img, cv2.CV_32F).var())
files = sorted(glob.glob(os.path.join(FRAME_DIR, "*.png")))
frames = [load_gray(f) for f in files]
# 清晰基准:取全片 B 的 90 分位,代表该相机在良好条件下的清晰度
all_b = np.array([blur_score(f) for f in frames])
b_ref = float(np.percentile(all_b, 90))
rows = []
for s in range(0, len(frames) - WIN_FRAMES + 1, WIN_FRAMES):
win = frames[s:s + WIN_FRAMES]
js = [shift_px(win[i], win[i + 1]) for i in range(len(win) - 1)]
bs = [blur_score(f) for f in win]
j_med, j_p95 = float(np.median(js)), float(np.percentile(js, 95))
b_med = float(np.median(bs))
b_ratio = b_med / b_ref if b_ref > 0 else 0.0
j_ok = (j_med <= J_MED_TH) and (j_p95 <= J_P95_TH)
b_ok = b_ratio >= B_RATIO_TH
if j_ok and b_ok:
verdict, action = "可判读", "直接进入缺陷判读"
elif not j_ok and b_ok:
verdict, action = "抖动超限", "重判读:选段内最稳帧或相邻窗复核"
elif j_ok and not b_ok:
verdict, action = "模糊超限", "重判读:换清晰帧;仍不足则补拍"
else:
verdict, action = "双重超限", "补拍:本段信息已不可恢复"
rows.append((s, s + WIN_FRAMES - 1, round(j_med, 1), round(j_p95, 1),
round(b_ratio, 2), verdict, action))
print(f"清晰基准 b_ref = {b_ref:.1f}")
print("窗起\t窗止\tJ_med\tJ_p95\tB比\t结论\t建议")
for r in rows:
print("\t".join(map(str, r)))
运行后得到逐窗表格,把“窗起/窗止”帧号经例 2 的对应表换算成里程区间,即可回填到判读任务清单。注意 b_ref 用全片 90 分位而非固定常数,是为了让阈值随本段视频的实际成像条件自适应;若整段视频普遍模糊,b_ref 会被拉低,此时应改用相机出厂标定的清晰基准,避免“矮子里拔将军”。
抖动与模糊的判读可用性关系
下图把两个指标组合成四象限,直接对应四种处置结论,并标出各象限的典型成因,便于现场快速归因。
结果判读与处置边界
四象限给出的是处置方向,不是最终结论。落到具体帧段时还要把握三条边界:
- 抖动超限不等于整段作废:若段内存在连续 5 帧以上 J 低于阈值,可截取该子窗重判读,并在记录中注明“子窗替代”,避免整段补拍浪费航次。
- 模糊超限要看缺陷尺度:B 比略低于 0.60 时,若目标缺陷为大口径凹陷(特征尺度远大于拖影长度),仍可能可判读;若为发丝裂纹,则必须补拍。因此模糊阈值应随判读目标分级,而非一刀切。
- 双重超限优先补拍:两指标同时越界时,帧间可比性与单帧可辨性都已丧失,重判读的收益极低,应直接进入补拍清单并标注里程区间与成因,供下一次巡检规划参考。