Converter imagem em escala de cinza

Eu estou tentando converter uma imagem em escala de cinza da seguinte maneira:

#define bytesPerPixel 4 #define bitsPerComponent 8 -(unsigned char*) getBytesForImage: (UIImage*)pImage { CGImageRef image = [pImage CGImage]; NSUInteger width = CGImageGetWidth(image); NSUInteger height = CGImageGetHeight(image); NSUInteger bytesPerRow = bytesPerPixel * width; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); unsigned char *rawData = malloc(height * width * 4); CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGColorSpaceRelease(colorSpace); CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); CGContextRelease(context); return rawData; } -(UIImage*) processImage: (UIImage*)pImage { DebugLog(@"processing image"); unsigned char *rawData = [self getBytesForImage: pImage]; NSUInteger width = pImage.size.width; NSUInteger height = pImage.size.height; DebugLog(@"width: %d", width); DebugLog(@"height: %d", height); NSUInteger bytesPerRow = bytesPerPixel * width; for (int xCoordinate = 0; xCoordinate < width; xCoordinate++) { for (int yCoordinate = 0; yCoordinate < height; yCoordinate++) { int byteIndex = (bytesPerRow * yCoordinate) + xCoordinate * bytesPerPixel; //Getting original colors float red = ( rawData[byteIndex] / 255.f ); float green = ( rawData[byteIndex + 1] / 255.f ); float blue = ( rawData[byteIndex + 2] / 255.f ); //Processing pixel data float averageColor = (red + green + blue) / 3.0f; red = averageColor; green = averageColor; blue = averageColor; //Assigning new color components rawData[byteIndex] = (unsigned char) red * 255; rawData[byteIndex + 1] = (unsigned char) green * 255; rawData[byteIndex + 2] = (unsigned char) blue * 255; } } NSData* newPixelData = [NSData dataWithBytes: rawData length: height * width * 4]; UIImage* newImage = [UIImage imageWithData: newPixelData]; free(rawData); DebugLog(@"image processed"); return newImage; } 

Então quando eu quero converter uma imagem eu apenas chamo processImage:

 imageToDisplay.image = [self processImage: image]; 

Mas imageToDisplay não é exibido. O que pode ser o problema?

Obrigado.

O que exatamente acontece quando você usa essa function? A function está retornando uma imagem inválida ou a canvas não está mostrando corretamente?

