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位处理。上面的代码是灰度图版本,彩色图把三个通道拆开分别跑一遍就行了。

相关推荐
阿里云大数据AI技术1 小时前
一句话即可用好 MaxCompute:AI 全能搭子 MaxAgent 来了——会运维、能分析
人工智能·agent
兔兔兔兔11 小时前
记录C++ 8
开发语言·c++
野小生1 小时前
从零搭建越用越聪明的 AI 第二大脑:Claude Code + Obsidian 9 步实战
人工智能
两万五千个小时1 小时前
DeepSeek Harness 从 0 开始:09 System Prompt 模块(提示词组装)
人工智能·程序员·架构
刘海东刘海东1 小时前
一条新的人工智能道路(刘海东)第一章、第二章
人工智能
lovingsoft1 小时前
AI Agent 不是复读机:一个“计划-观察-执行“闭环,把大模型从聊天框变成能闭环干活的实习生
人工智能
临江仙4551 小时前
同一个 AI Agent 如何同时服务 Web、微信和 QQ:PureChat 的渠道架构实践
前端·人工智能·后端
码路漫漫1 小时前
人类程序员还有用,记一次 GPT 把 Figma 两个接口搞反的事
人工智能·程序员
云存储小精灵1 小时前
DeepSeek Harness COS 插件上线:让 Agent 管理文件更简单
人工智能·产品