#ifdef HAVE_CONFIG_H
# include "config_auto.h"
#endif
#include "alignedblob.h"
#include "blobbox.h"
#include "crakedge.h"
#include "edgblob.h"
#include "linefind.h"
#include "tabvector.h"
#include <algorithm>
namespace tesseract {
const int kThinLineFraction = 20;
const int kMinLineLengthFraction = 4;
const int kCrackSpacing = 100;
const int kLineFindGridSize = 50;
const int kMinThickLineWidth = 12;
const int kMaxLineResidue = 6;
const double kThickLengthMultiple = 0.75;
const double kMaxNonLineDensity = 0.25;
const double kMaxStaveHeight = 1.0;
const double kMinMusicPixelFraction = 0.75;
static void RemoveUnusedLineSegments(bool horizontal_lines, BLOBNBOX_LIST *line_bblobs,
Image line_pix) {
int height = pixGetHeight(line_pix);
BLOBNBOX_IT bbox_it(line_bblobs);
for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
BLOBNBOX *blob = bbox_it.data();
if (blob->left_tab_type() != TT_VLINE) {
const TBOX &box = blob->bounding_box();
Box *pixbox = nullptr;
if (horizontal_lines) {
pixbox = boxCreate(box.bottom(), height - box.right(), box.height(), box.width());
} else {
pixbox = boxCreate(box.left(), height - box.top(), box.width(), box.height());
}
pixClearInRect(line_pix, pixbox);
boxDestroy(&pixbox);
}
}
}
static void SubtractLinesAndResidue(Image line_pix, Image non_line_pix,
Image src_pix) {
pixSubtract(src_pix, src_pix, line_pix);
Image residue_pix = pixSubtract(nullptr, src_pix, non_line_pix);
Image fat_line_pix = pixDilateBrick(nullptr, line_pix, 3, 3);
pixSeedfillBinary(fat_line_pix, fat_line_pix, residue_pix, 8);
pixSubtract(src_pix, src_pix, fat_line_pix);
fat_line_pix.destroy();
residue_pix.destroy();
}
static int MaxStrokeWidth(Image pix) {
Image dist_pix = pixDistanceFunction(pix, 4, 8, L_BOUNDARY_BG);
int width = pixGetWidth(dist_pix);
int height = pixGetHeight(dist_pix);
int wpl = pixGetWpl(dist_pix);
l_uint32 *data = pixGetData(dist_pix);
int max_dist = 0;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int pixel = GET_DATA_BYTE(data, x);
if (pixel > max_dist) {
max_dist = pixel;
}
}
data += wpl;
}
dist_pix.destroy();
return max_dist * 2;
}
static int NumTouchingIntersections(Box *line_box, Image intersection_pix) {
if (intersection_pix == nullptr) {
return 0;
}
Image rect_pix = pixClipRectangle(intersection_pix, line_box, nullptr);
Boxa *boxa = pixConnComp(rect_pix, nullptr, 8);
rect_pix.destroy();
if (boxa == nullptr) {
return false;
}
int result = boxaGetCount(boxa);
boxaDestroy(&boxa);
return result;
}
static int CountPixelsAdjacentToLine(int line_width, Box *line_box, Image nonline_pix) {
l_int32 x, y, box_width, box_height;
boxGetGeometry(line_box, &x, &y, &box_width, &box_height);
if (box_width > box_height) {
int bottom = std::min(pixGetHeight(nonline_pix), y + box_height + line_width);
y = std::max(0, y - line_width);
box_height = bottom - y;
} else {
int right = std::min(pixGetWidth(nonline_pix), x + box_width + line_width);
x = std::max(0, x - line_width);
box_width = right - x;
}
Box *box = boxCreate(x, y, box_width, box_height);
Image rect_pix = pixClipRectangle(nonline_pix, box, nullptr);
boxDestroy(&box);
l_int32 result;
pixCountPixels(rect_pix, &result, nullptr);
rect_pix.destroy();
return result;
}
static int FilterFalsePositives(int resolution, Image nonline_pix, Image intersection_pix,
Image line_pix) {
int min_thick_length = static_cast<int>(resolution * kThickLengthMultiple);
Pixa *pixa = nullptr;
Boxa *boxa = pixConnComp(line_pix, &pixa, 8);
int nboxes = boxaGetCount(boxa);
int remaining_boxes = nboxes;
for (int i = 0; i < nboxes; ++i) {
Box *box = boxaGetBox(boxa, i, L_CLONE);
l_int32 x, y, box_width, box_height;
boxGetGeometry(box, &x, &y, &box_width, &box_height);
Image comp_pix = pixaGetPix(pixa, i, L_CLONE);
int max_width = MaxStrokeWidth(comp_pix);
comp_pix.destroy();
bool bad_line = false;
if (box_width >= kMinThickLineWidth && box_height >= kMinThickLineWidth &&
box_width < min_thick_length && box_height < min_thick_length &&
max_width > kMinThickLineWidth) {
bad_line = true;
}
if (!bad_line && (NumTouchingIntersections(box, intersection_pix) < 2)) {
int nonline_count = CountPixelsAdjacentToLine(max_width, box, nonline_pix);
if (nonline_count > box_height * box_width * kMaxNonLineDensity) {
bad_line = true;
}
}
if (bad_line) {
pixClearInRect(line_pix, box);
--remaining_boxes;
}
boxDestroy(&box);
}
pixaDestroy(&pixa);
boxaDestroy(&boxa);
return remaining_boxes;
}
static void ConvertBoxaToBlobs(int image_width, int image_height, Boxa **boxes,
C_BLOB_LIST *blobs) {
C_OUTLINE_LIST outlines;
C_OUTLINE_IT ol_it = &outlines;
int nboxes = boxaGetCount(*boxes);
for (int i = 0; i < nboxes; ++i) {
l_int32 x, y, width, height;
boxaGetBoxGeometry(*boxes, i, &x, &y, &width, &height);
ICOORD top_left(x, y);
ICOORD bot_right(x + width, y + height);
CRACKEDGE startpt;
startpt.pos = top_left;
auto *outline = new C_OUTLINE(&startpt, top_left, bot_right, 0);
ol_it.add_after_then_move(outline);
}
BLOCK block;
ICOORD page_tl(0, 0);
ICOORD page_br(image_width, image_height);
outlines_to_blobs(&block, page_tl, page_br, &outlines);
C_BLOB_IT blob_it(blobs);
blob_it.add_list_after(block.blob_list());
boxaDestroy(boxes);
}
static void GetLineBoxes(bool horizontal_lines, Image pix_lines, Image pix_intersections,
C_BLOB_LIST *line_cblobs, BLOBNBOX_LIST *line_bblobs) {
int wpl = pixGetWpl(pix_lines);
int width = pixGetWidth(pix_lines);
int height = pixGetHeight(pix_lines);
l_uint32 *data = pixGetData(pix_lines);
if (horizontal_lines) {
for (int y = 0; y < height; ++y, data += wpl) {
for (int x = kCrackSpacing; x < width; x += kCrackSpacing) {
CLEAR_DATA_BIT(data, x);
}
}
} else {
for (int y = kCrackSpacing; y < height; y += kCrackSpacing) {
memset(data + wpl * y, 0, wpl * sizeof(*data));
}
}
Boxa *boxa = pixConnComp(pix_lines, nullptr, 8);
ConvertBoxaToBlobs(width, height, &boxa, line_cblobs);
C_BLOB_IT blob_it(line_cblobs);
BLOBNBOX_IT bbox_it(line_bblobs);
for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
C_BLOB *cblob = blob_it.data();
auto *bblob = new BLOBNBOX(cblob);
bbox_it.add_to_end(bblob);
const TBOX &bbox = bblob->bounding_box();
Box *box = boxCreate(bbox.left(), bbox.bottom(), bbox.width(), bbox.height());
bblob->set_line_crossings(NumTouchingIntersections(box, pix_intersections));
boxDestroy(&box);
if (horizontal_lines) {
TBOX new_box(height - bbox.top(), bbox.left(), height - bbox.bottom(), bbox.right());
bblob->set_bounding_box(new_box);
} else {
TBOX new_box(bbox.left(), height - bbox.top(), bbox.right(), height - bbox.bottom());
bblob->set_bounding_box(new_box);
}
}
}
static void FindLineVectors(const ICOORD &bleft, const ICOORD &tright,
BLOBNBOX_LIST *line_bblobs, int *vertical_x, int *vertical_y,
TabVector_LIST *vectors) {
BLOBNBOX_IT bbox_it(line_bblobs);
int b_count = 0;
AlignedBlob blob_grid(kLineFindGridSize, bleft, tright);
for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
BLOBNBOX *bblob = bbox_it.data();
bblob->set_left_tab_type(TT_MAYBE_ALIGNED);
bblob->set_left_rule(bleft.x());
bblob->set_right_rule(tright.x());
bblob->set_left_crossing_rule(bleft.x());
bblob->set_right_crossing_rule(tright.x());
blob_grid.InsertBBox(false, true, bblob);
++b_count;
}
if (b_count == 0) {
return;
}
BlobGridSearch lsearch(&blob_grid);
BLOBNBOX *bbox;
TabVector_IT vector_it(vectors);
*vertical_x = 0;
*vertical_y = 1;
lsearch.StartFullSearch();
while ((bbox = lsearch.NextFullSearch()) != nullptr) {
if (bbox->left_tab_type() == TT_MAYBE_ALIGNED) {
const TBOX &box = bbox->bounding_box();
if (AlignedBlob::WithinTestRegion(2, box.left(), box.bottom())) {
tprintf("Finding line vector starting at bbox (%d,%d)\n", box.left(), box.bottom());
}
AlignedBlobParams align_params(*vertical_x, *vertical_y, box.width());
TabVector *vector =
blob_grid.FindVerticalAlignment(align_params, bbox, vertical_x, vertical_y);
if (vector != nullptr) {
vector->Freeze();
vector_it.add_to_end(vector);
}
}
}
}
static Image FilterMusic(int resolution, Image pix_closed, Image pix_vline, Image pix_hline,
bool &v_empty, bool &h_empty) {
int max_stave_height = static_cast<int>(resolution * kMaxStaveHeight);
Image intersection_pix = pix_vline & pix_hline;
Boxa *boxa = pixConnComp(pix_vline, nullptr, 8);
int nboxes = boxaGetCount(boxa);
Image music_mask = nullptr;
for (int i = 0; i < nboxes; ++i) {
Box *box = boxaGetBox(boxa, i, L_CLONE);
l_int32 x, y, box_width, box_height;
boxGetGeometry(box, &x, &y, &box_width, &box_height);
int joins = NumTouchingIntersections(box, intersection_pix);
if (joins >= 5 && (joins - 1) * max_stave_height >= 4 * box_height) {
if (music_mask == nullptr) {
music_mask = pixCreate(pixGetWidth(pix_vline), pixGetHeight(pix_vline), 1);
}
pixSetInRect(music_mask, box);
}
boxDestroy(&box);
}
boxaDestroy(&boxa);
intersection_pix.destroy();
if (music_mask != nullptr) {
pixSeedfillBinary(music_mask, music_mask, pix_closed, 8);
Boxa *boxa = pixConnComp(music_mask, nullptr, 8);
int nboxes = boxaGetCount(boxa);
for (int i = 0; i < nboxes; ++i) {
Box *box = boxaGetBox(boxa, i, L_CLONE);
Image rect_pix = pixClipRectangle(music_mask, box, nullptr);
l_int32 music_pixels;
pixCountPixels(rect_pix, &music_pixels, nullptr);
rect_pix.destroy();
rect_pix = pixClipRectangle(pix_closed, box, nullptr);
l_int32 all_pixels;
pixCountPixels(rect_pix, &all_pixels, nullptr);
rect_pix.destroy();
if (music_pixels < kMinMusicPixelFraction * all_pixels) {
pixClearInRect(music_mask, box);
}
boxDestroy(&box);
}
boxaDestroy(&boxa);
if (music_mask.isZero()) {
music_mask.destroy();
} else {
pixSubtract(pix_vline, pix_vline, music_mask);
pixSubtract(pix_hline, pix_hline, music_mask);
v_empty = pix_vline.isZero();
h_empty = pix_hline.isZero();
}
}
return music_mask;
}
static void GetLineMasks(int resolution, Image src_pix, Image *pix_vline, Image *pix_non_vline,
Image *pix_hline, Image *pix_non_hline, Image *pix_intersections,
Image *pix_music_mask, Pixa *pixa_display) {
Image pix_closed = nullptr;
Image pix_hollow = nullptr;
int max_line_width = resolution / kThinLineFraction;
int min_line_length = resolution / kMinLineLengthFraction;
if (pixa_display != nullptr) {
tprintf("Image resolution = %d, max line width = %d, min length=%d\n", resolution,
max_line_width, min_line_length);
}
int closing_brick = max_line_width / 3;
pix_closed = pixCloseBrick(nullptr, src_pix, closing_brick, closing_brick);
if (pixa_display != nullptr) {
pixaAddPix(pixa_display, pix_closed, L_CLONE);
}
Image pix_solid = pixOpenBrick(nullptr, pix_closed, max_line_width, max_line_width);
if (pixa_display != nullptr) {
pixaAddPix(pixa_display, pix_solid, L_CLONE);
}
pix_hollow = pixSubtract(nullptr, pix_closed, pix_solid);
pix_solid.destroy();
if (pixa_display != nullptr) {
pixaAddPix(pixa_display, pix_hollow, L_CLONE);
}
*pix_vline = pixOpenBrick(nullptr, pix_hollow, 1, min_line_length);
*pix_hline = pixOpenBrick(nullptr, pix_hollow, min_line_length, 1);
pix_hollow.destroy();
bool v_empty = pix_vline->isZero();
bool h_empty = pix_hline->isZero();
if (pix_music_mask != nullptr) {
if (!v_empty && !h_empty) {
*pix_music_mask =
FilterMusic(resolution, pix_closed, *pix_vline, *pix_hline, v_empty, h_empty);
} else {
*pix_music_mask = nullptr;
}
}
pix_closed.destroy();
Image pix_nonlines = nullptr;
*pix_intersections = nullptr;
Image extra_non_hlines = nullptr;
if (!v_empty) {
pix_nonlines = pixSubtract(nullptr, src_pix, *pix_vline);
if (!h_empty) {
pixSubtract(pix_nonlines, pix_nonlines, *pix_hline);
*pix_intersections = *pix_vline & *pix_hline;
extra_non_hlines = pixSubtract(nullptr, *pix_vline, *pix_intersections);
}
*pix_non_vline = pixErodeBrick(nullptr, pix_nonlines, kMaxLineResidue, 1);
pixSeedfillBinary(*pix_non_vline, *pix_non_vline, pix_nonlines, 8);
if (!h_empty) {
*pix_non_vline |= *pix_hline;
pixSubtract(*pix_non_vline, *pix_non_vline, *pix_intersections);
}
if (!FilterFalsePositives(resolution, *pix_non_vline, *pix_intersections, *pix_vline)) {
pix_vline->destroy();
}
} else {
pix_vline->destroy();
*pix_non_vline = nullptr;
if (!h_empty) {
pix_nonlines = pixSubtract(nullptr, src_pix, *pix_hline);
}
}
if (h_empty) {
pix_hline->destroy();
*pix_non_hline = nullptr;
if (v_empty) {
return;
}
} else {
*pix_non_hline = pixErodeBrick(nullptr, pix_nonlines, 1, kMaxLineResidue);
pixSeedfillBinary(*pix_non_hline, *pix_non_hline, pix_nonlines, 8);
if (extra_non_hlines != nullptr) {
*pix_non_hline |= extra_non_hlines;
extra_non_hlines.destroy();
}
if (!FilterFalsePositives(resolution, *pix_non_hline, *pix_intersections, *pix_hline)) {
pix_hline->destroy();
}
}
if (pixa_display != nullptr) {
if (*pix_vline != nullptr) {
pixaAddPix(pixa_display, *pix_vline, L_CLONE);
}
if (*pix_hline != nullptr) {
pixaAddPix(pixa_display, *pix_hline, L_CLONE);
}
if (pix_nonlines != nullptr) {
pixaAddPix(pixa_display, pix_nonlines, L_CLONE);
}
if (*pix_non_vline != nullptr) {
pixaAddPix(pixa_display, *pix_non_vline, L_CLONE);
}
if (*pix_non_hline != nullptr) {
pixaAddPix(pixa_display, *pix_non_hline, L_CLONE);
}
if (*pix_intersections != nullptr) {
pixaAddPix(pixa_display, *pix_intersections, L_CLONE);
}
if (pix_music_mask != nullptr && *pix_music_mask != nullptr) {
pixaAddPix(pixa_display, *pix_music_mask, L_CLONE);
}
}
pix_nonlines.destroy();
}
static void FindAndRemoveVLines(Image pix_intersections, int *vertical_x,
int *vertical_y, Image *pix_vline, Image pix_non_vline,
Image src_pix, TabVector_LIST *vectors) {
if (pix_vline == nullptr || *pix_vline == nullptr) {
return;
}
C_BLOB_LIST line_cblobs;
BLOBNBOX_LIST line_bblobs;
GetLineBoxes(false, *pix_vline, pix_intersections, &line_cblobs, &line_bblobs);
int width = pixGetWidth(src_pix);
int height = pixGetHeight(src_pix);
ICOORD bleft(0, 0);
ICOORD tright(width, height);
FindLineVectors(bleft, tright, &line_bblobs, vertical_x, vertical_y, vectors);
if (!vectors->empty()) {
RemoveUnusedLineSegments(false, &line_bblobs, *pix_vline);
SubtractLinesAndResidue(*pix_vline, pix_non_vline, src_pix);
ICOORD vertical;
vertical.set_with_shrink(*vertical_x, *vertical_y);
TabVector::MergeSimilarTabVectors(vertical, vectors, nullptr);
} else {
pix_vline->destroy();
}
}
static void FindAndRemoveHLines(Image pix_intersections, int vertical_x,
int vertical_y, Image *pix_hline, Image pix_non_hline,
Image src_pix, TabVector_LIST *vectors) {
if (pix_hline == nullptr || *pix_hline == nullptr) {
return;
}
C_BLOB_LIST line_cblobs;
BLOBNBOX_LIST line_bblobs;
GetLineBoxes(true, *pix_hline, pix_intersections, &line_cblobs, &line_bblobs);
int width = pixGetWidth(src_pix);
int height = pixGetHeight(src_pix);
ICOORD bleft(0, 0);
ICOORD tright(height, width);
FindLineVectors(bleft, tright, &line_bblobs, &vertical_x, &vertical_y, vectors);
if (!vectors->empty()) {
RemoveUnusedLineSegments(true, &line_bblobs, *pix_hline);
SubtractLinesAndResidue(*pix_hline, pix_non_hline, src_pix);
ICOORD vertical;
vertical.set_with_shrink(vertical_x, vertical_y);
TabVector::MergeSimilarTabVectors(vertical, vectors, nullptr);
TabVector_IT h_it(vectors);
for (h_it.mark_cycle_pt(); !h_it.cycled_list(); h_it.forward()) {
h_it.data()->XYFlip();
}
} else {
pix_hline->destroy();
}
}
void LineFinder::FindAndRemoveLines(int resolution, bool debug, Image pix, int *vertical_x,
int *vertical_y, Image *pix_music_mask, TabVector_LIST *v_lines,
TabVector_LIST *h_lines) {
if (pix == nullptr || vertical_x == nullptr || vertical_y == nullptr) {
tprintf("Error in parameters for LineFinder::FindAndRemoveLines\n");
return;
}
Image pix_vline = nullptr;
Image pix_non_vline = nullptr;
Image pix_hline = nullptr;
Image pix_non_hline = nullptr;
Image pix_intersections = nullptr;
Pixa *pixa_display = debug ? pixaCreate(0) : nullptr;
GetLineMasks(resolution, pix, &pix_vline, &pix_non_vline, &pix_hline, &pix_non_hline,
&pix_intersections, pix_music_mask, pixa_display);
FindAndRemoveVLines(pix_intersections, vertical_x, vertical_y, &pix_vline,
pix_non_vline, pix, v_lines);
pix_intersections.destroy();
if (pix_hline != nullptr) {
if (pix_vline != nullptr) {
pix_intersections = pix_vline & pix_hline;
}
if (!FilterFalsePositives(resolution, pix_non_hline, pix_intersections, pix_hline)) {
pix_hline.destroy();
}
}
FindAndRemoveHLines(pix_intersections, *vertical_x, *vertical_y, &pix_hline,
pix_non_hline, pix, h_lines);
if (pixa_display != nullptr && pix_vline != nullptr) {
pixaAddPix(pixa_display, pix_vline, L_CLONE);
}
if (pixa_display != nullptr && pix_hline != nullptr) {
pixaAddPix(pixa_display, pix_hline, L_CLONE);
}
pix_intersections.destroy();
if (pix_vline != nullptr && pix_hline != nullptr) {
pix_intersections = pix_vline & pix_hline;
Image pix_join_residue = pixDilateBrick(nullptr, pix_intersections, 5, 5);
pixSeedfillBinary(pix_join_residue, pix_join_residue, pix, 8);
pixSubtract(pix, pix, pix_join_residue);
pix_join_residue.destroy();
}
if (pix_music_mask != nullptr && *pix_music_mask != nullptr) {
if (pixa_display != nullptr) {
pixaAddPix(pixa_display, *pix_music_mask, L_CLONE);
}
pixSubtract(pix, pix, *pix_music_mask);
}
if (pixa_display != nullptr) {
pixaAddPix(pixa_display, pix, L_CLONE);
}
pix_vline.destroy();
pix_non_vline.destroy();
pix_hline.destroy();
pix_non_hline.destroy();
pix_intersections.destroy();
if (pixa_display != nullptr) {
pixaConvertToPdf(pixa_display, resolution, 1.0f, 0, 0, "LineFinding", "vhlinefinding.pdf");
pixaDestroy(&pixa_display);
}
}
}