views:

945

answers:

2

I have an UIImage and want to shift it's saturation about +10%. Are there standard methods or functions that can be used for this?

+2  A: 

Nothing quite that straightforward. The easiest solution is probably to make an in-memory CGContext with a known pixel format, draw the image into that context, then read/modify the pixels in the known-format buffer.

I don't think CG supports a color space with a separate saturation channel, so you'll have to either convert from RGB to HSV or HSL, or do the calculations directly in the RGB space.


One way to do the calculation directly in RGB might be something like this:

average = (R + G + B) / 3;
red_delta = (R - average) * 11 / 10;
green_delta = (G - average) * 11 / 10;
blue_delta = (B - average) * 11 / 10;

R = average + red_delta;
G = average + green_delta;
B = average + blue_delta;
// clip R,G,B to be in 0-255 range

This will move the channels that are away from the mean about 10% further away. This is almost like increasing the saturation, though it'll give hue shifts for some colors.

Mark Bessey
+3  A: 

Starting with a View-based Application Template, create a new subclass of UIView like so:

// header file
@interface DesatView : UIView {
     UIImage *image;
     float saturation;
}
@property (nonatomic, retain) UIImage *image;
@property (nonatomic) float desaturation;
@end  

// implementation file
#import "DesatView.h"
@implementation DesatView
@synthesize image, desaturation;

-(void)setSaturation:(float)sat;
    {
        saturation = sat;
        [self setNeedsDisplay];
    }

- (id)initWithFrame:(CGRect)frame {
     if (self = [super initWithFrame:frame]) {
          self.backgroundColor = [UIColor clearColor]; // else background is black
          desaturation = 0.0; // default is no effect
     }
     return self;
}

- (void)drawRect:(CGRect)rect {
     CGContextRef context = UIGraphicsGetCurrentContext();
     CGContextSaveGState(context);

     CGContextTranslateCTM(context, 0.0, self.bounds.size.height); // flip image right side up
     CGContextScaleCTM(context, 1.0, -1.0);

     CGContextDrawImage(context, rect, self.image.CGImage);
     CGContextSetBlendMode(context, kCGBlendModeSaturation);
     CGContextClipToMask(context, self.bounds, image.CGImage); // restricts drawing to within alpha channel
     CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, desaturation);
     CGContextFillRect(context, rect);

     CGContextRestoreGState(context); // restore state to reset blend mode
}
@end

Now in your view controller's viewDidLoad method, put the view on screen and set it's saturation like this:

- (void)viewDidLoad {
    [super viewDidLoad];  

    DesatView *dv = [[DesatView alloc] initWithFrame:CGRectZero];
    dv.image = [UIImage imageNamed:@"someImage.png"];
    dv.frame = CGRectMake(0, 0, dv.image.size.width, dv.image.size.height);
    dv.center = CGPointMake(160, 240); // put it mid-screen
    dv.desaturation = 0.2; // desaturate by 20%,

    [self.view addSubview:dv];  // put it on screen   
}  

Change the saturation like this:

 dv.saturation = 0.8; // desaturate by 80%  

Obviously if you want to use it outside of a single method, you should make dv an ivar of the view controller. Hope this helps.

willc2
Why not just call [self setNeedsDisplay] in the setter for desaturation?
Mark Bessey
My brain shied away from overriding the synthesized setter for some unknown reason but you are right. I'll change the answer.
willc2
So this decreases saturation, how would you _increase_ saturation?
Shizam