سؤال

I have a python script I use to grab images from an ip camera through my home network and add date time information. In a 12 hour period it grabs about 200,000 pictures. But when using zoneminder (camera monitoring software) the camera manages 250,000 in a 7 hour period.

I was wondering if anyone could help me improve my script efficiency I have tried using the threading module to create 2 threads but it has not helped i am not sure if I have implemented it wrong or not. Below is code I am currently using:

#!/usr/bin/env python

# My First python script to grab images from an ip camera

import requests
import time
import urllib2
import sys
import os
import PIL
from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw
import datetime
from datetime import datetime
import threading

timecount = 43200
lock = threading.Lock()

wdir = "/workdir/"

y = len([f for f in os.listdir(wdir) 
     if f.startswith('Cam1') and os.path.isfile(os.path.join(wdir, f))])

def looper(timeCount):
   global y
   start = time.time()
   keepLooping = True
   while keepLooping:
    with lock:
        y += 1
    now = datetime.now()
    dte = str(now.day) + ":" +  str(now.month) + ":" + str(now.year)
    dte1 = str(now.hour) + ":" + str(now.minute) + ":" + str(now.second) + "." + str(now.microsecond)
    cname = "Cam1:"
    dnow = """Date: %s """ % (dte)
    dnow1 = """Time: %s""" % (dte1)
    buffer = urllib2.urlopen('http://(ip address)/snapshot.cgi?user=uname&pwd=password').read()
    img = str(wdir) + "Cam1-" + str('%010d' % y) + ".jpg"
    f = open(img, 'wb')
    f.write(buffer) 
    f.close()
    if time.time()-start > timeCount:
           keepLooping = False
    font = ImageFont.truetype("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf",10)
    img=Image.open(img)
    draw = ImageDraw.Draw(img)
    draw.text((0, 0),cname,fill="white",font=font)
    draw.text((0, 10),dnow,fill="white",font=font)
    draw.text((0, 20),dnow1,fill="white",font=font)
    draw = ImageDraw.Draw(img)
    draw = ImageDraw.Draw(img)
    img.save(str(wdir) + "Cam1-" + str('%010d' % y) + ".jpg")

for i in range(2):
        thread = threading.Thread(target=looper,args=(timecount,))
        thread.start()
        thread.join()

how could i improve this script or how do i open a stream from the camera then grab images from the stream? would that even increase the efficiency / capture rate?

Edit:

Thanks to kobejohn's help i have come up with the following implementation. running for a 12 hour period it has gotten over 420,000 pictures from 2 seperate cameras (at the same tme) each running on their own thread at the same time compared to about 200,000 from my origional implementation above. The following code will run 2 camera's in parallel (or close enough to it) and add text to them:

import base64
from datetime import datetime
import httplib
import io
import os
import time

from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw

import multiprocessing

wdir = "/workdir/"
stream_urlA = '192.168.3.21'
stream_urlB = '192.168.3.23'
usernameA = ''
usernameB = ''
password = ''

y = sum(1 for f in os.listdir(wdir) if f.startswith('CamA') and os.path.isfile(os.path.join(wdir, f)))
x = sum(1 for f in os.listdir(wdir) if f.startswith('CamB') and os.path.isfile(os.path.join(wdir, f)))

def main():
    time_count = 43200
#    time_count = 1
    procs = list()
    for i in range(1):
        p = multiprocessing.Process(target=CameraA, args=(time_count, y,))
        q = multiprocessing.Process(target=CameraB, args=(time_count, x,))
        procs.append(p)
        procs.append(q)
        p.start()
        q.start()
    for p in procs:
        p.join()

def CameraA(time_count, y):
    y = y
    h = httplib.HTTP(stream_urlA)
    h.putrequest('GET', '/videostream.cgi')
    h.putheader('Authorization', 'Basic %s' % base64.encodestring('%s:%s' % (usernameA, password))[:-1])
    h.endheaders()
    errcode, errmsg, headers = h.getreply()
    stream_file = h.getfile()
    start = time.time()
    end = start + time_count
    while time.time() <= end:
    y += 1
        now = datetime.now()
        dte = str(now.day) + "-" + str(now.month) + "-" + str(now.year)
        dte1 = str(now.hour) + ":" + str(now.minute) + ":" + str(now.second) + "." + str(now.microsecond)
        cname = "Cam#: CamA"
        dnow = """Date: %s """ % dte
        dnow1 = """Time: %s""" % dte1
        # your camera may have a different streaming format
        # but I think you can figure it out from the debug style below
        source_name = stream_file.readline()    # '--ipcamera'
        content_type = stream_file.readline()    # 'Content-Type: image/jpeg'
        content_length = stream_file.readline()   # 'Content-Length: 19565'
        #print 'confirm/adjust content (source?): ' + source_name
        #print 'confirm/adjust content (type?): ' + content_type
        #print 'confirm/adjust content (length?): ' + content_length
        # find the beginning of the jpeg data BEFORE pulling the jpeg framesize
        # there must be a more efficient way, but hopefully this is not too bad
        b1 = b2 = b''
        while True:
            b1 = stream_file.read(1)
            while b1 != chr(0xff):
                b1 = stream_file.read(1)
            b2 = stream_file.read(1)
            if b2 == chr(0xd8):
                break
        # pull the jpeg data
        framesize = int(content_length[16:])
        jpeg_stripped = b''.join((b1, b2, stream_file.read(framesize - 2)))
        # throw away the remaining stream data. Sorry I have no idea what it is
        junk_for_now = stream_file.readline()
        # convert directly to an Image instead of saving / reopening
        # thanks to SO: http://stackoverflow.com/a/12020860/377366
        image_as_file = io.BytesIO(jpeg_stripped)
        image_as_pil = Image.open(image_as_file)
        draw = ImageDraw.Draw(image_as_pil)
        draw.text((0, 0), cname, fill="white")
        draw.text((0, 10), dnow, fill="white")
        draw.text((0, 20), dnow1, fill="white")
        img_name = "CamA-" + str('%010d' % y) + ".jpg"
        img_path = os.path.join(wdir, img_name)
        image_as_pil.save(img_path)

