Пример #1
0
class VideoBuffer( object ):
	
	def __init__( self, camera, refTime=None, dirName='.', fps=25, bufferSeconds=4.0, owner=None, burstMode=True ):
		self.camera = camera
		self.refTime = refTime if refTime is not None else now()
		self.dirName = dirName
		self.fps = fps
		self.frameMax = int(fps * bufferSeconds)
		
		self.frameDelay = 1.0 / fps
		self.frameDelayTimeDelta = timedelta(seconds=self.frameDelay)
		
		self.frameSaver = None
		self.fcb = FrameCircBuf()
		self.owner = owner			# Destination to send photos after they are taken.
		self.burstMode = burstMode
		
		self.timer = CallbackTimer( self.recordVideoFrame )
		self.reset()
	
	def setOwner( self, owner=None ):
		self.owner = owner
	
	def reset( self ):
		if self.frameSaver and self.frameSaver.is_alive():
			self.frameSaver.stop()
		
		if self.timer.IsRunning():
			self.timer.Stop()
		
		self.fcb.reset( self.frameMax )
		self.frameCount = 0
		
		self.frameSaver = FrameSaver()
		self.frameSaver.start()
		
		self.tFindLast = now() - timedelta( days=1 )
		
		self.lastSampleTime = now()
		self.sampleCount = 0
	
	def recordVideoFrame( self ):
		tNow = now()
		self.sampleCount += 1
		if not self.camera:
			return
			
		try:
			image = self.camera.getImage()
			self.fcb.append( tNow, image )
			if self.owner is not None:
				wx.PostEvent( self.owner, PhotoEvent(t=tNew, photo=image) )
		except Exception as e:
			logException( e, sys.exc_info() )

	def getFrameRate( self ):
		tNow = now()
		rate = self.sampleCount / (tNow -self.lastSampleTime).total_seconds()
		self.lastSampleTime = tNow
		self.sampleCount = 0
		return rate
	
	def start( self ):
		self.reset()
		milliseconds = int(self.frameDelay * 1000.0)
		self.timer.Start( milliseconds, oneShot=False )
	
	def stop( self ):
		if self.timer.IsRunning():
			self.timer.Stop()
		if self.frameSaver and self.frameSaver.is_alive():
			self.frameSaver.stop()
		self.frameSaver = None
		self.lastSampleTime = now()
			
	def takePhoto( self, bib, t ):
		tNow = now()

		tFind = self.refTime + timedelta( seconds = t + Model.race.advancePhotoMilliseconds / 1000.0 )
		if tFind > tNow:
			wx.CallLater( int(((tFind - tNow).total_seconds() + 0.1) * 1000.0), self.takePhoto, bib, t )
			return
		
		# If burst mode, check if there was another rider before within frameDelay seconds.
		# If so, also save the frame just before the given time.
		# Always save the closest frame after the given time.
		times, frames = self.fcb.findBeforeAfter(
							tFind,
							1, # 1 if tFind - self.tFindLast < self.frameDelayTimeDelta and self.burstMode else 0,
							1 )
		for i, frame in enumerate( frames ):
			t = (times[i]-self.refTime).total_seconds()
			self.frameSaver.save( GetFilename(bib, t, self.dirName, i), bib, t, frame )
		self.frameCount += len(frames)
		self.tFindLast = tFind
	
	def getT( self, i ):
		return self.fcb.getT(i)
		
	def getFrame( self, i ):
		return self.fcb.getFrame(i)
	
	def getTimeFrame( self, i ):
		return (self.fcb.getT(i), self.fcb.getFrame(i))
		
	def findBeforeAfter( self, t, before=0, after=1 ):
		''' Call the frame cyclic buffer from race time, not clock time. '''
		tFind = self.refTime + timedelta( seconds=t )
		times, frames = self.fcb.findBeforeAfter( tFind, before, after )
		return zip( times, frames )
		
	def __len__( self ):
		return self.frameMax
