""" cface.pyw by leonardo maffi, v.1.2, July 20 2006. Translated to Python from Java from the original Java code (C) 1998 John Wiseman, http://people.cs.uchicago.edu/~wiseman/chernoff/ See docstring of CFace class for more info. Note: JavaDoc comments can be used with EpyDoc: http://epydoc.sourceforge.net/ Possible improvements: - draw_lip is ugly, it can be used the canvas.create_arc of Tkinter """ import random, math class CFace: """Class that defines and can draw a Chernoff face of any size at any position. It contains the 13-dimensional vector p of numbers in [0.0, 1.0] that completely describe the face: 0 head eccentricity 1 eye eccentricity 2 pupil size 3 eyebrow slope 4 nose size 5 mouth vert. offset 6 eye spacing 7 eye size 8 mouth width 9 mouth "openness" 10 face red color component 11 face green color component 12 face blue color component xOval, xFillOval, xLine methods can be shadowed to allow plotting with a different graphics lib. Explanation from wikipedia: http://en.wikipedia.org/wiki/Chernoff_faces Chernoff faces display multivariate data in the shape of a human face. The individual parts, such as eyes, ears, mouth and nose represent values of the variables by their shape, size, placement and orientation. The idea behind using faces is that humans easily recognize faces and notice small changes without difficulty. Chernoff faces handle each variable differently. Because the features of the faces vary in perceived importance, the way in which variables are mapped to the features should be carefully chosen. References: Herman Chernoff (1973). "Using faces to represent points in k-dimensional space graphically". Journal of the American Statistical Association 68 (342): 361-368. """ def __init__(self, coords=None): if coords: assert len(coords) == 13 self.p = [max(0.0, min(1.0, el)) for el in coords] else: # Constructs a random face vector, with black color. self.p = [random.random() for i in xrange(10)] + [0.0, 0.0, 0.0] # Various parameters that adjust the appearance of the face. self.head_radius = 30 self.eye_radius = 5 self.eye_left_x = 40 self.eye_right_x = 60 self.eye_y = 40 self.pupil_radius = 1.5 self.eyebrow_l_l_x = 35 self.eyebrow_r_l_x = 55 self.eyebrow_l_r_x = 45 self.eyebrow_r_r_x = 65 self.eyebrow_y = 30 self.nose_apex_x = 50 self.nose_apex_y = 45 self.nose_height = 16 self.nose_width = 8 self.mouth_y = 65 def draw(self, x=0, y=0, width=100, height=100, canvas=None): """draw(x=0, y=0, width=100, height=100, canvas=None): Draws the face into a logical space with dimensions 100x100, and scales it to the actual size specified by width and height. Input: x,y Position of the face to be painted. width The width of the Graphics context. height The height of the Graphics context. canvas The Graphics context to draw the face into. color The color to draw the face with. """ self.canvas = canvas self.color = "#%02x%02x%02x" % tuple( int(round(cc*255)) for cc in self.p[10:13] ) # Compute and store the scaling factors and origin used by xCircle, # xOval, xFillOval and xLine. self.x_factor = width / 100.0 self.y_factor = height / 100.0 self.x_origin = x self.y_origin = y # draw head e0, e1 = self.eccentricities(self.p[0]) self.xOval(50, 50, self.head_radius + e0, self.head_radius + e1) # draw eye eye_spacing = int((self.p[6] - 0.5) * 10) eye_size = int( ((self.p[7] - 0.5) / 2.0) * 10 ) e0, e1 = self.eccentricities(self.p[1]) self.xOval(self.eye_left_x - eye_spacing, self.eye_y, self.eye_radius + eye_size + e0, self.eye_radius + eye_size + e1) self.xOval(self.eye_right_x + eye_spacing, self.eye_y, self.eye_radius + eye_size + e0, self.eye_radius + eye_size + e1) # draw pupils pupil_size_x = int(max(1, self.p[2] * self.pupil_radius * self.x_factor)) pupil_size_y = int(max(1, self.p[2] * self.pupil_radius * self.y_factor)) self.xFillOval(self.eye_left_x - int((self.p[6] - 0.5) * 10), self.eye_y, pupil_size_x, pupil_size_y) self.xFillOval(self.eye_right_x + int((self.p[6] - 0.5) * 10), self.eye_y, pupil_size_x, pupil_size_y) # draw eyebrow y1 = self.eyebrow_y + int((self.p[3] - 0.5) * 10) y2 = self.eyebrow_y - int((self.p[3] - 0.5) * 10) self.xLine(self.eyebrow_l_l_x, y1, self.eyebrow_l_r_x, y2) self.xLine(self.eyebrow_r_l_x, y2, self.eyebrow_r_r_x, y1) # draw nose y = 55 + int(((self.p[4] - 0.5) / 2.0) * 10) self.xLine(self.nose_apex_x, self.nose_apex_y, self.nose_apex_x - (self.nose_width / 2), y) self.xLine(self.nose_apex_x - (self.nose_width / 2), y, self.nose_apex_x + (self.nose_width / 2), y) self.xLine(self.nose_apex_x + (self.nose_width / 2), y, self.nose_apex_x, self.nose_apex_y) # draw mouth mouth_size = ((self.p[8] - 0.5) * 10) x1 = 40 - mouth_size y1 = self.mouth_y x2 = 60 + mouth_size y2 = self.mouth_y x3 = ((x2 - x1) / 2) + x1 y3 = ((self.p[5] - 0.5) * 10) + self.mouth_y self.draw_lip(x1, y1, x2, y2, x3, y3) self.draw_lip(x1, y1, x2, y2, x3, y3 + ((self.p[9] / 2.0) * 10)) def draw_lip(self, x1, y1, x2, y2, x3, y3): # This is some nasty parabolic stuff. It doesn't look that good because of the stupid # way we scale to non- 100x100 displays. # This method is ugly, it can be used the canvas.create_arc of Tkinter x1_2 = x1 ** 2 x2_2 = x2 ** 2 x3_2 = x3 ** 2 denom = (x1_2 * (x2 - x3)) \ + (x1 * (x3_2 - x2_2)) \ + (x2_2 * x3) \ - (x3_2 * x2) denom = float(denom) a = ( (y1 * (x2 - x3)) + (x1 * (y3 - y2)) + (y2 * x3) + -(y3 * x2) ) / denom bb = ( (x1_2 * (y2 - y3)) + (y1 * (x3_2 - x2_2)) + (x2_2 * y3) - (x3_2 * y2) ) / denom c = ( (x1_2 * ((x2 * y3) - (x3 * y2))) + (x1 * ((x3_2 * y2) - (x2_2 * y3))) + (y1 * ((x2_2 * x3) - (x3_2 * x2))) ) / denom last_x = int(x1) last_y = int(y1) for i in xrange(int(x1), int(x2+1)): new_x = i new_y = int(a * i**2 + bb*i + c) self.xLine(last_x, last_y, new_x, new_y) last_x = new_x last_y = new_y def scale_x(self, x): return int(x * self.x_factor) def scale_y(self, y): return int(y * self.y_factor) def eccentricities(self, p): """Takes a number p between 0.0 and 1.0 and returns a 2-vector that should be added to the dimensions of a circle to create an oval.""" if p > 0.5: return [int((p - 0.5) * 20.0), 0] else: return [0, int(abs(p - 0.5) * 20.0)] def xOval(self, x, y, height_r, width_r): """Draws a scaled and translated oval with Tkinter.""" x1 = self.scale_x(x - width_r) + self.x_origin y1 = self.scale_y(y - height_r) + self.y_origin self.canvas.create_oval(x1, y1, x1 + self.scale_x(width_r * 2), y1 + self.scale_y(height_r * 2), fill="", outline=self.color, width=1) def xFillOval(self, x, y, height_r, width_r): """Draw a scaled, translated and filled oval with Tkinter.""" x1 = self.scale_x(x - width_r) + self.x_origin y1 = self.scale_y(y - height_r) + self.y_origin self.canvas.create_oval(x1, y1, x1 + self.scale_x(width_r * 2), y1 + self.scale_y(height_r * 2), fill=self.color, outline="") def xLine(self, x1, y1, x2, y2): """Draws a scaled and translated line using Tkinter.""" self.canvas.create_line(self.scale_x(x1) + self.x_origin, self.scale_y(y1) + self.y_origin, self.scale_x(x2) + self.x_origin, self.scale_y(y2) + self.y_origin, fill=self.color, width=1) def facesInterpolate(f1, f2, t=0.5): """facesInterpolate(f1, f2, t=0.5): function that returns a new CFace that is the result of the smooth interpolation between one given CFace to another. Input: f1, f1: the two input faces. t in [0,1], the control parameter. t=0.0 means that the output face is f1, t=1.0 means that the output face is f2, t=0.5 means that the output face is the middle one between f1 and f2.""" return CFace( [c1+(c2-c1)*t for c1,c2 in zip(f1.p, f2.p)] ) if __name__ == '__main__': import Tkinter as tk red_face = CFace([0.0] * 10 + [1.0, 0.0, 0.0]) green_face = CFace([1.0] * 10 + [0.0, 1.0, 0.0]) # Demo 1, shows faces from [0.0]*10 to [1.0]*10 def update_screen(): global t # Hack to test if we still exist try: canvas['width'] except: return if t > 1: return # Termination canvas.delete("all") # Clear all the canvas interp_face = facesInterpolate(red_face, green_face, t) interp_face.draw(canvas=canvas, x=30, y=50, width=250, height=200) t += 1 / steps canvas.after(1, update_screen) root = tk.Tk() steps = 100 canvas = tk.Canvas(root, height=300, width=300, background="white") canvas.pack() t = 0.0 # t goes from 0.0 to 1.0, in self.steps steps. steps = float(steps) update_screen() root.mainloop() # Demo 2, shows 10 aligned faces root = tk.Tk() canvas = tk.Canvas(root, height=100, width=1000, background="white") canvas.pack() for i in xrange(0, 11): interp_face = facesInterpolate(red_face, green_face, i/10.0) interp_face.draw(canvas=canvas, x=100*i, y=0, width=100, height=100) root.mainloop()