ImagingBinary.pas 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. {
  2. Vampyre Imaging Library
  3. by Marek Mauder
  4. http://imaginglib.sourceforge.net
  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 with operations on binary images. Binary images in Imaging are
  24. ifGray8 images where pixels with value 0 are considerend off, an pixels > 0
  25. are on.
  26. Note: Native ifBinary data format was later added to Imaging. However,
  27. these functions still use ifGray8 for representation for less complex
  28. and faster processing. ifBinary is meant moreless like interchange
  29. format for IO file formats. }
  30. unit ImagingBinary;
  31. {$I ImagingOptions.inc}
  32. interface
  33. uses
  34. Types, ImagingTypes, Imaging, ImagingFormats, ImagingUtility;
  35. type
  36. { Basic morphologic operators.}
  37. TMorphologyOp = (
  38. moErode, // Erosion
  39. moDilate // Dilatation
  40. );
  41. { Structuring element for morphology operations. Use ones and
  42. zeroes to define your struct elements.}
  43. TStructElement = array of array of Byte;
  44. TCalcSkewAngleStats = record
  45. PixelCount: Integer;
  46. TestedPixels: Integer;
  47. AccumulatorSize: Integer;
  48. AccumulatedCounts: Integer;
  49. BestCount: Integer;
  50. end;
  51. PCalcSkewAngleStats = ^TCalcSkewAngleStats;
  52. { Thresholding using Otsu's method (which chooses the threshold
  53. to minimize the intraclass variance of the black and white pixels!).
  54. Functions returns calculated threshold level value [0..255].
  55. If BinarizeImage is True then the Image is automatically converted to binary using
  56. computed threshold level.}
  57. function OtsuThresholding(var Image: TImageData; BinarizeImage: Boolean = False): Integer;
  58. { Applies basic morphology operators (Erode/Dilate) on Image using given structuring element
  59. Strel. You can do composite operations (Open/Close) by calling this function
  60. twice each time with different operator.}
  61. procedure Morphology(var Image: TImageData; const Strel: TStructElement; Op: TMorphologyOp);
  62. { Calculates rotation angle for given 8bit grayscale image.
  63. Useful for finding skew of scanned documents etc.
  64. Uses Hough transform internally.
  65. MaxAngle is maximal (abs. value) expected skew angle in degrees (to speed things up)
  66. and Threshold (0..255) is used to classify pixel as black (text) or white (background).
  67. Area of interest rectangle can be defined to restrict the detection to
  68. work only in defined part of image (useful when the document has text only in
  69. smaller area of page and non-text features outside the area confuse the rotation detector).
  70. Various calculations stats can be retrieved by passing Stats parameter.}
  71. function CalcRotationAngle(MaxAngle: Integer; Treshold: Integer;
  72. Width, Height: Integer; Pixels: PByteArray; DetectionArea: PRect = nil;
  73. Stats: PCalcSkewAngleStats = nil): Double;
  74. { Deskews given image. Finds rotation angle and rotates image accordingly.
  75. Works best on low-color document-like images (scans).
  76. MaxAngle is maximal (abs. value) expected skew angle in degrees (to speed things up)
  77. and Threshold (0..255) is used to classify pixel as black (text) or white (background).
  78. If Treshold=-1 then auto threshold calculated by OtsuThresholding is used.}
  79. procedure DeskewImage(var Image: TImageData; MaxAngle: Integer = 10; Threshold: Integer = -1);
  80. implementation
  81. function OtsuThresholding(var Image: TImageData; BinarizeImage: Boolean): Integer;
  82. var
  83. Histogram: array[Byte] of Single;
  84. Level, Max, Min, I, J, NumPixels: Integer;
  85. Pix: PByte;
  86. Mean, Variance: Single;
  87. Mu, Omega, LevelMean, LargestMu: Single;
  88. begin
  89. Assert(Image.Format = ifGray8);
  90. FillChar(Histogram, SizeOf(Histogram), 0);
  91. Min := 255;
  92. Max := 0;
  93. Level := 0;
  94. NumPixels := Image.Width * Image.Height;
  95. Pix := Image.Bits;
  96. // Compute histogram and determine min and max pixel values
  97. for I := 0 to NumPixels - 1 do
  98. begin
  99. Histogram[Pix^] := Histogram[Pix^] + 1.0;
  100. if Pix^ < Min then
  101. Min := Pix^;
  102. if Pix^ > Max then
  103. Max := Pix^;
  104. Inc(Pix);
  105. end;
  106. // Normalize histogram
  107. for I := 0 to 255 do
  108. Histogram[I] := Histogram[I] / NumPixels;
  109. // Compute image mean and variance
  110. Mean := 0.0;
  111. Variance := 0.0;
  112. for I := 0 to 255 do
  113. Mean := Mean + (I + 1) * Histogram[I];
  114. for I := 0 to 255 do
  115. Variance := Variance + Sqr(I + 1 - Mean) * Histogram[I];
  116. // Now finally compute threshold level
  117. LargestMu := 0;
  118. for I := 0 to 255 do
  119. begin
  120. Omega := 0.0;
  121. LevelMean := 0.0;
  122. for J := 0 to I - 1 do
  123. begin
  124. Omega := Omega + Histogram[J];
  125. LevelMean := LevelMean + (J + 1) * Histogram[J];
  126. end;
  127. Mu := Sqr(Mean * Omega - LevelMean);
  128. Omega := Omega * (1.0 - Omega);
  129. if Omega > 0.0 then
  130. Mu := Mu / Omega
  131. else
  132. Mu := 0;
  133. if Mu > LargestMu then
  134. begin
  135. LargestMu := Mu;
  136. Level := I;
  137. end;
  138. end;
  139. if BinarizeImage then
  140. begin
  141. // Do thresholding using computed level
  142. Pix := Image.Bits;
  143. for I := 0 to Image.Width * Image.Height - 1 do
  144. begin
  145. if Pix^ >= Level then
  146. Pix^ := 255
  147. else
  148. Pix^ := 0;
  149. Inc(Pix);
  150. end;
  151. end;
  152. Result := Level;
  153. end;
  154. procedure Morphology(var Image: TImageData; const Strel: TStructElement; Op: TMorphologyOp);
  155. var
  156. X, Y, I, J: Integer;
  157. SWidth, SHeight, PixCount, PixVal, NumOnes, PosX, PosY: Integer;
  158. ImgOut: TImageData;
  159. OutPix: PByte;
  160. begin
  161. Assert(Image.Format = ifGray8);
  162. Assert((Length(Strel) > 0) and (Length(Strel[0]) > 0));
  163. SWidth := Length(Strel);
  164. SHeight := Length(Strel[0]);
  165. NumOnes := 0;
  166. if Op = moErode then
  167. begin
  168. // We need to know number of ones in the strel for erosion
  169. for I := 0 to SWidth - 1 do
  170. for J := 0 to SHeight - 1 do
  171. NumOnes := NumOnes + Strel[I, J];
  172. end;
  173. InitImage(ImgOut);
  174. NewImage(Image.Width, Image.Height, ifGray8, ImgOut);
  175. OutPix := ImgOut.Bits;
  176. for J := 0 to Image.Height - 1 do
  177. for I := 0 to Image.Width - 1 do
  178. begin
  179. PixCount := 0;
  180. for X := 0 to SWidth - 1 do
  181. begin
  182. PosX := ClampInt(X + I - SWidth div 2, 0, Image.Width - 1);
  183. for Y := 0 to SHeight - 1 do
  184. begin
  185. PosY := ClampInt(Y + J - SHeight div 2, 0, Image.Height - 1);
  186. if (PosX >= 0) and (PosX < Image.Width) and
  187. (PosY >= 0) and (PosY < Image.Height) then
  188. begin
  189. PixVal := PByteArray(Image.Bits)[PosY * Image.Width + PosX];
  190. end
  191. else
  192. PixVal := 0;
  193. if (Strel[X, Y] > 0) and (PixVal > 0) then
  194. Inc(PixCount);
  195. end;
  196. end;
  197. case Op of
  198. moErode: OutPix^ := Iff(PixCount = NumOnes, 255, 0);
  199. moDilate: OutPix^ := Iff(PixCount > 0, 255, 0);
  200. end;
  201. Inc(OutPix);
  202. end;
  203. FreeImage(Image);
  204. Image := ImgOut;
  205. end;
  206. function CalcRotationAngle(MaxAngle: Integer; Treshold: Integer;
  207. Width, Height: Integer; Pixels: PByteArray; DetectionArea: PRect; Stats: PCalcSkewAngleStats): Double;
  208. const
  209. // Number of "best" lines we take into account when determining
  210. // resulting rotation angle (lines with most votes).
  211. BestLinesCount = 20;
  212. // Angle step used in alpha parameter quantization
  213. AlphaStep = 0.1;
  214. type
  215. TLine = record
  216. Count: Integer;
  217. Index: Integer;
  218. Alpha: Double;
  219. D: Double;
  220. end;
  221. TLineArray = array of TLine;
  222. var
  223. AlphaStart, MinD, SumAngles: Double;
  224. AlphaSteps, DCount, AccumulatorSize, I, AccumulatedCounts: Integer;
  225. BestLines: TLineArray;
  226. HoughAccumulator: array of Integer;
  227. PageWidth, PageHeight: Integer;
  228. ContentRect: TRect;
  229. // Classifies pixel at [X, Y] as black or white using threshold.
  230. function IsPixelBlack(X, Y: Integer): Boolean;
  231. begin
  232. Result := Pixels[Y * Width + X] < Treshold;
  233. end;
  234. // Calculates alpha parameter for given angle step.
  235. function GetAlpha(Index: Integer): Double;
  236. begin
  237. Result := AlphaStart + Index * AlphaStep;
  238. end;
  239. function CalcDIndex(D: Double): Integer;
  240. begin
  241. Result := Trunc(D - MinD);
  242. end;
  243. // Calculates angle and distance parameters for all lines
  244. // going through point [X, Y].
  245. procedure CalcLines(X, Y: Integer);
  246. var
  247. D, Rads: Double;
  248. I, DIndex, Index: Integer;
  249. begin
  250. for I := 0 to AlphaSteps - 1 do
  251. begin
  252. Rads := GetAlpha(I) * Pi / 180; // Angle for current step in radians
  253. D := Y * Cos(Rads) - X * Sin(Rads); // Parameter D of the line y=tg(alpha)x + d
  254. DIndex := CalcDIndex(D);
  255. Index := DIndex * AlphaSteps + I;
  256. HoughAccumulator[Index] := HoughAccumulator[Index] + 1; // Add one vote for current line
  257. end;
  258. end;
  259. // Uses Hough transform to calculate all lines that intersect
  260. // interesting points (those classified as beign on base line of the text).
  261. procedure CalcHoughTransform;
  262. var
  263. Y, X: Integer;
  264. begin
  265. for Y := 0 to PageHeight - 1 do
  266. for X := 0 to PageWidth - 1 do
  267. begin
  268. if IsPixelBlack(ContentRect.Left + X, ContentRect.Top + Y) and
  269. not IsPixelBlack(ContentRect.Left + X, ContentRect.Top + Y + 1) then
  270. begin
  271. CalcLines(X, Y);
  272. end;
  273. end;
  274. end;
  275. // Chooses "best" lines (with the most votes) from the accumulator
  276. function GetBestLines(Count: Integer): TLineArray;
  277. var
  278. I, J, DIndex, AlphaIndex: Integer;
  279. Temp: TLine;
  280. begin
  281. SetLength(Result, Count);
  282. for I := 0 to AccumulatorSize - 1 do
  283. begin
  284. if HoughAccumulator[I] > Result[Count - 1].Count then
  285. begin
  286. // Current line has more votes than the last selected one,
  287. // let's put it the pot
  288. Result[Count - 1].Count := HoughAccumulator[I];
  289. Result[Count - 1].Index := I;
  290. J := Count - 1;
  291. // Sort the lines based on number of votes
  292. while (J > 0) and (Result[J].Count > Result[J - 1].Count) do
  293. begin
  294. Temp := Result[J];
  295. Result[J] := Result[J - 1];
  296. Result[J - 1] := Temp;
  297. J := J - 1;
  298. end;
  299. end;
  300. AccumulatedCounts := AccumulatedCounts + HoughAccumulator[I];
  301. end;
  302. for I := 0 to Count - 1 do
  303. begin
  304. // Caculate line angle and distance according to index in the accumulator
  305. DIndex := Result[I].Index div AlphaSteps;
  306. AlphaIndex := Result[I].Index - DIndex * AlphaSteps;
  307. Result[I].Alpha := GetAlpha(AlphaIndex);
  308. Result[I].D := DIndex + MinD;
  309. end;
  310. end;
  311. begin
  312. AccumulatedCounts := 0;
  313. // Use supplied page content rect or just the whole image
  314. ContentRect := Rect(0, 0, Width, Height);
  315. if DetectionArea <> nil then
  316. begin
  317. Assert((RectWidth(DetectionArea^) <= Width) and (RectHeight(DetectionArea^) <= Height));
  318. ContentRect := DetectionArea^;
  319. end;
  320. PageWidth := ContentRect.Right - ContentRect.Left;
  321. PageHeight := ContentRect.Bottom - ContentRect.Top;
  322. AlphaStart := -MaxAngle;
  323. AlphaSteps := Round(2 * MaxAngle / AlphaStep); // Number of angle steps = samples from interval <-MaxAngle, MaxAngle>
  324. MinD := -PageWidth;
  325. DCount := 2 * (PageWidth + PageHeight);
  326. // Determine the size of line accumulator
  327. AccumulatorSize := DCount * AlphaSteps;
  328. SetLength(HoughAccumulator, AccumulatorSize);
  329. // Calculate Hough transform
  330. CalcHoughTransform;
  331. // Get the best lines with most votes
  332. BestLines := GetBestLines(BestLinesCount);
  333. // Average angles of the selected lines to get the rotation angle of the image
  334. SumAngles := 0;
  335. for I := 0 to BestLinesCount - 1 do
  336. SumAngles := SumAngles + BestLines[I].Alpha;
  337. Result := SumAngles / BestLinesCount;
  338. if Stats <> nil then
  339. begin
  340. Stats.BestCount := BestLines[0].Count;
  341. Stats.PixelCount := PageWidth * PageHeight;
  342. Stats.AccumulatorSize := AccumulatorSize;
  343. Stats.AccumulatedCounts := AccumulatedCounts;
  344. Stats.TestedPixels := AccumulatedCounts div AlphaSteps;
  345. end;
  346. end;
  347. procedure DeskewImage(var Image: TImageData; MaxAngle: Integer; Threshold: Integer);
  348. var
  349. Angle: Double;
  350. OutputImage: TImageData;
  351. Info: TImageFormatInfo;
  352. begin
  353. if not TestImage(Image) then
  354. raise EImagingBadImage.Create;
  355. // Clone input image and convert it to 8bit grayscale. This will be our
  356. // working image.
  357. CloneImage(Image, OutputImage);
  358. ConvertImage(Image, ifGray8);
  359. if Threshold < 0 then
  360. begin
  361. // Determine the threshold automatically if needed.
  362. Threshold := OtsuThresholding(Image);
  363. end;
  364. // Main step - calculate image rotation angle
  365. Angle := CalcRotationAngle(MaxAngle, Threshold, Image.Width, Image.Height, Image.Bits);
  366. // Finally, rotate the image. We rotate the original input image, not the working
  367. // one so the color space is preserved.
  368. GetImageFormatInfo(OutputImage.Format, Info);
  369. if Info.IsIndexed or Info.IsSpecial then
  370. ConvertImage(OutputImage, ifA8R8G8B8); // Rotation doesn't like indexed and compressed images
  371. RotateImage(OutputImage, Angle);
  372. FreeImage(Image);
  373. Image := OutputImage;
  374. end;
  375. {
  376. File Notes:
  377. -- TODOS ----------------------------------------------------
  378. - nothing now
  379. -- 0.77 -------------------------------------------------------
  380. - OtsuThresholding signature changed, now it's a function and
  381. always returns the computed level.
  382. - Extended CalcRotationAngle, added margins and stats.
  383. - Added CalcRotationAngle and DeskewImage functions.
  384. -- 0.25.0 Changes/Bug Fixes -----------------------------------
  385. - Unit created with basic stuff (otsu and erode/dilate morphology ops).
  386. }
  387. end.