Automatic Tool for Cropping Bills from Scans

What Is It?

At work, we’re developing a project aimed at small and medium-sized businesses that goes beyond simple accounting. It includes many innovative features, but today I want to focus on one in particular: the automatic processing of bills and invoices. Users can take advantage of this feature via a mobile client (Android, iOS) or a web application.

The Challenge

We encountered an issue where users were scanning multiple bills together in a single image, rather than following the "one image - one document" rule. This made it difficult to process the documents correctly. To address this, I developed a script that automatically detects and crops these multi-bill images into separate, individual images.

The Algorithm

To separate and crop the bills from a scanned image, I used a series of simple image preprocessing methods:

  1. Resize: The image is resized to a standard dimension for consistency.
  2. Morphological Operation (IMOPEN): This step helps in removing noise and refining the structure of the image.
  3. Convert to Binary: The image is converted into a binary format, where pixels are categorized as either black or white, making it easier to identify distinct regions.
  4. Blob Detection: The binary image is analyzed to detect continuous white areas (blobs), which represent the bills.
  5. Find Largest Blob: The largest blobs are identified as potential bill areas.
  6. Bounding Box: A bounding box is drawn around each detected blob to define the region of interest.
  7. Percentage Limit: This step ensures that only sufficiently large blobs (likely to be bills) are considered.
  8. Crop and Resize: The identified regions are cropped out and resized as individual images.

The code implementing these steps is straightforward and self-explanatory.

Requirements

To run this script, you’ll need:

  • GNU Octave, version 3.6.4
  • Octave with the image package
       
#! /bin/octave -qf
# a sample Octave program

% @Author Miroslav Bodis
% created: 2014-10-04
% updated: 2015-10-04

% -- -- CUT BILL/BILLS FROM PICTURE -- --
% 0. VALIDATE INPUT
% 1. RESIZE
% 2. IMOPEN - MORPHOLOGICAL OPERATION
% 4. IMG TO BINARY
% 5. BLOD DETECTS 
% 6. FIND BIGGEST BLOB
% 7. GET BOUNDING BOX
% 8. PERCENTAGE LIMIT
% 9. CROP WITH RESIZE



IMG_TO_BINARY_TRASH = 0.3;
IMG_LIMIT_BILL_DETECT = 1000;
IMG_LIMIT_BILL_PERCENT = 15;
IMG_SIZE_LIMIT = 550;
DEBUG = false;
% DEBUG = true;




% 0 ---- VALIDATE INPUT
%------------------------------
% - input require input image
 arg_list = argv();

 if ( size(arg_list, 1) == 0)
   printf("\nrequired 1 arg, input file \n");
   return;
 endif;  

 %validate input
 if ( size(arg_list, 1) != 1)
     printf("\nERROR invalid input, allowing only one image as input_file! \n\n");
   return;
 endif;

 %input exists
 if (!exist(arg_list{1}))
   fprintf("\n image %s not exists\n", arg_list{1});
   return;
 endif;

 pkg load image;



% 1 ---- RESIZE 
%------------------------------
 img_rgb_full = imread(arg_list{1}); 

 [height, width, rgb] = size(img_rgb_full);
 resize = 1;
 if (width > IMG_SIZE_LIMIT && width > height)
  resize = IMG_SIZE_LIMIT / width;
 elseif (height > IMG_SIZE_LIMIT)
  resize = IMG_SIZE_LIMIT / height;
 endif;

 img_rgb = imresize(img_rgb_full, resize, 'nearest');
 if (DEBUG)
  imwrite(img_rgb, 'debug_0_resize.jpg', 'jpg', 'Quality', 100);
 endif;



