UIImageView Transforming Touch Coordinates to Pixel Coordinates

Tutorial By 6 years ago

I have recently had a project where I had to do some image processing on an image based on where a user touches an image (in a UIImageView). Getting the touch coordinates was easy enough, but the challenge was turning those touch coordinates into pixel coordinates. Depending on the way the UIImageView is set to scale or crop an image it sometimes isn’t a big deal, however it can quickly become troublesome when you use basic scaling (the original images pixel coordinate will no longer match the touch coordinate). Looking around on the web showed that basically nothing was available in terms of open code, most users suggested people rolled their own, and although on the face of it, it sounds easy, when you look at the code I had to use to produce it is very boring and time consuming (hence why I am releasing it here).

Firstly I am declaring what is known as an objective C category. This allows me to add functions onto objects of a certain type of class in an ad-hoc way.

@interface UIImageView (CoordinateTransform)
-(CGPoint) pixelPointFromViewPoint:(CGPoint)viewPoint;
-(CGPoint) viewPointFromPixelPoint:(CGPoint)pixelPoint;
-(CGSize) pixelSizeFromViewSize:(CGSize)viewSize;
-(CGSize) viewSizeFromPixelSize:(CGSize)pixelSize;
@end

As you can see I have 4 new methods I want to add for UIImageView. One for converting a view (aka touch) point to a pixel point, one for converting a pixel point to a view point, a view size to a pixel size and a pixel size to a view size. This allows to me to do any kind of pixel operation on the image using only the touch from the view. The next thing we need to declare are the meat for the methods, done like so (warning, big scary chunk of code):

