Skip to content

Commit 2b5c2c9

Browse files
authored
Improve line number recognition: sparse/accumulated numbering, and don't strip footnotes (#241)
markLineNumber() missed valid line-number columns in two layouts the size-scaled coverage threshold from #226 does not cover, and a naive fix would strip footnote/reference numbers. Detection (low-coverage escape hatch): accept a margin cluster as line numbers when its values form a regular arithmetic progression (dominant constant increment) at the extreme page margin, via either - "big numbers": the smallest value exceeds the page's line count (accumulated numbering that a figure or column break may split, e.g. 428..444 on a late page), or - even vertical pitch (fresh sparse numbering such as every 5th line). Footnote guard (document level): footnote and bibliographic reference numbers also accumulate to large values at a margin, but their block sits at the bottom of the page and never covers it, whereas a line-number column reaches the top of the page's text on some page and runs down every page. So a low-coverage column is accepted only if it reaches the top of the text here, or was already confirmed as a line-number column on an earlier page (2-column pages and figures push the numbers down, so a confirmed column may start mid-page). Confirmed column x-positions are carried across pages via XmlAltoOutputDev. Validated on the GROBID end-to-end corpora and the grobid-footnote-flavour corpus: - bioRxiv line numbers stripped on ~24-28% of docs (test-2000 and train-6000), removing only digit tokens; 94% of the pre-guard recall retained. - grobid-footnote-flavour: 0/100 docs changed (no footnote/page numbers stripped). - eLife/PLOS/PMC (no line numbers): unchanged. Figure panel labels preserved.
1 parent 345ae8e commit 2b5c2c9

2 files changed

Lines changed: 155 additions & 8 deletions

File tree

‎src/XmlAltoOutputDev.cc‎

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5303,7 +5303,114 @@ int find_index_relaxed(vector<double> positions, double val, double margin) {
53035303
return index;
53045304
}
53055305

