¿Cómo detectar movimiento entre dos imágenes PIL? (Ejemplo de integración de webcam con wxPython incluido)

¿Alguien tiene alguna sugerencia sobre cómo podría hacer una comparación de imágenes en python para detectar cambios dentro de una imagen? Actualmente estoy trabajando en una aplicación que controlará mi área con mi cámara web. Me gustaría averiguar cómo comparar las imágenes tomadas en cada cuadro para ver si se ha detectado algún movimiento. A la larga, me gustaría configurar un control deslizante de sensibilidad para que, si eres capaz de guiarme en la dirección, estoy seguro de que puedo resolver el rest.

Como he visto algunas publicaciones aquí que preguntan sobre la integración de una cámara web con wxPython, aquí hay una pequeña demostración. Tenga en cuenta que lo empecé ayer por la noche, así que si está buscando el código de la parte superior de las sugerencias, es posible que deba revisarlo usted mismo (por ahora;):

Requisitos: PIL y captura de video

#videocapturepanel.py #Todo: # - Fix background colour after video is stopped # - Create image comparison method # - Add capture function # - Save stream to video file? import threading, wx from PIL import Image from VideoCapture import Device cam = Device(0) buffer, width, height = cam.getBuffer() cam.setResolution(width, height) DEFAULT_DEVICE_INDEX = 0 DEFAULT_DEVICE_WIDTH = width DEFAULT_DEVICE_HEIGHT = height DEFAULT_BACKGROUND_COLOUR = wx.Colour(0, 0, 0) class VideoCaptureThread(threading.Thread): def __init__(self, control, width=DEFAULT_DEVICE_WIDTH, height=DEFAULT_DEVICE_HEIGHT, backColour=DEFAULT_BACKGROUND_COLOUR): self.backColour = backColour self.width = width self.height = height self.control = control self.isRunning = True self.buffer = wx.NullBitmap threading.Thread.__init__(self) def getResolution(self): return (self.width, self.height) def setResolution(self, width, height): self.width = width self.height = height cam.setResolution(width, height) def getBackgroundColour(self): return self.backColour def setBackgroundColour(self, colour): self.backColour = colour def getBuffer(self): return self.buffer def stop(self): self.isRunning = False def run(self): while self.isRunning: buffer, width, height = cam.getBuffer() im = Image.fromstring('RGB', (width, height), buffer, 'raw', 'BGR', 0, -1) buff = im.tostring() self.buffer = wx.BitmapFromBuffer(width, height, buff) x, y = (0, 0) try: width, height = self.control.GetSize() if width > self.width: x = (width - self.width) / 2 if height > self.height: y = (height - self.height) / 2 dc = wx.BufferedDC(wx.ClientDC(self.control), wx.NullBitmap, wx.BUFFER_VIRTUAL_AREA) dc.SetBackground(wx.Brush(self.backColour)) dc.Clear() dc.DrawBitmap(self.buffer, x, y) except TypeError: pass except wx.PyDeadObjectError: pass self.isRunning = False class VideoCapturePanel(wx.Panel): def __init__(self, parent, id=-1, pos=wx.DefaultPosition, size=wx.DefaultSize, initVideo=False, style=wx.SUNKEN_BORDER): wx.Panel.__init__(self, parent, id, pos, size, style) if initVideo: self.StartVideo() self.Bind(wx.EVT_CLOSE, self.OnClose) def OnClose(self, event): try: self.Device.stop() except: pass def StopVideo(self): self.Device.stop() self.SetBackgroundColour(self.Device.backColour) dc = wx.BufferedDC(wx.ClientDC(self), wx.NullBitmap) dc.SetBackground(wx.Brush(self.Device.backColour)) dc.Clear() def StartVideo(self): self.Device = VideoCaptureThread(self) self.Device.start() def GetBackgroundColour(self): return self.Device.getBackgroundColour() def SetBackgroundColour(self, colour): self.Device.setBackgroundColour(colour) class Frame(wx.Frame): def __init__(self, parent, id=-1, title="A Frame", path="", pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE): wx.Frame.__init__(self, parent, id, title, pos, size, style) self.VidPanel = VideoCapturePanel(self, -1, initVideo=False) self.StartButton = wx.ToggleButton(self, -1, "Turn On") self.ColourButton = wx.Button(self, -1, "Change Background") szr = wx.BoxSizer(wx.VERTICAL) bszr = wx.BoxSizer(wx.HORIZONTAL) bszr.Add(self.StartButton, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT, 5) bszr.Add(self.ColourButton, 0, wx.ALIGN_CENTER_HORIZONTAL) szr.Add(self.VidPanel, 1, wx.EXPAND) szr.Add(bszr, 0, wx.ALIGN_CENTER_HORIZONTAL) self.SetSizer(szr) self.StartButton.Bind(wx.EVT_TOGGLEBUTTON, self.OnToggled) self.ColourButton.Bind(wx.EVT_BUTTON, self.OnColour) def OnColour(self, event): dlg = wx.ColourDialog(self) dlg.GetColourData().SetChooseFull(True) if dlg.ShowModal() == wx.ID_OK: data = dlg.GetColourData() self.VidPanel.SetBackgroundColour(data.GetColour()) dlg.Destroy() def OnToggled(self, event): if event.IsChecked(): self.VidPanel.StartVideo() else: self.VidPanel.StopVideo() #self.VidPanel.SetBackgroundColour(data.GetColour()) if __name__ == "__main__": # Run GUI app = wx.PySimpleApp() frame = Frame(None, -1, "Test Frame", size=(800, 600)) frame.Show() app.MainLoop() del app 

