ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB十一种帧间预测与运动估计统一评测方案

MATLAB十一种帧间预测与运动估计统一评测方案

简介:这份凸优化课程大作业资料围绕MV子视角的帧间预测展开,面向正在学习运动估计、光流与单应性变换的高校学生及课程实践者,提供可直接运行的十一种方法对比方案。内容将穷尽块匹配、三步搜索、分层块匹配三类运动估计,与光流估计、单应性矩阵方案进行交叉组合,便于读者复现实验、比较精度与耗时,并理解不同策略在帧间预测中的适用条件。压缩包共300个文件,约214.99MB,含216张png结果图、60个m脚本、10个txt说明、8个mat数据文件及少量py、asv文件,覆盖代码、数据与实验输出,代码含主流程与辅助函数,配合结果图便于检查。已有301人学习,适合用作课程大作业参考、算法对照实验和结果分析模板;受上传体积限制,随包数据集仅保留几个序列,但目录组织清晰,仍可据此搭建完整的运动估计与光流实验流程。

1. 帧间预测的十一种解法,值不值得跑一遍

第一次做帧间预测,多数人会把时间全压在光流法上,结果预测帧的 PSNR 还不如一个块匹配的粗糙结果,这事在凸优化课的答辩现场挺常见。这套用 MATLAB 代码搭起来的方案,把运动估计拆成十一种可对比的实现:块匹配三种、光流法四种、全局参数模型三种(含单应性矩阵)、网格模型一种,全部塞进同一条评测流水线。它面向的是要交凸优化大作业、图像处理课设或毕设的人——你需要的不是又一个调包 demo,而是一份能解释每种方法能量函数怎么写、参数怎么定、什么时候会崩的完整方案。

2. 总体架构:十一方法共用一条评测流水线

十一种方法最难的地方在于每种方法的输出格式、参数名、耗时量级都不一样。如果每种写一个独立脚本,最后想拼一张对比表,光是把变量名对齐就要花掉一晚上。通常的做法是先把接口固定下来,再往里填方法,这样新增一种方法只需要写一个文件、在注册表里加一行。

2.1 目录约定与数据流

整个工程按职责分四层:数据层放两帧或多帧序列,方法层放十一个predict_*.m,指标层放 PSNR、SSIM 和残差统计,结果层统一写入results/。数据流是单向的:读入参考帧和目标帧 → 转灰度、归一化到 [0,1] 的 double → 由注册表分派到具体方法 → 输出预测帧、运动矢量场、附加信息 → 指标层计算 → 结果写入文件。

project/ data/ # 序列帧,命名 frame_000.png 起 methods/ # predict_fs.m / predict_tss.m / predict_hs.m ... metrics/ # psnr_metric.m / ssim_metric.m / residual_stats.m utils/ # 梯度、双线性插值、坐标网格 results/ # 每种方法一个子目录,内含预测图与 log.mat run_all.m # 唯一入口,遍历注册表

数据流里有一个容易被忽略的约定:所有方法统一使用「目标帧 I0 由参考帧 I1 预测」这个方向,也就是运动矢量描述的是 I0 中像素在 I1 中的位置。方向一旦在某个方法里写反,后面所有指标都会失真,而且这种错误不会报错,只会让 PSNR 掉几个 dB,很难定位。我一般会在utils/里放一个check_direction.m,用已知的人工平移量做一次自检。

run_all.m只做三件事:读配置、循环注册表、汇总结果。它不关心任何方法内部细节,这样换数据集时不需要改方法代码。

2.2 统一函数签名与方法注册表

统一的函数签名是整个架构能够成立的基础。所有方法必须接受同样的三个输入,返回同样的三个输出:

function [pred, mv, info] = predict_xxx(I0, I1, cfg) % 输入: % I0 - 目标帧,double,范围 [0,1],尺寸 H×W % I1 - 参考帧,同尺寸同类型 % cfg - 结构体,含 block/search/alpha/iter/lambda 等公共字段 % 输出: % pred - 预测帧,H×W double,与 I0 同尺寸 % mv - 运动矢量场,H×W×2,第三维依次为垂直、水平位移 % info - 结构体,至少含 time 与 params 两个字段