Пример #2
0
class MainWin( wx.Frame ):
	def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ):
		wx.Frame.__init__(self, parent, id, title, size=size)
		
		self.fps = 25
		self.frameDelay = 1.0 / self.fps
		self.bufferSecs = 10
		
		self.tFrameCount = self.tLaunch = self.tLast = now()
		self.frameCount = 0
		self.frameCountUpdate = self.fps * 2
		self.fpsActual = 0.0
		self.fpt = timedelta(seconds=0)
		
		self.fcb = FrameCircBuf( self.bufferSecs * self.fps )
		
		self.config = wx.Config(appName="CrossMgrCamera",
						vendorName="SmartCyclingSolutions",
						#style=wx.CONFIG_USE_LOCAL_FILE
		)
		
		self.requestQ = Queue()			# Select photos from photobuf.
		self.writerQ = Queue( 400 )		# Selected photos waiting to be written out.
		self.ftpQ = Queue()				# Photos waiting to be ftp'd.
		self.renamerQ = Queue()			# Photos waiting to be renamed and possibly ftp'd.
		self.messageQ = Queue()			# Collection point for all status/failure messages.
		
		self.SetBackgroundColour( wx.Colour(232,232,232) )
		
		mainSizer = wx.BoxSizer( wx.VERTICAL )
		
		self.primaryImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.beforeImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.afterImage = ScaledImage( self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight) )
		self.beforeAfterImages = [self.beforeImage, self.afterImage]
		
		#------------------------------------------------------------------------------------------------
		headerSizer = wx.BoxSizer( wx.HORIZONTAL )
		
		self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
		headerSizer.Add( wx.StaticBitmap(self, wx.ID_ANY, self.logo) )
		
		self.title = wx.StaticText(self, label='CrossMgr Camera\nVersion {}'.format(AppVerName.split()[1]), style=wx.ALIGN_RIGHT )
		self.title.SetFont( wx.Font( (0,28), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) )
		headerSizer.Add( self.title, flag=wx.ALL, border=10 )
		
		#------------------------------------------------------------------------------
		self.cameraDeviceLabel = wx.StaticText(self, label='Camera Device:')
		self.cameraDevice = wx.StaticText( self )
		boldFont = self.cameraDevice.GetFont()
		boldFont.SetWeight( wx.BOLD )
		self.cameraDevice.SetFont( boldFont )
		self.cameraResolution = wx.StaticText( self )
		self.cameraResolution.SetFont( boldFont )
		bitmap = wx.Bitmap( clipboard_xpm )
		self.copyLogToClipboard = wx.BitmapButton( self, bitmap=bitmap )
		self.copyLogToClipboard.Bind( wx.EVT_BUTTON, self.onCopyLogToClipboard )
		self.reset = wx.Button( self, label="Reset Camera" )
		self.reset.Bind( wx.EVT_BUTTON, self.resetCamera )
		cameraDeviceSizer = wx.BoxSizer( wx.HORIZONTAL )
		cameraDeviceSizer.Add( self.cameraDeviceLabel, flag=wx.ALIGN_CENTRE_VERTICAL|wx.ALIGN_RIGHT )
		cameraDeviceSizer.Add( self.cameraDevice, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.cameraResolution, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.copyLogToClipboard, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=8 )
		cameraDeviceSizer.Add( self.reset, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=24 )

		#------------------------------------------------------------------------------
		self.targetProcessingTimeLabel = wx.StaticText(self, label='Target Frames:')
		self.targetProcessingTime = wx.StaticText(self, label=u'{:.3f}'.format(self.fps))
		self.targetProcessingTime.SetFont( boldFont )
		self.targetProcessingTimeUnit = wx.StaticText(self, label='per sec')
		
		self.framesPerSecondLabel = wx.StaticText(self, label='Actual Frames:')
		self.framesPerSecond = wx.StaticText(self, label='25.000')
		self.framesPerSecond.SetFont( boldFont )
		self.framesPerSecondUnit = wx.StaticText(self, label='per sec')
		
		self.availableMsPerFrameLabel = wx.StaticText(self, label='Available Time Per Frame:')
		self.availableMsPerFrame = wx.StaticText(self, label=u'{:.0f}'.format(1000.0*self.frameDelay))
		self.availableMsPerFrame.SetFont( boldFont )
		self.availableMsPerFrameUnit = wx.StaticText(self, label='ms')
		
		self.frameProcessingTimeLabel = wx.StaticText(self, label='Actual Frame Processing:')
		self.frameProcessingTime = wx.StaticText(self, label='20')
		self.frameProcessingTime.SetFont( boldFont )
		self.frameProcessingTimeUnit = wx.StaticText(self, label='ms')
		
		pfgs = wx.FlexGridSizer( rows=0, cols=6, vgap=4, hgap=8 )
		fRight = wx.ALIGN_CENTRE_VERTICAL|wx.ALIGN_RIGHT
		fLeft = wx.ALIGN_CENTRE_VERTICAL
		
		#------------------- Row 1 ------------------------------
		pfgs.Add( self.targetProcessingTimeLabel, flag=fRight )
		pfgs.Add( self.targetProcessingTime, flag=fRight )
		pfgs.Add( self.targetProcessingTimeUnit, flag=fLeft )
		pfgs.Add( self.availableMsPerFrameLabel, flag=fRight )
		pfgs.Add( self.availableMsPerFrame, flag=fRight )
		pfgs.Add( self.availableMsPerFrameUnit, flag=fLeft )
		
		#------------------- Row 2 ------------------------------
		pfgs.Add( self.framesPerSecondLabel, flag=fRight )
		pfgs.Add( self.framesPerSecond, flag=fRight )
		pfgs.Add( self.framesPerSecondUnit, flag=fLeft )
		pfgs.Add( self.frameProcessingTimeLabel, flag=fRight )
		pfgs.Add( self.frameProcessingTime, flag=fRight )
		pfgs.Add( self.frameProcessingTimeUnit, flag=fLeft )

		statsSizer = wx.BoxSizer( wx.VERTICAL )
		statsSizer.Add( cameraDeviceSizer )
		statsSizer.Add( pfgs, flag=wx.TOP, border=8 )
		headerSizer.Add( statsSizer, flag=wx.ALL, border=4 )
		mainSizer.Add( headerSizer )
		
		self.messagesText = wx.TextCtrl( self, style=wx.TE_READONLY|wx.TE_MULTILINE|wx.HSCROLL, size=(350,imageHeight) )
		self.messageManager = MessageManager( self.messagesText )
		
		border = 2
		row1Sizer = wx.BoxSizer( wx.HORIZONTAL )
		row1Sizer.Add( self.primaryImage, flag=wx.ALL, border=border )
		row1Sizer.Add( self.messagesText, 1, flag=wx.TOP|wx.BOTTOM|wx.RIGHT|wx.EXPAND, border=border )
		mainSizer.Add( row1Sizer, 1, flag=wx.EXPAND )
		
		row2Sizer = wx.BoxSizer( wx.HORIZONTAL )
		row2Sizer.Add( self.beforeImage, flag=wx.LEFT|wx.RIGHT|wx.BOTTOM, border=border )
		row2Sizer.Add( self.afterImage, flag=wx.RIGHT|wx.BOTTOM, border=border )
		mainSizer.Add( row2Sizer )
		
		#------------------------------------------------------------------------------------------------
		# Create a timer to update the frame loop.
		#
		self.timer = wx.Timer()
		self.timer.Bind( wx.EVT_TIMER, self.frameLoop )
		
		self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

		self.readOptions()
		self.SetSizerAndFit( mainSizer )
		
		# Start the message reporting thread so we can see what is going on.
		self.messageThread = threading.Thread( target=self.showMessages )
		self.messageThread.daemon = True
		self.messageThread.start()
		
		self.grabFrameOK = False
		
		for i in [self.primaryImage, self.beforeImage, self.afterImage]:
			i.SetTestImage()
		
		# Start the frame loop.
		delayAdjustment = 0.80 if 'win' in sys.platform else 0.98
		ms = int(1000 * self.frameDelay * delayAdjustment)
		self.timer.Start( ms, False )
		
	def Start( self ):
		self.messageQ.put( ('', '************************************************') )
		self.messageQ.put( ('started', now().strftime('%Y/%m/%d %H:%M:%S')) )
		self.startThreads()
		self.startCamera()
	
	def onCopyLogToClipboard( self, event ):
		with open( redirectFileName, 'r' ) as fp:
			logData = fp.read()
		dataObj = wx.TextDataObject()
		dataObj.SetText(logData)
		if wx.TheClipboard.Open():
			wx.TheClipboard.SetData( dataObj )
			wx.TheClipboard.Close()
			Utils.MessageOK(self, u'\n\n'.join( [_("Log file copied to clipboard."), _("You can now paste it into an email.")] ), _("Success") )
		else:
			Utils.MessageOK(self, _("Unable to open the clipboard."), _("Error"), wx.ICON_ERROR )
	
	def showMessages( self ):
		while 1:
			message = self.messageQ.get()
			assert len(message) == 2, 'Incorrect message length'
			cmd, info = message
			wx.CallAfter( self.messageManager.write, '{}:  {}'.format(cmd, info) if cmd else info )
		
	def startCamera( self ):
		self.camera = None
		try:
			self.camera = Device( max(self.getCameraDeviceNum(), 0) )
		except Exception as e:
			self.messageQ.put( ('camera', 'Error: {}'.format(e)) )
			return False
		
		try:
			self.camera.set_resolution( *self.getCameraResolution() )
			self.messageQ.put( ('camera', '{}x{} Supported'.format(*self.getCameraResolution())) )
		except Exception as e:
			self.messageQ.put( ('camera', '{}x{} Unsupported Resolution'.format(*self.getCameraResolution())) )
			
		self.messageQ.put( ('camera', 'Successfully Connected: Device: {}'.format(self.getCameraDeviceNum()) ) )
		for i in self.beforeAfterImages:
			i.SetTestImage()
		return True
	
	def startThreads( self ):
		self.grabFrameOK = False
		
		self.listenerThread = SocketListener( self.requestQ, self.messageQ )
		error = self.listenerThread.test()
		if error:
			wx.MessageBox('Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'.format(error),
				"Socket Error",
				wx.OK | wx.ICON_ERROR
			)
			wx.Exit()
				
		self.writerThread = threading.Thread( target=PhotoWriter, args=(self.writerQ, self.messageQ, self.ftpQ) )
		self.writerThread.daemon = True
		
		self.renamerThread = threading.Thread( target=PhotoRenamer, args=(self.renamerQ, self.writerQ, self.messageQ) )
		self.renamerThread.daemon = True
		
		self.ftpThread = threading.Thread( target=FTPWriter, args=(self.ftpQ, self.messageQ) )
		self.ftpThread.daemon = True
		
		self.fcb = FrameCircBuf( int(self.bufferSecs * self.fps) )
		
		self.listenerThread.start()
		self.writerThread.start()
		self.renamerThread.start()
		self.ftpThread.start()
		
		self.grabFrameOK = True
		self.messageQ.put( ('threads', 'Successfully Launched') )
		return True
	
	def frameLoop( self, event=None ):
		if not self.grabFrameOK:
			return
			
		self.frameCount += 1
		tNow = now()
		
		# Compute frame rate statistics.
		if self.frameCount == self.frameCountUpdate:
			self.fpsActual = self.frameCount / (tNow - self.tFrameCount).total_seconds()
			self.frameCount = 0
			self.tFrameCount = tNow
			self.framesPerSecond.SetLabel( u'{:.3f}'.format(self.fpsActual) )
			self.availableMsPerFrame.SetLabel( u'{:.0f}'.format(1000.0/self.fpsActual) )
			self.frameProcessingTime.SetLabel( u'{:.0f}'.format(1000.0*self.fpt.total_seconds()) )
		
		try:
			image = self.camera.getImage()
		except Exception as e:
			self.messageQ.put( ('error', 'Webcam Failure: {}'.format(e) ) )
			self.grabFrameOK = False
			return
		
		if not image:
			return
		
		image = AddPhotoHeader( PilImageToWxImage(image),
			time=tNow, raceSeconds=(tNow - self.tLaunch).total_seconds(),
			bib=999,
			firstNameTxt=u'Firstname', lastNameTxt=u'LASTNAME', teamTxt=u'Team',
			raceNameTxt='Racename'
		)
		self.fcb.append( tNow, image )
		wx.CallAfter( self.primaryImage.SetImage, image )
		
		# Process any save messages
		while 1:
			try:
				message = self.requestQ.get(False)
			except Empty:
				break
			
			tSearch = message['time']
			advanceSeconds = message.get('advanceSeconds', 0.0)
			if advanceSeconds:
				tSearch += timedelta(seconds=advanceSeconds)
			times, frames = self.fcb.findBeforeAfter( tSearch, 1, 1 )
			if not frames:
				self.messageQ.put( ('error', 'No photos for {} at {}'.format(message.get('bib', None), message['time'].isoformat()) ) )
				
			lastImage = None
			for i, f in enumerate(frames):
				
				fname = GetPhotoFName( message['dirName'], message.get('bib',None), message.get('raceSeconds',None), i, times[i] )
				image = AddPhotoHeader(
					f,
					message.get('bib', None),
					message.get('time', None),
					message.get('raceSeconds', None),
					message.get('firstName',u''),
					message.get('lastName',u''),
					message.get('team',u''),
					message.get('raceName',u'')
				)
				
				#if lastImage is not None and image.GetData() == lastImage.GetData():
				#	self.messageQ.put( ('duplicate', '"{}"'.format(os.path.basename(fname))) )
				#	continue
				
				wx.CallAfter( self.beforeAfterImages[i].SetImage, image )
				
				SaveImage( fname, image, message.get('ftpInfo', None), self.messageQ, self.writerQ )
				lastImage = image
				
			if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
				break
				
		self.fpt = now() - tNow
		self.tLast = tNow
	
	def shutdown( self ):
		# Ensure that all images in the queue are saved.
		self.grabFrameOK = False
		if hasattr(self, 'writerThread'):
			self.writerQ.put( ('terminate', ) )
			self.writerThread.join()
	
	def resetCamera( self, event ):
		dlg = ConfigDialog( self, self.getCameraDeviceNum(), self.getCameraResolution() )
		ret = dlg.ShowModal()
		cameraDeviceNum = dlg.GetCameraDeviceNum()
		cameraResolution = dlg.GetCameraResolution()
		dlg.Destroy()
		if ret != wx.ID_OK:
			return False
		
		self.setCameraDeviceNum( cameraDeviceNum )
		self.setCameraResolution( *cameraResolution )
		self.writeOptions()

		self.grabFrameOK = self.startCamera()
		return True
		
	def setCameraDeviceNum( self, num ):
		self.cameraDevice.SetLabel( unicode(num) )
		
	def setCameraResolution( self, width, height ):
		self.cameraResolution.SetLabel( u'{}x{}'.format(width, height) )
			
	def getCameraDeviceNum( self ):
		return int(self.cameraDevice.GetLabel())
		
	def getCameraResolution( self ):
		try:
			resolution = [int(v) for v in self.cameraResolution.GetLabel().split('x')]
			return resolution[0], resolution[1]
		except:
			return 640, 400
		
	def onCloseWindow( self, event ):
		self.shutdown()
		wx.Exit()
		
	def writeOptions( self ):
		self.config.Write( 'CameraDevice', self.cameraDevice.GetLabel() )
		self.config.Write( 'CameraResolution', self.cameraResolution.GetLabel() )
		self.config.Flush()
	
	def readOptions( self ):
		self.cameraDevice.SetLabel( self.config.Read('CameraDevice', u'0') )
		self.cameraResolution.SetLabel( self.config.Read('CameraResolution', u'640x480') )
