iOS修改图片颜色(修改像素色值)

假设这样一个场景:一张图片中有一朵白花,我们想要把它变成红花;或者一张图片中有一段黑色的文字,我们想要把它变成红色,应该怎么做?

想要实现这个需求,就需要从像素尺度上对图片进行修改,将指定区域内的像素的色值改为我们需要的颜色。但是,如何从这张图上找到那段文字或者那朵花,并不在本文的讨论范围内,那是OCR和机器学期的事ㄟ( ▔, ▔ )ㄏ。

进入正题

假设我们要把一张有一段黑色文字的图片中的文字修改为红色:


示例图片
修改后

要实现这个需求,我们应该怎么做?

  1. 创建一个画布,并将原始图片平铺在画布上
  2. 遍历图片上的像素,找到目标区域内的黑色文字的像素,将它改为红色
  3. 输出修改后的图片,并清理内存

我们需要哪些信息才足够实现这个功能?

  1. 一个Rect:需要修改这张图片上哪个区域的像素
  2. 需要被修改的色值区域:需要把哪个色值范围内的像素修改为目标颜色
  3. 目标颜色:需要将符合上述两点的像素修改为什么颜色

具体实现

在贴代码之前,先讲一些废话:

  • 图片的分辨率代表着它的像素个数,比如上图的分辨率为1054 * 316,那么它的像素个数就是 1054 * 316 = 333064;
  • 图片的宽度代表这张图一共有多少列像素,高度代表一共有多少行像素;即宽度代表列数,高度代表行数
  • 在像素尺度上,图片中元素边缘的颜色并不如我们肉眼看到的那样。比如上图中的文字是纯黑色的,但是如果你放大放大再放大,会发现文字边缘的颜色其实是灰色的(这也是上面为什么说需要一个色值区域的原因);
  • 图片转为2进制的数据时,每个像素为最小单元,从左上角开始,到右下角结束,从左到右从上到下排列像素,但它并不是二维,而是一维的。
  • alpha通道:一个像素的色值是由RGBA四个值确定的。如果不含alpha通道的话,则是由RGB三个值确定,而A则一直是0xFF,即RGBX(X代表不含alpha通道,X一直为0xFF)

下面就是实现这个功能的核心代码了,这里是作为UIImage的一个category方法实现的:

/** 
解释一下前两个参数的含义:
想象一个数轴,最左边是黑色(RGBX:0x000000FF),最右边是白色(0xFFFFFFFF),
nearBlackColor是靠近左边边界的色值,nearWhiteColor是靠近右边边界的色值,
它们中间则是需要被修改的色值范围 
*/
- (UIImage *)translatePixelColorByTargetNearBlackColorRGBA:(UInt32)nearBlackRGBA
                                        nearWhiteColorRGBA:(UInt32)nearWhiteRGBA
                                            transColorRGBA:(UInt32)transRGBA
                                                   inRect:(CGRect)rect {
    // 第一步:判断传入的rect是否在图片的bounds内
    CGRect canvas = CGRectMake(0, 0, self.size.width, self.size.height);
    if (!CGRectContainsRect(canvas, rect)) {
        if (CGRectIntersectsRect(canvas, rect)) {
            rect = CGRectIntersection(canvas, rect);    // 取交集
        } else {
            return self;
        }
    }
    
    
    UIImage *transImage = nil;
    
    int imageWidth = self.size.width;
    int imageHeight = self.size.height;
    
    // 第二步:创建色彩空间、画布上下文,并将图片以bitmap(不含alpha通道)的方式画在画布上。
    size_t bytesPerRow = imageWidth * 4;
    uint32_t *rgbImageBuf = (uint32_t *)malloc(bytesPerRow * imageHeight);
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    
    CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,
                                                 kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
    
    CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), self.CGImage);
    
    // 第三步:遍历并修改像素
    uint32_t *pCurPtr = rgbImageBuf;
    pCurPtr += (long)(rect.origin.y*imageWidth);    // 将指针移动到初始行的起始位置
    
    // 空间复杂度:O(rect.size.width * rect.size.height)
    for (int i = rect.origin.y; i < CGRectGetMaxY(rect); i++) {                     // row
        pCurPtr += (long)rect.origin.x;             // 将指针移动到当前行的起始列
        
        for (int j = rect.origin.x; j < CGRectGetMaxX(rect); j++, pCurPtr++) {      // column
            if (*pCurPtr < nearBlackRGBA || *pCurPtr > nearWhiteRGBA) { continue; }
            
            // 将图片转成想要的颜色
            uint8_t *ptr = (uint8_t *)pCurPtr;
            ptr[3] = (transRGBA >> 24) & 0xFF;              // R
            ptr[2] = (transRGBA >> 16) & 0xFF;              // G
            ptr[1] = (transRGBA >> 8)  & 0xFF;              // B
        }
        
        pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));    // 将指针移动到下一行的起始列
    }
    
    
    // 第四步:输出图片
    CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight, providerReleaseDataCallback);
    CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace,
                                        kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider,
                                        NULL, true, kCGRenderingIntentDefault);
    CGDataProviderRelease(dataProvider);
    transImage = [UIImage imageWithCGImage:imageRef];
    
    // end:清理空间
    CGImageRelease(imageRef);
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    
    return transImage ? : self;
}

