2

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.

ButtzyB
  • 77
  • 2
  • 2
  • 9
  • One change or may be small improve use genratar expression and sum (instead of len that need sequence) as : `sum(1 for f in os.listdir(wdir) if f.startswith('CamFront') and os.path.isfile(os.path.join(wdir, f)))` – Grijesh Chauhan Oct 11 '13 at 12:01
  • well that part is just to check if there are images already in the working directory to find out if the counter starts at 1 or at another number. it is more the rate of capture i am trying to improve in the looper function. and for that improvement areyou saying replace the entire y = part with just y = sum(1 for f in os.listdir(wdir) if f.startswith('CamFront') ? – ButtzyB Oct 11 '13 at 12:06
  • I am also new Python learner. I just read some where `sun(genrator expression)` is better then `len([listcompresion])`. Of-course this is not an answer to your question. I wish I could but at this stage I am not able to contribute :( :( – Grijesh Chauhan Oct 11 '13 at 12:13
  • 1
    The important question is - what is currently taking most of the resources. Is it slacking on system calls? Or networking? Or disk? Or CPU? Definitions of your hardware and how is the load on each piece of it is much more helpful than just showing us some code, without any attempt at profiling the issue and hoping that someone will do it for you. – Tymoteusz Paul Oct 11 '13 at 12:15
  • well i dont think disk or cpu would be the problem as it is running on an i7 930 cpu, ubuntu server os on a pcie ssd, images go to a sata3 hdd. as for cpu load never seems to be very high when the script is running i just ran it for 30 seconds and highest cpu load was 18% on 1 core while grabbing from 2 camera's. – ButtzyB Oct 11 '13 at 12:22
  • on the network side another 30 second test shows 400 - 500kb of traffic from the server while another person in the house is watching a movie from server at the same time – ButtzyB Oct 11 '13 at 12:27
  • Then your problem is clearly not within efficiency of the script but wrong utilization of resources, be it cpu (if you are not running enough threads) or networking (if it's slow and your application blocks while waiting for data instead of spawning another process to utilize CPU) and so on. It is a very broad subject too broad for stack over flow in my mind. By the way when you reply to someone in comments make sure to mention them with @Puciek (for example) so they get notified. – Tymoteusz Paul Oct 11 '13 at 12:29
  • thanks sorry i am new to stack overflow so not sure of how to use those little tricks yet. with the code i put up in my mind it should have 2 threads both grabbing images at the same time incrementing a counter and both writeing seperate images to disk at once. yet it does not seem to be functioning that way. @Puciek – ButtzyB Oct 11 '13 at 12:33
  • Well, what did you do to try to profile the issue? Trust me that no one here is going to run and profile it for you. What you have to do is put in place proper logging system (using the fabulous python logging facility), preferably with time stamps and see what is going on there, what is holding it and so on. – Tymoteusz Paul Oct 11 '13 at 12:36
  • i have added print statements to the code at the loop start, then after first wrieting the file, then at the end after adding date and time. It appears to take about 0.2 of a second to grab an image, write image to disk, then open it add date + time and write it again back to disk. @Puciek – ButtzyB Oct 11 '13 at 12:46
  • i think the problem lies in the threading of the looper function and locking / using of the y variable. for instance if it is running 2 threads thread 1 is grabbing and writeing image-00001.jpg while thread 2 should be grabbing and writeing image-00002.jpg. – ButtzyB Oct 11 '13 at 12:54
  • Comments are really not a place for lengthy discussion and as I said before this is a very broad topic. You need to sit down, put a lot of debugging information that allows you to profile the ran of your software, find the bottleneck (whatever it is) and resolve/circumvent it. – Tymoteusz Paul Oct 11 '13 at 13:02
  • I think instead of reading the raw data and writing to files, I would `curl` or `wget` the images to your computer and then use imagemagick to batch add the overlay. – beroe Oct 11 '13 at 19:06
  • http://codereview.stackexchange.com – original_username Oct 12 '13 at 19:26

3 Answers3

3

*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()
KobeJohn
  • 7,390
  • 6
  • 41
  • 62
  • thanks for the reply i tried using the multiprocessing there but with running the script for 10 seconds all images from the second process were taken 10 seconds after process 1. So process 1 ran for the 10 seconds took pictures then process 2 ran for 10 seconds and took more. @kobejohn – ButtzyB Oct 11 '13 at 14:06
  • right. silly me. if you join, then it's going to wait for the first to finish before the second. move the joins out. I've updated the code. – KobeJohn Oct 11 '13 at 14:32
  • i think that might be working they finish about 0.2 of a second apart now intead of the full 10 second trial run apart at most and nearly exactly at same time on a 60 second trial. will that worl if i add a second camera loop into the mix like haveing 4 threads 2 for each camera? or will i need to do something like create process's in the same way for say q instead of p? @kobejohn – ButtzyB Oct 11 '13 at 14:51
  • Assuming all the libraries you are using play well with multiprocessing, then multiple processes, multiple cameras will work the same as above. – KobeJohn Oct 11 '13 at 15:00
  • just tried it yes it does work when i add a second loop and use q instead of p to create process's for it. now to try get what paul sujested below to work and get the text to add in just 1 write. – ButtzyB Oct 11 '13 at 15:00
  • @ButtzyB check the full example code. Does that work any faster? – KobeJohn Oct 11 '13 at 16:49
  • i just tried that code and moved the image open out of the loop at it captured over 1000 images in the 5 seconds but i cant tell if they are all the exact same picture. – ButtzyB Oct 11 '13 at 18:06
  • with your code takeing the image open out of the loops ends up in the same picture being written to a new file every time it loops. – ButtzyB Oct 11 '13 at 18:27
  • @ButtzyB I guess you mean the comment about moving the buffer open out? I tried that and realized urllib2 doesn't work that way so yeah don't do that. I was just looking for a way to reduce overhead in the loop by somehow "connecting" (password etc.) once and reading many times. I removed the comment. Otherwise, it works on my system although I don't have a high speed ip camera to test it well. – KobeJohn Oct 11 '13 at 22:44
  • is there a way to open the camera video stream in python to say /dev/null open that connection once and grab images from that video stream? – ButtzyB Oct 12 '13 at 19:20
  • @ButtzyB do you have a link to the API for your camera? I wonder if it has a streaming interface that your other application is using. – KobeJohn Oct 13 '13 at 13:48
  • it does have a video stream located at (ip address)/videostream.cgi?user=user&pwd=password @kobejohn – ButtzyB Oct 13 '13 at 17:33
  • as for the api i think this might be the closest thing to an api for it http://www.foscam.es/descarga/ipcam_cgi_sdk.pdf @kobejohn – ButtzyB Oct 13 '13 at 17:46
  • I'm really struggling to get an IP feed working to try things with. It seems from the doc you sent that what you could use is the videostream.cgi which seems like it would be more efficient. Found various code doing that. Here is [one example](http://offkilterengineering.com/using-python-and-wxpython-to-display-a-motion-jpeg-from-the-trendnet-wireless-internet-camera/). – KobeJohn Oct 13 '13 at 18:47
  • I think i have the stream part of it working i will update the main post with the code that appears to work taken from that link you gave me. But all the gui parts to display the stream so i know its definatly working are broken i just used a simple print statement to tell me it was still connected. I am using python 2.7.4 on mint 15 if that helps. @kobejohn – ButtzyB Oct 13 '13 at 20:25
  • @ButtzyB I extracted the useful parts of that code and put it above. See if you can get it to work that way. I guess this will eventually be helpful for others who want to read efficiently from an IP camera with python. – KobeJohn Oct 14 '13 at 07:48
  • i just tried your code and thanks for all the help but its failing at this line framesize = int(s[16:]) with the error invalid literal for int() with base 10: '' @kobejohn – ButtzyB Oct 14 '13 at 08:41
  • @ButtzyB I have it working on my system. The trick was to find the beginning of the jpeg data before extracting framesize instead of after. Hope that works. – KobeJohn Oct 14 '13 at 23:13
  • thanks for the help just tried it on a quick run and seems to be alot neater and smoother than the way i managed to get it going in update 2 of my post above. – ButtzyB Oct 15 '13 at 10:43
  • @kobejohn Could you explain the code please. I am new to python and don't understand threading. – praxmon Feb 13 '14 at 13:15
  • @PrakharMohanSrivastava A few points - 1) the code in this answer does not use threading. 2) If you are new to both python and threading, I suggest you avoid threading until you have experience with python or threading in another language AND you have a clear need for threading. If you are having specific problems with this code, please post your problems as a new question. Or try [OpenCV Tutorials](https://opencv-python-tutroals.readthedocs.org/en/latest/py_tutorials/py_tutorials.html) or google for general python tutorials. – KobeJohn Feb 13 '14 at 13:52
  • @kobejohn http://stackoverflow.com/questions/21721813/ip-camera-python-error If you know the solution please answer the question. – praxmon Feb 14 '14 at 03:48
2

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)
Paul
  • 7,155
  • 8
  • 41
  • 40
  • i think i origionaly tried transferring straight from the buffer to adding the text (in 1 command) and only writeing the file once. But the writeing of the text would not work because the file had to be written otherwise there was an error about corruption in the file when trying to add the text. is there a way to open the video stream in python say to /dev/null and grab images from the stream? @Paul – ButtzyB Oct 11 '13 at 14:08
  • ButtzyB: I am not aware of such things in the default libraries. Did you consider the building of one string with all the time stamp info and one call to the draw.text? – Paul Oct 16 '13 at 16:57
  • I have tried that but every way I have tried ends up with a single line of text seperated by a white box. i think kobejohn ran into the same trouble trying to get a single string while helping me put together the new implementation. @Paul – ButtzyB Oct 17 '13 at 11:28
0

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)

Vatine
  • 20,782
  • 4
  • 54
  • 70