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()
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:
Image 1
Image 2
Image 3