2. 基于边缘指导的插值算法

任务说明

本案例实现基于边缘指导的插值算法(Edge-Guided Interpolation, EGI)。给定一张低分辨率灰度图像,算法先检测局部边缘方向,再沿边缘方向而非正交方向进行插值,从而减少斜向边缘的锯齿与模糊。输入为单通道图像,输出为2倍放大图像。

算法原理与实现

核心思路:对每个待插值像素,先在其邻域内计算四个方向的梯度能量(水平、垂直、45°、135°),选择能量最小的方向作为边缘方向(该方向像素变化平缓),然后沿该方向取两个已知像素的均值作为插值结果。对于非边缘区域(各方向能量接近),退化为双线性插值以保证平滑。

import numpy as np
from scipy.ndimage import convolve

def edge_guided_interpolate(img):
    """
    基于边缘指导的2倍插值
    img: 2D numpy array, 灰度图, dtype=float32, 范围[0,1]
    返回: 2倍放大图像, 尺寸 (2*H-1, 2*W-1)
    """
    H, W = img.shape
    out_h, out_w = 2*H - 1, 2*W - 1
    out = np.zeros((out_h, out_w), dtype=np.float32)
    
    # 已知像素直接映射
    out[::2, ::2] = img
    
    # 方向梯度核 (用于边缘方向检测)
    # 每个核与3x3邻域卷积,输出该方向的梯度强度
    kernels = {
        'h': np.array([[0,0,0],[1,-2,1],[0,0,0]], dtype=np.float32),   # 水平变化
        'v': np.array([[0,1,0],[0,-2,0],[0,1,0]], dtype=np.float32),   # 垂直变化
        'd': np.array([[1,0,0],[0,-2,0],[0,0,1]], dtype=np.float32),   # 45°变化
        'a': np.array([[0,0,1],[0,-2,0],[1,0,0]], dtype=np.float32)    # 135°变化
    }
    
    # 计算每个已知像素的梯度能量图(尺寸与原图相同)
    grad_maps = {}
    for name, k in kernels.items():
        # 对原图做卷积,得到每个像素位置的梯度响应
        grad_maps[name] = convolve(img, k, mode='mirror') ** 2
    
    # 插值行像素 (奇数行, 偶数列) —— 水平相邻已知像素的中点
    for i in range(1, out_h, 2):
        for j in range(0, out_w, 2):
            # 对应原图中的邻域中心 (左上角已知像素)
            si, sj = i//2, j//2
            # 取该邻域四个方向的梯度能量(取中心及右/下邻域的最大值作为该区域方向判断)
            # 简化:使用中心像素的梯度能量
            g = {name: grad_maps[name][si, sj] for name in kernels}
            # 选择最小梯度方向(变化最平缓)
            best = min(g, key=g.get)
            # 沿该方向取两个已知像素插值
            if best == 'h':
                # 水平方向:左右已知像素
                left = img[si, sj] if sj-1 >= 0 else img[si, sj]
                right = img[si, sj+1] if sj+1 < W else img[si, sj]
                val = (left + right) / 2
            elif best == 'v':
                # 垂直方向:上下已知像素(但当前插值点在水平线上,取上下均值)
                up = img[si-1, sj] if si-1 >= 0 else img[si, sj]
                down = img[si+1, sj] if si+1 < H else img[si, sj]
                val = (up + down) / 2
            elif best == 'd':
                # 45°方向:右上和左下
                ru = img[si-1, sj+1] if (si-1>=0 and sj+1<W) else img[si, sj]
                ld = img[si+1, sj-1] if (si+1<H and sj-1>=0) else img[si, sj]
                val = (ru + ld) / 2
            else:  # 'a' 135°
                lu = img[si-1, sj-1] if (si-1>=0 and sj-1>=0) else img[si, sj]
                rd = img[si+1, sj+1] if (si+1<H and sj+1<W) else img[si, sj]
                val = (lu + rd) / 2
            out[i, j] = val
    
    # 插值列像素 (偶数行, 奇数列)
    for i in range(0, out_h, 2):
        for j in range(1, out_w, 2):
            si, sj = i//2, j//2
            g = {name: grad_maps[name][si, sj] for name in kernels}
            best = min(g, key=g.get)
            if best == 'h':
                left = img[si, sj-1] if sj-1>=0 else img[si, sj]
                right = img[si, sj+1] if sj+1<W else img[si, sj]
                val = (left + right) / 2
            elif best == 'v':
                up = img[si, sj] if si-1>=0 else img[si, sj]  # 实际取上下
                down = img[si+1, sj] if si+1<H else img[si, sj]
                val = (up + down) / 2
            elif best == 'd':
                ru = img[si-1, sj+1] if (si-1>=0 and sj+1<W) else img[si, sj]
                ld = img[si+1, sj-1] if (si+1<H and sj-1>=0) else img[si, sj]
                val = (ru + ld) / 2
            else:
                lu = img[si-1, sj-1] if (si-1>=0 and sj-1>=0) else img[si, sj]
                rd = img[si+1, sj+1] if (si+1<H and sj+1<W) else img[si, sj]
                val = (lu + rd) / 2
            out[i, j] = val
    
    # 插值中心像素 (奇数行, 奇数列) —— 四个已知像素的中心
    for i in range(1, out_h, 2):
        for j in range(1, out_w, 2):
            si, sj = i//2, j//2
            g = {name: grad_maps[name][si, sj] for name in kernels}
            best = min(g, key=g.get)
            if best == 'h':
                # 水平方向:左右两个已知像素的均值(但中心点需要4邻域,这里取左右)
                left = img[si, sj]
                right = img[si, sj+1] if sj+1<W else img[si, sj]
                val = (left + right) / 2
            elif best == 'v':
                up = img[si, sj]
                down = img[si+1, sj] if si+1<H else img[si, sj]
                val = (up + down) / 2
            elif best == 'd':
                # 45°方向:右上和左下
                ru = img[si-1, sj+1] if (si-1>=0 and sj+1<W) else img[si, sj]
                ld = img[si+1, sj-1] if (si+1<H and sj-1>=0) else img[si, sj]
                val = (ru + ld) / 2
            else:
                lu = img[si-1, sj-1] if (si-1>=0 and sj-1>=0) else img[si, sj]
                rd = img[si+1, sj+1] if (si+1<H and sj+1<W) else img[si, sj]
                val = (lu + rd) / 2
            out[i, j] = val
    
    return out