def CameraB(time_count, x):
    x = x
    h = httplib.HTTP(stream_urlB)
    h.putrequest('GET', '/videostream.cgi')
    h.putheader('Authorization', 'Basic %s' % base64.encodestring('%s:%s' % (usernameB, password))[:-1])
    h.endheaders()
    errcode, errmsg, headers = h.getreply()
    stream_file = h.getfile()
    start = time.time()
    end = start + time_count
    while time.time() <= end:
    x += 1
        now = datetime.now()
        dte = str(now.day) + "-" + str(now.month) + "-" + str(now.year)
        dte1 = str(now.hour) + ":" + str(now.minute) + ":" + str(now.second) + "." + str(now.microsecond)
        cname = "Cam#: CamB"
        dnow = """Date: %s """ % dte
        dnow1 = """Time: %s""" % dte1
        # your camera may have a different streaming format
        # but I think you can figure it out from the debug style below
        source_name = stream_file.readline()    # '--ipcamera'
        content_type = stream_file.readline()    # 'Content-Type: image/jpeg'
        content_length = stream_file.readline()   # 'Content-Length: 19565'
        #print 'confirm/adjust content (source?): ' + source_name
        #print 'confirm/adjust content (type?): ' + content_type
        #print 'confirm/adjust content (length?): ' + content_length
        # find the beginning of the jpeg data BEFORE pulling the jpeg framesize
        # there must be a more efficient way, but hopefully this is not too bad
        b1 = b2 = b''
        while True:
            b1 = stream_file.read(1)
            while b1 != chr(0xff):
                b1 = stream_file.read(1)
            b2 = stream_file.read(1)
            if b2 == chr(0xd8):
                break
        # pull the jpeg data
        framesize = int(content_length[16:])
        jpeg_stripped = b''.join((b1, b2, stream_file.read(framesize - 2)))
        # throw away the remaining stream data. Sorry I have no idea what it is
        junk_for_now = stream_file.readline()
        # convert directly to an Image instead of saving / reopening
        # thanks to SO: http://stackoverflow.com/a/12020860/377366
        image_as_file = io.BytesIO(jpeg_stripped)
        image_as_pil = Image.open(image_as_file)
        draw = ImageDraw.Draw(image_as_pil)
        draw.text((0, 0), cname, fill="white")
        draw.text((0, 10), dnow, fill="white")
        draw.text((0, 20), dnow1, fill="white")
        img_name = "CamB-" + str('%010d' % x) + ".jpg"
        img_path = os.path.join(wdir, img_name)
        image_as_pil.save(img_path)

if __name__ == '__main__':
    main()

EDIT (26/05/2014):

I have spent the better part of 2 months trying to update this script / program to work with python 3 but have been completely unable to get it to do anything. would anyone be able to point me in the right direction?

I have tried the 2to3 script but it just changed a couple of entries and I still was unable to get it to function at all.

هل كانت مفيدة؟

المحلول

*edit I previously blamed GIL for the behavior which was silly. This is an I/O bound process, not a CPU-bound process. So multiprocessing is not a meaningful solution.


*update I finally found a demo ip camera with the same streaming interface as yours (I think). Using the streaming interface, it only makes a connection once and then reads from the stream of data as if it were a file to extract jpg image frames. With the code below, I grabbed for 2 seconds ==> 27 frames which I believe extrapolates to about 300k images in a 7 hour period.

If you want to get even more, you would move the image modification and file writing to a separate thread and have a worker doing that while the main thread just grabs from the stream and sends jpeg data to the worker.

import base64
from datetime import datetime
import httplib
import io
import os
import time

from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw


wdir = "workdir"
stream_url = ''
username = ''
password = ''


def main():
    time_count = 2
    looper_stream(time_count)


