iOS图片-像素操作

介绍下常见的几种对图片的像素级操作
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
//示例1 将一张图片变为蓝色
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;

//定义最高32位整形指针 *inputPixels
UInt32 * inputPixels;

//转换图片为CGImageRef,获取参数:长宽高,每个像素的字节数(4),每个R的比特数
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++) {
//原图 像素rgba信息
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);
// NSLog(@"r %d,g %d,b %d,a %d",thisR,thisG,thisB,thisA);
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
//示例2 像素格式 颜色空间变为灰色
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
//...其他同上例
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
//示例3 在一张图片中使用lsb方式隐藏二维码 。
//例子里简单处理了 原图和二维码大小相同
UIImage* oriImg = [UIImage imageNamed:@"channel.png"];
UIImage* qrImg = [self.class createQRForAlreadySpliceString:@"一些信息" size:CGSizeMake(300, 300)];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//将二维码图片信息藏入原图 得到result
UIImage* result = [self handleImage:oriImg hideQrImage:qrImg];
//从result中解析得到二维码
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
//MARK: 图片处理 隐藏二维码图片
- (UIImage*)handleImage:(UIImage*)originImg hideQrImage:(UIImage*)qrImg
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSUInteger bytesPerPixel = 4;
NSUInteger bitsPerComponent = 8;


//定义最高32位整形指针 *inputPixels
UInt32 * inputPixels;
UInt32 * inputQRPixels;

//转换图片为CGImageRef,获取参数:长宽高,每个像素的字节数(4),每个R的比特数
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);
// NSLog(@"r %d,g %d,b %d,a %d",thisR,thisG,thisB,thisA);
//二维码
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);
//透明度0信息会丢失
if(thisA == 0){
thisA = 1;
}
//像素最低位处理
if(qrthisR < 128){//二维码白色 设置最低位为0
thisR ^= (thisR & ( 1 << 0) ) ^ (0 << 0) ;
// thisA = 255;
}
else{//二维码黑色 设置最低位为1
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
//MARK: 图片处理 解析二维码
- (UIImage*)handleHideImage:(UIImage*)originImg
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSUInteger bytesPerPixel = 4;
NSUInteger bitsPerComponent = 8;

//定义最高32位整形指针 *inputPixels
UInt32 * inputPixels;

//转换图片为CGImageRef,获取参数:长宽高,每个像素的字节数(4),每个R的比特数
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){//最低位为0 白色
thisR=255;
thisG=255;
thisB=255;
thisA=255;
}
else{//最低位为1 黑色
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
//将二维码图片信息藏入原图 得到result
UIImage* result = [self handleImage:oriImg hideQrImage:qrImg];
NSData* resultData = UIImageJPEGRepresentation(result, 0.9);
result = [UIImage imageWithData:resultData];
//从result中解析得到二维码
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;
// 这里直接移位获得RBGA的值,以及输出写的非常好!
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 {

// C=A-(A反相×B反相)/B
// 如果上层越暗,则下层获取的光越少,加深效果越明显。
// 【如果上层为全黑色,则下层颜色值不是255的像素全变成0】,
// 如果上层为全白色,则根本不会影响下层。
// 结果最亮的地方不会高于下层的像素值。

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 {

//C=A+(A×B)/B反相
//该模式和上一个模式刚好相反。
//该模式下,上层的亮度决定了下层的暴露程度。
//如果上层越亮,下层获取的光越多,也就是越亮。
//如果上层是纯黑色,也就是没有亮度,则根本不会影响下层,
//【如果上层是纯白色,则下层颜色值不是0的像素全变成255】。
//结果最黑的地方不会低于下层的像素值。

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 {

//B<=128则 C=(A×B)/128
//B>128则 C=255-(A反相×B反相)/128
//该模式完全相对应于Overlay(叠加)模式下,两个图层进行次序交换的情况。
//如过上层的颜色高于50%灰,则下层越亮,反之越暗。
//【如果将上层图层设为叠加,下层设为强光,则改变图层顺序不影响结果。】


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++) {
//原图 像素rgba信息
UInt32 * currentPixel = inputPixels + (j * inputWidth) + i;


float markP = 10;//10x10区域
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