Unit Contours;
{
  Processing of BitMap Contours
  =============================
  This software has been designed and is CopyLefted
  by Han de Bruijn
                     (===)
                    @-O^O-@
                     #/_\#
                      ###
}
INTERFACE

Uses SysUtils, Forms, Graphics, Generaal;

type
  schakel = record
    x,y : integer;
    lnk : integer;
  end;
  RGB = (R,G,B);
  afbeelding = array of array of double;

type
  Omtrekken = class
  private
    log : Nuttig;
    blaat : boolean;
  { Graphics Input } 
    Wijd,Hoog : integer;
  { Edges/Vertices of Contours: }
    bewaar : array of schakel;
    reserve : array of schakel;
    Geteld : integer;
  { Indices of Contours: }
    randen : array of integer;

    procedure Contouren(serieus : boolean);
    procedure Verzamelen;
    procedure Gesloten;
  public
    funktie : afbeelding;
    Scherm : TBitmap;
    constructor Create;
    procedure afmetingen(W,H : integer); { Transfer Width and Height }
    procedure Rondom(blah : boolean); { Overall processing }
    function Aantal : integer; { Number of Contours }
    function Totaal(const klaar : boolean = true) : integer; { Total Length }
    procedure Tekenen(welke : integer ; C : TColor); { Draw }
    procedure Schetsen(welke : integer ; C : TColor; W : integer); { Draw }
    function StartPunt(deze : integer) : punt; { a Vertex of Contour }
    function Kromme(deze : integer) : punten; { Retrieve Contour }
  { Reducing the number of vertices in a contour by Fan Data Compression }
    procedure FanDataCompressie(deze : integer; epsilon : double);
  end;

IMPLEMENTATION

constructor Omtrekken.Create;
begin
  log := Nuttig.Create;
end;

procedure Omtrekken.afmetingen(W,H : integer);
begin
  Wijd := W;
  Hoog := H;
  SetLength(funktie,W,H);
end;

function Omtrekken.Totaal;
begin
  if not klaar then Contouren(false);
  Totaal := Geteld;
end;

function Omtrekken.Aantal : integer;
begin
  Aantal := Length(randen)-1;
end;

procedure Omtrekken.Contouren(serieus : boolean);
{
  Find Contour Lines in a Black & White Image
  -------------------------------------------
  The gist of the algorithm is that there exists
  kind of a linear interpolation between function
  values 1 for black pixels and 0 for white pixels.

  Now consider vertices in between and search for
  function values 1/2, being the contouring level.

  The edges are oriented in such a way that if you
  walk from start- to endpoint, any black pixel is
  at your right (uphill) side and any white pixel
  is at your left (downhill) side. Contours never
  intersect.
}
var
  x,y,x1,y1,x2,y2 : integer;
  getal,tel : integer;
  extra : string;
const                   { getal =  0 1 2 3 4 5 6 7 8 9 A B C D E F }
  dx1 : array[0..$F] of integer = (9,2,1,2,1,1,9,1,0,9,1,2,0,0,1,9);
  dy1 : array[0..$F] of integer = (9,1,2,1,0,0,9,0,1,9,2,1,1,1,2,9);
  dx2 : array[0..$F] of integer = (9,1,0,0,2,1,9,0,1,9,1,1,2,1,2,9);
  dy2 : array[0..$F] of integer = (9,2,1,1,1,2,9,1,0,9,0,0,1,2,1,9);
                        { geval =  0 1 1 3 1 3 4 2 1 4 3 2 3 2 2 0 }
procedure Opslaan;
{
  Storage of Contouring Edges
  where (x1,y1) = begin / odd
        (x2,y2) = end / even.
}
begin
{ Odd counts }
  bewaar[tel-1].x := x1-2;
  bewaar[tel-1].y := y1-2;
{ Even counts }
  bewaar[tel].x := x2-2;
  bewaar[tel].y := y2-2;
end;

