Multithreaded Image Viewer

John Hurst

Version 2.6.0

20071229:082507

Table of Contents

1 Overview
1.1 The Main Body
2 Definitions
2.1 Define Imports
2.2 Define Global Variables
3 Set Up
3.1 Handle Options
3.2 Initialization
3.3 Start the Main Loop
3.4 Display Image
3.5 End Main Loop
4 Threads
4.1 Define the Keyboard Thread
4.2 Define the Update Database Thread
5 Subroutines
5.1 Compute next image
6 Macros
7 The Image Class
7.1 class Image
7.2 class ImageList
7.3 image class utility routines
8 TO DO
9 Indices
9.1 Identifier Index
9.2 Chunk Index
9.3 File Index
9.4 Define Version Information


1. Overview

This document describes a python program to display background images on the Mac OS X desktop. It accesses an XML database containing a list of images for display, along with `scoring' data on the images themselves. The score of an image can be recorded by the program, and previously recorded scores can be used to determine the length of time an image is displayed.

The display operation is handled separately through a system call to an external script setBackground.sh.

1.1 The Main Body

"rootImage.py" 1.1 =
#!/usr/bin/python <define version information 9.3> <define imports 2.1> <define global variables 2.2> <define subroutines 5.1> <define the keyboard thread 4.1> <define the update database thread 4.6> if __name__ == '__main__': <run the main code 1.2> pass # end of if main pass # end of program

Define everything, then execute the main body of code.

<run the main code 1.2> =
xmlfilen='/home/ajh/etc/LSL06Images.xml'{Note 1.2.1} start=-1 <handle options 3.1> <do initialization 3.2> while running and not shutdown: <start main loop 3.6> <determine pause time 3.7> <display image and pause 3.8> <end main loop 3.9> pass # end of while if debug: print "main quitting ..."
Chunk referenced in 1.1
{Note 1.2.1}
The file containing the image database

Execute the main body of code. The flag running is true while we wish to display images, and the flag shutdown is set when the program enters its shutdown routine. The second flag is necessary, as each thread must be terminated in the shutdown routine before the program stops completely.

2. Definitions

2.1 Define Imports

<define imports 2.1> =
import getopt import image import math import os import Queue import random import re from subprocess import PIPE,Popen import sys import threading import time import xml.dom from xml.dom.minidom import parse from xml.dom.ext import PrettyPrint from image import *
Chunk referenced in 1.1

Grab all the imported modules here.

getopt
is used to perform option handling <handle options 3.1>;
image
is a private mode to handle `image' elements in the XML database;
math
is used for exponentiation in <determine pause time 3.7>;
os
for file operations;
Queue
for the input queue handling <handle input queue 4.2>;
random
for selecting images at random <define the compute next image routine 5.2>;
re
for various regular expression handling;
subprocess
for handling the desktop display <display image and pause 3.8>;
sys
threading
time
xml.dom
xml.dom.minidom
xml.dom.ext
for the PrettyPrint routine. This is used to output the updated XML database <define the update database thread 4.6>

2.2 Define Global Variables

<define global variables 2.2> =
debug{Note 2.2.1}=0 running{Note 2.2.2}=1 ; shutdown=0 userReq='~'; userScore=-1; userLock=threading.Lock() modified{Note 2.2.3}=0 modifyLock{Note 2.2.4}=threading.Lock() inputQueue{Note 2.2.5}=Queue.Queue(80) timerCV=threading.Condition() dom=None ignoreScored{Note 2.2.6}=0 timeOut=1 imageNo=0 hold=0 next=0 noSkip=0{Note 2.2.7} skipped=0{Note 2.2.8} randomImages=0{Note 2.2.9} defaultTime=30.0{Note 2.2.10}
Chunk referenced in 1.1
{Note 2.2.1}
flag to indicate that debugging output should be generated.
{Note 2.2.2}
set while we want images to be displayed
{Note 2.2.3}
flag to indicate that database has been modified, and not yet updated.
{Note 2.2.4}
A lock variable to control thread access to global variables.
{Note 2.2.5}
queue to handle input requests
{Note 2.2.6}
flag to indicate that scored entries are to be skipped in displaying
{Note 2.2.7}
set when we do not want to skip images due to explicit navigation to the image.
{Note 2.2.8}
count of contiguous images skipped: if this gets past the number of images in the file, quit.
{Note 2.2.9}
set to indicate random choice of image
{Note 2.2.10}
default length of time image is displayed if no count data

