图像处理中一些稍底层的东西:色彩空间与数据结构

引言

接触图像处理也有两年了,如果算上大二时H老师的课,甚至有三年多了。我真的很想“上岸”,而不是流于表面,只学到花拳绣腿。

色彩空间是最基础的概念,当然谈不上“底层”。但和色彩空间相关联的、图像的像素点是以和何种方式储存的,换句话说,数字图像的数据结构是怎样的,应当称得上是从表面到深入的一个转折点了。学数字图像处理,当然不能满足于output = intput.filter()这样的模式。

引言写于2020.1.3

数据结构

修改于2020.1.2

二维图像与一维数组

在AviSynth和VapourSynth中,单帧图像的像素按行储存,从图像的左上开始,一行一行地存储于一维数组中,像下面这样。

——————————>
——————————>
……
——————————>

所以读取/写入下一个像素,依据行列的不同,分两种情况,大概是这样

1
2
3
4
5
// 对于YUV格式图像
const T* srcp = reinterpret_cast<const T*>(vsapi->getReadPtr(src, plane)); // 创建指针
int src_stride = vsapi->getStride(src, plane) / sizeof(T); // 获取stride
srcp++; // 读取同行下一个像素
srcp += src_stride; // 读取同列下一个元素

至于stride,在avs中习惯写作pitch。详情看下面,大概两个星期前写的(当时还处于囫囵吞枣的模式)。

关于stridepitch

继续读VapourSynth滤镜的代码,这次看Deblock(),和相应的AviSynth代码一起看的。这种同为C++语言,但调用了不同的API,比之前使用同一个API完成从C到C++的迁移,又是另一种感觉。

但直观上来看,使用AviSynth API,感觉代码要短一些…

回到正题,不仅是在vs和avs框架下,在更一般的图像处理中,经常能够看到带有pitch的变量。

之前就查过,说是在处理图像时,会以4为单位存储,以提高硬件效率(若以3等比4更小的方式存储,反而会降低效率),所以会用pitch变量实现上述存储方式。

云里雾里地,不知道在说什么。这次看Deblock()的代码,因为涉及了判断块是否要被处理,类似下面的语句。

1
2
3
4
5
6
7
T * VS_RESTRICT sq0 = dstp1;

if (std::abs(sq0[i] - sq[i]) < alpha) {
...
}

srcp += src_pitch

看了上面这样的代码,似乎明白很多了,带有pitch的变量就是和内存/储存相关的(“单位内存”),srcp(或dstp)加上一个“单位内存”(并放到for循环中),就是这里已经处理过了,到下一个单位去。

虽然我上面糊里糊涂说了一堆,可能我自己也不明白,但就代码部分而言,真的让我有种《21天实战Caffe》里提到的“阅读源码自由”的感觉。

上面便是两个星期前写的,虽然云里雾里,但应该是说对了意思。然后翻了翻前两天写的记一次从AviSynth到VapourSynth的迁移——感受不同的API(二),理解上又近了一步,但还是没说完全。那篇博文中只说了以下四者单位已经过转换,可以直接在数值上比较。

  • VapourSynth中除过sizeof(T)stride
  • AviSynth中默认来自8bit图像的pitch
  • VapourSynth中一般意义上的width
  • AviSynth中由GetRowSize()得到的width

但我当时没明白一点,其实width和除过sizeof(T)stride(包括pitch),在数值上是近似相等的。stride直译过来是跨度,而基于“二维图像按行储存于一维数组”的数据结构,所谓跨度,就是宽度,所谓内存上的跨度,就是乘上变量类型所占字节的宽度。

一开始在读avs版AreaResize源码时,看到dstp += target_width惊呆了,心想还能直接加上宽度。其实,我更早之前遇见的dstp += dst_stridedstp[y + x * dst_stride],和直接加宽度都是差不多的,都是为了将指针移动到同列的下一个元素。

顺带,补充一下C++的基础知识。指针的加法,如

1
2
3
double point[5];
double* p = point;
p += 2;

等于指针移动sizeof(double)*2个单位。

同时,这个例子还说明了,可以将数组名赋值给指针,其指向数组的第一个元素。

2020.1.3补充:其实这只是YUV格式的一种储存方式,是我在VapourSynth与AviSynth中常见的方式。但其实还有其他的方式,顺带整理RGB的储存方式。在这之前,需要先补充一些基础概念,恰好最初写这篇Blog时,就写了这些概念。

一些基础概念

本以为我摸爬滚打这么长时间,就算不太懂代码,一些基础知识总该没问题吧…但是…关于色彩空间,有些别名还是记不住。

1
2
3
4
5
6
YV24  = YUV444
YV16 = YUV422
YV12 = YUV420
YV411 = YUV411
RGB24 = 8bit
RGB32 = 8bit(含透明通道)

顺带一提,在vs中,由一般的10bit,会得到RGB30,一般的16bit,则得到RGB48。

继续数据结构

概述与补充

无论是YUV还是RGB格式,都有两类存储方式,planar和packed,后者在我目前所接触到的代码中,更习惯写作interleave。

planar直译是平面,就是我所习惯的平面的概念,Y、U、V、R、G、B各作为一个平面。先把一个平面的数据/像素点全存储下来,再去存储下个平面的。我在前面小节提到的数据结构,便属于这类。

packed/interleave,我肯定是对interleave更熟悉,这个单词对应着另一个图像处理的概念,隔行。当然这里并不是讨论逐行隔行的问题。packed/interleave表示同一个像素点不同平面的数据连续存储,这就导致不同平面的数据是间隔存储的,interleave便取此意。

更详细的内容,随手一搜就有丰富的资料,比如图文详解YUV420数据格式。我这里写一下这几天写代码时,遇到相关问题。YUV格式已经在上面小节说过了,补充一句的就是,我所熟悉的YUV420P的p,应该不是process,而是planar。

RGB格式

下面写一下RGB格式。

在avs中,似乎是RGB interleave更推崇一点,似乎avs内部用的就是这种方式?因为我在一份plugin源码中没看到额外的处理。而与之对比的是,我在读vs的waifu2x-caffe源码时,发现RGB输入被手动转换成了interleave形式。

另外我还需知道一个问题:在OpenCV中RGB格式是按BGR顺序存储数据的;avs似乎也是这样,或者说习惯是这样;而vs我还没搞明白,直观上觉得还是用的RGB顺序。

一个直观的问题

在缩小时,插值算法做了什么

可以看一下这个回答

无论是放大还是缩小,正在进行的“插值”实际上都是在重新采样。