Learning OpenCV with iOS:掩膜操作

一、前言

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

增强对比度

二、开胃菜-Mat对象

Mat

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

Mat基础操作

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

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

image.create(480, 640, CV_8UC3);

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

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

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

uchar* ptr = image.ptr(row);

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

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

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

抠下铠的头

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

    // 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区域变成了白色。

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

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

src.copyTo(dst, mask);

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

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

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

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

mask.png

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

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

增强对比度

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

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

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

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

kernel

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

kernel

具体的代码如下:

    // 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之间。
*/

效果


增强对比度

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

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

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

demo1
demo2

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

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

    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搞定!定义如下:

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!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容