注册表负责把方法名映射到函数句柄和默认参数,一张表就能看清十一种方法的全貌:

function reg = method_registry() % 每行一个方法:名称、函数句柄、默认参数、是否属于凸模型 reg = { 'FS', @predict_fs, struct('block',16,'search',16), false 'TSS', @predict_tss, struct('block',16,'search',16), false 'DS', @predict_ds, struct('block',16,'search',16), false 'LK', @predict_lk, struct('levels',3,'win',15), false 'HS', @predict_hs, struct('alpha',1.0,'iter',200), true 'TVL1', @predict_tvl1, struct('lambda',0.35,'iter',300), true 'CLG', @predict_clg, struct('win',9,'iter',20), true 'TRANS',@predict_trans,struct('thresh',2.0), true 'AFF', @predict_aff, struct('thresh',2.0), true 'HOMO', @predict_homo, struct('thresh',2.0,'iter',500), true 'GRID', @predict_grid, struct('grid',[4 4],'blend',8), true }; end

这张表就是答辩时最有用的一张图。评审老师问「你这个作业到底做了什么」,你直接指着表说:三种块匹配是组合搜索,四种光流里 HS、TV-L1、CLG 是变分凸问题,三种全局模型是参数化最小二乘,网格模型是把单应性矩阵做成分块加权的凸组合。凸性的标注不是装饰,它决定了你能不能说清楚求解器为什么收敛——非凸的那几种只能给局部最优,这一点在报告里必须写明白,硬说全搜索块匹配是凸问题,现场很容易被追问到答不上来。

公共参数集中在cfg里,方法私有参数用cfg.method子结构传,避免字段名冲突。

2.3 评价指标与结果日志

指标层至少要给四个量:PSNR、SSIM、残差的一范数均值、单帧耗时。PSNR 是最直观的,但只靠它会把「模糊但平滑」的预测判成好结果,所以 SSIM 用来补结构相似性,残差一范数用来观察误差分布是不是集中在运动边界。

function m = eval_prediction(I0, pred, mv, elapsed, border) % border:评价时裁掉的边界像素数,必须所有方法一致 I0c = I0(1+border:end-border, 1+border:end-border); Pc = pred(1+border:end-border, 1+border:end-border); mse = mean((I0c(:) - Pc(:)).^2); m.psnr = 10*log10(1/mse); % 峰值按 1.0 计算 m.ssim = ssim(Pc, I0c); % 需要 Image Processing Toolbox r = abs(I0c - Pc); m.res_l1 = mean(r(:)); m.res_entropy = entropy(r); % 残差集中度 m.time = elapsed; m.mv_energy = mean(sqrt(mv(:,:,1).^2 + mv(:,:,2).^2)); end

border这个参数看着不起眼,实际影响很大。块匹配在图像边缘找不到完整块,光流在边界处的梯度是零填充,网格模型在块重叠区要融合——这些都会让边缘几个像素的误差明显偏高。统一裁掉search半径大小的边界,是对十一种方法最公平的口径。如果某个方法的边界处理特别粗糙,裁边会掩盖它的缺点,所以在报告里要同时给出裁边前后的两组数,说明差异来源。

结果日志用save(fullfile('results', name, 'log.mat'), 'm', 'cfg')落盘,run_all.m最后把所有log.mat读回来拼成一张总表。这张表就是验收和答辩的核心材料。

3. 运动估计:块匹配、光流法与变分凸松弛

这一章是方案的主体。十一种方法里有七种属于运动估计,三种块匹配负责「快而糙」的基线,四种光流法负责「密而准」,它们在同一个数据集上的差距,正好能说明凸优化里数据项和正则项各自在起什么作用。

3.1 块匹配三件套:全搜索、三步与菱形

全搜索是精度的上界,也是耗时的下界。它的逻辑非常简单:把目标帧切成 16×16 的块,每块在参考帧的 ±16 邻域里滑一遍,取 SAD 最小的位移作为该块的运动矢量。

