From e19e9a83f6b4f9acb3e78a0e952d068c1a51766e Mon Sep 17 00:00:00 2001 From: dswitkin Date: Thu, 21 May 2009 19:56:25 +0000 Subject: [PATCH] I moved a chunk of the histogram/black point code out of BaseMonochromeBitmapSource and into BlackPointEstimator which makes a lot more sense. Unfortunately I had to expose three new methods and touch a bunch of files. But I did manage to reuse an array on subsequent calls, which was being allocated on every row scanned, so that might be a bit faster. It will also be easier to convert this code to JNI in the future if we want to, and/or do more advanced thresholding. git-svn-id: https://zxing.googlecode.com/svn/trunk@945 59b500cc-1b3d-0410-9834-0bbf25fbcc57 --- .../android/YUVMonochromeBitmapSource.java | 6 +- .../RGBMonochromeBitmapSource.java | 6 +- .../google/zxing/MonochromeBitmapSource.java | 33 ++++++++ .../common/BaseMonochromeBitmapSource.java | 37 ++------- .../zxing/common/BlackPointEstimator.java | 82 ++++++++++++++++--- .../common/CroppedMonochromeBitmapSource.java | 12 +++ ....java => BlackPointEstimatorTestCase.java} | 6 +- .../LCDUIImageMonochromeBitmapSource.java | 6 +- .../BufferedImageMonochromeBitmapSource.java | 6 +- 9 files changed, 137 insertions(+), 57 deletions(-) rename core/test/src/com/google/zxing/common/{BlackPointEstimationMethodTestCase.java => BlackPointEstimatorTestCase.java} (86%) diff --git a/android/src/com/google/zxing/client/android/YUVMonochromeBitmapSource.java b/android/src/com/google/zxing/client/android/YUVMonochromeBitmapSource.java index 8aad46738..adc7648ca 100755 --- a/android/src/com/google/zxing/client/android/YUVMonochromeBitmapSource.java +++ b/android/src/com/google/zxing/client/android/YUVMonochromeBitmapSource.java @@ -97,12 +97,12 @@ public final class YUVMonochromeBitmapSource extends BaseMonochromeBitmapSource * @return The luminance as an int, from 0-255 */ @Override - protected int getLuminance(int x, int y) { + public int getLuminance(int x, int y) { return mYUVData[(y + mCropTop) * mDataWidth + x + mCropLeft] & 0xff; } @Override - protected int[] getLuminanceRow(int y, int[] row) { + public int[] getLuminanceRow(int y, int[] row) { int width = getWidth(); if (row == null || row.length < width) { row = new int[width]; @@ -116,7 +116,7 @@ public final class YUVMonochromeBitmapSource extends BaseMonochromeBitmapSource } @Override - protected int[] getLuminanceColumn(int x, int[] column) { + public int[] getLuminanceColumn(int x, int[] column) { int height = getHeight(); if (column == null || column.length < height) { column = new int[height]; diff --git a/androidtest/src/com/google/zxing/client/androidtest/RGBMonochromeBitmapSource.java b/androidtest/src/com/google/zxing/client/androidtest/RGBMonochromeBitmapSource.java index 8f1c249cd..6065bcbb3 100644 --- a/androidtest/src/com/google/zxing/client/androidtest/RGBMonochromeBitmapSource.java +++ b/androidtest/src/com/google/zxing/client/androidtest/RGBMonochromeBitmapSource.java @@ -67,12 +67,12 @@ public final class RGBMonochromeBitmapSource extends BaseMonochromeBitmapSource } @Override - protected int getLuminance(int x, int y) { + public int getLuminance(int x, int y) { return mLuminances[y * getWidth() + x] & 0xff; } @Override - protected int[] getLuminanceRow(int y, int[] row) { + public int[] getLuminanceRow(int y, int[] row) { int width = getWidth(); if (row == null || row.length < width) { row = new int[width]; @@ -85,7 +85,7 @@ public final class RGBMonochromeBitmapSource extends BaseMonochromeBitmapSource } @Override - protected int[] getLuminanceColumn(int x, int[] column) { + public int[] getLuminanceColumn(int x, int[] column) { int width = getWidth(); int height = getHeight(); if (column == null || column.length < height) { diff --git a/core/src/com/google/zxing/MonochromeBitmapSource.java b/core/src/com/google/zxing/MonochromeBitmapSource.java index 64155a191..c88ac2472 100644 --- a/core/src/com/google/zxing/MonochromeBitmapSource.java +++ b/core/src/com/google/zxing/MonochromeBitmapSource.java @@ -101,4 +101,37 @@ public interface MonochromeBitmapSource { */ boolean isRotateSupported(); + /** + * Retrieves the luminance at the pixel x,y in the bitmap. This method is only used for estimating + * the black point and implementing getBlackRow() - it is not meant for decoding, hence it is not + * part of MonochromeBitmapSource itself, and is protected. + * + * @param x The x coordinate in the image. + * @param y The y coordinate in the image. + * @return The luminance value between 0 and 255. + */ + public abstract int getLuminance(int x, int y); + + /** + * This is the main mechanism for retrieving luminance data. It is dramatically more efficient + * than repeatedly calling getLuminance(). As above, this is not meant for decoders. + * + * @param y The row to fetch + * @param row The array to write luminance values into. It is strongly suggested that you + * allocate this yourself, making sure row.length >= getWidth(), and reuse the same + * array on subsequent calls for performance. If you pass null, you will be flogged, + * but then I will take pity on you and allocate a sufficient array internally. + * @return The array containing the luminance data. This is the same as row if it was usable. + */ + public abstract int[] getLuminanceRow(int y, int[] row); + + /** + * The same as getLuminanceRow(), but for columns. + * + * @param x The column to fetch + * @param column The array to write luminance values into. See above. + * @return The array containing the luminance data. + */ + public abstract int[] getLuminanceColumn(int x, int[] column); + } diff --git a/core/src/com/google/zxing/common/BaseMonochromeBitmapSource.java b/core/src/com/google/zxing/common/BaseMonochromeBitmapSource.java index 0120391be..2e16a73a3 100644 --- a/core/src/com/google/zxing/common/BaseMonochromeBitmapSource.java +++ b/core/src/com/google/zxing/common/BaseMonochromeBitmapSource.java @@ -24,16 +24,12 @@ import com.google.zxing.ReaderException; */ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSource { - private static final int LUMINANCE_BITS = 5; - private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS; - private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS; - private final int height; private final int width; private int blackPoint; private BlackPointEstimationMethod lastMethod; private int lastArgument; - private int[] luminances; + private int[] luminances = null; protected BaseMonochromeBitmapSource(int width, int height) { this.height = height; @@ -113,30 +109,7 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour public void estimateBlackPoint(BlackPointEstimationMethod method, int argument) throws ReaderException { if (!method.equals(lastMethod) || argument != lastArgument) { - int width = getWidth(); - int height = getHeight(); - int[] histogram = new int[LUMINANCE_BUCKETS]; - if (method.equals(BlackPointEstimationMethod.TWO_D_SAMPLING)) { - int minDimension = width < height ? width : height; - int startX = (width - minDimension) >> 1; - int startY = (height - minDimension) >> 1; - for (int n = 0; n < minDimension; n++) { - int luminance = getLuminance(startX + n, startY + n); - histogram[luminance >> LUMINANCE_SHIFT]++; - } - } else if (method.equals(BlackPointEstimationMethod.ROW_SAMPLING)) { - if (argument < 0 || argument >= height) { - throw new IllegalArgumentException("Row is not within the image: " + argument); - } - initLuminances(); - luminances = getLuminanceRow(argument, luminances); - for (int x = 0; x < width; x++) { - histogram[luminances[x] >> LUMINANCE_SHIFT]++; - } - } else { - throw new IllegalArgumentException("Unknown method"); - } - blackPoint = BlackPointEstimator.estimate(histogram) << LUMINANCE_SHIFT; + blackPoint = BlackPointEstimator.estimate(this, method, argument); lastMethod = method; lastArgument = argument; } @@ -189,7 +162,7 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour * @param y The y coordinate in the image. * @return The luminance value between 0 and 255. */ - protected abstract int getLuminance(int x, int y); + public abstract int getLuminance(int x, int y); /** * This is the main mechanism for retrieving luminance data. It is dramatically more efficient @@ -202,7 +175,7 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour * but then I will take pity on you and allocate a sufficient array internally. * @return The array containing the luminance data. This is the same as row if it was usable. */ - protected abstract int[] getLuminanceRow(int y, int[] row); + public abstract int[] getLuminanceRow(int y, int[] row); /** * The same as getLuminanceRow(), but for columns. @@ -211,6 +184,6 @@ public abstract class BaseMonochromeBitmapSource implements MonochromeBitmapSour * @param column The array to write luminance values into. See above. * @return The array containing the luminance data. */ - protected abstract int[] getLuminanceColumn(int x, int[] column); + public abstract int[] getLuminanceColumn(int x, int[] column); } diff --git a/core/src/com/google/zxing/common/BlackPointEstimator.java b/core/src/com/google/zxing/common/BlackPointEstimator.java index 9f218dd02..9fdc04b6a 100644 --- a/core/src/com/google/zxing/common/BlackPointEstimator.java +++ b/core/src/com/google/zxing/common/BlackPointEstimator.java @@ -16,6 +16,8 @@ package com.google.zxing.common; +import com.google.zxing.BlackPointEstimationMethod; +import com.google.zxing.MonochromeBitmapSource; import com.google.zxing.ReaderException; /** @@ -26,37 +28,97 @@ import com.google.zxing.ReaderException; * http://webdiis.unizar.es/~neira/12082/thresholding.pdf. *

