############################################################################
#
#	File:     facebend.icn
#
#	Subject:  Program to generate caricatures
#
#	Author:   Gregg M. Townsend
#
#	Date:     October 7, 1997
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#  Facebender is a caricature generator.  Read in an image and use the
#  left mouse button to pick the key points as prompted.  Click the
#  right button to skip a feature.  Pull down "drawing" on the display
#  menu to see the caricature.  Move the slider to change the distortion.
#
############################################################################
#
#  References:
#
#  A. K. Dewdney, "Computer Recreations". Scientific American, Oct. 1986.
#  Reprinted in two collections of his columns, both from W. H. Freeman:
#  The Armchair Universe (1988) and The Tinkertoy Computer (1993).
#
#  Susan E. Brennan, "Caricature Generator: The Dynamic Exaggeration of
#  Faces by Computer."  Leonardo, Vol.18 no.3, 1985, pp. 170-178.
#
############################################################################
#
#  Requires:  Version 9 graphics
#
############################################################################
#
#  Links:  graphics, vsetup
#
############################################################################


link graphics				# graphics library
link vsetup				# VIB library


# constant definitions

$define PupilRadius 2			# radius for drawing pupils of eyes

$define TargetRad1 5			# radii for guide display target
$define TargetRad2 20

$define ImageMode 1			# drawing modes
$define DrawMode 2
$define DualMode 3


# vidgets and geometry

global vidgets				# vidget table

global display_xoff, display_yoff	# image area
global display_width, display_height
global image_xoff, image_yoff		# centered image

global guide_xoff, guide_yoff		# guide area
global guide_width, guide_height

global prompt_xoff, prompt_yoff		# prompt area
global prompt_width, prompt_height

global dmeter_xoff, dmeter_yoff		# distortion meter
global dmeter_width, dmeter_height

# windows and bindings

global image_win			# scanned image
global target_win			# binding for point targets
global display_win			# binding for image or caricature
global overlay_win			# binding for dual-mode display

# face data
#
# (A face is a list of curves, beginning with the left and right pupils;
# a curve is a list of x and y coordinates.)

global descriptions			# labels for facial curves

global stdface				# standard (average) face
global guideface			# scaled / translated guide face
global sketch				# points from subject face

global tcurve				# index of current curve to place
global tpoint				# index of point within curve

# miscellaneous globals

global pointfile			# file name for saving coordinates
global touched				# has data changed since last save?

global mode				# Image / Draw / Dual mode
global distortion			# distortion factor (0.0 = undistorted)


#  main program

procedure main()
   local l, r, y

   # Open the window, extract layout information, initialize dialogs.

   vidgets := ui()
   WAttrib("pointer=circle")		# may fail, but at least try
   init_geometry()

   # Make two clipped bindings for displaying the image and sketch.

   display_win := Clone("linewidth=2")
   Clip(display_win, display_xoff, display_yoff, display_width, display_height)
   overlay_win := Clone(display_win, "fillstyle=masked", "pattern=4,#9696")

   # Make a clipped binding for displaying targets on the guide display.

   target_win := Clone("drawop=reverse")
   Clip(target_win, guide_xoff, guide_yoff, guide_width, guide_height)

   # Initialize globals.

   init_stdface()			# coordinates of "standard" face
   mode := ImageMode			# display mode
   setdist(0)				# distortion factor

   # Use the standard face to create a guide display for locating targets.
   # Calculate eye locations to use for scaling;  then draw the face
   # with straight lines to emphasize the individual point locations.

   l := guide_xoff + 3 * guide_width / 8
   r := guide_xoff + 5 * guide_width / 8
   y := guide_yoff + guide_height / 2
   guideface := scaleface(stdface, [[l, y], [r, y]])
   drawface(&window, guideface, DrawLine)

   # Load and display an image; exit if dialog is cancelled.

   new() | exit()

   # Enter event loop.

   GetEvents(vidgets["root"], , shortcuts)

end


#  caricature() -- draw sketch distorted by current distortion factor