function [pred, mv, info] = predict_fs(I0, I1, cfg) t0 = tic; B = cfg.method.block; R = cfg.method.search; [H, W] = size(I0); mv = zeros(H, W, 2); pred = zeros(H, W); for y = 1:B:H for x = 1:B:W y2 = min(y+B-1, H); x2 = min(x+B-1, W); tmpl = I0(y:y2, x:x2); best = inf; dy0 = 0; dx0 = 0; for dy = -R:R for dx = -R:R yy = y+dy; xx = x+dx; if yy < 1 || xx < 1 || yy+B-1 > H || xx+B-1 > W, continue; end cand = I1(yy:yy+B-1, xx:xx+B-1); d = sum(abs(cand(:) - tmpl(:))); % SAD 代价 if d < best, best = d; dy0 = dy; dx0 = dx; end end end mv(y:y2, x:x2, 1) = dy0; mv(y:y2, x:x2, 2) = dx0; pred(y:y2, x:x2) = I1(y+dy0:y2+dy0, x+dx0:x2+dx0); end end info.time = toc(t0); info.params = cfg.method; end

块大小和搜索半径是两个关键参数。块取 8 时运动边界贴合得好,但平坦区容易出现多个等价匹配;块取 16 时矢量场平滑,代价是物体边缘被「糊」成整块。搜索半径取 16 能覆盖大部分平移量,超过 32 后耗时按平方增长,收益很低,除非序列里有快速运动。

三步搜索和菱形搜索是同一思路的加速版:先用大步长粗搜,再逐步缩小步长。三步搜索从 ±R/2 开始,每轮步长减半,共 log2(R) 轮,计算量从 (2R+1)² 降到 9·log2(R)。菱形搜索把搜索点排成菱形,利用了运动矢量分布集中的先验,在慢速运动序列上几乎能追平全搜索。这两种方法的代码结构和全搜索一致,只是把内层双重循环换成按步长生成候选点集,cfg.method.search的含义从「半径」变成「初始步长」,报告里要写清楚,否则复现的人会按全搜索的语义去读参数。

3.2 光流法两族:金字塔 LK 与 Horn-Schunck

光流法求的是稠密场,每个像素一个位移,这就从「块级搜索」跳到了「能量最小化」。金字塔 LK 的思路是在每层金字塔上解一个加权最小二乘,把大位移分解到粗层,细层只负责修正残差:

function [uv, info] = lk_pyramid(I0, I1, levels, win) % 由粗到细逐层求解,初值来自上一层上采样 uv = zeros([size(I0) 2]); for lv = levels:-1:1 s = 2^(lv-1); I0l = imresize(I0, 1/s); I1l = imresize(I1, 1/s); uv_l = imresize(uv, [size(I0l,1) size(I0l,2)]); for k = 1:5 % 每层 5 次迭代 Iw = warp_by_flow(I1l, uv_l); % 用当前流场反向映射 [Ix, Iy] = gradient(Iw); It = Iw - I0l; [Ix2,Ixy,Iy2,Ixt,Iyt] = local_sums(Ix,Iy,It,win); detA = Ix2.*Iy2 - Ixy.^2; detA(detA < 1e-6) = 1e-6; % 平坦区保护 du = -( Iy2.*Ixt - Ixy.*Iyt) ./ detA; dv = -(-Ixy.*Ixt + Ix2.*Iyt) ./ detA; uv_l(:,:,1) = uv_l(:,:,1) + dv; uv_l(:,:,2) = uv_l(:,:,2) + du; end uv = imresize(uv_l, [size(I0,1) size(I0,2)]); end end

窗口win取 15 或 21,太小会在纹理弱的地方解出病态矩阵,太大会把运动边界抹平。detA的下限保护是必须的,天空、白墙这类区域梯度接近零,不设下限会直接出现 Inf。金字塔层数取 3 能覆盖约 8 像素的位移,再大就要加层,但每加一层,细层的初值误差也会放大。

Horn-Schunck 是另一条路:不假设局部恒定,而是给全局一个光滑性约束,把光流写成能量泛函。