@implementation UIImageView (CoordinateTransform)
-(CGPoint) pixelPointFromViewPoint:(CGPoint)touch
{
	// Sanity check to see whether the touch is actually in the view
	if(touch.x >= 0.0 && touch.x <= self.frame.size.width && touch.y >= 0.0 && touch.y <= self.frame.size.height)
	{
		// http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIView_Class/UIView/UIView.html#//apple_ref/occ/cl/UIView
		switch(self.contentMode)
		{
			// Simply scale the image size by the size of the frame
			case UIViewContentModeScaleToFill:
			// Redraw is basically the same as scale to fill but redraws itself in the drawRect call (so when bounds change)
			case UIViewContentModeRedraw:
				return CGPointMake(floor(touch.x/(self.frame.size.width/self.image.size.width)),floor(touch.y/(self.frame.size.height/self.image.size.height)));
			// Although the documentation doesn't state it, we will assume a centered image. This mode makes the image fit into the view with its aspect ratio
			case UIViewContentModeScaleAspectFit:
			{
				// If the aspect ratio favours width over height in relation to the images aspect ratio
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
				{
					// Checking whether the touch coordinate is not in a 'blank' spot on the view
					if(touch.x >= (self.frame.size.width/2.0)-(((self.frame.size.height/self.image.size.height)*self.image.size.width)/2.0)
					   && touch.x <= (self.frame.size.width/2.0)+(((self.frame.size.height/self.image.size.height)*self.image.size.width)/2.0))
					{
						// Scaling by using the height ratio as a reference, and minusing the blank x coordiantes on the view
						return CGPointMake(floor((touch.x-((self.frame.size.width/2.0)-(((self.frame.size.height/self.image.size.height)*self.image.size.width)/2.0)))/(self.frame.size.height/self.image.size.height)),floor(touch.y/(self.frame.size.height/self.image.size.height)));
					}
					break;
				}
				// Or if the aspect ratio favours height over width in relation to the images aspect ratio
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
				{
					// Obtaining half of the view that is taken up by the aspect ratio
					CGFloat halfAspectFit = ((self.frame.size.width/self.image.size.width)*self.image.size.height)/2.0;
					// Checking whether the touch coordinate is not in a 'blank' spot on the view
					if(touch.y >= (self.frame.size.height/2.0)-halfAspectFit
					   && touch.y <= (self.frame.size.height/2.0)+halfAspectFit)
					{
						// Scaling by using the width ratio as a reference, and minusing the blank y coordinates on the view
						return CGPointMake(floor(touch.x/(self.frame.size.width/self.image.size.width)),floor((touch.y-((self.frame.size.width/2.0)-halfAspectFit))/(self.frame.size.height/self.image.size.height)));
					}
				}
				// This is just the same as a scale to fill mode if the aspect ratios from the view and the image are the same
				else return CGPointMake(floor(touch.x/(self.frame.size.width/self.image.size.width)),floor(touch.y/(self.frame.size.width/self.image.size.height)));
				break;
			}
			// This fills the view with the image in its aspect ratio, meaning that it could get cut off in either axis
			case UIViewContentModeScaleAspectFill:
			{
				// If the aspect ratio favours width over height in relation to the images aspect ratio
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
				{
					// Scaling by using the width ratio, this will cut off some height
					return CGPointMake(floor(touch.x/(self.frame.size.width/self.image.size.width)),floor(touch.y/(self.frame.size.width/self.image.size.width)));
				}
				// If the aspect ratio favours height over width in relation to the images aspect ratio
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
				{
					// Scaling by using the height ratio, this will cut off some width
					return CGPointMake(floor(touch.x/(self.frame.size.height/self.image.size.height)),floor(touch.y/(self.frame.size.height/self.image.size.height)));
				}
				// Again if the aspect ratios are the same, then it will just be another copy of scale to fill mode
				else return CGPointMake(floor(touch.x/(self.frame.size.width/self.image.size.width)),floor(touch.y/(self.frame.size.width/self.image.size.height)));
				break;
			}
			// This centers the image in the view both vertically and horizontally
			case UIViewContentModeCenter:
			{
				// Check whether our touch is on the image centered vertically and horizontally
				if(touch.x >= (self.frame.size.width/2.0)-(self.image.size.width/2.0)
				   && touch.x <= (self.frame.size.width/2.0)+(self.image.size.width/2.0)
				   && touch.y >= (self.frame.size.height/2.0)-(self.image.size.height/2.0)
				   && touch.y <= (self.frame.size.height/2.0)+(self.image.size.height/2.0))
					// Just return the touch coordinates and minus the offset
					return CGPointMake(floor(touch.x-((self.frame.size.width/2.0)-(self.image.size.width/2.0))),floor(touch.y-((self.frame.size.height/2.0)-(self.image.size.height/2.0))));
				break;
			}
			// This centers the image horizontally and moves it up to the top
			case UIViewContentModeTop:
			{
				// Check whether our touch is on the image centered horizontally and put at the vertical start
				if(touch.x >= (self.frame.size.width/2.0)-(self.image.size.width/2.0)
				   && touch.x <= (self.frame.size.width/2.0)+(self.image.size.width/2.0)
				   && touch.y <= self.image.size.height)
					// Just return the touch coordinates and minus the offset
					return CGPointMake(floor(touch.x-((self.frame.size.width/2.0)-(self.image.size.width/2.0))),floor(touch.y));
				break;
			}
			// This centers the image horizontally and moves it down to the bottom
			case UIViewContentModeBottom:
			{
				// Check whether our touch is on the image centered horizontally and put at the vertical end
				if(touch.x >= (self.frame.size.width/2.0)-(self.image.size.width/2.0)
				   && touch.x <= (self.frame.size.width/2.0)+(self.image.size.width/2.0)
				   && touch.y >= self.frame.size.height-self.image.size.height)
					// Just return the touch coordinates and minus the offset
					return CGPointMake(floor(touch.x-((self.frame.size.width/2.0)-(self.image.size.width/2.0))),floor(touch.y-(self.frame.size.height-self.image.size.height)));
				break;
			}
			// This moves the image to the horizontal start and centers it vertically
			case UIViewContentModeLeft:
			{
				// Check whether our touch is on the image at the horizontal start and centered vertically
				if(touch.x <= self.image.size.width
				   && touch.y >= (self.frame.size.height/2.0)-(self.image.size.height/2.0)
				   && touch.y <= (self.frame.size.height/2.0)+(self.image.size.height/2.0))
					return CGPointMake(floor(touch.x),floor(touch.y-((self.frame.size.height/2.0)-(self.image.size.height/2.0))));
				break;
			}
			// This moves the image to the horizontal end and centers it vertically
			case UIViewContentModeRight:
			{
				if(touch.x >= self.frame.size.width-self.image.size.width
				   && touch.y >= (self.frame.size.height/2.0)-(self.image.size.height/2.0)
				   && touch.y <= (self.frame.size.height/2.0)+(self.image.size.height/2.0))
					return CGPointMake(floor(touch.x-(self.frame.size.width-self.image.size.width)),floor(touch.y-((self.frame.size.height/2.0)-(self.image.size.height/2.0))));
				break;
			}
			// This simply moves the image to the horizontal and vertical start
			case UIViewContentModeTopLeft:
			{
				if(touch.x <= self.image.size.width
				   && touch.x <= self.image.size.height)
					// My favourite
					return CGPointMake(floor(touch.x),floor(touch.y));
				break;
			}
			// This moves the image to the horizontal end and vertical start
			case UIViewContentModeTopRight:
			{
				if(touch.x >= self.frame.size.width-self.image.size.width
				   && touch.y <= self.image.size.height)
					return CGPointMake(floor(touch.x-(self.frame.size.width-self.image.size.width)),floor(touch.y));
				break;
			}
			// This moves the image to the horizontal start and vertical end
			case UIViewContentModeBottomLeft:
			{
				if(touch.x <= self.image.size.width
				   && touch.y <= self.frame.size.height-self.image.size.height)
					return CGPointMake(floor(touch.x),floor(touch.y-(self.frame.size.height-self.image.size.height)));
				break;
			}
			// This moves the image to the horizontal and vertical end
			case UIViewContentModeBottomRight:
			{
				if(touch.x <= self.frame.size.width-self.image.size.width
				   && touch.y <= self.frame.size.height-self.image.size.height)
					return CGPointMake(floor(touch.x-(self.frame.size.width-self.image.size.width)),floor(touch.y-(self.frame.size.height-self.image.size.height)));
				break;
			}
			default: break;
		}
	}
	return CGPointZero;
}