% 2 ---- IMOPEN - MORPHOLOGICAL OPERATION
%------------------------------
 gray = rgb2gray(img_rgb);

 % disc = uint8 ([0 0 0 1 0 0 0
 %               0 1 1 1 1 1 0
 %               0 1 1 1 1 1 0
 %               1 1 1 1 1 1 1
 %               0 1 1 1 1 1 0
 %               0 1 1 1 1 1 0
 %               0 0 0 1 0 0 0]);

 disc = uint8 ([0 0 0 0 0 1 0 0 0 0 0
      0 0 0 1 1 1 1 1 0 0 0
      0 0 1 1 1 1 1 1 1 0 0
      0 1 1 1 1 1 1 1 1 1 0 
      0 1 1 1 1 1 1 1 1 1 0
      1 1 1 1 1 1 1 1 1 1 1
      0 1 1 1 1 1 1 1 1 1 0
      0 1 1 1 1 1 1 1 1 1 0
      0 0 1 1 1 1 1 1 1 0 0
      0 0 0 1 1 1 1 1 0 0 0
      0 0 0 0 0 1 0 0 0 0 0]);

 gray = imdilate (imerode(gray, disc), disc);
 if (DEBUG)
  imwrite(gray, 'debug_1_opening.jpg', 'jpg', 'Quality', 100);
 endif;


% 3 ---- IMG TO BINARY
%------------------------------
 bw = im2bw(gray, IMG_TO_BINARY_TRASH);
 if (DEBUG)
  imwrite(bw, 'debug_2_im2bw.jpg', 'jpg', 'Quality', 100);
 endif;



% 4 ---- BLOB DETECTS 
%------------------------------
 cc = bwconncomp(bw, 4);
 % cc.NumObjects % print number of blobs
 % cc.ImageSize % print image size

 % -- -- select blob number 225
 % grain = false(size(bw));
 % grain(cc.PixelIdxList{225}) = true;
 % imwrite(grain, 'output.jpg', 'jpg', 'Quality', 100);



% 5 ---- FIND BIGGEST BLOB 
%------------------------------
 % -- -- biggest blob
 % numPixels = cellfun(@numel,cc.PixelIdxList); 
 % [biggest,idx] = max(numPixels);   

 % -- find more bigger objects
 object = 0;
 for j = 1:cc.NumObjects  
  
  [s1,s2] = size(cc.PixelIdxList{j});

  if (s1 > IMG_LIMIT_BILL_DETECT)
   object+=1;   
   bw = false(size(bw));    % black img   
   bw(cc.PixelIdxList{j}) = true; % add selected object  
   if (DEBUG)
    imwrite(bw, 'debug_34_biggest_bloc.jpg', 'jpg', 'Quality', 100);
   endif;



% 6 ---- GET BOUNDING BOX
%------------------------------
   boundaries = bwboundaries(bw);
   numberOfBoundaries = size(boundaries);

   from = boundaries{1};
   fromx = min(from(:,1));
   fromy = min(from(:,2));

   to = boundaries{2};
   tox = max(to(:,1));
   toy = max(to(:,2));


   if (DEBUG)
    for k = 1 : numberOfBoundaries
        thisBoundary = boundaries{k};
        plot(thisBoundary(:,2), thisBoundary(:,1), 'g', 'LineWidth', 2); 
    end  
    print("debug_5_boundbox_plot.png", "-dpng");
   endif;



% 7 ---- PERCENTAGE LIMIT
%------------------------------
   l=tox-fromx;
   w=toy-fromy;
   [x,y,rgb] = size(img_rgb);
   blob_percent_area = ((l*w) / (x*y) *100)
   if ( blob_percent_area > IMG_LIMIT_BILL_PERCENT)  

% 8 ---- CROP WITH RESIZE 
%------------------------------
   extra = (1/resize);
   x1 = round( fromx * extra);
   x2 = round( (x1 + l * extra) );
   y1 = round( fromy * extra );
   y2 = round( (y1 + w * extra) );

   crop_img = img_rgb_full(x1:x2, y1:y2);

    name = char ([98, 105, 108, 108, 45, 48+object]); % bill-1, bill-2 ... 
    imwrite(crop_img, name, 'jpg', 'Quality', 100);

   endif; 



  endif; 
 endfor


        
         

Comments

Popular posts from this blog

Skate Tricks Recognition Using Gyroscope

Play table

Counting dice and train wagons using computer vision