function [u, v] = hs_flow(I0, I1, alpha, iters) [Ix, Iy] = gradient(I0); It = I1 - I0; K = [1 2 1; 2 0 2; 1 2 1] / 12; % 拉普拉斯核,中心为 0 u = zeros(size(I0)); v = u; for k = 1:iters ubar = conv2(u, K, 'same'); % 邻域均值作为光滑项近似 vbar = conv2(v, K, 'same'); den = alpha^2 + Ix.^2 + Iy.^2; u = ubar - Ix .* (Ix.*ubar + Iy.*vbar + It) ./ den; v = vbar - Iy .* (Ix.*ubar + Iy.*vbar + It) ./ den; end end

alpha是数据项和光滑项的权重比。取 0.5 时流场细节多但噪声大,取 2 以上整个场会过度平滑,物体边界被抹成斜坡。这个参数没有通用最优值,取决于序列的纹理密度,报告里要给一张alpha从 0.1 扫到 5 的 PSNR 曲线,说明你选的依据。HS 的能量泛函是严格凸的,Gauss-Seidel 迭代收敛性可以证明,这正是它和块匹配的本质区别——前者有唯一解,后者只有「搜到的最小值」。

3.3 TV-L1 与一阶原始-对偶求解

TV-L1 把 HS 的平方惩罚换成 L1,好处是运动边界不会被过度平滑,坏处是能量不可微,普通梯度下降直接失效。标准做法是写成鞍点问题,用 Chambolle-Pock 的原始-对偶迭代:

$$ \min_{u,v}\ \lambda \int |\nabla I \cdot (u,v) + I_t| + \int |\nabla u| + |\nabla v| $$

function [u, v] = tvl1_core(I0, I1, lambda, tau, sigma, nIter) [Ix, Iy] = gradient(I0); It = I1 - I0; [H, W] = size(I0); u = zeros(H,W); v = u; ubar = u; vbar = v; p = zeros(H,W,2); q = zeros(H,W,4); % p: 数据项对偶, q: 正则项对偶 for k = 1:nIter % 对偶上升:数据项次梯度 r = Ix.*ubar + Iy.*vbar + It; p(:,:,1) = bsxfun(@rdivide, p(:,:,1) + sigma*Ix.*r, 1 + sigma*abs(r)); p(:,:,2) = bsxfun(@rdivide, p(:,:,2) + sigma*Iy.*r, 1 + sigma*abs(r)); % 对偶上升:正则项梯度,逐点投影到单位球 [qx, qy] = grad2(ubar, vbar); q = proj_unit_ball(q + sigma*[qx qy]); % 原始下降:对偶散度回代 [dx, dy] = div2(p(:,:,1), p(:,:,2)); u_new = u + tau*dx; v_new = v + tau*dy; [rx, ry] = div2(q(:,:,1), q(:,:,2)); u_new = u_new + tau*lambda*rx; v_new = v_new + tau*lambda*ry; ubar = 2*u_new - u; vbar = 2*v_new - v; % 外推,加速收敛 u = u_new; v = v_new; end end

步长必须满足tau * sigma * ||K||² < 1,K是梯度算子,离散情况下的范数上界是 8,所以tau = sigma = 0.3是安全区。这一条是硬约束,违反了不会报错,只会让对偶变量在两三轮内涨到 1e6 然后全图 NaN。lambda控制数据项权重,取 0.3 到 0.5 之间比较稳,太小会让流场过度贴合噪声,太大会退化成接近全零的场。

CLG 是 HS 和 LK 的折中,用局部窗口做加权最小二乘,同时保留一阶正则,实现上是 HS 迭代里把ubar换成窗口加权均值。四种光流法放在一起,正好构成一条「局部恒定假设强度递减、正则强度递增」的谱系,这是报告里最值得写的一段分析。

4. 全局运动:单应性矩阵、仿射与网格模型

