C#图像像素放大:邻域平均、双线性插值实现像素放大的C#实现代码

C#图像像素放大:邻域平均、双线性插值实现像素放大的C#实现代码

C#图像像素放大:邻域平均、双线性插值实现像素放大的C#实现代码

前阵子做一个项目,需要把相机拍到的ROI区域放大4倍显示在界面上。直接拉伸,边缘全是锯齿,跟马赛克一样。

用了双线性插值之后,平滑多了。但代价是计算量翻了好几倍。

图像放大这事,不存在"最好"的算法,只有"最合适"的场景。下面把两种常用的方法------最近邻插值(邻域平均)和双线性插值------的C#实现贴出来,顺便说说什么时候该用哪个。

一、最近邻插值:简单粗暴,速度最快

原理四个字:找最近的。

目标图像里每个像素,映射回原图坐标,四舍五入取整,直接把最近的那个像素值拿过来。不计算、不插值,纯粹是"复制粘贴"。

csharp 复制代码
/// <summary>
/// 最近邻插值放大(邻域平均)
/// </summary>
/// <param name="src">原始图像数据(8位灰度)</param>
/// <param name="srcWidth">原图宽度</param>
/// <param name="srcHeight">原图高度</param>
/// <param name="scaleX">水平放大倍数</param>
/// <param name="scaleY">垂直放大倍数</param>
/// <returns>放大后的图像数据</returns>
public byte[] ResizeNearestNeighbor(byte[] src, int srcWidth, int srcHeight, 
                                    float scaleX, float scaleY)
{
    int dstWidth = (int)(srcWidth * scaleX);
    int dstHeight = (int)(srcHeight * scaleY);
    byte[] dst = new byte[dstWidth * dstHeight];
    
    for (int y = 0; y < dstHeight; y++)
    {
        // 目标像素映射回原图坐标,四舍五入取整
        int srcY = (int)Math.Round(y / scaleY);
        if (srcY >= srcHeight) srcY = srcHeight - 1;
        
        int rowOffset = y * dstWidth;
        int srcRowOffset = srcY * srcWidth;
        
        for (int x = 0; x < dstWidth; x++)
        {
            int srcX = (int)Math.Round(x / scaleX);
            if (srcX >= srcWidth) srcX = srcWidth - 1;
            dst[rowOffset + x] = src[srcRowOffset + srcX];
        }
    }
    return dst;
}

优点:快,极快。纯内存复制操作,不涉及浮点运算。

缺点:放大后边缘全是锯齿,缩小时会有摩尔纹。

适合场景:实时预览、对画面质量要求不高的缩略图、像素艺术风格(游戏像素风放大就是用的这个)

二、双线性插值:平滑过渡,质量高

原理稍微复杂一点。

目标像素映射回原图,坐标大概率不是整数(比如原图的(2.3, 1.7)位置)。双线性插值取这个点周围4个最近像素,按距离加权平均。

先水平插两次,再垂直插一次,一共三次线性插值。

csharp 复制代码
/// <summary>
/// 双线性插值放大
/// </summary>
public byte[] ResizeBilinear(byte[] src, int srcWidth, int srcHeight, 
                             float scaleX, float scaleY)
{
    int dstWidth = (int)(srcWidth * scaleX);
    int dstHeight = (int)(srcHeight * scaleY);
    byte[] dst = new byte[dstWidth * dstHeight];
    
    for (int y = 0; y < dstHeight; y++)
    {
        // 目标像素映射回原图的浮点坐标
        float srcY = y / scaleY;
        // 取整(向下)和分数部分
        int y0 = (int)Math.Floor(srcY);
        float yFrac = srcY - y0;
        // 边界处理
        if (y0 < 0) y0 = 0;
        if (y0 >= srcHeight - 1) y0 = srcHeight - 2;
        
        int rowOffset = y * dstWidth;
        int srcRow0 = y0 * srcWidth;
        int srcRow1 = (y0 + 1) * srcWidth;
        
        for (int x = 0; x < dstWidth; x++)
        {
            float srcX = x / scaleX;
            int x0 = (int)Math.Floor(srcX);
            float xFrac = srcX - x0;
            if (x0 < 0) x0 = 0;
            if (x0 >= srcWidth - 1) x0 = srcWidth - 2;
            
            // 四个相邻像素的值
            float p00 = src[srcRow0 + x0];
            float p10 = src[srcRow0 + x0 + 1];
            float p01 = src[srcRow1 + x0];
            float p11 = src[srcRow1 + x0 + 1];
            
            // 水平方向插值(先算上边一行,再算下边一行)
            float top = p00 + (p10 - p00) * xFrac;
            float bottom = p01 + (p11 - p01) * xFrac;
            // 垂直方向插值
            float value = top + (bottom - top) * yFrac;
            
            dst[rowOffset + x] = (byte)Math.Round(Math.Clamp(value, 0, 255));
        }
    }
    return dst;
}

