HOME/Articles/

pil example neural v08 (snippet)

Article Outline

Python pil example 'neural v08'

Functions in program:

  • def clearAll_action(sender):
  • def clear_action(sender):
  • def guess_action(sender):
  • def showTrainData(i,n):
  • def showLearning(i,v):
  • def trainNN():
  • def updateLearninImage(img):
  • def train_action(sender):
  • def pil2ui(pil_image):
  • def ui2pil(ui_img):
  • def snapshot(view):
  • def vector2img(v):
  • def getVector(k, dx=0, dy=0, theta=0, z=1.0):
  • def zoom(img, z):

Modules used in program:

  • import objc_util
  • import console, math
  • import matplotlib.pyplot as plt
  • import numpy as np
  • import ui, io

python neural v08

Python pil example: neural v08

import ui, io
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image as PILImage
from PIL import ImageChops as chops
import console, math
import objc_util

###########################################################################
# history
# v01: 1/format output. 2/Landscape view.
# v02: 1/format output in % certainty. 2/ move templates by -a/0/+a in x and y, a =5
#      3/adjusted learning rate by x0.02 and learning epochs to 200
#       https://gist.github.com/d87a0833a64f0128a12c59547984ad2f
# v03: 1/put 2 neurons in output, to compare reliabilities 
#      2/show the bestloss (check bad learning)
#      3/random seed before weight initilizalization (to have another chance when learning is wrong)
#      4/added rotation by -th/0/+th in learning
#      5/learning is getting long: limit to 100 epoch, and stop when bestloss<0.002
#      https://gist.github.com/e373904d3ccba03803d80173f44b5eee
# v04: 1/ introducing a Layer class
#      2/ modified NN class to work with various layer numbers
#      https://gist.github.com/aea7738590793eefcd786be8657fa88b
# v05: 1/ made vector2img for cleaner image mngt
#      2/ change the learning order and the trace image creation
# v06: 1/ 3 channels: many changes to make code easier to manage, results easier to view
#      https://gist.github.com/3c9f5917224d8a70ea319af1df973c73
# v07: 1/ add images in ui
#      https://gist.github.com/549d071893cac00e84fcd1875d422d1a
# v08: 1/ added a white image and random image should return 0: to improve robustness
#      
###########################################################################

tracesOn = False # True for debug and illustration of learning process

# Simple Neuron layer
class Layer(object):
  def __init__(self, outputSize, inputLayer=False): 
    self.outputSize = outputSize
    if inputLayer != False:
      self.hasInputLayer = True
      self.input = inputLayer
      self.inputSize = inputLayer.outputSize
      self.weights = np.random.randn(self.inputSize, self.outputSize) 
    else:
      self.hasInputLayer = False
    self.states = []

  def forward(self):
    #forward propagation through 1 network layer
    z = np.dot(self.input.states, self.weights) # dot product of input and set of weights
    self.states = self.sigmoid(z) # activation function

  def backward(self, err):
    #backward propagation through 1 network layer
    delta = err*self.sigmoidPrime( self.states ) # applying derivative of sigmoid to error
    newErr = delta.dot( self.weights.T ) # back-propagate error through the layer
    self.weights += self.input.states.T.dot(delta)*0.02 # adjusting weights
    return newErr

  def sigmoid(self, s):
    # activation function
    return 1/(1+np.exp(-s))

  def sigmoidPrime(self, s):
    #derivative of sigmoid
    return s * (1 - s)

  def weights2img(self,i,h):
    # that is for the fun!
    v = self.weights.T[i]
    w = math.ceil(len(v)/h)
    maxi = max(v)
    mini = min(v)
    r = maxi-mini
    if r == 0: r = 1
    tempPil = PILImage.new('L',[w,h])
    for k in range(len(v)):
      x1 = int(math.fmod(k,w))
      y1 = int(math.floor(k/w))
      val = v[k]
      val = int(255 - (val-mini)/r*250)
      tempPil.putpixel([x1,y1],val)
    return tempPil

  def allWeights2img(self,h):
    img = self.weights2img(0,h)
    w = img.width + 1
    n = len(self.weights[0])
    wtot = w*n-1
    temp = PILImage.new('L',[wtot,10],250)
    for j in range(n):
      img = self.weights2img(j,h)
      temp.paste(img,(j*w,0))
    zoom = 3
    temp.resize((wtot*zoom,10*zoom)).show()