* + * NOTE: This class is not threadsafe. + * * @author Sean Owen * @author dswitkin@google.com (Daniel Switkin) */ public final class BlackPointEstimator { + private static final int LUMINANCE_BITS = 5; + private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS; + private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS; + + private static int[] luminances = null; + private static int[] histogram = null; + private BlackPointEstimator() { } + private static void initArrays(int luminanceSize) { + if (luminances == null || luminances.length < luminanceSize) { + luminances = new int[luminanceSize]; + } + if (histogram == null) { + histogram = new int[LUMINANCE_BUCKETS]; + } else { + for (int x = 0; x < LUMINANCE_BUCKETS; x++) { + histogram[x] = 0; + } + } + } + + /** + * Calculates the black point for the supplied bitmap. + * + * @param source The bitmap to analyze. + * @param method The pixel sampling technique to use. + * @param argument The row index in the case of ROW_SAMPLING, otherwise ignored. + * @return The black point as an integer 0-255. + * @throws ReaderException An exception thrown if the blackpoint cannot be determined. + */ + public static int estimate(MonochromeBitmapSource source, BlackPointEstimationMethod method, + int argument) throws ReaderException { + int width = source.getWidth(); + int height = source.getHeight(); + initArrays(width); + + if (method.equals(BlackPointEstimationMethod.TWO_D_SAMPLING)) { + int minDimension = width < height ? width : height; + int startX = (width - minDimension) >> 1; + int startY = (height - minDimension) >> 1; + for (int n = 0; n < minDimension; n++) { + int luminance = source.getLuminance(startX + n, startY + n); + histogram[luminance >> LUMINANCE_SHIFT]++; + } + } else if (method.equals(BlackPointEstimationMethod.ROW_SAMPLING)) { + if (argument < 0 || argument >= height) { + throw new IllegalArgumentException("Row is not within the image: " + argument); + } + + luminances = source.getLuminanceRow(argument, luminances); + for (int x = 0; x < width; x++) { + histogram[luminances[x] >> LUMINANCE_SHIFT]++; + } + } else { + throw new IllegalArgumentException("Unknown method"); + } + return findBestValley(histogram) << LUMINANCE_SHIFT; + } + /** *

Given an array of counts of luminance values (i.e. a histogram), this method * decides which bucket of values corresponds to the black point -- which bucket contains the * count of the brightest luminance values that should be considered "black".

* - * @param histogram an array of counts of luminance values + * @param buckets an array of counts of luminance values * @return index within argument of bucket corresponding to brightest values which should be * considered "black" * @throws ReaderException if "black" and "white" appear to be very close in luminance */ - public static int estimate(int[] histogram) throws ReaderException{ - int numBuckets = histogram.length; + public static int findBestValley(int[] buckets) throws ReaderException { + int numBuckets = buckets.length; int maxBucketCount = 0; // Find tallest peak in histogram int firstPeak = 0; int firstPeakSize = 0; for (int i = 0; i < numBuckets; i++) { - if (histogram[i] > firstPeakSize) { + if (buckets[i] > firstPeakSize) { firstPeak = i; - firstPeakSize = histogram[i]; + firstPeakSize = buckets[i]; } - if (histogram[i] > maxBucketCount) { - maxBucketCount = histogram[i]; + if (buckets[i] > maxBucketCount) { + maxBucketCount = buckets[i]; } } @@ -67,7 +129,7 @@ public final class BlackPointEstimator { for (int i = 0; i < numBuckets; i++) { int distanceToBiggest = i - firstPeak; // Encourage more distant second peaks by multiplying by square of distance - int score = histogram[i] * distanceToBiggest * distanceToBiggest; + int score = buckets[i] * distanceToBiggest * distanceToBiggest; if (score > secondPeakScore) { secondPeak = i; secondPeakScore = score; @@ -81,7 +143,7 @@ public final class BlackPointEstimator { secondPeak = temp; } - // Kind of aribtrary; if the two peaks are very close, then we figure there is so little + // Kind of arbitrary; if the two peaks are very close, then we figure there is so little // dynamic range in the image, that discriminating black and white is too error-prone. // Decoding the image/line is either pointless, or may in some cases lead to a false positive // for 1D formats, which are relatively lenient. @@ -97,7 +159,7 @@ public final class BlackPointEstimator { int fromFirst = i - firstPeak; // Favor a "valley" that is not too close to either peak -- especially not the black peak -- // and that has a low value of course - int score = fromFirst * fromFirst * (secondPeak - i) * (maxBucketCount - histogram[i]); + int score = fromFirst * fromFirst * (secondPeak - i) * (maxBucketCount - buckets[i]); if (score > bestValleyScore) { bestValley = i; bestValleyScore = score; diff --git a/core/src/com/google/zxing/common/CroppedMonochromeBitmapSource.java b/core/src/com/google/zxing/common/CroppedMonochromeBitmapSource.java index c7d625d5e..a3adf57f1 100644 --- a/core/src/com/google/zxing/common/CroppedMonochromeBitmapSource.java +++ b/core/src/com/google/zxing/common/CroppedMonochromeBitmapSource.java @@ -95,4 +95,16 @@ public final class CroppedMonochromeBitmapSource implements MonochromeBitmapSour return delegate.isRotateSupported(); } + public int getLuminance(int x, int y) { + return delegate.getLuminance(x, y); + } + + public int[] getLuminanceRow(int y, int[] row) { + return delegate.getLuminanceRow(y, row); + } + + public int[] getLuminanceColumn(int x, int[] column) { + return delegate.getLuminanceColumn(x, column); + } + } diff --git a/core/test/src/com/google/zxing/common/BlackPointEstimationMethodTestCase.java b/core/test/src/com/google/zxing/common/BlackPointEstimatorTestCase.java similarity index 86% rename from core/test/src/com/google/zxing/common/BlackPointEstimationMethodTestCase.java rename to core/test/src/com/google/zxing/common/BlackPointEstimatorTestCase.java index 2878a973c..d780e9fb2 100644 --- a/core/test/src/com/google/zxing/common/BlackPointEstimationMethodTestCase.java +++ b/core/test/src/com/google/zxing/common/BlackPointEstimatorTestCase.java @@ -22,18 +22,18 @@ import junit.framework.TestCase; /** * @author Sean Owen */ -public final class BlackPointEstimationMethodTestCase extends TestCase { +public final class BlackPointEstimatorTestCase extends TestCase { public void testBasic() throws ReaderException { int[] histogram = { 0, 0, 11, 43, 37, 18, 3, 1, 0, 0, 13, 36, 24, 0, 11, 2 }; - int point = BlackPointEstimator.estimate(histogram); + int point = BlackPointEstimator.findBestValley(histogram); assertEquals(8, point); } public void testTooLittleRange() { try { int[] histogram = { 0, 0, 0, 0, 0, 0, 1, 43, 48, 18, 3, 1, 0, 0, 0, 0 }; - BlackPointEstimator.estimate(histogram); + BlackPointEstimator.findBestValley(histogram); fail("Should have thrown an exception"); } catch (ReaderException re) { // good diff --git a/javame/src/com/google/zxing/client/j2me/LCDUIImageMonochromeBitmapSource.java b/javame/src/com/google/zxing/client/j2me/LCDUIImageMonochromeBitmapSource.java index bd6b12208..0d65ba343 100644 --- a/javame/src/com/google/zxing/client/j2me/LCDUIImageMonochromeBitmapSource.java +++ b/javame/src/com/google/zxing/client/j2me/LCDUIImageMonochromeBitmapSource.java @@ -38,7 +38,7 @@ public final class LCDUIImageMonochromeBitmapSource extends BaseMonochromeBitmap } // This is expensive and should be used very sparingly. - protected int getLuminance(int x, int y) { + public int getLuminance(int x, int y) { image.getRGB(pixelHolder, 0, getWidth(), x, y, 1, 1); int pixel = pixelHolder[0]; @@ -60,7 +60,7 @@ public final class LCDUIImageMonochromeBitmapSource extends BaseMonochromeBitmap } // For efficiency, the RGB data and the luminance data share the same array. - protected int[] getLuminanceRow(int y, int[] row) { + public int[] getLuminanceRow(int y, int[] row) { int width = getWidth(); if (row == null || row.length < width) { row = new int[width]; @@ -75,7 +75,7 @@ public final class LCDUIImageMonochromeBitmapSource extends BaseMonochromeBitmap return row; } - protected int[] getLuminanceColumn(int x, int[] column) { + public int[] getLuminanceColumn(int x, int[] column) { int height = getHeight(); if (column == null || column.length < height) { column = new int[height]; diff --git a/javase/src/com/google/zxing/client/j2se/BufferedImageMonochromeBitmapSource.java b/javase/src/com/google/zxing/client/j2se/BufferedImageMonochromeBitmapSource.java index 5c6d75b0b..be7ad9a8f 100644 --- a/javase/src/com/google/zxing/client/j2se/BufferedImageMonochromeBitmapSource.java +++ b/javase/src/com/google/zxing/client/j2se/BufferedImageMonochromeBitmapSource.java @@ -123,7 +123,7 @@ public final class BufferedImageMonochromeBitmapSource extends BaseMonochromeBit * where R, G, and B are values in [0,1]. */ @Override - protected int getLuminance(int x, int y) { + public int getLuminance(int x, int y) { int pixel = image.getRGB(left + x, top + y); // Coefficients add up to 1024 to make the divide into a fast shift return (306 * ((pixel >> 16) & 0xFF) + @@ -132,7 +132,7 @@ public final class BufferedImageMonochromeBitmapSource extends BaseMonochromeBit } @Override - protected int[] getLuminanceRow(int y, int[] row) { + public int[] getLuminanceRow(int y, int[] row) { int width = getWidth(); if (row == null || row.length < width) { row = new int[width]; @@ -148,7 +148,7 @@ public final class BufferedImageMonochromeBitmapSource extends BaseMonochromeBit } @Override - protected int[] getLuminanceColumn(int x, int[] column) { + public int[] getLuminanceColumn(int x, int[] column) { int height = getHeight(); if (column == null || column.length < height) { column = new int[height];