Lode 的计算机图形学教程

图像算术

目录

返回目录

简介

图像算术是对两张图像的颜色通道进行计算,得到一张新的合成图像。这些操作可以在 Paint Shop Pro 的算术工具或 Photoshop 的"计算"功能中完成。

这篇文章代码简单,更多的是观察图像效果。

注意:如光与颜色教程末尾所述,最好在线性 RGB 空间中进行这些操作,因此在实际应用中建议在以下操作前后进行伽马校正。

代码

以下是加载 .png 图像并进行算术运算及显示的代码。

.png 图像位于 /pics 文件夹中,名为 photo1.png、photo2.png 和 photo3.png。可以在这里下载。

以下是低质量的源图像,拍摄于比利时阿登山区徒步旅行途中。



以下是代码,应该非常直观。在执行图像算术的双重循环中,目前填入的是"平均"的公式,将代码中的这部分替换为后续章节中给出的代码即可。

int main(int argc, char *argv[])
{
  unsigned long w = 0, h = 0;
  //declare image buffers
  std::vector<ColorRGB> image1, image2, image3, result;

  //load the images into the buffers. This assumes all have the same size.
  loadImage(image1, w, h, "pics/photo1.png");
  loadImage(image2, w, h, "pics/photo2.png");
  loadImage(image3, w, h, "pics/photo3.png");
  result.resize(w * h);

  //set up the screen
  screen(w,h,0, "Image Arithmetic");

  //do the image arithmetic (here: 'average')
  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    result[y * w + x].r = (image1[y * w + x].r + image2[y * w + x].r) / 2;
    result[y * w + x].g = (image1[y * w + x].g + image2[y * w + x].g) / 2;
    result[y * w + x].b = (image1[y * w + x].b + image2[y * w + x].b) / 2;
  }

  //draw the result buffer to the screen
  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    pset(x, y, result[y * w + x]);
  }

  //redraw & sleep
  redraw();
  sleep();
}

注意也可以用 "result[y * w + x] = (image1[y * w + x] + image2[y * w + x]) / 2" 代替逐通道操作,但这种写法并不适用于所有示例。

加法

加法是将两张图像对应颜色通道相加。每个颜色分量是 0 到 255 之间的数值,因此若两个颜色之和超过 255,需截断为 255,即取结果与 255 的最小值。将以下代码复制粘贴到算术循环中并运行,即可看到两张照片相加的结果。

    result[y * w + x].r = min(image2[y * w + x].r + image3[y * w + x].r, 255);
    result[y * w + x].g = min(image2[y * w + x].g + image3[y * w + x].g, 255);
    result[y * w + x].b = min(image2[y * w + x].b + image3[y * w + x].b, 255);



第二张照片的天空非常明亮,加上另一张图像的颜色分量后亮度超限,颜色被截断为 255,因此极为白。但在图像底部仍能辨认出奶牛。

不必对全部三个通道都这样做,也可以只对一个颜色通道操作,但这里这样做会产生大量截图 ;)

减法

减法的工作方式类似,但现在需要将负数结果截断为 0。

    result[y * w + x].r = max(image2[y * w + x].r - image1[y * w + x].r, 0);
    result[y * w + x].g = max(image2[y * w + x].g - image1[y * w + x].g, 0);
    result[y * w + x].b = max(image2[y * w + x].b - image1[y * w + x].b, 0);



奶牛图片被从马的图片中减去,因此奶牛图片的颜色变为负值,这解释了偏紫的颜色。明亮的天空被减去后,花卉图片的整个上部变成纯黑。

减法的顺序很重要,若改为第三张减第二张,则得到如下结果:


差值

差值与减法几乎相同,只是不截断负值,而是取其绝对值。这样得到的是两张图像颜色之间的差值。

    result[y * w + x].r = abs(image1[y * w + x].r - image2[y * w + x].r);
    result[y * w + x].g = abs(image1[y * w + x].g - image2[y * w + x].g);
    result[y * w + x].b = abs(image1[y * w + x].b - image2[y * w + x].b);



现在一张图像中黑色的地方被另一张图像的颜色填充了。

乘法

乘法不能直接将 0-255 的颜色分量值相乘,否则最大值会是 255 × 255 = 65025,这么大的颜色值没什么用。应该将值转换为 0 到 1 之间的浮点数再相乘,结果也始终在 0 到 1 之间。乘法完成后再乘以 255。

    result[y * w + x].r = int(255 * (image2[y * w + x].r / 255.0 * image1[y * w + x].r / 255.0));
    result[y * w + x].g = int(255 * (image2[y * w + x].g / 255.0 * image1[y * w + x].g / 255.0));
    result[y * w + x].b = int(255 * (image2[y * w + x].b / 255.0 * image1[y * w + x].b / 255.0));

以下是奶牛图像与马的图像相乘的结果:



奶牛图像明亮的天空可以看作值 1,1 乘以马的图像保留了马,因此上部看起来与原始马的图像几乎相同。下部是两个较暗部分的乘积,变得更暗了。

平均

平均值通过将两张图像相加后除以 2 得到。

    result[y * w + x].r = (image1[y * w + x].r + image2[y * w + x].r) / 2;
    result[y * w + x].g = (image1[y * w + x].g + image2[y * w + x].g) / 2;
    result[y * w + x].b = (image1[y * w + x].b + image2[y * w + x].b) / 2;



交叉淡化