void providerReleaseDataCallback (void *info, const void *data, size_t size) {
    free((void*)data);
}

怎么调用呢?

[image translatePixelColorByTargetNearBlackColorRGBA:0x000000FF nearWhiteColorRGBA:0x323232FF transColorRGBA:0xFF0000FF inRect:rect];

看起来有些麻烦是吗?色值要写那么长,而且既然是以不含alpha通道的方式实现的,那么alpha值便没有意义,所以我们还可以再封装几个方法以便使用起来更方便:

- (UIImage *)translatePixelColorByTargetNearBlackColor:(UIColor *)nearBlackColor
                                        nearWhiteColor:(UIColor *)nearWhiteColor
                                            transColor:(UIColor *)transColor {
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    return [self translatePixelColorByTargetNearBlackColor:nearBlackColor nearWhiteColor:nearWhiteColor transColor:transColor inRect:rect];
}

- (UIImage *)translatePixelColorByTargetNearBlackColor:(UIColor *)nearBlackColor
                                        nearWhiteColor:(UIColor *)nearWhiteColor
                                            transColor:(UIColor *)transColor
                                                inRect:(CGRect)rect {
    // UIColor 转 RGBA
    UInt32 nearBlackRGBA = nearBlackColor.RGBA;
    UInt32 nearWhiteRGBA = nearWhiteColor.RGBA;
    UInt32 transRGBA = transColor.RGBA;

    return [self translatePixelColorByTargetNearBlackColorRGBA:nearBlackRGBA nearWhiteColorRGBA:nearWhiteRGBA transColorRGBA:transRGBA inRect:rect];
}


- (UIImage *)translatePixelColorByTargetNearBlackColorHex:(UInt32)nearBlackRGB
                                        nearWhiteColorHex:(UInt32)nearWhiteRGB
                                            transColorHex:(UInt32)transRGB {
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    return [self translatePixelColorByTargetNearBlackColorHex:nearBlackRGB nearWhiteColorHex:nearWhiteRGB transColorHex:transRGB inRect:rect];
}


- (UIImage *)translatePixelColorByTargetNearBlackColorHex:(UInt32)nearBlackRGB
                                        nearWhiteColorHex:(UInt32)nearWhiteRGB
                                            transColorHex:(UInt32)transRGB
                                                   inRect:(CGRect)rect {
    // RGB 转 RGBA
    UInt32 nearBlackRGBA = (nearBlackRGB << 8) + 0xFF;
    UInt32 nearWhiteRGBA = (nearWhiteRGB << 8) + 0xFF;
    UInt32 transRGBA = (transRGB << 8) + 0xFF;
    
    return [self translatePixelColorByTargetNearBlackColorRGBA:nearBlackRGBA nearWhiteColorRGBA:nearWhiteRGBA transColorRGBA:transRGBA inRect:rect];
}

另外,这是上面使用到的UIColor转RGBA的方法,它是作为UIColor的category方法实现的:

- (UInt32)RGBA {
    CGFloat red = 0;
    CGFloat green = 0;
    CGFloat blue = 0;
    CGFloat alpha = 0;
    
    BOOL succ = [self getRed:&red green:&green blue:&blue alpha:&alpha];
    
    UInt32 r = round(red*255);
    UInt32 g = round(green*255);
    UInt32 b = round(blue*255);
    UInt32 a = round(alpha*255);

    r = (r << 24);
    g = (g << 16);
    b = (b << 8);
    
    UInt32 rgba = r + g + b + a;
    return succ ? rgba : 0x00000000;
}

如果上述正好能符合你目前遇到的问题,而你又急于验证能否解决问题的话,把上面的代码copy一下就可以了。如果你既想知其然,又想知其所以然,那么我们继续。

上述核心代码中分四步实现了修改图片像素色值,其中第一、二、四没有什么可说的,都是固定代码。
但第三步的算法我认为有必要解释一下,所以有了下面这些内容。
当然,如果你已经从代码中看明白了,那么我可以负责任的告诉你,本文已经结束啦~!
如果你觉得有些懵哔,那太好了!我又可以继续讲(zhuang)解(bi)了!那么,来嘛客官,咱们继续~

