Repo for the search and displace ingest module that takes odf, docx and pdf and transforms it into .md to be used with search and displace operations
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

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