* ACTUALIZACIÓN *

Usando el ejemplo de Paul, creé una clase y la implementé en mi código:

 class Images: def __init__(self, image1, image2, threshold=98, grayscale=True): self.image1 = image1 if type(image1) == str: self.image1 = Image.open(self.image1) self.image2 = image2 if type(image2) == str: self.image2 = Image.open(image2) self.threshold = threshold def DoComparison(self, image1=None, image2=None): if not image1: image1 = self.image1 if not image2: image2 = self.image2 diffs = ImageChops.difference(image1, image2) return self.ImageEntropy(diffs) def ImageEntropy(self, image): histogram = image.histogram() histlength = sum(histogram) probability = [float(h) / histlength for h in histogram] return -sum([p * math.log(p, 2) for p in probability if p != 0]) 

y luego agregó la variable self.image = False a la función __init__() , y agregó el siguiente código a la función run () de VideoCaptureThread después de la línea im = Image.fromstring (…) :

  if self.image: img = compare.Images2(im, self.image).DoComparison() print img self.image = im 

Cuando ejecuto la muestra, parece que funciona bien, pero estoy un poco confundido con los resultados que obtengo:

 1.58496250072 5.44792407663 1.58496250072 5.44302784225 1.58496250072 5.59144486002 1.58496250072 5.37568050189 1.58496250072 

¿Hasta ahora parece que todas las demás imágenes están un poco apagadas aunque los cambios son mínimos? La adición a ejecutar debería, en teoría, capturar la imagen anterior bajo la variable self.image y compararla con la nueva imagen im . Después de la comparación, self.image se actualiza a la imagen actual usando self.image = im , así que ¿por qué habría una diferencia en cada segunda imagen? A lo sumo, mis ojos podrían haberse movido hacia atrás / adelante dentro de las dos imágenes, ¿y no puedo ver que causando tal diferencia con mis resultados?

* ACTUALIZACIÓN 2 *

Aquí está lo que tengo hasta ahora, hay tres clases de comparación de comparación con tres métodos diferentes para detectar movimiento.

Imágenes de clase ~ El primer bash de usar un código que encontré mientras buscaba en Google, ni siquiera puedo recordar cómo funciona tbh. :PAG

clase Images2 ~ Creado con el código de Paul de este hilo, implementando su función de entropía de imagen actualizada.

clase Images3 ~ Versión modificada de la función DetectMotion encontrada aquí . (Devuelve el porcentaje cambiado y parece tener en cuenta la iluminación)

Sinceramente, realmente no tengo idea de lo que está haciendo ninguno de ellos, literalmente, pero lo que sí puedo decir es que hasta ahora la clase Image3 parece ser la forma más sencilla y precisa de configurar la detección, la desventaja es que lleva más tiempo procesar que la otras dos clases.

