package org.theburninators.cardreader; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.Log; import java.util.Arrays; import java.util.Random; public class CardRecognizer { private static final String TAG = "CardRecognizer"; private static final int EDGE_THRESHOLD = 15000; private static final int LINE_SAMPLES = 10000; private static final int LINE_THRESHOLD = 10; private static final int LINE_DISTANCE = 30; private static final int SLOPE_OCTANT = 100; private static final int SLOPE_BUCKET = 5; private static final int INTERCEPT_BUCKET = 5; private static final int BEST_LINES = 10; private final Random mRandom = new Random(); private final int width, height; private final int[] sobelX, sobelY; private final int[] edgeX, edgeY; private final int slopeBuckets, interceptBuckets, interceptOffset; private final int[] lineVotes, lineSlope, lineIntercept; private final int[] bestLines; private static final class Line { public int slope; public int intercept; } public CardRecognizer(int width, int height) { this.width = width; this.height = height; sobelX = new int[width * height]; sobelY = new int[width * height]; edgeX = new int[width * height / 10]; edgeY = new int[width * height / 10]; interceptOffset = Math.max(width, height); interceptBuckets = 3 * interceptOffset / INTERCEPT_BUCKET; slopeBuckets = 4 * SLOPE_OCTANT / SLOPE_BUCKET; lineVotes = new int[interceptBuckets * slopeBuckets + 1]; lineSlope = new int[interceptBuckets * slopeBuckets]; lineIntercept = new int[interceptBuckets * slopeBuckets]; bestLines = new int[BEST_LINES]; } public void onFrame(byte[] data, CardReaderService.StatusListener log) { // Compute the Sobel transform Arrays.fill(sobelX, 0); Arrays.fill(sobelY, 0); for (int dx = -1; dx <= 1; dx++) { for (int dy = -1; dy <= 1; dy++) { addPixels(data, width, height, dx * (dy == 0 ? 2 : 1), sobelX, dx, dy); addPixels(data, width, height, dy * (dx == 0 ? 2 : 1), sobelY, dx, dy); } } int edgeCount = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int pos = x + y * width; int sxp = sobelX[pos], syp = sobelY[pos]; if ((sxp * sxp + syp * syp) > EDGE_THRESHOLD) { int i = edgeCount < edgeX.length ? edgeCount++ : mRandom.nextInt(edgeX.length); edgeX[i] = x; edgeY[i] = y; } } } // Monte Carlo variant of Hough Transform Arrays.fill(lineVotes, 0); Arrays.fill(lineSlope, 0); Arrays.fill(lineIntercept, 0); if (edgeCount > 10) { Line line = new Line(); for (int s = 0; s < LINE_SAMPLES; s++) { int i = mRandom.nextInt(edgeCount), j = mRandom.nextInt(edgeCount); if (!getLine(edgeX[i], edgeY[i], edgeX[j], edgeY[j], line)) continue; int slopeBucket = line.slope / SLOPE_BUCKET; int interceptBucket = (line.intercept + interceptOffset) / INTERCEPT_BUCKET; int pos = interceptBucket + slopeBucket * interceptBuckets; /* Log.i(TAG, "(" + edgeX[i] + "," + edgeY[i] + ")-(" + edgeX[j] + "," + edgeY[j] + "): " + line.slope + "@" + line.intercept + " -> {" + slopeBucket + "," + interceptBucket + "} -> " + pos); */ lineVotes[pos]++; lineSlope[pos] += line.slope; lineIntercept[pos] += line.intercept; } } Arrays.fill(bestLines, lineVotes.length - 1); int bestLeast = lineVotes[bestLines[0]] = LINE_THRESHOLD; for (int s = 0; s < slopeBuckets; s++) { int sMin = Math.max(0, s - 1), sMax = Math.min(slopeBuckets - 1, s + 1); for (int i = 0; i < interceptBuckets; i++) { int pos = i + s * interceptBuckets; int votes = lineVotes[pos]; if (votes <= bestLeast) continue; // Log.i(TAG, "Candidate: " + s + "@" + i + " with " + votes + " votes"); boolean localMax = true; int iMin = i + interceptBuckets - 1, iMax = i + interceptBuckets + 1; for (int sN = sMin; sN <= sMax && localMax; sN++) { for (int iN = iMin; iN <= iMax && localMax; iN++) { int posN = (iN % interceptBuckets) + sN * interceptBuckets; localMax = (votes >= lineVotes[posN]); // if (!localMax) Log.i(TAG, " --> beat by " + sN + "@" + (iN % interceptBuckets) + " with " + lineVotes[posN]); } } if (!localMax) continue; int newLeast = votes; for (int b = 0; b < bestLines.length; b++) { int bestVotes = lineVotes[bestLines[b]]; if (bestVotes == bestLeast) { // Log.i(TAG, " --> reuse slot #" + b + " (had " + bestVotes + ")"); bestLines[b] = pos; bestLeast = -1; } else if (bestVotes < newLeast) { newLeast = bestVotes; } } bestLeast = newLeast; // Log.i(TAG, " (least has " + bestLeast + " votes)"); } } if (log != null) { Bitmap debug = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); int[] row = new int[width]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) row[x] = 0x010101 * data[x + y * width]; debug.setPixels(row, 0, width, 0, y, width, 1); } for (int i = 0; i < edgeCount; i++) { debug.setPixel(edgeX[i], edgeY[i], Color.BLUE); } Canvas canvas = new Canvas(debug); Paint paint = new Paint(); paint.setColor(Color.GREEN); for (int b = 0; b < bestLines.length; b++) { int pos = bestLines[b]; if (pos >= lineSlope.length) continue; int slope = (pos / interceptBuckets) * SLOPE_BUCKET; int intercept = (pos % interceptBuckets) * INTERCEPT_BUCKET - interceptOffset; // int slope = lineSlope[pos] / lineVotes[pos]; // int intercept = lineIntercept[pos] / lineVotes[pos]; switch (slope / SLOPE_OCTANT) { case 0: canvas.drawLine( 0, intercept, width, intercept + width * slope / SLOPE_OCTANT, paint); break; case 1: case 2: canvas.drawLine( intercept, 0, intercept + height * (2 * SLOPE_OCTANT - slope) / SLOPE_OCTANT, height, paint); break; case 3: canvas.drawLine( 0, intercept, width, intercept + width * (slope - 4 * SLOPE_OCTANT) / SLOPE_OCTANT, paint); break; default: continue; } } log.onFrame(debug); } } private void addPixels(byte[] in, int w, int h, int m, int[] out, int dx, int dy) { if (m == 0) return; for (int yOut = 0; yOut < h; yOut++) { int yIn = Math.max(0, Math.min(h - 1, yOut + dy)); int xMin = Math.max(0, -dx), xMax = Math.min(w, w - dx); int posMin = xMin + yOut * w, posMax = xMax + yOut * w, posOff = dx + (yIn - yOut) * w; for (int pos = yOut * w; pos < posMin; pos++) { out[pos] += m * (0xFF & in[yIn * w]); } for (int pos = posMin; pos < posMax; pos++) { out[pos] += m * (0xFF & in[pos + posOff]); } for (int pos = posMax; pos < (yOut + 1) * w; pos++) { out[pos] += m * (0xFF & in[w - 1 + yIn * w]); } } } private boolean getLine(int x1, int y1, int x2, int y2, Line out) { int dx = x2 - x1, dy = y2 - y1; if (dy < 0 || (dy == 0 && dx < 0)) { dx = -dx; dy = -dy; } if (dx > 0) { if (dx > dy) { if (dx < LINE_DISTANCE) return false; out.slope = SLOPE_OCTANT * dy / dx; out.intercept = y1 - x1 * dy / dx; } else { if (dy < LINE_DISTANCE) return false; out.slope = 2 * SLOPE_OCTANT - SLOPE_OCTANT * dx / dy; out.intercept = x1 - y1 * dx / dy; } } else { if (-dx < dy) { if (dy < LINE_DISTANCE) return false; out.slope = 2 * SLOPE_OCTANT - SLOPE_OCTANT * dx / dy; out.intercept = x1 - y1 * dx / dy; } else { if (-dx < LINE_DISTANCE) return false; out.slope = 4 * SLOPE_OCTANT + SLOPE_OCTANT * dy / dx; if (out.slope >= 4 * SLOPE_OCTANT) out.slope = 4 * SLOPE_OCTANT - 1; out.intercept = y1 - x1 * dy / dx; } } return true; } }