You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
7.6 KiB
228 lines
7.6 KiB
{
|
|
Deskew
|
|
by Marek Mauder
|
|
http://galfar.vevb.net/deskew
|
|
|
|
The contents of this file are used with permission, subject to the Mozilla
|
|
Public License Version 1.1 (the "License"); you may not use this file except
|
|
in compliance with the License. You may obtain a copy of the License at
|
|
http://www.mozilla.org/MPL/MPL-1.1.html
|
|
|
|
Software distributed under the License is distributed on an "AS IS" basis,
|
|
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
|
|
the specific language governing rights and limitations under the License.
|
|
|
|
Alternatively, the contents of this file may be used under the terms of the
|
|
GNU Lesser General Public License (the "LGPL License"), in which case the
|
|
provisions of the LGPL License are applicable instead of those above.
|
|
If you wish to allow use of your version of this file only under the terms
|
|
of the LGPL License and not to allow others to use your version of this file
|
|
under the MPL, indicate your decision by deleting the provisions above and
|
|
replace them with the notice and other provisions required by the LGPL
|
|
License. If you do not delete the provisions above, a recipient may use
|
|
your version of this file under either the MPL or the LGPL License.
|
|
|
|
For more information about the LGPL: http://www.gnu.org/copyleft/lesser.html
|
|
}
|
|
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
|
|
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.
|