karthik.Barcode.MatrixBarcode.java Source code

Java tutorial

Introduction

Here is the source code for karthik.Barcode.MatrixBarcode.java

Source

/*
 * Copyright (C) 2014 karthik
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package karthik.Barcode;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfPoint;
import org.opencv.core.MatOfPoint2f;
import org.opencv.core.RotatedRect;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;
import org.opencv.utils.Converters;

/**
 *
 * @author karthik
 */
public class MatrixBarcode extends Barcode {

    // used in histogram calculation
    private static final int DUMMY_ANGLE = 255;
    private static final Scalar ZERO_SCALAR = new Scalar(0);

    private static Mat hist = new MatOfInt(ImageInfo.bins, 1);
    private static Mat histIdx = new Mat();
    private static Mat mask = new Mat(); // empty Mat to use as mask for histogram calculation
    private static Mat angles;

    private static final Mat hierarchy = new Mat(); // empty Mat required as parameter in contour finding. Not used anywhere else.
    private static final Map<Integer, Scalar> scalarDict = new HashMap<Integer, Scalar>();

    static {
        // create a hashmap with Scalar objects used during histogram calculation
        // done so that we can reuse these objects instead of creating and destroying them
        for (int r = 1; r <= 181; r += ImageInfo.BIN_WIDTH)
            scalarDict.put(r, new Scalar(r));

        // add objects used when trimming angles to 0-360 range
        scalarDict.put(170, new Scalar(170));
        scalarDict.put(180, new Scalar(180));
        scalarDict.put(-180, new Scalar(-180));
        scalarDict.put(360, new Scalar(360));
        scalarDict.put(DUMMY_ANGLE, new Scalar(DUMMY_ANGLE));
    }

    public MatrixBarcode(String filename, boolean debug, TryHarderFlags flag) throws IOException {
        super(filename, flag);
        DEBUG_IMAGES = debug;
        img_details.searchType = CodeType.MATRIX;
    }

    public MatrixBarcode(String image_name, Mat img, TryHarderFlags flag) throws IOException {
        super(img, flag);
        name = image_name;
        img_details.searchType = CodeType.MATRIX;
        DEBUG_IMAGES = false;
    }

    public List<CandidateResult> locateBarcode() throws IOException {

        calcGradientDirectionAndMagnitude();
        for (int tileSize = searchParams.tileSize; tileSize < rows && tileSize < cols; tileSize *= 4) {
            img_details.probabilities = calcProbabilityMatrix(tileSize); // find areas with low variance in gradient direction

            //    connectComponents();
            List<MatOfPoint> contours = new ArrayList<MatOfPoint>();
            // findContours modifies source image so probabilities pass it a clone of img_details.probabilities
            // img_details.probabilities will be used again shortly to expand the barcode region
            Imgproc.findContours(img_details.probabilities.clone(), contours, hierarchy, Imgproc.RETR_LIST,
                    Imgproc.CHAIN_APPROX_SIMPLE);

            double bounding_rect_area = 0;
            RotatedRect minRect;
            CandidateResult ROI;
            int area_multiplier = (searchParams.RECT_HEIGHT * searchParams.RECT_WIDTH)
                    / (searchParams.PROB_MAT_TILE_SIZE * searchParams.PROB_MAT_TILE_SIZE);
            // pictures were downsampled during probability calc so we multiply it by the tile size to get area in the original picture

            for (int i = 0; i < contours.size(); i++) {
                double area = Imgproc.contourArea(contours.get(i));

                if (area * area_multiplier < searchParams.THRESHOLD_MIN_AREA) // ignore contour if it is of too small a region
                    continue;

                minRect = Imgproc.minAreaRect(new MatOfPoint2f(contours.get(i).toArray()));
                bounding_rect_area = minRect.size.width * minRect.size.height;
                if (DEBUG_IMAGES) {
                    System.out.println("Area is " + area * area_multiplier + " MIN_AREA is "
                            + searchParams.THRESHOLD_MIN_AREA);
                    System.out.println("area ratio is " + ((area / bounding_rect_area)));
                }

                if ((area / bounding_rect_area) > searchParams.THRESHOLD_AREA_RATIO) // check if contour is of a rectangular object
                {
                    CandidateMatrixBarcode cb = new CandidateMatrixBarcode(img_details, minRect, searchParams);
                    if (DEBUG_IMAGES)
                        cb.debug_drawCandidateRegion(new Scalar(0, 255, 128), img_details.src_scaled);
                    // get candidate regions to be a barcode

                    // rotates candidate region to straighten it based on the angle of the enclosing RotatedRect                
                    ROI = cb.NormalizeCandidateRegion(Barcode.USE_ROTATED_RECT_ANGLE);
                    if (postProcessResizeBarcode)
                        ROI.ROI = scale_candidateBarcode(ROI.ROI);

                    candidateBarcodes.add(ROI);

                    if (DEBUG_IMAGES)
                        cb.debug_drawCandidateRegion(new Scalar(0, 0, 255), img_details.src_scaled);
                }
            }
        }
        return candidateBarcodes;
    }

