diff --git a/.hgignore b/.hgignore
new file mode 100644
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1,1 @@
+^images/
\ No newline at end of file
diff --git a/grid.py b/grid.py
new file mode 100644
--- /dev/null
+++ b/grid.py
@@ -0,0 +1,2 @@
+class Grid:
+  pass
\ No newline at end of file
diff --git a/gui.py b/gui.py
new file mode 100644
--- /dev/null
+++ b/gui.py
@@ -0,0 +1,69 @@
+import tkinter as tk
+from PIL import ImageTk
+import PIL
+import math
+
+class EPoint:
+  def __init__(self,x,y):
+    self.x=x
+    self.y=y
+  
+  def dist(self,a):
+    return math.sqrt((self.x-a.x)**2+(self.y-a.y)**2)
+
+class Application(tk.Frame):
+  def __init__(self, master=None):
+    self.corners=[]
+    
+    tk.Frame.__init__(self, master)
+    self.pack()
+    self.createWidgets()
+
+  def createWidgets(self):
+    self.hi_there = tk.Button(self)
+    self.hi_there["text"] = "Hello World\n(click me)"
+    self.hi_there["command"] = self.say_hi
+    self.hi_there.pack(side="top")
+    
+    self.canvas=tk.Canvas(self)
+    self.canvas.configure(width=480,height=360,background="#ff4444")
+    imgOrig=PIL.Image.open("images/1.jpg")
+    self.img=ImageTk.PhotoImage(imgOrig.resize((int(self.canvas['width']),int(self.canvas['height'])),resample=PIL.Image.BILINEAR))
+    
+    self.canvas.bind('<1>',lambda e: self.addCorner(e.x,e.y))
+    self.canvas.create_image(2,2,anchor="nw",image=self.img)
+    self.canvas.create_line(30,30,40,40,fill="#00ff00")
+    self.canvas.create_line(30,40,40,30,fill="#00ff00")
+    
+    self.canvas.pack()
+
+    self.QUIT = tk.Button(self, text="QUIT", fg="red", command=root.destroy)
+    self.QUIT.pack(side="bottom")
+
+  def say_hi(self):
+    print("hi there, everyone!")
+    
+  def addCorner(self,x,y):
+    a=EPoint(x,y)
+    for i,c in enumerate(self.corners): # move an improperly placed point
+      if a.dist(c)<20:
+        self.corners[i]=a
+        return
+    
+    if len(self.corners)<4: # add a new corner
+      self.corners.append(a)
+      return
+    
+    index,minDist=0,float('inf') # replace the corner closest to the clicked point
+    for i,c in enumerate(self.corners):
+      if a.dist(c)<minDist:
+        index,minDist=i,a.dist(c)
+    
+    self.corners[index]=a
+    
+  def redrawGrid(self):
+    pass
+
+root = tk.Tk()
+app = Application(master=root)
+app.mainloop()
diff --git a/project.md b/project.md
new file mode 100644
--- /dev/null
+++ b/project.md
@@ -0,0 +1,35 @@
+OneEye
+======
+
+Program to extract moves of a go game from video and save them or broadcast online.
+
+Modules:
+  - Video: grabbing still frames from a video file / stream. Using FFmpeg.
+  - Graphic: extracting game position from an image. Using Pillow.
+  - Watcher: interpreting sequence of game positions as a sequence of moves.
+  - Broadcaster: interfacing with a go server.
+
+
+Graphic
+-------
+Interpolating the board grid from specified corner points. Using vanishing points and horizon in projective geometry.
+
+Determining the point status based on majority voting of pixels in its neighbourhood (deciding by HSI intensity).
+
+Autodetection of the board?
+
+
+Watcher
+-------
+Base case: we have two correctly recognized positions differing by a single move (single added stone). Easy.
+
+Issues:
+  - illegal positions -> ignorable
+  - positions unreachable from the previous state
+    - reachable from any past state. (Incorrect states inbetween). How to pick the correct leaf of such a tree?
+    - reachable by more than one move. Issues with branching factor.
+  - board shifts -> repaired manually (or automatically), further positions have to be reevaluated
+  - stone shifts
+    - stone stops being recognized -> fixable manually and even ignorable
+    - stone is recognized at an empty intersection. It can be occupied later for real. What do?
+
diff --git a/vector3.py b/vector3.py
new file mode 100644
--- /dev/null
+++ b/vector3.py
@@ -0,0 +1,89 @@
+import math
+
+
+class Vector3:
+
+  def __init__(self,x,y,z):
+    self.x=x
+    self.y=y
+    self.z=z
+    
+  def __add__(self,v):
+    return Vector3(self.x+v.x, self.y+v.y, self.z+v.z)
+    
+  def __iadd__(self,v):
+    self.x+=v.x
+    self.y+=v.y
+    self.z+=v.z
+    return self
+    
+  def __sub__(self,v):
+    return Vector3(self.x-v.x, self.y-v.y, self.z-v.z)
+    
+  def __isub__(self,v):
+    self.x-=v.x
+    self.y-=v.y
+    self.z-=v.z
+    return self
+    
+  def __neg__(self):
+    return Vector3(-self.x, -self.y, -self.z)
+  
+  def __mul__(self,a): # scalar or dot product
+    if isinstance(a,Vector3):
+      return self.x*a.x + self.y*a.y + self.z*a.z
+    else:
+      return Vector3(self.x*a, self.y*a, self.z*a)
+    
+  def __imul__(self,a):
+    if isinstance(a,Vector3): raise BadOperandError(self,a,'attempted in-place dot multiplication')
+    self.x*=a
+    self.y*=a
+    self.z*=a
+    return self
+    
+  def __rmul__(self,a):
+    return self.__mul__(a)
+    
+  def __truediv__(self,a):
+    return Vector3(self.x/a, self.y/a, self.z/a)
+    
+  def __itruediv__(self,a):
+    self.x/=a
+    self.y/=a
+    self.z/=a
+    return self
+  
+  def __floordiv__(self,a):
+    return Vector3(self.x//a, self.y//a, self.z//a)
+    
+  def __ifloordiv__(self,a):
+    self.x//=a
+    self.y//=a
+    self.z//=a
+    return self
+    
+  def __xor__(self,v): # vector cross product
+    return Vector3(self.y*v.z-self.z*v.y, self.z*v.x-self.x*v.z, self.x*v.y-self.y*v.x)
+    
+  def __ixor__(self,v):
+    self.x=self.y*v.z-self.z*v.y
+    self.y=self.z*v.x-self.x*v.z
+    self.z=self.x*v.y-self.y*v.x
+    return self
+    
+  def __abs__(self):
+    return math.sqrt(self.x*self.x + self.y*self.y + self.z*self.z)
+    
+  def __str__(self):
+    return str((self.x,self.y,self.z))
+    
+  def __repr__(self):
+    return 'Vector3'+self.__str__()
+    
+    
+class BadOperandError(ArithmeticError):
+  def __init__(self,u,v,message):
+    self.u=u
+    self.v=v
+    self.message=message