10

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() 
9
  • I did some more seraching and found this link. Although there are many suggestions as to how to accomplish this task, only a couple of examples were provided, and it doesn't really touch up on which of the suggestions are best suited for webcam image comparison. As my knowledge in this area is quite limited, most of what was discussed makes little sense to me so if someone could help me with a For Dummies version I would be greatful! :P Commented Apr 2, 2011 at 16:47
  • that discussion is about image comparison in general, but you need to detect motion. it's much easier. May be OpenCV library meet your requirements too Commented Apr 2, 2011 at 17:19
  • 1.58496250072 is the entropy value I get when I compare two exactly identical 320 X 240 RGB images. I'm guessing that your compare operation is occurring 2X faster than your camera is updating it's image. Commented Apr 2, 2011 at 19:28
  • @Andrey, Thank you for your suggestion. When I first started this project I spent 2 days tinkering around with OpenCV as it appeared to be my best bet. After trying a few versions & python modules I was unable to even get my cam to initialize, which brought me to the conclusion that my cam is not compatible. I was however able to get the buffer code to work using the basic structure of the OpenCV examples so it did come in handy in the long run. The other downfall is having to include OpenCV in my dist, it is a bit bulky and if it is not required than i would prefer to skip it altogether. Commented Apr 2, 2011 at 19:42
  • @Paul, I did a little more testing with your image by taking a few snapshots and running the code in a for loop to capture them from my cam. when I compare the image to itself, I still get a difference of around 1.5xx? If the image is identical, why would there be a difference? I believe you are right with your assumption that the image is comparing itself because it runs quicker than my cam, but how could I add a proper if statement if the results of a same image comparison is not 0? I would have just changed it to if self.image and self.image != im:. Commented Apr 2, 2011 at 20:03

1 Answer 1

13

This might be a naive approach, but it's a simple place to begin. I'm sure you will be influenced by camera noise and you may want to distinguish changes in lighting from changes in image composition. But here's what came to my mind:

You can use PIL ImageChops to efficiently take a difference between images. Then you can take the entropy of that diff to get a single-value threshold.

It seems to work:

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 

This, I believe is a much better algorithm for image entropy since it bins 3-dimensionally in color-space rather than creating a separate histogram for each band.

EDIT- this function was changed 6-Apr-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)) 

These are my test images:

enter image description here

Image 1

enter image description here

Image 2

enter image description here

Image 3

Sign up to request clarification or add additional context in comments.

8 Comments

This looks great! I'm just about to try this out now, let me edit it to handle the raw buffer and I'll post back once I have something put together :D
I edited your code and have started to implement it. I edited my original post to include the changes. I am still a bit confused with my results but I think I am on the right track, please see the above edit. :)
I now see what you were referring to when you said I would need to distinguish changes in lighting, anyone able to point mein the right direction?
@AWainb You could try just detecting changes in the image's hue stackoverflow.com/questions/4890373/… or you could try "normalizing" an image by subtracting a very smoothed version from the detailed version. This may warrant a new question if you want more opinions/ideas.
In your entropy2 function, don't you need to multiply the result by prob?
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.