procedure caricature()
   local base, face, win

   if /sketch | /sketch[1, 1] | /sketch[2, 1] then
      fail				# must have both pupils to draw

   if mode = DrawMode then
      win := display_win		# use all the display area pixels
   else
      win := overlay_win		# use subpattern of display pixels

   Fg(win, "white")
   FillRectangle(win)			# clear clipped area using fillstyle
   Fg(win, "black")

   base := scaleface(stdface, sketch)
   face := distort(sketch, base, distortion)
   drawface(win, face, DrawCurve)	# draw distorted face

   return

end


# check_save() -- check to see if previous coordinate needs to be saved
#
# check_save fails if cancelled.

procedure check_save()

   if \touched then
      case SaveDialog("Save coordinates first?", pointfile) of {
         "Yes": {
            pointfile := dialog_value
            save() | save_as() | fail
            }
         "No":  return
         "Cancel":  fail
         }

   return

end


#  distort(f, b, m) -- return distortion of face f from face b by factor m

procedure distort(f, b, m)
   local r, t, i, j, curve, base

   r := []
   every i := 1 to *f do {
      base := b[i]
      put(r, curve := copy(f[i]))
      if /curve[-1] | /base[-1] then
         next				# incomplete placeholder
      every j := 1 to *curve by 2 do {
         curve[j] +:= m * (curve[j] - base[j])
         curve[j + 1] +:= m * (curve[j + 1] - base[j + 1])
         }
      }

   return r

end


#  drawface(win, f, proc) -- draw face from curve list using proc

procedure drawface(win, f, proc)
   local curve

   every curve := copy(!f) do {
      if /curve[-1] then		# null coordinate
         next				# incomplete curve
      if *curve = 2 then
         FillCircle(win, curve[1], curve[2], PupilRadius)
      else {
         push(curve, win)
         proc ! curve
         }
      }

   return

end


#  init_geometry() -- extract layout information from vidgets

procedure init_geometry()

   guide_xoff := vidgets["guide"].ax
   guide_yoff := vidgets["guide"].ay
   guide_width := vidgets["guide"].aw
   guide_height := vidgets["guide"].ah

   display_xoff := vidgets["image"].ax
   display_yoff := vidgets["image"].ay
   display_width := vidgets["image"].aw
   display_height := vidgets["image"].ah

   prompt_xoff := vidgets["prompt"].ax
   prompt_yoff := vidgets["prompt"].ay
   prompt_width := vidgets["prompt"].aw
   prompt_height := vidgets["prompt"].ah

   dmeter_xoff := vidgets["dmeter"].ax
   dmeter_yoff := vidgets["dmeter"].ay
   dmeter_width := vidgets["dmeter"].aw
   dmeter_height := vidgets["dmeter"].ah

   return

end


#  init_stdface() -- initialize standard face and description list

