13

UIImage has a read-only property CGImage. I have to read its pixels to a memory block and edit them and then make a new UIImage to replace the old one. I want to know if there is a way bypass the read-only property and edit those pixels directly.

Thanks.


Thanks all. I have found a way to do it. Write a class with those method:

-(void)preProcess:(UIImage*)srcImage {
    m_Context = ...// Created by calling CGBitmapContextCreate(...)
    ...
    CGContextDrawImage(m_Context, rect, srcImage.CGImage);
    m_Bits = (unsigned char*)CGBitmapContextGetData (mContext);
}

-(void)postProcess {
    CGContextRelease(m_Context);
    free(m_Bits);
}

-(UIImage*)doProcess:(CGPoint)pt {// just a example 
    unsigned char* ppxl = m_Bits + ...
    // do something...
    CGImageRef imRef = CGBitmapContextCreateImage(mContext);
    return [UIImage imageWithCGImage:imRef];
}

And preProcess and postProcess are called just once.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
Yantao Xie
  • 12,300
  • 15
  • 49
  • 79
  • hey i have understand your given example but is it possible to post a source code here.? i am also trying to implement like the asked question. – Hrushikesh Betai Apr 16 '12 at 12:46

7 Answers7

14

You cannot get at the original pixels. However, you can get a copy. One option is to do what Matt suggested, and convert it into a PNG/JPG - though remember, the image is now compressed, and you will be manipulating the compressed file and not the pixels directly.

If you want to get at a copy of the raw pixels, you can do something like:

UIImage* image = ...; // An image
NSData* pixelData = (NSData*) CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
void* pixelBytes = [pixelData bytes];

// Take away the red pixel, assuming 32-bit RGBA
for(int i = 0; i < [pixelData length]; i += 4) {
    bytes[i] = 0; // red
    bytes[i+1] = bytes[i+1]; // green
    bytes[i+2] = bytes[i+2]; // blue
    bytes[i+3] = bytes[i+3]; // alpha
}

Now, if you wanted to make this into a new UIImage, you can do something like:

NSData* newPixelData = [NSData dataWithBytes:pixelBytes length:[pixelData length]];
UIImage* newImage = [UIImage imageWithData:newPixelData]; // Huzzah
Itay
  • 1,669
  • 5
  • 18
  • 23
  • 2
    I tried but failed. CGDataProviderCopyData return a CFDataRef which is a reference to an immutable CFData object according to the iPhone doc. – Yantao Xie Aug 16 '09 at 07:26
14

For the more obtuse among us (read: future me) here's some working code based on Itay and Dave R's answers. It starts with a UIImage and ends with a modified UIImage:

    // load image
    UIImage *image      = [UIImage imageNamed:@"test.png"];
    CGImageRef imageRef = image.CGImage;
    NSData *data        = (NSData *)CGDataProviderCopyData(CGImageGetDataProvider(imageRef));
    char *pixels        = (char *)[data bytes];

    // this is where you manipulate the individual pixels
    // assumes a 4 byte pixel consisting of rgb and alpha
    // for PNGs without transparency use i+=3 and remove int a
    for(int i = 0; i < [data length]; i += 4)
    {
        int r = i;
        int g = i+1;
        int b = i+2;
        int a = i+3;

        pixels[r]   = 0; // eg. remove red
        pixels[g]   = pixels[g];
        pixels[b]   = pixels[b];
        pixels[a]   = pixels[a];
    }

    // create a new image from the modified pixel data
    size_t width                    = CGImageGetWidth(imageRef);
    size_t height                   = CGImageGetHeight(imageRef);
    size_t bitsPerComponent         = CGImageGetBitsPerComponent(imageRef);
    size_t bitsPerPixel             = CGImageGetBitsPerPixel(imageRef);
    size_t bytesPerRow              = CGImageGetBytesPerRow(imageRef);

    CGColorSpaceRef colorspace      = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo         = CGImageGetBitmapInfo(imageRef);
    CGDataProviderRef provider      = CGDataProviderCreateWithData(NULL, pixels, [data length], NULL);

    CGImageRef newImageRef = CGImageCreate (
                              width,
                              height,
                              bitsPerComponent,
                              bitsPerPixel,
                              bytesPerRow,
                              colorspace,
                              bitmapInfo,
                              provider,
                              NULL,
                              false,
                              kCGRenderingIntentDefault
                              );
    // the modified image
    UIImage *newImage   = [UIImage imageWithCGImage:newImageRef];

    // cleanup
    free(pixels);
    CGImageRelease(imageRef);
    CGColorSpaceRelease(colorspace);
    CGDataProviderRelease(provider);
    CGImageRelease(newImageRef);
Shaun Inman
  • 1,968
  • 1
  • 19
  • 28
  • 3
    This example assumes you are using a png with transparency. If you are not using transparency change the `i += 4` in the for loop to `i += 3` (I'm sure there's a more intelligent way to determine the pixel size but graphics programming is not my strong suit). – Shaun Inman Jul 05 '11 at 21:31
  • Treating the pixels as `unsigned char` (0-255) makes it easier to work with them. – fphilipe Mar 28 '12 at 13:15