def looper_stream(time_count):
    h = httplib.HTTP(stream_url)
    h.putrequest('GET', '/videostream.cgi')
    h.putheader('Authorization', 'Basic %s' % base64.encodestring('%s:%s' % (username, password))[:-1])
    h.endheaders()
    errcode, errmsg, headers = h.getreply()
    stream_file = h.getfile()
    start = time.time()
    end = start + time_count
    while time.time() <= end:
        now = datetime.now()
        dte = str(now.day) + "-" + str(now.month) + "-" + str(now.year)
        dte1 = str(now.hour) + "-" + str(now.minute) + "-" + str(now.second) + "." + str(now.microsecond)
        cname = "Cam1-"
        dnow = """Date: %s """ % dte
        dnow1 = """Time: %s""" % dte1
        # your camera may have a different streaming format
        # but I think you can figure it out from the debug style below
        source_name = stream_file.readline()    # '--ipcamera'
        content_type = stream_file.readline()    # 'Content-Type: image/jpeg'
        content_length = stream_file.readline()   # 'Content-Length: 19565'
        print 'confirm/adjust content (source?): ' + source_name
        print 'confirm/adjust content (type?): ' + content_type
        print 'confirm/adjust content (length?): ' + content_length
        # find the beginning of the jpeg data BEFORE pulling the jpeg framesize
        # there must be a more efficient way, but hopefully this is not too bad
        b1 = b2 = b''
        while True:
            b1 = stream_file.read(1)
            while b1 != chr(0xff):
                b1 = stream_file.read(1)
            b2 = stream_file.read(1)
            if b2 == chr(0xd8):
                break
        # pull the jpeg data
        framesize = int(content_length[16:])
        jpeg_stripped = b''.join((b1, b2, stream_file.read(framesize - 2)))
        # throw away the remaining stream data. Sorry I have no idea what it is
        junk_for_now = stream_file.readline()
        # convert directly to an Image instead of saving / reopening
        # thanks to SO: http://stackoverflow.com/a/12020860/377366
        image_as_file = io.BytesIO(jpeg_stripped)
        image_as_pil = Image.open(image_as_file)
        draw = ImageDraw.Draw(image_as_pil)
        draw.text((0, 0), cname, fill="white")
        draw.text((0, 10), dnow, fill="white")
        draw.text((0, 20), dnow1, fill="white")
        img_name = "Cam1-" + dte + dte1 + ".jpg"
        img_path = os.path.join(wdir, img_name)
        image_as_pil.save(img_path)


if __name__ == '__main__':
    main()

*jpg capture below doesn't seem fast enough which is logical. making so many http requests would be slow for anything.

from datetime import datetime
import io
import threading
import os
import time

import urllib2

from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw


wdir = "workdir"


def looper(time_count, loop_name):
    start = time.time()
    end = start + time_count
    font = ImageFont.truetype("/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf", 10)
    while time.time() <= end:
        now = datetime.now()
        dte = str(now.day) + "-" + str(now.month) + "-" + str(now.year)
        dte1 = str(now.hour) + "-" + str(now.minute) + "-" + str(now.second) + "." + str(now.microsecond)
        cname = "Cam1-"
        dnow = """Date: %s """ % dte
        dnow1 = """Time: %s""" % dte1
        image = urllib2.urlopen('http://(ip address)/snapshot.cgi?user=uname&pwd=password').read()
        # convert directly to an Image instead of saving / reopening
        # thanks to SO: http://stackoverflow.com/a/12020860/377366
        image_as_file = io.BytesIO(image)
        image_as_pil = Image.open(image_as_file)
        draw = ImageDraw.Draw(image_as_pil)
        draw_text = "\n".join((cname, dnow, dnow1))
        draw.text((0, 0), draw_text, fill="white", font=font)
        #draw.text((0, 0), cname, fill="white", font=font)
        #draw.text((0, 10), dnow, fill="white", font=font)
        #draw.text((0, 20), dnow1, fill="white", font=font)
        img_name = "Cam1-" + dte + dte1 + "(" + loop_name + ").jpg"
        img_path = os.path.join(wdir, img_name)
        image_as_pil.save(img_path)


if __name__ == '__main__':
    time_count = 5
    threads = list()
    for i in range(2):
        name = str(i)
        t = threading.Thread(target=looper, args=(time_count, name))
        threads.append(p)
        t.start()
    for t in threads:
        t.join()

نصائح أخرى

The speeds you are getting for the implementation you have given are not bad.

You are writing about 4.5 frames per second (fps), and zoneminder is writing out nearly 10 fps. Below is your flow diagram with a few comments to speed things up

  1. You are reading the url buffer (network latency),
  2. then writing the image (disk latency 1) (you might not need to write the image here to disk - consider passing it directly to your img class)
  3. reading the image (disk latency 2)
  4. then manipulating the image using fonts, text boxes etc... (3 image draws) - Can you build one string with newlines so that you only make one call to the draw.text function?
  5. writing the output image (disk latency 3)

There are a couple of things that might help.

  • Lift the font-opening from the function to the main code, then pass in the font object (opening a font is likely to be non-trivial in time, by doing it once, you're not taking the time hit for every image; you never modify the font on the fly, so sharing the same font object should be OK).

  • You can probably scrap two of the three lines that contain:

    draw = ImageDraw.Draw(img)

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top