Как обнаружить движение между двумя изображениями PIL?(включен пример интеграции с веб-камерой wxPython) - PullRequest
9 голосов
/ 02 апреля 2011

У кого-нибудь есть предложения относительно того, как я могу выполнить сравнение изображений в python, чтобы обнаружить изменения в изображении? В настоящее время я работаю над приложением, которое будет контролировать мою область с помощью моей веб-камеры, и я хотел бы выяснить, как сравнивать снимки, сделанные в каждом кадре, чтобы увидеть, было ли обнаружено какое-либо движение. В долгосрочной перспективе я хотел бы настроить ползунок чувствительности, чтобы, если вы смогли направить меня в том направлении, я уверен, что смогу понять все остальное.

Поскольку я видел несколько постов, в которых просят об интеграции веб-камеры с wxPython, вот небольшая демонстрация. Обратите внимание, что я только начал его вчера вечером, поэтому, если вы ищете код подсказки, вам, возможно, придется пересмотреть его самостоятельно (сейчас;):

Требования: 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 *

на примере Пола я создал класс и внедрил его в свой код:

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

, а затем добавили переменную self.image = False к функции __init__() VideoCaptureThread и добавили приведенный ниже код к функции run () функции VideoCaptureThread после строка im = Image.fromstring (...) :

        if self.image:
            img = compare.Images2(im, self.image).DoComparison()
            print img
        self.image = im

Когда я запускаю образец, он, кажется, работает нормально, но я немного запутался с полученными результатами:

1.58496250072
5.44792407663
1.58496250072
5.44302784225
1.58496250072
5.59144486002
1.58496250072
5.37568050189
1.58496250072

Пока что кажется, что любое другое изображение совсем немного, хотя изменения минимальны? Дополнение к запуску должно теоретически захватить предыдущее изображение с переменной self.image и сравнить с новым изображением im . После сравнения self.image обновляется до текущего изображения с использованием self.image = im , так почему бы так разница в каждом втором изображении? В большинстве случаев мои глаза могли смещаться взад-вперед на двух изображениях, и я не вижу, что вызывает такое расхождение с моими результатами?

* ОБНОВЛЕНИЕ 2 *

Вот то, что у меня есть, есть три сравнительных класса сравнения с тремя различными методами для обнаружения движения.

Изображения класса ~ Первая попытка использовать некоторый код, который я нашел во время поиска в Google, даже не помню, как он работает. : P

class Images2 ~ Создано с использованием кода Пола из этого потока, реализующего его обновленную функцию энтропии изображений.

класс Images3 ~ Обнаружена измененная версия функции DetectMotion здесь . (Процент возврата изменен и, по-видимому, учитывает освещение)

По правде говоря, я действительно понятия не имею, что делает каждый из них, в буквальном смысле, но я могу сказать, что класс Image3 пока кажется самым простым / точным способом настройки обнаружения, и недостатком является то, что для этого требуется больше времени. процесс, чем два других класса.

(Обратите внимание, что некоторые изменения импорта были сделаны, чтобы избежать коллизий со scipy, sys.modules ["Image"] такой же, как 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()

1 Ответ

10 голосов
/ 02 апреля 2011

Это может быть наивный подход, но это простое место для начала.Я уверен, что на вас будет влиять шум камеры, и вы можете отличить изменения освещения от изменений в композиции изображения.Но вот что пришло мне в голову:

Вы можете использовать PIL ImageChops , чтобы эффективно воспринимать разницу между изображениями.Затем вы можете взять энтропию этого различия, чтобы получить пороговое значение с одним значением.

Кажется, это работает:

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

Я полагаю, что это гораздо лучший алгоритм для энтропии изображения, так как он объединяет трехмерное пространство цветов, а не создает отдельную гистограмму дляband.

EDIT - эта функция была изменена 6 апреля 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))

Это мои тестовые изображения:

enter image description here

Изображение 1

enter image description here

Изображение 2

enter image description here

Изображение 3

...