块匹配和光流都在求「每个点怎么动」,全局模型反过来:假设整幅图遵循一个参数变换,用少量参数解释大部分运动。摄像机平移、绕光轴旋转这类场景,全局模型的预测质量能明显超过稠密光流,而且参数量从百万级降到 8 个,这是它存在的意义。

4.1 特征点检测与 RANSAC 剔除误匹配

全局模型的第一步是找对应点。Harris 角点不依赖工具箱,实现也短:

function pts = harris_points(I, n, minDist) [Ix, Iy] = gradient(I); Ix2 = Ix.^2; Iy2 = Iy.^2; Ixy = Ix.*Iy; K = fspecial('gaussian', 5, 1.5); Sxx = conv2(Ix2,K,'same'); Syy = conv2(Iy2,K,'same'); Sxy = conv2(Ixy,K,'same'); k = 0.04; R = Sxx.*Syy - Sxy.^2 - k*(Sxx + Syy).^2; % Harris 响应 R = R / max(R(:)); cand = R > 0.01; [y, x] = find(cand); [~, idx] = sort(R(sub2ind(size(R), y, x)), 'descend'); pts = [x(idx) y(idx)]'; pts = greedy_nms(pts, minDist, n); % 贪心非极大值抑制 end

k取 0.04 是经验值,minDist取 8 能避免角点扎堆。取完点用简单的 SSD 匹配,得到一组含大量误匹配的对应点对——平面纹理重复、光照变化都会让匹配错位。RANSAC 就是用来把这些外点踢出去的:

function [H, inlier] = ransac_homography(p1, p2, thresh, nIter) N = size(p1, 2); bestCnt = 0; H = eye(3); inlier = false(1, N); for k = 1:nIter idx = randperm(N, 4); % 最少 4 点定一个单应 Hk = dlt_homography(p1(:,idx), p2(:,idx)); if isempty(Hk) || any(~isfinite(Hk(:))), continue; end q = Hk * [p1; ones(1,N)]; q = q(1:2,:) ./ q(3,:); d = sqrt(sum((q - p2).^2, 1)); in = d < thresh; if nnz(in) > bestCnt, bestCnt = nnz(in); H = Hk; inlier = in; end end end

阈值thresh的单位是像素,取 2 到 3 比较合适。取 1 会导致内点数太少、估计不稳;取 10 会把明显错位的点也放进来,最后解出的单应性矩阵在图像一角出现荒谬的缩放。迭代次数nIter取 500 在几百个点对的规模下足够,内点率低于 20% 时建议加到 2000。

4.2 DLT 求解与归一化精化

直接线性变换把x2 = H x1展开成两组线性方程,八个未知量(去掉尺度自由度)用 SVD 解最小二乘。裸写 DLT 数值上很脆弱,因为像素坐标在几百量级,构造出的矩阵条件数极差,标准做法是先做 Hartley 归一化:

function H = dlt_homography(p1, p2) % p1, p2: 2×N,对应点坐标 [T1, n1] = normalise2d(p1); [T2, n2] = normalise2d(p2); n = size(n1, 2); A = zeros(2*n, 9); for i = 1:n x = n1(1,i); y = n1(2,i); u = n2(1,i); v = n2(2,i); A(2*i-1,:) = [-x -y -1 0 0 0 u*x u*y u]; A(2*i, :) = [ 0 0 0 -x -y -1 v*x v*y v]; end [~, ~, V] = svd(A); Hn = reshape(V(:,end), 3, 3)'; H = T2 \ Hn * T1; % 反归一化 H = H / H(3,3); end function [T, pn] = normalise2d(p) c = mean(p, 2); d = mean(sqrt(sum((p - c).^2, 1))); s = sqrt(2) / max(d, eps); T = [s 0 -s*c(1); 0 s -s*c(2); 0 0 1]; pn = T * [p; ones(1, size(p,2))]; pn = pn(1:2,:) ./ pn(3,:); end

归一化的作用是让两幅图的点云质心落在原点、平均距离为 √2,矩阵条件数从 1e8 量级降到 1e2 量级。不做这一步,SVD 的解在数值上完全不可信,表现出来就是「大部分时候能跑,偶尔某帧解出一个翻转的矩阵」。反归一化时注意是T2 \ Hn * T1,顺序写反是另一个高频错误。