Пример #3
0
class MainWin(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title='', size=(1000, 800)):
        wx.Frame.__init__(self, parent, id, title, size=size)

        self.fps = 25
        self.frameDelay = 1.0 / self.fps
        self.bufferSecs = 10

        self.tFrameCount = self.tLaunch = self.tLast = now()
        self.frameCount = 0
        self.frameCountUpdate = self.fps * 2
        self.fpsActual = 0.0
        self.fpt = timedelta(seconds=0)

        self.fcb = FrameCircBuf(self.bufferSecs * self.fps)

        self.config = wx.Config(
            appName="CrossMgrCamera",
            vendorName="SmartCyclingSolutions",
            #style=wx.CONFIG_USE_LOCAL_FILE
        )

        self.requestQ = Queue()  # Select photos from photobuf.
        self.writerQ = Queue(400)  # Selected photos waiting to be written out.
        self.ftpQ = Queue()  # Photos waiting to be ftp'd.
        self.renamerQ = Queue(
        )  # Photos waiting to be renamed and possibly ftp'd.
        self.messageQ = Queue(
        )  # Collection point for all status/failure messages.

        self.SetBackgroundColour(wx.Colour(232, 232, 232))

        mainSizer = wx.BoxSizer(wx.VERTICAL)

        self.primaryImage = ScaledImage(self,
                                        style=wx.BORDER_SUNKEN,
                                        size=(imageWidth, imageHeight))
        self.beforeImage = ScaledImage(self,
                                       style=wx.BORDER_SUNKEN,
                                       size=(imageWidth, imageHeight))
        self.afterImage = ScaledImage(self,
                                      style=wx.BORDER_SUNKEN,
                                      size=(imageWidth, imageHeight))
        self.beforeAfterImages = [self.beforeImage, self.afterImage]

        #------------------------------------------------------------------------------------------------
        headerSizer = wx.BoxSizer(wx.HORIZONTAL)

        self.logo = Utils.GetPngBitmap('CrossMgrHeader.png')
        headerSizer.Add(wx.StaticBitmap(self, wx.ID_ANY, self.logo))

        self.title = wx.StaticText(self,
                                   label='CrossMgr Camera\nVersion {}'.format(
                                       AppVerName.split()[1]),
                                   style=wx.ALIGN_RIGHT)
        self.title.SetFont(
            wx.Font((0, 28), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL,
                    wx.FONTWEIGHT_NORMAL))
        headerSizer.Add(self.title, flag=wx.ALL, border=10)

        #------------------------------------------------------------------------------
        self.cameraDeviceLabel = wx.StaticText(self, label='Camera Device:')
        self.cameraDevice = wx.StaticText(self)
        boldFont = self.cameraDevice.GetFont()
        boldFont.SetWeight(wx.BOLD)
        self.cameraDevice.SetFont(boldFont)
        self.cameraResolution = wx.StaticText(self)
        self.cameraResolution.SetFont(boldFont)
        bitmap = wx.Bitmap(clipboard_xpm)
        self.copyLogToClipboard = wx.BitmapButton(self, bitmap=bitmap)
        self.copyLogToClipboard.Bind(wx.EVT_BUTTON, self.onCopyLogToClipboard)
        self.reset = wx.Button(self, label="Reset Camera")
        self.reset.Bind(wx.EVT_BUTTON, self.resetCamera)
        cameraDeviceSizer = wx.BoxSizer(wx.HORIZONTAL)
        cameraDeviceSizer.Add(self.cameraDeviceLabel,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        cameraDeviceSizer.Add(self.cameraDevice,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.cameraResolution,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.copyLogToClipboard,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=8)
        cameraDeviceSizer.Add(self.reset,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT,
                              border=24)

        #------------------------------------------------------------------------------
        self.targetProcessingTimeLabel = wx.StaticText(self,
                                                       label='Target Frames:')
        self.targetProcessingTime = wx.StaticText(self,
                                                  label=u'{:.3f}'.format(
                                                      self.fps))
        self.targetProcessingTime.SetFont(boldFont)
        self.targetProcessingTimeUnit = wx.StaticText(self, label='per sec')

        self.framesPerSecondLabel = wx.StaticText(self, label='Actual Frames:')
        self.framesPerSecond = wx.StaticText(self, label='25.000')
        self.framesPerSecond.SetFont(boldFont)
        self.framesPerSecondUnit = wx.StaticText(self, label='per sec')

        self.availableMsPerFrameLabel = wx.StaticText(
            self, label='Available Time Per Frame:')
        self.availableMsPerFrame = wx.StaticText(self,
                                                 label=u'{:.0f}'.format(
                                                     1000.0 * self.frameDelay))
        self.availableMsPerFrame.SetFont(boldFont)
        self.availableMsPerFrameUnit = wx.StaticText(self, label='ms')

        self.frameProcessingTimeLabel = wx.StaticText(
            self, label='Actual Frame Processing:')
        self.frameProcessingTime = wx.StaticText(self, label='20')
        self.frameProcessingTime.SetFont(boldFont)
        self.frameProcessingTimeUnit = wx.StaticText(self, label='ms')

        pfgs = wx.FlexGridSizer(rows=0, cols=6, vgap=4, hgap=8)
        fRight = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT
        fLeft = wx.ALIGN_CENTRE_VERTICAL

        #------------------- Row 1 ------------------------------
        pfgs.Add(self.targetProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.targetProcessingTime, flag=fRight)
        pfgs.Add(self.targetProcessingTimeUnit, flag=fLeft)
        pfgs.Add(self.availableMsPerFrameLabel, flag=fRight)
        pfgs.Add(self.availableMsPerFrame, flag=fRight)
        pfgs.Add(self.availableMsPerFrameUnit, flag=fLeft)

        #------------------- Row 2 ------------------------------
        pfgs.Add(self.framesPerSecondLabel, flag=fRight)
        pfgs.Add(self.framesPerSecond, flag=fRight)
        pfgs.Add(self.framesPerSecondUnit, flag=fLeft)
        pfgs.Add(self.frameProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.frameProcessingTime, flag=fRight)
        pfgs.Add(self.frameProcessingTimeUnit, flag=fLeft)

        statsSizer = wx.BoxSizer(wx.VERTICAL)
        statsSizer.Add(cameraDeviceSizer)
        statsSizer.Add(pfgs, flag=wx.TOP, border=8)
        headerSizer.Add(statsSizer, flag=wx.ALL, border=4)
        mainSizer.Add(headerSizer)

        self.messagesText = wx.TextCtrl(self,
                                        style=wx.TE_READONLY | wx.TE_MULTILINE
                                        | wx.HSCROLL,
                                        size=(350, imageHeight))
        self.messageManager = MessageManager(self.messagesText)

        border = 2
        row1Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row1Sizer.Add(self.primaryImage, flag=wx.ALL, border=border)
        row1Sizer.Add(self.messagesText,
                      1,
                      flag=wx.TOP | wx.BOTTOM | wx.RIGHT | wx.EXPAND,
                      border=border)
        mainSizer.Add(row1Sizer, 1, flag=wx.EXPAND)

        row2Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row2Sizer.Add(self.beforeImage,
                      flag=wx.LEFT | wx.RIGHT | wx.BOTTOM,
                      border=border)
        row2Sizer.Add(self.afterImage,
                      flag=wx.RIGHT | wx.BOTTOM,
                      border=border)
        mainSizer.Add(row2Sizer)

        #------------------------------------------------------------------------------------------------
        # Create a timer to update the frame loop.
        #
        self.timer = wx.Timer()
        self.timer.Bind(wx.EVT_TIMER, self.frameLoop)

        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

        self.readOptions()
        self.SetSizerAndFit(mainSizer)

        # Start the message reporting thread so we can see what is going on.
        self.messageThread = threading.Thread(target=self.showMessages)
        self.messageThread.daemon = True
        self.messageThread.start()

        self.grabFrameOK = False

        for i in [self.primaryImage, self.beforeImage, self.afterImage]:
            i.SetTestImage()

        # Start the frame loop.
        delayAdjustment = 0.80 if 'win' in sys.platform else 0.98
        ms = int(1000 * self.frameDelay * delayAdjustment)
        self.timer.Start(ms, False)

    def Start(self):
        self.messageQ.put(
            ('', '************************************************'))
        self.messageQ.put(('started', now().strftime('%Y/%m/%d %H:%M:%S')))
        self.startThreads()
        self.startCamera()

    def onCopyLogToClipboard(self, event):
        with open(redirectFileName, 'r') as fp:
            logData = fp.read()
        dataObj = wx.TextDataObject()
        dataObj.SetText(logData)
        if wx.TheClipboard.Open():
            wx.TheClipboard.SetData(dataObj)
            wx.TheClipboard.Close()
            Utils.MessageOK(
                self, u'\n\n'.join([
                    _("Log file copied to clipboard."),
                    _("You can now paste it into an email.")
                ]), _("Success"))
        else:
            Utils.MessageOK(self, _("Unable to open the clipboard."),
                            _("Error"), wx.ICON_ERROR)

    def showMessages(self):
        while 1:
            message = self.messageQ.get()
            assert len(message) == 2, 'Incorrect message length'
            cmd, info = message
            wx.CallAfter(self.messageManager.write,
                         '{}:  {}'.format(cmd, info) if cmd else info)

    def startCamera(self):
        self.camera = None
        try:
            self.camera = Device(max(self.getCameraDeviceNum(), 0))
        except Exception as e:
            self.messageQ.put(('camera', 'Error: {}'.format(e)))
            return False

        try:
            self.camera.set_resolution(*self.getCameraResolution())
            self.messageQ.put(
                ('camera',
                 '{}x{} Supported'.format(*self.getCameraResolution())))
        except Exception as e:
            self.messageQ.put(('camera', '{}x{} Unsupported Resolution'.format(
                *self.getCameraResolution())))

        self.messageQ.put(
            ('camera', 'Successfully Connected: Device: {}'.format(
                self.getCameraDeviceNum())))
        for i in self.beforeAfterImages:
            i.SetTestImage()
        return True

    def startThreads(self):
        self.grabFrameOK = False

        self.listenerThread = SocketListener(self.requestQ, self.messageQ)
        error = self.listenerThread.test()
        if error:
            wx.MessageBox(
                'Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?'
                .format(error), "Socket Error", wx.OK | wx.ICON_ERROR)
            wx.Exit()

        self.writerThread = threading.Thread(target=PhotoWriter,
                                             args=(self.writerQ, self.messageQ,
                                                   self.ftpQ))
        self.writerThread.daemon = True

        self.renamerThread = threading.Thread(target=PhotoRenamer,
                                              args=(self.renamerQ,
                                                    self.writerQ,
                                                    self.messageQ))
        self.renamerThread.daemon = True

        self.ftpThread = threading.Thread(target=FTPWriter,
                                          args=(self.ftpQ, self.messageQ))
        self.ftpThread.daemon = True

        self.fcb = FrameCircBuf(int(self.bufferSecs * self.fps))

        self.listenerThread.start()
        self.writerThread.start()
        self.renamerThread.start()
        self.ftpThread.start()

        self.grabFrameOK = True
        self.messageQ.put(('threads', 'Successfully Launched'))
        return True

    def frameLoop(self, event=None):
        if not self.grabFrameOK:
            return

        self.frameCount += 1
        tNow = now()

        # Compute frame rate statistics.
        if self.frameCount == self.frameCountUpdate:
            self.fpsActual = self.frameCount / (
                tNow - self.tFrameCount).total_seconds()
            self.frameCount = 0
            self.tFrameCount = tNow
            self.framesPerSecond.SetLabel(u'{:.3f}'.format(self.fpsActual))
            self.availableMsPerFrame.SetLabel(u'{:.0f}'.format(1000.0 /
                                                               self.fpsActual))
            self.frameProcessingTime.SetLabel(u'{:.0f}'.format(
                1000.0 * self.fpt.total_seconds()))

        try:
            image = self.camera.getImage()
        except Exception as e:
            self.messageQ.put(('error', 'Webcam Failure: {}'.format(e)))
            self.grabFrameOK = False
            return

        if not image:
            return

        image = AddPhotoHeader(PilImageToWxImage(image),
                               time=tNow,
                               raceSeconds=(tNow -
                                            self.tLaunch).total_seconds(),
                               bib=999,
                               firstNameTxt=u'Firstname',
                               lastNameTxt=u'LASTNAME',
                               teamTxt=u'Team',
                               raceNameTxt='Racename')
        self.fcb.append(tNow, image)
        wx.CallAfter(self.primaryImage.SetImage, image)

        # Process any save messages
        while 1:
            try:
                message = self.requestQ.get(False)
            except Empty:
                break

            tSearch = message['time']
            advanceSeconds = message.get('advanceSeconds', 0.0)
            if advanceSeconds:
                tSearch += timedelta(seconds=advanceSeconds)
            times, frames = self.fcb.findBeforeAfter(tSearch, 1, 1)
            if not frames:
                self.messageQ.put(('error', 'No photos for {} at {}'.format(
                    message.get('bib', None), message['time'].isoformat())))

            lastImage = None
            for i, f in enumerate(frames):

                fname = GetPhotoFName(message['dirName'],
                                      message.get('bib', None),
                                      message.get('raceSeconds', None), i,
                                      times[i])
                image = AddPhotoHeader(f, message.get('bib', None),
                                       message.get('time', None),
                                       message.get('raceSeconds', None),
                                       message.get('firstName', u''),
                                       message.get('lastName', u''),
                                       message.get('team', u''),
                                       message.get('raceName', u''))

                #if lastImage is not None and image.GetData() == lastImage.GetData():
                #	self.messageQ.put( ('duplicate', '"{}"'.format(os.path.basename(fname))) )
                #	continue

                wx.CallAfter(self.beforeAfterImages[i].SetImage, image)

                SaveImage(fname, image, message.get('ftpInfo', None),
                          self.messageQ, self.writerQ)
                lastImage = image

            if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
                break

        self.fpt = now() - tNow
        self.tLast = tNow

    def shutdown(self):
        # Ensure that all images in the queue are saved.
        self.grabFrameOK = False
        if hasattr(self, 'writerThread'):
            self.writerQ.put(('terminate', ))
            self.writerThread.join()

    def resetCamera(self, event):
        dlg = ConfigDialog(self, self.getCameraDeviceNum(),
                           self.getCameraResolution())
        ret = dlg.ShowModal()
        cameraDeviceNum = dlg.GetCameraDeviceNum()
        cameraResolution = dlg.GetCameraResolution()
        dlg.Destroy()
        if ret != wx.ID_OK:
            return False

        self.setCameraDeviceNum(cameraDeviceNum)
        self.setCameraResolution(*cameraResolution)
        self.writeOptions()

        self.grabFrameOK = self.startCamera()
        return True

    def setCameraDeviceNum(self, num):
        self.cameraDevice.SetLabel(unicode(num))

    def setCameraResolution(self, width, height):
        self.cameraResolution.SetLabel(u'{}x{}'.format(width, height))

    def getCameraDeviceNum(self):
        return int(self.cameraDevice.GetLabel())

    def getCameraResolution(self):
        try:
            resolution = [
                int(v) for v in self.cameraResolution.GetLabel().split('x')
            ]
            return resolution[0], resolution[1]
        except:
            return 640, 400

    def onCloseWindow(self, event):
        self.shutdown()
        wx.Exit()

    def writeOptions(self):
        self.config.Write('CameraDevice', self.cameraDevice.GetLabel())
        self.config.Write('CameraResolution', self.cameraResolution.GetLabel())
        self.config.Flush()

    def readOptions(self):
        self.cameraDevice.SetLabel(self.config.Read('CameraDevice', u'0'))
        self.cameraResolution.SetLabel(
            self.config.Read('CameraResolution', u'640x480'))
Пример #4
0
class VideoBuffer( object ):
	
	def __init__( self, camera, refTime=None, dirName='.', fps=25, bufferSeconds=4.0, owner=None, burstMode=True ):
		self.camera = camera
		self.refTime = refTime if refTime is not None else now()
		self.dirName = dirName
		self.fps = fps
		self.frameMax = int(fps * bufferSeconds)
		
		self.frameDelay = 1.0 / fps
		self.frameDelayTimeDelta = timedelta(seconds=self.frameDelay)
		
		self.frameSaver = None
		self.fcb = FrameCircBuf()
		self.owner = owner			# Destination to send photos after they are taken.
		self.burstMode = burstMode
		
		self.timer = CallbackTimer( self.recordVideoFrame )
		self.reset()
	
	def setOwner( self, owner=None ):
		self.owner = owner
	
	def reset( self ):
		if self.frameSaver and self.frameSaver.is_alive():
			self.frameSaver.stop()
		
		if self.timer.IsRunning():
			self.timer.Stop()
		
		self.fcb.reset( self.frameMax )
		self.frameCount = 0
		
		self.frameSaver = FrameSaver()
		self.frameSaver.start()
		
		self.tFindLast = now() - timedelta( days=1 )
		
		self.lastSampleTime = now()
		self.sampleCount = 0
	
	def recordVideoFrame( self ):
		tNow = now()
		self.sampleCount += 1
		if not self.camera:
			return
			
		try:
			image = self.camera.getImage()
			self.fcb.append( tNow, image )
			if self.owner is not None:
				wx.PostEvent( self.owner, PhotoEvent(t=tNow, photo=image) )
		except Exception as e:
			logException( e, sys.exc_info() )

	def getFrameRate( self ):
		tNow = now()
		rate = self.sampleCount / (tNow -self.lastSampleTime).total_seconds()
		self.lastSampleTime = tNow
		self.sampleCount = 0
		return rate
	
	def start( self ):
		self.reset()
		milliseconds = int(self.frameDelay * 1000.0)
		self.timer.Start( milliseconds, oneShot=False )
	
	def stop( self ):
		if self.timer.IsRunning():
			self.timer.Stop()
		if self.frameSaver and self.frameSaver.is_alive():
			self.frameSaver.stop()
		self.frameSaver = None
		self.lastSampleTime = now()
			
	def takePhoto( self, bib, t ):
		tNow = now()

		tFind = self.refTime + timedelta( seconds = t + Model.race.advancePhotoMilliseconds / 1000.0 )
		if tFind > tNow:
			wx.CallLater( max(1,int(((tFind - tNow).total_seconds() + 0.1) * 1000.0)), self.takePhoto, bib, t )
			return
		
		# If burst mode, check if there was another rider before within frameDelay seconds.
		# If so, also save the frame just before the given time.
		# Always save the closest frame after the given time.
		times, frames = self.fcb.findBeforeAfter(
							tFind,
							1, # 1 if tFind - self.tFindLast < self.frameDelayTimeDelta and self.burstMode else 0,
							1 )
		for i, frame in enumerate( frames ):
			t = (times[i]-self.refTime).total_seconds()
			self.frameSaver.save( GetFilename(bib, t, self.dirName, i), bib, t, frame )
		self.frameCount += len(frames)
		self.tFindLast = tFind
	
	def getT( self, i ):
		return self.fcb.getT(i)
		
	def getFrame( self, i ):
		return self.fcb.getFrame(i)
	
	def getTimeFrame( self, i ):
		return (self.fcb.getT(i), self.fcb.getFrame(i))
		
	def findBeforeAfter( self, t, before=0, after=1 ):
		''' Call the frame cyclic buffer from race time, not clock time. '''
		tFind = self.refTime + timedelta( seconds=t )
		times, frames = self.fcb.findBeforeAfter( tFind, before, after )
		return zip( times, frames )
		
	def __len__( self ):
		return self.frameMax
Пример #5
0
class VideoBuffer( threading.Thread ):
	def __init__( self, camera, refTime = None, dirName = '.', fps = 25, bufferSeconds = 4.0 ):
		threading.Thread.__init__( self )
		self.daemon = True
		self.name = 'VideoBuffer'
		self.camera = camera
		self.refTime = refTime if refTime is not None else now()
		self.dirName = dirName
		self.fps = fps
		self.frameMax = int(fps * bufferSeconds)
		self.frameDelay = 1.0 / fps
		self.frameSaver = None
		self.fcb = FrameCircBuf()
		self.reset()
	
	def reset( self ):
		
		if self.frameSaver and self.frameSaver.is_alive():
			self.frameSaver.stop()
		
		self.fcb.reset( self.frameMax )
		self.frameCount = 0
		
		self.frameSaver = FrameSaver()
		self.frameSaver.start()
		
		self.queue = Queue()
	
	def run( self ):
		self.reset()
		keepGoing = True
		while keepGoing:
			#sys.stderr.write( '.' )
			tNow = now()
			try:
				self.fcb.append( tNow, self.camera.getImage() )
			except Exception as e:
				logException( e, sys.exc_info() )
				break
			
			while 1:
				try:
					message = self.queue.get( False )
				except Empty:
					break
				
				if   message[0] == 'Save':
					cmd, bib, t = message
					tFind = self.refTime + timedelta( seconds = t + getattr(Model.race, 'advancePhotoMilliseconds', Model.Race.advancePhotoMillisecondsDefault) / 1000.0 )
					if tFind > tNow:
						threading.Timer( (tFind - tNow).total_seconds() + 0.1, self.takePhoto, args=[bib, t] ).start()
						continue
						
					times, frames = self.fcb.findBeforeAfter( tFind, 0, 1 )
					for i, frame in enumerate( frames ):
						t = (times[i]-self.refTime).total_seconds()
						self.frameSaver.save( GetFilename(bib, t, self.dirName, i), bib, t, frame )
					self.frameCount += len(frames)
					self.queue.task_done()
					
				elif message[0] == 'Terminate':
					self.queue.task_done()
					self.reset()
					keepGoing = False
					break
				
			# Sleep until we need to grab the next frame.
			frameWait = self.frameDelay - (now() - tNow).total_seconds()
			if frameWait < 0.0:
				frameWait = self.frameDelay		# Give some more time if we are falling behind.
			time.sleep( frameWait )
	
	def stop( self ):
		self.queue.put( ['Terminate'] )
		self.join()
		if self.frameSaver.is_alive():
			self.frameSaver.stop()
		self.frameSaver = None
	
	def takePhoto( self, bib, t ):
		self.queue.put( ['Save', bib, t] )
	
	def getT( self, i ):
		return self.fcb.getT(i)
		
	def getFrame( self, i ):
		return self.fcb.getFrame(i)
	
	def getTimeFrame( self, i ):
		return (self.fcb.getT(i), self.fcb.getFrame(i))
		
	def findBeforeAfter( self, t, before = 0, after = 1 ):
		''' Call the frame cyclic buffer from race time, not clock time. '''
		tFind = self.refTime + timedelta( seconds = t )
		times, frames = self.fcb.findBeforeAfter( tFind, before, after )
		return zip( times, frames )
		
	def __len__( self ):
		return self.frameMax
Пример #6
0
class MainWin(wx.Frame):
    def __init__(self, parent, id=wx.ID_ANY, title="", size=(1000, 800)):
        wx.Frame.__init__(self, parent, id, title, size=size)

        self.fps = 25
        self.frameDelay = 1.0 / self.fps
        self.bufferSecs = 10

        self.tFrameCount = self.tLaunch = self.tLast = now()
        self.frameCount = 0
        self.frameCountUpdate = self.fps * 2
        self.fpsActual = 0.0
        self.fpt = timedelta(seconds=0)

        self.fcb = FrameCircBuf(self.bufferSecs * self.fps)

        self.config = wx.Config(
            appName="CrossMgrCamera", vendorName="SmartCyclingSolutions", style=wx.CONFIG_USE_LOCAL_FILE
        )

        self.requestQ = Queue()  # Select photos from photobuf.
        self.writerQ = Queue(400)  # Selected photos waiting to be written out.
        self.ftpQ = Queue()  # Photos waiting to be ftp'd.
        self.renamerQ = Queue()  # Photos waiting to be renamed and possibly ftp'd.
        self.messageQ = Queue()  # Collection point for all status/failure messages.

        self.SetBackgroundColour(wx.Colour(232, 232, 232))

        mainSizer = wx.BoxSizer(wx.VERTICAL)

        self.primaryImage = ScaledImage(self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight))
        self.beforeImage = ScaledImage(self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight))
        self.afterImage = ScaledImage(self, style=wx.BORDER_SUNKEN, size=(imageWidth, imageHeight))
        self.beforeAfterImages = [self.beforeImage, self.afterImage]

        # ------------------------------------------------------------------------------------------------
        headerSizer = wx.BoxSizer(wx.HORIZONTAL)

        self.logo = Utils.GetPngBitmap("CrossMgrHeader.png")
        headerSizer.Add(wx.StaticBitmap(self, bitmap=self.logo))

        self.title = wx.StaticText(
            self, label="CrossMgr Camera\nVersion {}".format(AppVerName.split()[1]), style=wx.ALIGN_RIGHT
        )
        self.title.SetFont(wx.FontFromPixelSize(wx.Size(0, 28), wx.FONTFAMILY_SWISS, wx.NORMAL, wx.FONTWEIGHT_NORMAL))
        headerSizer.Add(self.title, flag=wx.ALL, border=10)

        # ------------------------------------------------------------------------------
        self.cameraDeviceLabel = wx.StaticText(self, label="Camera Device:")
        self.cameraDevice = wx.StaticText(self)
        boldFont = self.cameraDevice.GetFont()
        boldFont.SetWeight(wx.BOLD)
        self.cameraDevice.SetFont(boldFont)
        self.reset = wx.Button(self, label="Reset Camera")
        self.reset.Bind(wx.EVT_BUTTON, self.resetCamera)
        cameraDeviceSizer = wx.BoxSizer(wx.HORIZONTAL)
        cameraDeviceSizer.Add(self.cameraDeviceLabel, flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        cameraDeviceSizer.Add(self.cameraDevice, flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT, border=8)
        cameraDeviceSizer.Add(self.reset, flag=wx.ALIGN_CENTRE_VERTICAL | wx.LEFT, border=8)

        # ------------------------------------------------------------------------------
        self.targetProcessingTimeLabel = wx.StaticText(self, label="Target Frames:")
        self.targetProcessingTime = wx.StaticText(self, label=u"{:.3f}".format(self.fps))
        self.targetProcessingTime.SetFont(boldFont)
        self.targetProcessingTimeUnit = wx.StaticText(self, label="per sec")

        self.framesPerSecondLabel = wx.StaticText(self, label="Actual Frames:")
        self.framesPerSecond = wx.StaticText(self, label="25.000")
        self.framesPerSecond.SetFont(boldFont)
        self.framesPerSecondUnit = wx.StaticText(self, label="per sec")

        self.availableMsPerFrameLabel = wx.StaticText(self, label="Available Time Per Frame:")
        self.availableMsPerFrame = wx.StaticText(self, label=u"{:.0f}".format(1000.0 * self.frameDelay))
        self.availableMsPerFrame.SetFont(boldFont)
        self.availableMsPerFrameUnit = wx.StaticText(self, label="ms")

        self.frameProcessingTimeLabel = wx.StaticText(self, label="Actual Frame Processing:")
        self.frameProcessingTime = wx.StaticText(self, label="20")
        self.frameProcessingTime.SetFont(boldFont)
        self.frameProcessingTimeUnit = wx.StaticText(self, label="ms")

        pfgs = wx.FlexGridSizer(rows=0, cols=6, vgap=4, hgap=8)
        fRight = wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT
        fLeft = wx.ALIGN_CENTRE_VERTICAL

        # ------------------- Row 1 ------------------------------
        pfgs.Add(self.targetProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.targetProcessingTime, flag=fRight)
        pfgs.Add(self.targetProcessingTimeUnit, flag=fLeft)
        pfgs.Add(self.availableMsPerFrameLabel, flag=fRight)
        pfgs.Add(self.availableMsPerFrame, flag=fRight)
        pfgs.Add(self.availableMsPerFrameUnit, flag=fLeft)

        # ------------------- Row 2 ------------------------------
        pfgs.Add(self.framesPerSecondLabel, flag=fRight)
        pfgs.Add(self.framesPerSecond, flag=fRight)
        pfgs.Add(self.framesPerSecondUnit, flag=fLeft)
        pfgs.Add(self.frameProcessingTimeLabel, flag=fRight)
        pfgs.Add(self.frameProcessingTime, flag=fRight)
        pfgs.Add(self.frameProcessingTimeUnit, flag=fLeft)

        statsSizer = wx.BoxSizer(wx.VERTICAL)
        statsSizer.Add(cameraDeviceSizer)
        statsSizer.Add(pfgs, flag=wx.TOP, border=8)
        headerSizer.Add(statsSizer, flag=wx.ALL, border=4)
        mainSizer.Add(headerSizer)

        self.messagesText = wx.TextCtrl(
            self, style=wx.TE_READONLY | wx.TE_MULTILINE | wx.HSCROLL, size=(350, imageHeight)
        )
        self.messageManager = MessageManager(self.messagesText)

        border = 2
        row1Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row1Sizer.Add(self.primaryImage, flag=wx.ALL, border=border)
        row1Sizer.Add(self.messagesText, 1, flag=wx.TOP | wx.BOTTOM | wx.RIGHT | wx.EXPAND, border=border)
        mainSizer.Add(row1Sizer, 1, flag=wx.EXPAND)

        row2Sizer = wx.BoxSizer(wx.HORIZONTAL)
        row2Sizer.Add(self.beforeImage, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM, border=border)
        row2Sizer.Add(self.afterImage, flag=wx.RIGHT | wx.BOTTOM, border=border)
        mainSizer.Add(row2Sizer)

        # ------------------------------------------------------------------------------------------------
        # Create a timer to update the frame loop.
        #
        self.timer = wx.Timer()
        self.timer.Bind(wx.EVT_TIMER, self.frameLoop)

        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

        self.readOptions()
        self.SetSizerAndFit(mainSizer)

        # Start the message reporting thread so we can see what is going on.
        self.messageThread = threading.Thread(target=self.showMessages)
        self.messageThread.daemon = True
        self.messageThread.start()

        self.grabFrameOK = False

        for i in [self.primaryImage, self.beforeImage, self.afterImage]:
            i.SetTestImage()

            # Start the frame loop.
        delayAdjustment = 0.80 if "win" in sys.platform else 0.98
        ms = int(1000 * self.frameDelay * delayAdjustment)
        self.timer.Start(ms, False)

    def Start(self):
        self.messageQ.put(("", "************************************************"))
        self.messageQ.put(("started", now().strftime("%Y/%m/%d %H:%M:%S")))
        self.startThreads()
        self.startCamera()

    def showMessages(self):
        while 1:
            message = self.messageQ.get()
            assert len(message) == 2, "Incorrect message length"
            cmd, info = message
            wx.CallAfter(self.messageManager.write, "{}:  {}".format(cmd, info) if cmd else info)

    def startCamera(self):
        self.camera = None
        try:
            self.camera = Device(max(self.getCameraDeviceNum(), 0))
        except Exception as e:
            self.messageQ.put(("camera", "Error: {}".format(e)))
            return False

            # self.camera.set_resolution( 640, 480 )
        self.messageQ.put(("camera", "Successfully Connected: Device: {}".format(self.getCameraDeviceNum())))
        for i in self.beforeAfterImages:
            i.SetTestImage()
        return True

    def startThreads(self):
        self.grabFrameOK = False

        self.listenerThread = SocketListener(self.requestQ, self.messageQ)
        error = self.listenerThread.test()
        if error:
            wx.MessageBox(
                "Socket Error:\n\n{}\n\nIs another CrossMgrVideo or CrossMgrCamera running on this computer?".format(
                    error
                ),
                "Socket Error",
                wx.OK | wx.ICON_ERROR,
            )
            wx.Exit()

        self.writerThread = threading.Thread(target=PhotoWriter, args=(self.writerQ, self.messageQ, self.ftpQ))
        self.writerThread.daemon = True

        self.renamerThread = threading.Thread(target=PhotoRenamer, args=(self.renamerQ, self.writerQ, self.messageQ))
        self.renamerThread.daemon = True

        self.ftpThread = threading.Thread(target=FTPWriter, args=(self.ftpQ, self.messageQ))
        self.ftpThread.daemon = True

        self.fcb = FrameCircBuf(int(self.bufferSecs * self.fps))

        self.listenerThread.start()
        self.writerThread.start()
        self.renamerThread.start()
        self.ftpThread.start()

        self.grabFrameOK = True
        self.messageQ.put(("threads", "Successfully Launched"))
        return True

    def frameLoop(self, event=None):
        if not self.grabFrameOK:
            return

        self.frameCount += 1
        tNow = now()

        # Compute frame rate statistics.
        if self.frameCount == self.frameCountUpdate:
            self.fpsActual = self.frameCount / (tNow - self.tFrameCount).total_seconds()
            self.frameCount = 0
            self.tFrameCount = tNow
            self.framesPerSecond.SetLabel(u"{:.3f}".format(self.fpsActual))
            self.availableMsPerFrame.SetLabel(u"{:.0f}".format(1000.0 / self.fpsActual))
            self.frameProcessingTime.SetLabel(u"{:.0f}".format(1000.0 * self.fpt.total_seconds()))

        try:
            image = self.camera.getImage()
        except Exception as e:
            self.messageQ.put(("error", "Webcam Failure: {}".format(e)))
            self.grabFrameOK = False
            return

        image = AddPhotoHeader(
            PilImageToWxImage(image),
            time=tNow,
            raceSeconds=(tNow - self.tLaunch).total_seconds(),
            bib=999,
            firstNameTxt=u"Firstname",
            lastNameTxt=u"LASTNAME",
            teamTxt=u"Team",
            raceNameTxt="Racename",
        )
        self.fcb.append(tNow, image)
        wx.CallAfter(self.primaryImage.SetImage, image)

        # Process any save messages
        while 1:
            try:
                message = self.requestQ.get(False)
            except Empty:
                break

            tSearch = message["time"]
            advanceSeconds = message.get("advanceSeconds", 0.0)
            if advanceSeconds:
                tSearch += timedelta(seconds=advanceSeconds)
            times, frames = self.fcb.findBeforeAfter(tSearch, 1, 1)
            if not frames:
                self.messageQ.put(
                    ("error", "No photos for {} at {}".format(message.get("bib", None), message["time"].isoformat()))
                )

            lastImage = None
            for i, f in enumerate(frames):

                fname = GetPhotoFName(
                    message["dirName"], message.get("bib", None), message.get("raceSeconds", None), i, times[i]
                )
                image = AddPhotoHeader(
                    f,
                    message.get("bib", None),
                    message.get("time", None),
                    message.get("raceSeconds", None),
                    message.get("firstName", u""),
                    message.get("lastName", u""),
                    message.get("team", u""),
                    message.get("raceName", u""),
                )

                # if lastImage is not None and image.GetData() == lastImage.GetData():
                # 	self.messageQ.put( ('duplicate', '"{}"'.format(os.path.basename(fname))) )
                # 	continue

                wx.CallAfter(self.beforeAfterImages[i].SetImage, image)

                SaveImage(fname, image, message.get("ftpInfo", None), self.messageQ, self.writerQ)
                lastImage = image

            if (now() - tNow).total_seconds() > self.frameDelay / 2.0:
                break

        self.fpt = now() - tNow
        self.tLast = tNow

    def shutdown(self):
        # Ensure that all images in the queue are saved.
        self.grabFrameOK = False
        if hasattr(self, "writerThread"):
            self.writerQ.put(("terminate",))
            self.writerThread.join()

    def resetCamera(self, event):
        self.writeOptions()

        dlg = ConfigDialog(self, self.getCameraDeviceNum())
        ret = dlg.ShowModal()
        cameraDeviceNum = dlg.GetCameraDeviceNum()
        dlg.Destroy()
        if ret != wx.ID_OK:
            return

        self.setCameraDeviceNum(cameraDeviceNum)
        self.grabFrameOK = self.startCamera()

    def setCameraDeviceNum(self, num):
        self.cameraDevice.SetLabel(unicode(num))

    def getCameraDeviceNum(self):
        return int(self.cameraDevice.GetLabel())

    def onCloseWindow(self, event):
        self.shutdown()
        wx.Exit()

    def writeOptions(self):
        self.config.Write("CameraDevice", self.cameraDevice.GetLabel())
        self.config.Flush()

    def readOptions(self):
        self.cameraDevice.SetLabel(self.config.Read("CameraDevice", u"0"))