Este é o método que uso para converter em escala de cinza.

 - (UIImage *) convertToGreyscale:(UIImage *)i { int kRed = 1; int kGreen = 2; int kBlue = 4; int colors = kGreen | kBlue | kRed; int m_width = i.size.width; int m_height = i.size.height; uint32_t *rgbImage = (uint32_t *) malloc(m_width * m_height * sizeof(uint32_t)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(rgbImage, m_width, m_height, 8, m_width * 4, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast); CGContextSetInterpolationQuality(context, kCGInterpolationHigh); CGContextSetShouldAntialias(context, NO); CGContextDrawImage(context, CGRectMake(0, 0, m_width, m_height), [i CGImage]); CGContextRelease(context); CGColorSpaceRelease(colorSpace); // now convert to grayscale uint8_t *m_imageData = (uint8_t *) malloc(m_width * m_height); for(int y = 0; y < m_height; y++) { for(int x = 0; x < m_width; x++) { uint32_t rgbPixel=rgbImage[y*m_width+x]; uint32_t sum=0,count=0; if (colors & kRed) {sum += (rgbPixel>>24)&255; count++;} if (colors & kGreen) {sum += (rgbPixel>>16)&255; count++;} if (colors & kBlue) {sum += (rgbPixel>>8)&255; count++;} m_imageData[y*m_width+x]=sum/count; } } free(rgbImage); // convert from a gray scale image back into a UIImage uint8_t *result = (uint8_t *) calloc(m_width * m_height *sizeof(uint32_t), 1); // process the image back to rgb for(int i = 0; i < m_height * m_width; i++) { result[i*4]=0; int val=m_imageData[i]; result[i*4+1]=val; result[i*4+2]=val; result[i*4+3]=val; } // create a UIImage colorSpace = CGColorSpaceCreateDeviceRGB(); context = CGBitmapContextCreate(result, m_width, m_height, 8, m_width * sizeof(uint32_t), colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast); CGImageRef image = CGBitmapContextCreateImage(context); CGContextRelease(context); CGColorSpaceRelease(colorSpace); UIImage *resultUIImage = [UIImage imageWithCGImage:image]; CGImageRelease(image); free(m_imageData); // make sure the data will be released by giving it to an autoreleased NSData [NSData dataWithBytesNoCopy:result length:m_width * m_height]; return resultUIImage; } 

Eu precisava de uma versão que preservasse o canal alpha, então modifiquei o código postado por Dutchie432:

 @implementation UIImage (grayscale) typedef enum { ALPHA = 0, BLUE = 1, GREEN = 2, RED = 3 } PIXELS; - (UIImage *)convertToGrayscale { CGSize size = [self size]; int width = size.width; int height = size.height; // the pixels will be painted to this array uint32_t *pixels = (uint32_t *) malloc(width * height * sizeof(uint32_t)); // clear the pixels so any transparency is preserved memset(pixels, 0, width * height * sizeof(uint32_t)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // create a context with RGBA pixels CGContextRef context = CGBitmapContextCreate(pixels, width, height, 8, width * sizeof(uint32_t), colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedLast); // paint the bitmap to our context which will fill in the pixels array CGContextDrawImage(context, CGRectMake(0, 0, width, height), [self CGImage]); for(int y = 0; y < height; y++) { for(int x = 0; x < width; x++) { uint8_t *rgbaPixel = (uint8_t *) &pixels[y * width + x]; // convert to grayscale using recommended method: http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale uint32_t gray = 0.3 * rgbaPixel[RED] + 0.59 * rgbaPixel[GREEN] + 0.11 * rgbaPixel[BLUE]; // set the pixels to gray rgbaPixel[RED] = gray; rgbaPixel[GREEN] = gray; rgbaPixel[BLUE] = gray; } } // create a new CGImageRef from our context with the modified pixels CGImageRef image = CGBitmapContextCreateImage(context); // we're done with the context, color space, and pixels CGContextRelease(context); CGColorSpaceRelease(colorSpace); free(pixels); // make a new UIImage to return UIImage *resultUIImage = [UIImage imageWithCGImage:image]; // we're done with image now too CGImageRelease(image); return resultUIImage; } @end 

Aqui está um código usando apenas o UIKit e o modo de mesclagem de luminosidade. Um pouco de hack, mas funciona bem.

 // Transform the image in grayscale. - (UIImage*) grayishImage: (UIImage*) inputImage { // Create a graphic context. UIGraphicsBeginImageContextWithOptions(inputImage.size, YES, 1.0); CGRect imageRect = CGRectMake(0, 0, inputImage.size.width, inputImage.size.height); // Draw the image with the luminosity blend mode. // On top of a white background, this will give a black and white image. [inputImage drawInRect:imageRect blendMode:kCGBlendModeLuminosity alpha:1.0]; // Get the resulting image. UIImage *filteredImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return filteredImage; } 

Para manter a transparência, talvez você possa simplesmente definir o parâmetro do modo opaque do UIGraphicsBeginImageContextWithOptions como NO . Precisa ser verificado.

Baseado no código do Cam com a capacidade de lidar com a escala de displays Retina.

 - (UIImage *) toGrayscale { const int RED = 1; const int GREEN = 2; const int BLUE = 3; // Create image rectangle with current image width/height CGRect imageRect = CGRectMake(0, 0, self.size.width * self.scale, self.size.height * self.scale); int width = imageRect.size.width; int height = imageRect.size.height; // the pixels will be painted to this array uint32_t *pixels = (uint32_t *) malloc(width * height * sizeof(uint32_t)); // clear the pixels so any transparency is preserved memset(pixels, 0, width * height * sizeof(uint32_t)); CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); // create a context with RGBA pixels CGContextRef context = CGBitmapContextCreate(pixels, width, height, 8, width * sizeof(uint32_t), colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedLast); // paint the bitmap to our context which will fill in the pixels array CGContextDrawImage(context, CGRectMake(0, 0, width, height), [self CGImage]); for(int y = 0; y < height; y++) { for(int x = 0; x < width; x++) { uint8_t *rgbaPixel = (uint8_t *) &pixels[y * width + x]; // convert to grayscale using recommended method: http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale uint8_t gray = (uint8_t) ((30 * rgbaPixel[RED] + 59 * rgbaPixel[GREEN] + 11 * rgbaPixel[BLUE]) / 100); // set the pixels to gray rgbaPixel[RED] = gray; rgbaPixel[GREEN] = gray; rgbaPixel[BLUE] = gray; } } // create a new CGImageRef from our context with the modified pixels CGImageRef image = CGBitmapContextCreateImage(context); // we're done with the context, color space, and pixels CGContextRelease(context); CGColorSpaceRelease(colorSpace); free(pixels); // make a new UIImage to return UIImage *resultUIImage = [UIImage imageWithCGImage:image scale:self.scale orientation:UIImageOrientationUp]; // we're done with image now too CGImageRelease(image); return resultUIImage; } 

Eu gostava da resposta de Mathieu Godart, mas não parecia funcionar corretamente para imagens de retina ou alfa. Aqui está uma versão atualizada que parece funcionar para ambos para mim:

 - (UIImage*)convertToGrayscale { UIGraphicsBeginImageContextWithOptions(self.size, NO, self.scale); CGRect imageRect = CGRectMake(0.0f, 0.0f, self.size.width, self.size.height); CGContextRef ctx = UIGraphicsGetCurrentContext(); // Draw a white background CGContextSetRGBFillColor(ctx, 1.0f, 1.0f, 1.0f, 1.0f); CGContextFillRect(ctx, imageRect); // Draw the luminosity on top of the white background to get grayscale [self drawInRect:imageRect blendMode:kCGBlendModeLuminosity alpha:1.0f]; // Apply the source image's alpha [self drawInRect:imageRect blendMode:kCGBlendModeDestinationIn alpha:1.0f]; UIImage* grayscaleImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return grayscaleImage; } 

Abordagem diferente com CIFilter. Preserva o canal alfa e funciona com fundo transparente:

 + (UIImage *)convertImageToGrayScale:(UIImage *)image { CIImage *inputImage = [CIImage imageWithCGImage:image.CGImage]; CIContext *context = [CIContext contextWithOptions:nil]; CIFilter *filter = [CIFilter filterWithName:@"CIColorControls"]; [filter setValue:inputImage forKey:kCIInputImageKey]; [filter setValue:@(0.0) forKey:kCIInputSaturationKey]; CIImage *outputImage = filter.outputImage; CGImageRef cgImageRef = [context createCGImage:outputImage fromRect:outputImage.extent]; UIImage *result = [UIImage imageWithCGImage:cgImageRef]; CGImageRelease(cgImageRef); return result; } 

Uma extensão rápida para UIImage , preservando alpha:

 extension UIImage { private func convertToGrayScaleNoAlpha() -> CGImageRef { let colorSpace = CGColorSpaceCreateDeviceGray(); let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.None.rawValue) let context = CGBitmapContextCreate(nil, UInt(size.width), UInt(size.height), 8, 0, colorSpace, bitmapInfo) CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), self.CGImage) return CGBitmapContextCreateImage(context) } /** Return a new image in shades of gray + alpha */ func convertToGrayScale() -> UIImage { let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.Only.rawValue) let context = CGBitmapContextCreate(nil, UInt(size.width), UInt(size.height), 8, 0, nil, bitmapInfo) CGContextDrawImage(context, CGRectMake(0, 0, size.width, size.height), self.CGImage); let mask = CGBitmapContextCreateImage(context) return UIImage(CGImage: CGImageCreateWithMask(convertToGrayScaleNoAlpha(), mask), scale: scale, orientation:imageOrientation)! } } 

Aqui está outra boa solução como um método de categoria no UIImage. É baseado neste post e seus comentários. Mas eu consertei um problema de memory aqui:

 - (UIImage *)grayScaleImage { // Create image rectangle with current image width/height CGRect imageRect = CGRectMake(0, 0, self.size.width * self.scale, self.size.height * self.scale); // Grayscale color space CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // Create bitmap content with current image size and grayscale colorspace CGContextRef context = CGBitmapContextCreate(nil, self.size.width * self.scale, self.size.height * self.scale, 8, 0, colorSpace, kCGImageAlphaNone); // Draw image into current context, with specified rectangle // using previously defined context (with grayscale colorspace) CGContextDrawImage(context, imageRect, [self CGImage]); // Create bitmap image info from pixel data in current context CGImageRef grayImage = CGBitmapContextCreateImage(context); // release the colorspace and graphics context CGColorSpaceRelease(colorSpace); CGContextRelease(context); // make a new alpha-only graphics context context = CGBitmapContextCreate(nil, self.size.width * self.scale, self.size.height * self.scale, 8, 0, nil, kCGImageAlphaOnly); // draw image into context with no colorspace CGContextDrawImage(context, imageRect, [self CGImage]); // create alpha bitmap mask from current context CGImageRef mask = CGBitmapContextCreateImage(context); // release graphics context CGContextRelease(context); // make UIImage from grayscale image with alpha mask CGImageRef cgImage = CGImageCreateWithMask(grayImage, mask); UIImage *grayScaleImage = [UIImage imageWithCGImage:cgImage scale:self.scale orientation:self.imageOrientation]; // release the CG images CGImageRelease(cgImage); CGImageRelease(grayImage); CGImageRelease(mask); // return the new grayscale image return grayScaleImage; } 

Uma implementação rápida e eficiente do Swift 3 para iOS 9/10. Eu sinto que isso é eficiente, tendo agora tentado cada método de filtragem de imagens que eu poderia encontrar para processar centenas de imagens de cada vez (ao baixar usando a opção ImageFilter do AlamofireImage). Eu estabeleci este método como muito melhor do que qualquer outro que eu tentei (para o meu caso de uso) em termos de memory e velocidade.

 func convertToGrayscale() -> UIImage? { UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) let imageRect = CGRect(x: 0.0, y: 0.0, width: self.size.width, height: self.size.height) let context = UIGraphicsGetCurrentContext() // Draw a white background context!.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) context!.fill(imageRect) // optional: increase contrast with colorDodge before applying luminosity // (my images were too dark when using just luminosity - you may not need this) self.draw(in: imageRect, blendMode: CGBlendMode.colorDodge, alpha: 0.7) // Draw the luminosity on top of the white background to get grayscale of original image self.draw(in: imageRect, blendMode: CGBlendMode.luminosity, alpha: 0.90) // optional: re-apply alpha if your image has transparency - based on user1978534's answer (I haven't tested this as I didn't have transparency - I just know this would be the the syntax) // self.draw(in: imageRect, blendMode: CGBlendMode.destinationIn, alpha: 1.0) let grayscaleImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return grayscaleImage } 

Sobre o uso de colorDodge: Inicialmente tive problemas para obter minhas imagens leves o suficiente para combinar com a coloração em tons de cinza produzida usando o CIFilter (“CIPhotoEffectTonal”) – meus resultados ficaram muito escuros. Eu era capaz de obter uma correspondência decente aplicando CGBlendMode.colorDodge @ ~ 0.7 alpha, o que parece aumentar o contraste geral.

Outros efeitos de mistura de colors podem funcionar também – mas acho que você gostaria de aplicar antes da luminosidade, que é o efeito de filtragem em escala de cinza. Eu achei esta página muito útil para fazer referência aos diferentes BlendModes .

Ganho de eficiência Re descoberto: Preciso processar centenas de imagens em miniatura à medida que elas são carregadas de um servidor (usando o AlamofireImage para carregamento asynchronous, armazenamento em cache e aplicação de um filtro). Comecei a ter falhas quando o tamanho total das minhas imagens excedeu o tamanho do cache, então experimentei outros methods.

A abordagem CIFilter baseada em CPU CoreImage foi a primeira que eu tentei, e não foi eficiente em termos de memory para o número de imagens que estou manipulando.

Eu também tentei aplicar um CIFilter através da GPU usando o EAGLContext(api: .openGLES3) , que foi ainda mais intensivo em memory – na verdade, recebi avisos de memory para uso de mais de 450 mb ao carregar mais de 200 imagens.

Eu tentei processamento de bitmap (ou seja, CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue) … que funcionou bem, exceto eu couldn ‘ t obter uma resolução alta o suficiente para um dispositivo de retina moderno.As imagens eram muito granuladas, mesmo quando eu adicionei context.scaleBy(x: scaleFactor, y: scaleFactor) .

Então, de tudo o que eu tentei, este método (UIGraphics Context Draw) para ser VASTLY mais eficiente re velocidade e memory ao aplicar como um filtro para AlamofireImage. Como em, vendo menos de 70 mbs de ram ao processar minhas mais de 200 imagens e eles basicamente carregando instantaneamente, ao invés de mais de 35 segundos, ele levou com os methods openEAGL. Eu sei que estes não são benchmarks muito científicos. Vou instrumentar se alguém estiver muito curioso embora 🙂

E, por último, se você precisar passar este ou outro filtro em escala de cinza para o AlamofireImage – isto é como fazê-lo: (note que você deve importar o AlamofireImage para sua class para usar o ImageFilter)

 public struct GrayScaleFilter: ImageFilter { public init() { } public var filter: (UIImage) -> UIImage { return { image in return image.convertToGrayscale() ?? image } } } 

Para usá-lo, crie o filtro assim e passe para af_setImage da seguinte forma:

 let filter = GrayScaleFilter() imageView.af_setImage(withURL: url, filter: filter) 
 @interface UIImageView (Settings) - (void)convertImageToGrayScale; @end @implementation UIImageView (Settings) - (void)convertImageToGrayScale { // Create image rectangle with current image width/height CGRect imageRect = CGRectMake(0, 0, self.image.size.width, self.image.size.height); // Grayscale color space CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); // Create bitmap content with current image size and grayscale colorspace CGContextRef context = CGBitmapContextCreate(nil, self.image.size.width, self.image.size.height, 8, 0, colorSpace, kCGImageAlphaNone); // Draw image into current context, with specified rectangle // using previously defined context (with grayscale colorspace) CGContextDrawImage(context, imageRect, [self.image CGImage]); // Create bitmap image info from pixel data in current context CGImageRef imageRef = CGBitmapContextCreateImage(context); // Create a new UIImage object UIImage *newImage = [UIImage imageWithCGImage:imageRef]; // Release colorspace, context and bitmap information CGColorSpaceRelease(colorSpace); CGContextRelease(context); CFRelease(imageRef); // Return the new grayscale image self.image = newImage; } @end 

Eu ainda tenho outra resposta. Este é extremamente eficaz e lida com charts de retina, bem como transparência. Ele expande a abordagem de Sargis Gevorgyan:

 + (UIImage*) grayScaleFromImage:(UIImage*)image opaque:(BOOL)opaque { // NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; CGSize size = image.size; CGRect bounds = CGRectMake(0, 0, size.width, size.height); // Create bitmap content with current image size and grayscale colorspace CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); size_t bitsPerComponent = 8; size_t bytesPerPixel = opaque ? 1 : 2; size_t bytesPerRow = bytesPerPixel * size.width * image.scale; CGContextRef context = CGBitmapContextCreate(nil, size.width, size.height, bitsPerComponent, bytesPerRow, colorSpace, opaque ? kCGImageAlphaNone : kCGImageAlphaPremultipliedLast); // create image from bitmap CGContextDrawImage(context, bounds, image.CGImage); CGImageRef cgImage = CGBitmapContextCreateImage(context); UIImage* result = [[UIImage alloc] initWithCGImage:cgImage scale:image.scale orientation:UIImageOrientationUp]; CGImageRelease(cgImage); CGContextRelease(context); // performance results on iPhone 6S+ in Release mode. // Results are in photo pixels, not device pixels: // ~ 5ms for 500px x 600px // ~ 15ms for 2200px x 600px // NSLog(@"generating %dx %d @ %dx grayscale took %f seconds", (int)size.width, (int)size.height, (int)image.scale, [NSDate timeIntervalSinceReferenceDate] - start); return result; } 

O uso de modos de mesclagem é elegante, mas copiar para um bitmap em escala de cinza é mais eficiente, pois você usa apenas um ou dois canais de colors em vez de quatro. O bool de opacidade destina-se a receber a bandeira opaca do UIView, para que você possa desativar o uso de um canal alfa, caso saiba que não precisará de um.

Eu não tentei as soluções baseadas no Core Image neste segmento de resposta, mas eu seria muito cauteloso sobre o uso do Core Image se o desempenho for importante.

Essa é minha tentativa de converter rapidamente, desenhando diretamente no espaço de colors em tons de cinza, sem cada enumeração de pixels. Funciona 10x mais rápido que CIImageFilter soluções CIImageFilter .

 @implementation UIImage (Grayscale) static UIImage *grayscaleImageFromCIImage(CIImage *image, CGFloat scale) { CIImage *blackAndWhite = [CIFilter filterWithName:@"CIColorControls" keysAndValues:kCIInputImageKey, image, kCIInputBrightnessKey, @0.0, kCIInputContrastKey, @1.1, kCIInputSaturationKey, @0.0, nil].outputImage; CIImage *output = [CIFilter filterWithName:@"CIExposureAdjust" keysAndValues:kCIInputImageKey, blackAndWhite, kCIInputEVKey, @0.7, nil].outputImage; CGImageRef ref = [[CIContext contextWithOptions:nil] createCGImage:output fromRect:output.extent]; UIImage *result = [UIImage imageWithCGImage:ref scale:scale orientation:UIImageOrientationUp]; CGImageRelease(ref); return result; } static UIImage *grayscaleImageFromCGImage(CGImageRef imageRef, CGFloat scale) { NSInteger width = CGImageGetWidth(imageRef) * scale; NSInteger height = CGImageGetHeight(imageRef) * scale; NSMutableData *pixels = [NSMutableData dataWithLength:width*height]; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); CGContextRef context = CGBitmapContextCreate(pixels.mutableBytes, width, height, 8, width, colorSpace, 0); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGImageRef ref = CGBitmapContextCreateImage(context); UIImage *result = [UIImage imageWithCGImage:ref scale:scale orientation:UIImageOrientationUp]; CGContextRelease(context); CGColorSpaceRelease(colorSpace); CGImageRelease(ref); return result; } - (UIImage *)grayscaleImage { if (self.CIImage) { return grayscaleImageFromCIImage(self.CIImage, self.scale); } else if (self.CGImage) { return grayscaleImageFromCGImage(self.CGImage, self.scale); } return nil; } @end