得到单应性矩阵后,用反向映射生成预测帧,也就是对目标帧每个像素,用H的逆找到它在参考帧里的位置:

function pred = warp_homography(I, H, sz) [Hh, Ww] = deal(sz(1), sz(2)); [xx, yy] = meshgrid(1:Ww, 1:Hh); p = H \ [xx(:)'; yy(:)'; ones(1, numel(xx))]; xs = p(1,:) ./ p(3,:); ys = p(2,:) ./ p(3,:); pred = interp2(I, xs, ys, 'linear', 0); % 越界填 0 pred = reshape(pred, Hh, Ww); end

interp2的参数顺序是 (图像, 列坐标, 行坐标),xs对应列、ys对应行,写反的后果是整幅图沿对角线错位,PSNR 会比不预测还低。

4.3 网格运动模型与凸组合融合

单应性矩阵假设整幅图共面,一旦场景有前后景分层,视差会让全局解在某一层上严重失配。网格模型的做法是把图切成 4×4 块,每块单独估一个单应性矩阵,再在重叠区做加权融合。融合权重取双线性核,权重和恒为 1,本质上是若干局部预测的凸组合——这一步是整套方案里「凸优化」三个字最直接的落点。

function pred = blend_grid(preds, weights) % preds: H×W×K,weights: H×W×K,每个像素上 sum(weights,3) == 1 pred = sum(preds .* weights, 3); end

权重图用fspecial('gaussian', blend*2+1, blend/2)生成,blend取 8 到 16。太窄会出现块边界接缝,太宽会让相邻块的运动互相污染。网格模型在复杂场景上通常比全局单应好 1 到 3 个 dB,代价是参数从 8 个涨到 8×16=128 个,且每块的特征点数量可能不够,需要在报告里说明外点率。

5. 避坑与排查:同一条流水线上最常翻车的五处

十一种方法跑同一套数据,出错的地方高度集中。下面五条是实际调试里最耗时间的,每条都按「看到什么 → 为什么 → 怎么改」写。

5.1 预测图整体偏移几个像素

现象是预测帧看起来「差不多」,但 PSNR 明显低于预期,把两张图叠起来看会发现固定的对角偏移。原因是坐标系语义不统一:MATLAB 是 1-based,图像坐标是 (行, 列) 即 (y, x),而很多论文公式写的是 (x, y),interp2的参数顺序又是先列后行。三套约定混在一起,位移的正负号和维度顺序迟早会错一次。

解决方式是在utils/里固定一个make_grid(H,W),全工程只用它生成坐标网格,并且在每个predict_*.m的头部注释里写清mv(:,:,1)是垂直位移、mv(:,:,2)是水平位移。再写一个自检函数:把 I1 整体平移 (dy, dx) 得到 I0,喂给方法,看输出的 mv 是否等于 (−dy, −dx)。符号对不对,一跑就知道。

5.2 光流的 PSNR 崩到不如不平移

现象是块匹配还能看,四种光流法全部输出接近全零的场,或者场的方向明显反了。原因有两个:一是光流定义了 I0 到 I1 的位移,但 warp 时用的是正向映射,应该用反向;二是块匹配返回整数像素,光流返回浮点像素,代码里如果对光流做了round或者int32转换,亚像素信息直接丢失,精度掉一个档次。

解决方式是统一规定:所有方法的mv都表示「目标帧像素在参考帧中的相对位置」,warp 一律走反向映射,插值一律用interp2(..., 'linear')。在run_all.m里加一条断言,检查每种方法输出的mv是否至少有一个非零分量,全零直接报警——这能拦住大部分方向写反的情况。

5.3 单应性矩阵解出镜像或极端缩放

现象是 RANSAC 报告内点数很高,但解出的 H 让图像翻转或者缩到只剩一角。原因是特征点匹配用的是原始像素坐标,没有归一化,DLT 的矩阵条件数爆炸,SVD 返回的极小奇异向量在数值上不可信;另一种情况是 RANSAC 阈值放得太宽,把一些方向错误但距离恰好落在阈值内的点当成了内点。

