马赛克
马赛克在图片效果中应该是一种最常见的处理方式,日常生活中也几乎处处可见。前段时间项目中要实现图片马赛克处理,就研究了一下。其实,用苹果滤镜CIFilter就能非常便捷的实现加码,但使用的过程中我发现滤镜只能处理.png格式的图片,如果遇到.jpeg格式的图片就没有效果了,于是决定研究一下马赛克算法,发现可以通过操作图片的像素点来实现同样的效果。当然后者的实用性更加广泛,随便你什么类型的图片都可以。文章最后还有涂抹马赛克效果实现以及“复原”的Demo,希望多多star支持~
操作像素点实现马赛克
我们都知道图片是一个一个像素点构成的,其实很早之前就有想过为什么所有的图片都是矩形的?有没有那种不规则的图片?计算机中的图片为什么都是矩形的?显示圆形也只能周围透明?,估计只有非科班出身的我才会问这种问题吧~简单来说,其实就是为了统一、更加方便的来处理图片,所以图片就是由像素矩阵构成的,平时我们看到的不规则的图片没有颜色的地方只是透明了而已。然后我就会想能不能局部的改变图片的颜色呢,比如把指定的不规则区域颜色改为别的颜色,以上都是自己以前胡乱想的,研究了之后发现其实都可以实现(不过没那么精确),我们可以通过操作图片的像素点来实现,直接上代码吧……
+ (NSArray *)getRGBsArrFromImage:(UIImage *)image{
//1.get the image into your data buffer
CGImageRef imageRef = [image CGImage];
NSUInteger imageW = CGImageGetWidth(imageRef);
NSUInteger imageH = CGImageGetHeight(imageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSUInteger bytesPerPixel = 4;//一个像素四个分量,即ARGB
NSUInteger bytesPerRow = bytesPerPixel * imageW;
unsigned char *rawData = (unsigned char *)calloc(imageH*imageW*bytesPerPixel, sizeof(unsigned char));
NSUInteger bitsPerComponent = 8;//每个分量8个字节
/*
参数1:数据源
参数2:图片宽
参数3:图片高
参数4:表示每一个像素点,每一个分量大小
在我们图像学中,像素点:ARGB组成 每一个表示一个分量(例如,A,R,G,B)
在我们计算机图像学中每一个分量的大小是8个字节
参数5:每一行大小(其实图片是由像素数组组成的)
如何计算每一行的大小,所占用的内存
首先计算每一个像素点大小(我们取最大值): ARGB是4个分量 = 每个分量8个字节 * 4
参数6:颜色空间
参数7:是否需要透明度
*/
CGContextRef context = CGBitmapContextCreate(rawData, imageW, imageH, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextDrawImage(context, CGRectMake(0, 0, imageW, imageH), imageRef);
//2.Now your rawData contains the image data int the RGBA8888 pixel format
NSUInteger blackPixel = 0;
NSMutableArray *pixelsArr = [NSMutableArray array];
for (int y = 0; y < imageH; y++) {
for (int x = 0; x < imageW; x++) {
NSUInteger byteIndex = bytesPerRow*y + bytesPerPixel*x;
//rawData一维数组存储方式RGBA(第一个像素)RGBA(第二个像素)
NSUInteger red = rawData[byteIndex];
NSUInteger green = rawData[byteIndex+1];
NSUInteger blue = rawData[byteIndex+2];
NSUInteger alpha = rawData[byteIndex+3];
XPixelItem *pixelItem = [[XPixelItem alloc] init];
pixelItem.color = [UIColor colorWithRed:red/255.0 green:green/255.0 blue:blue/255.0 alpha:alpha/255.0];
pixelItem.location = CGPointMake(x, y);
[pixelsArr addObject:pixelItem];
if (red+green+blue == 0 && (alpha/255.0 >= 0.5)){//计算黑色部分所占比例
blackPixel++;
}
}
}
NSLog(@"黑色所占的面积--%f,%lu",blackPixel*1.0/(imageW*imageH),(unsigned long)pixelsArr.count);
imageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
free(rawData);
return pixelsArr;
}
其中最主要的方法就是CGBitmapContextCreate,根据分配好的内存创建一个Bitmap的上下文,其中rawData中存放的就是我们所需要的像素点的集合。每个像素点由ARGB四个分量组成,我们看到的不规则图片没有颜色的地方也就是A为0的像素点,像素在数组中存放的是方式也是一个分量一个分量的存进去的,这样我们就可以通过修改数组中的数据来实现修改像素点。通过上述方法我们就可以把一个图片的局部不规则区域修改颜色,效果如下图:
马赛克算法
马赛克核心算法的大概原理就是把某一个点的颜色赋值给它周围的指定区域,这个区域大小可以我们自己来定义。
unsigned char *pixels[4] = {0};
if (i % sizeLevel == 0) {
if (j % sizeLevel == 0) {
memcpy(pixels, bitMapData+4*currentIndex, 4);
}else{
//将上一个像素点的值赋给第二个
memcpy(bitMapData+4*currentIndex, pixels, 4);
}
}else{
preCurrentIndex = (i-1)*imageW+j;
memcpy(bitMapData+4*currentIndex, bitMapData+4*preCurrentIndex, 4);
}
memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中。其实就是先把某个像素点的ARGB存到一个空数组pixels中,然后遍历像素点,如果是sizeLevel的整数倍就获取新的像素信息,不是的话就更换当前像素点的信息,这样就能实现sizeLevel大小区域的颜色是统一的了,也就是我们看到的马赛克中一个方块区域。下边这张图片描述的就很贴切
其实这样做的话还会有问题,因为透明区域的像素点RGB信息也为0,0,0,跟黑色一样,这么一来后边再跟根据bitmap去绘制图片的时候,会把透明区域当成黑色来处理,所以我在中间进行了一下过滤
if (red+green+blue == 0 && (alpha/255.0 <= 0.5)) {
rawData[currentIndex*4] = 255;
rawData[currentIndex*4+1] = 255;
rawData[currentIndex*4+2] = 255;
rawData[currentIndex*4+3] = 0;
continue;
}
这种做法也不太严谨,但暂时想不到什么好的办法。
涂抹实现马赛克
马赛克平时的运用更多的是跟用户交互息息相关的,比如手指涂抹区域打上马赛克,其实这种实现也挺简单。刚开始做的时候觉得还要去计算,但这样显然不易于实现。其实用两张图片就可以搞定了,一张是原图,用imageView来显示;一张是用马赛克处理过的图片,用CALayer来显示;马赛克处理过的图片覆盖在原图上边,然后利用layer的mask属性来控制CALayer指定区域的显示与否。
self.imageLayer = [CALayer layer];
self.imageLayer.frame = self.bounds;
[self.layer addSublayer:self.imageLayer];
self.shapeLayer = [CAShapeLayer layer];
self.shapeLayer.frame = self.bounds;
self.shapeLayer.lineCap = kCALineCapRound;
self.shapeLayer.lineJoin = kCALineJoinRound;
self.shapeLayer.lineWidth = 20;
self.shapeLayer.strokeColor = [UIColor blueColor].CGColor;
self.shapeLayer.fillColor = nil;//此处必须设为nil,否则后边添加addLine的时候会自动填充
self.imageLayer.mask = self.shapeLayer;
self.path = CGPathCreateMutable();
然后我们在touchMove方法中根据手指移动轨迹设置self.shapeLayer的path属性就可以实现想要的效果了。我把这些都封装在XScratchView类中了,使用的时候只需要初始化并给图片属性赋值,
XScratchView *scratchView = [[XScratchView alloc] initWithFrame:CGRectMake(0, 100, kScreenWidth, 300)];
scratchView.surfaceImage = [UIImage imageNamed:@"smoke.jpeg"];
scratchView.mosaicImage = [XRGBTool getMosaicImageWith:[UIImage imageNamed:@"smoke.jpeg"] level:0];
复原时只需要调用recover方法,
[_scratchView recover];
需要保存的时候只需要截取图片区域就可以获取加码后的图片了。效果如下
具体实现代码都在我的RGBTool这个Demo中,有什么问题还请大家多多指教,共同进步!