Learning OpenCV with iOS:掩膜操作

前言

上一篇我们简单讲解了 OpenCV 的概念和基础架构。本篇主要向大家介绍下图像处理中一个比较重要的概念 – 掩膜操作。开始前我们先看下利用矩阵掩膜操作来加强图像对比度的效果。

开胃菜-Mat 对象

我们用眼睛看到的是图像,而计算机却不认识。于是,人们使用数值的形式来记录图像,比如用 RGB 值记录图像的每个点,以此来表示图像。就如上图,我们看到的是一辆车,而计算机“看到”的是一个包含图像值的矩阵。OpenCV 的 Mat 对象对应的就是矩阵。Mat 提供了许多便捷的 API 来创建、操作矩阵。

Mat 基础操作

1
Mat image = Mat(240, 320, CV_8UC3);

第一个参数是 rows,该矩阵的行数;第二个参数是 cols,该矩阵的列数;第三个参数是该矩阵元素的类型。这句话表示创建一个大小为 240×320 的矩阵,里面的元素为 8 位 unsigned 型,通道数(channel)有 3 个。

1
image.create(480, 640, CV_8UC3);

分配(或重新分配)image 矩阵,把大小设为 480×640,类型设为 CV8UC3。

1
Mat image = Mat(3, 3, CV_32F, Scalar(5));

定义并初始化一个 3×3 的 32bit 浮点数矩阵,每个元素都设为 5。

1
uchar* ptr = image.ptr(row);

指针操作,表示拿到 image 第 row 行的指针

1
2
uchar* output = image.ptr(row);
output[1] = value;

利用指针修改图像,表示修改 image 第 row 行的第 2 个数据为 value。

Mat 常用成员介绍

1、data

Mat 对象中的一个指针,指向存放矩阵数据的内存(uchar* data)

2、dims

矩阵的维度,34 的矩阵维度为 2 维,34*5 的矩阵维度为 3 维

3、channels

矩阵通道,矩阵中的每一个矩阵元素拥有的值的个数,比如说 3 * 4 矩阵中一共 12 个元素,如果每个元素有三个值,那么就说这个矩阵是 3 通道的,即 channels = 3。常见的是一张彩色图片有红、绿、蓝三个通道。

4、rows

矩阵的行数

5、cols

矩阵的列数

Mat 与 IplImage

OpenCV1 使用基于 C 接口定义的图像存储格式 IplImage 存储图像。IplImage 直接暴露内存,如果忘记释放内存,就会造成内存泄漏。

从 OpenCV2 开始,开始使用 Mat 类存储图像,具有以下优势:

  • 图像的内存分配和释放由 Mat 类自动管理

  • Mat 类由两部分数据组成:矩阵头(包含矩阵尺寸、存储方法、存储地址等)和一个指向存储所有像素值的矩阵(根据所选存储方法的不同,矩阵可以是不同的维数)的指针。Mat 在进行赋值和拷贝时,只复制矩阵头,而不复制矩阵,提高效率。如果矩阵属于多个 Mat 对象,则通过引用计数来判断,当最后一个使用它的对象,则负责释放矩阵。

  • 可以使用 clone 和 copyTo 函数,不仅复制矩阵头还复制矩阵。

掩膜操作

数字图像处理中的掩膜的概念是借鉴于 PCB 制版的过程,在半导体制造中,许多芯片工艺步骤采用光刻技术,用于这些步骤的图形“底片”称为掩膜(也称作“掩模”),其作用是:在硅片上选定的区域中对一个不透明的图形模板遮盖,继而下面的腐蚀或扩散将只影响选定的区域以外的区域。 图像掩膜与其类似,用选定的图像、图形或物体,对处理的图像(全部或局部)进行遮挡,来控制图像处理的区域或处理过程。 光学图像处理中,掩模可以是胶片、滤光片等。数字图像处理中,掩模为二维矩阵数组,有时也用多值图像。

是不是概念看得一头雾水,没事的,我第一次看概念的也是一样样的。下面我以例子来辅助大家了解掩膜。

抠下铠的头

接下来我们以代码角度分析下究竟什么是掩膜。

1
2
3
4
5
6
7
8
9
10
    // image为铠的图片
    Mat src;
    UIImageToMat(image, src);

    Mat mask = Mat::zeros(src.size(), CV_8UC1);
    Rect2i r = Rect2i(120, 80, 100, 100);
    mask(r).setTo(255);

    Mat dst;
    src.copyTo(dst, mask);

