unit EDM; {Euclidian distance maps, ultimate eroded points, and watershed segmentation} interface uses Types, Memory, QuickDraw, QuickDrawText, Packages, Menus, Events, Fonts, Scrap, ToolUtils, Resources, Errors, Palettes, StandardFile, Windows, Controls, TextEdit, Files, Dialogs, TextUtils, Finder, MixedMode, Processes, globals, Utilities, Graphics, Analysis; procedure MakeEDM(item: integer); implementation type Image16Data = packed array[0..maxImageSize] of integer; Image16Ptr = ^Image16Data; StartsArray = array[0..255] of LongInt; function FindUtimatePoints(MaxEDM,item: LongInt; image2: ImageP; LevelStart: StartsArray; xCoordinate, yCoordinate: Image16Ptr): boolean; { Finds peaks in the EDM that contain pixels equal to or greater than all of their neighbors. } var x, y, level, rowsize, offset, i, count: LongInt; CoordOffset, xmax, ymax: LongInt; NextTick: LongInt; image: ImageP; SetPixel: boolean; begin with info^ do begin image := ImageP(PicbaseAddr); BlockMove(image, image2, PixMapSize); rowsize := BytesPerRow; xmax := PixelsPerLine - 1; ymax := nLines - 1; NextTick := TickCount + 15; for level := maxEDM - 1 downto 1 do begin repeat count := 0; for i := 0 to Histogram[level] - 1 do begin CoordOffset := LevelStart[level] + i; x:= xCoordinate^[CoordOffset]; y:= yCoordinate^[CoordOffset]; offset := x + y * rowsize; if image^[offset] <> 255 then begin SetPixel := false; if (x > 0) and (y > 0) then if image^[offset - rowSize - 1] > level then SetPixel := true; if y > 0 then if image^[offset - rowSize] > level then SetPixel := true; if (x < xmax) and (y > 0) then if image^[offset - rowSize + 1] > level then SetPixel := true; if x < xmax then if image^[offset + 1] > level then SetPixel := true; if (x < xmax) and (y < ymax) then if image^[offset + rowSize + 1] > level then SetPixel := true; if y < ymax then if image^[offset + rowSize] > level then SetPixel := true; if (x > 0) and (y < ymax) then if image^[offset + rowSize - 1] > level then SetPixel := true; if x > 0 then if image^[offset - 1] > level then SetPixel := true; if SetPixel then begin image^[offset] := 255; count := count + 1; end; end; {if} end; {for i} until count = 0; if TickCount > NextTick then begin ShowAnimatedWatch; if CommandPeriod then begin beep; undo; WhatToUndo := NothingToUndo; FindUtimatePoints := false; exit(FindUtimatePoints); end; NextTick := TickCount + 15; end; end; {for level} if item = WatershedItem then begin for i := 0 to info^.PixMapSize - 1 do if (image^[i] > 0) and (image^[i] < 255) then image2^[i] := 255; BlockMove(image2, image, PixMapSize); end else begin for i := 0 to info^.PixMapSize - 1 do if image^[i] = 255 then image^[i] := 0; end; FindUtimatePoints := true; end; {with} end; procedure DoWatershedSegmentation(MaxEDM: LongInt; image2: ImageP; LevelStart: StartsArray; xCoordinate, yCoordinate: Image16Ptr; var iterations: LongInt); { Assumes the current image contains an EDM and that the peaks in the EDM (the ultimate eroded points) have been set to 255. The EDM is dilated from these peaks, starting at the highest peak and working down. At each level, the dilation is constrained to pixels whose values are at that level and also constrained to prevent features from merging. } var rowSize, NextTicks: LongInt; level, x, y, offset, xmax, ymax: LongInt; count, FirstCount: LongInt; table: FateTable; image: ImageP; procedure CheckAbort; begin UpdatePicWindow; if CommandPeriod then begin beep; undo; WhatToUndo := NothingToUndo; exit(DoWatershedSegmentation); end; NextTicks := TickCount + 30; if OptionKeyDown then wait(30); end; procedure MakeFateTable; {Add pixel on 1st pass if bit 0 is set, on 2nd pass if bit 1 is} {set, on 3rd pass if bit 2 is set, and on 4th pass if bit 3 is set.} {E.g. '4' = add on 3rd pass, '3' = add on either 1st or 2nd pass,} {'f' = add on any pass. There is a routine in 'user.p' that draws} {all 256 neighborhoods.} const s999 = '01234567890123456789012345678901'; s000 = '0044004480cc80cc0000000080cc80cc'; s032 = '1000000080ff80ff1000000090ff90ff'; s064 = '00000000000000000000000000000000'; s096 = '1000000090ff90ff1000000090ff90ff'; s128 = '2266006600ff00ff0000000000ff00ff'; s160 = '13ff00ffffffffff33ff00ffffffffff'; s192 = '2266006600ff00ff0000000000ff00ff'; s224 = '33ff00ffffffffff33ff00fffffffff'; var s: str255; c: char; i: LongInt; begin s := concat(s000, s032, s064, s096, s128, s160, s192, s224); for i := 0 to 254 do begin c := s[i + 1]; if c <= '9' then table[i] := ord(c) - ord('0') else table[i] := 10 + ord(c) - ord('a') end; table[255] := 15; {f} end; procedure ProcessLevel(level, pass: LongInt); var index, x, y, i, CoordOffset, offset: LongInt; begin BlockMove(image, image2, info^.PixMapSize); for i := 0 to Histogram[level] - 1 do begin CoordOffset := LevelStart[level] + i; x:= xCoordinate^[CoordOffset]; y:= yCoordinate^[CoordOffset]; offset := x + y * rowsize; if image2^[offset] <> 255 then begin index := 0; if (x > 0) and (y > 0) then if image2^[offset - rowSize - 1] = 255 then index := bor(index, 1); if y > 0 then if image2^[offset - rowSize] = 255 then index := bor(index, 2); if (x < xmax) and (y > 0) then if image2^[offset - rowSize + 1] = 255 then index := bor(index, 4); if x < xmax then if image2^[offset + 1] = 255 then index := bor(index, 8); if (x < xmax) and (y < ymax) then if image2^[offset + rowSize + 1] = 255 then index := bor(index, 16); if y < ymax then if image2^[offset + rowSize] = 255 then index := bor(index, 32); if (x > 0) and (y < ymax) then if image2^[offset + rowSize - 1] = 255 then index := bor(index, 64); if x > 0 then if image2^[offset - 1] = 255 then index := bor(index, 128); case pass of 1: if band(table[index], 1) = 1 then begin image^[offset] := 255; count := count + 1; end; 2: if band(table[index], 2) = 2 then begin image^[offset] := 255; count := count + 1; end; 3: if band(table[index], 4) = 4 then begin image^[offset] := 255; count := count + 1; end; 4: if band(table[index], 8) = 8 then begin image^[offset] := 255; count := count + 1; end; end; {case} end; {if} end; {for i} end; {ProcessLevel} procedure PostProcess; var i: LongInt; begin for i := 0 to info^.PixMapSize - 1 do if image^[i] < 255 then image^[i] := 0 end; begin with info^ do begin rowSize := BytesPerRow; MakeFateTable; image := ImageP(PicbaseAddr); xmax := PixelsPerLine - 1; ymax := nLines - 1; NextTicks := TickCount; iterations := 0; for level := maxEDM - 1 downto 1 do begin repeat count := 0; ProcessLevel(level, 1); ProcessLevel(level, 3); ProcessLevel(level, 2); ProcessLevel(level, 4); iterations := iterations + 1; until count = 0; if TickCount > NextTicks then CheckAbort; end; {for level} PostProcess; UpdatePicWindow; end; end; procedure SmoothEDM(image2: ImageP); var x, y, rowsize, offset, sum: LongInt; xmax, ymax: LongInt; image: ImageP; begin with info^ do begin image := ImageP(PicbaseAddr); BlockMove(image, image2, PixMapSize); rowsize := BytesPerRow; xmax := PixelsPerLine - 1; ymax := nLines - 1; for y := 0 to nLines - 1 do begin for x := 0 to PixelsPerLine - 1 do begin offset := x + y * rowsize; if image2^[offset] <> 1 then begin sum := image2^[offset] * 2; if (x > 0) and (y > 0) then sum := sum + image2^[offset - rowSize - 1]; if y > 0 then sum := sum + image2^[offset - rowSize]; if (x < xmax) and (y > 0) then sum := sum + image2^[offset - rowSize + 1]; if x < xmax then sum := sum + image2^[offset + 1]; if (x < xmax) and (y < ymax) then sum := sum + image2^[offset + rowSize + 1]; if y < ymax then sum := sum + image2^[offset + rowSize]; if (x > 0) and (y < ymax) then sum := sum + image2^[offset + rowSize - 1]; if x > 0 then sum := sum + image2^[offset - 1]; image^[offset] := sum div 10; end; end; {for x} end; {for y} end; {with} end; procedure MakeEDM(item: integer); { Converts a binary image into a grayscale Euclidian distance map (EDM). Each foreground (black) pixel in the binary image is assigned a value equal to its distance from the nearest background (white) pixel. Uses the two pass EDM algorithm from the "Image Pricessing Handbook" by John Russ. } const one = 41; sqrt2 = 58; {~41*sqrt(2)} sqrt5 = 92; {~41*sqrt(5)} var x, y, xmax, ymax, i, MaxEDM: LongInt; StartTicks, NextTicks, offset, rowsize: LongInt; iterations: LongInt; PointsOK: boolean; image16: Image16Ptr; image2: ImageP; str: str255; LevelStart: StartsArray; xCoordinate, yCoordinate: Image16Ptr; function ConvertToIntegers:boolean; var x, y, offset: LongInt; begin with info^ do begin image16 := Image16Ptr(NewPtr(rowsize * nLines * SizeOf(integer))); if image16 = nil then begin ConvertToIntegers := false; exit(ConvertToIntegers); end; for y := 0 to nLines - 1 do for x := 0 to PixelsPerLine - 1 do begin offset := x + y * rowsize; image16^[offset] := ImageP(PicBaseAddr)^[offset] * one; end; ConvertToIntegers := true; end; end; procedure ConvertToBytes; var x, y, v, round, offset: LongInt; begin round := one div 2; MaxEDM := 0; with info^ do begin for y := 0 to nLines - 1 do for x := 0 to PixelsPerLine - 1 do begin offset := x + y * rowsize; v := (image16^[offset] + round) div one; if v > 255 then v := 255; if v > maxEDM then maxEDM := v; ImageP(PicBaseAddr)^[offset] := v; end; end; end; procedure SetEdgeValue; var min, inc, v: LongInt; r1, r2, r3, r4, r5, offimage: LongInt; begin r1 := offset - rowsize - rowsize - 2; r2 := r1 + rowsize; r3 := r2 + rowsize; r4 := r3 + rowsize; r5 := r4 + rowsize; min := 32767; offimage := image16^[r3 + 2]; if y < 2 then v := offImage + one else v := image16^[r2 + 2] + one; if v < min then min := v; if x < 2 then v := offImage + one else v := image16^[r3 + 1] + one; if v < min then min := v; if x > xmax then v := offImage + one else v := image16^[r3 + 3] + one; if v < min then min := v; if y > ymax then v := offImage + one else v := image16^[r4 + 2] + one; if v < min then min := v; if (x < 2) or (y < 2) then v := offImage + sqrt2 else v := image16^[r2 + 1] + sqrt2; if v < min then min := v; if (x > xmax) or (y < 2) then v := offImage + sqrt2 else v := image16^[r2 + 3] + sqrt2; if v < min then min := v; if (x < 2) or (y > ymax) then v := offImage + sqrt2 else v := image16^[r4 + 1] + sqrt2; if v < min then min := v; if (x > xmax) or (y > ymax) then v := offImage + sqrt2 else v := image16^[r4 + 3] + sqrt2; if v < min then min := v; if (x < 2) or (y < 2) then v := offImage + sqrt5 else v := image16^[r1 + 1] + sqrt5; if v < min then min := v; if (x > xmax) or (y < 2) then v := offImage + sqrt5 else v := image16^[r1 + 3] + sqrt5; if v < min then min := v; if (x > xmax) or (y < 2) then v := offImage + sqrt5 else v := image16^[r2 + 4] + sqrt5; if v < min then min := v; if (x > xmax) or (y > ymax) then v := offImage + sqrt5 else v := image16^[r4 + 4] + sqrt5; if v < min then min := v; if (x > xmax) or (y > ymax) then v := offImage + sqrt5 else v := image16^[r5 + 3] + sqrt5; if v < min then min := v; if (x < 2) or (y > ymax) then v := offImage + sqrt5 else v := image16^[r5 + 1] + sqrt5; if v < min then min := v; if (x < 2) or (y > ymax) then v := offImage + sqrt5 else v := image16^[r4] + sqrt5; if v < min then min := v; if (x < 2) or (y < 2) then v := offImage + sqrt5 else v := image16^[r2] + sqrt5; if v < min then min := v; image16^[offset] := min; end; {SetEdgeValue} procedure SetValue; var min, inc, v: LongInt; r1, r2, r3, r4, r5: LongInt; begin r1 := offset - rowsize - rowsize - 2; r2 := r1 + rowsize; r3 := r2 + rowsize; r4 := r3 + rowsize; r5 := r4 + rowsize; min := 32767; v := image16^[r2 + 2] + one; if v < min then min := v; v := image16^[r3 + 1] + one; if v < min then min := v; v := image16^[r3 + 3] + one; if v < min then min := v; v := image16^[r4 + 2] + one; if v < min then min := v; v := image16^[r2 + 1] + sqrt2; if v < min then min := v; v := image16^[r2 + 3] + sqrt2; if v < min then min := v; v := image16^[r4 + 1] + sqrt2; if v < min then min := v; v := image16^[r4 + 3] + sqrt2; if v < min then min := v; v := image16^[r1 + 1] + sqrt5; if v < min then min := v; v := image16^[r1 + 3] + sqrt5; if v < min then min := v; v := image16^[r2 + 4] + sqrt5; if v < min then min := v; v := image16^[r4 + 4] + sqrt5; if v < min then min := v; v := image16^[r5 + 3] + sqrt5; if v < min then min := v; v := image16^[r5 + 1] + sqrt5; if v < min then min := v; v := image16^[r4] + sqrt5; if v < min then min := v; v := image16^[r2] + sqrt5; if v < min then min := v; image16^[offset] := min; end; {SetValue} procedure CheckAbort; begin ShowAnimatedWatch; if CommandPeriod then begin beep; DisposePtr(ptr(image16)); exit(MakeEDM); end; NextTicks := TickCount + 15; end; procedure MakeCoordinateArrays; {Generates the XY coordinate arrays that allow pixels at each level to be accesed directly without searching through the entire image. This method, suggested by Stein Roervik (steinr@kjemi.unit.no), greatly speeds up the watershed segmentation routine.} var i, x, y, rowsize, offset, v, ArraySize: LongInt; LevelOffset: array[0..255] of LongInt; image: ImageP; begin with info^ do begin RoiRect := PicRect; GetRectHistogram; ArraySize := 0; for i := 1 to maxEDM - 1 do ArraySize := ArraySize + histogram[i]; xCoordinate := Image16Ptr(NewPtr(ArraySize * SizeOf(integer))); yCoordinate := Image16Ptr(NewPtr(ArraySize * SizeOf(integer))); if (xCoordinate = nil) or (yCoordinate = nil) then begin if xCoordinate <> nil then DisposePtr(ptr(xCoordinate)); DisposePtr(ptr(image2)); PutError('Out of memory'); undo; WhatToUndo := NothingToUndo; exit(MakeEDM); end; image := ImageP(PicbaseAddr); offset := 0; for i := 0 to 255 do begin LevelStart[i] := offset; if (i >= 1) and (i <= (MaxEDM - 1)) then offset := offset + histogram[i]; end; for I := 0 to 255 do LevelOffset[i] := 0; rowsize := BytesPerRow; for y := 0 to nLines - 1 do begin for x := 0 to PixelsPerLine - 1 do begin v := image^[x + y * rowsize]; if (v >= 1) and (v <= (MaxEDM - 1)) then begin offset := LevelStart[v] + LevelOffset[v]; xCoordinate^[offset] := x; yCoordinate^[offset] := y; LevelOffset[v] := LevelOffset[v] + 1; end; end; {for x} end; {for y} end; {with} end; begin with info^ do begin ShowWatch; KillRoi; rowsize := BytesPerRow; xmax := PixelsPerLine - 3; ymax := nLines - 3; StartTicks := TickCount; NextTicks := TickCount; SetupUndo; WhatToUndo := UndoFilter; if not ConvertToIntegers then begin AbortMacro; PutError('Out of Memory'); exit(MakeEDM); end; for y := 0 to nLines - 1 do begin for x := 0 to PixelsPerLine - 1 do begin offset := x + y * rowsize; if image16^[offset] > 0.0 then begin if (x < 2) or (x > xmax) or (y < 2) or (y > ymax) then SetEdgeValue else SetValue; end; end; if TickCount > NextTicks then CheckAbort; end; for y := nLines - 1 downto 0 do begin for x := PixelsPerLine - 1 downto 0 do begin offset := x + y * rowsize; if image16^[offset] > 0.0 then begin if (x < 2) or (x > xmax) or (y < 2) or (y > ymax) then SetEdgeValue else SetValue; end; end; if TickCount > NextTicks then CheckAbort; end; ConvertToBytes; DisposePtr(ptr(image16)); if (item = UltimateItem) or (item = WatershedItem) then begin image2 := ImageP(NewPtr(PixMapSize)); if image2 = nil then exit(MakeEDM); if ((item = WatershedItem) and not OptionKeyWasDown) or ((item = UltimateItem) and OptionKeyWasDown) then SmoothEDM(image2); MakeCoordinateArrays; PointsOK := FindUtimatePoints(MaxEDM, item, image2, LevelStart, xCoordinate, yCoordinate); end; if (item = WatershedItem) and PointsOK then begin DoWatershedSegmentation(MaxEDM, image2, LevelStart, xCoordinate, yCoordinate, iterations); str := StringOf(MaxEDM - 1:1, ' levels', crStr, iterations/(MaxEDM - 1):1:2, ' iterations/level'); end else str := ''; if (item = UltimateItem) or (item = WatershedItem) then begin DisposePtr(ptr(xCoordinate)); DisposePtr(ptr(yCoordinate)); DisposePtr(ptr(image2)); end; ShowTime(StartTicks, PicRect, str); changes := true; UpdatePicWindow; end; {with} end; end.