
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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(); |
Cropping Algorithm
- I merge the two bitmaps together making sure template is placed on top.
- Since I know the size of the template, create a new blank bitmap with those dimensions.
- From the centre of the combined image, go out quadrant by quadrant, going through every pixel value and copy it onto the blank bitmap.
- Once it hits the colour of the template lines, then from that point on set the pixel value of the blank bitmap transparent.
- After it has gotten through all 4 quadrants, return the created bitmap, which is the cropped image.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; | |
} |
Enjoy :)
Link to android-cropping-example.