(Tenga en cuenta que se hicieron algunos cambios de importación para evitar colisiones con scipy, sys.modules [“Image”] es lo mismo que PIL.Image)

 import math, sys, numpy as np import PIL.Image, PIL.ImageChops sys.modules["Image"] = PIL.Image sys.modules["ImageChops"] = PIL.ImageChops from scipy.misc import imread from scipy.linalg import norm from scipy import sum, average DEFAULT_DEVICE_WIDTH = 640 DEFAULT_DEVICE_HEIGHT = 480 class Images: def __init__(self, image1, image2, threshold=98, grayscale=True): if type(image1) == str: self.image1 = sys.modules["Image"].open(image1) self.image2 = sys.modules["Image"].open(image2) if grayscale: self.image1 = self.DoGrayscale(imread(image1).astype(float)) self.image2 = self.DoGrayscale(imread(image2).astype(float)) else: self.image1 = imread(image1).astype(float) self.image2 = imread(image2).astype(float) self.threshold = threshold def DoComparison(self, image1=None, image2=None): if image1: image1 = self.Normalize(image1) else: image1 = self.Normalize(self.image1) if image2: image2 = self.Normalize(image2) else: image2 = self.Normalize(self.image2) diff = image1 - image2 m_norm = sum(abs(diff)) z_norm = norm(diff.ravel(), 0) return (m_norm, z_norm) def DoGrayscale(self, arr): if len(arr.shape) == 3: return average(arr, -1) else: return arr def Normalize(self, arr): rng = arr.max()-arr.min() amin = arr.min() return (arr-amin)*255/rng class Images2: def __init__(self, image1, image2, threshold=98, grayscale=True): self.image1 = image1 if type(image1) == str: self.image1 = sys.modules["Image"].open(self.image1) self.image2 = image2 if type(image2) == str: self.image2 = sys.modules["Image"].open(image2) self.threshold = threshold def DoComparison(self, image1=None, image2=None): if not image1: image1 = self.image1 if not image2: image2 = self.image2 diffs = sys.modules["ImageChops"].difference(image1, image2) return self.ImageEntropy(diffs) def ImageEntropy(self, image): w,h = image.size a = np.array(image.convert('RGB')).reshape((w*h,3)) h,e = np.histogramdd(a, bins=(16,)*3, range=((0,256),)*3) prob = h/np.sum(h) return -np.sum(np.log2(prob[prob>0])) def OldImageEntropy(self, image): histogram = image.histogram() histlength = sum(histogram) probability = [float(h) / histlength for h in histogram] return -sum([p * math.log(p, 2) for p in probability if p != 0]) class Images3: def __init__(self, image1, image2, threshold=8): self.image1 = image1 if type(image1) == str: self.image1 = sys.modules["Image"].open(self.image1) self.image2 = image2 if type(image2) == str: self.image2 = sys.modules["Image"].open(image2) self.threshold = threshold def DoComparison(self, image1=None, image2=None): if not image1: image1 = self.image1 if not image2: image2 = self.image2 image = image1 monoimage1 = image1.convert("P", palette=sys.modules["Image"].ADAPTIVE, colors=2) monoimage2 = image2.convert("P", palette=sys.modules["Image"].ADAPTIVE, colors=2) imgdata1 = monoimage1.getdata() imgdata2 = monoimage2.getdata() changed = 0 i = 0 acc = 3 while i < DEFAULT_DEVICE_WIDTH * DEFAULT_DEVICE_HEIGHT: now = imgdata1[i] prev = imgdata2[i] if now != prev: x = (i % DEFAULT_DEVICE_WIDTH) y = (i / DEFAULT_DEVICE_HEIGHT) try: #if self.view == "normal": image.putpixel((x,y), (0,0,256)) #else: # monoimage.putpixel((x,y), (0,0,256)) except: pass changed += 1 i += 1 percchange = float(changed) / float(DEFAULT_DEVICE_WIDTH * DEFAULT_DEVICE_HEIGHT) return percchange if __name__ == "__main__": # image1 & image2 MUST be legit paths! image1 = "C:\\Path\\To\\Your\\First\\Image.jpg" image2 = "C:\\Path\\To\\Your\\Second\\Image.jpg" print "Images Result:" print Images(image1, image2).DoComparison() print "\nImages2 Result:" print Images2(image1, image2).DoComparison() print "\nImages3 Result:" print Images3(image1, image2).DoComparison() 

Este podría ser un enfoque ingenuo, pero es un lugar simple para comenzar. Estoy seguro de que el ruido de la cámara lo influenciará y es posible que desee distinguir los cambios en la iluminación de los cambios en la composición de la imagen. Pero esto es lo que me vino a la mente:

Puede usar PIL ImageChops para tomar una diferencia entre imágenes de manera eficiente. Luego puede tomar la entropía de esa diferencia para obtener un umbral de valor único.

Parece funcionar:

 from PIL import Image, ImageChops import math def image_entropy(img): """calculate the entropy of an image""" # this could be made more efficient using numpy histogram = img.histogram() histogram_length = sum(histogram) samples_probability = [float(h) / histogram_length for h in histogram] return -sum([p * math.log(p, 2) for p in samples_probability if p != 0]) # testing.. img1 = Image.open('SnowCam_main1.jpg') img2 = Image.open('SnowCam_main2.jpg') img3 = Image.open('SnowCam_main3.jpg') # No Difference img = ImageChops.difference(img1,img1) img.save('test_diff1.png') print image_entropy(img) # 1.58496250072 # Small Difference img = ImageChops.difference(img1,img2) img.save('test_diff2.png') print image_entropy(img) # 5.76452986917 # Large Difference img = ImageChops.difference(img1,img3) img.save('test_diff3.png') print image_entropy(img) # 8.15698432026 

Creo que este es un algoritmo mucho mejor para la entropía de imágenes, ya que se divide en tres dimensiones en el espacio de color en lugar de crear un histogtwig separado para cada banda.

EDITAR – esta función fue cambiada el 6 de abril de 2012

 import numpy as np def image_entropy(img): w,h = img.size a = np.array(img.convert('RGB')).reshape((w*h,3)) h,e = np.histogramdd(a, bins=(16,)*3, range=((0,256),)*3) prob = h/np.sum(h) # normalize prob = prob[prob>0] # remove zeros return -np.sum(prob*np.log2(prob)) 

Estas son mis imágenes de prueba:

introduzca la descripción de la imagen aquí

Imagen 1

introduzca la descripción de la imagen aquí

Imagen 2

introduzca la descripción de la imagen aquí

Imagen 3