views:

1023

answers:

4

Hi,

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.

+3  A: 

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.

Ahruman
+4  A: 

You could let Quartz do the grayscale conversion for you:

CGImageRef grayscaleCGImageFromCGImage(CGImageRef inputImage) {
    size_t width = CGImageGetWidth(inputImage);
    size_t height = CGImageGetHeight(inputImage);

    // Create a gray scale context and render the input image into that
    CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceGray();
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 
                             4*width, colorspace, kCGBitmapByteOrderDefault);
    CGContextDrawImage(context, CGRectMake(0,0, width,height), inputImage);

    // Get an image representation of the grayscale context which the input
    //    was rendered into.
    CGImageRef outputImage = CGBitmapContextCreateImage(context);

    // Cleanup
    CGContextRelease(context);
    CGColorSpaceRelease(colorspace);
    return (CGImageRef)[(id)outputImage autorelease];

}

retainCount
+1  A: 

I had to solve this same problem recently and came up with the following code (it also preserves alpha):

@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
Cam
+1  A: 
mahboudz