在开发过程中会遇到一些很小但有意思的功能,有一个功能是把一张图片的灰度作为另一张图片的alpha。功能实现很简单,把实现过程和遇到的一些知识分享给大家。
实现的方法是把一张彩色图转成灰度图,之后循环取这个灰度图的灰度值赋值给另一张图的alpha通道。这里边涉及到了灰度图、alpha通道等只是,我们先来说下这些相关概念。
首先用取色器取图片一点的颜色,看到获取到的的信息:
红绿蓝就是图片这一点的信息,我们改变这一点的红绿蓝,这一点的视觉效果就产生了变化。在开发的过程中,标注图会有#1ebc21ff这样的标注,这就是RGBA值,#1ebc21ff这个值我们在设置色值的时候用代码写给机器,最后转化成二进制数据,这些数据和视觉呈现颜色一一对应,是因为这些数据符合了制定的规则。规则有很多种,我们告诉机器用那种颜色空间,机器就执行对应的规则。
这个功能里涉及到灰度图片和彩色图片,这就是两种颜色空间。彩色图片所用的空间是设备RGB颜色空间。我们看到的设备上显示的图片是由一个点一个点拼起来的,取色器取出来的就是其中一个点。机器从我们这接收一个点的信息,把这个点显示到设备上。下图是我们传给机器的一个点的信息:
它包含了4部分,红色、绿色、蓝色、透明度。把这4部分称为4个通道。每个通道占用8bit(1btye),4个通道一共32bit(4byte),也就是说每个点需要4byte空间存储信息。点的个数*4byte就是一张图片需要空间大小。在处理图片时,首先要创建一个容纳图片数据的空间,一个点大小32bit,所以用uint32_t数据类型,一个通道8bit,所以用uint8_t数据类型。在后续的操作中,就是对这每一个点中的通道的数据进行更改。
我们只想在屏幕上看到一种颜色,为什么要给机器传3种颜色呢?因为在显示时看到的不同颜色点都是由这3种颜色组合之后显示出来的,三种颜色数值的变化就显示出不同的颜色。这3种颜色被称为三原色。下图就是三原色和三原色组合显示出来的颜色。
上边说的都是我们传给机器的数据,下面看下机器是如何用这个数据在屏幕上显示我们想要看到的颜色的。
上图显示的机器如何在屏幕上显示一个点的,它是按照我们传给机器的红、绿、蓝这样的结构进行显示的。一个点包含了3个颜色的显示区,数值代表了各颜色亮度的高低。当一点的颜色为纯黑的时候,是三色都不发光,所以色值是:0,0,0(0x000000)。当为白色的时候,是三色发光到最强,所以设置是:255,255,255(0xffffff)。alpha通道是没有显示区域的,在颜色点显示到屏幕之前,我们传给机器的alpha通道数值与传的R、G、B三个通道的数值进行运算,运算之后的RGB数值显示到屏幕上。
再说下和颜色相关的内容,这样我们在调试的时候也可以进行一些简单的颜色运算规则,增加一点乐趣。先看下下图:
这个图是色谱环,直径两端的颜色值相加为#ffffff,也就是255,255,255。这两种颜色称为反转色,也成为互补色。以蓝色(0x0000ff)这条直径为例,越是远离蓝色,蓝色亮度越低,B通道的数值越小,到顶端为0,成了黄色,这一点也就是上面三原色图中红色和绿色交叉,没有蓝色参与的区域。
另一张图片用到了彩色图片转成的灰度图,灰度图包含色值信息只有一个通道,颜色是黑白,和RGB中一个通道格式相同,都是8bit,256个数值。如果包含alpha,那么就16bit,使用uint16_t数据类型。先来看下彩色图片转化成灰度图片在内存中:
代码语言:javascript复制 UIGraphicsBeginImageContext(grayImg.size);
CGImageRef gryImgRef = grayImg.CGImage;
//宽度
size_t grayImgWidth = CGImageGetWidth(gryImgRef);
//高度
size_t grayImgHeihgt = CGImageGetHeight(gryImgRef);
//创建灰度颜色空间
CGColorSpaceRef graySpaceRef = CGColorSpaceCreateDeviceGray();
//创建内存空间: 大小 = 图片像素点数(宽 * 高) * 每点空间大小(2byte:1byte灰度值,1byte:alpha值)
uint16_t * grayImgBuf = (uint16_t *)malloc(grayImgWidth * grayImgHeihgt * 2);
size_t grayBytesPerRow = grayImgWidth * 2;//每一行所占的byte数
//创建一个位图绘制环境(BitmapContext)
CGContextRef grayContext = CGBitmapContextCreate(grayImgBuf, //指向要渲染的灰度图内存地址
grayImgWidth, //宽度
grayImgHeihgt, //高度
8, //一个像素中每个通道占的位数
grayBytesPerRow, //每一行所占的byte数
graySpaceRef, //颜色空间
kCGImageAlphaPremultipliedLast);//通道 A(GA)
//把图绘制到上下文中
CGContextDrawImage(grayContext,
CGRectMake(0, 0, grayImgWidth, grayImgHeihgt),
gryImgRef);
CGColorSpaceRelease(graySpaceRef);
CGContextRelease(grayContext);
给CGBitmapContextCreate函数的最后一个参数传了一个值:kCGImageAlphaPremultipliedLast,先看下这个值的意思:
代码语言:javascript复制typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
kCGImageAlphaNone, // 没有alhpa
kCGImageAlphaPremultipliedLast, // 有alpha的话,在渲染前就将alpha乘到乘到R、G、B上,输出格式是RGBA
kCGImageAlphaPremultipliedFirst, // 有alpha的话,在渲染前就将alpha乘到乘到R、G、B上,输出格式是ARGB
kCGImageAlphaLast, // 输出格式是RGBA
kCGImageAlphaFirst, // 输出格式是ARGB
kCGImageAlphaNoneSkipLast, // 有alpha值,但是忽略。输出格式是RBGX
kCGImageAlphaNoneSkipFirst, // 有alpha值,但是忽略。输出格式是XRGB.
kCGImageAlphaOnly // 只输出alpha值,没有颜色值
};
这个参数规定了通道的输出规则,这里获取灰度图我们用的是kCGImageAlphaPremultiplitedLast,alpha通道放到后8位。下图展示了如何把灰度图赋值给彩图alpha通道
灰度图渲染地址grayImgBuf已经获取到,下面就是把前景图片写入内存中,方法和灰度图方法相同
代码语言:javascript复制 CGImageRef foreImgRef = foreImg.CGImage;
size_t foreImgWidth = CGImageGetWidth(foreImgRef);
size_t foreImgHeight = CGImageGetHeight(foreImgRef);
//创建RGB颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
//创建内存空间: 大小 = 图片像素点数(宽 * 高) * 每点空间大小(4byte:R、G、B、Alpha各1byte)
uint32_t* rgbImgBuf = (uint32_t*)malloc(foreImgWidth * foreImgHeight * 4);
//每一行所占的byte数
size_t bytesPerRow = foreImgWidth * 4;
//创建一个位图绘制环境(BitmapContext)
CGContextRef context = CGBitmapContextCreate(rgbImgBuf,
foreImgWidth,
foreImgHeight,
8,
bytesPerRow,
colorSpace,
kCGImageAlphaNoneSkipFirst);
CGContextDrawImage(context,
CGRectMake(0, 0, foreImgWidth, foreImgHeight),
foreImg.CGImage);
现在两张图片都已经在内存中,下面就进行灰度值对alpha通道的赋值
代码语言:javascript复制 //所有像素点个个数
size_t pixelNum = foreImgWidth * foreImgHeight;
//ARGB图
uint32_t * rgbCurPtr = rgbImgBuf;
//GA图
uint16_t * grayCurPtr = grayImgBuf;
//遍历像素,彩色图以4byte循环,灰度图以2byte循环
for (int i = 0; i < pixelNum; i ,rgbCurPtr ,grayCurPtr )
{
//ARGB四个通道中的R通道
uint8_t * rptr = (uint8_t*)rgbCurPtr;
//GA两个通道中的灰度
uint8_t * gptr = (uint8_t*)grayCurPtr;
//把灰度赋值给彩色的alpha通道
rptr[0] = gptr[0];
}
这段代码是实现这个功能的核心,在这个循环里可以对这些像素做一些运算,比如灰度图和前景图各通道色值的加减,前景图各像素点取反转色,过滤某一点的颜色。这个一次循环就是取一点的信息,这也实现了取色器的功能。
两张图片数据处理完成,现在把处理过后的图片输出得到我们希望得到的图片。
代码语言:javascript复制 //数据源提供者
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL,
rgbImgBuf,
bytesPerRow * foreImgHeight,
ProviderReleaseData);
//将内存转成image
CGImageRef imageRef = CGImageCreate(foreImgWidth, //宽度
foreImgHeight,//高度
8, //每个通道的bit数
32, //每个像素的bit数
bytesPerRow, //每一行所占的byte数
colorSpace, //颜色空间
kCGImageAlphaFirst | kCGBitmapByteOrder32Big,//位图像素布局
dataProvider,//数据源提供者
NULL, //解码渲染数组
true, //是否抗锯齿
kCGRenderingIntentDefault);//颜色渲染意图
CGDataProviderRelease(dataProvider);
// 结果图
UIImage* resultImage = [UIImage imageWithCGImage:imageRef];
// 释放
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
这里有一个这个参数值 kCGImageAlphaFirst | kCGBitmapByteOrder32Big,和上边CGBitmapContextCreate这个函数中kCGImageAlphaNoneSkipFirst含义是一样的,但是参数类型不同,一个是unit32_t,一个是CGBitmapInfo。
kCGBitmapByteOrder32Big 是大字节序,把高有效位放在低地址段。这个参数也可以是:kCGImageAlphaLast | kCGBitmapByteOrder32Little,但要注意的是CGBitmapContextCreate这个的参数也需要改成:kCGImageAlphaNoneSkipLast|kCGBitmapByteOrder32Little。
最后输出resultImage,得到我们想要得到的图片。下图就是最后两张素材和得到的结果:
我们也可以只用一张图片,取它的反转色:
代码语言:javascript复制 //遍历像素,彩色图以4byte循环,灰度图以2byte循环
for (int i = 0; i < pixelNum; i ,grayCurPtr )
{
//ARGB四个通道中的R通道
uint8_t * rptr = (uint8_t*)rgbCurPtr;
//R通道取反转色
rptr[1] = 255 - rptr[1];
//G通道去反转色
rptr[2] = 255 - rptr[2];
//B通道取反转色
rptr[3] = 255 - rptr[3];
}
也可以两张图片的个通道相加
代码语言:javascript复制 for (int i = 0; i < pixelNum; i ,rgbCurPtr ,grayCurPtr )
{
//ARGB四个通道中的R通道
uint8_t * rptr = (uint8_t*)rgbCurPtr;
//ARGB四个通道中的R通道
uint8_t * gptr = (uint8_t*)grayCurPtr;
//两个R通道相加
rptr[1] = rptr[1] gptr[1];
//两个G通道相加
rptr[2] = rptr[2] gptr[2];
//两个B通道相加
rptr[3] = rptr[3] gptr[3];
}
我很早之前写的文章,在这里同步下
I