-
-
Notifications
You must be signed in to change notification settings - Fork 26
/
RotationDetector.pas
212 lines (185 loc) · 6.78 KB
/
RotationDetector.pas
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
{
Deskew
by Marek Mauder
https://galfar.vevb.net/deskew
https://github.com/galfar/deskew
- - - - -
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
}
unit RotationDetector;
interface
uses
Types,
SysUtils,
Math,
ImagingUtility;
type
TCalcSkewAngleStats = record
PixelCount: Integer;
TestedPixels: Integer;
AccumulatorSize: Integer;
AccumulatedCounts: Integer;
BestCount: Integer;
end;
PCalcSkewAngleStats = ^TCalcSkewAngleStats;
{ Calculates rotation angle for given 8bit grayscale image.
Useful for finding skew of scanned documents etc.
Uses Hough transform internally.
MaxAngle is maximal (abs. value) expected skew angle in degrees (to speed things up)
and Threshold (0..255) is used to classify pixel as black (text) or white (background).
Area of interest rectangle can be defined to restrict the detection to
work only in defined part of image (useful when the document has text only in
smaller area of page and non-text features outside the area confuse the rotation detector).
Various calculations stats can be retrieved by passing Stats parameter.}
function CalcRotationAngle(const MaxAngle: Double; Treshold: Integer;
Width, Height: Integer; Pixels: PByteArray; DetectionArea: PRect = nil;
Stats: PCalcSkewAngleStats = nil): Double;
implementation
function CalcRotationAngle(const MaxAngle: Double; Treshold: Integer;
Width, Height: Integer; Pixels: PByteArray; DetectionArea: PRect; Stats: PCalcSkewAngleStats): Double;
const
// Number of "best" lines we take into account when determining
// resulting rotation angle (lines with most votes).
BestLinesCount = 20;
// Angle step used in alpha parameter quantization (degrees)
AlphaStep = 0.1;
type
TLine = record
Count: Integer;
Index: Integer;
Alpha: Double;
Distance: Double;
end;
TLineArray = array of TLine;
var
AlphaStart, MinDist, SumAngles: Double;
AlphaSteps, DistCount, AccumulatorSize, I, AccumulatedCounts: Integer;
BestLines: TLineArray;
HoughAccumulator: array of Integer;
PageWidth, PageHeight: Integer;
ContentRect: TRect;
// Classifies pixel at [X, Y] as black or white using threshold.
function IsPixelBlack(X, Y: Integer): Boolean;
begin
Result := Pixels[Y * Width + X] < Treshold;
end;
// Calculates final angle for given angle step.
function GetFinalAngle(StepIndex: Integer): Double;
begin
Result := AlphaStart + StepIndex * AlphaStep;
end;
// Calculates angle and distance parameters for all lines
// going through point [X, Y].
procedure CalcLines(X, Y: Integer);
var
D, Rads: Double;
I, DIndex, Index: Integer;
Sin, Cos: Extended;
begin
for I := 0 to AlphaSteps - 1 do
begin
// Angle for current step in radians
Rads := GetFinalAngle(I) * PI / 180;
SinCos(Rads, Sin, Cos);
// Parameter D(distance from origin) of the line y=tg(alpha)x + d
D := Y * Cos - X * Sin;
// Calc index into accumulator for current line
DIndex := Trunc(D - MinDist);
Index := DIndex * AlphaSteps + I;
// Add one vote for current line
HoughAccumulator[Index] := HoughAccumulator[Index] + 1;
end;
end;
// Uses Hough transform to calculate all lines that intersect
// interesting points (those classified as beign on base line of the text).
procedure CalcHoughTransform;
var
Y, X: Integer;
begin
for Y := 0 to PageHeight - 1 do
for X := 0 to PageWidth - 1 do
begin
if IsPixelBlack(ContentRect.Left + X, ContentRect.Top + Y) and
not IsPixelBlack(ContentRect.Left + X, ContentRect.Top + Y + 1) then
begin
CalcLines(X, Y);
end;
end;
end;
// Chooses "best" lines (with the most votes) from the accumulator
function GetBestLines(Count: Integer): TLineArray;
var
I, J, DistIndex, AlphaIndex: Integer;
Temp: TLine;
begin
SetLength(Result, Count);
for I := 0 to AccumulatorSize - 1 do
begin
if HoughAccumulator[I] > Result[Count - 1].Count then
begin
// Current line has more votes than the last selected one,
// let's put it the pot
Result[Count - 1].Count := HoughAccumulator[I];
Result[Count - 1].Index := I;
J := Count - 1;
// Sort the lines based on number of votes
while (J > 0) and (Result[J].Count > Result[J - 1].Count) do
begin
Temp := Result[J];
Result[J] := Result[J - 1];
Result[J - 1] := Temp;
J := J - 1;
end;
end;
AccumulatedCounts := AccumulatedCounts + HoughAccumulator[I];
end;
for I := 0 to Count - 1 do
begin
// Caculate line angle and distance according to index in the accumulator
DistIndex := Result[I].Index div AlphaSteps;
AlphaIndex := Result[I].Index - DistIndex * AlphaSteps;
Result[I].Alpha := GetFinalAngle(AlphaIndex);
Result[I].Distance := DistIndex + MinDist;
end;
end;
begin
AccumulatedCounts := 0;
// Use supplied page content rect or just the whole image
ContentRect := Rect(0, 0, Width, Height);
if DetectionArea <> nil then
begin
Assert((RectWidth(DetectionArea^) <= Width) and (RectHeight(DetectionArea^) <= Height));
ContentRect := DetectionArea^;
end;
PageWidth := ContentRect.Right - ContentRect.Left;
PageHeight := ContentRect.Bottom - ContentRect.Top;
if (ContentRect.Bottom = Height) then
Dec(PageHeight); // Don't check for black pixels outsize of image in CalcHoughTransform()
AlphaStart := -MaxAngle;
AlphaSteps := Ceil(2 * MaxAngle / AlphaStep); // Number of angle steps = samples from interval <-MaxAngle, MaxAngle>
MinDist := -Max(PageWidth, PageHeight);
DistCount := 2 * (PageWidth + PageHeight);
// Determine the size of line accumulator
AccumulatorSize := DistCount * AlphaSteps;
SetLength(HoughAccumulator, AccumulatorSize);
// Calculate Hough transform
CalcHoughTransform;
// Get the best lines with most votes
BestLines := GetBestLines(BestLinesCount);
// Average angles of the selected lines to get the rotation angle of the image
SumAngles := 0;
for I := 0 to BestLinesCount - 1 do
SumAngles := SumAngles + BestLines[I].Alpha;
Result := SumAngles / BestLinesCount;
if Stats <> nil then
begin
Stats.BestCount := BestLines[0].Count;
Stats.PixelCount := PageWidth * PageHeight;
Stats.AccumulatorSize := AccumulatorSize;
Stats.AccumulatedCounts := AccumulatedCounts;
Stats.TestedPixels := AccumulatedCounts div AlphaSteps;
end;
end;
end.