美文网首页iOS高级文章
iOS修改图片颜色(修改像素色值)

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

作者: 乌鸢 | 来源:发表于2018-09-04 17:20 被阅读3227次

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

    想要实现这个需求,就需要从像素尺度上对图片进行修改,将指定区域内的像素的色值改为我们需要的颜色。但是,如何从这张图上找到那段文字或者那朵花,并不在本文的讨论范围内,那是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地址

    相关文章

      网友评论

        本文标题:iOS修改图片颜色(修改像素色值)

        本文链接:https://www.haomeiwen.com/subject/yghnwftx.html