Sunday, 11 August 2013

Optimising Canny Edge Detector for Android

Optimising Canny Edge Detector for Android

I'm creating an Android app that runs some image processing on pictures,
and the first phase of the processing is to run a Canny Edge Detector.
While it works for smaller pictures, as soon as there is above a certain
threshold of pixels in the picture, the app gets shut down for excessive
memory use.
I have found that the app gets killed in the computeGradients() method
because there are just too many (pretty large) arrays created, but I can't
see any way to get around this issue! Each array is completely necessary
in this implementation (adapted for Android from
http://www.tomgibara.com/computer-vision/CannyEdgeDetector.java). Could I
get any ideas as to how to prevent this from happening?
CannyEdgeDetector code:
package com.example.myfirstapp;
import android.app.Activity;
import android.graphics.Bitmap;
import android.util.Log;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* User: Wesley
* Date: 7/29/13
* Time: 5:26 PM
*/
public class CannyEdgeDetector extends Activity {
private final static float GAUSSIAN_CUT_OFF = 0.005f;
private final static float MAGNITUDE_SCALE = 10F;
private final static float MAGNITUDE_LIMIT = 100F;
private final static int MAGNITUDE_MAX = (int) (MAGNITUDE_SCALE *
MAGNITUDE_LIMIT);
private int height;
private int width;
private int picSize;
private int[] data;
private int[] magnitude;
private Bitmap sourceImg;
private Bitmap edgesImg;
private float gaussianKernelRadius;
private float lowThreshold;
private float highThreshold;
private int gaussianKernelWidth;
private boolean contrastNormalised;
private int mFollowStackDepth = 100;
public CannyEdgeDetector () {
lowThreshold = 2.5f;
highThreshold = 7.5f;
gaussianKernelRadius = 3f;
gaussianKernelWidth = 18;
contrastNormalised = false;
}
/**
* Accessor method which simply returns the bitmap image that is
* being used as the source.
* @return the source image, or null
*/
public Bitmap getSourceImg () {
return sourceImg;
}
/**
* Accessor method which simply returns the bitmap image with edge
* detection.
* @return the source image, or null
*/
public Bitmap getEdgesImg () {
return edgesImg;
}
/**
* Mutator method to set the source image
* @param sourceImg_ the image to set the source image variable to
*/
public void setSourceImg (Bitmap sourceImg_) {
sourceImg = sourceImg_.copy(Bitmap.Config.ARGB_8888, true);
}
/**
* Mutator method to set the source image
* @param edgesImg_ the image to set the source image variable to
*/
public void setEdgesImg (Bitmap edgesImg_) {
edgesImg = edgesImg_;
}
/**
* Sets whether the contrast is normalized
* @param contrastNormalised true if the contrast should be normalized,
* false otherwise
*/
public void setContrastNormalised(boolean contrastNormalised) {
this.contrastNormalised = contrastNormalised;
}
/**
* Whether the luminance data extracted from the source image is
normalized
* by linearizing its histogram prior to edge extraction. The default
value
* is false.
*
* @return whether the contrast is normalized
*/
public boolean isContrastNormalized() {
return contrastNormalised;
}
public void process () {
long start = System.nanoTime();
height = sourceImg.getHeight();
width = sourceImg.getWidth();
picSize = height*width;
initArrs();
readLuminance();
if (contrastNormalised) {
normalizeContrast();
}
computeGradients(gaussianKernelRadius, gaussianKernelWidth);
int low = Math.round (lowThreshold * MAGNITUDE_SCALE);
int high = Math.round (highThreshold * MAGNITUDE_SCALE);
performHysteresis(low, high);
thresholdEdges();
writeEdges(data);
Log.i("Processing", "Processing terminated, time required: " +
(System.nanoTime() - start));
}
private void initArrs() {
if (data == null || picSize != data.length) {
data = new int[picSize];
magnitude = new int[picSize];
}
}
private void readLuminance() {
int [] pixels = new int[picSize];
sourceImg.getPixels(pixels, 0, width, 0, 0, width, height);
for (int i = 0 ; i < picSize ; i++) {
int p = pixels[i];
int r = (p & 0xff0000) >> 16;
int g = (p & 0xff00) >> 8;
int b = p & 0xff;
data[i] = luminance(r, g, b);
}
}
private int luminance (int R, int G, int B) {
return Math.round (0.299f * R + 0.587f * G + 0.114f * B);
}
private void normalizeContrast() {
int[] histogram = new int[256];
for (int i = 0 ; i < data.length ; i++) {
histogram[data[i]]++;
}
// for (int i = 0 ; i < data.length ; i++) {
// Log.i("BEFORE contrast data", "Pixel number " + i + " = " +
data[i]);
// }
int[] remap = new int[256];
int sum = 0;
int j = 0;
for (int i = 0; i < histogram.length; i++) {
sum += histogram[i];
int target = sum*255/picSize;
for (int k = j+1; k <=target; k++) {
remap[k] = i;
}
j = target;
}
for (int i = 0; i < data.length; i++) {
data[i] = remap[data[i]];
}
// for (int i = 0 ; i < data.length ; i++) {
// Log.i("AFTER contrast data", "Pixel number " + i + " = " +
data[i]);
// }
}
private void computeGradients(float kernelRadius, int kernelWidth) {
float[] xConv = new float[picSize];
float[] yConv = new float[picSize];
//generate the gaussian convolution masks
float kernel[] = new float[kernelWidth];
float diffKernel[] = new float[kernelWidth];
int kwidth;
for (kwidth = 0 ; kwidth < kernelWidth ; kwidth++) {
float g1 = gaussian(kwidth, kernelRadius);
if (g1 <= GAUSSIAN_CUT_OFF && kwidth >= 2) break;
float g2 = gaussian(kwidth - 0.5f, kernelRadius);
float g3 = gaussian(kwidth + 0.5f, kernelRadius);
kernel[kwidth] = (g1 + g2 + g3) / 3f / (2f * (float) Math.PI *
kernelRadius * kernelRadius);
diffKernel[kwidth] = g3 - g2;
}
//kwidth now == kernelWidth-1
int initX = kwidth - 1; //So that it does not go too close to edges
int maxX = width - (kwidth - 1); //Same same
int initY = width * (kwidth - 1); //Same same
int maxY = width * (height - (kwidth - 1)); //Same same
//perform convolution (Gaussian blurring) in x and y directions
for (int x = initX; x < maxX; x++) {
for (int y = initY; y < maxY; y += width) {
int index = x + y;
float sumX = data[index] * kernel[0];
float sumY = sumX;
int xOffset = 1; //Initial values
int yOffset = width;
for( ; xOffset < kwidth ; ) {
sumY += kernel[xOffset] * (data[index - yOffset] +
data[index + yOffset]);
sumX += kernel[xOffset] * (data[index - xOffset] +
data[index + xOffset]);
yOffset += width;
xOffset++;
}
yConv[index] = sumY;
xConv[index] = sumX;
}
}
float[] xGradient = new float[picSize];
float[] yGradient = new float[picSize];
for (int x = initX; x < maxX; x++) {
for (int y = initY; y < maxY; y += width) {
float sum = 0f;
int index = x + y;
for (int i = 1; i < kwidth; i++)
sum += diffKernel[i] * (yConv[index - i] - yConv[index
+ i]);
xGradient[index] = sum;
}
}
for (int x = kwidth; x < width - kwidth; x++) {
for (int y = initY; y < maxY; y += width) {
float sum = 0.0f;
int index = x + y;
int yOffset = width;
for (int i = 1; i < kwidth; i++) {
sum += diffKernel[i] * (xConv[index - yOffset] -
xConv[index + yOffset]);
yOffset += width;
}
yGradient[index] = sum;
}
}
initX = kwidth;
maxX = width - kwidth;
initY = width * kwidth;
maxY = width * (height - kwidth);
for (int x = initX; x < maxX; x++) {
for (int y = initY; y < maxY; y += width) {
int index = x + y;
int indexN = index - width;
int indexS = index + width;
int indexW = index - 1;
int indexE = index + 1;
int indexNW = indexN - 1;
int indexNE = indexN + 1;
int indexSW = indexS - 1;
int indexSE = indexS + 1;
float xGrad = xGradient[index];
float yGrad = yGradient[index];
float gradMag = hypot(xGrad, yGrad);
//perform non-maximal supression
float nMag = hypot(xGradient[indexN], yGradient[indexN]);
float sMag = hypot(xGradient[indexS], yGradient[indexS]);
float wMag = hypot(xGradient[indexW], yGradient[indexW]);
float eMag = hypot(xGradient[indexE], yGradient[indexE]);
float neMag = hypot(xGradient[indexNE], yGradient[indexNE]);
float seMag = hypot(xGradient[indexSE], yGradient[indexSE]);
float swMag = hypot(xGradient[indexSW], yGradient[indexSW]);
float nwMag = hypot(xGradient[indexNW], yGradient[indexNW]);
float tmp;
/*
* An explanation of what's happening here, for those who
want
* to understand the source: This performs the "non-maximal
* supression" phase of the Canny edge detection in which we
* need to compare the gradient magnitude to that in the
* direction of the gradient; only if the value is a local
* maximum do we consider the point as an edge candidate.
*
* We need to break the comparison into a number of different
* cases depending on the gradient direction so that the
* appropriate values can be used. To avoid computing the
* gradient direction, we use two simple comparisons:
first we
* check that the partial derivatives have the same sign (1)
* and then we check which is larger (2). As a
consequence, we
* have reduced the problem to one of four identical cases
that
* each test the central gradient magnitude against the
values at
* two points with 'identical support'; what this means is
that
* the geometry required to accurately interpolate the
magnitude
* of gradient function at those points has an identical
* geometry (upto right-angled-rotation/reflection).
*
* When comparing the central gradient to the two
interpolated
* values, we avoid performing any divisions by
multiplying both
* sides of each inequality by the greater of the two partial
* derivatives. The common comparand is stored in a temporary
* variable (3) and reused in the mirror case (4).
*
*/
if (xGrad * yGrad <= (float) 0 /*(1)*/
? Math.abs(xGrad) >= Math.abs(yGrad) /*(2)*/
? (tmp = Math.abs(xGrad * gradMag)) >=
Math.abs(yGrad * neMag - (xGrad + yGrad) * eMag)
/*(3)*/
&& tmp > Math.abs(yGrad * swMag - (xGrad + yGrad)
* wMag) /*(4)*/
: (tmp = Math.abs(yGrad * gradMag)) >=
Math.abs(xGrad * neMag - (yGrad + xGrad) * nMag)
/*(3)*/
&& tmp > Math.abs(xGrad * swMag - (yGrad + xGrad)
* sMag) /*(4)*/
: Math.abs(xGrad) >= Math.abs(yGrad) /*(2)*/
? (tmp = Math.abs(xGrad * gradMag)) >=
Math.abs(yGrad * seMag + (xGrad - yGrad) * eMag)
/*(3)*/
&& tmp > Math.abs(yGrad * nwMag + (xGrad - yGrad)
* wMag) /*(4)*/
: (tmp = Math.abs(yGrad * gradMag)) >=
Math.abs(xGrad * seMag + (yGrad - xGrad) * sMag)
/*(3)*/
&& tmp > Math.abs(xGrad * nwMag + (yGrad - xGrad)
* nMag) /*(4)*/
) {
magnitude[index] = gradMag >= MAGNITUDE_LIMIT ?
MAGNITUDE_MAX : (int) (MAGNITUDE_SCALE * gradMag);
//NOTE: The orientation of the edge is not employed by
this
//implementation. It is a simple matter to compute it at
//this point as: Math.atan2(yGrad, xGrad);
} else {
magnitude[index] = 0;
}
}
}
}
private float hypot(float x, float y) {
return (float) Math.hypot(x, y);
}
private float gaussian (float x, float sigma) {
return (float) Math.exp(-(x * x) / (2f * sigma * sigma));
} //I guess the box blur basically doesn't use this shit
private void performHysteresis(int low, int high) {
//NOTE: this implementation reuses the data array to store both
//luminance data from the image, and edge intensity from the
processing.
//This is done for memory efficiency, other implementations may wish
//to separate these functions.
Arrays.fill(data, 0);
int offset = 0;
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (data[offset] == 0 && magnitude[offset] >= high) {
follow(x, y, offset, low, 0);
}
offset++;
}
}
}
private void follow(int x1, int y1, int i1, int threshold, int depth) {
if( depth > mFollowStackDepth) // don't run out of stack!
return;
int x0 = x1 == 0 ? x1 : x1 - 1;
int x2 = x1 == width - 1 ? x1 : x1 + 1;
int y0 = y1 == 0 ? y1 : y1 - 1;
int y2 = y1 == height -1 ? y1 : y1 + 1;
data[i1] = magnitude[i1];
for (int x = x0; x <= x2; x++) {
for (int y = y0; y <= y2; y++) {
int i2 = x + y * width;
if ((y != y1 || x != x1)
&& data[i2] == 0
&& magnitude[i2] >= threshold) {
follow(x, y, i2, threshold, depth+1);
return;
}
}
}
}
private void thresholdEdges() {
for (int i = 0 ; i < picSize ; i++) {
data[i] = data[i] > 0 ? -1 : 0xff000000;
}
}
private void writeEdges (int pixels[]) {
if (edgesImg == null) {
edgesImg = Bitmap.createBitmap(width, height,
Bitmap.Config.ARGB_8888);
}
edgesImg.setPixels(pixels, 0, width, 0, 0, width, height);
}
}
Please help!

No comments:

Post a Comment