图像基础与坐标系:像素坐标系、图像格式、缩放与插值算法

做裁剪系统,第一个绕不开的问题就是:图像在计算机里到底长什么样?

很多人一上来就写代码,结果裁出来的图颜色不对、坐标偏移、边缘锯齿严重。说白了,就是没把图像基础打牢。这一章,我把几个核心概念掰开揉碎讲清楚。

2.1 像素坐标系——图像里的“经纬度”

图像本质上是一个二维数组。每个格子就是一个像素。那怎么定位一个像素呢?用坐标。

我习惯把像素坐标系想象成一张方格纸:

  • 原点 (0, 0) 在左上角
  • x 轴 向右为正,代表列
  • y 轴 向下为正,代表行

这和数学里的笛卡尔坐标系不一样。数学里 y 轴向上,图像里 y 轴向下。很多新手在这栽跟头——用数学思维去算图像坐标,结果上下颠倒。

核心公式:

对于一张宽 W、高 H 的图像,像素 (x, y) 的取值范围是:

x ∈ [0, W-1]
y ∈ [0, H-1]

注意是 0 到 W-1,不是 1 到 W。数组下标从 0 开始,这是编程常识,但很多人写循环时容易写成 for i in range(1, W),然后发现最后一列丢了。

我曾经在一个项目里排查了半天,发现裁剪区域总是偏右一个像素。最后定位到问题:坐标计算时用了 Math.ceil 而不是 Math.floor。嗯,边界处理一定要小心。

2.2 图像格式——RGB、RGBA、YUV

图像格式决定了每个像素占多少字节、颜色怎么表示。裁剪系统里,格式选错了,性能差几倍都有可能。

2.2.1 RGB 与 RGBA

RGB 是最常见的格式。每个像素用三个通道表示:红、绿、蓝。每个通道 8 位,一个像素就是 3 字节。

RGBA 多了一个 Alpha 通道,表示透明度。一个像素 4 字节。裁剪 PNG 图片时,必须保留 Alpha 通道,否则透明背景会变成黑色。

格式 通道数 每像素字节 适用场景
RGB 3 3 JPEG、BMP 等不透明图像
RGBA 4 4 PNG、WebP 等带透明通道的图像

我的习惯:在裁剪系统内部,统一用 RGBA 处理。虽然多占点内存,但省去了判断格式的麻烦。输出时再根据需要转成 RGB 或 YUV。

2.2.2 YUV——视频领域的“老大哥”

YUV 不是一种格式,是一类格式。Y 是亮度,UV 是色度。人眼对亮度敏感,对色度不敏感,所以 UV 可以降低分辨率——这叫色度子采样。

常见的子采样模式:

  • YUV444:不压缩,每个像素都有完整的 Y、U、V
  • YUV422:水平方向每两个像素共享一组 UV
  • YUV420:水平和垂直方向每四个像素共享一组 UV。最常用,H.264/H.265 默认就是这个

你想想看,如果从摄像头拿到的数据是 YUV420,直接当 RGB 处理,颜色肯定不对。必须先做颜色空间转换。

避坑指南:我曾经接手过一个裁剪模块,输入是 YUV420,但代码里直接按 RGB 的 stride 去算偏移量。结果裁出来的图颜色全花了。后来发现 YUV420 的 UV 平面大小只有 Y 平面的四分之一,偏移量必须单独计算。

YUV 转 RGB 的公式(BT.601 标准):

R = Y + 1.402 * (V - 128)
G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
B = Y + 1.772 * (U - 128)

注意:计算出来的 R、G、B 要 clamp 到 [0, 255] 范围内。

2.3 图像缩放与插值算法

裁剪系统里,缩放是家常便饭。用户可能上传一张 4000x3000 的照片,但预览区只有 800x600。怎么缩?直接扔掉像素?那画面会惨不忍睹。

插值算法就是用来解决这个问题的。它根据已知像素的值,推算出新位置上的像素值。

2.3.1 最近邻插值——简单粗暴

原理:新像素的位置映射回原图,取最近的整数坐标像素值。