第一步建立与原图一样大小的 mask 图像,并将所有像素初始化为 0,因此全图成了一张全黑色图。 第二步将 mask 图中的 r 区域的所有像素值设置为 255,也就是整个 r 区域变成了白色。

1
2
 Mat mask = Mat::zeros(src.size(), CV_8UC1);
 mask(r).setTo(255);

使用 mask 将原始图 src 拷贝到目的图 dst 上。

1
src.copyTo(dst, mask);

这个拷贝的动作完整版本是这样的:

原图(src)与掩膜(mask)进行与运算后得到了结果图(dst)。

其实就是原图中的每个像素和掩膜中的每个对应像素进行与运算。比如 1 & 1 = 1;1 & 0 = 0;

比如一个 3 _ 3 的图像与 3 _ 3 的掩膜进行运算,得到的结果图像就是:

所以,mask 就是位图,来过滤像素。如果 mask 像素的值是非 0 的,我就保留,否则就丢弃。

因为我们上面得到的 mask 中,感兴趣的区域是白色的,表明感兴趣区域的像素都是非 0,而非感兴趣区域都是黑色,表明那些区域的像素都是 0。一旦原图与 mask 图进行与运算后,得到的结果图只留下原始图感兴趣区域的图像了。也正剩下铠的头部了。

增强对比度

矩阵的掩膜操作就是根据掩膜来重新计算每个像素的像素值,掩膜(mask)也被称为 kernel。 通过掩膜操作实现图像对比度提高的公式如下。

1
I(i,j) = 5*I(i,j) - [I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]

注:这里看不懂不要紧,先看具体的实现,回头我们再一起回顾这里。

上面的公式,转换成矩阵就如下图所示

红色是中心像素,从上到下,从左到右对每个像素做同样的处理操作,具体过程如下图,深灰色底表示原图像,每次移动 kernel 便根据公司计算新值并更新矩阵。最终得到的结果就是对比度提高之后的输出图像。

具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    // image为铠的图片
    Mat src;
    UIImageToMat(image, src);

    int cols = (src.cols-1) * src.channels();
    int offset = src.channels();
    int rows = src.rows;

    Mat dst = Mat(src.size(), src.type());
    for (int row = 1; row < rows-1; row++) {
        uchar* previous = src.ptr(row-1);
        uchar* current = src.ptr(row);
        uchar* next = src.ptr(row+1);
        uchar* output = dst.ptr(row);
        for (int col = offset; col < cols; col++) {
            output[col] = saturate_cast<uchar>(5*current[col] - (current[col-offset] + current[col +offset] + previous[col] + next[col]));
        }
    }

/*
注:
saturate_cast<uchar>(-100),返回0
saturate_cast<uchar>(288),返回255
saturate_cast<uchar>(100),返回100
这个函数的功能是确保RGB值范围在0~255之间。
*/

效果

接下来我们来回顾下上面的那个公式

1
I(i,j) = 5*I(i,j) - [I(i-1,j) + I(i+1,j) + I(i,j-1) + I(i,j+1)]

其实这个公式就是 5 倍的中心像素减去周边的四个像素之和。 我们举两个例子来看下这个公式的结果。

我们可以大致看到若是中心点的值大于周围,则计算后的结果会将中心点与周围的值差距拉得更大; 若是中心点的值小于周围,则计算后的结果也会将中心点与周围的值差距拉大。这样“大的大,小的小”结果不就是对比明显了吗,也就是提高了对比度。

大家会发现这样做掩膜操作也太麻烦了。这个时候我们就找 OpenCV 来帮个忙,看看它是怎么实现的。

1
2
3
4
5
6
    Mat src;
    UIImageToMat(image, src);

    Mat dst;
    Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
    filter2D(src, dst, src.depth(), kernel);

一个 filter2D 搞定!定义如下:

1
2
3
void filter2D( InputArray src, OutputArray dst, int ddepth,
                            InputArray kernel, Point anchor=Point(-1,-1),
                            double delta=0, int borderType=BORDER_DEFAULT );

其中 src 与 dst 是 Mat 类型变量、depth 表示位图深度,有 32、24、8 等。

小结

本篇主要介绍了 Mat 对象的基本用法,并通过两个例子讲解了掩膜操作的原理和实现。下一篇还是会以这样的形式讲解 OpenCV 的其他知识,有更好建议的朋友可以给我留言,see you later!

CatchZeng
Written by CatchZeng Follow
AI (Machine Learning) and DevOps enthusiast.