I am developing android app that uses tesseract OCR to scan a text from image,
I heard that binarizing image before performing OCR on it will give better result,
So
simple clean and first first convert image to grayscale(if u don't you'll get an input image error) after conversion use the adaptive threshold method to complete task code:
Mat tmp = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC1);
// Convert
Utils.bitmapToMat(bitmap, tmp);
Mat gray = new Mat(bitmap.getWidth(), bitmap.getHeight(), CvType.CV_8UC1);
// Conver the color
Imgproc.cvtColor(tmp, gray, Imgproc.COLOR_RGB2GRAY);
// Convert back to bitmap
Mat destination = new Mat(gray.rows(),gray.cols(),gray.type());
Imgproc.adaptiveThreshold(gray, destination, 255,
Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY_INV, 15, 4);
Utils.matToBitmap(destination, bitmap);
imv_binary.setImageBitmap(bitmap);
A Simple Solution
In the following, I simply alter each pixel in an image based on the normal 3-dimension-space distance formula. I decide whether a pixel should be black or white based on how far it is from each of these colors. For example, (1,2,3) is closer to (0,0,0) than it is to (255,255,255) and therefore it is decided to be black. I'm sure there are more clever algorithms out there. This is just a simple one
MainActivity.java
package com.example.binarizeimage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Environment;
import android.widget.ImageView;
import com.example.binarizeimage.R.drawable;
/**
* @author Sherif elKhatib - shush
*
*/
public class MainActivity extends Activity {
/**
* Boolean that tells me how to treat a transparent pixel (Should it be black?)
*/
private static final boolean TRASNPARENT_IS_BLACK = false;
/**
* This is a point that will break the space into Black or white
* In real words, if the distance between WHITE and BLACK is D;
* then we should be this percent far from WHITE to be in the black region.
* Example: If this value is 0.5, the space is equally split.
*/
private static final double SPACE_BREAKING_POINT = 13.0/30.0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//this is the original image
Bitmap theOriginalImage = BitmapFactory.decodeResource(this.getResources(), drawable.ic_launcher);
//this is the image that is binarized
Bitmap binarizedImage = convertToMutable(theOriginalImage);
// I will look at each pixel and use the function shouldBeBlack to decide
// whether to make it black or otherwise white
for(int i=0;i<binarizedImage.getWidth();i++) {
for(int c=0;c<binarizedImage.getHeight();c++) {
int pixel = binarizedImage.getPixel(i, c);
if(shouldBeBlack(pixel))
binarizedImage.setPixel(i, c, Color.BLACK);
else
binarizedImage.setPixel(i, c, Color.WHITE);
}
}
ImageView iv = (ImageView) findViewById(R.id.imageView1);
ImageView ivb = (ImageView) findViewById(R.id.ImageView01);
//show the original image
iv.setImageBitmap(BitmapFactory.decodeResource(this.getResources(), drawable.ic_launcher));
//show the binarized image
ivb.setImageBitmap(binarizedImage);
}
/**
* @param pixel the pixel that we need to decide on
* @return boolean indicating whether this pixel should be black
*/
private static boolean shouldBeBlack(int pixel) {
int alpha = Color.alpha(pixel);
int redValue = Color.red(pixel);
int blueValue = Color.blue(pixel);
int greenValue = Color.green(pixel);
if(alpha == 0x00) //if this pixel is transparent let me use TRASNPARENT_IS_BLACK
return TRASNPARENT_IS_BLACK;
// distance from the white extreme
double distanceFromWhite = Math.sqrt(Math.pow(0xff - redValue, 2) + Math.pow(0xff - blueValue, 2) + Math.pow(0xff - greenValue, 2));
// distance from the black extreme //this should not be computed and might be as well a function of distanceFromWhite and the whole distance
double distanceFromBlack = Math.sqrt(Math.pow(0x00 - redValue, 2) + Math.pow(0x00 - blueValue, 2) + Math.pow(0x00 - greenValue, 2));
// distance between the extremes //this is a constant that should not be computed :p
double distance = distanceFromBlack + distanceFromWhite;
// distance between the extremes
return ((distanceFromWhite/distance)>SPACE_BREAKING_POINT);
}
/**
* @author Derzu
*
* @see http://stackoverflow.com/a/9194259/833622
*
* Converts a immutable bitmap to a mutable bitmap. This operation doesn't allocates
* more memory that there is already allocated.
*
* @param imgIn - Source image. It will be released, and should not be used more
* @return a copy of imgIn, but muttable.
*/
public static Bitmap convertToMutable(Bitmap imgIn) {
try {
//this is the file going to use temporally to save the bytes.
// This file will not be a image, it will store the raw image data.
File file = new File(Environment.getExternalStorageDirectory() + File.separator + "temp.tmp");
//Open an RandomAccessFile
//Make sure you have added uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
//into AndroidManifest.xml file
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
// get the width and height of the source bitmap.
int width = imgIn.getWidth();
int height = imgIn.getHeight();
Config type = imgIn.getConfig();
//Copy the byte to the file
//Assume source bitmap loaded using options.inPreferredConfig = Config.ARGB_8888;
FileChannel channel = randomAccessFile.getChannel();
MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, imgIn.getRowBytes()*height);
imgIn.copyPixelsToBuffer(map);
//recycle the source bitmap, this will be no longer used.
imgIn.recycle();
System.gc();// try to force the bytes from the imgIn to be released
//Create a new bitmap to load the bitmap again. Probably the memory will be available.
imgIn = Bitmap.createBitmap(width, height, type);
map.position(0);
//load it back from temporary
imgIn.copyPixelsFromBuffer(map);
//close the temporary file and channel , then delete that also
channel.close();
randomAccessFile.close();
// delete the temp file
file.delete();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return imgIn;
}
}
*activity_main.xml*
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView2"
android:layout_centerHorizontal="true"
android:text="Original Image" />
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView1"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/TextView02"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView1"
android:layout_below="@+id/imageView1"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:text="YES/NO Image" />
<ImageView
android:id="@+id/ImageView01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/TextView02"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_launcher" />
</RelativeLayout>
You can use Catalano Framework, It's simple and there is more than 60 filters
http://code.google.com/p/catalano-framework/
FastBitmap fb = new FastBitmap(bitmap);
Grayscale g = new Grayscale(fb);
g.applyInPlace(fb);
Threshold t = new Threshold(100);
t.applyInPlace(fb);
bitmap = fb.toBitmap();
Wouldn't be to hard to port this from java to android:
/**
* Image binarization - Otsu algorithm
*
* Author: Bostjan Cigan (http://zerocool.is-a-geek.net)
*
*/
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
public class OtsuBinarize {
private static BufferedImage original, grayscale, binarized;
public static void main(String[] args) throws IOException {
File original_f = new File(args[0]+".jpg");
String output_f = args[0]+"_bin";
original = ImageIO.read(original_f);
grayscale = toGray(original);
binarized = binarize(grayscale);
writeImage(output_f);
}
private static void writeImage(String output) throws IOException {
File file = new File(output+".jpg");
ImageIO.write(binarized, "jpg", file);
}
// Return histogram of grayscale image
public static int[] imageHistogram(BufferedImage input) {
int[] histogram = new int[256];
for(int i=0; i<histogram.length; i++) histogram[i] = 0;
for(int i=0; i<input.getWidth(); i++) {
for(int j=0; j<input.getHeight(); j++) {
int red = new Color(input.getRGB (i, j)).getRed();
histogram[red]++;
}
}
return histogram;
}
// The luminance method
private static BufferedImage toGray(BufferedImage original) {
int alpha, red, green, blue;
int newPixel;
BufferedImage lum = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());
for(int i=0; i<original.getWidth(); i++) {
for(int j=0; j<original.getHeight(); j++) {
// Get pixels by R, G, B
alpha = new Color(original.getRGB(i, j)).getAlpha();
red = new Color(original.getRGB(i, j)).getRed();
green = new Color(original.getRGB(i, j)).getGreen();
blue = new Color(original.getRGB(i, j)).getBlue();
red = (int) (0.21 * red + 0.71 * green + 0.07 * blue);
// Return back to original format
newPixel = colorToRGB(alpha, red, red, red);
// Write pixels into image
lum.setRGB(i, j, newPixel);
}
}
return lum;
}
// Get binary treshold using Otsu's method
private static int otsuTreshold(BufferedImage original) {
int[] histogram = imageHistogram(original);
int total = original.getHeight() * original.getWidth();
float sum = 0;
for(int i=0; i<256; i++) sum += i * histogram[i];
float sumB = 0;
int wB = 0;
int wF = 0;
float varMax = 0;
int threshold = 0;
for(int i=0 ; i<256 ; i++) {
wB += histogram[i];
if(wB == 0) continue;
wF = total - wB;
if(wF == 0) break;
sumB += (float) (i * histogram[i]);
float mB = sumB / wB;
float mF = (sum - sumB) / wF;
float varBetween = (float) wB * (float) wF * (mB - mF) * (mB - mF);
if(varBetween > varMax) {
varMax = varBetween;
threshold = i;
}
}
return threshold;
}
private static BufferedImage binarize(BufferedImage original) {
int red;
int newPixel;
int threshold = otsuTreshold(original);
BufferedImage binarized = new BufferedImage(original.getWidth(), original.getHeight(), original.getType());
for(int i=0; i<original.getWidth(); i++) {
for(int j=0; j<original.getHeight(); j++) {
// Get pixels
red = new Color(original.getRGB(i, j)).getRed();
int alpha = new Color(original.getRGB(i, j)).getAlpha();
if(red > threshold) {
newPixel = 255;
}
else {
newPixel = 0;
}
newPixel = colorToRGB(alpha, newPixel, newPixel, newPixel);
binarized.setRGB(i, j, newPixel);
}
}
return binarized;
}
// Convert R, G, B, Alpha to standard 8 bit
private static int colorToRGB(int alpha, int red, int green, int blue) {
int newPixel = 0;
newPixel += alpha;
newPixel = newPixel << 8;
newPixel += red; newPixel = newPixel << 8;
newPixel += green; newPixel = newPixel << 8;
newPixel += blue;
return newPixel;
}
}
I have to do a similar task as part of a project for an asignment. I found in my workspace this piece of code, I think this is what you need:
Bitmap img = BitmapFactory.decodeResource(this.getResources(), drawable.testimage);
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
float a = 77f;
float b = 151f;
float c = 28f;
float t = 120 * -256f;
cm.set(new float[] { a, b, c, 0, t, a, b, c, 0, t, a, b, c, 0, t, 0, 0, 0, 1, 0 });
paint.setColorFilter(new ColorMatrixColorFilter(cm));
canvas.drawBitmap(img, 0, 0, paint);
Here I used ColorMatrix to generate a black and white image from a color one. Also I found this piece of code that I used to convert a color image to a gray scale image:
Bitmap result = Bitmap.createBitmap(destWidth, destHeight,Bitmap.Config.RGB_565);
RectF destRect = new RectF(0, 0, destWidth, destHeight);
Canvas canvas = new Canvas(result);
Paint paint = new Paint();
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.setSaturation(0);
ColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
paint.setColorFilter(filter);
canvas.drawBitmap(bitmap, sourceRect, destRect, paint);
Hope this help you.
You can take a look at the simple approach that Barcode Scanner uses on Android to convert an image to luminance and then to black and white. It would probably work well for OCR.
https://code.google.com/p/zxing/source/browse/trunk/core/src/com/google/zxing/common/HybridBinarizer.java https://code.google.com/p/zxing/source/browse/trunk/core/src/com/google/zxing/PlanarYUVLuminanceSource.java