/* * Copyright (c) 2015 OpenALPR Technology, Inc. * Open source Automated License Plate Recognition [http://www.openalpr.com] * * This file is part of OpenALPR. * * OpenALPR is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License * version 3 as published by the Free Software Foundation * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include "charactersegmenter.h" using namespace cv; using namespace std; namespace alpr { CharacterSegmenter::CharacterSegmenter(PipelineData* pipeline_data) { this->pipeline_data = pipeline_data; this->config = pipeline_data->config; this->confidence = 0; if (this->config->debugCharSegmenter) cout << "Starting CharacterSegmenter" << endl; //CharacterRegion charRegion(img, debug); timespec startTime; getTimeMonotonic(&startTime); if (pipeline_data->plate_inverted) bitwise_not(pipeline_data->crop_gray, pipeline_data->crop_gray); pipeline_data->clearThresholds(); pipeline_data->thresholds = produceThresholds(pipeline_data->crop_gray, config); // TODO: Perhaps a bilateral filter would be better here. medianBlur(pipeline_data->crop_gray, pipeline_data->crop_gray, 3); if (this->config->debugCharSegmenter) cout << "Segmenter: inverted: " << pipeline_data->plate_inverted << endl; if (pipeline_data->plate_inverted) bitwise_not(pipeline_data->crop_gray, pipeline_data->crop_gray); if (this->config->debugCharSegmenter) { displayImage(config, "CharacterSegmenter Thresholds", drawImageDashboard(pipeline_data->thresholds, CV_8U, 3)); } for (unsigned int lineidx = 0; lineidx < pipeline_data->textLines.size(); lineidx++) { this->top = pipeline_data->textLines[lineidx].topLine; this->bottom = pipeline_data->textLines[lineidx].bottomLine; float avgCharHeight = pipeline_data->textLines[lineidx].lineHeight; float height_to_width_ratio = pipeline_data->config->charHeightMM / pipeline_data->config->charWidthMM; float avgCharWidth = avgCharHeight / height_to_width_ratio; removeSmallContours(pipeline_data->thresholds, avgCharHeight, pipeline_data->textLines[lineidx]); // Do the histogram analysis to figure out char regions timespec startTime; getTimeMonotonic(&startTime); vector allHistograms; vector lineBoxes; for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++) { Mat histogramMask = Mat::zeros(pipeline_data->thresholds[i].size(), CV_8U); fillConvexPoly(histogramMask, pipeline_data->textLines[lineidx].linePolygon.data(), pipeline_data->textLines[lineidx].linePolygon.size(), Scalar(255,255,255)); VerticalHistogram vertHistogram(pipeline_data->thresholds[i], histogramMask); if (this->config->debugCharSegmenter) { Mat histoCopy(vertHistogram.histoImg.size(), vertHistogram.histoImg.type()); //vertHistogram.copyTo(histoCopy); cvtColor(vertHistogram.histoImg, histoCopy, CV_GRAY2RGB); string label = "threshold: " + toString(i); allHistograms.push_back(addLabel(histoCopy, label)); } float score = 0; vector charBoxes = getHistogramBoxes(vertHistogram, avgCharWidth, avgCharHeight, &score); if (this->config->debugCharSegmenter) { for (unsigned int cboxIdx = 0; cboxIdx < charBoxes.size(); cboxIdx++) { rectangle(allHistograms[i], charBoxes[cboxIdx], Scalar(0, 255, 0)); } Mat histDashboard = drawImageDashboard(allHistograms, allHistograms[0].type(), 1); displayImage(config, "Char seg histograms", histDashboard); } for (unsigned int z = 0; z < charBoxes.size(); z++) lineBoxes.push_back(charBoxes[z]); //drawAndWait(&histogramMask); } float medianCharWidth = avgCharWidth; vector widthValues; // Compute largest char width for (unsigned int i = 0; i < lineBoxes.size(); i++) { widthValues.push_back(lineBoxes[i].width); } medianCharWidth = median(widthValues.data(), widthValues.size()); if (config->debugTiming) { timespec endTime; getTimeMonotonic(&endTime); cout << " -- Character Segmentation Create and Score Histograms Time: " << diffclock(startTime, endTime) << "ms." << endl; } vector candidateBoxes = getBestCharBoxes(pipeline_data->thresholds[0], lineBoxes, medianCharWidth); if (this->config->debugCharSegmenter) { // Setup the dashboard images to show the cleaning filters for (unsigned int i = 0; i < pipeline_data->thresholds.size(); i++) { Mat cleanImg = Mat::zeros(pipeline_data->thresholds[i].size(), pipeline_data->thresholds[i].type()); Mat boxMask = getCharBoxMask(pipeline_data->thresholds[i], candidateBoxes); pipeline_data->thresholds[i].copyTo(cleanImg); bitwise_and(cleanImg, boxMask, cleanImg); cvtColor(cleanImg, cleanImg, CV_GRAY2BGR); for (unsigned int c = 0; c < candidateBoxes.size(); c++) rectangle(cleanImg, candidateBoxes[c], Scalar(0, 255, 0), 1); imgDbgCleanStages.push_back(cleanImg); } } getTimeMonotonic(&startTime); filterEdgeBoxes(pipeline_data->thresholds, candidateBoxes, medianCharWidth, avgCharHeight); candidateBoxes = filterMostlyEmptyBoxes(pipeline_data->thresholds, candidateBoxes); candidateBoxes = combineCloseBoxes(candidateBoxes, medianCharWidth); candidateBoxes = filterMostlyEmptyBoxes(pipeline_data->thresholds, candidateBoxes); for (unsigned int cbox = 0; cbox < candidateBoxes.size(); cbox++) pipeline_data->charRegions.push_back(candidateBoxes[cbox]); if (config->debugTiming) { timespec endTime; getTimeMonotonic(&endTime); cout << " -- Character Segmentation Box cleaning/filtering Time: " << diffclock(startTime, endTime) << "ms." << endl; } if (this->config->debugCharSegmenter) { Mat imgDash = drawImageDashboard(pipeline_data->thresholds, CV_8U, 3); displayImage(config, "Segmentation after cleaning", imgDash); Mat generalDash = drawImageDashboard(this->imgDbgGeneral, this->imgDbgGeneral[0].type(), 2); displayImage(config, "Segmentation General", generalDash); Mat cleanImgDash = drawImageDashboard(this->imgDbgCleanStages, this->imgDbgCleanStages[0].type(), 3); displayImage(config, "Segmentation Clean Filters", cleanImgDash); } } cleanCharRegions(pipeline_data->thresholds, pipeline_data->charRegions); if (config->debugTiming) { timespec endTime; getTimeMonotonic(&endTime); cout << "Character Segmenter Time: " << diffclock(startTime, endTime) << "ms." << endl; } } CharacterSegmenter::~CharacterSegmenter() { } // Given a histogram and the horizontal line boundaries, respond with an array of boxes where the characters are // Scores the histogram quality as well based on num chars, char volume, and even separation vector CharacterSegmenter::getHistogramBoxes(VerticalHistogram histogram, float avgCharWidth, float avgCharHeight, float* score) { float MIN_HISTOGRAM_HEIGHT = avgCharHeight * config->segmentationMinCharHeightPercent; float MAX_SEGMENT_WIDTH = avgCharWidth * config->segmentationMaxCharWidthvsAverage; //float MIN_BOX_AREA = (avgCharWidth * avgCharHeight) * 0.25; int pxLeniency = 2; vector charBoxes; vector allBoxes = get1DHits(histogram.histoImg, pxLeniency); for (unsigned int i = 0; i < allBoxes.size(); i++) { if (allBoxes[i].width >= config->segmentationMinBoxWidthPx && allBoxes[i].width <= MAX_SEGMENT_WIDTH && allBoxes[i].height > MIN_HISTOGRAM_HEIGHT ) { charBoxes.push_back(allBoxes[i]); } else if (allBoxes[i].width > avgCharWidth * 2 && allBoxes[i].width < MAX_SEGMENT_WIDTH * 2 && allBoxes[i].height > MIN_HISTOGRAM_HEIGHT) { // rectangle(histogram.histoImg, allBoxes[i], Scalar(255, 0, 0) ); // drawAndWait(&histogram.histoImg); // Try to split up doubles into two good char regions, check for a break between 40% and 60% int leftEdge = allBoxes[i].x + (int) (((float) allBoxes[i].width) * 0.4f); int rightEdge = allBoxes[i].x + (int) (((float) allBoxes[i].width) * 0.6f); int minX = histogram.getLocalMinimum(leftEdge, rightEdge); int maxXChar1 = histogram.getLocalMaximum(allBoxes[i].x, minX); int maxXChar2 = histogram.getLocalMaximum(minX, allBoxes[i].x + allBoxes[i].width); int minHeight = histogram.getHeightAt(minX); int maxHeightChar1 = histogram.getHeightAt(maxXChar1); int maxHeightChar2 = histogram.getHeightAt(maxXChar2); if (maxHeightChar1 > MIN_HISTOGRAM_HEIGHT && minHeight < (0.25 * ((float) maxHeightChar1))) { // Add a box for Char1 Point botRight = Point(minX - 1, allBoxes[i].y + allBoxes[i].height); charBoxes.push_back(Rect(allBoxes[i].tl(), botRight) ); } if (maxHeightChar2 > MIN_HISTOGRAM_HEIGHT && minHeight < (0.25 * ((float) maxHeightChar2))) { // Add a box for Char2 Point topLeft = Point(minX + 1, allBoxes[i].y); charBoxes.push_back(Rect(topLeft, allBoxes[i].br()) ); } } } return charBoxes; } vector CharacterSegmenter::getBestCharBoxes(Mat img, vector charBoxes, float avgCharWidth) { float MAX_SEGMENT_WIDTH = avgCharWidth * 1.65; // This histogram is based on how many char boxes (from ALL of the many thresholded images) are covering each column // Makes a sort of histogram from all the previous char boxes. Figures out the best fit from that. Mat histoImg = Mat::zeros(Size(img.cols, img.rows), CV_8U); int columnCount; for (int col = 0; col < img.cols; col++) { columnCount = 0; for (unsigned int i = 0; i < charBoxes.size(); i++) { if (col >= charBoxes[i].x && col < (charBoxes[i].x + charBoxes[i].width)) columnCount++; } // Fill the line of the histogram for (; columnCount > 0; columnCount--) histoImg.at(histoImg.rows - columnCount, col) = 255; } VerticalHistogram histogram(histoImg, Mat::ones(histoImg.size(), CV_8U)); // Go through each row in the histoImg and score it. Try to find the single line that gives me the most right-sized character regions (based on avgCharWidth) int bestRowIndex = 0; float bestRowScore = 0; vector bestBoxes; for (int row = 0; row < histoImg.rows; row++) { vector validBoxes; vector allBoxes = get1DHits(histoImg, row); if (this->config->debugCharSegmenter) cout << "All Boxes size " << allBoxes.size() << endl; if (allBoxes.size() == 0) break; float rowScore = 0; for (unsigned int boxidx = 0; boxidx < allBoxes.size(); boxidx++) { int w = allBoxes[boxidx].width; if (w >= config->segmentationMinBoxWidthPx && w <= MAX_SEGMENT_WIDTH) { float widthDiffPixels = abs(w - avgCharWidth); float widthDiffPercent = widthDiffPixels / avgCharWidth; rowScore += 10 * (1 - widthDiffPercent); if (widthDiffPercent < 0.25) // Bonus points when it's close to the average character width rowScore += 8; validBoxes.push_back(allBoxes[boxidx]); } else if (w > avgCharWidth * 2 && w <= MAX_SEGMENT_WIDTH * 2 ) { // Try to split up doubles into two good char regions, check for a break between 40% and 60% int leftEdge = allBoxes[boxidx].x + (int) (((float) allBoxes[boxidx].width) * 0.4f); int rightEdge = allBoxes[boxidx].x + (int) (((float) allBoxes[boxidx].width) * 0.6f); int minX = histogram.getLocalMinimum(leftEdge, rightEdge); int maxXChar1 = histogram.getLocalMaximum(allBoxes[boxidx].x, minX); int maxXChar2 = histogram.getLocalMaximum(minX, allBoxes[boxidx].x + allBoxes[boxidx].width); int minHeight = histogram.getHeightAt(minX); int maxHeightChar1 = histogram.getHeightAt(maxXChar1); int maxHeightChar2 = histogram.getHeightAt(maxXChar2); if ( minHeight < (0.25 * ((float) maxHeightChar1))) { // Add a box for Char1 Point botRight = Point(minX - 1, allBoxes[boxidx].y + allBoxes[boxidx].height); validBoxes.push_back(Rect(allBoxes[boxidx].tl(), botRight) ); } if ( minHeight < (0.25 * ((float) maxHeightChar2))) { // Add a box for Char2 Point topLeft = Point(minX + 1, allBoxes[boxidx].y); validBoxes.push_back(Rect(topLeft, allBoxes[boxidx].br()) ); } } } if (rowScore > bestRowScore) { bestRowScore = rowScore; bestRowIndex = row; bestBoxes = validBoxes; } } if (this->config->debugCharSegmenter) { cvtColor(histoImg, histoImg, CV_GRAY2BGR); line(histoImg, Point(0, histoImg.rows - 1 - bestRowIndex), Point(histoImg.cols, histoImg.rows - 1 - bestRowIndex), Scalar(0, 255, 0)); Mat imgBestBoxes(img.size(), img.type()); img.copyTo(imgBestBoxes); cvtColor(imgBestBoxes, imgBestBoxes, CV_GRAY2BGR); for (unsigned int i = 0; i < bestBoxes.size(); i++) rectangle(imgBestBoxes, bestBoxes[i], Scalar(0, 255, 0)); this->imgDbgGeneral.push_back(addLabel(histoImg, "All Histograms")); this->imgDbgGeneral.push_back(addLabel(imgBestBoxes, "Best Boxes")); } return bestBoxes; } vector CharacterSegmenter::get1DHits(Mat img, int yOffset) { vector hits; bool onSegment = false; int curSegmentLength = 0; for (int col = 0; col < img.cols; col++) { bool isOn = img.at(img.rows - 1 - yOffset, col); if (isOn) { // We're on a segment. Increment the length onSegment = true; curSegmentLength++; } if (onSegment && (isOn == false || (col == img.cols - 1))) { // A segment just ended or we're at the very end of the row and we're on a segment Point topLeft = Point(col - curSegmentLength, top.getPointAt(col - curSegmentLength) - 1); Point botRight = Point(col, bottom.getPointAt(col) + 1); hits.push_back(Rect(topLeft, botRight)); onSegment = false; curSegmentLength = 0; } } return hits; } void CharacterSegmenter::removeSmallContours(vector thresholds, float avgCharHeight, TextLine textLine) { //const float MIN_CHAR_AREA = 0.02 * avgCharWidth * avgCharHeight; // To clear out the tiny specks const float MIN_CONTOUR_HEIGHT = 0.3 * avgCharHeight; Mat textLineMask = Mat::zeros(thresholds[0].size(), CV_8U); fillConvexPoly(textLineMask, textLine.linePolygon.data(), textLine.linePolygon.size(), Scalar(255,255,255)); for (unsigned int i = 0; i < thresholds.size(); i++) { vector > contours; vector hierarchy; Mat thresholdsCopy = Mat::zeros(thresholds[i].size(), thresholds[i].type()); thresholds[i].copyTo(thresholdsCopy, textLineMask); findContours(thresholdsCopy, contours, hierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE); for (unsigned int c = 0; c < contours.size(); c++) { if (contours[c].size() == 0) continue; Rect mr = boundingRect(contours[c]); if (mr.height < MIN_CONTOUR_HEIGHT) { // Erase it drawContours(thresholds[i], contours, c, Scalar(0, 0, 0), -1); continue; } } } } vector CharacterSegmenter::combineCloseBoxes( vector charBoxes, float biggestCharWidth) { vector newCharBoxes; for (unsigned int i = 0; i < charBoxes.size(); i++) { if (i == charBoxes.size() - 1) { newCharBoxes.push_back(charBoxes[i]); break; } float bigWidth = (charBoxes[i + 1].x + charBoxes[i + 1].width - charBoxes[i].x); float w1Diff = abs(charBoxes[i].width - biggestCharWidth); float w2Diff = abs(charBoxes[i + 1].width - biggestCharWidth); float bigDiff = abs(bigWidth - biggestCharWidth); bigDiff *= 1.3; // Make it a little harder to merge boxes. if (bigDiff < w1Diff && bigDiff < w2Diff) { Rect bigRect(charBoxes[i].x, charBoxes[i].y, bigWidth, charBoxes[i].height); newCharBoxes.push_back(bigRect); if (this->config->debugCharSegmenter) { for (unsigned int z = 0; z < pipeline_data->thresholds.size(); z++) { Point center(bigRect.x + bigRect.width / 2, bigRect.y + bigRect.height / 2); RotatedRect rrect(center, Size2f(bigRect.width, bigRect.height + (bigRect.height / 2)), 0); ellipse(imgDbgCleanStages[z], rrect, Scalar(0,255,0), 1); } cout << "Merging 2 boxes -- " << i << " and " << i + 1 << endl; } i++; } else { newCharBoxes.push_back(charBoxes[i]); } } return newCharBoxes; } void CharacterSegmenter::cleanCharRegions(vector thresholds, vector charRegions) { const float MIN_SPECKLE_HEIGHT_PERCENT = 0.13; const float MIN_SPECKLE_WIDTH_PX = 3; const float MIN_CONTOUR_AREA_PERCENT = 0.1; const float MIN_CONTOUR_HEIGHT_PERCENT = config->segmentationMinCharHeightPercent; Mat mask = getCharBoxMask(thresholds[0], charRegions); for (unsigned int i = 0; i < thresholds.size(); i++) { bitwise_and(thresholds[i], mask, thresholds[i]); vector > contours; Mat tempImg(thresholds[i].size(), thresholds[i].type()); thresholds[i].copyTo(tempImg); //Mat element = getStructuringElement( 1, // Size( 2 + 1, 2+1 ), // Point( 1, 1 ) ); //dilate(thresholds[i], tempImg, element); //morphologyEx(thresholds[i], tempImg, MORPH_CLOSE, element); //drawAndWait(&tempImg); findContours(tempImg, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); for (unsigned int j = 0; j < charRegions.size(); j++) { const float MIN_SPECKLE_HEIGHT = ((float)charRegions[j].height) * MIN_SPECKLE_HEIGHT_PERCENT; const float MIN_CONTOUR_AREA = ((float)charRegions[j].area()) * MIN_CONTOUR_AREA_PERCENT; int tallestContourHeight = 0; float totalArea = 0; for (unsigned int c = 0; c < contours.size(); c++) { if (contours[c].size() == 0) continue; if (charRegions[j].contains(contours[c][0]) == false) continue; Rect r = boundingRect(contours[c]); if (r.height <= MIN_SPECKLE_HEIGHT || r.width <= MIN_SPECKLE_WIDTH_PX) { // Erase this speckle drawContours(thresholds[i], contours, c, Scalar(0,0,0), CV_FILLED); if (this->config->debugCharSegmenter) { drawContours(imgDbgCleanStages[i], contours, c, COLOR_DEBUG_SPECKLES, CV_FILLED); } } else { if (r.height > tallestContourHeight) tallestContourHeight = r.height; totalArea += contourArea(contours[c]); } //else if (r.height > tallestContourHeight) //{ // tallestContourIndex = c; // tallestContourHeight = h; //} } if (totalArea < MIN_CONTOUR_AREA) { // Character is not voluminous enough. Erase it. if (this->config->debugCharSegmenter) { cout << "Character CLEAN: (area) removing box " << j << " in threshold " << i << " -- Area " << totalArea << " < " << MIN_CONTOUR_AREA << endl; Rect boxTop(charRegions[j].x, charRegions[j].y - 10, charRegions[j].width, 10); rectangle(imgDbgCleanStages[i], boxTop, COLOR_DEBUG_MIN_AREA, -1); } rectangle(thresholds[i], charRegions[j], Scalar(0, 0, 0), -1); } else if (tallestContourHeight < ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT)) { // This character is too short. Black the whole thing out if (this->config->debugCharSegmenter) { cout << "Character CLEAN: (height) removing box " << j << " in threshold " << i << " -- Height " << tallestContourHeight << " < " << ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT) << endl; Rect boxBottom(charRegions[j].x, charRegions[j].y + charRegions[j].height, charRegions[j].width, 10); rectangle(imgDbgCleanStages[i], boxBottom, COLOR_DEBUG_MIN_HEIGHT, -1); } rectangle(thresholds[i], charRegions[j], Scalar(0, 0, 0), -1); } } Mat closureElement = getStructuringElement( 1, Size( 2 + 1, 2+1 ), Point( 1, 1 ) ); //morphologyEx(thresholds[i], thresholds[i], MORPH_OPEN, element); //dilate(thresholds[i], thresholds[i], element); //erode(thresholds[i], thresholds[i], element); morphologyEx(thresholds[i], thresholds[i], MORPH_CLOSE, closureElement); // Lastly, draw a clipping line between each character boxes for (unsigned int j = 0; j < charRegions.size(); j++) { line(thresholds[i], Point(charRegions[j].x - 1, charRegions[j].y), Point(charRegions[j].x - 1, charRegions[j].y + charRegions[j].height), Scalar(0, 0, 0)); line(thresholds[i], Point(charRegions[j].x + charRegions[j].width + 1, charRegions[j].y), Point(charRegions[j].x + charRegions[j].width + 1, charRegions[j].y + charRegions[j].height), Scalar(0, 0, 0)); } } } void CharacterSegmenter::cleanBasedOnColor(vector thresholds, Mat colorMask, vector charRegions) { // If I knock out x% of the contour area from this thing (after applying the color filter) // Consider it a bad news bear. REmove the whole area. const float MIN_PERCENT_CHUNK_REMOVED = 0.6; for (unsigned int i = 0; i < thresholds.size(); i++) { for (unsigned int j = 0; j < charRegions.size(); j++) { Mat boxChar = Mat::zeros(thresholds[i].size(), CV_8U); rectangle(boxChar, charRegions[j], Scalar(255,255,255), CV_FILLED); bitwise_and(thresholds[i], boxChar, boxChar); float meanBefore = mean(boxChar, boxChar)[0]; Mat thresholdCopy(thresholds[i].size(), CV_8U); bitwise_and(colorMask, boxChar, thresholdCopy); float meanAfter = mean(thresholdCopy, boxChar)[0]; if (meanAfter < meanBefore * (1-MIN_PERCENT_CHUNK_REMOVED)) { rectangle(thresholds[i], charRegions[j], Scalar(0,0,0), CV_FILLED); if (this->config->debugCharSegmenter) { //vector tmpDebug; //Mat thresholdCopy2 = Mat::zeros(thresholds[i].size(), CV_8U); //rectangle(thresholdCopy2, charRegions[j], Scalar(255,255,255), CV_FILLED); //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask")); //bitwise_and(thresholds[i], thresholdCopy2, thresholdCopy2); //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask + Thresh")); //bitwise_and(colorMask, thresholdCopy2, thresholdCopy2); //tmpDebug.push_back(addLabel(thresholdCopy2, "box Mask + Thresh + Color")); // // Mat tmpytmp = addLabel(thresholdCopy2, "box Mask + Thresh + Color"); // Mat tmpx = drawImageDashboard(tmpDebug, tmpytmp.type(), 1); //drawAndWait( &tmpx ); cout << "Segmentation Filter Clean by Color: Removed Threshold " << i << " charregion " << j << endl; cout << "Segmentation Filter Clean by Color: before=" << meanBefore << " after=" << meanAfter << endl; Point topLeft(charRegions[j].x, charRegions[j].y); circle(imgDbgCleanStages[i], topLeft, 5, COLOR_DEBUG_COLORFILTER, CV_FILLED); } } } } } vector CharacterSegmenter::filterMostlyEmptyBoxes(vector thresholds, const vector charRegions) { // Of the n thresholded images, if box 3 (for example) is empty in half (for example) of the thresholded images, // clear all data for every box #3. //const float MIN_AREA_PERCENT = 0.1; const float MIN_CONTOUR_HEIGHT_PERCENT = config->segmentationMinCharHeightPercent; Mat mask = getCharBoxMask(thresholds[0], charRegions); vector boxScores(charRegions.size()); for (unsigned int i = 0; i < charRegions.size(); i++) boxScores[i] = 0; for (unsigned int i = 0; i < thresholds.size(); i++) { for (unsigned int j = 0; j < charRegions.size(); j++) { //float minArea = charRegions[j].area() * MIN_AREA_PERCENT; Mat tempImg = Mat::zeros(thresholds[i].size(), thresholds[i].type()); rectangle(tempImg, charRegions[j], Scalar(255,255,255), CV_FILLED); bitwise_and(thresholds[i], tempImg, tempImg); vector > contours; findContours(tempImg, contours, RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); vector allPointsInBox; for (unsigned int c = 0; c < contours.size(); c++) { if (contours[c].size() == 0) continue; for (unsigned int z = 0; z < contours[c].size(); z++) allPointsInBox.push_back(contours[c][z]); } float height = 0; if (allPointsInBox.size() > 0) { height = boundingRect(allPointsInBox).height; } if (height >= ((float) charRegions[j].height * MIN_CONTOUR_HEIGHT_PERCENT)) { boxScores[j] = boxScores[j] + 1; } else if (this->config->debugCharSegmenter) { drawX(imgDbgCleanStages[i], charRegions[j], COLOR_DEBUG_EMPTYFILTER, 3); } } } vector newCharRegions; int maxBoxScore = 0; for (unsigned int i = 0; i < charRegions.size(); i++) { if (boxScores[i] > maxBoxScore) maxBoxScore = boxScores[i]; } // Need a good char sample in at least 50% of the boxes for it to be valid. int MIN_FULL_BOXES = maxBoxScore * 0.49; // Now check each score. If it's below the minimum, remove the charRegion for (unsigned int i = 0; i < charRegions.size(); i++) { if (boxScores[i] > MIN_FULL_BOXES) newCharRegions.push_back(charRegions[i]); else { // Erase the box from the Mat... mainly for debug purposes if (this->config->debugCharSegmenter) { cout << "Mostly Empty Filter: box index: " << i; cout << " this box had a score of : " << boxScores[i];; cout << " MIN_FULL_BOXES: " << MIN_FULL_BOXES << endl;; for (unsigned int z = 0; z < thresholds.size(); z++) { rectangle(thresholds[z], charRegions[i], Scalar(0,0,0), -1); drawX(imgDbgCleanStages[z], charRegions[i], COLOR_DEBUG_EMPTYFILTER, 1); } } } if (this->config->debugCharSegmenter) cout << " Box Score: " << boxScores[i] << endl; } return newCharRegions; } void CharacterSegmenter::filterEdgeBoxes(vector thresholds, const vector charRegions, float avgCharWidth, float avgCharHeight) { const float MIN_ANGLE_FOR_ROTATION = 0.4; int MIN_CONNECTED_EDGE_PIXELS = (avgCharHeight * 1.5); // Sometimes the rectangle won't be very tall, making it impossible to detect an edge // Adjust for this here. int alternate = thresholds[0].rows * 0.92; if (alternate < MIN_CONNECTED_EDGE_PIXELS && alternate > avgCharHeight) MIN_CONNECTED_EDGE_PIXELS = alternate; // // Pay special attention to the edge boxes. If it's a skinny box, and the vertical height extends above our bounds... remove it. //while (charBoxes.size() > 0 && charBoxes[charBoxes.size() - 1].width < MIN_SEGMENT_WIDTH_EDGES) // charBoxes.erase(charBoxes.begin() + charBoxes.size() - 1); // Now filter the "edge" boxes. We don't want to include skinny boxes on the edges, since these could be plate boundaries //while (charBoxes.size() > 0 && charBoxes[0].width < MIN_SEGMENT_WIDTH_EDGES) // charBoxes.erase(charBoxes.begin() + 0); // TECHNIQUE #1 // Check for long vertical lines. Once the line is too long, mask the whole region if (charRegions.size() <= 1) return; // Check both sides to see where the edges are // The first starts at the right edge of the leftmost char region and works its way left // The second starts at the left edge of the rightmost char region and works its way right. // We start by rotating the threshold image to the correct angle // then check each column 1 by 1. vector leftEdges; vector rightEdges; for (unsigned int i = 0; i < thresholds.size(); i++) { Mat rotated; if (abs(top.angle) > MIN_ANGLE_FOR_ROTATION) { // Rotate image: rotated = Mat(thresholds[i].size(), thresholds[i].type()); Mat rot_mat( 2, 3, CV_32FC1 ); Point center = Point( thresholds[i].cols/2, thresholds[i].rows/2 ); rot_mat = getRotationMatrix2D( center, top.angle, 1.0 ); warpAffine( thresholds[i], rotated, rot_mat, thresholds[i].size() ); } else { rotated = thresholds[i]; } int leftEdgeX = 0; int rightEdgeX = rotated.cols; // Do the left side int col = charRegions[0].x + charRegions[0].width; while (col >= 0) { int rowLength = getLongestBlobLengthBetweenLines(rotated, col); if (rowLength > MIN_CONNECTED_EDGE_PIXELS) { leftEdgeX = col; break; } col--; } col = charRegions[charRegions.size() - 1].x; while (col < rotated.cols) { int rowLength = getLongestBlobLengthBetweenLines(rotated, col); if (rowLength > MIN_CONNECTED_EDGE_PIXELS) { rightEdgeX = col; break; } col++; } if (leftEdgeX != 0) leftEdges.push_back(leftEdgeX); if (rightEdgeX != thresholds[i].cols) rightEdges.push_back(rightEdgeX); } int leftEdge = 0; int rightEdge = thresholds[0].cols; // Assign the edge values to the SECOND closest value if (leftEdges.size() > 1) { sort (leftEdges.begin(), leftEdges.begin()+leftEdges.size()); leftEdge = leftEdges[leftEdges.size() - 2] + 1; } if (rightEdges.size() > 1) { sort (rightEdges.begin(), rightEdges.begin()+rightEdges.size()); rightEdge = rightEdges[1] - 1; } if (leftEdge != 0 || rightEdge != thresholds[0].cols) { Mat mask = Mat::zeros(thresholds[0].size(), CV_8U); rectangle(mask, Point(leftEdge, 0), Point(rightEdge, thresholds[0].rows), Scalar(255,255,255), -1); if (abs(top.angle) > MIN_ANGLE_FOR_ROTATION) { // Rotate mask: Mat rot_mat( 2, 3, CV_32FC1 ); Point center = Point( mask.cols/2, mask.rows/2 ); rot_mat = getRotationMatrix2D( center, top.angle * -1, 1.0 ); warpAffine( mask, mask, rot_mat, mask.size() ); } // If our edge mask covers more than x% of the char region, mask the whole thing... const float MAX_COVERAGE_PERCENT = 0.6; int leftCoveragePx = leftEdge - charRegions[0].x; float leftCoveragePercent = ((float) leftCoveragePx) / ((float) charRegions[0].width); float rightCoveragePx = (charRegions[charRegions.size() -1].x + charRegions[charRegions.size() -1].width) - rightEdge; float rightCoveragePercent = ((float) rightCoveragePx) / ((float) charRegions[charRegions.size() -1].width); if ((leftCoveragePercent > MAX_COVERAGE_PERCENT) || (charRegions[0].width - leftCoveragePx < config->segmentationMinBoxWidthPx)) { rectangle(mask, charRegions[0], Scalar(0,0,0), -1); // Mask the whole region if (this->config->debugCharSegmenter) cout << "Edge Filter: Entire left region is erased" << endl; } if ((rightCoveragePercent > MAX_COVERAGE_PERCENT) || (charRegions[charRegions.size() -1].width - rightCoveragePx < config->segmentationMinBoxWidthPx)) { rectangle(mask, charRegions[charRegions.size() -1], Scalar(0,0,0), -1); if (this->config->debugCharSegmenter) cout << "Edge Filter: Entire right region is erased" << endl; } for (unsigned int i = 0; i < thresholds.size(); i++) { bitwise_and(thresholds[i], mask, thresholds[i]); } if (this->config->debugCharSegmenter) { cout << "Edge Filter: left=" << leftEdge << " right=" << rightEdge << endl; Mat bordered = addLabel(mask, "Edge Filter #1"); imgDbgGeneral.push_back(bordered); Mat invertedMask(mask.size(), mask.type()); bitwise_not(mask, invertedMask); for (unsigned int z = 0; z < imgDbgCleanStages.size(); z++) fillMask(imgDbgCleanStages[z], invertedMask, Scalar(0,0,255)); } } } int CharacterSegmenter::getLongestBlobLengthBetweenLines(Mat img, int col) { int longestBlobLength = 0; bool onSegment = false; bool wasbetweenLines = false; float curSegmentLength = 0; for (int row = 0; row < img.rows; row++) { bool isbetweenLines = false; bool isOn = img.at(row, col); // check two rows at a time. if (!isOn && col < img.cols) isOn = img.at(row, col); if (isOn) { // We're on a segment. Increment the length isbetweenLines = top.isPointBelowLine(Point(col, row)) && !bottom.isPointBelowLine(Point(col, row)); float incrementBy = 1; // Add a little extra to the score if this is outside of the lines if (!isbetweenLines) incrementBy = 1.1; onSegment = true; curSegmentLength += incrementBy; } if (isOn && isbetweenLines) { wasbetweenLines = true; } if (onSegment && (isOn == false || (row == img.rows - 1))) { if (wasbetweenLines && curSegmentLength > longestBlobLength) longestBlobLength = curSegmentLength; onSegment = false; isbetweenLines = false; curSegmentLength = 0; } } return longestBlobLength; } // Checks to see if a skinny, tall line (extending above or below the char Height) is inside the given box. // Returns the contour index if true. -1 otherwise int CharacterSegmenter::isSkinnyLineInsideBox(Mat threshold, Rect box, vector > contours, vector hierarchy, float avgCharWidth, float avgCharHeight) { float MIN_EDGE_CONTOUR_HEIGHT = avgCharHeight * 1.25; // Sometimes the threshold is smaller than the MIN_EDGE_CONTOUR_HEIGHT. // In that case, adjust to be smaller int alternate = threshold.rows * 0.92; if (alternate < MIN_EDGE_CONTOUR_HEIGHT && alternate > avgCharHeight) MIN_EDGE_CONTOUR_HEIGHT = alternate; Rect slightlySmallerBox(box.x, box.y, box.width, box.height); Mat boxMask = Mat::zeros(threshold.size(), CV_8U); rectangle(boxMask, slightlySmallerBox, Scalar(255, 255, 255), -1); for (unsigned int i = 0; i < contours.size(); i++) { // Only bother with the big boxes if (boundingRect(contours[i]).height < MIN_EDGE_CONTOUR_HEIGHT) continue; Mat tempImg = Mat::zeros(threshold.size(), CV_8U); drawContours(tempImg, contours, i, Scalar(255,255,255), -1, 8, hierarchy, 1); bitwise_and(tempImg, boxMask, tempImg); vector > subContours; findContours(tempImg, subContours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE); int tallestContourIdx = -1; int tallestContourHeight = 0; int tallestContourWidth = 0; float tallestContourArea = 0; for (unsigned int s = 0; s < subContours.size(); s++) { Rect r = boundingRect(subContours[s]); if (r.height > tallestContourHeight) { tallestContourIdx = s; tallestContourHeight = r.height; tallestContourWidth = r.width; tallestContourArea = contourArea(subContours[s]); } } if (tallestContourIdx != -1) { //cout << "Edge Filter: " << tallestContourHeight << " -- " << avgCharHeight << endl; if (tallestContourHeight >= avgCharHeight * 0.9 && ((tallestContourWidth < config->segmentationMinBoxWidthPx) || (tallestContourArea < avgCharWidth * avgCharHeight * 0.1))) { cout << "Edge Filter: Avg contour width: " << avgCharWidth << " This guy is: " << tallestContourWidth << endl; cout << "Edge Filter: tallestContourArea: " << tallestContourArea << " Minimum: " << avgCharWidth * avgCharHeight * 0.1 << endl; return i; } } } return -1; } Mat CharacterSegmenter::getCharBoxMask(Mat img_threshold, vector charBoxes) { Mat mask = Mat::zeros(img_threshold.size(), CV_8U); for (unsigned int i = 0; i < charBoxes.size(); i++) rectangle(mask, charBoxes[i], Scalar(255, 255, 255), -1); return mask; } }