6

This may help you out.

When you're done, you can use CGImageCreate to create a CGImageRef, then use +[UIImage imageWithCGImage:] to create a new UIImage.

Dave R
  • 605
  • 3
  • 5
  • hey i have understand your given example but is it possible to post a source code here.? i am also trying to implement like the asked question. – Hrushikesh Betai Apr 16 '12 at 12:45
4

I used the following code to add 2 thin red lines at right and left of an image

-(UIImage*)ModifyImage:(UIImage*) img
{
    UIGraphicsBeginImageContext(img.size);

    [img drawInRect:CGRectMake(0,0,img.size.width,img.size.height) blendMode:kCGBlendModeSourceOut alpha:1.0f];

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    int w =img.size.width;
    int cw,ch;

    cw = img.size.width / 35;
    ch = img.size.height / 35;

    unsigned char* data = CGBitmapContextGetData (ctx);

    for(int y = 0 ; y < img.size.height ; y++)
    {
        for(int x = 0 ; x < img.size.width ; x++)
        {
            //int offset = 4*((w * y) + x);

            int offset = (CGBitmapContextGetBytesPerRow(ctx)*y) + (4 * x);

            int blue    =  data[offset];
            int green   = data[offset+1];
            int red     = data[offset+2];
            //int alpha   = data[offset+3];

            if(x <= (cw * 2) || x >= (cw * 35))
            {
                data[offset]    = 0;
                data[offset+1]  = 0;
                data[offset+2]  = 255;
                data[offset+3]  = 255;
            }
        }
    }

    UIImage *rtimg = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return rtimg;
}
ossamacpp
  • 656
  • 5
  • 11
3

The short answer is no. However, you say you have to make a copy anyhow, so why not just get an NSData object and manipulate its bytes.

From the Apple docs on UIImage:

Because image objects are immutable, they also do not provide direct access to their underlying image data. However, you can get an NSData object containing either a PNG or JPEG representation of the image data using the UIImagePNGRepresentation and UIImageJPEGRepresentation functions.

To get the data as a PNG, use:

NSData * UIImagePNGRepresentation (
   UIImage *image
);

for JPEG, use:

NSData * UIImageJPEGRepresentation (
   UIImage *image,
   CGFloat compressionQuality
);
Matt Long
  • 24,438
  • 4
  • 73
  • 99
  • You really don't have to sign your posts. We know who you are, your name and picture are less than 300px to the right of your extra sig. You also totally failed to explain why he can't directly edit the pixels or why it's read only in the first place. – Sneakyness Aug 15 '09 at 06:04
  • 3
    I would say you've just been awarded the "Captain Kranky Pants" badge. Re-read the question. I assure you. He did not ask 'why'. -Matt (<-- oops. that darned extra sig again.) – Matt Long Aug 15 '09 at 07:00
  • 3
    Matt certainly did answer why it's read-only - he quoted from the Apple docs, specifically saying images are considered immutable. Given that as a constraint, any modification you wanted to make would have to be on a copy. However, the answer is not correct. You would not be getting the raw pixels in this case, but a compressed version of the image as a PNG/JPG. Unless you implement a JPG/PNG decompression algorithm, you will not be able to modify the pixel data. – Itay Aug 15 '09 at 09:28
  • Thanks for the clarification Itay. That's quite helpful. I was forgetting that part. You're absolutely right. You would have the compressed data at that point and not the image data. Very nice solution. I voted you up! ;-) – Matt Long Aug 15 '09 at 16:57
  • Matt's excerpt is helpful for me. – Yantao Xie Aug 16 '09 at 02:04
2

Change the NSData *data line, in Shaun's answer, to this and it works perfectly in iOS7!

 NSData *data        = (NSData *)CFBridgingRelease(CGDataProviderCopyData(CGImageGetDataProvider(imageRef)));
user229044
  • 232,980
  • 40
  • 330
  • 338
0

@shaun Inman's post has been the only working i've found on the web so far! THANK YOU! However it seems like at least in IOS3 the PNG colors are saved a bit different to what you stated, this is what worked for me (only for PNG!):

for(int i = 0; i < [data length]; i += 4) {
    pixels[i+0]   = 0; // eg. alpha?
    pixels[i+1]   = 0; // eg. red!
    pixels[i+2]   = 0; // eg. green!
    pixels[i+3]   = pixels[i+3]; // eg. blue!
}

Would be cool if someone could figure out a way that worked for any filetype!

OZZIE
  • 6,609
  • 7
  • 55
  • 59
  • You have to look at the `CGBitmapInfo` and bits per component to decipher how it's encoded and then programmatically handle every permutation (RGBA, ARGB, CMYK, 8bpc, 16bpc, integer vs floating point components, etc.) – Rob Apr 07 '14 at 13:26