1

My app is an OCR app base on Tesseract. It will do OCR task from camera picture. Users can take many pictures and put them into an OCR queue. To get more accuracy, I want to keep high quality image (I choose min size is 1024 x 768 (maybe larger in future), JPEG, 100% quality). When users take many pictures, there are three things to do:

  1. Save the image data byte[] to file and correct EXIF.
  2. Correct the image orientation base on device's orientation. I know there are some answers that said the image which comes out of the camera is not oriented automatically, have to correct it from file, like here and here. I'm not sure about it, I can setup the camera preview orientation correctly, but the image results aren't correct.
  3. Load bitmap from taken picture, convert it to grayscale and save to another file for OCR task.

And here is my try:

public static boolean saveBitmap(byte[] bitmapData, int orientation, String imagePath, String grayScalePath) throws Exception {
    Boolean rotationSuccess = false;
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    Bitmap originalBm = null;
    Bitmap bitmapRotate = null;
    Bitmap grayScale = null;
    FileOutputStream outStream = null;
    try {
          // save directly from byte[] to file
        saveBitmap(bitmapData, imagePath);

          // down sample
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(imagePath, options);
        int sampleSize = calculateInSampleSize(options, Config.CONFIG_IMAGE_WIDTH, Config.CONFIG_IMAGE_HEIGHT);
        options.inJustDecodeBounds = false;
        options.inSampleSize = sampleSize;

        originalBm = BitmapFactory.decodeFile(imagePath, options);
        Matrix mat = new Matrix();
        mat.postRotate(orientation);
        bitmapRotate = Bitmap.createBitmap(originalBm, 0, 0, originalBm.getWidth(), originalBm.getHeight(), mat, true);
        originalBm.recycle();
        originalBm = null;

        outStream = new FileOutputStream(new File(imagePath));
        bitmapRotate.compress(CompressFormat.JPEG, 100, outStream);

        // convert to gray scale
         grayScale = UIUtil.convertToGrayscale(bitmapRotate);
        saveBitmap(grayScale, grayScalePath);
        grayScale.recycle();
        grayScale = null;

        bitmapRotate.recycle();
        bitmapRotate = null;
        rotationSuccess = true;
    } catch (OutOfMemoryError e) {
        e.printStackTrace();
        System.gc();
    } finally {
        if (originalBm != null) {
            originalBm.recycle();
            originalBm = null;
        }

        if (bitmapRotate != null) {
            bitmapRotate.recycle();
            bitmapRotate = null;
        }

        if (grayScale != null) {
            grayScale.recycle();
            grayScale = null;
        }

        if (outStream != null) {
            try {
                outStream.close();
            } catch (IOException e) {
            }
            outStream = null;
        }
    }

            Log.d(TAG,"save completed");
    return rotationSuccess;
}

Save to file directly from byte[]

   public static void saveBitmap(byte[] bitmapData, String fileName) throws Exception {
    File file = new File(fileName);
    FileOutputStream fos;
    BufferedOutputStream bos = null;
    try {
        final int bufferSize = 1024 * 4;
        fos = new FileOutputStream(file);
        bos = new BufferedOutputStream(fos, bufferSize);
        bos.write(bitmapData);
        bos.flush();
    } catch (Exception ex) {
        throw ex;
    } finally {
        if (bos != null) {
            bos.close();
        }
    }
}

Calculate scale size

    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and
        // keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

When save complete, this image is loaded into thumbnail image view by UIL. The problem is the save task is very slow (wait some second before save complete and load into view), and sometime I got OutOfMemory exception.
Is there any ideas to reduce the save task and avoid OutOfMemory exception?
Any help would be appreciated!

P/S: the first time I try to convert byte[] to bitmap instead of save to file, and then rotate and convert to grayscale, but I still got above issues.
Update: here is the grayscale bitmap process:

   public static Bitmap convertToGrayscale(Bitmap bmpOriginal) {
    int width, height;
    height = bmpOriginal.getHeight();
    width = bmpOriginal.getWidth();    

    Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas c = new Canvas(bmpGrayscale);
    Paint paint = new Paint();
    ColorMatrix cm = new ColorMatrix();
    cm.setSaturation(0);
    ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
    paint.setColorFilter(f);
    c.drawBitmap(bmpOriginal, 0, 0, paint);
    return bmpGrayscale;
}

The OutOfMemory exception seldom occurred (just a few times) and I can't reproduce it now.

Community
  • 1
  • 1
