转自:https://www.jianshu.com/p/619ef8423895

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

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

转自:https://www.jianshu.com/p/619ef8423895 假设这样一个场景:一张图片中有一朵白花,我们想要把它变成红花;或者一张图片中有一段黑色的文字,我们想要把它变成红色,应该怎么做?想要实现这个需求,就需要从像素尺度上对图片进行修改,将指定区域内的像素的色值改为我们需要的颜色。但是,如何从这张图上找到那段文字或者那朵花,并不在本文的讨论范围内,那是OCR和机...
实际项目场景:去除图片的纯白色背景图,获得一张透明底图片用于拼图功能 介绍两种途径的三种处理方式(不知道为啥想起了孔乙己),具体性能鶸并未对比,如果有大佬能告知,不胜感激。 Core Image Core Graphics/Quarz 2D Core Image Core Image是一个很强大的框架。它可以让你简单地应用各种滤镜来处理图像,比如修改鲜艳程度,色泽,或者曝光。 它利用GPU(或者CPU)来非常快速、甚至实时地处理图像数据和视频的帧。并且隐藏了底层图形处理的所有细节,通过提供的API就能简单的使用了,无须关心OpenGL或者OpenGL ES是如何充分利用GPU的能力的,也不需要
self.view.backgroundColor = [UIColor blackColor]; MyButton *button = [[MyButton alloc] initWithFrame:CGRectMake(55, 55, 100, 50)]; [button s
1.滤镜链 在一个复合滤镜中,多种滤镜效果处理时,通常都是图片 -> 设置顶点/纹理坐标 -> 滤镜效果处理 -> 帧缓冲区 -> 新的纹理 -> 滤镜效果处理 -> 帧缓冲区 -> 新的纹理。 此过程就是滤镜链。 思考:那如何将滤镜处理完之后存到帧缓冲区,并生成纹理的呢? 2.滤镜处理 -> 帧缓冲区 -> 新的纹理 -> 相册 2.1 步骤 根据屏幕上的显示,重新获取顶点坐标和纹理坐标,并生成新的纹理(将纹理加载到帧缓冲区中) - 根据屏幕
IOS 开发中,有时候需要获取图像中某个像素点的颜色,返回 UIColor 值。网上收集资料,参考各种方案,最后总结如下:- (UIColor *)colorAtPixel:(CGPoint)point { // 如果点超出图像范围,则退出 if (!CGRectContainsPoint(CGRectMake(0.0f, 0.0f, self.size.width, self.
有时候,我们通常会有修改图片底色,或者让底色透明,也或者颜色交换的类似的需求,特别是我们做界面或类似渲染等功能时可能需求更特殊一些。 我们可能清楚像素如何修改,但是使用常用的mspaint又不太好修改,这时可能我们想写个程序来修改,不过用qt/mfc等写还是太重型了,还需要打开vs,编译运行调试,比较麻烦。 用js就比较简单了,打开chrome浏览器的开发者工具的console窗口,代码写进来就可
在做项目的时候碰到了一个需求,把app的主题颜色改变成另外一种颜色,由于没有给出新的素材图片,需要把现有图片颜色改变一下。下面是改变图片颜色的方法(一般对于单一颜色图片) imageWithRenderingMode: UIImage有一个方法imageWithRenderingMode:,通过它设置图片的渲染模式,再通过设置tintcolor,就可以改变图片颜色。其中参数部分UIIMageR...
2. 你的图片太大,超过了服务器能够处理的大小限制。 3. 服务器端出了问题,导致无法正常接收上传的图片。 4. 你的 uniapp 客户端代码存在 bug,导致无法正常上传图片。 如果你不确定是哪个原因造成的上传失败,建议你尝试以下步骤来解决这个问题: 1. 确保你的网络环境良好。 2. 尝试将图片的大小压缩到合理的范围内再上传。 3. 尝试在不同的时间上传图片,如果总是在同一时间上传失败,可能是服务器端出了问题。 4. 检查你的 uniapp 客户端代码,确保没有任何 bug 影响了图片上传的过程。