# Simple Neural Network
class Neural_Network(object):
  def __init__(self):
    #create layers
    np.random.seed()
    self.layer = [] # now create layers from input to output:
    self.addLayer(100)
    self.addLayer(25)
    self.addLayer(3)

  def addLayer(self, nbr):
    n = len(self.layer)
    if n == 0:
      self.layer.append(Layer(nbr))
    else:
      self.layer.append(Layer(nbr, self.layer[n-1]))

  def forward(self, X):
    #forward propagation through our network
    n = len(self.layer)
    self.layer[0].states = X     # update input layer 
    for i in range(1,n):
      self.layer[i].forward()      # propagate through other layers
    return self.layer[n-1].states

  def backward(self, err):
    # backward propagate through the network
    n = len(self.layer)
    for i in range(1,n):
      err = self.layer[n-i].backward(err)

  def train(self, X, y):
    o = self.forward(X)
    self.backward(y - o)

  def predict(self, predict):
    o = self.forward(predict)
    #self.layer[1].weights2img(0,10).resize((30,30)).show()
    decision = ''
    if o[0]>o[1]:
      decision = 'Top'
    else:
      decision = 'Bot'
    reliability0 = 'Top: {:d}%'.format(int(100*float(o[0])))
    reliability1 = 'Bot: {:d}%'.format(int(100*float(o[1])))
    output = decision + ' (' + reliability0 + ', ' + reliability1 + ')'
    if tracesOn:
      print(o)
    return o

  def trainAll(self, iterations= None):
    if iterations:
      self.iterations = iterations
      self.lossArray = []
    loss = np.mean(np.square(y - NN.forward(X)))
    if self.iterations > 0:
      self.lossArray.append(loss)
      self.train(X, y)
      if self.iterations % 10 == 0:
        showLearning(len(self.lossArray),loss)
      self.iterations-=1
      ui.delay(self.trainAll, 0.01)
    else:
      console.hud_alert('Ready!')
      if tracesOn: 
        plt.plot(self.lossArray)
        plt.grid(1)
        plt.xlabel('Iterations')
        plt.ylabel('Cost')
        plt.show()
        self.layer[1].allWeights2img(10)



###########################################################################
# The PathView class is responsible for tracking
# touches and drawing the current stroke.
# It is used by SketchView.

class PathView (ui.View):
  def __init__(self, frame):
    self.frame = frame
    self.flex = ''
    self.path = None
    self.action = None

  def touch_began(self, touch):
    x, y = touch.location
    self.path = ui.Path()
    self.path.line_width = 8.0
    self.path.line_join_style = ui.LINE_JOIN_ROUND
    self.path.line_cap_style = ui.LINE_CAP_ROUND
    self.path.move_to(x, y)

  def touch_moved(self, touch):
    x, y = touch.location
    self.path.line_to(x, y)
    self.set_needs_display()

  def touch_ended(self, touch):
    # Send the current path to the SketchView:
    if callable(self.action):
      self.action(self)
    # Clear the view (the path has now been rendered
    # into the SketchView's image view):
    self.path = None
    self.set_needs_display()

  def draw(self):
    if self.path:
      self.path.stroke()

###########################################################################
# The main SketchView contains a PathView for the current
# line and an ImageView for rendering completed strokes.

# We use a square canvas, so that the same image can be used in portrait and landscape orientation.
w, h = ui.get_screen_size()
canvas_size = max(w, h)
mv = ui.View(canvas_size, canvas_size)
mv.bg_color = 'white'
sketch = []  # global to handle the sketch views

class SketchView (ui.View):
  def __init__(self, x, y, width=200, height=200):
    # the sketch region
    self.bg_color = 'lightgrey'
    iv = ui.ImageView(frame=(0, 0, width, height)) #, border_width=1, border_color='black')
    pv = PathView(iv.bounds)
    pv.action = self.path_action
    self.add_subview(iv)
    self.add_subview(pv)
    self.image_view = iv
    self.bounds = iv.bounds
    self.x = x
    self.y = y
    mv.add_subview(self)
    sketch.append(self)
    # some info
    lb = ui.Label()
    self.text='sample ' + str(len(sketch))
    lb.text=self.text
    lb.flex = ''
    lb.x = x+50
    lb.y = y+205
    lb.widht = 100
    lb.height = 20
    lb.alignment = ui.ALIGN_CENTER
    mv.add_subview(lb)
    self.label = lb

  def resetImage(self):
    self.image_view.image = None

  def resetText(self,newText=None):
    if newText != None:
      self.text = newText
    self.label.text = self.text
    self.label.bg_color = 'white'

  def showResult(self,v):
    txt = '{:d}%'.format(int(100*float(v)))
    self.label.text = txt
    if   v > 0.90: c = 'lightgreen'
    elif v > 0.75: c = 'lightblue'
    elif v > 0.50: c = 'yellow'
    elif v > 0.25: c = 'orange'
    else : c = 'red'
    self.label.bg_color = c

  def path_action(self, sender):
    path = sender.path
    old_img = self.image_view.image
    width, height = self.image_view.width, self.image_view.height
    with ui.ImageContext(width, height) as ctx:
      if old_img:
        old_img.draw()
      path.stroke()
      self.image_view.image = ctx.get_image()

