All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.ojil.algorithm.HaarClassifierCascade Maven / Gradle / Ivy

There is a newer version: 0.0.11
Show newest version
/*
 * HaarClassifierCascade.java
 *
 * Created on July 7, 2007, 3:23 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 *
 * Copyright 2007 by Jon A. Webb
 *     This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
 *
 *    You should have received a copy of the Lesser GNU General Public License
 *    along with this program.  If not, see .
 *
 */

package com.github.ojil.algorithm;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;

import com.github.ojil.core.Error;
import com.github.ojil.core.Gray32Image;
import com.github.ojil.core.Gray8Image;
import com.github.ojil.core.Image;
/**
 * HaarClassifierCascade implements a Haar classifier, which is a trainable
 * image processing tool for detecting the presence of a feature or class of
 * features. A Haar classifier is trained by providing it with a large collection
 * of positive and negative sample images. The training technique develops a
 * collection of simple feature detection operations (like simple edge detectors)
 * that are applied to the image and then thresholded. The feature detectors
 * are organized into a tree so that they work as a cascade. An image which makes
 * it to the end of the cascade has a high probability of actually containing the
 * feature in question (depending on how well the sample image selection was done
 * and how thorough the training was.) 
* The code here does not implement the training step, which is compute-intensive. * That should be run on a PC, using code from the Open Computer Vision (OpenCV) * library. The OpenCV is available on-line at * http://sourceforge.net/projects/opencvlibrary/. It can be run under Windows or * Linux and has been optimized for best performance on Intel processors. It * also includes multiprocessor support.
* Once the Haar classifier has been trained using the OpenCV HaarTraining * application, the cascade has to be transformed into a text file that can be * loaded into this code. This is done with a C++ program called * haar2j2me. Haar2j2me changes the floating-point values in the OpenCV's Haar * cascade into integer, scaling appropriately, and greatly reduces the size * of the file (the XML files produced by HaarTraining are just too large to fit * on many cellphones). You can find a copy of haar2j2me where you got this code.
* Note: the code below does not implement tilted features, and has not been * tested for anything but stump-based Haar classifiers. * @author webb */ public abstract class HaarClassifierCascade implements Serializable { /** * */ private static final long serialVersionUID = 3103210998383577218L; /** * The width of the image. */ protected int width; /** * Haar cascade image height. */ protected int height; // size of image /** * Returns the Haar cascade image width. * @return the Haar cascade image width. */ public int getWidth() { return width; }; /** * Returns the Haar cascade image height. * @return the Haar cascade image height. */ public int getHeight() { return height; } /** * Reads an array of characters, skipping newlines. * @param isr the input stream to read * @param rChars array of characters * @param nStart starting position in array to assign * @param nLength number of characters to read * @throws IOException if InputStreamReader.read() does * @throws Error if input is terminated before all characters are read */ protected static void readChars(InputStreamReader isr, char[] rChars, int nStart, int nLength) throws IOException, Error { for (int i=0; i * The separator character can be any non-numeric character.
* Note that I can't use Integer.parseInt because I don't know how many * characters are to be read. * @return the next integer read from the input stream. * @param isr The input stream. * @throws com.github.ojil.core.Error if there is a parse error in the file. * @throws java.io.IOException if the read method of isr returns an IOException. */ protected static int readInt(InputStreamReader isr) throws com.github.ojil.core.Error, IOException, IOException { int n = 0; int sign = 1; do { int nChar = isr.read(); if (nChar == -1) { throw new Error( Error.PACKAGE.ALGORITHM, ErrorCodes.INPUT_TERMINATED_EARLY, isr.toString(), null, null); } char c = (char) nChar; if (c == '-') { sign = -1; } else { if (!Character.isDigit(c)) { return n * sign; } n = n * 10 + Character.digit(c, 10); } } while (true); } // One Haar feature. Each consists of up to three weighted rectangles /** * HaarFeature defines an individual feature used by the Haar cascade. * A feature consists of up to three weighted rectangles (implemented * by HaarRect) which are convolved with the image. Their sum is the * result of applying the HaarFeature to the image. */ protected class HaarFeature implements Serializable { /** * */ private static final long serialVersionUID = 1636121702312072988L; /* * HaarRect.java * HaarRect is an abstract class to make the computation of the rectangular * basic component of a Haar feature. It is abstract because the placement * of the rectangle affects the sum that has to be done. * The basic computation is shown by the diagram below * ..........n1|_______n3| * ............|/////////| * ............|/////////| (// is the area we're summing) * ..........n4|///////n2| * xx is the sum of all pixels at and to the left and above of positions xx. * The computation is br - bl - tr + tl (tl is added because bl and * tr both include tl). * If the rectangle is at the edge of the image we don't use some of tl, tr, * or bl because they're off the edge of the image. So we have HaarRectTop, * HaarRectTopLeft, etc. */ abstract class HaarRect implements Serializable { /** * */ private static final long serialVersionUID = -8673919841412300053L; // eval returns the rectangle feature value for the current image. // input image is the cumulative sum of the original protected abstract int eval(Gray32Image i); // We precompute the indices of the features so we have to // change their values whenever the image width changes. protected abstract void setWidth(int nWidth); } // Used for third null rectangle when a HaarFeature only uses 2 // rectangles. class HaarRectNone extends HaarRect implements Serializable { /** * */ private static final long serialVersionUID = 3419647846408882196L; /** Creates a new instance of HaarRectNone */ public HaarRectNone() { } protected int eval(Gray32Image i) { return 0; } protected void setWidth(int nWidth) { } public String toString() { return "(hr 0 0 0 0 0)"; //$NON-NLS-1$ } } // HaarRect describes one rectangle in a Haar feature. It is used except // at the top or left side of the image or when the area of // the rectangle is 0. // There are up to 3 rectangles in a feature class HaarRectAny extends HaarRect implements Serializable { /** * */ private static final long serialVersionUID = -6033181980691702489L; private int n1, n2, n3, n4; private int tlx, tly, w, h; // rectangle coordinates private int weight; // convolution weight assigned to rectangle public HaarRectAny(int tlx, int tly, int w, int h, int weight) { this.tlx = tlx; this.tly = tly; this.w = w; this.h = h; this.weight = weight; } protected int eval(Gray32Image image) { Integer[] data = image.getData(); return weight * ( data[this.n1] + data[this.n2] - data[this.n3] - data[this.n4] ); } // when image width is changed we have to recompute the indices. protected void setWidth(int nWidth) { this.n1 = (tly-1)*nWidth + (tlx-1); this.n2 = (tly+h-1)*nWidth + (tlx+w-1); this.n3 = (tly-1)*nWidth + (tlx+w-1); this.n4 = (tly+h-1)*nWidth + (tlx-1); } public String toString() { return "(hr " + this.tlx + " " + this.tly + //$NON-NLS-1$ //$NON-NLS-2$ " " + this.w + " " + this.h + //$NON-NLS-1$ //$NON-NLS-2$ " " + this.weight + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } }; // HaarRectLeft describes one rectangle in a Haar feature // where the rectangle is at the left side of the image (x = 0 and y != 0) // There are up to 3 rectangles in a feature. class HaarRectLeft extends HaarRect implements Serializable { /** * */ private static final long serialVersionUID = 3287079704778038972L; private int n2, n3; private int tly, w, h; // rectangle coordinates private int weight; // convolution weight assigned to rectangle public HaarRectLeft(int tly, int w, int h, int weight) { this.tly = tly; this.w = w; this.h = h; this.weight = weight; } protected int eval(Gray32Image image) { Integer[] data = image.getData(); return weight * ( data[this.n2] - data[this.n3] ); } protected void setWidth(int nWidth) { // we precompute the indices so that we don't // have to do computation using nWidth in the // usual case, when nWidth doesn't change.' this.n2 = (tly+h-1)*nWidth + w - 1; this.n3 = (tly-1)*nWidth + w - 1; } public String toString() { return "(hr 0 " + this.tly + //$NON-NLS-1$ " " + this.w + " " + this.h + //$NON-NLS-1$ //$NON-NLS-2$ " " + this.weight + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } }; // HaarRectTop describes one rectangle in a Haar feature // where the rectangle is at the top of the image (y = 0 but x != 0) // There are up to 3 rectangles in a feature. class HaarRectTop extends HaarRect implements Serializable { /** * */ private static final long serialVersionUID = 770007602125395214L; private int n2, n4; private int tlx, w, h; // rectangle coordinates private int weight; // convolution weight assigned to rectangle public HaarRectTop(int tlx, int w, int h, int weight) { this.tlx = tlx; this.w = w; this.h = h; this.weight = weight; } protected int eval(Gray32Image image) { Integer[] data = image.getData(); return weight * ( data[this.n2] - data[this.n4] ); } protected void setWidth(int nWidth) { // we precompute the indices so that we don't // have to do computation using nWidth in the // usual case, when nWidth doesn't change.' this.n2 = (h - 1)*nWidth + (tlx+w - 1); this.n4 = (h - 1)*nWidth + (tlx-1); } public String toString() { return "(hr " + this.tlx + " 0 " + //$NON-NLS-1$ //$NON-NLS-2$ this.w + " " + this.h + //$NON-NLS-1$ " " + this.weight + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } }; // Used when the rectangle is at the top left of the image (x=0 and y=0) class HaarRectTopLeft extends HaarRect implements Serializable { /** * */ private static final long serialVersionUID = -2184273133213370871L; private int n2; private int w, h; // rectangle coordinates private int weight; // convolution weight assigned to rectangle public HaarRectTopLeft(int w, int h, int weight) { this.w = w; this.h = h; this.weight = weight; } protected int eval(Gray32Image image) { Integer[] data = image.getData(); return weight * ( data[this.n2] ); } protected void setWidth(int nWidth) { // we precompute the indices so that we don't // have to do computation using nWidth in the // usual case, when nWidth doesn't change.' this.n2 = (h - 1)*nWidth + w - 1; } public String toString() { return "(hr 0 0 " + this.w + " " + this.h + //$NON-NLS-1$ //$NON-NLS-2$ " " + this.weight + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } }; // construct from input // the expected input is // (hr ,,,,) // note: this is really a static constructor for HaarRect and // should be a static member of the abstract HaarRect class but // since I've made HaarRect an inner class it can't be. I couldn't // think of a better way to do this than to make this a member of // HaarFeature and make the name as below. private HaarRect makeHaarRectFromStream(InputStreamReader isr) throws com.github.ojil.core.Error, IOException { char[] rC = new char[4]; readChars(isr,rC, 0, 4); if ("(hr ".compareTo(new String(rC)) != 0) { //$NON-NLS-1$ throw new Error( Error.PACKAGE.ALGORITHM, ErrorCodes.PARSE_ERROR, new String(rC), "(hr ", isr.toString()); } int tlx = readInt(isr); int tly = readInt(isr); int w = readInt(isr); int h = readInt(isr); int weight = readInt(isr); if (w == 0 || h == 0) { return new HaarRectNone(); } else if (tlx == 0 && tly == 0) { return new HaarRectTopLeft(w, h, weight); } else if (tlx == 0) { return new HaarRectLeft(tly, w, h, weight); } else if (tly == 0) { return new HaarRectTop(tlx, w, h, weight); } else { return new HaarRectAny(tlx, tly, w, h, weight); } } // Private variables in HaarFeature private boolean bTilted; // in the present implementation bTilted // must always be false private HaarRect rect[]; // create HaarFeature from stream // expected input: (hf ) /** * Loads a HaarFeature from an input stream. This is the only way * to create a HaarFeature. The expected input is "(hf "Haar rect [0]" * "Haar rect [1]" "Haar rect [2]" tilted) where tilted is 1 if the rectangles * are tilted, 0 if not. Tilted rectangles are currently not implemented. The * Haar rectangles can be null, which means they have an area of 0. * @param isr Input stream. The expected input is "(hf "Haar rect [0]" * "Haar rect [1]" "Haar rect [2]" tilted) where tilted is 1 if the rectangles * are tilted, 0 if not. * @throws java.io.IOException if the input stream reader methods return IOException, or we get an early * end of file. * @throws com.github.ojil.core.Error if the input is not in the expected format. */ public HaarFeature(InputStreamReader isr) throws com.github.ojil.core.Error, IOException { char[] rC = new char[4]; readChars(isr,rC, 0, 4); if ("(hf ".compareTo(new String(rC)) != 0) { //$NON-NLS-1$ throw new Error( Error.PACKAGE.ALGORITHM, ErrorCodes.PARSE_ERROR, new String(rC), "(hf ", isr.toString()); } this.rect = new HaarRect[3]; this.rect[0] = makeHaarRectFromStream(isr); this.rect[1] = makeHaarRectFromStream(isr); this.rect[2] = makeHaarRectFromStream(isr); this.bTilted = (readInt(isr) == 1); } /** * Applies the HaarFeature to the image and returns the integer equal to the * result of convolving the rectangles in the feature with the image. * @param image the input image to which the feature is to be applied. The width should be * equal to the last width parameter passed to setWidth(). * @return the integer equal to the result of convolving the rectangles in the feature with the image. */ public int eval(Gray32Image image) { int nSum = 0; for (int i=0; i= private int alpha; // return result if successor = 0 public int eval(Gray32Image image) { int nHf = this.feature.eval(image); HaarWeakClassifier hcNext; if (nHf < this.threshold) { hcNext = this.left; } else { hcNext = this.right; } if (hcNext == null) { return this.alpha; } else { return hcNext.eval(image); } } }; // A HaarStageClassifer consists of an array of HaarClassifier's. // Each is evaluated and the sum of the results is compared with the // threshold. If it is >= the threshold then we // evaluate the HaarStageClassifer at child (if child is null we are successful). // If it is < the threshold we // go to the parent HaarStageClassifier and then continue at the parent's next, // unless it is null, in which case the result is 0. private int threshold; private HaarWeakClassifier classifier[]; private HaarStageClassifier next; private HaarStageClassifier child; private HaarClassifierTreeBase parent; public boolean eval(Image image) throws com.github.ojil.core.Error { if (!(image instanceof Gray32Image)) { throw new Error( Error.PACKAGE.ALGORITHM, ErrorCodes.IMAGE_NOT_GRAY32IMAGE, image.toString(), null, null); } Gray32Image g32 = (Gray32Image) image; int nSumHc = 0; for (int i=0; i= this.threshold) { if (this.child == null) { return true; } else { return this.child.eval(g32); } } else { if (this.parent == null || this.parent.next == null) { return false; } else { return this.parent.next.eval(g32); } } } } ///////////////////////////////////////////////////////////////////////// // // Stump-structured Haar classifier clases // ///////////////////////////////////////////////////////////////////////// // A stump-structured Haar classifier consists of a Haar feature and a // threshold. The Haar feature is evaluated. class HaarClassifierStumpBase extends HaarClassifierCascade implements Serializable { /** * */ private static final long serialVersionUID = -8724165260201623422L; // A stump-structured Haar classifier consists of a Haar feature and a // threshold. The Haar feature is evaluated. The result is compared with // t = threshold * variance_norm_factor. If < t then it returns a, // o/w b. private int nWidth = 0; // for detecting when image width changes public class HaarWeakClassifierStump implements HaarWeakClassifier, Serializable { /** * */ private static final long serialVersionUID = -559548922459735861L; private HaarFeature feature; // Haar feature tested by this classifier // threshold, a, and b are scaled by 2**16 = 65536 private int modThreshold; // calculated threshold private int threshold; // threshold feature compared with private int a, b; // return result if successor = 0 private int stdDev; // the standard deviation of the image, // scaled by 256 private int width, height; // create from input stream // expected data: (hwcs ,) public HaarWeakClassifierStump(InputStreamReader isr, int width, int height) throws com.github.ojil.core.Error, IOException { char[] rC = new char[6]; readChars(isr,rC, 0, 6); if ("(hwcs ".compareTo(new String(rC)) != 0) { //$NON-NLS-1$ throw new Error( Error.PACKAGE.ALGORITHM, ErrorCodes.PARSE_ERROR, new String(rC), "(hwcs ", isr.toString()); } this.feature = new HaarFeature(isr); this.threshold = readInt(isr); this.a = readInt(isr); this.b = readInt(isr); this.width = width; this.height = height; } public int eval(Gray32Image image) { int nHf = this.feature.eval(image) << 12; if (nHf < this.modThreshold) { return a; } else { return b; } } public void setWidth(int nWidth) { this.feature.setWidth(nWidth); // width affects threshold setThreshold(); } // this should be called whenever the underlying image changes // it accepts the standard deviation of the image, multiplied by // 256 public void setStdDev(int stdDev) { this.stdDev = stdDev; setThreshold(); } private void setThreshold() { this.modThreshold = ((this.threshold * this.stdDev >> 6) * this.width * this.height) >> 6; // this.threshold * this.stdDev / 256 / 65536; } public String toString() { return "(hwcs " + this.feature.toString() + //$NON-NLS-1$ this.threshold + " " + this.a + " " + this.b + " " + //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ this.width + " " + this.height + ")"; //$NON-NLS-1$ //$NON-NLS-2$ } }; // A stump-structured Haar classifier consists of a Haar feature and a // threshold. The Haar feature is evaluated. public class HaarClassifierStump implements HaarStageClassifier, Serializable { /** * */ private static final long serialVersionUID = 8964434533022477157L; private HaarWeakClassifierStump[] hwcs; // Haar feature tested by this classifier // theshold is scaled by 2**16 = 65536 private int threshold; // threshold feature compared with // create from stream // expected input (hcs ^count) public HaarClassifierStump(InputStreamReader isr, int width, int height) throws com.github.ojil.core.Error, IOException { char[] rC = new char[5]; readChars(isr,rC, 0, 5); if ("(hcs ".compareTo(new String(rC)) != 0) { //$NON-NLS-1$ throw new Error( Error.PACKAGE.ALGORITHM, ErrorCodes.PARSE_ERROR, new String(rC), "(hcs ", isr.toString()); } int n = readInt(isr); this.hwcs = new HaarWeakClassifierStump[n]; for (int i=0; i= this.threshold); } public void setWidth(int nWidth) { for (int i=0; i ^count) // the '(hcsb ' has already been read before this gets called public HaarClassifierStumpBase(InputStreamReader isr) throws com.github.ojil.core.Error, IOException { /* char[] rC = new char[6]; readChars(isr,rC, 0, 6); if ("(hcsb ".compareTo(new String(rC)) != 0) { throw new ParseException("Error at " + isr.toString() + "; read '" + new String(rC) + "'; expected '(hcsb '"); } */ this.width = readInt(isr); this.height = readInt(isr); int n = readInt(isr); this.hsc = new HaarClassifierStump[n]; for (int i=0; i




© 2015 - 2024 Weber Informatics LLC | Privacy Policy