Does anyone have any suggestions as to how I might do image comparison in python to detect changes within an image? I'm currently working on an app that will monitor my area with my webcam, I would like to figure out how to compare the images taken each frame to see if any motion has been detected. In the long run I would like to setup a sensitivity slider so if you are able to guide me in the direction I'm sure I can figure the rest out.
As I have seen a few posts on here asking about integrating a webcam with wxPython, here is a small demo. Please note that I just started it last night so if you are looking for tip top code, you might have to revise it yourself (for now;):
Requirements: PIL & VideoCapture
#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 *UPDATE*
using Paul's example I created a class and implemented it into my code:
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]) and then added the variable self.image = False to VideoCaptureThread's __init__() function, and added the below code to VideoCaptureThread's run() function after the line im = Image.fromstring(...):
if self.image: img = compare.Images2(im, self.image).DoComparison() print img self.image = im When I run the sample it appears to work ok but I am a bit confused with the results I get:
1.58496250072 5.44792407663 1.58496250072 5.44302784225 1.58496250072 5.59144486002 1.58496250072 5.37568050189 1.58496250072 So far it appears that every other image is off by quite a bit although the changes are minimal? The addition to run should in theory capture the previous image under the variable self.image and compare to the new image im. After the comparison, self.image is updated to the current image using self.image = im, so why would there be such a difference in every second image? At most my eyes might have shifted back/forth within the two images, and I cant see that causing such a differece with my results?
*UPDATE 2*
Here is what I have so far, there are three comparison comparison classes with three different methods to detect motion.
class Images ~ The first attempt using some code I found while googling, can't even remember how it works tbh. :P
class Images2 ~ Created using Paul's code from this thread, implementing his updated image entropy function.
class Images3 ~ Modified version of DetectMotion function found here. (Returns percentage changed and appears to take lighting into consideration)
Truthfully I really have no idea what any of them are doing, literally, but what I can tell is that so far class Image3 seems to be the simplest/accurate way to setup the detection, the downfall is it takes more time to process than the other two classes.
(Please note that some import changes were made to avoid collisions with scipy, sys.modules["Image"] is the same as 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() 


if self.image and self.image != im:.