function getPixel(x,y : integer) : boolean;
{
  Dark Pixel
}
begin
  getPixel := false;
  if x < 0 then Exit;
  if x > Wijd-1 then Exit;
  if y < 0 then Exit;
  if y > Hoog-1 then Exit;
  getPixel := (funktie[x,y] > 0);
end;

function MetVieren(x,y : integer) : integer;
{
  Take four pixels at a time and    0 1
  caracterize their B/W structure   2 3
  by a number called 'MetVieren'.
}
var
  getal, k : integer;
  zwart : boolean;
const
  ix : array[1..4] of integer = ( 0, 1, 0, 1);
  iy : array[1..4] of integer = ( 0, 0, 1, 1);
begin
  getal := 0;
  for k := 1 to 4 do
  begin
    getal := getal shl 1;
    zwart := getPixel(x+ix[k]-1,y+iy[k]-1);
    if zwart then getal := getal + 1;
  end;
  MetVieren := getal;
end;
{ ======================== }
{ MAIN routine starts here }
{ ======================== }
begin
  if blaat then log.TijdMeting('');
  tel := 0;
  for y := 0 to Hoog do
  begin
    Application.ProcessMessages;

    for x := 0 to Wijd do
    begin
    { Take four pixels at a time and    0 1
      caracterize their B/W structure   2 3
      by a number called 'getal': }

      getal := MetVieren(x,y);

    { All coordinates are multipied by a factor 2     0 1
      in order to preserve their "integer" nature.    2 3
      In comments below  O = White , X = Black : }

      Case getal of

        $0,$F: Continue; { Nothing to do = geval 0 }

      { One black pixel of four gives an edge which joins   0 1
        the two points midway between the one black pixel   2 3
        and two white pixels: }

        $1,$2,$4,$8,  { geval 1 }
        { X O   O X   O O   O O }
        { O O   O O   X O   O X }

      { Three black pixels of four gives an edge joining    0 1
        the two points midway between the one white pixel   2 3
        and two black pixels: }

        $D,$E,$B,$7,  { geval 2 }
        { X O   O X   X X   X X }
        { X X   X X   O X   X O }

        { Two black pixels of four gives an edge which joins  0 1
          the two points midway between the black pixels and  2 3
          the white pixels: }

        $3,$C,$A,$5:  { geval 3 }
        { X X   O O   O X   X O }
        { O O   X X   O X   X O }

        begin
          tel := tel + 2;
          if serieus then
          begin
            x1 := 2*x + dx1[getal];
            y1 := 2*y + dy1[getal];
            x2 := 2*x + dx2[getal];
            y2 := 2*y + dy2[getal];
            Opslaan;
          end;
        end;

        { Two black pixels of four gives two edges joining
          four points midway between the black pixels and    0 1
          the white pixels in such a way that both edges     2 3
          lie inside a white area. Meaning that the other
          possibility is deliberately excluded: it's Black
          on White, it's Not White on Black here ! }

        $9: { geval 4 }
        begin
          tel := tel + 2;
          if serieus then
          begin
            x1 := 2*x + 2 ; y1 := 2*y + 1 ;  { O X }
            x2 := 2*x + 1 ; y2 := 2*y + 0 ;  { X O }
            Opslaan;
          end;
          tel := tel + 2;
          if serieus then
          begin
            x1 := 2*x + 0 ; y1 := 2*y + 1 ;
            x2 := 2*x + 1 ; y2 := 2*y + 2 ;
            Opslaan;
          end;
        end;
        $6: { geval 4 }
        begin
          tel := tel + 2;
          if serieus then
          begin
            x1 := 2*x + 1 ; y1 := 2*y + 0 ;  { X O }
            x2 := 2*x + 0 ; y2 := 2*y + 1 ;  { O X }
            Opslaan;
          end;
          tel := tel + 2;
          if serieus then
          begin
            x1 := 2*x + 1 ; y1 := 2*y + 2 ;
            x2 := 2*x + 2 ; y2 := 2*y + 1 ;
            Opslaan;
          end;
        end;
      end;
    end;
  end;

  Geteld := tel div 2;
  extra := '';
  if serieus then extra := 'I';
  if blaat then log.TijdMeting('Contouring pass I' + extra);
end;

procedure Omtrekken.Verzamelen;
{
  Collect Contouring Edges
  to form Closed Curves
}
var
  start,einde : schakel;
  p,q, k : integer;
  pq,qp : boolean;
begin
  p := 0;
  while p < Geteld do
  begin
    Application.ProcessMessages;
    p := p + 1;
    if (bewaar[2*p-1].lnk > 0)
     and (bewaar[2*p].lnk > 0) then Continue;
    q := p;
    while (q <= Geteld) do
    begin
      q := q + 1;
      einde := bewaar[2*p];
      start := bewaar[2*q-1];
      pq := (start.x = einde.x)
        and (start.y = einde.y);
      if pq then
      begin
        bewaar[2*p].lnk := q;
        bewaar[2*q-1].lnk := p;
      end;
      einde := bewaar[2*q];
      start := bewaar[2*p-1];
      qp := (start.x = einde.x)
        and (start.y = einde.y);
      if qp then
      begin
        bewaar[2*q].lnk := p;
        bewaar[2*p-1].lnk := q;
      end;
      if (bewaar[2*p-1].lnk = 0)
        or (bewaar[2*p].lnk = 0) then Continue;
      if (pq or qp) then Break;
    end;
  end;

{ Keep only half of the data }
  SetLength(reserve,2*Geteld);
  for k := 1 to Geteld do
  begin
    reserve[2*k-2] := bewaar[2*k-1]; 
    reserve[2*k-1] := bewaar[2*k]; 
  end;
  for k := 1 to Geteld do
    bewaar[k] := bewaar[2*k]; 
  SetLength(bewaar,Geteld+1);
end;

procedure Omtrekken.Gesloten;
{
  Closure of Contours
}
var
  done : array of boolean;
  tel, k,L : integer;
begin
  SetLength(done,Geteld+1);
  for k := 1 to Geteld do
    done[k] := false;

  tel := 0;
  for k := 1 to Geteld do
  begin
    if done[k] then Continue;
    tel := tel + 1;
    L := k;
    while not done[L] do
    begin
      done[L] := true;
      L := bewaar[L].lnk;
    end;
  end;

  SetLength(randen,tel+1);
  for k := 1 to Geteld do
    done[k] := false;

  tel := 0;
  for k := 1 to Geteld do
  begin
    if done[k] then Continue;
    tel := tel + 1;
    randen[tel] := k;
    L := k;
    while not done[L] do
    begin
      done[L] := true;
      L := bewaar[L].lnk;
    end;
  end;

  SetLength(done,0);
end;

procedure ZamelTest(OM : Omtrekken);
{
  Just a Test
}
var
  k : integer;
  ff : TextFile;
  w : schakel;
begin
  AssignFile(ff,'bewaar.txt');
  Rewrite(ff);
  for k := 1 to OM.Geteld do
  begin
    w := OM.bewaar[k];
    Writeln(ff,w.x:6,w.y:6,w.lnk:6);
  end;
  CloseFile(ff);
end;

procedure SluitTest(OM : Omtrekken);
{
  Just a Test
}
var
  k,aantal : integer;
  ff : TextFile;
begin
  AssignFile(ff,'gesloten.txt');
  Rewrite(ff);
  aantal := Length(OM.randen)-1;
  for k := 1 to aantal do
    Writeln(ff,OM.randen[k]:5);
  CloseFile(ff);
end;

procedure Omtrekken.Rondom(blah : boolean);
{
  All Together
}
var
  genoeg,k : integer;
begin
  blaat := blah;
  Contouren(false);
  if blah then
    Writeln('# contour segments = ' + IntToStr(Geteld));

  genoeg := 2*Geteld;
  SetLength(bewaar, genoeg+1);
  for k := 1 to genoeg do
  begin
    bewaar[k].x := 0;
    bewaar[k].y := 0;
    bewaar[k].lnk := 0;
  end;

  Contouren(true);
  Verzamelen;
  Gesloten;
end;

function Omtrekken.Kromme(deze : integer) : punten;
{
  Define Contour
}
var
  k,L,tel : integer;
  rond : punten;
begin
  k := randen[deze];
  L := k;
  tel := 0;
  while true do
  begin
    L := bewaar[L].lnk;
    tel := tel + 1;
    if k = L then Break;
  end;
{ Number of Vertices }
  SetLength(rond,tel);

  L := k;
  tel := 0;
  while true do
  begin
    rond[tel].x := bewaar[L].x/2;
    rond[tel].y := bewaar[L].y/2;
    L := bewaar[L].lnk;
    tel := tel + 1;
    if k = L then Break;
  end;
  Kromme := rond;
  SetLength(rond,0);
end;

procedure Omtrekken.Tekenen(welke : integer; C : TColor);
{
  Do the Drawing of a
  fine grained Contour
}
var
  L,k, x,y, xe,ye : integer;
  x1,y1,x2,y2 : double;

  function links(xp,yp : integer) : boolean;
  {
    At the Left of an Edge (1) => (2)
  }
  var
    xd,yd : double;
  begin
    xd := xp; yd := yp;
    links := (y2-y1)*(xd-x1)-(x2-x1)*(yd-y1) < 0;
  end;

begin
  k := randen[welke];
  x2 := bewaar[k].x/2 ; y2 := bewaar[k].y/2;
  L := k;
  while true do
  begin
    L := bewaar[L].lnk;
    x1 := x2 ; y1 := y2 ;
    x2 := bewaar[L].x/2 ; y2 := bewaar[L].y/2;
    x := bewaar[L].x ; y := bewaar[L].y ;
    if (abs(x mod 2) = 1) and ((y mod 2) = 0) then
    begin
      xe := (x-1) div 2 ; ye := y div 2 ;
      With Scherm.Canvas do
      if links(xe,ye) then Pixels[xe,ye] := C;
      xe := (x+1) div 2 ; ye := y div 2 ;
      With Scherm.Canvas do
      if links(xe,ye) then Pixels[xe,ye] := C;
    end;
    if ((x mod 2) = 0) and (abs(y mod 2) = 1) then
    begin
      xe := x div 2 ; ye := (y-1) div 2 ;
      With Scherm.Canvas do
      if links(xe,ye) then Pixels[xe,ye] := C;
      xe := x div 2 ; ye := (y+1) div 2 ;
      With Scherm.Canvas do
      if links(xe,ye) then Pixels[xe,ye] := C;
    end;
    if L = k then Break;
  end;
end;

procedure Omtrekken.Schetsen(welke : integer; C : TColor; W : integer);
{
  Do the Drawing of a
  Compressed Contour
}
var
  L,k, x,y : integer;
begin
  k := randen[welke];
  x := bewaar[k].x div 2;
  y := bewaar[k].y div 2;
  Scherm.Canvas.MoveTo(x,y);
  Scherm.Canvas.Pen.Color := C;
  Scherm.Canvas.Pen.Width := W;
  L := k;
  while true do
  begin
    L := bewaar[L].lnk;
    x := bewaar[L].x div 2;
    y := bewaar[L].y div 2;
    Scherm.Canvas.LineTo(x,y);
    if L = k then Break;
  end;
end;

procedure KansloosMaken(OM : Omtrekken; deze : integer);
{
  Disable Contour
}
var
  ingang : integer;
begin
  ingang := OM.randen[deze];
  OM.bewaar[ingang].lnk := ingang;
end;

function isKansloos(OM : Omtrekken; deze : integer) : boolean;
{
  Disabled Contour
}
var
  ingang : integer;
begin
  ingang := OM.randen[deze];
  isKansloos := (OM.bewaar[ingang].lnk = ingang);
end;

function Omtrekken.StartPunt(deze : integer) : punt;
{
  Pixel near Start of Contour
}
var
  ingang : integer;
  x1,y1,x2,y2 : double;
  L,x,y,xe,ye : integer;
  p : punt;

  function rechts(xp,yp : integer) : boolean;
  {
    At the Right of an Edge (1) => (2)
  }
  var
    xd,yd : double;
  begin
    xd := xp; yd := yp;
    rechts := (y2-y1)*(xd-x1)-(x2-x1)*(yd-y1) > 0;
  end;

begin
  ingang := randen[deze];
  x1 := bewaar[ingang].x/2;
  y1 := bewaar[ingang].y/2;
  L := bewaar[ingang].lnk;
  x2 := bewaar[L].x/2;
  y2 := bewaar[L].y/2;
  x := bewaar[L].x ; y := bewaar[L].y ;
  if (abs(x mod 2) = 1) and ((y mod 2) = 0) then
  begin
    xe := (x-1) div 2 ; ye := y div 2 ;
    if rechts(xe,ye) then
    begin
      p.x := xe; p.y := ye;
    end;
    xe := (x+1) div 2 ; ye := y div 2 ;
    if rechts(xe,ye) then
    begin
      p.x := xe; p.y := ye;
    end;
  end;
  if ((x mod 2) = 0) and (abs(y mod 2) = 1) then
  begin
    xe := x div 2 ; ye := (y-1) div 2 ;
    if rechts(xe,ye) then
    begin
      p.x := xe; p.y := ye;
    end;
    xe := x div 2 ; ye := (y+1) div 2 ;
    if rechts(xe,ye) then
    begin
      p.x := xe; p.y := ye;
    end;
  end;
  StartPunt := p;
end;

procedure Omtrekken.FanDataCompressie(deze : integer; epsilon : double);
{
  Fan Compression algorithm
  -------------------------
  IEEE Computer Graphics & Applications
  "Faster Plots by Fan Data-Compression"
  Richard A. Fowell and David D. McNeil
  March 1989.

  epsilon = desired accuracy in pixels
}
var
  L,k,m,kept,tel, memo : integer;
  pu,pv, dist, h,dh, eps : double;
  keep,verder : boolean;
  tmp,o,p : punt;
  u1,u2,f1,f2,f3 : double;
begin
  eps := epsilon;
  kept := 0;
  tel := 0;
  k := randen[deze];
  L := k;
  o.x := bewaar[L].x/2;
  o.y := bewaar[L].y/2;
  verder := true;
  memo := L;
  u1 := 0 ; u2 := 0 ;
  f1 := 0 ; f2 := 0 ; f3 := 0 ;

  while true do
  begin
    L := bewaar[L].lnk;
    if L = k then Break;
    tel := tel + 1;
  { Local coordinates }
    if verder then
    begin
      p.x := bewaar[L].x/2 - o.x;
      p.y := bewaar[L].y/2 - o.y;
      dist := sqrt(p.x*p.x+p.y*p.y);
      if dist > eps then
      begin
        verder := false;
        u1 := p.x/dist;
        u2 := p.y/dist;
        f1 := dist;
        f2 := eps/dist;
        f3 := -eps/dist;
      end;
    end;

  { Elimination process }
    if not verder then
    begin
      keep := true;
      m := bewaar[L].lnk;
      tmp.x := bewaar[m].x/2;
      tmp.y := bewaar[m].y/2;
      pu :=  (tmp.x-o.x)*u1+(tmp.y-o.y)*u2;
      pv := -(tmp.x-o.x)*u2+(tmp.y-o.y)*u1;
      if pu >= f1 then
      begin
        h := pv/pu;
        if (h <= f2) and (h >= f3) then
        begin
          dh := eps/pu;
          keep := false;
          f1 := pu;
          if h+dh < f2 then f2 := h+dh;
          if h-dh > f3 then f3 := h-dh;
        end;
      end;
      if keep then
      begin
        kept := kept + 1;
        o.x := bewaar[L].x/2;
        o.y := bewaar[L].y/2;
        bewaar[memo].lnk := L;
        memo := L;
        verder := true;
      end;
    end;
  end;
  bewaar[memo].lnk := k;
  o.x := bewaar[k].x/2;
  o.y := bewaar[k].y/2;
  Writeln('Left after Compression = ',(100*kept/tel):5:2,' %');
end;

END.