procedure init_stdface()
   local spec

   descriptions := []
   stdface := []
   every spec := ![
      ["left pupil",145,203],		# must be first
      ["right pupil",255,203],		# must be second
      ["top of left eyebrow",101,187,105,177,126,168,153,170,177,176,181,185],
      ["top of right eyebrow",219,185,223,176,247,170,274,168,295,177,299,187],
      ["bottom of left eyebrow",102,188,124,177,151,181,181,185],
      ["bottom of right eyebrow",219,185,249,181,276,177,298,188],
      ["top of left eye",114,199,141,187,172,198],
      ["top of right eye",228,198,259,187,286,199],
      ["bottom of left eyelid",116,207,143,194,170,206],
      ["bottom of right eyelid",230,206,257,194,284,207],
      ["bottom of left eye",120,208,142,213,170,206],
      ["bottom of right eye",230,206,258,213,280,208],
      ["left iris",144,195,132,201,144,211,156,201,145,195],
      ["right iris",255,195,244,201,256,211,268,201,256,195],
      ["left side of nose",190,193,190,219,190,244,186,257,189,271,200,277],
      ["right side of nose",210,193,210,219,210,244,214,257,211,271,200,277],
      ["left nostril",177,250,171,258,169,269,174,277,183,271,198,277],
      ["right nostril",223,250,229,258,231,269,226,277,217,271,202,277],
      ["top of upper lip",152,318,172,311,188,306,200,311,212,306,
         228,311,248,318],
      ["bottom of upper lip",152,318,170,319,186,317,200,319,214,317,
         230,319,248,318],
      ["top of lower lip",152,318,172,318,186,317,200,319,214,317,
         228,318,248,318],
      ["bottom of lower lip",152,318,169,327,184,333,200,335,216,333,
         231,327,248,318],
      ["left ear",75,212,61,201,54,213,58,233,64,260,75,285,85,281],
      ["right ear",325,212,339,201,346,213,342,233,336,260,325,285,315,281],
      ["top of head",60,317,28,254,31,189,46,108,82,47,141,4,200,1,259,4,
         318,47,354,108,369,189,372,254,340,317],
      ["hairline",79,200,90,168,104,141,119,120,143,104,172,100,200,99,
         228,100,257,104,281,120,296,141,310,168,321,200],
      ["left side of face",84,194,79,232,86,273],
      ["right side of face",316,194,321,232,314,273],
      ["jaw",85,272,93,311,108,342,133,369,167,392,200,399,233,392,
         267,369,292,342,307,311,315,272],
      ["left eye line",131,221,148,220,166,214],
      ["right eye line",234,214,252,220,269,221],
      ["left cheek line",167,264,154,278,145,294],
      ["right cheek line",233,264,246,278,255,294],
      ["left cheekbone",87,269,95,280,101,292],
      ["right cheekbone",313,269,305,280,299,292],
      ["chin cleft",200,377,200,389],
      ["chin line",180,350,200,345,220,350]
      ] do {
         put(descriptions, get(spec))
         put(stdface, spec)
         }

   return

end


#  load() -- load coordinate data

procedure load()
   local input, face

   check_save() | fail
   repeat {
      case OpenDialog("Load coordinates:") of {
         "Okay": {
            if input := open(dialog_value) then break else
               Notice("Can't open " || dialog_value)
            }
         "Cancel":  fail
         }
      }

   if sketch := rdface(input) then {
      close(input)
      pointfile := dialog_value
      touched := &null
      if mode ~= ImageMode then
         redisplay()
      target(1, 1)
      return
      }

   else {
      Notice("Not a valid coordinate file")
      close(input)
      fail
      }

end


#  menu_cb() -- handle menu selections

procedure menu_cb(vidget, menu)

   case menu[1] of {

      "load    @L":	load()
      "new     @N":	new()
      "save    @S":	save()
      "save as   ":	save_as()
      "quit    @Q":	quit()

      "image    @I": {
         mode := ImageMode
         redisplay()
         }
      "drawing  @D": {
         mode := DrawMode
         redisplay()
         }
      "both     @B": {
         mode := DualMode
         redisplay()
         }
      }

   return

end


#  new() -- load new image

procedure new()
   local input, f

   check_save() | fail
   repeat {
      case OpenDialog("Load image:") of {
         "Okay": {
            if rdimage(dialog_value) then
               return
            if f := open(dialog_value) then {
               close(f)
               Notice(dialog_value || " is not a valid image")
               }
            else
               Notice("Can't open " || dialog_value)
            }
         "Cancel":  fail
         }
      }

end


#  point_cb() -- handle event in display region

procedure point_cb(vidget, e)

   if /tcurve then			# if no points are left unset
      return

   case e of {

      &lrelease: {			# left button sets current point
         sketch[tcurve, 2 * tpoint - 1] := &x
         sketch[tcurve, 2 * tpoint] := &y
         touched := 1
         if mode ~= ImageMode & *sketch[tcurve] = 2 * tpoint then
            redisplay()			# redraw if new curve done
         target(tcurve, tpoint)		# update target display
         }

      &rrelease: {			# right button skips a curve
         every !sketch[tcurve] := &null	# clear all points on curve
         if (tcurve +:= 1) > *sketch then
            tcurve := 1
         target(tcurve, 1)		# set target to next curve
         }

      }

   return

end


#  quit() -- terminate session

procedure quit()

   check_save() | fail
   exit()