-(CGPoint) viewPointFromPixelPoint:(CGPoint)pixelPoint
{
	// Sanity check to see whether the pixel point is actually in the image
	if(pixelPoint.x >= 0.0 && pixelPoint.x <= self.image.size.width && pixelPoint.y >= 0.0 && pixelPoint.y <= self.image.size.height)
	{
		switch(self.contentMode)
		{
			case UIViewContentModeScaleToFill:
			case UIViewContentModeRedraw:
				return CGPointMake(floor(pixelPoint.x*(self.frame.size.width/self.image.size.width)),floor(pixelPoint.y*(self.frame.size.height/self.image.size.height)));
			case UIViewContentModeScaleAspectFit:
			{
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
					return CGPointMake(floor(((self.frame.size.width/2.0)-((self.image.size.width/2.0)*(self.frame.size.height/self.image.size.height)))+pixelPoint.x*(self.frame.size.height/self.image.size.height)),floor(pixelPoint.y*(self.frame.size.height/self.image.size.height)));
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
					return CGPointMake(floor(pixelPoint.x*(self.frame.size.width/self.image.size.width)),floor(((self.frame.size.height/2.0)-((self.image.size.height/2.0)*(self.frame.size.width/self.image.size.width)))+pixelPoint.y*(self.frame.size.width/self.image.size.width)));
				return CGPointMake(floor(pixelPoint.x*(self.frame.size.width/self.image.size.width)),floor(pixelPoint.y*(self.frame.size.height/self.image.size.height)));
			}
			case UIViewContentModeScaleAspectFill:
			{
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
					return CGPointMake(floor(pixelPoint.x*(self.frame.size.width/self.image.size.width)),floor(pixelPoint.y*(self.frame.size.width/self.image.size.width)));
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
					return CGPointMake(floor(pixelPoint.x*(self.frame.size.height/self.image.size.height)),floor(pixelPoint.y*(self.frame.size.height/self.image.size.height)));
				return CGPointMake(floor(pixelPoint.x*(self.frame.size.width/self.image.size.width)),floor(pixelPoint.y*(self.frame.size.height/self.image.size.height)));
			}
			case UIViewContentModeCenter:
				return CGPointMake(floor(pixelPoint.x+(self.frame.size.width/2.0)-(self.image.size.width/2.0)),floor(pixelPoint.y+(self.frame.size.height/2.0)-(self.image.size.height/2.0)));
			case UIViewContentModeTop:
				return CGPointMake(floor(pixelPoint.x+(self.frame.size.width/2.0)-(self.image.size.width/2.0)),floor(pixelPoint.y));
			case UIViewContentModeBottom:
				return CGPointMake(floor(pixelPoint.x+(self.frame.size.width/2.0)-(self.image.size.width/2.0)),floor(pixelPoint.y-(self.frame.size.height-self.image.size.height)));
			case UIViewContentModeLeft:
				return CGPointMake(floor(pixelPoint.x),floor(pixelPoint.y+(self.frame.size.height/2.0)-(self.image.size.height/2.0)));
			case UIViewContentModeRight:
				return CGPointMake(floor(pixelPoint.x-(self.frame.size.width-self.image.size.width)),floor(pixelPoint.y+(self.frame.size.height/2.0)-(self.image.size.height/2.0)));
			case UIViewContentModeTopLeft:
				return CGPointMake(floor(pixelPoint.x),floor(pixelPoint.y));
			case UIViewContentModeTopRight:
				return CGPointMake(floor(pixelPoint.x-(self.frame.size.width-self.image.size.width)),floor(pixelPoint.y));
			case UIViewContentModeBottomLeft:
				return CGPointMake(floor(pixelPoint.x),floor(pixelPoint.y-(self.frame.size.height-self.image.size.height)));
			case UIViewContentModeBottomRight:
				return CGPointMake(floor(pixelPoint.x-(self.frame.size.width-self.image.size.width)),floor(pixelPoint.y-(self.frame.size.height-self.image.size.height)));
			default: break;
		}
	}
	return CGPointZero;
}

