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()
AWainb
  • 868
  • 2
  • 13
  • 27
  • I did some more seraching and found this [link](http://stackoverflow.com/questions/189943/how-can-i-quantify-difference-between-two-images). 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 – AWainb Apr 02 '11 at 16:47
  • that discussion is about image comparison in general, but you need to detect motion. it's much easier. May be [OpenCV](http://opencv.willowgarage.com/documentation/cpp/motion_analysis_and_object_tracking.html) library meet your requirements too – Andrey Sboev Apr 02 '11 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. – Paul Apr 02 '11 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. – AWainb Apr 02 '11 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:`. – AWainb Apr 02 '11 at 20:03
  • think I figured it out, after comparing multiple images with themselves I was able to figure out that if the images are the same, the threshold will always equal 1.5849625007211561, so I can use this number for reference instead of **if self.image != im**. – AWainb Apr 02 '11 at 20:40
  • This seems to be the case only for RGB images. B&W and grey-scale images give 0.0 for "no diff". I think it has something to do with the way PIL creates a histogram (it treats each band separately). I started to perform a multi-D histogram with numpy hoping to get what might be a more accurate way of getting entropy, but ran out of time.. – Paul Apr 02 '11 at 23:51
  • See my version of the entropy function in my updated answer. It should be an improvement over the linked version. It will bin in 3D and returns zero for identical images as expected. – Paul Apr 03 '11 at 05:05
  • I've been made aware of an error in my "improved" entropy function and my answer has been updated. Just thought you should be aware in case it was being used for something important. – Paul Apr 06 '12 at 20:23

1 Answers1

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

Paul
  • 42,322
  • 15
  • 106
  • 123
  • 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 – AWainb Apr 02 '11 at 18:08
  • 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. :) – AWainb Apr 02 '11 at 19:17
  • 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 Apr 02 '11 at 23:37
  • @AWainb You could try just detecting changes in the image's hue http://stackoverflow.com/questions/4890373/detecting-thresholds-in-hsv-color-space-from-rgb-using-python-pil/4890878#4890878 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. – Paul Apr 03 '11 at 05:28
  • In your `entropy2` function, don't you need to multiply the result by `prob`? – Geoff Apr 04 '12 at 18:07
  • @Geoff Since `prob` is an array, no, definitely not. But maybe you mean the result should be the sum of the products of prob and log(prob)? Yep, I think that's it. Thanks. – Paul Apr 05 '12 at 05:52
  • @Paul you got it. Sorry I was too lazy to write out the full suggestion. I know it's been a year since you wrote this answer. Thanks for your time. – Geoff Apr 06 '12 at 13:11
  • Broken link to ImageChops – Grimxn Nov 16 '20 at 17:29