###########################################################################
# Various helper functions

def zoom(img, z):
  if z==1.0:
    return img 
  w0 = img.width
  h0 = img.height
  w = int( w0 * z )
  h = int( h0 * z )
  img1 = img.resize((w,h))
  if z<1.0:
    img = img.copy()
    x = int((w0-w)/2)
    y = int((h0-h)/2)
    img.paste(img1,(x,y))
  if z>1.0:
    x = int((w-w0)/2)
    y = int((h-h0)/2)
    img = img1.crop((x,y,x+w0-1,y+h0-1))
    img = img.copy()
  return img

def getVector(k, dx=0, dy=0, theta=0, z=1.0):
  kmax = len(sketch)
  if (type(k)==type(sketch[0])) or k < kmax:
    if (type(k)==type(sketch[0])):
      v = k
    else:
      v = sketch[k]
    pil_image = ui2pil(snapshot(v.subviews[0]))
    pil_image = pil_image.resize((200,200))
    pil_image = chops.offset(pil_image, dx, dy)
    pil_image = pil_image.rotate(theta)
    pil_image = zoom(pil_image, z)
    #w, h = int(v.image_view.width), int(v.image_view.height)
    w, h = 200, 200
    px = 20
    p = int(w / px)
    xStep = int(w / p)
    yStep = int(h / p)
    vector = []
    for x in range(0, w, xStep):
      for y in range(0, h, yStep):
        crop_area = (x, y, xStep + x, yStep + y)
        cropped_pil = pil_image.crop(crop_area)
        crop_arr = cropped_pil.load()
        nonEmptyPixelsCount = 0
        for x1 in range(xStep):
          for y1 in range(yStep):
            isEmpty = (crop_arr[x1,y1][3] == 0)
            if not isEmpty:
              nonEmptyPixelsCount += 1
        if nonEmptyPixelsCount > 0:
           nonEmptyPixelsCount = 1
        vector.append(nonEmptyPixelsCount)
  elif k == kmax:
    vector = [0]*100
  elif k == kmax+1:
    vector = np.random.choice([0, 1], size=100, p=[.8, .2])
  return vector

def vector2img(v):
  w,h = 10,10
  maxi = max(v)
  mini = min(v)
  r = maxi-mini
  if r == 0: r = 1
  tempPil = PILImage.new('L',[w,h])
  k=0
  for x1 in range(w):
    for y1 in range(h):
      val = v[k]
      val = 255 - (val-mini)/r*250
      tempPil.putpixel([x1,y1],val)
      k+=1
  return tempPil

def snapshot(view):
  with ui.ImageContext(view.width, view.height) as ctx:
    view.draw_snapshot()
    return ctx.get_image()

def ui2pil(ui_img):
  return PILImage.open(io.BytesIO(ui_img.to_png()))

def pil2ui(pil_image):
  buffer = io.BytesIO()
  pil_image.save(buffer, format='PNG')
  return ui.Image.from_data(buffer.getvalue())

def train_action(sender):
  ui.delay(trainNN,0.2)

class prepareTrainSet():
  def __init__(self):
    global X, y
    X = []
    y = []
    self.y0 = [ [1,0,0], 
                [0,1,0],
                [0,0,1],
                [0,0,0],
                [0,0,0]]

    self.temp = None
    a = 10
    th = 10
    vars = []
    for dx in (-a, 0, a):
      for dy in (-a, 0, a):
        for th in (-th, 0, th):
          for z in (0.9, 1.0, 1.2):
            for k in range(len(sketch)+2):
              vars.append((dx,dy,th,k,z))
    self.vars = vars
    self.count = 0
    self.run()

  def run(self):
    global X, y, pts, NN
    n = len(self.vars)
    count = self.count
    if count<n:
      dx,dy,th,k,z = self.vars[count]
      if count%10==0:
        showTrainData(count+1,n)
      y.append(self.y0[k])
      v = getVector(k, dx, dy, th, z)
      X.append(v)
      if True or tracesOn:
        nb = 35
        if self.temp == None:
          w = (10+1)*nb-1
          h = (10+1)*math.ceil(n/nb)-1
          temp = PILImage.new('L',[w,h],250)
          self.temp = temp

        x1 = int(math.fmod(count,nb))
        y1 = int(math.floor(count/nb))
        img = vector2img(v)
        self.temp.paste(img,(x1*11,y1*11))
        if count%27 == 26:
          updateLearninImage(self.temp)
      self.count+=1
      ui.delay(self.run, 0.001)
    else:
      self.count = 0
      X = np.array(X, dtype=float)
      y = np.array(y, dtype=float)
      updateLearninImage(self.temp)

      NN.trainAll(300)
      pts = None