解决方式是 DLT 内部强制做 Hartley 归一化,并且在 RANSAC 之后加一道合理性检查:det(H(1:2,1:2))必须为正且落在 [0.2, 5] 区间,否则这一帧的单应判定为失败,退回平移模型。失败帧的比例要写进报告,这个数字比 PSNR 更能说明全局模型在什么场景下不可用。

5.4 变分求解器两三轮后全图 NaN

现象是 HS 或 TV-L1 跑到第 3 到 5 次迭代,u、v突然变成 NaN 或 1e6 量级。原因是步长违反了收敛条件。HS 的迭代式里alpha如果取到 0.01 这种量级,分母接近零,更新量会爆;TV-L1 的原始-对偶迭代必须满足tau*sigma*||K||² < 1,||K||²的离散上界是 8,取tau = sigma = 0.25才安全。

解决方式是给所有迭代加三重保护:分母加eps、每轮检查any(~isfinite(u(:)))并提前退出、把最终步长和迭代次数写进info。我一般还会在run_all.m里记录「发散帧数」,这个指标在答辩时很有说服力——它说明你知道方法的适用边界,而不是只会调包。

5.5 对比表里各方法的评价口径不一致

现象是总表拼出来以后,某个方法的 PSNR 高得离谱,检查发现它没有裁边、或者用了 [0,255] 的灰度范围而别人用 [0,1]。PSNR 的定义里峰值项对结果影响极大,范围差 255 倍,dB 值直接差 48。

解决方式是把评价参数写进配置文件,由run_all.m统一注入,方法代码不允许自己调eval_prediction。同时固定三件事:灰度范围 [0,1]、裁边宽度等于该方法的最大搜索半径、SSIM 用默认的 11×11 高斯窗。这三条写进 README,复现的人照做就能得到同一张表。

6. 验收与答辩:一张对比表加三条必答追问

验收的核心材料只有一张表。把十一种方法按「模型类型、凸性、关键参数、精度排序、耗时量级、失败场景」六列排开,精度用相对排序而不是绝对数值,避免不同数据集之间不可比。

编号方法模型类型凸性关键参数精度耗时典型失败场景
1FS块匹配否block=16, search=16中高大位移、重复纹理
2TSS块匹配否初始步长=8中低低落入局部极小
3DS块匹配否菱形半径=2中低快速旋转
4LK局部光流否levels=3, win=15中高中弱纹理、大位移
5HS变分光流是alpha=1.0, iter=200中中运动边界模糊
6TV-L1变分光流是lambda=0.35, iter=300高高步长越界发散
7CLG混合光流是win=9, iter=20中高中窗口跨边界
8TRANS全局参数是thresh=2.0低极低存在旋转或缩放
9AFF全局参数是thresh=2.0中低极低强透视
10HOMO全局参数是thresh=2.0, iter=500高(平面场景)低前后景视差
11GRID网格模型是grid=4×4, blend=8高中高块内特征点不足

答辩现场最容易被追问的三个问题,提前准备好答案:第一,「哪些方法是凸的,为什么」。答案要落到能量泛函上——HS、TV-L1、CLG 的数据项加正则项是凸的,有唯一全局最优;块匹配是组合搜索,只能保证搜到的局部最小;单应性矩阵的 DLT 是线性最小二乘,凸,但 RANSAC 的外层循环是非凸的。第二,「为什么不直接上深度学习光流」。答案不是「深度学习不好」,而是这个作业的交付目标是可解释的能量泛函与收敛性分析,网络权重给不了这个。第三,「单应性矩阵什么时候会崩」。答案是场景存在明显前后景视差、或者特征点集中在图像一角时,前者让共面假设失效,后者让归一化后的条件数依然很差。

从那以后我每次带这类作业,都要求学生先跑一遍「人工平移自检」,再开始写方法——坐标系和方向这两个坑,提前花十分钟验证,能省掉后面两天对着 PSNR 曲线猜原因的时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进