Wednesday, July 24, 2013

Android Image Cropping Example

When I was creating PsyYourself, I was faced with a challenge on how I can crop out the face with an image that the user provides. One option was to somehow apply Optical Character Recognition (OCR) to detect faces in an image and crop that part out, but at the time OCR was something out of my league to create.

Instead I optioned to have a set of templates that make up
the various shapes of faces and from there, users will be able to orient, either by zooming in or out or moving the image, to fit inside the template. Once that was done, it was up to the device to crop out the image. The problem with this is that there were no available cropping tools/algorithms that I can make use of. I mean, there are existing cropping APIs, but the only problem with that is that they crop out rectangular shaped images, which is something that I did not want. Therefore I am posting this blog to show how I did this. The example project can be found on Github android-cropping-example.

Project Setup

Now, both the image that is going to cropped and the template image has to be overlapped on the screen so that users can easily orient the image to fit into the template. This can be accomplished by using FrameLayout and having two ImageView as its children. As seen below.

<FrameLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1.0" >
<ImageView
android:id="@+id/cp_img"
android:contentDescription="@string/cp_image_contentDesc"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:scaleType="matrix" />
<ImageView
android:id="@+id/cp_face_template"
android:contentDescription="@string/cp_template_contentDesc"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="@drawable/face_oval"
android:scaleType="centerInside" />
</FrameLayout>
The first ImageView with the id cp_img will be the image that user will be orienting. While the second ImageView cp_face_template with scaleType as centerInside because I want to use the exact size of my templates that I have created.

Once the users are done with orienting the image, we then now have to get those images from ImageView the way how it is displayed on the screen. To do this we must first call View.buildDrawingCache() and View.setDrawingCacheEnabled() with the boolean value true as their arguments. Then, to get the bitmap, call View.getDrawingCache(). Lastly, call View.setDrawingCacheEnabled() again and this time with the boolean value false passed in. This process will allow you to retrieve images from ImageView multiple times.

// Setting values so that we can retrive the image from
// ImageView multiple times.
mImg.buildDrawingCache(true);
mImg.setDrawingCacheEnabled(true);
mTemplateImg.buildDrawingCache(true);
mTemplateImg.setDrawingCacheEnabled(true);
// Create new thread to crop.
new Thread(new Runnable() {
@Override
public void run() {
// Crop image using the correct template size.
Bitmap croppedImg = null;
if (mScreenWidth == 320 && mScreenHeight == 480) {
// getDrawingCache() method gets the bitmap.
croppedImg = ImageProcess.cropImage(mImg.getDrawingCache(true), mTemplateImg.getDrawingCache(true), 218, 300);
} else {
croppedImg = ImageProcess.cropImage(mImg.getDrawingCache(true), mTemplateImg.getDrawingCache(true), 320, 440);
}
// Set cache back to false to that we can retrieve bitmap again.
mImg.setDrawingCacheEnabled(false);
mTemplateImg.setDrawingCacheEnabled(false);
// Send a message to the Handler indicating the Thread has finished.
mCropHandler.obtainMessage(DISPLAY_IMAGE, -1, -1, croppedImg).sendToTarget();
}
}).start();
Once, we have retrieved both the template and the oriented image, it's time to put the device to work. As we can see above, I created another thread to handle that work as we don't want the main thread to be blocked because it's processing. So, now this is where my algorithm that I have create comes into play.

Cropping Algorithm

The main idea of how my algorithm works is:
  1. I merge the two bitmaps together making sure template is placed on top.
  2. Since I know the size of the template, create a new blank bitmap with those dimensions.
  3. From the centre of the combined image, go out quadrant by quadrant, going through every pixel value and copy it onto the blank bitmap.
  4. Once it hits the colour of the template lines, then from that point on set the pixel value of the blank bitmap transparent.
  5. After it has gotten through all 4 quadrants, return the created bitmap, which is the cropped image. 