end


#  rdface(f) -- read face coordinates from file f

procedure rdface(f)
   local face, line, curve, i, n

   face := []
   while line := read(f) do line ? {
      =":" | next			# ignore line missing ":"
      curve := []

      while tab(upto(&digits)) do {
         n := integer(tab(many(&digits)))
         if n ~= 0 then n +:= image_xoff else n := &null
         put(curve, n)

         tab(upto(&digits)) | break
         n := integer(tab(many(&digits)))
         if n ~= 0 then n +:= image_yoff else n := &null
         put(curve, n)
         }

      put(face, curve)
      }

   # Validate the number of curves and points.

   if *face ~= *stdface then fail
   every i := 1 to *stdface do
      if *face[i] ~= *stdface[i] then fail

   return face

end


#  rdimage(filename) -- load image from file, failing if unsuccessful

procedure rdimage(filename)
   local curve

   image_win := WOpen("image=" || filename, "canvas=hidden") | fail
   pointfile := &null
   touched := &null

   # Calculate offsets that center the image in display area.

   image_xoff := display_xoff +
      (display_width - WAttrib(image_win, "width")) / 2
   image_yoff := display_yoff +
      (display_height - WAttrib(image_win, "height")) / 2

   # Initialize a new set of (unset) points.

   sketch := []
   every curve := !stdface do
      put(sketch, list(*curve, &null))
   target(1, 1)				# reset to start with first point

   # Ensure that current mode includes the image, and update the display.

   if mode = DrawMode then
      mode := ImageMode
   EraseArea(display_xoff, display_yoff, display_width, display_height)
   redisplay()

   return

end


#  redisplay() -- display image and/or drawing, depending on mode

procedure redisplay()

   if mode ~= DrawMode then
      CopyArea(image_win, display_win, , , , , image_xoff, image_yoff)
   if mode ~= ImageMode then
      caricature()

   return

end


#  save() -- save coordinate data

procedure save()
   local output

   if /pointfile then
      return save_as()

   output := open(pointfile, "w") | {
      Notice("Can't write " || pointfile)
      fail
      }
   wtface(output, sketch)
   close(output)
   touched := &null

   return

end


#  save_as() -- save coordinate data in alternate file

procedure save_as()
   local output

   repeat {
      case SaveDialog("Save coordinates?", "") of {
         "No":	    return
         "Cancel":  fail
         "Yes":
            if output := open(dialog_value, "w") then break else
               Notice("Can't write " || dialog_value)
         }
      }

   wtface(output, sketch)
   close(output)
   pointfile := dialog_value
   touched := &null

   return

end


#  scaleface(f, g) -- return copy of face f scaled to overlay face g

procedure scaleface(f, g)
   local fl, fr, gl, gr, fx, fy, gx, gy, m, r, t, curve

   fl := f[1] | fail			# left iris
   fr := f[2] | fail			# right iris
   gl := g[1] | fail			# target left iris
   gr := g[2] | fail			# target right iris
   fx := (fl[1] + fr[1]) / 2.0		# x offset of f
   fy := (fl[2] + fr[2]) / 2.0		# y offset of f
   gx := (gl[1] + gr[1]) / 2.0		# x offset of g
   gy := (gl[2] + gr[2]) / 2.0		# y offset of g
   m := (gr[1] - gl[1]) / real(fr[1] - fl[1])
					# multiplier

   r := []
   every curve := copy(!f) do {
      if /curve[-1] then
         put(r, curve)			# incomplete placeholder
      else {
         put(r, t := [])
         while put(t, m * (get(curve) - fx) + gx) do
            put(t, m * (get(curve) - fy) + gy)
         }
      }

   return r

end


#  setdist(val) -- set and display distortion value, in percent

procedure setdist(val)

   distortion := val / 100.0
   GotoXY(dmeter_xoff, dmeter_yoff + dmeter_height)
   WWrites(right(integer(val), 4), "%")

   return

end


#  shortcuts() -- check event for keyboard shortcut