交叉淡化可以通过加权平均实现:起初第一张图像权重高、第二张权重低,随着时间推移,第二张图像的权重增加而第一张减少,产生漂亮的淡化效果。以下是第一张图像权重为 0.75、第二张为 0.25 的示例:

    result[y * w + x].r = int(image1[y * w + x].r * 0.75 + image2[y * w + x].r * 0.25);
    result[y * w + x].g = int(image1[y * w + x].g * 0.75 + image2[y * w + x].g * 0.25);
    result[y * w + x].b = int(image1[y * w + x].b * 0.75 + image2[y * w + x].b * 0.25);



现在马的图像比奶牛图像更清晰可见。

要实现真正的交叉淡化,需要修改主函数,因为结果需要每帧重绘并考虑时间。为此添加一个新的 while 循环,第一张图像的权重从 0 到 1,第二张图像用"1 - weight"从 1 到 0。

权重本身通常从 0 到 1 线性变化,但这里做了一些特殊处理:将其计算为时间的余弦值,这意味着图像会不断从第一张淡化到第二张,再回到第一张,如此循环……由于余弦给出 -1 到 +1 之间的值,而我们需要 0 到 1 的值,因此加 1 后除以 2。

int main(int argc, char *argv[])
{
  unsigned long w = 0, h = 0;
  //declare image buffers
  std::vector<ColorRGB> image1, image2, image3, result;

  //load the images into the buffers. This assumes all have the same size.
  loadImage(image1, w, h, "pics/photo1.png");
  loadImage(image2, w, h, "pics/photo2.png");
  loadImage(image3, w, h, "pics/photo3.png");
  result.resize(w * h);

  //set up the screen
  screen(w,h,0, "Image Arithmetic");

  float weight;

  while(!done())
  {
    weight = (1.0 + cos(getTicks() / 1000.0)) / 2.0;

    //do the image arithmetic
    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      result[y * w + x].r = int(image1[y * w + x].r * weight + image2[y * w + x].r * (1 - weight));
      result[y * w + x].g = int(image1[y * w + x].g * weight + image2[y * w + x].g * (1 - weight));
      result[y * w + x].b = int(image1[y * w + x].b * weight + image2[y * w + x].b * (1 - weight));
    }

    //draw the result buffer to the screen
    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      pset(x, y, result[y * w + x]);
    }

    //redraw
    redraw();
  }
}

以下是几帧截图:



最小值与最大值

这涉及只取像素中最低或最高的值,例如取两者的最小值:

    result[y * w + x].r = min(image1[y * w + x].r, image2[y * w + x].r);
    result[y * w + x].g = min(image1[y * w + x].g, image2[y * w + x].g);
    result[y * w + x].b = min(image1[y * w + x].b, image2[y * w + x].b);

结果非常直观:



由于奶牛图像的天空非常明亮,较暗的马的图像在那里胜出。但在底部,马的图像比奶牛更亮,所以奶牛在那里胜出。因此这个操作恰好过滤掉了奶牛的天空,用马来代替。

取最大值则相反:

    result[y * w + x].r = max(image1[y * w + x].r, image2[y * w + x].r);
    result[y * w + x].g = max(image1[y * w + x].g, image2[y * w + x].g);
    result[y * w + x].b = max(image1[y * w + x].b, image2[y * w + x].b);



振幅


振幅通过对两个对应颜色通道使用振幅公式来计算,公式为 sqrt(x² + y²)。由于结果可能比 255 大 1.41 倍,最后除以 1.41 = sqrt(2.0)。还需要进行各种 double 和 int 之间的转换,因为当前 gcc 编译器的标准 sqrt 函数对整数会报歧义错误而非自动转换。

    result[y * w + x].r = int(sqrt(double(image1[y * w + x].r * image1[y * w + x].r + image2[y * w + x].r * image2[y * w + x].r)) / sqrt(2.0));
    result[y * w + x].g = int(sqrt(double(image1[y * w + x].g * image1[y * w + x].g + image2[y * w + x].g * image2[y * w + x].g)) / sqrt(2.0));
    result[y * w + x].b = int(sqrt(double(image1[y * w + x].b * image1[y * w + x].b + image2[y * w + x].b * image2[y * w + x].b)) / sqrt(2.0));



AND、OR 与 XOR

现在我们对颜色值的二进制值应用 "&"、"|" 和 "^" 运算符。在普通照片上的结果相当……难看。

以下是 AND 运算符:

    result[y * w + x].r = image1[y * w + x].r & image2[y * w + x].r;
    result[y * w + x].g = image1[y * w + x].g & image2[y * w + x].g;
    result[y * w + x].b = image1[y * w + x].b & image2[y * w + x].b;



或者你更喜欢 OR?

    result[y * w + x].r = image1[y * w + x].r | image2[y * w + x].r;
    result[y * w + x].g = image1[y * w + x].g | image2[y * w + x].g;
    result[y * w + x].b = image1[y * w + x].b | image2[y * w + x].b;



XOR 运算符的结果也不是立即就好看的:

    result[y * w + x].r = image1[y * w + x].r ^ image2[y * w + x].r;
    result[y * w + x].g = image1[y * w + x].g ^ image2[y * w + x].g;
    result[y * w + x].b = image1[y * w + x].b ^ image2[y * w + x].b;



你永远不知道什么时候会用到它,也许可以用于图像加密 ;)


最后编辑:2004 年

版权所有 (c) 2004-2007 Lode Vandevenne,保留所有权利。