-(CGSize) pixelSizeFromViewSize:(CGSize)viewSize
{
	if(viewSize.width >= 0.0 && viewSize.width <= self.frame.size.width && viewSize.height >= 0.0 && viewSize.height <= self.frame.size.height)
	{
		switch(self.contentMode)
		{
			case UIViewContentModeScaleToFill:
			case UIViewContentModeRedraw:
				return CGSizeMake(floor(viewSize.width/(self.frame.size.width/self.image.size.width)),floor(viewSize.height/(self.frame.size.height/self.image.size.height)));
			case UIViewContentModeScaleAspectFit:
			{
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(viewSize.width/(self.frame.size.height/self.image.size.height)),floor(viewSize.height/(self.frame.size.height/self.image.size.height)));
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(viewSize.width/(self.frame.size.width/self.image.size.width)),floor(viewSize.height/(self.frame.size.height/self.image.size.height)));
				return CGSizeMake(floor(viewSize.width/(self.frame.size.width/self.image.size.width)),floor(viewSize.height/(self.frame.size.height/self.image.size.height)));
			}
			case UIViewContentModeScaleAspectFill:
			{
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(viewSize.width/(self.frame.size.width/self.image.size.width)),floor(viewSize.height/(self.frame.size.width/self.image.size.width)));
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(viewSize.width/(self.frame.size.height/self.image.size.height)),floor(viewSize.height/(self.frame.size.height/self.image.size.height)));
				return CGSizeMake(floor(viewSize.width/(self.frame.size.width/self.image.size.width)),floor(viewSize.height/(self.frame.size.height/self.image.size.height)));
			}
			case UIViewContentModeCenter:
			case UIViewContentModeTop:
			case UIViewContentModeBottom:
			case UIViewContentModeLeft:
			case UIViewContentModeRight:
			case UIViewContentModeTopLeft:
			case UIViewContentModeTopRight:
			case UIViewContentModeBottomLeft:
			case UIViewContentModeBottomRight:
				return CGSizeMake(floor(viewSize.width),floor(viewSize.height));
			default: break;
		}
	}
	return CGSizeZero;
}