ductran
  • 10,043
  • 19
  • 82
  • 165
  • Could you try putting your filesink into a separate process? Various ways to connect the 2 and 2 do the memory exchange. The 2nd process may give you more flexibility in using memory for the init process of the byte array from the camera. see Camera.ACTION_NEW_PICTURE: and use a service in separate process for the file-sink. – Robert Rowntree Jan 01 '14 at 17:06
  • Maybe I misunderstand. All my works are executed on background thread. When saving completed, they just notify on UI and tell UIL display thumbnail image. And my app using a custom camera instead of sending intent to default camera. My problem is that save task takes so much time, users have to wait for a while before seeing thumbnail image (but I don't lock UI). – ductran Jan 02 '14 at 03:10
  • @R4j why not let your app listen to the ACTION_NEW_PICTURE broadcast that the camera sends when picture gets added to mediastore, the Uri to the image is sent in the intent that hold that action as well. Then you could simply read the image uri and orientation from mediastore and perhaps also rotate your views instead of the image which will lead to that your method only needs to convert image to grayscale basically (will elaborate more in answer if you like my suggestion). – Magnus Jan 04 '14 at 19:15
  • Because my app need to use a custom camera view look like this http://images.techhive.com/images/article/2013/09/05-be-a-square-100054418-medium.png instead of sending intent to default camera. I need to save taken pictures and display them immediately on thumbnail list below camera button. And all stuffs should be in background. – ductran Jan 05 '14 at 03:41
  • @R4j Ok I see your problem then, have you done any manual time measurements of your method (what takes time?)? Perhaps it's the initial save that takes time, if you for instance take large pictures? Other than that, since your code depends on previous steps, rotation can't be done until first image is saved and black/white conversion can't be done before rotation is done you seem to be out of luck in running your tasks in parallell, since they'll effectively will be sequential (need to wait for previous step). – Magnus Jan 05 '14 at 22:45
  • Creating rotate bitmap cost so much time, then grayscale and save bitmap. I know my task is sequential, so I try to split them to another place. – ductran Jan 06 '14 at 08:29
  • Is the UIUtil.convertToGrayscale() method part of a library, or did you write it? Could you provide the code for that method as well as the stack trace from the OOM exception? – alanv Jan 07 '14 at 05:46
  • @alanv I have updated my question. The OOM exception seldom occurred and I can't reproduce it now. – ductran Jan 08 '14 at 03:20

1 Answers1

0

Update: Since you're still saying that the method takes too long time I would define a callback interface

interface BitmapCallback {
    onBitmapSaveComplete(Bitmap bitmap, int orientation);

    onBitmapRotateAndBWComlete(Bitmap bitmap);
}

Let your activity implement the above interface and convert the byte[] to bitmap in top of your saveBitmap method and fire the callback, before the first call to save. Rotate the imageView based on the orientation parameter and set a black/white filter on the imageView to fool the user into thinking that the bitmap is black and white (do this in your activity). See to that the calls are done on main thread (the calls to imageView). Keep your old method as you have it. (all steps need to be done anyway) Something like:

public static boolean saveBitmap(byte[] bitmapData, int orientation, String imagePath, String grayScalePath, BitmapCallback callback) throws Exception {
Boolean rotationSuccess = false;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap originalBm = null;
Bitmap bitmapRotate = null;
Bitmap grayScale = null;
FileOutputStream outStream = null;
try {
    // TODO: convert byte to Bitmap, see to that the image is not larger than your wanted size (1024z768)
    callback.onBitmapSaveComplete(bitmap, orientation);

    // save directly from byte[] to file
    saveBitmap(bitmapData, imagePath);
    .
    .
    // same as old 
    .
    .
    saveBitmap(grayScale, grayScalePath);
    // conversion done callback with the real fixed bitmap
    callback.onBitmapRotateAndBWComlete(grayScale);

    grayScale.recycle();
    grayScale = null;

    bitmapRotate.recycle();
    bitmapRotate = null;
    rotationSuccess = true;
  1. How do you setup your camera? What might be causing the long execution time in the first saveBitmap call, could be that you are using the default camera picture size settings and not reading the supported camera picture size and choosing best fit for your 1024x768 image needs. You might be taking big mpixel images and saving such, but in the end need you need < 1 mpixles (1024x768). Something like this in code:

    Camera camera = Camera.open();
    Parameters params = camera.getParameters();
    List sizes = params.getSupportedPictureSizes();
    // Loop camera sizes and find best match, larger than 1024x768

    This is probably where you will save most of the time if you are not doing this already. And do it only once, during some initialization phase.

  2. Increase the buffer to 8k in saveBitmap, change the 1024*4 to 1024*8, this would increase the performance at least, not save any significant time perhaps.

  3. To save/reuse bitmap memory consider using inBitmap field, if you have a post honeycomb version, of BitmapFactory.Options and set that field to point to bitmapRotate bitmap and send options down to your convertToGrayscale method to not need allocating yet another bitmap down in that method. Read about inBitmap here: inBitmap

Magnus
  • 1,483
  • 11
  • 14
  • I have done items 1 and 2 but it still slow. For the item 3, do you mean caching bitmap on memory? I don't understand how to apply it for my case. Could you provide a piece of code for it? And does it work for pre-Honeycomb version? – ductran Jan 08 '14 at 03:19
  • @R4j no it does not work pre honeycomb, if that's what your're dealing with? Will update answer as well. – Magnus Jan 08 '14 at 06:22
  • Hmm, this way maybe work. But I have to do 3 things before notifying callback: down sample byte[] to Bitmap with thumbnail size, correct orientation, set a black/white filter on the imageView (I don't know how, does it need to create bitmap?). Then do my 3 main tasks after display thumbnail. Seems this hack needs to do a lot of works. – ductran Jan 08 '14 at 17:35
  • @R4j What I'm trying to state above is that you don't have to do all those three steps (before you swhow something) you could fake both the rotation and black and white conversion by rotating and setting filter on imageView, that is what I would have done. You could add another callback that sends the correct oriented black and white image and sets the imageView after those steps are completed aswell, but first you should try to only get the bitmap and fake the other parts, and when rotate and that is complete callback again. (will update code aswell) – Magnus Jan 08 '14 at 17:55
  • Seems there is no way better than yours, so I will choose this.Thanks for your help :) – ductran Jan 09 '14 at 13:45
  • Thanks! Let me know if there's something else you need help with – Magnus Jan 09 '14 at 13:54