# 示例:构造一个带斜边的4x4测试图
test = np.array([
    [0.1, 0.1, 0.9, 0.9],
    [0.1, 0.1, 0.9, 0.9],
    [0.1, 0.9, 0.9, 0.1],
    [0.1, 0.9, 0.9, 0.1]
], dtype=np.float32)

result = edge_guided_interpolate(test)
print("输入尺寸:", test.shape, "输出尺寸:", result.shape)
print("输出像素范围: [{:.3f}, {:.3f}]".format(result.min(), result.max()))

边缘方向检测与插值示意

原图 4×4 边缘方向 → 边缘指导插值 (2×) 45°方向 (能量最小) 水平/垂直 能量较大 已知像素 待插值像素 选中边缘方向 未选中方向

运行与验证

依赖安装:

pip install numpy scipy

运行方式:将上述代码保存为 egi_demo.py,执行 python egi_demo.py。

验证方法:观察输出尺寸是否为 (7, 7)(即 2×4−1)。检查输出像素值是否全部落在 [0, 1] 区间内。为验证边缘保持效果,可构造一个理想斜边图像(如黑色背景上的白色45°斜线),分别用双线性插值和本算法放大,对比斜边区域的锯齿程度——本算法输出中斜边应更平滑、阶梯感更弱。

判定标准:程序无异常退出,输出尺寸正确,像素范围合法;在斜边测试图上,边缘指导插值结果的斜向边缘过渡像素数比双线性插值更少(即边缘更锐利)。