介绍下常见的几种对图片的像素级操作 1.修改颜色 2. 颜色空间变化 3.lsb 隐藏信息 4. 颜色混合 5. 马赛克
介绍 在 iOS图片-位图信息与图片解码 中介绍了位图信息中像素 颜色空间等概念。这次来实践一下如何对图片的像素进行操作。位图又被叫做点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每个像素点的信息,我们就知道如何对图片进行像素级操作了。
将一张图片修改为蓝色 由于在iOS开发中使用的位图大部分是32位RGBA模式,所以我们先说下这种模式的简单图像处理。 首先我们需要知道什么是32位RGBA模式的位图。32位就表示一个这种模式位图的一个像素所占内存为32位,也就是4个字节的长度。R、G、B、A分别代表red,green,blue和alpha,也就是颜色组成的三原色与透明度值。RGBA每一个占用一个字节的内存。 知道了上面这些,我们就有了思路:通过改变每一个像素中的RGBA值来进行一些位图图像的处理了。
示例代码 效果如下:
将原图片 转换为蓝色图片
1 2 3 4 5 6 UIImage * sampleImg1 = [UIImage imageNamed:@"share.png" ];dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ UIImage * sample1ResultImg = [self .class sample1With:sampleImg1]; NSLog (@"示例1 将一张图片变为蓝色 END" ); });
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 + (UIImage *)sample1With:(UIImage *)originImg { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB (); NSUInteger bytesPerPixel = 4 ; NSUInteger bitsPerComponent = 8 ; UInt32 * inputPixels; CGImageRef inputCGImage = [originImg CGImage ]; NSUInteger inputWidth = CGImageGetWidth (inputCGImage); NSUInteger inputHeight = CGImageGetHeight (inputCGImage); NSUInteger inputBytesPerRow = bytesPerPixel * inputWidth; inputPixels = (UInt32 *)calloc(inputHeight * inputWidth, sizeof (UInt32 )); CGContextRef context = CGBitmapContextCreate (inputPixels, inputWidth, inputHeight, bitsPerComponent, inputBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextDrawImage (context, CGRectMake (0 , 0 , inputWidth, inputHeight), inputCGImage); for (int j = 0 ; j < inputHeight; j++) { for (int i = 0 ; i < inputWidth; i++) { UInt32 * currentPixel = inputPixels + (j * inputWidth) + i; UInt32 color = *currentPixel; UInt32 thisR,thisG,thisB,thisA; thisR=R(color); thisG=G(color); thisB=B(color); thisA=A(color); if (thisA){ thisR = 0 ; thisG = 0 ; thisB = 255 ; } *currentPixel = RGBAMake(thisR, thisG, thisB, thisA); } } CGImageRef newCGImage = CGBitmapContextCreateImage (context); UIImage * processedImage = [UIImage imageWithCGImage:newCGImage]; CGImageRelease (inputCGImage); CGColorSpaceRelease (colorSpace); CGContextRelease (context); free(inputPixels); return processedImage; }
像素格式色彩空间变为灰色 iOS 支持的组合不多 Supported Pixel Formats
这里尝试将像素格式色彩空间变为灰色 示例代码 效果如下:
将原图片 转换为灰色颜色空间图片
1 2 3 4 5 6 UIImage * sampleImg2 = [UIImage imageNamed:@"channel.png" ];dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ UIImage * sample1ResultImg = [self .class sample2With:sampleImg2]; NSLog (@"示例2 颜色空间变化 END" ); });
1 2 3 4 5 6 7 8 9 10 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray (); NSUInteger bytesPerPixel = 1 ; NSUInteger bitsPerComponent = 8 ; CGContextRef context = CGBitmapContextCreate (inputPixels, inputWidth, inputHeight, bitsPerComponent, inputBytesPerRow, colorSpace, kCGImageAlphaOnly);
LSB方式隐藏二维码到一张图片 LSB ,最低有效位,英文是Least Significant Bit 。我们知道图像像素一般是由RGB三原色(即红绿蓝)组成的,每一种颜色占用8位,0x00~0xFF,即一共有256种颜色,一共包含了256的3次方的颜色,颜色太多,而人的肉眼能区分的只有其中一小部分,这导致了当我们修改RGB颜色分量中最低的二进制位的时候,我们的肉眼是区分不出来的。
鉴于LSB想法,一张普通的二维码图片只有黑色和白色。那么对于RGBA的图片 可以修改R的最低位。0表示白色 1表示黑色,这样处理后肉眼看不出原图修改的痕迹。
示例代码 效果如下:
左图是原图 右图是经过LSB操作的图,肉眼完全看不出区别
1 2 3 4 5 6 7 8 9 10 11 12 UIImage * oriImg = [UIImage imageNamed:@"channel.png" ];UIImage * qrImg = [self .class createQRForAlreadySpliceString:@"一些信息" size:CGSizeMake (300 , 300 )];dispatch_async (dispatch_get_global_queue(0 , 0 ), ^{ UIImage * result = [self handleImage:oriImg hideQrImage:qrImg]; UIImage * qrResult = [self handleHideImage:result]; NSLog (@"end" ); });
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 - (UIImage *)handleImage:(UIImage *)originImg hideQrImage:(UIImage *)qrImg { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB (); NSUInteger bytesPerPixel = 4 ; NSUInteger bitsPerComponent = 8 ; UInt32 * inputPixels; UInt32 * inputQRPixels; CGImageRef inputCGImage = [originImg CGImage ]; NSUInteger inputWidth = CGImageGetWidth (inputCGImage); NSUInteger inputHeight = CGImageGetHeight (inputCGImage); NSUInteger inputBytesPerRow = bytesPerPixel * inputWidth; CGImageRef inputQRCGImage = [qrImg CGImage ]; NSUInteger inputQRWidth = CGImageGetWidth (inputQRCGImage); NSUInteger inputQRHeight = CGImageGetHeight (inputQRCGImage); NSUInteger inputQRBytesPerRow = bytesPerPixel * inputQRWidth; inputPixels = (UInt32 *)calloc(inputHeight * inputWidth, sizeof (UInt32 )); inputQRPixels = (UInt32 *)calloc(inputQRHeight * inputQRWidth, sizeof (UInt32 )); CGContextRef context = CGBitmapContextCreate (inputPixels, inputWidth, inputHeight, bitsPerComponent, inputBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextRef qrcontext = CGBitmapContextCreate (inputQRPixels, inputQRWidth, inputQRHeight, bitsPerComponent, inputQRBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextDrawImage (context, CGRectMake (0 , 0 , inputWidth, inputHeight), inputCGImage); CGContextDrawImage (qrcontext, CGRectMake (0 , 0 , inputQRWidth, inputQRHeight), inputQRCGImage); for (int j = 0 ; j < inputHeight; j++) { for (int i = 0 ; i < inputWidth; i++) { UInt32 * currentPixel = inputPixels + (j * inputWidth) + i; UInt32 color = *currentPixel; UInt32 thisR,thisG,thisB,thisA; thisR=R(color); thisG=G(color); thisB=B(color); thisA=A(color); UInt32 * currentQRPixel = inputQRPixels + (j * inputWidth) + i; UInt32 qrcolor = *currentQRPixel; UInt32 qrthisR,qrthisG,qrthisB,qrthisA; qrthisR=R(qrcolor); qrthisG=G(qrcolor); qrthisB=B(qrcolor); qrthisA=A(qrcolor); if (thisA == 0 ){ thisA = 1 ; } if (qrthisR < 128 ){ thisR ^= (thisR & ( 1 << 0 ) ) ^ (0 << 0 ) ; } else { thisR ^= (thisR & ( 1 << 0 ) ) ^ (1 << 0 ) ; } *currentPixel = RGBAMake(thisR, thisG, thisB, thisA); } } CGImageRef newCGImage = CGBitmapContextCreateImage (context); UIImage * processedImage = [UIImage imageWithCGImage:newCGImage]; CGImageRelease (inputCGImage); CGColorSpaceRelease (colorSpace); CGContextRelease (context); free(inputPixels); CGImageRelease (inputQRCGImage); CGContextRelease (qrcontext); free(inputQRPixels); return processedImage; }
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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 - (UIImage *)handleHideImage:(UIImage *)originImg { CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB (); NSUInteger bytesPerPixel = 4 ; NSUInteger bitsPerComponent = 8 ; UInt32 * inputPixels; CGImageRef inputCGImage = [originImg CGImage ]; NSUInteger inputWidth = CGImageGetWidth (inputCGImage); NSUInteger inputHeight = CGImageGetHeight (inputCGImage); NSUInteger inputBytesPerRow = bytesPerPixel * inputWidth; inputPixels = (UInt32 *)calloc(inputHeight * inputWidth, sizeof (UInt32 )); CGContextRef context = CGBitmapContextCreate (inputPixels, inputWidth, inputHeight, bitsPerComponent, inputBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextDrawImage (context, CGRectMake (0 , 0 , inputWidth, inputHeight), inputCGImage); for (int j = 0 ; j < inputHeight; j++) { for (int i = 0 ; i < inputWidth; i++) { UInt32 * currentPixel = inputPixels + (j * inputWidth) + i; UInt32 color = *currentPixel; UInt32 thisR,thisG,thisB,thisA; thisR=R(color); thisG=G(color); thisB=B(color); thisA=A(color); int tag = (thisR >> 0 ) & 1 ; if (tag == 0 ){ thisR=255 ; thisG=255 ; thisB=255 ; thisA=255 ; } else { thisR=0 ; thisG=0 ; thisB=0 ; thisA=255 ; } *currentPixel = RGBAMake(thisR, thisG, thisB, thisA); } } CGImageRef newCGImage = CGBitmapContextCreateImage (context); UIImage * processedImage = [UIImage imageWithCGImage:newCGImage]; CGImageRelease (inputCGImage); CGColorSpaceRelease (colorSpace); CGContextRelease (context); free(inputPixels); return processedImage; }
当然LSB的方式也能隐藏文字等其他信息。与之类似的想法还有 可以根据容差进行信息的隐藏。
若是有两张图片,则对两张图片的每一个像素点进行对比,设置一个容差的阈值α,超出这个阈值的像素点RGB值设置为(255,255,255),若是没超过阈值,则设置该像素点的RGB值为(0,0,0)。因此,通过调整不同的α值,可以使对比生成的图片呈现不同的画面。比如两张图完全一样,设置阈值α为任何值,最后得到的对比图都只会是全黑。若两张图每一个像素点都不同,阈值α设置为1,则对比图将是全白。如果将隐藏信息附加到某些像素点上,这时调整阈值α即可看到隐藏信息。
JPEG压缩 目前例子中有个比较大的问题就是,经过LSB操作后的图片是需要无损的才能 解析出数据来。假设图片经过JPEG压缩后 如下:
1 2 3 4 5 6 UIImage * result = [self handleImage:oriImg hideQrImage:qrImg];NSData * resultData = UIImageJPEGRepresentation (result, 0.9 );result = [UIImage imageWithData:resultData]; UIImage * qrResult = [self handleHideImage:result];
则qrResult会得到如下图:
这是由于JPEG图像格式使用离散余弦变换(Discrete Cosine Transform,DCT)函数来压缩图像,而这个图像压缩方法的核心是:通过识别每个8×8像素块中相邻像素中的重复像素来减少显示图像所需的位数,并使用近似估算法降低其冗余度。因此,我们可以把DCT看作一个用于执行压缩的近似计算方法。因为丢失了部分数据,例子中最低位信息其实已经被改变。
混色模式 在PS 的混色模式中我们可以看到可以选项,混合模式的基本原理就是取A层任意一个像素a [R1, G1, B1],与B层对应位置的像素b [R2, G2, B2] 进行数学运算,得到c [R3, G3, B3]。有兴趣可以看下 一篇文章彻底搞清PS混合模式的原理
基于这种想法 我们可以模拟下PS的混合模式:
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 27 for (int j = 0 ; j < inputHeight; j++) { for (int i = 0 ; i < inputWidth; i++) { @autoreleasepool { UInt32 *currentPixel = inputPixels + (j * inputWidth) + i; UInt32 color = *currentPixel; UInt32 thisR,thisG,thisB,thisA; thisR = R(color); thisG = G(color); thisB = B(color); thisA = A(color); UInt32 newR,newG,newB; newR = [self SoftLight:thisR]; newG = [self SoftLight:thisG]; newB = [self SoftLight:thisB]; *currentPixel = RGBAMake(newR, newG, newB, thisA); } } }
Color Burn 颜色加深:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + (int )ColorBurn:(int )originValue { int bValue = 64 ; int resultValue = 0 ; resultValue = originValue - (255 - originValue) * (255 - bValue) / bValue; if (resultValue < 0 ) { resultValue = 0 ; } if (resultValue > 255 ) { resultValue = 255 ; } return resultValue; }
Color Dodge 颜色减淡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + (int )ColorDodge:(int )originValue { int bValue = 128 ; int resultValue = 0 ; resultValue = originValue + originValue * bValue / (255 - bValue); if (resultValue < 0 ) { resultValue = 0 ; } if (resultValue > 255 ) { resultValue = 255 ; } return resultValue; }
Hard Light 强光:
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 27 28 + (int )HardLight:(int )originValue { int bValue = 64 ; int resultValue = 0 ; if (bValue <= 128 ){ resultValue = originValue * bValue / 128 ; } else { resultValue = 255 - (255 - originValue) * (255 - bValue) / 128 ; } if (resultValue < 0 ) { resultValue = 0 ; } if (resultValue > 255 ) { resultValue = 255 ; } return resultValue; }
发现一篇文章 iOS——隐形水印的实现和『颜色加深』算法 文中说用 颜色加深来显示隐形水印,但是我换了几张图片实践了一下 效果不理想。有兴趣的同学可以自己实践下。
马赛克 简单马赛克核心算法的大概原理就是把某一个点的颜色赋值给它周围的指定区域,这个区域大小可以我们自己来定义。
按照这种想法可以做到:
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 27 28 29 for (int j = 0 ; j < inputHeight; j++) { for (int i = 0 ; i < inputWidth; i++) { UInt32 * currentPixel = inputPixels + (j * inputWidth) + i; float markP = 10 ; NSUInteger centerX = 0 , centerY = 0 ; centerX = floor(i/markP)*markP + 0.5 *markP; centerX = centerX >= (inputWidth-1 ) ? (inputWidth-1 ) : centerX; centerY = floor(j/markP)*markP + 0.5 *markP; centerY = centerY >= (inputHeight-1 ) ? (inputHeight-1 ) : centerY; UInt32 * centerPixel = inputPixels + (centerY * inputWidth) + centerX; UInt32 centerColor = *centerPixel; UInt32 centerR,centerG,centerB,centerA; centerR=R(centerColor); centerG=G(centerColor); centerB=B(centerColor); centerA=A(centerColor); *currentPixel = RGBAMake(centerR, centerG, centerB, centerA); } }
提取主色调 ref:iOS-palette android-palette js-palette
遍历一遍图片的所有像素信息,然后统计一下哪个RGB值最多,不就是主色调嘛?但是人眼和冷冰冰的数据还是存在差异的。 Palette通过饱和度筛选 颜色区域解决颜色分散来选出主色调。参考iOS图片精确提取主色调算法iOS-Palette