说白了就是:离谁近就抄谁的值。

// 最近邻插值伪代码
function nearestNeighbor(src, dstWidth, dstHeight):
    for each pixel (dstX, dstY) in dst:
        srcX = round(dstX * srcWidth / dstWidth)
        srcY = round(dstY * srcHeight / dstHeight)
        dst[dstY][dstX] = src[srcY][srcX]

优点:速度快,计算量极小。
缺点:放大后出现明显的马赛克和锯齿。

我一般只在做缩略图或者实时预览时用最近邻。正式输出?绝对不用。

2.3.2 双线性插值——主流选择

原理:新像素映射回原图后,取周围 2x2 四个像素,按距离加权平均。

先在一个方向(比如 x 方向)做两次线性插值,再在另一个方向(y 方向)做一次线性插值。所以叫“双线性”。

// 双线性插值核心计算
function bilinear(src, x, y):
    x1 = floor(x), x2 = ceil(x)
    y1 = floor(y), y2 = ceil(y)
    
    // x 方向插值
    top = src[y1][x1] * (x2 - x) + src[y1][x2] * (x - x1)
    bottom = src[y2][x1] * (x2 - x) + src[y2][x2] * (x - x1)
    
    // y 方向插值
    return top * (y2 - y) + bottom * (y - y1)

优点:效果比最近邻好很多,边缘平滑,计算量适中。
缺点:会轻微模糊图像,尤其是放大时。

双线性插值是我在裁剪系统里的默认选项。性能和质量之间平衡得最好。

2.3.3 双三次插值——追求极致质量

原理:取周围 4x4 共 16 个像素,用三次多项式拟合曲面,然后采样。

计算量比双线性大得多,但效果也更好。边缘保留更锐利,放大后细节更丰富。

双三次插值用的核函数是:

W(x) = {
    (a+2)|x|³ - (a+3)|x|² + 1,          0 ≤ |x| < 1
    a|x|³ - 5a|x|² + 8a|x| - 4a,        1 ≤ |x| < 2
    0,                                   |x| ≥ 2
}
// 常用 a = -0.5 或 a = -0.75

看着复杂?其实你不需要手写。OpenCV 的 cv::INTER_CUBIC 就是干这个的。

三种算法对比:

算法 采样窗口 计算量 质量 推荐场景
最近邻 1x1 极低 差(锯齿) 实时预览、缩略图
双线性 2x2 低 中(轻微模糊) 默认缩放、批量处理
双三次 4x4 高 高(锐利) 高质量输出、放大

2.4 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作裁剪系统图像基础部分的“地图”。

图像基础与坐标系 · 知识体系 裁剪系统图像基础 像素坐标系 原点左上角,x向右,y向下 取值范围 [0, W-1] × [0, H-1] 图像格式 RGB / RGBA / YUV YUV420 最常用,需注意色度子采样 插值算法 最近邻 · 双线性 · 双三次 采样窗口 1×1 / 2×2 / 4×4 核心原则:坐标准确 → 格式正确 → 算法匹配 三者缺一不可,否则裁剪结果必然出错

2.5 实战建议

学完这些理论,怎么落地到裁剪系统里?我给出几条具体建议:

  1. 统一内部格式:所有图像进入裁剪流程后,先转成 RGBA。处理完再转回原始格式输出。省去格式判断的麻烦。
  2. 坐标边界检查:每次计算像素坐标后,必须 clamp 到 [0, W-1] 和 [0, H-1]。浮点运算的舍入误差可能导致越界。
  3. 插值算法可配置:不要写死一种算法。给用户提供“快速/均衡/高质量”三个选项,分别对应最近邻、双线性、双三次。
  4. YUV 处理要小心:如果输入是 YUV420,缩放时 Y 平面和 UV 平面要分别处理。UV 平面的宽高只有 Y 平面的一半。

一个小技巧:在做双线性插值时,如果目标图像是缩小(比如从 4000x3000 缩到 800x600),可以先做一次高斯模糊再缩,能有效减少摩尔纹。我试过,效果立竿见影。