3. Set Up

3.1 Handle Options

<handle options 3.1> =
(vals,path{Note 3.1.1})=getopt.getopt(sys.argv[1:],'dfinrs:vVx', ['debug','file=','ignoreScored','next','random',\ 'start=','verbose','version','images=']) for (opt,val) in vals: if opt=='-d' or opt=='--debug': debug=1 elif opt=='-i' or opt=='--ignoreScored': ignoreScored=1 elif opt=='--images': xmlfilen=val elif opt=='-n' or opt=='--next': next=1 elif opt=='-r' or opt=='--random': randomImages=1 elif opt=='-s' or opt=='--start': if val: start=int(val) else: print "no value for starting image number" sys.exit(1) elif opt=='-v' or opt=='--verbose': verbose=1 elif opt=='-V' or opt=='--version': print versiondate,version,versioninfo sys.exit(0) else: pass
Chunk referenced in 1.2
{Note 3.1.1}
path is used later to add images to the XML database

The following options are available:

-d/--debug
turn on loads of debugging output
-i/--ignoreScored
Do not display images that have a score associated with them. (This is to facilitate processing of large numbers of images.)
--images=file
Use the file file for the image database
-n/--next
Display the next image and quit.
-r/--random
choose images randomly. If used in conjunction with -n, choose one image randomly, display it, and exit.
--start=number
Start the display at image number number.
-V/--version
Display the version information and quit.

3.2 Initialization

<do initialization 3.2> =
<initialize the dom tree 3.3,3.4> # start the input thread, but not if single shotting (next==1) if not next: inputThread=threading.Thread(target=readKB,args=()) inputThread.start() # start the output thread outputThread=threading.Thread(target=updateDB,args=(xmlfilen,dom)) outputThread.start() images=dom.getElementsByTagName('image') rootElem=dom.getElementsByTagName('images')[0] workingdir=rootElem.getAttribute('directory') if workingdir: if workingdir[-1]!='/': workingdir+='/' nextTD=rootElem.getAttribute('nextToDisplay') if start>-1: imageNo=start else: imageNo=int(nextTD) if next: imageNo+=1 if debug: print "imageNo incremented to %d" % (imageNo) if imageNo>=len(images): imageNo=0 <debug 6.1>(msg='"imageNo=%d (image list size = %d)" % (imageNo,len(images))')
Chunk referenced in 1.2
<initialize the dom tree 3.3> =
if os.path.isfile(xmlfilen): # Parse the input XML file xmlfile=open(xmlfilen,'r') dom=parse(xmlfile) xmlfile.close() imagelist=buildImageList(dom) else: # Build an (empty) XML dom tree for subsequent population domimp=xml.dom.getDOMImplementation() doctype=domimp.createDocumentType('images',None,None) dom=domimp.createDocument(None,'images',doctype) if debug: PrettyPrint(dom,sys.stdout) images=dom.documentElement images.setAttribute('nextToDisplay','0') imagelist={}
Chunk referenced in 3.2
Chunk defined in 3.3,3.4

If the input file exists and is an XML file, we parse it for the list of files to display. Otherwise create an empty dom tree, which will be filled later.

<initialize the dom tree 3.4> =
if len(path)>0: # Add images from the file list to the DOM tree images=dom.documentElement for image in path: <make image element from image name 3.5> if debug: PrettyPrint(dom,sys.stdout) # build image list from the new dom tree imagelist=buildImageList(dom)
Chunk referenced in 3.2
Chunk defined in 3.3,3.4

If we are given a command line list of files, add these to the DOM tree. Then scan it for the list of images to process.