-(CGSize) viewSizeFromPixelSize:(CGSize)pixelSize
{
	if(pixelSize.width >= 0.0 && pixelSize.width <= self.image.size.width && pixelSize.height >= 0.0 && pixelSize.height <= self.image.size.height)
	{
		switch(self.contentMode)
		{
			case UIViewContentModeScaleToFill:
			case UIViewContentModeRedraw:
				return CGSizeMake(floor(pixelSize.width*(self.frame.size.width/self.image.size.width)),floor(pixelSize.height*(self.frame.size.height/self.image.size.height)));
			case UIViewContentModeScaleAspectFit:
			{
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(pixelSize.width*(self.frame.size.height/self.image.size.height)),floor(pixelSize.height*(self.frame.size.height/self.image.size.height)));
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(pixelSize.width*(self.frame.size.width/self.image.size.width)),floor(pixelSize.height*(self.frame.size.height/self.image.size.height)));
				return CGSizeMake(floor(pixelSize.width*(self.frame.size.width/self.image.size.width)),floor(pixelSize.height*(self.frame.size.height/self.image.size.height)));
			}
			case UIViewContentModeScaleAspectFill:
			{
				if(self.frame.size.width/self.frame.size.height > self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(pixelSize.width*(self.frame.size.width/self.image.size.width)),floor(pixelSize.height*(self.frame.size.width/self.image.size.width)));
				else if(self.frame.size.width/self.frame.size.height < self.image.size.width/self.image.size.height)
					return CGSizeMake(floor(pixelSize.width*(self.frame.size.height/self.image.size.height)),floor(pixelSize.height*(self.frame.size.height/self.image.size.height)));
				return CGSizeMake(floor(pixelSize.width*(self.frame.size.width/self.image.size.width)),floor(pixelSize.height*(self.frame.size.height/self.image.size.height)));
			}
			case UIViewContentModeCenter:
			case UIViewContentModeTop:
			case UIViewContentModeBottom:
			case UIViewContentModeLeft:
			case UIViewContentModeRight:
			case UIViewContentModeTopLeft:
			case UIViewContentModeTopRight:
			case UIViewContentModeBottomLeft:
			case UIViewContentModeBottomRight:
				return CGSizeMake(floor(pixelSize.width),floor(pixelSize.height));
			default: break;
		}
	}
	return CGSizeZero;
}
@end

As you can see, quite a lot of code for simply transforming points and sizes, I had to do this all by hand so hopefully people can use this tutorial to avoid hours of gruelling programming. I uploaded the source files to the b2cloud server for easy insertion into a project:
UIImageView+CoordinateTransform.h
UIImageView+CoordinateTransform.m
In later posts, I will show you how to get the pixels from a UIImage using a category just like this one.

  • Pingback: UIImage Pre-Processing Category | b2cloud()

  • Paulo Miguel Almeida

    it has saved me from a big headache. I must confess I was struggling with it for a couple days until I find this post.
    Now that I saw how to solve I don’t think I was going to figure that out soon. Thanks mate