OCR 詞框行聚類前估算傾斜角:三種估算方法之一在 5 度時失效
Estimating skew from OCR word boxes before row clustering: one of my three estimators broke at 5 degrees
作者以一張合成表格影像比較三種 OCR 詞框傾斜估算方法;傾斜 5 度時,「所有配對」法把表格分成 19 行,鄰近詞框法和 Profile 法仍分成正確的 9 行。Profile 法在 2 度影像的 200 組隨機 10 詞框子集中,有 157 次估算斜率與真值相差不超過 0.003,分箱法則有 59 次。測試只用一張英文表格的合成影像,結果並非準確率指標,亦未涵蓋真實掃描。
Tilt a table image by 2 degrees and the two ends of a row end up about 43 px apart vertically (1,228 px of text width times tan 2 degrees), while the rows on my test sheet are 56 px apart. A fixed y tolerance gives up somewhere in that range. The obvious repair is to estimate one global slope and subtract slope times x before clustering. I assumed that would take ten lines, so I wrote three versions and compared them.
The sheet is a quote list I generated: 7 columns, a header and 8 data rows, 63 cells, no ruling lines, English and digits only. I rotated it by 0.5 to 5 degrees on white and ran tesseract 5.5.3 (English pack, --psm 6) on an Apple M4, about 70 word boxes per image. Afterwards I cluster with half the median word height and count rows; 9 is right. It is one sheet, so nothing here is an accuracy figure.
Three ways to read the slope
Neighbours: for each box take the nearest box to its right within 1.5 word heights vertically, then the median of dy/dx. All pairs: the median of dy/dx over every pair more than 50 px apart, skipping pairs steeper than 0.12. Profile: try slopes from -0.15 to 0.15 and keep the one that piles up y minus slope times x most tightly, which is what flat rows do. I fixed those numbers before the first run and never touched them; only the profile score changed, below.
| Tilt | True slope | Neighbours | All pairs | Profile | Rows, no fix |
|---|---|---|---|---|---|
| 0.5 deg | -0.0087 | -0.0097 | -0.0087 | -0.0086 | 9 |
| 1 deg | -0.0175 | -0.0176 | -0.0172 | -0.0174 | 10 |
| 2 deg | -0.0349 | -0.0352 | -0.0336 | -0.0350 | 18 |
| 3 deg | -0.0524 | -0.0511 | -0.0475 | -0.0528 | 19 |
| 5 deg | -0.0875 | -0.0848 | -0.0352 | -0.0880 | 24 |
All three gave 9 rows up to 3 degrees. At 5 degrees neighbours and profile still gave 9 and all pairs gave 19.
All pairs broke at 5 degrees
A pair from two different rows passes the steepness cap once the page leans far enough. Among the pairs that passed at 5 degrees, 253 came from the same row and 263 from other rows. At 2 degrees it was 238 and 226 and the estimate was fine; my guess is that the strays scatter to both sides there and cancel in the median, which I did not plot. A lower cap is one more number to tune, so I dropped it.
My first profile score was jumpy
I first counted boxes in horizontal bins a quarter of a word height wide and scored a slope by the sum of squared counts. On the straight bordered score sheet it returned -0.0022 instead of 0: the score was 534 at a slope of exactly 0.0 and 440 at 1.4e-16. I read that as centres hopping across 5.5 px bin edges, checked only through those two scores. A Gaussian kernel over all pairs of y values has no edges:
def tilt_slope(cx, cy, sigma):
def tightness(s):
y = cy - s * cx
gap = y[:, None] - y[None, :]
return np.exp(-0.5 * (gap / sigma) ** 2).sum()
grid = np.arange(-0.15, 0.1501, 0.002)
best = grid[np.argmax([tightness(s) for s in grid])]
fine = np.arange(best - 0.002, best + 0.0021, 0.0002)
return fine[np.argmax([tightness(s) for s in fine])]
With sigma at a quarter of the median word height this is the Profile column. It also needs fewer boxes: over 200 random subsets of 10 boxes from the 2 degree sheet, the kernel landed within 0.003 of the true slope 157 times and the bins 59.
A straight page should not move
A correction that moves flat tables is a regression, so I checked. On three straight sheets (quote, bordered score and merged-header sheets, the last two with lines painted out) the profile estimate stayed between -0.0004 and +0.0016 and row counts did not change: 9, 9 and 10. The merged-header sheet gives 10 either way, for a reason I haven't found.
A simulated phone photo
The same sheet as a simulated phone photo (rotated 4.5 degrees, trapezoid warp, glare patch, JPEG; made with Pillow and OpenCV, not a real photo) gave 17 rows with no fix. Neighbours estimated 4.83 degrees and profile 5.07, both gave 9 rows, all pairs gave 15. Per-row slopes after the fix varied by only about 15 px across the page, against a word height of 39, so one slope was enough here; I have not tried a stronger warp.
For comparison I tried the OCR page of ImgIng (https://imging.ai/) in a desktop browser with its default model. On clean synthetic tables its copied text has tabs between only some cells, so the columns are incomplete. On the four simulated photos it found almost every word and wrote each as one scrambled line; it only offers 90 degree turns, and it does not run on phones.
What I have not settled
Rows were right at 3 and 5 degrees, the text was not: 6 of 9 rows matched the source at 3 degrees and 3 of 9 at 5, since tesseract starts misreading digits (196.10 came back as 496.10). Everything here is one English sheet rotated by a script, so no real scan went through it. Run the estimator on a straight page first: if the slope is not near 0.000 there, the estimator is the problem.
來源:DEV Community · dev.to