<make image element from image name 3.5> =
# 'image' is the image name, 'imageElement' is the constructed element if not imagelist.has_key(image): imageElement=dom.createElement('image') imageElement.setAttribute('name',image) images.appendChild(imageElement) else: print "%s is already in the database, not added" % (image)
Chunk referenced in 3.4

3.3 Start the Main Loop

<start main loop 3.6> =
<debug 6.1>(msg='"main running=%d, shutdown=%d" % (running,shutdown)') threadList=threading.enumerate() if debug: for th in threadList: print "thread %s" % (th)
Chunk referenced in 1.2

Start the main loop by issuing a debug message if required, identifying the facts that a) the loop has started, and b) the threads that are running.

3.4 Display Image

<determine pause time 3.7> =
<debug 6.1>(msg='"main determining pause time"') power=0.0 count=images[imageNo].getAttribute('count') if debug: print "noSkip=%d at entry to ignore loop" % (noSkip) if ignoreScored and not noSkip: while count and not shutdown: print "skipped %d" % (imageNo) skipped+=1 lenImages=len(images) if skipped>lenImages: shutdown=1 {Note 3.7.1} print "Nothing to display!" imageNo=nextImage(imageNo) count=images[imageNo].getAttribute('count') if count: count=int(count) image=images[imageNo].getAttribute('name') score=images[imageNo].getAttribute('score') if count: count=int(count) score=int(score) if count: power=float(score)/float(count) else: power=2.0 # arbitrary value else: count=0;score=0; power=0.0 sleeptime=defaultTime if noSkip: if debug: print "noSkip causes count check branch avoidance" hold=1 elif count: if score>0: sleeptime=math.pow(2,power-1) else: sleeptime=0.0 if hold: sleeptime=128.0 msg='holding' else: msg='background' noSkip=0 if debug: print "noSkip reset" print "%s %d: %s for %3.1f (score:%3.1f)" % (msg,imageNo,image,sleeptime,power)
Chunk referenced in 1.2
{Note 3.7.1}
If we have skipped lenImages in a row, then stop running
<display image and pause 3.8> =
<debug 6.1>(msg='"main displaying image %s" % image') if inputQueue.empty() and sleeptime>0: if image[0]=='/': filename=image else: filename="%s%s" % (workingdir,image) (root,ext)=os.path.splitext(filename) if ext=='': filename=filename+'.JPG' if os.path.isfile(filename): if debug: print "main displaying image %s" % filename args=["/home/ajh/bin/setBackground.sh", filename] output=Popen(args,stdout=PIPE).communicate()[0] skipped=0 if debug: print output if next: shutdown=1 else: timerCV.acquire() timeOut=1 timerCV.wait(sleeptime) timerCV.release() if timeOut: imageNo=nextImage(imageNo) else: print "No such image: %s" % (image) images[imageNo].setAttribute('score',"%d" % 0) images[imageNo].setAttribute('count',"%d" % 1) modifyLock.acquire() modified=1 modifyLock.release() imageNo=nextImage(imageNo) if debug: print "finish display: (imageNo)=(%d)" % (imageNo) else: imageNo=nextImage(imageNo) hold=0
Chunk referenced in 1.2

Collect the filename of the image to be displayed, and check that it is OK. Run a system command pipe to set the display image via the command setBackground.sh (so that we can check its output if necessary), and start the timer (if this is not a single shot display).

