From 3c939a264e9403b4ecfd7249864424d26362bdc4 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Wed, 8 Feb 2017 23:17:26 +0000 Subject: [PATCH 01/19] Initial commit --- Input.py | 11 +++++++++++ Main.py | 0 Perceptron.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 Input.py create mode 100644 Main.py create mode 100644 Perceptron.py diff --git a/Input.py b/Input.py new file mode 100644 index 0000000..ae7c255 --- /dev/null +++ b/Input.py @@ -0,0 +1,11 @@ +class Input(object): + + def __init__(self, p_value, p_weight): + self.value = p_value + self.weight = p_weight + + def print(self): + print("value: %f\nweight: %f") % (self.value, self.weight) + + def setweight(self, p_weight): + self.weight = p_weight \ No newline at end of file diff --git a/Main.py b/Main.py new file mode 100644 index 0000000..e69de29 diff --git a/Perceptron.py b/Perceptron.py new file mode 100644 index 0000000..820a11f --- /dev/null +++ b/Perceptron.py @@ -0,0 +1,18 @@ +from Input import Input + + +class Perceptron(object): + + def __init__(self): + self.inputs = {} + + def print(self): + for key, value in self.inputs: + print("%s - )") % key + value.print() + + def addinput(self, p_name, p_value, p_weight): + self.inputs[p_name] = Input(p_value, p_weight) + + def setweight(self, p_input, p_weight): + self.inputs[p_input].weight = p_weight From 8fc14e1e0064768e15a91fea86c0dfd1835f826e Mon Sep 17 00:00:00 2001 From: George Lacey Date: Thu, 9 Feb 2017 19:04:52 +0000 Subject: [PATCH 02/19] Ignore IDE files --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc8a670 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/* \ No newline at end of file From 4edf73d6741c053283c3b4ca9624d42a1039dcdf Mon Sep 17 00:00:00 2001 From: George Lacey Date: Thu, 9 Feb 2017 19:05:32 +0000 Subject: [PATCH 03/19] Fix print method, generate output --- Input.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Input.py b/Input.py index ae7c255..1f1c49c 100644 --- a/Input.py +++ b/Input.py @@ -5,7 +5,10 @@ class Input(object): self.weight = p_weight def print(self): - print("value: %f\nweight: %f") % (self.value, self.weight) + print("value: %f\tweight: %f" % (self.value, self.weight)) - def setweight(self, p_weight): - self.weight = p_weight \ No newline at end of file + def set_weight(self, p_weight): + self.weight = p_weight + + def output(self): + return self.value * self.weight From 9e8d0d290677cd33c8fd8077d2cc07cf554318f5 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Thu, 9 Feb 2017 19:06:01 +0000 Subject: [PATCH 04/19] Add activation method, fix printing --- Perceptron.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/Perceptron.py b/Perceptron.py index 820a11f..327d17e 100644 --- a/Perceptron.py +++ b/Perceptron.py @@ -1,18 +1,32 @@ from Input import Input - class Perceptron(object): def __init__(self): self.inputs = {} def print(self): - for key, value in self.inputs: - print("%s - )") % key - value.print() + for key in self.inputs: + print(key + ":") + self.inputs[key].print() - def addinput(self, p_name, p_value, p_weight): + def add_input(self, p_name, p_value, p_weight): self.inputs[p_name] = Input(p_value, p_weight) - def setweight(self, p_input, p_weight): + def set_weight(self, p_input, p_weight): self.inputs[p_input].weight = p_weight + + def _sum_inputs(self): + total = 0 + for key, value in self.inputs.items(): + total += value.output() + return total + + def activation(self): + input = self._sum_inputs() + print("Total = %f" % input) + + if input > 0: + return True + else: + return False From 878c2c857a3bc49b3325685f7874bd1d3f5be9a8 Mon Sep 17 00:00:00 2001 From: George Lacey <490796@hull.ac.uk> Date: Fri, 10 Feb 2017 12:11:46 +0000 Subject: [PATCH 05/19] Create Data class --- Data.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Data.py diff --git a/Data.py b/Data.py new file mode 100644 index 0000000..29ff058 --- /dev/null +++ b/Data.py @@ -0,0 +1,6 @@ +class Data(object): + + def __init__(self, p_value1, p_value2, p_target): + self.value1 = p_value1 + self.value2 = p_value2 + self.target = p_target From 64286d6aa65ed2071fcf529a13a795b601937b46 Mon Sep 17 00:00:00 2001 From: George Lacey <490796@hull.ac.uk> Date: Fri, 10 Feb 2017 12:12:16 +0000 Subject: [PATCH 06/19] Create Dataset class --- Dataset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Dataset.py diff --git a/Dataset.py b/Dataset.py new file mode 100644 index 0000000..864ee5b --- /dev/null +++ b/Dataset.py @@ -0,0 +1,16 @@ +from Data import Data + + +class Dataset(object): + + def __init__(self, p_input, p_target): + self.inputs = {} + + for index, item in enumerate(p_input): + input1 = float(p_input[index].split(',')[0].strip()) + input2 = float(p_input[index].split(',')[1].strip()) + if p_target[index] == "1": + output = 1 + else: + output = -1 + self.inputs[index] = Data(input1, input2, output) From d91911691afddba8a3aba0e2766259135446a8cf Mon Sep 17 00:00:00 2001 From: George Lacey <490796@hull.ac.uk> Date: Fri, 10 Feb 2017 12:12:44 +0000 Subject: [PATCH 07/19] Implement guess method --- Perceptron.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Perceptron.py b/Perceptron.py index 327d17e..f499cc0 100644 --- a/Perceptron.py +++ b/Perceptron.py @@ -27,6 +27,11 @@ class Perceptron(object): print("Total = %f" % input) if input > 0: - return True + return 1 else: - return False + return -1 + + def guess(self, p_desired): + prediction = float(self.activation()) + return p_desired - prediction + From e4accc5927f73fa34444ed0f4ff9d838b15bf724 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:16:53 +0000 Subject: [PATCH 08/19] Add Main --- Main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Main.py b/Main.py index e69de29..7bcc4db 100644 --- a/Main.py +++ b/Main.py @@ -0,0 +1,16 @@ +from Perceptron import Perceptron +from random import Random +from Dataset import Dataset + +rand = Random() +data = Dataset(open("input.txt").read().split('\n'), open("target.txt").read().split('\n')) + + +for key in data.inputs: + i = data.inputs + print("v1: ", i[key].value1, "v2: ", i[key].value2, "target: ", i[key].target) + p = Perceptron() + p.add_input("1", i[key].value1, rand.uniform(-1, 1)) + p.add_input("2", i[key].value2, rand.uniform(-1, 1)) + p.add_input("bias", 1, rand.uniform(-1, 1)) + print("%f" % p.guess(i[key].target)) From a950e3e99c6000ad4b26db2ea0845c00886c0a86 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:12:16 +0000 Subject: [PATCH 09/19] Create Dataset class --- Dataset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Dataset.py diff --git a/Dataset.py b/Dataset.py new file mode 100644 index 0000000..864ee5b --- /dev/null +++ b/Dataset.py @@ -0,0 +1,16 @@ +from Data import Data + + +class Dataset(object): + + def __init__(self, p_input, p_target): + self.inputs = {} + + for index, item in enumerate(p_input): + input1 = float(p_input[index].split(',')[0].strip()) + input2 = float(p_input[index].split(',')[1].strip()) + if p_target[index] == "1": + output = 1 + else: + output = -1 + self.inputs[index] = Data(input1, input2, output) From d1b8f731d418af87c1d204b7fa6237d6191ddfd2 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:12:44 +0000 Subject: [PATCH 10/19] Implement guess method --- Perceptron.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Perceptron.py b/Perceptron.py index 327d17e..f499cc0 100644 --- a/Perceptron.py +++ b/Perceptron.py @@ -27,6 +27,11 @@ class Perceptron(object): print("Total = %f" % input) if input > 0: - return True + return 1 else: - return False + return -1 + + def guess(self, p_desired): + prediction = float(self.activation()) + return p_desired - prediction + From de0be12ccbce5f313f47bd5b1056f4cba01f8255 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:16:53 +0000 Subject: [PATCH 11/19] Implement basic functionality --- Main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Main.py b/Main.py index e69de29..7bcc4db 100644 --- a/Main.py +++ b/Main.py @@ -0,0 +1,16 @@ +from Perceptron import Perceptron +from random import Random +from Dataset import Dataset + +rand = Random() +data = Dataset(open("input.txt").read().split('\n'), open("target.txt").read().split('\n')) + + +for key in data.inputs: + i = data.inputs + print("v1: ", i[key].value1, "v2: ", i[key].value2, "target: ", i[key].target) + p = Perceptron() + p.add_input("1", i[key].value1, rand.uniform(-1, 1)) + p.add_input("2", i[key].value2, rand.uniform(-1, 1)) + p.add_input("bias", 1, rand.uniform(-1, 1)) + print("%f" % p.guess(i[key].target)) From ba0f5036057eda287480a7d744978308d39709dd Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:12:16 +0000 Subject: [PATCH 12/19] Create Dataset class --- Dataset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Dataset.py diff --git a/Dataset.py b/Dataset.py new file mode 100644 index 0000000..864ee5b --- /dev/null +++ b/Dataset.py @@ -0,0 +1,16 @@ +from Data import Data + + +class Dataset(object): + + def __init__(self, p_input, p_target): + self.inputs = {} + + for index, item in enumerate(p_input): + input1 = float(p_input[index].split(',')[0].strip()) + input2 = float(p_input[index].split(',')[1].strip()) + if p_target[index] == "1": + output = 1 + else: + output = -1 + self.inputs[index] = Data(input1, input2, output) From 833560968ebcddb10bb36f4b0c39369d8098346f Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:12:44 +0000 Subject: [PATCH 13/19] Implement guess method --- Perceptron.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Perceptron.py b/Perceptron.py index 327d17e..f499cc0 100644 --- a/Perceptron.py +++ b/Perceptron.py @@ -27,6 +27,11 @@ class Perceptron(object): print("Total = %f" % input) if input > 0: - return True + return 1 else: - return False + return -1 + + def guess(self, p_desired): + prediction = float(self.activation()) + return p_desired - prediction + From e75627010d0e39366866707dff4adfe86639b8a6 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 12:16:53 +0000 Subject: [PATCH 14/19] Implement basic functionality --- Main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Main.py b/Main.py index e69de29..7bcc4db 100644 --- a/Main.py +++ b/Main.py @@ -0,0 +1,16 @@ +from Perceptron import Perceptron +from random import Random +from Dataset import Dataset + +rand = Random() +data = Dataset(open("input.txt").read().split('\n'), open("target.txt").read().split('\n')) + + +for key in data.inputs: + i = data.inputs + print("v1: ", i[key].value1, "v2: ", i[key].value2, "target: ", i[key].target) + p = Perceptron() + p.add_input("1", i[key].value1, rand.uniform(-1, 1)) + p.add_input("2", i[key].value2, rand.uniform(-1, 1)) + p.add_input("bias", 1, rand.uniform(-1, 1)) + print("%f" % p.guess(i[key].target)) From d00a8312cc541ac3b52569c62c0a51ce8b17bfee Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 13:22:34 +0000 Subject: [PATCH 15/19] Ignore __pycache__ directory --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bc8a670..43244de 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea/* \ No newline at end of file +.idea/* +__pycache__/* \ No newline at end of file From 79694344a5855d47f178c10ace4149fd5a4b7b69 Mon Sep 17 00:00:00 2001 From: George Lacey Date: Fri, 10 Feb 2017 13:51:00 +0000 Subject: [PATCH 16/19] Add input method and implement learning constant --- Perceptron.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Perceptron.py b/Perceptron.py index f499cc0..382867f 100644 --- a/Perceptron.py +++ b/Perceptron.py @@ -1,5 +1,6 @@ from Input import Input + class Perceptron(object): def __init__(self): @@ -10,6 +11,10 @@ class Perceptron(object): print(key + ":") self.inputs[key].print() + def input(self, p_input1, p_input2): + self.inputs[0] = p_input1 + self.inputs[1] = p_input2 + def add_input(self, p_name, p_value, p_weight): self.inputs[p_name] = Input(p_value, p_weight) @@ -24,14 +29,16 @@ class Perceptron(object): def activation(self): input = self._sum_inputs() - print("Total = %f" % input) + print("Sum of inputs = %f" % input) if input > 0: return 1 else: return -1 - def guess(self, p_desired): + def guess(self, p_desired, p_learning_constant): prediction = float(self.activation()) - return p_desired - prediction - + error = p_desired - prediction + for key in self.inputs: + self.inputs[key].weight += error * self.inputs[key].value * p_learning_constant + return error From 6a3253df568bed6c11b940c404cec14e66e2704f Mon Sep 17 00:00:00 2001 From: George Lacey <490796@hull.ac.uk> Date: Fri, 10 Feb 2017 15:31:47 +0000 Subject: [PATCH 17/19] Fix input method --- Perceptron.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Perceptron.py b/Perceptron.py index 382867f..3d5a31d 100644 --- a/Perceptron.py +++ b/Perceptron.py @@ -12,8 +12,8 @@ class Perceptron(object): self.inputs[key].print() def input(self, p_input1, p_input2): - self.inputs[0] = p_input1 - self.inputs[1] = p_input2 + self.inputs['1'].value = float(p_input1) + self.inputs['2'].value = float(p_input2) def add_input(self, p_name, p_value, p_weight): self.inputs[p_name] = Input(p_value, p_weight) @@ -23,8 +23,8 @@ class Perceptron(object): def _sum_inputs(self): total = 0 - for key, value in self.inputs.items(): - total += value.output() + for key in self.inputs: + total += self.inputs[key].output() return total def activation(self): From f274146d58d7cf7cb673bda4a041a0dfae5a906a Mon Sep 17 00:00:00 2001 From: George Lacey <490796@hull.ac.uk> Date: Fri, 10 Feb 2017 15:32:06 +0000 Subject: [PATCH 18/19] Implement training sequence --- Main.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Main.py b/Main.py index 7bcc4db..5ce31e3 100644 --- a/Main.py +++ b/Main.py @@ -4,13 +4,26 @@ from Dataset import Dataset rand = Random() data = Dataset(open("input.txt").read().split('\n'), open("target.txt").read().split('\n')) +learn_rate = 0.01 +p = Perceptron() +i = data.inputs +p.add_input("1", 1, rand.uniform(-1, 1)) +p.add_input("2", 1, rand.uniform(-1, 1)) +p.add_input("bias", 1, rand.uniform(-1, 1)) -for key in data.inputs: +# train +for ind in range(0, 500): i = data.inputs - print("v1: ", i[key].value1, "v2: ", i[key].value2, "target: ", i[key].target) - p = Perceptron() - p.add_input("1", i[key].value1, rand.uniform(-1, 1)) - p.add_input("2", i[key].value2, rand.uniform(-1, 1)) - p.add_input("bias", 1, rand.uniform(-1, 1)) - print("%f" % p.guess(i[key].target)) + for key in data.inputs: + p.input(i[key].value1, i[key].value2) + print("error: %f" % p.guess(i[key].target, learn_rate)) + +for i in range(0, 1000): + x = input("Arg1") + y = input("Arg2") + p.input(x, y) + if p.activation() == 1: + print("TRUE") + else: + print("FALSE") \ No newline at end of file From 0511c5d7d3c591aa86fdc8e6da6dc0ea80bb5f1a Mon Sep 17 00:00:00 2001 From: George Lacey <490796@hull.ac.uk> Date: Fri, 10 Feb 2017 17:17:22 +0000 Subject: [PATCH 19/19] Add graph drawing functionality --- Graph.py | 15 + Main.py | 7 +- graphics.py | 1001 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1021 insertions(+), 2 deletions(-) create mode 100644 Graph.py create mode 100644 graphics.py diff --git a/Graph.py b/Graph.py new file mode 100644 index 0000000..5a7b1cd --- /dev/null +++ b/Graph.py @@ -0,0 +1,15 @@ +from graphics import * + +class Graph(object): + + def __init__(self, p_width, p_height, p_name): + self.win = GraphWin("perceptron", p_width, p_height) + self.win.setCoords(0, 0, 640, 480) + self.x_offset = p_width / 2 + self.y_offset = p_height / 2 + self.win.setCoords(0, 0, 640, 480) + Line(Point(0, p_height / 2), Point(p_width, p_height / 2)).draw(self.win) + Line(Point(p_width / 2, 0), Point(p_width / 2, p_height)).draw(self.win) + + def drawLine(self, p_point1, p_point2): + Line(p_point1, p_point2).draw(self.win).move(self.x_offset, self.y_offset) diff --git a/Main.py b/Main.py index 5ce31e3..04259b9 100644 --- a/Main.py +++ b/Main.py @@ -1,10 +1,13 @@ from Perceptron import Perceptron from random import Random from Dataset import Dataset +from Graph import * rand = Random() data = Dataset(open("input.txt").read().split('\n'), open("target.txt").read().split('\n')) learn_rate = 0.01 +graph = Graph(640, 480, "Perceptron") +graph.drawLine(Point(-1000, -1000 + 20), Point(1000, 1000 + 20)) p = Perceptron() i = data.inputs @@ -20,8 +23,8 @@ for ind in range(0, 500): print("error: %f" % p.guess(i[key].target, learn_rate)) for i in range(0, 1000): - x = input("Arg1") - y = input("Arg2") + x = input("Arg1: ") + y = input("Arg2: ") p.input(x, y) if p.activation() == 1: print("TRUE") diff --git a/graphics.py b/graphics.py new file mode 100644 index 0000000..d59d796 --- /dev/null +++ b/graphics.py @@ -0,0 +1,1001 @@ +# graphics.py +"""Simple object oriented graphics library + +The library is designed to make it very easy for novice programmers to +experiment with computer graphics in an object oriented fashion. It is +written by John Zelle for use with the book "Python Programming: An +Introduction to Computer Science" (Franklin, Beedle & Associates). + +LICENSE: This is open-source software released under the terms of the +GPL (http://www.gnu.org/licenses/gpl.html). + +PLATFORMS: The package is a wrapper around Tkinter and should run on +any platform where Tkinter is available. + +INSTALLATION: Put this file somewhere where Python can see it. + +OVERVIEW: There are two kinds of objects in the library. The GraphWin +class implements a window where drawing can be done and various +GraphicsObjects are provided that can be drawn into a GraphWin. As a +simple example, here is a complete program to draw a circle of radius +10 centered in a 100x100 window: + +-------------------------------------------------------------------- +from graphics import * + +def main(): + win = GraphWin("My Circle", 100, 100) + c = Circle(Point(50,50), 10) + c.draw(win) + win.getMouse() # Pause to view result + win.close() # Close window when done + +main() +-------------------------------------------------------------------- +GraphWin objects support coordinate transformation through the +setCoords method and mouse and keyboard interaction methods. + +The library provides the following graphical objects: + Point + Line + Circle + Oval + Rectangle + Polygon + Text + Entry (for text-based input) + Image + +Various attributes of graphical objects can be set such as +outline-color, fill-color and line-width. Graphical objects also +support moving and hiding for animation effects. + +The library also provides a very simple class for pixel-based image +manipulation, Pixmap. A pixmap can be loaded from a file and displayed +using an Image object. Both getPixel and setPixel methods are provided +for manipulating the image. + +DOCUMENTATION: For complete documentation, see Chapter 4 of "Python +Programming: An Introduction to Computer Science" by John Zelle, +published by Franklin, Beedle & Associates. Also see +http://mcsp.wartburg.edu/zelle/python for a quick reference""" + +__version__ = "5.0" + +# Version 5 8/26/2016 +# * update at bottom to fix MacOS issue causing askopenfile() to hang +# * update takes an optional parameter specifying update rate +# * Entry objects get focus when drawn +# * __repr_ for all objects +# * fixed offset problem in window, made canvas borderless + +# Version 4.3 4/25/2014 +# * Fixed Image getPixel to work with Python 3.4, TK 8.6 (tuple type handling) +# * Added interactive keyboard input (getKey and checkKey) to GraphWin +# * Modified setCoords to cause redraw of current objects, thus +# changing the view. This supports scrolling around via setCoords. +# +# Version 4.2 5/26/2011 +# * Modified Image to allow multiple undraws like other GraphicsObjects +# Version 4.1 12/29/2009 +# * Merged Pixmap and Image class. Old Pixmap removed, use Image. +# Version 4.0.1 10/08/2009 +# * Modified the autoflush on GraphWin to default to True +# * Autoflush check on close, setBackground +# * Fixed getMouse to flush pending clicks at entry +# Version 4.0 08/2009 +# * Reverted to non-threaded version. The advantages (robustness, +# efficiency, ability to use with other Tk code, etc.) outweigh +# the disadvantage that interactive use with IDLE is slightly more +# cumbersome. +# * Modified to run in either Python 2.x or 3.x (same file). +# * Added Image.getPixmap() +# * Added update() -- stand alone function to cause any pending +# graphics changes to display. +# +# Version 3.4 10/16/07 +# Fixed GraphicsError to avoid "exploded" error messages. +# Version 3.3 8/8/06 +# Added checkMouse method to GraphWin +# Version 3.2.3 +# Fixed error in Polygon init spotted by Andrew Harrington +# Fixed improper threading in Image constructor +# Version 3.2.2 5/30/05 +# Cleaned up handling of exceptions in Tk thread. The graphics package +# now raises an exception if attempt is made to communicate with +# a dead Tk thread. +# Version 3.2.1 5/22/05 +# Added shutdown function for tk thread to eliminate race-condition +# error "chatter" when main thread terminates +# Renamed various private globals with _ +# Version 3.2 5/4/05 +# Added Pixmap object for simple image manipulation. +# Version 3.1 4/13/05 +# Improved the Tk thread communication so that most Tk calls +# do not have to wait for synchonization with the Tk thread. +# (see _tkCall and _tkExec) +# Version 3.0 12/30/04 +# Implemented Tk event loop in separate thread. Should now work +# interactively with IDLE. Undocumented autoflush feature is +# no longer necessary. Its default is now False (off). It may +# be removed in a future version. +# Better handling of errors regarding operations on windows that +# have been closed. +# Addition of an isClosed method to GraphWindow class. + +# Version 2.2 8/26/04 +# Fixed cloning bug reported by Joseph Oldham. +# Now implements deep copy of config info. +# Version 2.1 1/15/04 +# Added autoflush option to GraphWin. When True (default) updates on +# the window are done after each action. This makes some graphics +# intensive programs sluggish. Turning off autoflush causes updates +# to happen during idle periods or when flush is called. +# Version 2.0 +# Updated Documentation +# Made Polygon accept a list of Points in constructor +# Made all drawing functions call TK update for easier animations +# and to make the overall package work better with +# Python 2.3 and IDLE 1.0 under Windows (still some issues). +# Removed vestigial turtle graphics. +# Added ability to configure font for Entry objects (analogous to Text) +# Added setTextColor for Text as an alias of setFill +# Changed to class-style exceptions +# Fixed cloning of Text objects + +# Version 1.6 +# Fixed Entry so StringVar uses _root as master, solves weird +# interaction with shell in Idle +# Fixed bug in setCoords. X and Y coordinates can increase in +# "non-intuitive" direction. +# Tweaked wm_protocol so window is not resizable and kill box closes. + +# Version 1.5 +# Fixed bug in Entry. Can now define entry before creating a +# GraphWin. All GraphWins are now toplevel windows and share +# a fixed root (called _root). + +# Version 1.4 +# Fixed Garbage collection of Tkinter images bug. +# Added ability to set text atttributes. +# Added Entry boxes. + +import time, os, sys + +try: # import as appropriate for 2.x vs. 3.x + import tkinter as tk +except: + import Tkinter as tk + + +########################################################################## +# Module Exceptions + +class GraphicsError(Exception): + """Generic error class for graphics module exceptions.""" + pass + + +OBJ_ALREADY_DRAWN = "Object currently drawn" +UNSUPPORTED_METHOD = "Object doesn't support operation" +BAD_OPTION = "Illegal option value" + +########################################################################## +# global variables and funtions + +_root = tk.Tk() +_root.withdraw() + +_update_lasttime = time.time() + + +def update(rate=None): + global _update_lasttime + if rate: + now = time.time() + pauseLength = 1 / rate - (now - _update_lasttime) + if pauseLength > 0: + time.sleep(pauseLength) + _update_lasttime = now + pauseLength + else: + _update_lasttime = now + + _root.update() + + +############################################################################ +# Graphics classes start here + +class GraphWin(tk.Canvas): + """A GraphWin is a toplevel window for displaying graphics.""" + + def __init__(self, title="Graphics Window", + width=200, height=200, autoflush=True): + assert type(title) == type(""), "Title must be a string" + master = tk.Toplevel(_root) + master.protocol("WM_DELETE_WINDOW", self.close) + tk.Canvas.__init__(self, master, width=width, height=height, + highlightthickness=0, bd=0) + self.master.title(title) + self.pack() + master.resizable(0, 0) + self.foreground = "black" + self.items = [] + self.mouseX = None + self.mouseY = None + self.bind("", self._onClick) + self.bind_all("", self._onKey) + self.height = int(height) + self.width = int(width) + self.autoflush = autoflush + self._mouseCallback = None + self.trans = None + self.closed = False + master.lift() + self.lastKey = "" + if autoflush: _root.update() + + def __repr__(self): + if self.isClosed(): + return "" + else: + return "GraphWin('{}', {}, {})".format(self.master.title(), + self.getWidth(), + self.getHeight()) + + def __str__(self): + return repr(self) + + def __checkOpen(self): + if self.closed: + raise GraphicsError("window is closed") + + def _onKey(self, evnt): + self.lastKey = evnt.keysym + + def setBackground(self, color): + """Set background color of the window""" + self.__checkOpen() + self.config(bg=color) + self.__autoflush() + + def setCoords(self, x1, y1, x2, y2): + """Set coordinates of window to run from (x1,y1) in the + lower-left corner to (x2,y2) in the upper-right corner.""" + self.trans = Transform(self.width, self.height, x1, y1, x2, y2) + self.redraw() + + def close(self): + """Close the window""" + + if self.closed: return + self.closed = True + self.master.destroy() + self.__autoflush() + + def isClosed(self): + return self.closed + + def isOpen(self): + return not self.closed + + def __autoflush(self): + if self.autoflush: + _root.update() + + def plot(self, x, y, color="black"): + """Set pixel (x,y) to the given color""" + self.__checkOpen() + xs, ys = self.toScreen(x, y) + self.create_line(xs, ys, xs + 1, ys, fill=color) + self.__autoflush() + + def plotPixel(self, x, y, color="black"): + """Set pixel raw (independent of window coordinates) pixel + (x,y) to color""" + self.__checkOpen() + self.create_line(x, y, x + 1, y, fill=color) + self.__autoflush() + + def flush(self): + """Update drawing to the window""" + self.__checkOpen() + self.update_idletasks() + + def getMouse(self): + """Wait for mouse click and return Point object representing + the click""" + self.update() # flush any prior clicks + self.mouseX = None + self.mouseY = None + while self.mouseX == None or self.mouseY == None: + self.update() + if self.isClosed(): raise GraphicsError("getMouse in closed window") + time.sleep(.1) # give up thread + x, y = self.toWorld(self.mouseX, self.mouseY) + self.mouseX = None + self.mouseY = None + return Point(x, y) + + def checkMouse(self): + """Return last mouse click or None if mouse has + not been clicked since last call""" + if self.isClosed(): + raise GraphicsError("checkMouse in closed window") + self.update() + if self.mouseX != None and self.mouseY != None: + x, y = self.toWorld(self.mouseX, self.mouseY) + self.mouseX = None + self.mouseY = None + return Point(x, y) + else: + return None + + def getKey(self): + """Wait for user to press a key and return it as a string.""" + self.lastKey = "" + while self.lastKey == "": + self.update() + if self.isClosed(): raise GraphicsError("getKey in closed window") + time.sleep(.1) # give up thread + + key = self.lastKey + self.lastKey = "" + return key + + def checkKey(self): + """Return last key pressed or None if no key pressed since last call""" + if self.isClosed(): + raise GraphicsError("checkKey in closed window") + self.update() + key = self.lastKey + self.lastKey = "" + return key + + def getHeight(self): + """Return the height of the window""" + return self.height + + def getWidth(self): + """Return the width of the window""" + return self.width + + def toScreen(self, x, y): + trans = self.trans + if trans: + return self.trans.screen(x, y) + else: + return x, y + + def toWorld(self, x, y): + trans = self.trans + if trans: + return self.trans.world(x, y) + else: + return x, y + + def setMouseHandler(self, func): + self._mouseCallback = func + + def _onClick(self, e): + self.mouseX = e.x + self.mouseY = e.y + if self._mouseCallback: + self._mouseCallback(Point(e.x, e.y)) + + def addItem(self, item): + self.items.append(item) + + def delItem(self, item): + self.items.remove(item) + + def redraw(self): + for item in self.items[:]: + item.undraw() + item.draw(self) + self.update() + + +class Transform: + """Internal class for 2-D coordinate transformations""" + + def __init__(self, w, h, xlow, ylow, xhigh, yhigh): + # w, h are width and height of window + # (xlow,ylow) coordinates of lower-left [raw (0,h-1)] + # (xhigh,yhigh) coordinates of upper-right [raw (w-1,0)] + xspan = (xhigh - xlow) + yspan = (yhigh - ylow) + self.xbase = xlow + self.ybase = yhigh + self.xscale = xspan / float(w - 1) + self.yscale = yspan / float(h - 1) + + def screen(self, x, y): + # Returns x,y in screen (actually window) coordinates + xs = (x - self.xbase) / self.xscale + ys = (self.ybase - y) / self.yscale + return int(xs + 0.5), int(ys + 0.5) + + def world(self, xs, ys): + # Returns xs,ys in world coordinates + x = xs * self.xscale + self.xbase + y = self.ybase - ys * self.yscale + return x, y + + +# Default values for various item configuration options. Only a subset of +# keys may be present in the configuration dictionary for a given item +DEFAULT_CONFIG = {"fill": "", + "outline": "black", + "width": "1", + "arrow": "none", + "text": "", + "justify": "center", + "font": ("helvetica", 12, "normal")} + + +class GraphicsObject: + """Generic base class for all of the drawable objects""" + + # A subclass of GraphicsObject should override _draw and + # and _move methods. + + def __init__(self, options): + # options is a list of strings indicating which options are + # legal for this object. + + # When an object is drawn, canvas is set to the GraphWin(canvas) + # object where it is drawn and id is the TK identifier of the + # drawn shape. + self.canvas = None + self.id = None + + # config is the dictionary of configuration options for the widget. + config = {} + for option in options: + config[option] = DEFAULT_CONFIG[option] + self.config = config + + def setFill(self, color): + """Set interior color to color""" + self._reconfig("fill", color) + + def setOutline(self, color): + """Set outline color to color""" + self._reconfig("outline", color) + + def setWidth(self, width): + """Set line weight to width""" + self._reconfig("width", width) + + def draw(self, graphwin): + + """Draw the object in graphwin, which should be a GraphWin + object. A GraphicsObject may only be drawn into one + window. Raises an error if attempt made to draw an object that + is already visible.""" + + if self.canvas and not self.canvas.isClosed(): raise GraphicsError(OBJ_ALREADY_DRAWN) + if graphwin.isClosed(): raise GraphicsError("Can't draw to closed window") + self.canvas = graphwin + self.id = self._draw(graphwin, self.config) + graphwin.addItem(self) + if graphwin.autoflush: + _root.update() + return self + + def undraw(self): + + """Undraw the object (i.e. hide it). Returns silently if the + object is not currently drawn.""" + + if not self.canvas: return + if not self.canvas.isClosed(): + self.canvas.delete(self.id) + self.canvas.delItem(self) + if self.canvas.autoflush: + _root.update() + self.canvas = None + self.id = None + + def move(self, dx, dy): + + """move object dx units in x direction and dy units in y + direction""" + + self._move(dx, dy) + canvas = self.canvas + if canvas and not canvas.isClosed(): + trans = canvas.trans + if trans: + x = dx / trans.xscale + y = -dy / trans.yscale + else: + x = dx + y = dy + self.canvas.move(self.id, x, y) + if canvas.autoflush: + _root.update() + + def _reconfig(self, option, setting): + # Internal method for changing configuration of the object + # Raises an error if the option does not exist in the config + # dictionary for this object + if option not in self.config: + raise GraphicsError(UNSUPPORTED_METHOD) + options = self.config + options[option] = setting + if self.canvas and not self.canvas.isClosed(): + self.canvas.itemconfig(self.id, options) + if self.canvas.autoflush: + _root.update() + + def _draw(self, canvas, options): + """draws appropriate figure on canvas with options provided + Returns Tk id of item drawn""" + pass # must override in subclass + + def _move(self, dx, dy): + """updates internal state of object to move it dx,dy units""" + pass # must override in subclass + + +class Point(GraphicsObject): + def __init__(self, x, y): + GraphicsObject.__init__(self, ["outline", "fill"]) + self.setFill = self.setOutline + self.x = float(x) + self.y = float(y) + + def __repr__(self): + return "Point({}, {})".format(self.x, self.y) + + def _draw(self, canvas, options): + x, y = canvas.toScreen(self.x, self.y) + return canvas.create_rectangle(x, y, x + 1, y + 1, options) + + def _move(self, dx, dy): + self.x = self.x + dx + self.y = self.y + dy + + def clone(self): + other = Point(self.x, self.y) + other.config = self.config.copy() + return other + + def getX(self): return self.x + + def getY(self): return self.y + + +class _BBox(GraphicsObject): + # Internal base class for objects represented by bounding box + # (opposite corners) Line segment is a degenerate case. + + def __init__(self, p1, p2, options=["outline", "width", "fill"]): + GraphicsObject.__init__(self, options) + self.p1 = p1.clone() + self.p2 = p2.clone() + + def _move(self, dx, dy): + self.p1.x = self.p1.x + dx + self.p1.y = self.p1.y + dy + self.p2.x = self.p2.x + dx + self.p2.y = self.p2.y + dy + + def getP1(self): return self.p1.clone() + + def getP2(self): return self.p2.clone() + + def getCenter(self): + p1 = self.p1 + p2 = self.p2 + return Point((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0) + + +class Rectangle(_BBox): + def __init__(self, p1, p2): + _BBox.__init__(self, p1, p2) + + def __repr__(self): + return "Rectangle({}, {})".format(str(self.p1), str(self.p2)) + + def _draw(self, canvas, options): + p1 = self.p1 + p2 = self.p2 + x1, y1 = canvas.toScreen(p1.x, p1.y) + x2, y2 = canvas.toScreen(p2.x, p2.y) + return canvas.create_rectangle(x1, y1, x2, y2, options) + + def clone(self): + other = Rectangle(self.p1, self.p2) + other.config = self.config.copy() + return other + + +class Oval(_BBox): + def __init__(self, p1, p2): + _BBox.__init__(self, p1, p2) + + def __repr__(self): + return "Oval({}, {})".format(str(self.p1), str(self.p2)) + + def clone(self): + other = Oval(self.p1, self.p2) + other.config = self.config.copy() + return other + + def _draw(self, canvas, options): + p1 = self.p1 + p2 = self.p2 + x1, y1 = canvas.toScreen(p1.x, p1.y) + x2, y2 = canvas.toScreen(p2.x, p2.y) + return canvas.create_oval(x1, y1, x2, y2, options) + + +class Circle(Oval): + def __init__(self, center, radius): + p1 = Point(center.x - radius, center.y - radius) + p2 = Point(center.x + radius, center.y + radius) + Oval.__init__(self, p1, p2) + self.radius = radius + + def __repr__(self): + return "Circle({}, {})".format(str(self.getCenter()), str(self.radius)) + + def clone(self): + other = Circle(self.getCenter(), self.radius) + other.config = self.config.copy() + return other + + def getRadius(self): + return self.radius + + +class Line(_BBox): + def __init__(self, p1, p2): + _BBox.__init__(self, p1, p2, ["arrow", "fill", "width"]) + self.setFill(DEFAULT_CONFIG['outline']) + self.setOutline = self.setFill + + def __repr__(self): + return "Line({}, {})".format(str(self.p1), str(self.p2)) + + def clone(self): + other = Line(self.p1, self.p2) + other.config = self.config.copy() + return other + + def _draw(self, canvas, options): + p1 = self.p1 + p2 = self.p2 + x1, y1 = canvas.toScreen(p1.x, p1.y) + x2, y2 = canvas.toScreen(p2.x, p2.y) + return canvas.create_line(x1, y1, x2, y2, options) + + def setArrow(self, option): + if not option in ["first", "last", "both", "none"]: + raise GraphicsError(BAD_OPTION) + self._reconfig("arrow", option) + + +class Polygon(GraphicsObject): + def __init__(self, *points): + # if points passed as a list, extract it + if len(points) == 1 and type(points[0]) == type([]): + points = points[0] + self.points = list(map(Point.clone, points)) + GraphicsObject.__init__(self, ["outline", "width", "fill"]) + + def __repr__(self): + return "Polygon" + str(tuple(p for p in self.points)) + + def clone(self): + other = Polygon(*self.points) + other.config = self.config.copy() + return other + + def getPoints(self): + return list(map(Point.clone, self.points)) + + def _move(self, dx, dy): + for p in self.points: + p.move(dx, dy) + + def _draw(self, canvas, options): + args = [canvas] + for p in self.points: + x, y = canvas.toScreen(p.x, p.y) + args.append(x) + args.append(y) + args.append(options) + return GraphWin.create_polygon(*args) + + +class Text(GraphicsObject): + def __init__(self, p, text): + GraphicsObject.__init__(self, ["justify", "fill", "text", "font"]) + self.setText(text) + self.anchor = p.clone() + self.setFill(DEFAULT_CONFIG['outline']) + self.setOutline = self.setFill + + def __repr__(self): + return "Text({}, '{}')".format(self.anchor, self.getText()) + + def _draw(self, canvas, options): + p = self.anchor + x, y = canvas.toScreen(p.x, p.y) + return canvas.create_text(x, y, options) + + def _move(self, dx, dy): + self.anchor.move(dx, dy) + + def clone(self): + other = Text(self.anchor, self.config['text']) + other.config = self.config.copy() + return other + + def setText(self, text): + self._reconfig("text", text) + + def getText(self): + return self.config["text"] + + def getAnchor(self): + return self.anchor.clone() + + def setFace(self, face): + if face in ['helvetica', 'arial', 'courier', 'times roman']: + f, s, b = self.config['font'] + self._reconfig("font", (face, s, b)) + else: + raise GraphicsError(BAD_OPTION) + + def setSize(self, size): + if 5 <= size <= 36: + f, s, b = self.config['font'] + self._reconfig("font", (f, size, b)) + else: + raise GraphicsError(BAD_OPTION) + + def setStyle(self, style): + if style in ['bold', 'normal', 'italic', 'bold italic']: + f, s, b = self.config['font'] + self._reconfig("font", (f, s, style)) + else: + raise GraphicsError(BAD_OPTION) + + def setTextColor(self, color): + self.setFill(color) + + +class Entry(GraphicsObject): + def __init__(self, p, width): + GraphicsObject.__init__(self, []) + self.anchor = p.clone() + # print self.anchor + self.width = width + self.text = tk.StringVar(_root) + self.text.set("") + self.fill = "gray" + self.color = "black" + self.font = DEFAULT_CONFIG['font'] + self.entry = None + + def __repr__(self): + return "Entry({}, {})".format(self.anchor, self.width) + + def _draw(self, canvas, options): + p = self.anchor + x, y = canvas.toScreen(p.x, p.y) + frm = tk.Frame(canvas.master) + self.entry = tk.Entry(frm, + width=self.width, + textvariable=self.text, + bg=self.fill, + fg=self.color, + font=self.font) + self.entry.pack() + # self.setFill(self.fill) + self.entry.focus_set() + return canvas.create_window(x, y, window=frm) + + def getText(self): + return self.text.get() + + def _move(self, dx, dy): + self.anchor.move(dx, dy) + + def getAnchor(self): + return self.anchor.clone() + + def clone(self): + other = Entry(self.anchor, self.width) + other.config = self.config.copy() + other.text = tk.StringVar() + other.text.set(self.text.get()) + other.fill = self.fill + return other + + def setText(self, t): + self.text.set(t) + + def setFill(self, color): + self.fill = color + if self.entry: + self.entry.config(bg=color) + + def _setFontComponent(self, which, value): + font = list(self.font) + font[which] = value + self.font = tuple(font) + if self.entry: + self.entry.config(font=self.font) + + def setFace(self, face): + if face in ['helvetica', 'arial', 'courier', 'times roman']: + self._setFontComponent(0, face) + else: + raise GraphicsError(BAD_OPTION) + + def setSize(self, size): + if 5 <= size <= 36: + self._setFontComponent(1, size) + else: + raise GraphicsError(BAD_OPTION) + + def setStyle(self, style): + if style in ['bold', 'normal', 'italic', 'bold italic']: + self._setFontComponent(2, style) + else: + raise GraphicsError(BAD_OPTION) + + def setTextColor(self, color): + self.color = color + if self.entry: + self.entry.config(fg=color) + + +class Image(GraphicsObject): + idCount = 0 + imageCache = {} # tk photoimages go here to avoid GC while drawn + + def __init__(self, p, *pixmap): + GraphicsObject.__init__(self, []) + self.anchor = p.clone() + self.imageId = Image.idCount + Image.idCount = Image.idCount + 1 + if len(pixmap) == 1: # file name provided + self.img = tk.PhotoImage(file=pixmap[0], master=_root) + else: # width and height provided + width, height = pixmap + self.img = tk.PhotoImage(master=_root, width=width, height=height) + + def __repr__(self): + return "Image({}, {}, {})".format(self.anchor, self.getWidth(), self.getHeight()) + + def _draw(self, canvas, options): + p = self.anchor + x, y = canvas.toScreen(p.x, p.y) + self.imageCache[self.imageId] = self.img # save a reference + return canvas.create_image(x, y, image=self.img) + + def _move(self, dx, dy): + self.anchor.move(dx, dy) + + def undraw(self): + try: + del self.imageCache[self.imageId] # allow gc of tk photoimage + except KeyError: + pass + GraphicsObject.undraw(self) + + def getAnchor(self): + return self.anchor.clone() + + def clone(self): + other = Image(Point(0, 0), 0, 0) + other.img = self.img.copy() + other.anchor = self.anchor.clone() + other.config = self.config.copy() + return other + + def getWidth(self): + """Returns the width of the image in pixels""" + return self.img.width() + + def getHeight(self): + """Returns the height of the image in pixels""" + return self.img.height() + + def getPixel(self, x, y): + """Returns a list [r,g,b] with the RGB color values for pixel (x,y) + r,g,b are in range(256) + + """ + + value = self.img.get(x, y) + if type(value) == type(0): + return [value, value, value] + elif type(value) == type((0, 0, 0)): + return list(value) + else: + return list(map(int, value.split())) + + def setPixel(self, x, y, color): + """Sets pixel (x,y) to the given color + + """ + self.img.put("{" + color + "}", (x, y)) + + def save(self, filename): + """Saves the pixmap image to filename. + The format for the save image is determined from the filname extension. + + """ + + path, name = os.path.split(filename) + ext = name.split(".")[-1] + self.img.write(filename, format=ext) + + +def color_rgb(r, g, b): + """r,g,b are intensities of red, green, and blue in range(256) + Returns color specifier string for the resulting color""" + return "#%02x%02x%02x" % (r, g, b) + + +def test(): + win = GraphWin() + win.setCoords(0, 0, 10, 10) + t = Text(Point(5, 5), "Centered Text") + t.draw(win) + p = Polygon(Point(1, 1), Point(5, 3), Point(2, 7)) + p.draw(win) + e = Entry(Point(5, 6), 10) + e.draw(win) + win.getMouse() + p.setFill("red") + p.setOutline("blue") + p.setWidth(2) + s = "" + for pt in p.getPoints(): + s = s + "(%0.1f,%0.1f) " % (pt.getX(), pt.getY()) + t.setText(e.getText()) + e.setFill("green") + e.setText("Spam!") + e.move(2, 0) + win.getMouse() + p.move(2, 3) + s = "" + for pt in p.getPoints(): + s = s + "(%0.1f,%0.1f) " % (pt.getX(), pt.getY()) + t.setText(s) + win.getMouse() + p.undraw() + e.undraw() + t.setStyle("bold") + win.getMouse() + t.setStyle("normal") + win.getMouse() + t.setStyle("italic") + win.getMouse() + t.setStyle("bold italic") + win.getMouse() + t.setSize(14) + win.getMouse() + t.setFace("arial") + t.setSize(20) + win.getMouse() + win.close() + + +# MacOS fix 2 +# tk.Toplevel(_root).destroy() + +# MacOS fix 1 +update() + +if __name__ == "__main__": + test() \ No newline at end of file