def updateLearninImage(img):
  w1,h1 = int(learningSetImage.width), int(learningSetImage.height)
  temp = img.resize((w1,h1))
  learningSetImage.image = pil2ui(temp)

def trainNN():
  global pts,NN
  pts = prepareTrainSet()

def showLearning(i,v):
  if   v > 0.1: c = 'red'
  elif v > 0.02: c = 'orange'
  elif v > 0.005: c = 'yellow'
  elif v > 0.001: c = 'lightblue'
  else : c = 'lightgreen'
  trainInfo.bg_color = c
  txt = 'Loss {:d} : {:5.2f}%'.format(i+1, int(10000*float(v))/100)
  trainInfo.text = txt

def showTrainData(i,n):
  trainInfo.bg_color = 'white'
  txt = 'Preparing {:d} / {:d}'.format(i, n)
  trainInfo.text = txt

def guess_action(sender):
  global NN, X, y
  if len(X) == 0:
    console.hud_alert('You need to do Steps 1 and 2 first.', 'error')
  else:
    p = getVector(newSketch)
    if tracesOn:
      img = vector2img(p)
      zoom = 3
      img.resize((10*zoom,10*zoom)).show()
    p = np.array(p, dtype=float)
    result = NN.predict(p)
    #console.hud_alert('done')
    for i in range(len(sketch)):
      sketch[i].showResult(result[i])

def clear_action(sender):
  newSketch.resetImage()
  for sv in sketch:
      sv.resetText()

def clearAll_action(sender):
  for sv in sketch:
    sv.resetImage()
    sv.resetText()
  newSketch.resetImage()
  showLearning(0,1)


##############################################

NN = Neural_Network()

clearAll_button = ui.ButtonItem()
clearAll_button.title = 'Reset !!'
clearAll_button.tint_color = 'red'
clearAll_button.action = clearAll_action
mv.right_button_items = [clearAll_button]

lb = ui.Label()
lb.text='First, prepare the data:'
lb.flex = 'W'
lb.x = 290
lb.y = 0
mv.add_subview(lb)

lb = ui.Label()
lb.text='Draw 3 different images (ex: A, B, C)'
lb.flex = 'W'
lb.alignment = ui.ALIGN_CENTER
lb.x = -150
lb.y = 20
mv.add_subview(lb)

sv = SketchView( 30, 100)
sv = SketchView(260, 100)
sv = SketchView(490, 100)
#sv = SketchView( 30, 340)
#sv = SketchView(260, 340)
#sv = SketchView(490, 340)

iv = ui.ImageView(frame=(30, 350, 660, 300), border_width=1, border_color='black')
learningSetImage = iv
mv.add_subview(iv)

lb = ui.Label()
lb.text='Now, Train the Model'
lb.flex = 'W'
lb.x = 690+50
lb.y = 50+50
lb.height = 20
mv.add_subview(lb)

train_button = ui.Button(frame = (800, 80+50, 80, 32))
train_button.border_width = 2
train_button.corner_radius = 4
train_button.title = '1/ Train'
train_button.action = train_action
mv.add_subview(train_button)

trainInfo = ui.Label()
lb = trainInfo
lb.text='0%'
lb.flex = ''
lb.x = 750
lb.y = 120+50
lb.height = 20
lb.width = 200
lb.alignment = ui.ALIGN_CENTER
mv.add_subview(trainInfo)
showLearning(0, 1.0)

lb = ui.Label()
lb.text='OK now lets see if it can Guess right'
lb.flex = 'w'
lb.x = 700
lb.y = 200
mv.add_subview(lb)

sv = SketchView(740, 280)
sketch = sketch[:-1] # this last view is not part of the example set => remove it
sv.resetText('')
mv.add_subview(sv)
newSketch = sv

guess_button = ui.Button(frame = (750, 530, 80, 32))
guess_button.border_width = 2
guess_button.corner_radius = 4
guess_button.title = '2/ Guess'
guess_button.action = guess_action
mv.add_subview(guess_button)

clear_button = ui.Button(frame = (850, 530, 80, 32))
clear_button.border_width = 2
clear_button.corner_radius = 4
clear_button.title = 'Clear'
clear_button.action = clear_action
mv.add_subview(clear_button)

mv.name = 'Image Recognition'
mv.present('full_screen', orientations='landscape')