procedure shortcuts(e)

   if &meta then case map(e) of {
      "l": load()
      "n": new()
      "s": save()
      "q": quit()
      "i": {
         mode := ImageMode
         redisplay()
         }
      "d": {
         mode := DrawMode
         redisplay()
         }
      "b": {
         mode := DualMode
         redisplay()
         }
      }

   return

end


#  slider_cb() -- handle adjustments of distortion slider

procedure slider_cb(vidget, val)

   setdist(val)				# update and display value
   if mode = ImageMode then		# ensure that mode includes drawing
      mode := DualMode
   redisplay()				# draw updated sketch

   return

end


#  target(curve, point) -- display next point to be placed

procedure target(curve, point)
   local s, n, x, y
   static tx, ty

   # Undraw the previous target and erase the previous prompt.

   FillCircle(target_win, \tx, \ty, TargetRad1)
   FillCircle(target_win, \tx, \ty, TargetRad2)
   EraseArea(prompt_xoff, prompt_yoff, prompt_width, prompt_height)

   # Start from specified place unless the pupils remain unplaced.

   if \sketch[1, 1] & \sketch[2, 1] then {
      tcurve := curve
      tpoint := point
      }
   else {
      tcurve := 1
      tpoint := 1
      }

   # Find the next unset point.

   until /sketch[tcurve, 2 * tpoint - 1] do {
      tpoint +:= 1			# advance to next point
      if tpoint > (2 * *guideface[tcurve]) then {
         tpoint := 1			# need to move to next curve
         tcurve +:= 1
         }
      if tcurve > *guideface then
         tcurve := 1			# wrapped around list of curves
      if tcurve = curve & tpoint = point then {
         tcurve := tx := ty := &null	# there are no unset points
         return
         }
      }

   # Draw a target on the guide face.

   tx := guideface[tcurve, 2 * tpoint - 1]
   ty := guideface[tcurve, 2 * tpoint]
   FillCircle(target_win, tx, ty, TargetRad1)
   FillCircle(target_win, tx, ty, TargetRad2)

   # Display the prompt.

   x := prompt_xoff + prompt_width / 2
   y := prompt_yoff + prompt_height / 2
   s := "locate " || descriptions[tcurve]
   n := *guideface[tcurve]
   if n > 2 then
      s ||:= " (select " || n / 2 || " points)"
   CenterString(x, y, s)

   return

end


#  wtface(f, face) -- write face data to file f

procedure wtface(f, face)
   local curve, i

   every curve := !face do {
      writes(f, ":")
      every i := 1 to *curve by 2 do {
         writes(f, " ", (\curve[i] - image_xoff) | 0)
         writes(f, " ", (\curve[i + 1] - image_yoff) | 0)
         }
      write(f)
      }

   return

end


#===<<vib:begin>>===	modify using vib; do not remove this marker line
procedure ui_atts()
   return ["size=640,480", "bg=pale gray", "label=Caricaturist"]
end

procedure ui(win, cbk)
return vsetup(win, cbk,
   [":Sizer:::0,0,640,480:Caricaturist",],
   ["distort:Slider:h:1:10,436,230,22:-300,300,0",slider_cb],
   ["dmenu:Menu:pull::36,0,57,21:Display",menu_cb,
      ["image    @I","drawing  @D","both     @B"]],
   ["fmenu:Menu:pull::0,0,36,21:File",menu_cb,
      ["new     @N","load    @L","save    @S","save as   ","quit    @Q"]],
   ["header_line:Line:::0,22,639,22:",],
   ["label1:Label:::11,409,77,13:distortion:",],
   ["label2:Label:::9,460,28,13:anti",],
   ["label3:Label:::104,460,42,13:normal",],
   ["label4:Label:::213,460,28,13:wild",],
   ["vert_line:Line:::250,23,250,479:",],
   ["dmeter:Rect:invisible::104,410,41,10:",],
   ["prompt:Rect:invisible::252,1,387,19:",],
   ["guide:Rect:invisible::1,24,247,280:",],
   ["image:Rect:invisible::252,24,387,455:",point_cb],
   )
end
#===<<vib:end>>===	end of section maintained by vib