    private void calcGradientDirectionAndMagnitude() {
        // calculates magnitudes and directions of gradients in the image
        // results are stored in appropriate matrices in img_details object

        Imgproc.Scharr(img_details.src_grayscale, img_details.scharr_x, CvType.CV_32F, 1, 0);
        Imgproc.Scharr(img_details.src_grayscale, img_details.scharr_y, CvType.CV_32F, 0, 1);

        // calc angle using Core.phase function - quicker than using atan2 manually
        Core.phase(img_details.scharr_x, img_details.scharr_y, img_details.gradient_direction, true);

        // convert angles from 180-360 to 0-180 range and set angles from 170-180 to 0
        Core.inRange(img_details.gradient_direction, scalarDict.get(180), scalarDict.get(360), img_details.mask);
        Core.add(img_details.gradient_direction, scalarDict.get(-180), img_details.gradient_direction,
                img_details.mask);
        Core.inRange(img_details.gradient_direction, scalarDict.get(170), scalarDict.get(180), img_details.mask);
        img_details.gradient_direction.setTo(ZERO_SCALAR, img_details.mask);

        // convert type after modifying angle so that angles above 360 don't get truncated
        img_details.gradient_direction.convertTo(img_details.gradient_direction, CvType.CV_8U);
        if (DEBUG_IMAGES)
            write_Mat("angles.csv", img_details.gradient_direction);

        // calculate magnitude of gradient, normalize and threshold
        Core.magnitude(img_details.scharr_x, img_details.scharr_y, img_details.gradient_magnitude);
        Core.normalize(img_details.gradient_magnitude, img_details.gradient_magnitude, 0, 255, Core.NORM_MINMAX,
                CvType.CV_8U);
        Imgproc.threshold(img_details.gradient_magnitude, img_details.gradient_magnitude, 50, 255,
                Imgproc.THRESH_BINARY | Imgproc.THRESH_OTSU);

        // set angle to DUMMY_ANGLE = 255 at all points where gradient magnitude is 0 i.e. where there are no edges
        // these angles will be ignored in the histogram calculation since that counts only up to 180
        Core.inRange(img_details.gradient_magnitude, ZERO_SCALAR, ZERO_SCALAR, img_details.mask);
        img_details.gradient_direction.setTo(scalarDict.get(DUMMY_ANGLE), img_details.mask);
        // add 1 to gradient directions so that gradients of 0 can be located
        Core.add(img_details.gradient_direction, new Scalar(1), img_details.gradient_direction);

        // calculate integral image for edge density
        img_details.edgeDensity = calcEdgeDensityIntegralImage();

        // calculate histograms for each tile
        calcHistograms();

        if (DEBUG_IMAGES) {
            write_Mat("magnitudes.csv", img_details.gradient_magnitude);
            write_Mat("angles_modified.csv", img_details.gradient_direction);
        }
    }

    private Mat calcProbabilityMatrix(int tileSize) {
        // calculate probability of a barcode region in each tile based on HOG data for each tile

        // calculate probabilities for each pixel from window around it, normalize and threshold
        Mat probabilities = calcProbabilityTilings(tileSize);

        double debug_prob_thresh = Imgproc.threshold(probabilities, probabilities, 128, 255, Imgproc.THRESH_BINARY);

        return probabilities;
    }