首先先来看下面一张图:


像素矩阵示例

前面已经说过了,我们采用不含alpha通道的方式实现。那么一个像素就是由RGBX四个值确定,其中X是无效的。这是上图中“Pixel”想要表示的含义。
“Image Raw Data”想要表示的是,图片在转为2进制后,像素在其中是怎样排列的。其中的数字表示的是像素在整张图片中的索引。前面也说过,是由一个二维的图片像素矩阵(就是上图最后那个4*4的“Image Pixel Matrix”)从左到右从上到下转换成的一维队列。

可以看出,在二维的图片上,我们需要修改的区域是连续的一块,但是在转化为二进制的数据中,它们则是断续的。
我把上面的那段代码再贴一下,以便对照解释:

// 第三步:遍历并修改像素
    uint32_t *pCurPtr = rgbImageBuf;
    pCurPtr += (long)(rect.origin.y*imageWidth);    // 将指针移动到初始行的起始位置
    
    // 空间复杂度:O(rect.size.width * rect.size.height)
    for (int i = rect.origin.y; i < CGRectGetMaxY(rect); i++) {                     // row
        pCurPtr += (long)rect.origin.x;             // 将指针移动到当前行的起始列
        
        for (int j = rect.origin.x; j < CGRectGetMaxX(rect); j++, pCurPtr++) {      // column
            if (*pCurPtr < nearBlackRGBA || *pCurPtr > nearWhiteRGBA) { continue; }
            
            // 将图片转成想要的颜色
            uint8_t *ptr = (uint8_t *)pCurPtr;
            ptr[3] = (transRGBA >> 24) & 0xFF;              // R
            ptr[2] = (transRGBA >> 16) & 0xFF;              // G
            ptr[1] = (transRGBA >> 8)  & 0xFF;              // B
        }
        
        pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));    // 将指针移动到下一行的起始列
    }

所以按上图所示,整张图片的bounds为(0, 0, 4, 4),我们需要修改rect(1, 1, 2, 2)内的像素色值。下面所讲要学会自动脑补二维图片转换一维二进制数据,凡是指出坐标的都是二维图片,而说指针的都是在说一维的二进制数据中某个像素的指针。

  1. 我们的空间复杂度为O(rect.size.width * rect.size.height),所以遍历时第一层的for循环遍历次数为rect.size.width(即2),而i是从rect.origin.y(即1)开始的;第二层for循环的遍历次数为rect.size.height(也是2),而j是从rect.origin.x(即1)开始的。总之,我们是从point(1, 1)位置开始遍历的。
  2. 首先需要将指针移动到初始行的起始列:pCurPtr += (long)(rect.origin.y*imageWidth);,即像素4的所在的位置。目的是为了跳过目标区域上方的无关行。
  3. 只跳过了上面的无关行还不够,我们还需要跳过左边的无关列,即pCurPtr += (long)rect.origin.x;,这时候指针指到了像素5的位置(就是步骤1中所说point(1, 1)的位置),然后我们就可以开始真正的遍历了。
  4. 在遍历完这一行的目标区域后,指针指到了像素7的位置;然后还需要跳过右边的无关列pCurPtr += (long)(imageWidth - CGRectGetMaxX(rect));,这时候指针指到了像素8的位置。此时这一行已经完全遍历结束,跳到了下一行的起始位置,又回到了步骤3的状态(只是row+1了)
  5. 然后重复执行3、4步骤,直到i >= CGRectGetMaxY(rect)结束

至此,这个算法解释完毕~

唉~,这一块我也是想破头该怎么描述,可是写出来发现还是不太理想。。。
我只能祈祷我太低估读者的水平,其实大家都是能直接看懂代码的,根本不需要我解释ㄟ( ▔, ▔ )ㄏ。
如果大家看完之后还是有不理解的地方;还有一些我没详细解释的地方,如果有不理解的,都欢迎在留言区讨论。
本人作为写文章的新手,如果有错误的地方,也欢迎大家在留言区指正!

最后,这里是Demo地址

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,027评论 8 265
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 走过黑色的高中生活,我来到了心心念念的大学校园,对于我的大学生活,我感慨万千,所以我决定用文字把它记录下来。 我是...
    樾樾_7fdb阅读 278评论 0 0
  • 最近无意中在闺蜜的微博中看到了“简书”这个平台,于是立马下载过来,不为别的,只想能够在一个没有人认识我的平台畅所欲...
    楠木也会失去方向阅读 242评论 2 2