If there is no image, update the datebase to give it a score of zero (which means don't attempt to display it again).

3.5 End Main Loop

<end main loop 3.9> =
<debug 6.1>(msg='"main updates nextToDisplay"') rootElem.setAttribute('nextToDisplay',"%d" % (imageNo)) <set main loop modification flag 3.10>
Chunk referenced in 1.2

Issue a debug message to indicate that we have reached the end of the main loop. As we have also updated some variables, indicate this by setting the nextToDisplay attribute in the database, and the modified flag. Since this is a shared variable between the threads, modified must be protected by a lock.

<set main loop modification flag 3.10> =
if debug: print "main waits to acquire modifyLock" modifyLock.acquire() if debug: print "main acquires modifyLock" modified=1 modifyLock.release() if debug: print "main releases modifyLock"
Chunk referenced in 3.9

Flag the fact that database has been modified, and use the modifyLock lock variable to avoid mutual update.

4. Threads

4.1 Define the Keyboard Thread

<define the keyboard thread 4.1> =
def readKB(): global debug,\ running,shutdown,inputQueue,timeOut,imageNo,\ modified,hold,noSkip # loop forever (while running), reading keyboard input line="" while running and not shutdown: if debug: print "readKB: start read" line=sys.stdin.readline() line.strip() if debug: print "readKB: end read, got '%s'" % (line) while line: {Note 4.1.1} # check for score/command input res=re.match('(([0-9]+)|([a-z])|.)(.*)$',line) if res: if debug: print "readKB: match a command: %s" % (line) command=res.group(1) inputQueue.put(command) line=res.group(4) pass # end of while line <handle input queue 4.2> pass
Chunk referenced in 1.1
{Note 4.1.1}
put commands on line into input queue (see below)

The keyboard thread is responsible for reading characters from the keyboard and assembling them into commands. It loops continuously while images are being displayed, waiting for input, and using a regular expression to recognize command strings. Each command (there may be more than one per line) is extracted on each cycle of the while line command. The queue of commands is then processed by chunk <handle input queue 4.2>

<handle input queue 4.2> =
while not inputQueue.empty(): # we have a command, update data and abort timer in the main program if debug: print "while not inputEmpty starts with (imageNo)=(%d)" % (imageNo) command=inputQueue.get() if debug: print "handling command %s" % (command) res=re.match('([0-9]+)',command) if res: <handle an input score 4.3> res=re.match('([a-z])',command) if res: <handle an input command 4.4> if debug: print "readKB computes next image as %d" % (imageNo) print "readKB: cancel time out on display" timerCV.acquire() timeOut=0 timerCV.notify() timerCV.release() if debug: print "readKB: timer done" if shutdown: if debug: print "readKB returns" return pass # end of while not inputQueue.empty()
Chunk referenced in 4.1

While there are input commands on the queue, extract one, decompose it (some commands have parameters), and handle it appropriately. Notice the use of a Condition variable from the Threading module to handle a forced termination of the display of the current image. In particular, the notify call signals the wait in chunk <display image and pause 3.8>, thus terminating the timer for the (current) image that is being displayed.

<handle an input score 4.3> =
userScore=int(res.group(1)) if debug: print "got a score '%d'" % (userScore) userLock.acquire() count=images[imageNo].getAttribute('count') score=images[imageNo].getAttribute('score') if count: count=int(count) score=int(score) count+=1 score+=userScore else: count=1 score=userScore images[imageNo].setAttribute('score',"%d" % score) images[imageNo].setAttribute('count',"%d" % count) if debug: image=images[imageNo].getAttribute('name') print "score updated (%d,%d) for %s" % (count,score,image) userScore=-1 userLock.release() modifyLock.acquire() modified=1 modifyLock.release() imageNo=nextImage(imageNo) hold=0
Chunk referenced in 4.2

User has entered a score for this image. Update the (local copy of the) database, and mark it modified. Release any hold.

<handle an input command 4.4> =
userReq=res.group(1) if debug: print "got a command '%s'" % (userReq) userLock.acquire() hold=0 if userReq=='d': # delete image from db <delete this image 4.5> elif userReq=='g': # go to a particular image pass # yet to figure this out elif userReq=='h': # hold this image hold=1 elif userReq=='n': imageNo=(imageNo+1) % len(images) elif userReq=='p': imageNo=(imageNo-1) % len(images) elif userReq=='v': # toggle debug mode debug = not debug elif userReq=='q': threadList=threading.enumerate() if debug: for th in threadList: print "thread %s" % (th) print "waiting to update database ..." shutdown=1 if debug: print "readKB quitting ..." noSkip=1 if debug: print "noSkip set" userReq='~' userLock.release()
Chunk referenced in 4.2

Parse a single character command.

<delete this image 4.5> =
delel=images[imageNo] parent=delel.parentNode parent.removeChild(delel) del images[imageNo]
Chunk referenced in 4.4

4.2 Define the Update Database Thread

<define the update database thread 4.6> =
def updateDB(xmlfilen,dom): global running,shutdown,modified,modifyLock while running: if modified: modifyLock.acquire() xmlout=open(xmlfilen+'.new','w') PrettyPrint(dom,xmlout) xmlout.close() os.rename(xmlfilen+'.new',xmlfilen) modified=0 modifyLock.release() if shutdown: <debug 6.1>(msg='"updateDB quitting ..."') running=0 return else: time.sleep(2.0)
Chunk referenced in 1.1

This thread is quite simple. It runs a main loop, as long as running is required, and on each iteration of the loop, updates the main database if data has been modified. If a shutdown is imminent, turn the running variable off only when it is safe to do so (DB has been updated and modified is off). Otherwise, pause for 2 seconds to give other things a chance to run. (Note: this could be a bit smarter, by making modified a signal.)

5. Subroutines

<define subroutines 5.1> =
<define the compute next image routine 5.2>
Chunk referenced in 1.1

5.1 Compute next image

<define the compute next image routine 5.2> =
def nextImage(n): global randomImages lenImages=len(images) if randomImages: return random.randrange(0,lenImages,1) else: n = (n+1) % lenImages return n
Chunk referenced in 5.1

Compute the index of the next image. If choosing randomly, choose a value between 0 and len(images)-1. Otherwise increment the current index modulo the number of images.

6. Macros

Define some useful macros

<debug 6.1> = if debug: print msg
Chunk referenced in 3.2 3.6 3.7 3.8 3.9 4.6

7. The Image Class

Handling of the image database is done in this class, defined as a separate module.

"image.py" 7.1 =
<classImage 7.2> <classImageList 7.3> <image class utility routines 7.4>

This module defines two key components: the dom and the imagelist.

7.1 class Image

<classImage 7.2> =
class Image: def __init__(self,name='',elem=None): self.name=name self.orgname='' self.albumpath='' self.totalvotes=0 self.votescast=0 self.score=0.0 self.elem=elem def setOrgName(self,name): self.orgname=name def setAlbumPath(self,path): self.albumpath=path def setTotalVotes(self,votes): self.totalvotes=votes def incTotalVotes(self,votes): self.totalvotes+=votes self.votescast+=1 self.score=self.totalvotes/self.votescast def resetTotalVotes(self): self.totalvotes=0 def str(self): out='<image name="%s" ' % (self.name) out+= 'orgname="%s" ' % (self.orgname) out+= 'albumpath="%s" ' % (self.albumpath) out+= 'totalvotes="%d" ' % (self.totalvotes) out+= 'votescast="%d" ' % (self.votescast) out+= 'score="%5.2f" ' % (self.score) out+= '/>\n' return out
Chunk referenced in 7.1

7.2 class ImageList

<classImageList 7.3> =
class ImageList: def __init__(self): self.list={} def addImage(self,image): name=image.name if self.list.has_key(name): print "already have image %s" % (name) else: self.list[name]=image
Chunk referenced in 7.1

7.3 image class utility routines

<image class utility routines 7.4> =
def buildImageList(dom): imagelist={} images=dom.getElementsByTagName('image') for image in images: name=image.getAttribute('name') #print "got image name %s" % (name) imagelist[name]=image return imagelist def setImageAttr(dom,imagelist,name,attr,value): if imagelist.has_key(name): image=imagelist[name] thisname=image.getAttribute('name') #print "checking image name %s" % (thisname) if thisname==name: image.setAttribute(attr,value) return # no element found, must add a new one image=dom.createElement('image') image.setAttribute('name',name) image.setAttribute(attr,value) images=dom.getElementsByTagName('images')[0] imagelist[name]=image images.appendChild(image) def getImageAttr(dom,imagelist,name,attr): if imagelist.has_key(name): image=imagelist[name] thisname=image.getAttribute('name') #print "checking image name %s" % (thisname) if thisname==name: return image.getAttribute(attr) # no element found, return empty return '' def sortImages(dom,imagelist): keys=imagelist.keys() keys.sort() images=dom.getElementsByTagName('images')[0] for key in keys: print "removing key %s" % (key) imageElem=imagelist[key] images.removeChild(imageElem) for key in keys: print "adding key %s" % (key) imageElem=imagelist[key] images.appendChild(imageElem)
Chunk referenced in 7.1

8. TO DO

9. Indices

9.1 Identifier Index

Identifier Defined in Used in
buildImageList 7.4
debug 2.2 1.2, 3.1, 3.2, 3.6, 3.7, 3.7, 3.7, 3.8, 3.10, 3.10, 3.10, 4.1, 4.1, 4.1, 4.1, 4.2, 4.2, 4.2, 4.2, 4.2, 4.3, 4.3, 4.4, 4.4, 4.4, 4.4, 4.4, 4.4, 6.1
defaultTime 2.2
modified 2.2 3.8, 3.10, 4.1, 4.3, 4.6, 4.6, 4.6
modifyLock 2.2 3.8, 3.8, 3.10, 3.10, 4.3, 4.3, 4.6, 4.6, 4.6
nextImage 5.2 3.7, 3.8, 3.8, 3.8, 4.3
path 3.1
randomImages 2.2 5.2
skipped 2.2 3.7, 3.7, 3.8
timerCV 2.2 3.8, 3.8, 3.8, 4.2, 4.2, 4.2
workingdir 3.2 3.2, 3.2, 3.2, 3.8
xmlfilen 1.2 3.1, 3.2, 3.3, 4.6, 4.6, 4.6, 4.6

9.2 Chunk Index

Chunk Name Defined in Used in
classImage 7.2 7.1
classImageList 7.3 7.1
debug 6.1 3.2, 3.6, 3.7, 3.8, 3.9, 4.6
define global variables 2.2 1.1
define imports 2.1 1.1
define subroutines 5.1 1.1
define the compute next image routine 5.2 5.1
define the keyboard thread 4.1 1.1
define the update database thread 4.6 1.1
define version information 9.3 1.1
delete this image 4.5 4.4
determine pause time 3.7 1.2
display image and pause 3.8 1.2
do initialization 3.2 1.2
end main loop 3.9 1.2
handle an input command 4.4 4.2
handle an input score 4.3 4.2
handle input queue 4.2 4.1
handle options 3.1 1.2
image class utility routines 7.4 7.1
initialize the dom tree 3.3, 3.4 3.2
make image element from image name 3.5 3.4
run the main code 1.2 1.1
set main loop modification flag 3.10 3.9
start main loop 3.6 1.2
version 9.1 9.3
version date 9.2 9.3

9.3 File Index

File Name Defined in
image.py 7.1
rootImage.py 1.1

9.4 Define Version Information

<version 9.1> = 2.6.0
Chunk referenced in 9.3
<version date 9.2> = 20071229:082507
Chunk referenced in 9.3
<define version information 9.3> =
version='<version 9.1>' versioninfo='added "d" delete command (debug renamed to "v")' versiondate='<version date 9.2>'
Chunk referenced in 1.1

Document History

20071229:082507 ajh 2.6.0 added "d" delete command (debug renamed to "v")
20071229:080505 ajh 2.5.2 allow absolute or relative file names in image list
20070912:112557 2.5.1 ajh fixed bug in generating new image list
20070630:171701 ajh 2.5.0 include the image module in this literate program
20070528:141128 ajh 2.4.1 literate program amplification
20070524:154309 ajh 2.4.0 added build of XML file form file list
20070506:114936 ajh 2.3.0 added random choice of image
20070418:161100 ajh 2.2.2 toggle debug mode command added
20070417:121047 ajh 2.2.1 added database recovery
20070413:112145 ajh 2.2.0 added 'next' option