简介:这是一份面向电子信息工程、计算机视觉与图像处理方向本科生的数字图像处理毕业设计论文文档,围绕数字电视背景下的数字图像缩放技术展开,适合需要参考论文结构、算法实现与实验对比思路的读者。压缩包共1个doc文件,大小约2.05MB,内容以完整毕业论文为主体,包含任务书、摘要、目录及章节正文,便于直接查阅选题背景、研究方法和写作框架。论文重点讨论最近邻域插值、双线性插值、双三次插值等经典算法,并结合MATLAB仿真环境完成编程实验,对多种图像放大与缩小算法的性能和运算量进行对比分析,可帮助读者理解从理论基础、算法实现到实验结果整理的完整过程,也能为图像处理入门实验和论文排版提供参考。目前已有915人学习,对撰写相关方向毕业论文或开展图像缩放实验具有一定借鉴价值。
1. 从一份 2006 年的毕业设计说起:图像缩放到底难在哪
把一张 720P 的素材塞进 1080P 的播放器,画面究竟变清楚了还是变糊了?这个问题我第一次被问住,是在拆一份数字图像处理毕业论文的时候。论文的题目是数字图像缩放技术研究,核心就一件事:用插值算法完成分辨率变换,再把几种算法的效果和运算量摆到一张表上对比。它适合想亲手实现插值核函数、搞清坐标映射和边缘方向的开发者,不适合只想调一行imresize交差的人。整份资料的骨架很清楚:数字电视视频格式 → 数学插值理论 → 三种经典插值 → 四种改进算法 → 实验对比,最后一章落到多帧融合超分辨。我按能跑起来、能改得动的顺序,把这套东西重新拆了一遍。
2. 工程结构与跑通:把六章论文摊成一套 MATLAB 实验台
论文是线性的六章结构,但代码不能按章节写成一堆散脚本。想让实验可复现、可反复对比,第一件事是把论文的论证顺序翻译成目录结构,让每个算法函数都能被同一个评测入口调用。这一章先把工程骨架搭起来,再把 MATLAB 环境和第一次跑通的过程捋清楚。
2.1 目录规划:论文六章对应到脚本树
论文的章节划分天然对应四层:数据层、算法层、评测层、输出层。我在本地一般会这么摊开:
imgscale/ ├── data/ # 测试图:人脸、纹理密集图、含文字边缘的截图 │ ├── face.png │ ├── texture.png │ └── text_edge.png ├── src/ │ ├── common/ │ │ ├── map_coord.m # 反向坐标映射,所有插值共用 │ │ └── load_gray.m # 读图 + 转灰度 + 转 double,统一入口 │ ├── classic/ │ │ ├── interp_nearest.m # 第 3 章:最近邻 │ │ ├── interp_bilinear.m # 第 3 章:双线性 │ │ └── interp_bicubic.m # 第 3 章:双三次 │ ├── improved/ │ │ ├── shrink_avg.m # 第 4 章:近邻 + 邻域取平均 │ │ ├── ferguson_scale.m # 第 4 章:Ferguson 曲面插值 │ │ ├── adaptive_bicubic.m# 第 4 章:带系数自适应 │ │ └── edge_directed.m # 第 4 章:边缘定向插值 │ └── eval/ │ ├── bench_scale.m # 计时 + PSNR │ └── run_compare.m # 一键跑完所有算法,输出结果表 ├── out/ # 缩放结果图 + 结果 csv └── README.md几个点值得说明。common/单独抽出来,是因为坐标映射和图像归一化这两件事一旦写错,后面所有算法的对比结论都是废的——这是血泪经验,先统一再谈算法。classic/和improved/分开放,是为了让论文第三章和第四章的实验能各自独立跑,也能混着跑。eval/里只放评测,不放任何算法,保证「测什么」和「怎么测」解耦。
所有算法函数统一签名,这一点很关键:
% 统一接口:输入源图 + 输出尺寸,返回缩放后的 double 图 % img : H×W×C 的 double,值域 [0,1] % outH : 输出高度(像素) % outW : 输出宽度(像素) function out = any_interp(img, outH, outW)有了这个约定,run_compare.m里用一个函数句柄数组就能遍历全部算法,新增一种算法只要往数组里加一行,评测脚本完全不用改。论文里七种算法(三种传统 + 四种改进)如果各自写一套读图、显示、存图的重复代码,改一次路径要改七个地方,翻车概率高得离谱。
2.2 统一实验骨架:读图、缩放、评测三段式
论文的实验流程本质上是三段:准备一张源图,用不同算法把它缩放到同一个目标尺寸,然后对比结果和耗时。落到代码上就是run_compare.m:
% run_compare.m —— 一键跑完论文里的所有算法并落表 clear; clc; I = load_gray('data/face.png'); % 统一转成 [0,1] 的 double 灰度图 [H, W] = size(I); scale = 2; % 放大倍数,可改成 0.5 做缩小实验 outH = round(H * scale); outW = round(W * scale); % 每个元素是 {显示名, 函数句柄} algos = { 'nearest', @(im,h,w) interp_nearest(im,h,w); 'bilinear', @(im,h,w) interp_bilinear(im,h,w); 'bicubic', @(im,h,w) interp_bicubic(im,h,w,-0.5); 'avg_shrink',@(im,h,w) shrink_avg(im, round(1/scale)); 'edge_dir', @(im,h,w) edge_directed(im,h,w) }; results = cell(size(algos,1), 4); for k = 1:size(algos,1) name = algos{k,1}; fn = algos{k,2}; [p, t] = bench_scale(fn, I, outH, outW, 5); % PSNR + 中位耗时 O = fn(I, outH, outW); imwrite(O, fullfile('out', [name '.png'])); % 落盘,方便肉眼比对 results(k,:) = {name, p, t, [outH outW]}; fprintf('%-12s PSNR=%6.3f dB time=%8.2f ms\n', name, p, t); end writecell(results, 'out/result.csv'); % 直接进论文表格scale、outH、outW这三个参数是整个实验的自变量,改一个就换一组实验条件。bench_scale后面第 5 章会展开,这里先理解为「跑 n 次取中位数 + 算 PSNR」。imwrite那一步别省,算法调错了光看数字看不出来,把图存下来并排一看,振铃、锯齿、边缘发虚立刻就现原形。
load_gray.m只有三行,但必须存在:
function G = load_gray(path) I = imread(path); if size(I,3) == 3 I = rgb2gray(I); % 论文实验以灰度为主,先降到单通道 end G = im2double(I); % 关键:转 double 并归一化到 [0,1] endim2double这一步是分水岭。uint8类型下做加权求和,0.5*200 + 0.5*100不会得到 150,而是会在整数运算里被截断,边界处直接出现色阶断层。整个工程统一用[0,1]的 double 计算,最后imwrite时 MATLAB 会自动乘回 255,这条纪律从第一天就要立住。
2.3 环境准备与第一次跑通
MATLAB 版本上,R2016b 以上比较省心,因为zeros(..., 'like', img)这类写法在更早的版本里不支持。必须有的是 Image Processing Toolbox,padarray、rgb2gray、imresize(对照组用)都在里面。开跑前先做一次环境自检:
% 环境自检:工具箱、读图链路、数据范围 assert(license('test','Image_Toolbox') == 1, '缺少 Image Processing Toolbox'); I = imread('data/face.png'); fprintf('尺寸 %s,类型 %s\n', mat2str(size(I)), class(I)); G = im2double(I); fprintf('double 化后范围 [%.2f, %.2f]\n', min(G(:)), max(G(:)));第三行输出必须落在[0, 1]。如果打印出来是[0, 255],说明中间某一步漏了im2double,后面 PSNR 会算出一堆莫名其妙的值。第一次跑通的目标不是效果好,而是run_compare.m能把五张结果图吐到out/目录、控制台五行数字不报错。先跑通最近邻,再逐个打开其余算法,这样出错时能立刻定位是哪一层的锅。
提示:如果
padarray报未定义,八成是装的 MATLAB 只有基础模块。padarray属于 Image Processing Toolbox,不能用zeros加手动赋值替代——手动补边的索引极容易差一,是新手最常见的翻车点之一。
3. 三种经典插值的实现:最近邻、双线性、双三次的核函数
论文第三章是整份资料的地基,三种传统插值不是并列关系,而是「邻域取几个点、用什么权重」的递进:最近邻取 1 个点,双线性取 4 个点做线性加权,双三次取 16 个点做三次核加权。把这三段的代码写透,后面四种改进算法才有参照系。
3.1 坐标映射:所有插值算法共用的第一道关
插值的第一步不是加权,是「目标图的这个像素,对应源图的哪个位置」。这一步错了,后面核函数再漂亮也白搭。论文里给的是连续坐标下的映射关系,落到 MATLAB 的 1-based 离散坐标上,必须补一个半像素偏移:
function [sx, sy] = map_coord(ox, oy, scaleX, scaleY) % ox, oy : 目标图坐标(1-based,代表像素中心) % scaleX : 输出宽度 / 源宽度,scaleY 同理 % 返回源图上的浮点坐标,(x.5) 的写法对应像素中心对齐 sx = (ox - 0.5) / scaleX + 0.5; sy = (oy - 0.5) / scaleY + 0.5; end为什么要减 0.5 再加 0.5?因为 MATLAB 图像坐标 1 代表第一个像素的中心,而不是左上角。如果直接写sx = ox / scaleX,整幅图会整体偏移半个像素,放大后看不太出来,缩小后再放大做对比时,PSNR 能凭空掉两三个 dB,而且掉得毫无道理。这个偏移是后面第 5 章第一条坑的根源。
scaleX、scaleY分开传是为了支持非等比缩放。论文里的实验都是等比,但真实场景下把 1920×1080 塞进 1280×720 之外的异形屏时,两个方向的比不一样,拆开传就顺手了。
3.2 最近邻插值:一行 round 取整,坑全在边界
最近邻的逻辑最简单:源坐标四舍五入取整,直接搬值。论文里它作为基准算法出现,效果最差但运算量最小,是后面所有算法运算量对比的分母。
function out = interp_nearest(img, outH, outW) [H, W, C] = size(img); scaleY = outH / H; scaleX = outW / W; out = zeros(outH, outW, C, 'like', img); for oy = 1:outH for ox = 1:outW [sx, sy] = map_coord(ox, oy, scaleX, scaleY); iy = min(max(round(sy), 1), H); % 取整后夹紧到 [1,H] ix = min(max(round(sx), 1), W); % 取整后夹紧到 [1,W] out(oy, ox, :) = img(iy, ix, :); end end endmin(max(...))这一层夹紧不能省。round(sy)在极端情况下可能算出 0(当sy < 0.5)或者H+1(当sy > H+0.5),直接拿去索引就是经典的Index exceeds matrix dimensions。夹紧之后语义上也合理——越界就取最近的那条边。
zeros(..., 'like', img)保证输出和输入同类型同设备,在 GPU 数组上跑时不会莫名其妙掉回 CPU。老版本 MATLAB 不支持这个写法,换成zeros(outH, outW, C)就行。
最近邻的视觉效果是锯齿,尤其在斜边缘上,放大 4 倍后每一个源像素变成一个 4×4 的色块,边缘像楼梯。但它在两个场景下依然是正解:一是实时性要求极高、且缩放比接近 1 的场合;二是做标签图(label map)的缩放,语义分割的掩码用双线性插值会插出根本不存在的类别编号。
3.3 双线性插值:四邻点加权与向量化改写
双线性把「取一个点」变成「取周围四个点,按距离加权」。论文里用插值核的角度解释:它的核函数是一个三角形,频域上是 sinc²,比最近邻的矩形核泄漏小得多。
function out = interp_bilinear(img, outH, outW) [H, W, C] = size(img); scaleY = outH / H; scaleX = outW / W; Ip = padarray(img, [1 1], 'symmetric'); % 四周补一圈,省掉逐点判边界 out = zeros(outH, outW, C, 'like', img); for oy = 1:outH for ox = 1:outW [sx, sy] = map_coord(ox, oy, scaleX, scaleY); x0 = floor(sx); y0 = floor(sy); dx = sx - x0; dy = sy - y0; % 小数部分即权重 % 填充后索引整体 +1,四个邻点 p00 = Ip(y0+1, x0+1, :); p10 = Ip(y0+1, x0+2, :); p01 = Ip(y0+2, x0+1, :); p11 = Ip(y0+2, x0+2, :); out(oy, ox, :) = (1-dx)*(1-dy)*p00 + dx*(1-dy)*p10 ... + (1-dx)*dy*p01 + dx*dy*p11; end end end四个权重(1-dx)(1-dy)、dx(1-dy)、(1-dx)dy、dxdy加起来恒等于 1,这是双线性能保持亮度均值不漂移的原因。dx、dy就是源坐标的小数部分,取值范围[0,1),正好当权重用。
padarray(..., 'symmetric')用镜像方式补边,比补零好。补零会在图像四周形成一圈暗边,因为边界像素和「虚拟的 0」做了加权平均;镜像补边相当于假设图像在边界外自然延续,视觉上无感。这一步在缩小实验里尤其重要,补零会让整幅图看起来像被暗角滤镜处理过。
双线性的短板是高频细节。它在每个方向上只有两个采样点,核函数的支撑太窄,放大后边缘会「糊成一片」,专业说法叫过度平滑。但它的运算量只有双三次的四分之一左右,在嵌入式设备上到今天仍然大量在用。
3.4 双三次插值:4×4 邻域与 Keys 核函数
双三次把邻域从 2×2 扩到 4×4,权重不再是线性的三角,而是三次多项式核。论文里重点分析了核函数形状对结果的影响,这也是它比前两个算法值得多写几段的原因。
Keys 核函数(a 为锐度参数,常用 -0.5):
W(t) = (a+2)|t|³ - (a+3)|t|² + 1 , |t| ≤ 1 W(t) = a|t|³ - 5a|t|² + 8a|t| - 4a , 1 < |t| < 2 W(t) = 0 , |t| ≥ 2落到代码上,先写核函数,再写主体。主体部分做成向量化,否则 16 次邻域访问的四重循环能把 512×512 的图跑到几十秒:
function out = interp_bicubic(img, outH, outW, a) if nargin < 4, a = -0.5; end % 默认值与 imresize 保持一致 [H, W, C] = size(img); scaleY = outH / H; scaleX = outW / W; Ip = padarray(img, [2 2], 'symmetric'); % 4×4 邻域,两边各要两圈 [yy, xx] = ndgrid(1:outH, 1:outW); % 全图像素坐标一次性生成 sx = (xx - 0.5) ./ scaleX + 0.5; sy = (yy - 0.5) ./ scaleY + 0.5; x0 = floor(sx); y0 = floor(sy); dx = sx - x0; dy = sy - y0; out = zeros(outH, outW, C, 'like', img); for m = -1:2 wx = cubic_kernel(dx - m, a); % 横向 4 个权重,矩阵形式 for n = -1:2 wy = cubic_kernel(dy - n, a); % 纵向 4 个权重 iy = min(max(y0 + n + 2, 1), H + 4); ix = min(max(x0 + m + 2, 1), W + 4); for c = 1:C out(:,:,c) = out(:,:,c) + wx .* wy .* Ip(iy, ix, c); end end end end function w = cubic_kernel(t, a) t = abs(t); w = (a+2).*t.^3 - (a+3).*t.^2 + 1; % |t| ≤ 1 段 m1 = t > 1; w(m1) = a.*t(m1).^3 - 5*a.*t(m1).^2 + 8*a.*t(m1) - 4*a; w(t >= 2) = 0; % 支撑外直接归零 endndgrid生成全图坐标后,sx、sy、dx、dy都是outH×outW的矩阵,Ip(iy, ix, c)用两个同尺寸矩阵索引,返回的也是同尺寸矩阵,一次就处理完整幅图。这是 MATLAB 里做逐像素算法的标准套路:把「点」变成「矩阵」,剩下的交给向量化。
a这个参数是双三次的灵魂。a = -0.5是 Keys 原文的取值,也是 MATLABimresize的默认值,锐度适中;a = -0.75更锐,边缘对比更强,但过冲(overshoot)更明显,文字边缘会出现白边黑边的「振铃」;a趋近 0 时核函数退化成接近双线性的平滑形状。论文第四章的「带系数自适应插值」本质上就是让a跟着局部内容变,而不是全图一个常数。
iy、ix那两行夹紧同样不能省。y0最小时可能是 0,加 2 后得 2,索引安全;但y0 + 2 + 2在图像最下缘可能超出H + 4,不夹紧就报越界。夹紧到边界的效果是边缘几行重复使用最外圈像素,对视觉影响可以忽略。
4. 四种改进算法的落地:邻域取平均缩小、Ferguson 曲面与边缘定向
论文第四章是整份资料里最有嚼头的部分。它没有推翻传统插值,而是在同样的框架里换掉某一个环节:换采样方式、换曲面拼接方式、换核函数参数、换插值方向。这一章逐个拆,每个算法给出可运行的骨架。
4.1 近邻插值 + 邻域取平均:专治缩小时的混叠
论文 4.1 节的问题引入很直接:缩小图像时,最近邻只从源图取一个点,源图里被跳过的那些像素信息全部丢失,结果是高频纹理变成摩尔纹,细密条纹直接「跳」出错误的花纹。这在数字电视里是灾难,因为视频里的织物、栅栏、条纹衬衫全都是这类内容。
改进思路是把「取一个点」换成「取一块取平均」,本质上是一个盒式低通滤波器加抽样:
function out = shrink_avg(img, s) % s 为整数缩小倍数,目标图每个像素对应源图 s×s 的块 [H, W, C] = size(img); outH = floor(H / s); outW = floor(W / s); out = zeros(outH, outW, C, 'like', img); for oy = 1:outH for ox = 1:outW ys = (oy-1)*s + 1 : oy*s; % 源图对应的行块 xs = (ox-1)*s + 1 : ox*s; % 源图对应的列块 blk = img(ys, xs, :); out(oy, ox, :) = mean(mean(blk, 1), 2); % 先按行再按列求均值 end end endmean(mean(blk,1),2)这个写法要留意。mean(blk,1)沿第一维求均值,把s×s×C压成1×s×C;再mean(..., 2)沿第二维,压成1×1×C。两次求均值等价于对整块求均值,但避免了mean(blk(:))在多通道时把通道也混在一起——这是很多人第一次写就踩的坑,彩图缩完变成灰度。
论文里还提到一个细节:块平均在图像边缘会「吃」掉不满一块的部分,floor(H/s)保证了不越界,但会丢掉最下面或最右边不足s行的内容。工程上通常先用padarray补齐再缩,或者改成浮点缩小比、按浮点边界做加权平均。论文选了整数倍的简化版,做实验够用。
这个算法只在缩小时成立。放大时s < 1,块就退化成单个像素,跟最近邻没区别。这也是论文把它归到「图像缩小算法」而不是通用缩放算法的原因。
4.2 Ferguson 曲面插值:用切向量把曲面片拼起来
论文 4.2 节引入了 Ferguson 曲面。它的出发点是:双三次插值把图像看成一个个孤立的采样点,只保证函数值连续,不保证一阶导数连续。放大后相邻像素块之间的过渡会有肉眼可见的「折痕」,尤其在大倍数放大时。
Ferguson 曲面的做法是给每个角点额外带上 x、y 两个方向的切向量,用三次 Hermite 基函数把 2×2 的四个角点拼成一张光滑曲面片。Hermite 基函数长这样:
function h = hermite_basis(t) % 返回 [h00, h10, h01, h11] 四个基函数在 t 处的值 h = [ 2*t^3 - 3*t^2 + 1, ... % h00:起点函数值权重 t^3 - 2*t^2 + t, ... % h10:起点切向量权重 -2*t^3 + 3*t^2, ... % h01:终点函数值权重 t^3 - t^2 ]; % h11:终点切向量权重 end有了基函数,一张曲面片就是 4×4 系数矩阵的双侧加权:
function v = ferguson_eval(M, u, v_) % M : 4×4 系数矩阵,按 [f; f_u; f_v; f_uv] 的行列顺序排 % u, v_ : 曲面片内的局部坐标,范围 [0,1] hu = hermite_basis(u); hv = hermite_basis(v_); v = hu * M * hv'; % 双三次 Hermite 求值 end function M = ferguson_coef(f, gx, gy, gxy) % f, gx, gy, gxy 均为 2×2,对应四个角点的值、偏导、混合偏导 M = [ f(1,1) f(1,2) gy(1,1) gy(1,2); f(2,1) f(2,2) gy(2,1) gy(2,2); gx(1,1) gx(1,2) gxy(1,1) gxy(1,2); gx(2,1) gx(2,2) gxy(2,1) gxy(2,2) ]; endM的排布顺序是固定的:第一行是四个角点的函数值,第二行是沿 y 方向的偏导,第三行是沿 x 方向,第四行是混合偏导。顺序错了结果不会报错,只会出来一张扭曲的图,排查起来极其费劲,建议在ferguson_coef里加注释固定下来。
实际使用时,四个角点的f直接从源图取;gx、gy用中心差分估:
gx = (img(y, x+1) - img(y, x-1)) / 2; % 中心差分比前向差分准 gy = (img(y+1, x) - img(y-1, x)) / 2;gxy用四邻点的二阶差分近似,gxy = (f(x+1,y+1) - f(x+1,y-1) - f(x-1,y+1) + f(x-1,y-1)) / 4。偏导估计的精度直接决定曲面光滑度,用前向差分(f(x+1)-f(x))会让整张图往一个方向「歪」,这是很低级但很常见的错。
Hermite 基的代价是每个曲面片要算 16 次乘法,比双三次多不了一倍,但换来一阶导连续,斜边缘放大后不会出现分段折线感。论文给它的评价是「运算量可接受,边缘连续性优于双三次」,这个结论在含大量曲线边缘的图像上确实成立。
4.3 带系数自适应插值:让核函数跟着局部方差走
论文 4.3 节的出发点是双三次那个a参数的两难:全图用一个a = -0.5,平坦区域(比如天空、皮肤)会残留轻微振铃,边缘区域又锐度不够。既然a控制核的锐度,那就让它跟着局部内容变。
function a = adaptive_a(blk, aFlat, aEdge) % blk : 目标像素对应的源图局部窗口(如 5×5) % aFlat : 平坦区用的核参数,偏平滑 % aEdge : 边缘区用的核参数,偏锐 if nargin < 3, aEdge = -1.0; end if nargin < 2, aFlat = -0.5; end v = var(blk(:)); % 局部方差,边缘区明显更大 t = min(v / 0.02, 1); % 0.02 是经验归一化尺度,需按图调 a = aFlat + (aEdge - aFlat) * t; % 线性插值到 [aFlat, aEdge] end0.02这个归一化尺度是玄学。图像的方差范围跟内容、位深、是否归一化都有关,[0,1]的 double 图里,平坦天空的局部方差可能只有 1e-4 量级,而锐利文字边缘能到 0.05 以上。用固定阈值必然在某类图上失灵。我的做法是先跑一张测试图,把var的分布打印出来(histogram或者干脆prctile),取 70% 分位数当归一化尺度,比拍脑袋定一个数靠谱得多。
调好之后,主循环里对每个目标像素取周围 5×5 的源图块算a,再调interp_bicubic的核函数。注意a是逐像素变的,不能像 3.4 节那样把wx、wy一次性算成矩阵,得在循环里逐点算,速度会掉一个量级。折中方案是把a量化成 4 档,预计算 4 组核函数权重表,再按a的档位查表,速度快很多。
参数表如下,实际调的时候从中间值往两边试:
| 参数 | 含义 | 典型取值 | 调大的后果 |
|---|---|---|---|
aFlat | 平坦区锐度 | -0.5 ~ -0.6 | 越负越容易出现细微振铃 |
aEdge | 边缘区锐度 | -0.9 ~ -1.2 | 过负会在文字边缘出现白边 |
| 窗大小 | 局部方差统计范围 | 5×5 或 7×7 | 太大则边缘判定迟钝 |
| 归一化尺度 | 方差映射到 [0,1] | 按图统计取分位数 | 太小则全图都判成边缘 |
论文对这个算法的评价是「主观效果提升明显,客观 PSNR 提升有限」,这个结论很实在。自适应锐化本质上是讨好眼睛,不是讨好指标,PSNR 高不代表好看,第 5 章会专门说这件事。
4.4 边缘定向插值:先判断往哪个方向插
论文 4.4 节是四种改进里思路最「像人」的一个。传统插值不分方向,横向纵向一视同仁地加权,结果是在斜边缘上把边缘两侧的像素混在一起,边缘变模糊。人眼看斜边缘时会自动沿着边缘的方向去补,边缘定向插值做的就是这件事。
核心判断只有一句:如果水平方向梯度大,说明边缘是竖直走向的,那就沿着竖直方向(y 方向)做插值,避免跨越边缘。
function out = edge_directed(img, outH, outW) [H, W, C] = size(img); scaleY = outH / H; scaleX = outW / W; Ip = padarray(img, [2 2], 'symmetric'); out = zeros(outH, outW, C, 'like', img); for oy = 1:outH for ox = 1:outW [sx, sy] = map_coord(ox, oy, scaleX, scaleY); x0 = floor(sx); y0 = floor(sy); dx = sx - x0; dy = sy - y0; % 用中心差分估局部梯度(填充后索引 +2) gx = (Ip(y0+2, x0+3) - Ip(y0+2, x0+1)) / 2; gy = (Ip(y0+3, x0+2) - Ip(y0+1, x0+2)) / 2; if abs(gx) > abs(gy) % 水平梯度大 → 边缘竖直 → 沿 y 方向插值 out(oy, ox, :) = (1-dy) * Ip(y0+2, x0+2, :) + dy * Ip(y0+3, x0+2, :); else % 否则沿 x 方向插值 out(oy, ox, :) = (1-dx) * Ip(y0+2, x0+2, :) + dx * Ip(y0+2, x0+3, :); end end end endgx、gy用中心差分而不是简单的相邻差,抗噪好一些。abs(gx) > abs(gy)这个二选一的门限非常粗暴,在纹理区域会频繁抖动,产生细碎的「毛刺」。论文里的简化算法就到这里,再往下做一般是把二选一换成按|gx|、|gy|的比值做加权融合,或者沿多个候选方向做最小方差选择(类似方向性插值 DDI 的思路)。
这个算法在含文字、建筑边缘、卡通线条的图上效果明显好于双三次,因为这些图有大量强方向性的边缘。在自然风景照上提升有限,因为自然图像的边缘方向分布太杂。做实验时选图很关键,用一张全是树叶的照片去测边缘定向插值,结论会很难看。
4.5 多帧融合超分辨:论文最后一节的思路延伸
论文 4.5 节跳出了单帧插值的框架,转向多帧融合。核心假设是:同一场景拍多张,每张的亚像素位移略有不同,把这些互补的高频信息对齐后融合,能恢复出超过单帧插值上限的分辨率。论文里用了共轭梯度法做融合求解,这一节的完整实现代码量很大,不适合塞进演示脚本,但思路值得记一笔:
- 帧间配准:估计每帧相对参考帧的亚像素位移,常用相位相关或光流。
- 构建观测方程:低分辨率帧 = 高分辨率图经过下采样 + 模糊 + 位移后的结果。
- 迭代求解:把超分辨重建写成一个最小二乘问题,用共轭梯度法迭代,比直接梯度下降收敛快。
从工程角度看,第三条是这一节的核心。共轭梯度法在这里的价值是:观测矩阵巨大但稀疏,直接求逆不可能,而共轭梯度只需要矩阵-向量乘,几十次迭代就能收敛到可用的解。这条思路和今天深度学习超分里那些「退化建模 + 迭代优化」的做法一脉相承,只是 2006 年没有 GPU 和大数据集。
单帧插值和多帧融合的定位要分清:前者是从已有像素里「猜」,信息量上限就是原图;后者是从多帧的互补信息里「恢复」,上限更高但依赖配准精度。配准差半个像素,融合结果会比单帧还糊,这是多帧路线最现实的落地门槛。
5. 对比实验与避坑排查:PSNR、耗时和五个必踩的坑
论文第五章的价值在于它把「哪个算法好」变成了可测量的数字。但测量这件事本身陷阱极多,代码跑通和结果可信之间隔着好几道坎。这一章先把评测脚本写对,再把这几年在 MATLAB 图像实验里踩过的坑列出来。
5.1 评测脚本:PSNR、耗时和结果落表
评测脚本要解决两个问题:时间怎么测才准,PSNR 跟谁比才合理。
function [p, t] = bench_scale(fn, I, outH, outW, nRun) % fn : 算法函数句柄,签名 @(img,outH,outW) % nRun : 重复次数,取中位数,避开 JIT 预热的第一次 if nargin < 5, nRun = 5; end fn(I, outH, outW); % warm-up,结果丢弃 ts = zeros <p> <a href="https://download.csdn.net/download/zhaojiawei2872/1331333" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>