计算过程拆解:

  1. 目标像素(x,y)映射回原图坐标(srcX, srcY),是浮点数
  2. 取srcX的整数部分x0和小数部分xFrac
  3. 取srcY的整数部分y0和小数部分yFrac
  4. 取原图四个点:(x0,y0)、(x0+1,y0)、(x0,y0+1)、(x0+1,y0+1)
  5. 水平方向插两次:top = lerp(p00, p10, xFrac),bottom = lerp(p01, p11, xFrac)
  6. 垂直方向插一次:result = lerp(top, bottom, yFrac)

优点:放大后边缘平滑,没有锯齿,视觉质量好得多

缺点:计算量大(每个像素要做6次浮点乘加),比最近邻慢3-5倍

适合场景:最终出图、对画质有要求的场合、缩小时抗锯齿

三、实际测试对比

用一张500万像素的灰度图,放大2倍:

算法 耗时(ms) 内存分配 视觉质量
最近邻 12 一次 边缘锯齿明显
双线性 45 一次 边缘平滑,稍模糊

双线性插值慢一些,但画质差距肉眼可见。如果放大倍数超过3倍,双线性也会变模糊,这时候可以考虑更高级的双三次插值 (Bicubic)或者深度学习超分,但那是另一篇文章的内容了。

四、性能优化建议

1. 预计算映射表

如果同一个图像要反复放大(比如视频流每一帧都显示在界面上),可以提前算好每个目标像素对应的原图坐标和权重,存下来复用。这样每次只需要做内存拷贝,不用重复计算浮点数。

2. 使用unsafe代码

上面的代码为了可读性用了数组索引。如果对性能有极致要求,可以用指针+Span改写成unsafe版本,省掉边界检查的开销。

3. 并行化

每个目标像素的计算是独立的,可以用Parallel.For并行处理行,在多核CPU上能再快不少。

4. 分块处理

大图放大时,可以按行分块,每块单独计算,降低缓存miss率。

五、什么时候用哪个

只追求速度、对画质没要求:最近邻。比如实时预览、缩略图、嵌入式设备。

画质优先、速度能接受:双线性。比如缺陷检测的放大查看、最终出图。

既想快又想好:先用最近邻做粗放大,再加一层后处理平滑(比如高斯模糊边缘)。效果不如双线性,但速度快不少。

极致画质:双三次插值(Bicubic)或Lanczos。但计算量更大,一般的工业场景用不上。

六、总结

最近邻和双线性插值,本质上是速度和质量的取舍。

最近邻"拿来就用",适合实时场景;双线性"加权平均",适合出图场景。没有哪个更好,看你的项目更在乎帧率还是画质。

代码已经贴出来了,复制过去改改就能用。如果需要在界面上显示放大后的图像,配合上一篇文章的ArrayPool和SkiaSharp,做成零GC的实时放大显示,效果会更好。

小提示:双线性插值对彩色图像(BGR/RGB)的处理,需要对三个通道分别做插值,不能直接把24位数据当8位处理。上面的代码是灰度图版本,彩色图把三个通道拆开分别跑一遍就行了。

相关推荐
学编程就要猛5 小时前
RAG(Retrieval Augmented Generation)
人工智能·spring ai
冬奇Lab5 小时前
一天一个开源项目(第259篇):Modern Web Guidance —— Chrome 团队给 AI 编码 Agent 的「现代 Web 最佳实践」外挂
人工智能·开源
§ꦿCFོ༉6 小时前
嵌入式八股文—C语言入门学习(四)
c语言·开发语言·学习
飞哥数智坊7 小时前
打造自己的 Personal Agent,从抄大肥鱼作业开始
人工智能·deepseek
投图匠7 小时前
十种不同风格的 CTP接口 框架
开发语言·python
对你无可奈何7 小时前
Magpie 实战:把 WorkBuddy、Qoder 的订阅模型接进一个本地池子
人工智能
Csvn7 小时前
每请求到底多少钱?把 Token 成本算成可观测指标(C01)
人工智能·aigc·agent
吴名氏.7 小时前
Java转AI Agent教程(第十三周)混合检索与重排
java·数据库·人工智能·ai·ai agent
老金带你玩AI9 小时前
惊人的一致性!Seedream 5.5实测!
人工智能