Convert to grayscale - too slow

cocoa-touch, iphone

Solution

The way to find out your speed issue is to profile using Shark. (In Xcode, Run->Start with Performance Tool->Shark.) However, in this case I feel reasonably certain that the primary problems are the per-pixel malloc/free, the floating-point arithmetic, and the two method calls in the inner processing loop.

To avoid the malloc/free, you want to be doing something like this instead:

- (void) getColorForPixelX:(NSUInteger)x y:(NSUInteger)y pixel:(float[3])pixel
{ /* Write stuff to pixel[0], pixel[1], pixel[2] */ }

// To call:
float pixel[3];
for (each pixel)
{
    [self getColorForPixelX:x y:y pixel:pixel];
    // Do stuff
}

The second likely source of slowdown is the use of floating point – or rather, the cost of converting to and from floating point. For the filter you’re writing, working in integer maths is simple – add the integer pixel values and divide by 255*3. (Incidentally, that’s a pretty bad way to convert to greyscale. See http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale.)

Method calls are fast for what they are, but still pretty slow compared to the basic arithmetic of the filter. (For some numbers, see this article.) The easy way to eliminate the method calls is to replace them with inline functions.

Problem

I've made a class that converts an image into grayscale. But it works way too slow. Is there a way to make it work faster? Here's my class: ``` @implementation PixelProcessing SYNTHESIZE_SINGLETON_FOR_CLASS(PixelProcessing); #define bytesPerPixel 4 #define bitsPerComponent 8 -(UIImage*)scaleAndRotateImage: (UIImage*)img withMaxResolution: (int)kMaxResolution { CGImageRef imgRef = img.CGImage; CGFloat width = CGImageGetWidth(imgRef); CGFloat height = CGImageGetHeight(imgRef); CGAffineTransform transform = CGAffineTransformIdentity; CGRect bounds = CGRectMake(0, 0, width, height); if ( (kMaxResolution != 0) && (width > kMaxResolution || height > kMaxResolution) ) { CGFloat ratio = width/height; if (ratio > 1) { bounds.size.width = kMaxResolution; bounds.size.height = bounds.size.width / ratio; } else { bounds.size.height = kMaxResolution; bounds.size.width = bounds.size.height * ratio; } } CGFloat scaleRatio; if (kMaxResolution != 0){ scaleRatio = bounds.size.width / width; } else { scaleRatio = 1.0f; } CGSize imageSize = CGSizeMake(CGImageGetWidth(imgRef), CGImageGetHeight(imgRef)); CGFloat boundHeight; UIImageOrientation orient = img.imageOrientation; switch(orient) { case UIImageOrientationUp: //EXIF = 1 transform = CGAffineTransformIdentity; break; case UIImageOrientationUpMirrored: //EXIF = 2 transform = CGAffineTransformMakeTranslation(imageSize.width, 0.0); transform = CGAffineTransformScale(transform, -1.0, 1.0); break; case UIImageOrientationDown: //EXIF = 3 transform = CGAffineTransformMakeTranslation(imageSize.width, imageSize.height); transform = CGAffineTransformRotate(transform, M_PI); break; case UIImageOrientationDownMirrored: //EXIF = 4 transform = CGAffineTransformMakeTranslation(0.0, imageSize.height); transform = CGAffineTransformScale(transform, 1.0, -1.0); break; case UIImageOrientationLeftMirrored: //EXIF = 5 boundHeight = bounds.size.height; bounds.size.height = bounds.size.width; bounds.size.width = boundHeight; transform = CGAffineTransformMakeTranslation(imageSize.height, imageSize.width); transform = CGAffineTransformScale(transform, -1.0, 1.0); transform = CGAffineTransformRotate(transform, 3.0 * M_PI / 2.0); break; case UIImageOrientationLeft: //EXIF = 6 boundHeight = bounds.size.height; bounds.size.height = bounds.size.width; bounds.size.width = boundHeight; transform = CGAffineTransformMakeTranslation(0.0, imageSize.width); transform = CGAffineTransformRotate(transform, 3.0 * M_PI / 2.0); break; case UIImageOrientationRightMirrored: //EXIF = 7 boundHeight = bounds.size.height; bounds.size.height = bounds.size.width; bounds.size.width = boundHeight; transform = CGAffineTransformMakeScale(-1.0, 1.0); transform = CGAffineTransformRotate(transform, M_PI / 2.0); break; case UIImageOrientationRight: //EXIF = 8 boundHeight = bounds.size.height; bounds.size.height = bounds.size.width; bounds.size.width = boundHeight; transform = CGAffineTransformMakeTranslation(imageSize.height, 0.0); transform = CGAffineTransformRotate(transform, M_PI / 2.0); break; default: [NSException raise:NSInternalInconsistencyException format: @"Invalid image orientation"]; } UIGraphicsBeginImageContext(bounds.size); CGContextRef context = UIGraphicsGetCurrentContext(); if (orient == UIImageOrientationRight || orient == UIImageOrientationLeft) { CGContextScaleCTM(context, -scaleRatio, scaleRatio); CGContextTranslateCTM(context, -height, 0); } else { CGContextScaleCTM(context, scaleRatio, -scaleRatio); CGContextTranslateCTM(context, 0, -height); } CGContextConcatCTM(context, transform); CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, width, height), imgRef); UIImage *tempImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return tempImage; } #pragma mark Getting Ans Writing Pixels -(float*) getColorForPixel: (NSUInteger)xCoordinate andForY: (NSUInteger)yCoordinate { int byteIndex = (bytesPerRow * yCoordinate) + xCoordinate * bytesPerPixel; float *colorToReturn = malloc(3); colorToReturn[0] = bitmap[byteIndex] / 255.f; //Red colorToReturn[1] = bitmap[byteIndex + 1] / 255.f; //Green colorToReturn[2] = bitmap[byteIndex + 2] / 255.f; //Blue return colorToReturn; } -(void) writeColor: (float*)colorToWrite forPixelAtX: (NSUInteger)xCoordinate andY: (NSUInteger)yCoordinate { int byteIndex = (bytesPerRow * yCoordinate) + xCoordinate * bytesPerPixel; bitmap[byteIndex] = (unsigned char) ( colorToWrite[0] * 255); bitmap[byteIndex + 1] = (unsigned char) ( colorToWrite[1] * 255); bitmap[byteIndex + 2] = (unsigned char) ( colorToWrite[2] * 255); } #pragma mark Bitmap -(float) getAverageBrightnessForImage: (UIImage*)img { UIImage *tempImage = [self scaleAndRotateImage: img withMaxResolution: 100]; unsigned char *rawData = [self getBytesForImage: tempImage]; double aBrightness = 0; for(int y = 0; y < tempImage.size.height; y++) { for(int x = 0; x < tempImage.size.width; x++) { int byteIndex = ( (tempImage.size.width * y) + x) * bytesPerPixel; aBrightness += (rawData[byteIndex] + rawData[byteIndex + 1] + rawData[byteIndex + 2]); } } free(rawData); aBrightness /= 3.0f; aBrightness /= 255.0f; aBrightness /= tempImage.size.width * tempImage.size.height; return aBrightness; } -(unsigned char*) getBytesForImage: (UIImage*)pImage { CGImageRef image = [pImage CGImage]; NSUInteger width = CGImageGetWidth(image); NSUInteger height = CGImageGetHeight(image); bytesPerRow = bytesPerPixel * width; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); unsigned char *rawData = malloc(height * width * bytesPerPixel); CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast); CGColorSpaceRelease(colorSpace); CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); CGContextRelease(context); return rawData; } -(void) loadWithImage: (UIImage*)img { averageBrightness = [self getAverageBrightnessForImage: img]; currentImage = [self scaleAndRotateImage: img withMaxResolution: 0]; imgWidth = currentImage.size.width; imgHeight = currentImage.size.height; bitmap = [self getBytesForImage: currentImage]; bytesPerRow = bytesPerPixel * imgWidth; } -(void) processImage { // now convert to grayscale for(int y = 0; y < imgHeight; y++) { for(int x = 0; x < imgWidth; x++) { float *currentColor = [self getColorForPixel: x andForY: y]; //Grayscale float averageColor = (currentColor[0] + currentColor[1] + currentColor[2]) / 3.0f; averageColor += 0.5f - averageBrightness; if (averageColor > 1.0f) averageColor = 1.0f; currentColor[0] = averageColor; currentColor[1] = averageColor; currentColor[2] = averageColor; [self writeColor: currentColor forPixelAtX: x andY: y]; free(currentColor); } } } -(UIImage*) getProcessedImage { // create a UIImage CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(bitmap, imgWidth, imgHeight, bitsPerComponent, bytesPerRow, colorSpace, kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast); CGImageRef image = CGBitmapContextCreateImage(context); CGContextRelease(context); CGColorSpaceRelease(colorSpace); UIImage *resultUIImage = [UIImage imageWithCGImage: image]; CGImageRelease(image); return resultUIImage; } -(void) releaseCurrentImage { free(bitmap); } @end ``` And I convert an image into grayscale in the following way: ``` [ [PixelProcessing sharedPixelProcessing] loadWithImage: imageToDisplay.image]; [ [PixelProcessing sharedPixelProcessing] processImage]; imageToDisplay.image = [ [PixelProcessing sharedPixelProcessing] getProcessedImage]; [ [PixelProcessing sharedPixelProcessing] releaseCurrentImage]; ``` Why is it working so slow? Is there a way to get float values for RGB color components of pixel? How can I optimize it? Thanks.

Original source