Here is the cropping code:
/*
* This method gets the image inside the template area and returns that bitmap.
* Process of how this method works:
* 1) Combine img and templateImage together with templateImage being on the top.
* 2) Create a new blank bitmap using the given width and height, which is the
* size of the template on the screen.
* 3) Starting in the center go through the image quadrant by quadrant copying
* the pixel value onto the new blank bitmap.
* 4) Once it hits the pixel colour values of the template lines, then set the pixel
* values from that point on transparent.
* 5) Return the cropped bitmap.
*/
public static Bitmap cropImage(Bitmap img, Bitmap templateImage, int width, int height) {
// Merge two images together.
Bitmap bm = Bitmap.createBitmap(img.getWidth(), img.getHeight(), Bitmap.Config.ARGB_8888);
Canvas combineImg = new Canvas(bm);
combineImg.drawBitmap(img, 0f, 0f, null);
combineImg.drawBitmap(templateImage, 0f, 0f, null);
// Create new blank ARGB bitmap.
Bitmap finalBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
// Get the coordinates for the middle of combineImg.
int hMid = bm.getHeight() / 2;
int wMid = bm.getWidth() / 2;
int hfMid = finalBm.getHeight() / 2;
int wfMid = finalBm.getWidth() / 2;
int y2 = hfMid;
int x2 = wfMid;
// Top half of the template.
for (int y = hMid; y >= 0; y--) {
boolean template = false;
// Check Upper-left section of combineImg.
for (int x = wMid; x >= 0; x--) {
if (x2 < 0) {
break;
}
int px = bm.getPixel(x, y);
if (Color.red(px) == 234 && Color.green(px) == 157 && Color.blue(px) == 33) {
template = true;
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else if (template) {
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else {
finalBm.setPixel(x2, y2, px);
}
x2--;
}
// Check upper-right section of combineImage.
x2 = wfMid;
template = false;
for (int x = wMid; x < bm.getWidth(); x++) {
if (x2 >= finalBm.getWidth()) {
break;
}
int px = bm.getPixel(x, y);
if (Color.red(px) == 234 && Color.green(px) == 157 && Color.blue(px) == 33) {
template = true;
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else if (template) {
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else {
finalBm.setPixel(x2, y2, px);
}
x2++;
}
// Once we reach the top-most part on the template line, set pixel value transparent
// from that point on.
int px = bm.getPixel(wMid, y);
if (Color.red(px) == 234 && Color.green(px) == 157 && Color.blue(px) == 33) {
for (int y3 = y2; y3 >= 0; y3--) {
for (int x3 = 0; x3 < finalBm.getWidth(); x3++) {
finalBm.setPixel(x3, y3, Color.TRANSPARENT);
}
}
break;
}
x2 = wfMid;
y2--;
}
x2 = wfMid;
y2 = hfMid;
// Bottom half of the template.
for (int y = hMid; y <= bm.getHeight(); y++) {
boolean template = false;
// Check bottom-left section of combineImage.
for (int x = wMid; x >= 0; x--) {
if (x2 < 0) {
break;
}
int px = bm.getPixel(x, y);
if (Color.red(px) == 234 && Color.green(px) == 157 && Color.blue(px) == 33) {
template = true;
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else if (template) {
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else {
finalBm.setPixel(x2, y2, px);
}
x2--;
}
// Check bottom-right section of combineImage.
x2 = wfMid;
template = false;
for (int x = wMid; x < bm.getWidth(); x++) {
if (x2 >= finalBm.getWidth()) {
break;
}
int px = bm.getPixel(x, y);
if (Color.red(px) == 234 && Color.green(px) == 157 && Color.blue(px) == 33) {
template = true;
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else if (template) {
finalBm.setPixel(x2, y2, Color.TRANSPARENT);
} else {
finalBm.setPixel(x2, y2, px);
}
x2++;
}
// Once we reach the bottom-most part on the template line, set pixel value transparent
// from that point on.
int px = bm.getPixel(wMid, y);
if (Color.red(px) == 234 && Color.green(px) == 157 && Color.blue(px) == 33) {
for (int y3 = y2; y3 < finalBm.getHeight(); y3++) {
for (int x3 = 0; x3 < finalBm.getWidth(); x3++) {
finalBm.setPixel(x3, y3, Color.TRANSPARENT);
}
}
break;
}
x2 = wfMid;
y2++;
}
return finalBm;
}
Feel free to view, download, and make use of my code HERE!!
Enjoy :)

Link to android-cropping-example.