5306-
bool TextPage::markLineNumber() {
5306+
// Non-negative integer value of an all-digit word, or -1 if it holds a non-digit.
5307+
static long word_number_value(TextRawWord *word) {
5308+
long val = 0;
5309+
for (int i = 0; i < word->len; i++) {
5310+
Unicode u = ((TextChar *) word->chars->get(i))->c;
5311+
if (u < '0' || u > '9')
5312+
return -1;
5313+
val = val * 10 + (long) (u - '0');
5314+
}
5315+
return val;
5316+
}
5317+
5318+
// A margin cluster of numbers is line numbering even at low page coverage when its values
5319+
// form a regular arithmetic progression (dominant constant increment). Two accept paths:
5320+
// - "big numbers": the smallest value already exceeds this page's line count, so the numbers
5321+
// cannot be a per-page list (references / figure labels start near 1) - they are line
5322+
// numbers accumulated from earlier pages (e.g. 428..444 on a late page). Accept regardless
5323+
// of vertical spacing, which a figure or column break may disrupt.
5324+
// - "even pitch": otherwise (fresh, small-valued numbering such as every-5th-line on page 1)
5325+
// require roughly even vertical spacing.
5326+
// Footnote / bibliographic reference numbers match neither: they restart near 1 and are not
5327+
// evenly spaced down the page.
5328+
static bool is_regular_number_sequence(const vector<int> &cluster,
5329+
const vector<TextRawWord *> &lineNumberWords,
5330+
int totalNumberOfLines) {
5331+
int n = (int) cluster.size();
5332+
if (n < 3)
5333+
return false;
5334+
5335+
// collect (yMin, value), ordered top-to-bottom
5336+
vector<pair<double, long> > seq;
5337+
for (int i = 0; i < n; i++) {
5338+
TextRawWord *w = lineNumberWords[cluster[i]];
5339+
long v = word_number_value(w);
5340+
if (v < 0)
5341+
return false;
5342+
seq.push_back(make_pair(w->yMin, v));
5343+
}
5344+
sort(seq.begin(), seq.end());
5345+
5346+
// values must strictly increase going down the page
5347+
vector<long> dv;
5348+
vector<double> dy;
5349+
for (int i = 1; i < (int) seq.size(); i++) {
5350+
long d = seq[i].second - seq[i - 1].second;
5351+
double gy = seq[i].first - seq[i - 1].first;
5352+
if (d <= 0 || gy <= 0)
5353+
return false;
5354+
dv.push_back(d);
5355+
dy.push_back(gy);
5356+
}
5357+
5358+
// dominant (median) increment; line numbering steps are small (1, 2, 5, 10, 25...)
5359+
vector<long> sortedDv = dv;
5360+
sort(sortedDv.begin(), sortedDv.end());
5361+
long g = sortedDv[sortedDv.size() / 2];
5362+
if (g < 1 || g > 50)
5363+
return false;
5364+
5365+
// a strong majority of steps must share that exact increment; a line number missed
5366+
// by extraction shows up as a multiple of g and is simply not counted here
5367+
int agree = 0;
5368+
for (int i = 0; i < (int) dv.size(); i++)
5369+
if (dv[i] == g)
5370+
agree++;
5371+
if (agree < (int) ((dv.size() * 3 + 4) / 5)) // >= 60%
5372+
return false;
5373+
5374+
// "big numbers" path: smallest value exceeds this page's line count -> accumulated line
5375+
// numbers, accept without requiring even pitch (figure/column breaks disrupt spacing).
5376+
long minVal = seq[0].second;
5377+
if (minVal > (long) totalNumberOfLines)
5378+
return true;
5379+
5380+
// otherwise require roughly even vertical pitch per unit increment
5381+
vector<double> pitch;
5382+
for (int i = 0; i < (int) dv.size(); i++)
5383+
if (dv[i] == g)
5384+
pitch.push_back(dy[i] / (double) dv[i]);
5385+
if ((int) pitch.size() < 2)
5386+
return false;
5387+
double mean = 0.0;
5388+
for (int i = 0; i < (int) pitch.size(); i++)
5389+
mean += pitch[i];
5390+
mean /= pitch.size();
5391+
if (mean <= 0.0)
5392+
return false;
5393+
double var = 0.0;
5394+
for (int i = 0; i < (int) pitch.size(); i++)
5395+
var += (pitch[i] - mean) * (pitch[i] - mean);
5396+
double cv = sqrt(var / pitch.size()) / mean;
5397+
return cv < 0.30;
5398+
}
5399+
5400+
// Minimum y (top) of the margin number column near x=clusterX, across the cluster's page.
5401+
// Line numbers jitter over a few x-positions, so we scan all candidates near that x.
5402+
static double column_top(const vector<TextRawWord *> &lineNumberWords, double clusterX) {
5403+
double top = 1e18;
5404+
for (int k = 0; k < (int) lineNumberWords.size(); k++) {
5405+
TextRawWord *w = lineNumberWords[k];
5406+
if (fabs(w->xMin - clusterX) < 12.0 || fabs(w->xMax - clusterX) < 12.0)
5407+
if (w->yMin < top)
5408+
top = w->yMin;
5409+
}
5410+
return top;
5411+
}
5412+
5413+
bool TextPage::markLineNumber(vector<double> &lineNumberColumnsX) {
53075414
// Detect the presence of line number column in the page and mark the corresponding TextWord objects for further appropriate handling
53085415

53095416
// Line number conditions:
@@ -5333,6 +5440,11 @@ bool TextPage::markLineNumber() {
53335440
int rightMostBoundary = 0;
53345441
int leftMostBoundary = 999990;
53355442

5443+
// vertical extent of the page's text content (used to tell a full-height line-number column
5444+
// from a footnote/reference number block, which is confined to the bottom of the page)
5445+
double contentTop = 1e9;
5446+
double contentBottom = -1e9;
5447+
53365448
// the total number of lines on the page
53375449
int totalNumberOfLines = 0;
53385450

@@ -5375,6 +5487,10 @@ bool TextPage::markLineNumber() {
53755487
leftMostBoundary = line1->xMin;
53765488
if (line1->xMax > rightMostBoundary)
53775489
rightMostBoundary = line1->xMax;
5490+
if (line1->yMin < contentTop)
5491+
contentTop = line1->yMin;
5492+
if (line1->yMax > contentBottom)
5493+
contentBottom = line1->yMax;
53785494
}
53795495

53805496
if (par->xMin < leftMostBoundary)
@@ -5626,8 +5742,38 @@ bool TextPage::markLineNumber() {
56265742
else
56275743
requiredCoverage = 0.5;
56285744

5629-
if (coverage < requiredCoverage)
5630-
continue;
5745+
// Low coverage can still be genuine sparse line numbering (every Nth line). Accept it
5746+
// only when the cluster is a regular arithmetic progression at even vertical pitch AND
5747+
// sits at the extreme page margin. Figure panels / legends with small ascending labels
5748+
// (e.g. rows numbered 1..6) are regular too but are inset from the margin, so they must
5749+
// not be stripped; footnote/reference numbers fail the regular-sequence test.
5750+
if (coverage < requiredCoverage) {
5751+
double span = (double) (rightMostBoundary - leftMostBoundary);
5752+
double marginTol = span * 0.06;
5753+
bool atMargin = (final_vpos - leftMostBoundary <= marginTol) ||
5754+
((double) rightMostBoundary - final_vpos <= marginTol);
5755+
if (!(atMargin &&
5756+
is_regular_number_sequence(clusters[bestClusterIndex[j]], lineNumberWords, totalNumberOfLines)))
5757+
continue;
5758+
5759+
// Document-level footnote guard. A line-number column reaches the top of the page's
5760+
// text on some page of the document (numbering starts at the top and runs down every
5761+
// page); a footnote / reference block never does - it sits at the bottom. Accept this
5762+
// low-coverage column only if it reaches the top HERE, or was already confirmed as a
5763+
// line-number column on an earlier page (2-column pages and figures push the numbers
5764+
// down, so a confirmed column may start mid-page on some pages).
5765+
double contentHeight = contentBottom - contentTop;
5766+
double colTop = column_top(lineNumberWords, final_vpos);
5767+
bool reachesTop = (contentHeight <= 0) ||
5768+
((colTop - contentTop) <= 0.4 * contentHeight);
5769+
bool confirmed = false;
5770+
for (int c = 0; c < (int) lineNumberColumnsX.size(); c++)
5771+
if (fabs(lineNumberColumnsX[c] - final_vpos) < 15.0) { confirmed = true; break; }
5772+
if (!reachesTop && !confirmed)
5773+
continue;
5774+
if (reachesTop && !confirmed)
5775+
lineNumberColumnsX.push_back(final_vpos);
5776+
}
56315777
validClusterIndices.push_back(bestClusterIndex[j]);
56325778
}
56335779

@@ -5706,7 +5852,7 @@ static void clampIllustrationBox(double &x, double &y, double &w, double &h) {
57065852
if (h < 0) { h = 0; }
57075853
}
57085854

5709-
void TextPage::dump(GBool noLineNumbers, GBool fullFontName, const vector<bool> &lineNumberStatus) {
5855+
void TextPage::dump(GBool noLineNumbers, GBool fullFontName, const vector<bool> &lineNumberStatus, vector<double> &lineNumberColumnsX) {
57105856
// Output the page in raw (content stream) order
57115857
// Release the previous page's block tree before building this page's.
57125858
// The TextRawWords held by these lines are borrowed: TextPage::words owns
@@ -6313,7 +6459,7 @@ void TextPage::dump(GBool noLineNumbers, GBool fullFontName, const vector<bool>
63136459

63146460
bool hasLineNumber = false;
63156461
if ( (currentPageNumber < nbTotalPage/2) || (previousLineNumber && nbTotalPage>4)) {
6316-
hasLineNumber = markLineNumber();
6462+
hasLineNumber = markLineNumber(lineNumberColumnsX);
63176463
}
63186464
setLineNumber(hasLineNumber);
63196465

@@ -9407,7 +9553,7 @@ void XmlAltoOutputDev::endPage() {
94079553
// if (readingOrder) {
94089554
// text->dumpInReadingOrder(noLineNumbers, fullFontName);
94099555
// } else
9410-
text->dump(noLineNumbers, fullFontName, lineNumberStatus);
9556+
text->dump(noLineNumbers, fullFontName, lineNumberStatus, lineNumberColumnsX);
94119557
appendLineNumberStatus(text->getLineNumber());
94129558
}
94139559

‎src/XmlAltoOutputDev.h‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ class TextPage {
10141014
/** Dump contents of the current page
10151015
* @param blocks To know if the blocks option is selected
10161016
* @param fullFontName To know if the fullFontName option is selected */
1017-
void dump(GBool noLineNumbers, GBool fullFontName, const vector<bool> &lineNumberStatus);
1017+
void dump(GBool noLineNumbers, GBool fullFontName, const vector<bool> &lineNumberStatus, vector<double> &lineNumberColumnsX);
10181018

10191019
/** Dump the current <Page> node to a streaming buffer, then (only on a
10201020
* successful write) unlink it from the DOM and free it, clearing the internal
@@ -1200,7 +1200,7 @@ class TextPage {
12001200
void restoreState(GfxState *state);
12011201

12021202
/** Identify line numbers and mark corresponding raw word */
1203-
bool markLineNumber();
1203+
bool markLineNumber(vector<double> &lineNumberColumnsX);
12041204

12051205
/** Set if the page contains a column of line numbers*/
12061206
void setLineNumber(bool theBool);
@@ -1941,6 +1941,7 @@ class XmlAltoOutputDev: public OutputDev {
19411941

19421942
/** give for each page if line numbers have been found */
19431943
vector<bool> lineNumberStatus;
1944+
vector<double> lineNumberColumnsX; // x-positions of line-number columns confirmed across the document
19441945
};
19451946

19461947
#endif

0 commit comments

Comments
 (0)