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()