    private Mat calcProbabilityTilings(int tileSize) {
        // calculates probability of each tile being in a 2D barcode region
        // tiles must be square
        assert (searchParams.RECT_HEIGHT == searchParams.RECT_WIDTH) : "RECT_HEIGHT and RECT_WIDTH must be equal in searchParams imageSpecificParams";

        int probMatTileSize = (int) (tileSize * (searchParams.PROB_MAT_TILE_SIZE / (1.0 * searchParams.tileSize)));
        int threshold_min_gradient_edges = (int) (tileSize * tileSize
                * searchParams.THRESHOLD_MIN_GRADIENT_EDGES_MULTIPLIER);

        int right_col, bottom_row;
        int prob_mat_right_col, prob_mat_bottom_row;

        Mat imgWindow; // used to hold sub-matrices from the image that represent the window around the current point
        Mat prob_window; // used to hold sub-matrices into probability matrix that represent window around current point

        int num_edges;
        double prob;
        int max_angle_idx, second_highest_angle_index, max_angle_count, second_highest_angle_count, angle_diff;

        img_details.probabilities.setTo(ZERO_SCALAR);

        for (int i = 0, row_offset = 0; i < rows; i += tileSize, row_offset += probMatTileSize) {
            // first do bounds checking for bottom right of tiles

            bottom_row = java.lang.Math.min((i + tileSize), rows);
            prob_mat_bottom_row = java.lang.Math.min((row_offset + probMatTileSize), img_details.probMatRows);

            for (int j = 0, col_offset = 0; j < cols; j += tileSize, col_offset += probMatTileSize) {

                // then calculate the column locations of the rectangle and set them to -1 
                // if they are outside the matrix bounds                
                right_col = java.lang.Math.min((j + tileSize), cols);
                prob_mat_right_col = java.lang.Math.min((col_offset + probMatTileSize), img_details.probMatCols);

                // calculate number of edges in the tile using the already calculated integral image 
                num_edges = (int) calc_rect_sum(img_details.edgeDensity, img_details.histNumRows,
                        img_details.histNumCols, i, bottom_row, j, right_col);

                if (num_edges < threshold_min_gradient_edges)
                    // if gradient density is below the threshold level, prob of matrix code in this tile is 0
                    continue;

                for (int r = 0; r < img_details.bins; r++) {

                    img_details.histArray[r] = (int) calc_rect_sum(img_details.histIntegralArrays[r],
                            img_details.histNumRows, img_details.histNumCols, i, bottom_row, j, right_col);
                }

                hist = Converters.vector_int_to_Mat(Arrays.asList(img_details.histArray));
                Core.sortIdx(hist, histIdx, Core.SORT_EVERY_COLUMN + Core.SORT_DESCENDING);

                max_angle_idx = (int) histIdx.get(0, 0)[0];
                max_angle_count = (int) hist.get(max_angle_idx, 0)[0];

                second_highest_angle_index = (int) histIdx.get(1, 0)[0];
                second_highest_angle_count = (int) hist.get(second_highest_angle_index, 0)[0];

                angle_diff = Math.abs(max_angle_idx - second_highest_angle_index);

                // formula below is modified from Szentandrasi, Herout, Dubska paper pp. 4
                prob = 0;
                if (angle_diff != 1) // ignores tiles where there is just noise between adjacent bins in the histogram
                    prob = 2.0 * Math.min(max_angle_count, second_highest_angle_count)
                            / (max_angle_count + second_highest_angle_count);

                prob_window = img_details.probabilities.submat(row_offset, prob_mat_bottom_row, col_offset,
                        prob_mat_right_col);
                prob_window.setTo(new Scalar((int) (prob * 255)));

            } // for j
        } // for i

        return img_details.probabilities;

    }

    private int[] calcEdgeDensityIntegralImage() {
        // calculates number of edges in the image and returns it as an integral image
        // first set all non-zero gradient magnitude points (i.e. all edges) to 1
        // then calculate the integral image from the above
        // we can now calculate the number of edges in any tile in the matrix using the integral image

        Imgproc.threshold(img_details.gradient_magnitude, img_details.temp_integral, 1, 1, Imgproc.THRESH_BINARY);
        Imgproc.integral(img_details.temp_integral, img_details.temp_integral);

        img_details.temp_integral.get(0, 0, img_details.edgeDensity);

        return img_details.edgeDensity;
    }

    private void calcHistograms() {
        /* calculate histogram by masking for angles inside each bin, thresholding to set all those values to 1
           and then creating an integral image. We can now calculate histograms for any size tile within 
           the original image more efficiently than by using the built in calcHist method which would have to 
           recalculate the histogram for every tile size.
        */
        Mat target;
        angles = img_details.gradient_direction.clone();
        target = img_details.temp_integral;

        for (int binRange = 1, integralIndex = 0; binRange < 181; binRange += img_details.BIN_WIDTH, integralIndex++) {
            target.setTo(ZERO_SCALAR);

            img_details.gradient_direction.copyTo(angles);
            Core.inRange(img_details.gradient_direction, scalarDict.get(binRange),
                    scalarDict.get(binRange + img_details.BIN_WIDTH), mask);
            Core.bitwise_not(mask, mask);
            angles.setTo(ZERO_SCALAR, mask);

            Imgproc.threshold(angles, target, 0, 1, Imgproc.THRESH_BINARY);
            Imgproc.integral(target, target);
            target.get(0, 0, img_details.histIntegralArrays[integralIndex]);
        }

        // there is some problem if the created integral image does not have exactly one channel
        assert (target.channels() == 1) : "Integral does not have exactly one channel";

    }

}