From 23945f0748d2d088dbc1a5171f44fd164d3f2cd6 Mon Sep 17 00:00:00 2001 From: Giaphage <73594574+Giaphage@users.noreply.github.com> Date: Tue, 16 Feb 2021 18:01:51 +0000 Subject: [PATCH] Initial Commit Added Comments --- Source/BasicControls.py | 35 ++++- Source/Livestream.py | 169 ++++++++++++++++++++++++ Source/MotorControls.py | 52 ++++++++ Source/PhotoParams.py | 2 +- Source/PiCameraApp.py | 231 ++++++++++++++++++++------------- Source/PositionalTracking.py | 169 ++++++++++++++++++++++++ Source/Timelapse.py | 222 ++++++++++--------------------- Source/tests/cursor example.py | 12 ++ 8 files changed, 642 insertions(+), 250 deletions(-) create mode 100644 Source/Livestream.py create mode 100644 Source/MotorControls.py create mode 100644 Source/PositionalTracking.py create mode 100644 Source/tests/cursor example.py diff --git a/Source/BasicControls.py b/Source/BasicControls.py index c33498d..b5b7ece 100644 --- a/Source/BasicControls.py +++ b/Source/BasicControls.py @@ -42,8 +42,8 @@ from Dialog import * from Mapping import * -from NotePage import * -from Utils import * +from NotePage import * +from Utils import * from VideoParams import * from PhotoParams import * from ImageEffects import * @@ -96,6 +96,7 @@ def BuildPage ( self ): self.FixedResolutionChanged) self.FixedResolutionsCombo.grid(row=0,column=1,columnspan=3,sticky='W') ToolTip(self.FixedResolutionsCombo,121) + #------------ Capture Width and Height ---------------- # OrderedDict is used to ensure the keys stay in the same order as # entered. I want the combobox to display in this order @@ -355,10 +356,13 @@ def Reset ( self ): self.effects.current(0) self.UseRadio.focus_set() self.FlashModeOffRadio.invoke() + def UseVideoPort ( self , val): pass #self.camera.use_video_port = val + def LedOnChecked ( self ): self.camera.led = self.LedOn.get() + def SetupLabelCombo ( self, parent, textname, rownum, colnum, minto, maxto, callback, cameraVal, label=''): l = Label(parent,text=textname) @@ -373,40 +377,52 @@ def SetupLabelCombo ( self, parent, textname, rownum, colnum, scale.set(val) # this would attempt to call any callback scale.config(command=callback) # now supply the callback return label, scale, val + def UpdateMe( self, newVal, label ): val = int(float(newVal)) label.config(text='%d' % val, foreground='red' if val < 0 else 'blue' if val > 0 else 'black' ) return val + def CameraBrightnessChanged ( self, newVal ): self.brightness.focus_set() + self.camera.brightness = self.UpdateMe(newVal,self.brightLabel) + def ContrastChanged ( self, newVal ): self.contrast.focus_set() self.camera.contrast = self.UpdateMe(newVal,self.contrastLabel) + def SaturationChanged ( self, newVal ): self.saturation.focus_set() self.camera.saturation = self.UpdateMe(newVal,self.saturationLabel) + def SharpnessChanged ( self, newVal ): self.sharpness.focus_set() self.camera.sharpness = self.UpdateMe(newVal,self.sharpnessLabel) + def ResetGeneralSliders ( self ): self.brightness.set(50) self.contrast.set(0) self.saturation.set(0) self.sharpness.set(0) #self.ResetGeneralButton.focus_set() + def UpdateWidthHeightLabels ( self ): res = self.camera.resolution # in case a different default value self.WidthLabel.config(text='%d' % int(res[0])) self.HeightLabel.config(text='%d' % int(res[1])) + def ResolutionChanged(self,event): self.camera.resolution = (int(self.cb.get()),int(self.cb1.get())) + self.UpdateWidthHeightLabels() + def FixedResolutionChanged ( self, event ): key = self.FixedResolutionsCombo.get().split(':')[0] self.camera.resolution = self.StandardResolutions[key] self.UpdateWidthHeightLabels() + def UseFixedResRadios ( self ): states = {False:'disabled', True:'readonly'} useFixedRes = self.UseFixedResolutions.get() @@ -419,20 +435,24 @@ def UseFixedResRadios ( self ): self.FixedResolutionsCombo.config(state=states[useFixedRes]) self.cb.config(state=states[not useFixedRes]) self.cb1.config(state=states[not useFixedRes]) + def Zoom ( self, newVal, scale ): self.camera.zoom = (float(self.Xzoom.get()),float(self.Yzoom.get()), float(self.Widthzoom.get()),float(self.Heightzoom.get())) scale.focus_set() + def SetZoom ( self, x, y, w, h ): self.Xzoom.set(x) self.Yzoom.set(y) self.Widthzoom.set(w) self.Heightzoom.set(h) + def ZoomReset ( self ): self.Xzoom.set(0.0) self.Yzoom.set(0.0) self.Widthzoom.set(1.0) self.Heightzoom.set(1.0) + def AllowImageResizeAfter ( self, allowResizeAfter ): if allowResizeAfter: state = 'readonly' @@ -443,11 +463,14 @@ def AllowImageResizeAfter ( self, allowResizeAfter ): state = 'disabled' self.resizeWidthAfterCombo.config(state=state) self.resizeHeightAfterCombo.config(state=state) + def ResizeAfterChanged ( self, event ): self.resizeAfter = ( int(self.resizeWidthAfterCombo.get()), int(self.resizeHeightAfterCombo.get()) ) + def GetResizeAfter ( self ): return self.resizeAfter + def EffectsChecked ( self, EffectsEnabled ): if EffectsEnabled == True: self.effects.config(state='readonly') @@ -457,6 +480,7 @@ def EffectsChecked ( self, EffectsEnabled ): self.effects.config(state='disabled') self.ModParams.config(state='disabled') self.camera.image_effect = 'none' + def EffectsChanged ( self, event ): self.camera.image_effect = self.effects.get() if self.camera.image_effect in ['solarize', 'colorpoint', @@ -469,15 +493,20 @@ def EffectsChanged ( self, event ): Effects1Page.EffectParam[self.camera.image_effect] else: self.ModParams.config(state='disabled') + def ModifyEffectsParamsPressed ( self ): ImageEffectsDialog(self,title='Image Effects Parameters', camera=self.camera,okonly=False) + def ImageDenoiseChecked ( self ): self.camera.image_denoise = self.ImageDenoise.get() + def VideoDenoiseChecked ( self ): self.camera.video_denoise = self.VideoDenoise.get() + def VideoStabChecked ( self ): self.camera.video_stabilization = self.VideoStab.get() + def FlashModeButton ( self, FlashMode ): if FlashMode == 'set': self.FlashModeCombo.config(state='readonly') @@ -486,6 +515,6 @@ def FlashModeButton ( self, FlashMode ): else: self.FlashModeCombo.config(state='disabled') self.camera.flash_mode = FlashMode + def FlashModeChanged ( self, event ): self.camera.flash_mode = self.FlashModeCombo.get() - diff --git a/Source/Livestream.py b/Source/Livestream.py new file mode 100644 index 0000000..3d339e4 --- /dev/null +++ b/Source/Livestream.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +# Timelapse.py +# +# Copyright 2018 Bill Williams +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +''' +from time import sleep +from Dialog import * +from Mapping import * +from NotePage import * + +class Livestream ( BasicNotepage ): + def BuildPage ( self ): + f = ttk.LabelFrame(self,text='Time lapse settings',padding=(5,5,5,5)) + f.grid(row=0,column=0,columnspan=4,sticky='NEWS',pady=5) + f.columnconfigure(2,weight=1) + f.columnconfigure(4,weight=1) + + Label(f,text='Default').grid(row=0,column=0,sticky='E') + self.LowLightCaptureButton = Button(f,text='Low Light',width=15, \ + command=self.CaptureLowLight) + self.LowLightCaptureButton.grid(row=1,column=0,sticky='W') + self.StartDelayCaptureButton = Button(f,text='Delay Capture',width=15, \ + command=self.StartDelayCapture) + self.StartDelayCaptureButton.grid(row=2,column=0,sticky='W') + + def CaptureLowLight ( self ): + self.camera.capture('foo.jpg') + pass + def StartDelayCapture ( self ): + pass + #### TODO: Implement Reset NEEDS LOTS OF WORK!! + def Reset ( self ): + pass + +''' + What controls are needed for this page? + Photo captures: + Type of Time lapse + Burst + Timed + etc + Whether the image settings stay the same for each picture - Checkbox + Whether the Video port is used or not (faster) - Checkbox + Filename (Textbox entry) + Start + Immediately + Delay XXX YYY SEC, MIN HR + On a specific date/time + Delay between each shot.... or at a specific time each day, etc.... + e.g., Every XX YYY where XX is a number YYY is SEC, MIN, HR, DAY + e.g., On every MIN, 1/2 HR, HOUR + e.g., Every Day at XX:XX Time + When does the capture end + After XXX shots XXX from 1 to 1000? + After XXX minutes, Hours, Days + On XXXX date + Append a number or a date/time to 'Filename' - or both + Use Drop down ComboBox + e.g. Bill_1.jpg, Bill_2.jpg, ... etc + or Bill_Date_Time.jpg, Bill_Date_Time.jpg, ... etc + or both Bill_Date_Time_1.jpg, Bill_Date_Time_2.jpg, ... etc + What about video captures? +''' + + +''' + Examples from the picamera documentation + https://picamera.readthedocs.io/en/release-1.13/recipes1.html + +The following script provides a brief example of configuring these settings: + +from time import sleep +from picamera import PiCamera + +camera = PiCamera(resolution=(1280, 720), framerate=30) +# Set ISO to the desired value +camera.iso = 100 +# Wait for the automatic gain control to settle +sleep(2) +# Now fix the values +camera.shutter_speed = camera.exposure_speed +camera.exposure_mode = 'off' +g = camera.awb_gains +camera.awb_mode = 'off' +camera.awb_gains = g +# Finally, take several photos with the fixed settings +camera.capture_sequence(['image%02d.jpg' % i for i in range(10)]) + +from time import sleep +from picamera import PiCamera + +camera = PiCamera() +camera.start_preview() +sleep(2) +for filename in camera.capture_continuous('img{counter:03d}.jpg'): + print('Captured %s' % filename) + sleep(300) # wait 5 minutes +''' + +''' +from time import sleep +from picamera import PiCamera +from datetime import datetime, timedelta + +def wait(): + # Calculate the delay to the start of the next hour + next_hour = (datetime.now() + timedelta(hour=1)).replace( + minute=0, second=0, microsecond=0) + delay = (next_hour - datetime.now()).seconds + sleep(delay) + +camera = PiCamera() +camera.start_preview() +wait() +for filename in camera.capture_continuous('img{timestamp:%Y-%m-%d-%H-%M}.jpg'): + print('Captured %s' % filename) + wait() + + +3.7. Capturing in low light +Using similar tricks to those in Capturing consistent images, the Pi’s +camera can capture images in low light conditions. The primary objective +is to set a high gain, and a long exposure time to allow the camera to +gather as much light as possible. However, the shutter_speed attribute +is constrained by the camera’s framerate so the first thing we need to +do is set a very slow framerate. The following script captures an image +with a 6 second exposure time (the maximum the Pi’s V1 camera module is +capable of; the V2 camera module can manage 10 second exposures): + +from picamera import PiCamera +from time import sleep +from fractions import Fraction + +# Force sensor mode 3 (the long exposure mode), set +# the framerate to 1/6fps, the shutter speed to 6s, +# and ISO to 800 (for maximum gain) +camera = PiCamera( + resolution=(1280, 720), + framerate=Fraction(1, 6), + sensor_mode=3) +camera.shutter_speed = 6000000 +camera.iso = 800 +# Give the camera a good long time to set gains and +# measure AWB (you may wish to use fixed AWB instead) +sleep(30) +camera.exposure_mode = 'off' +# Finally, capture an image with a 6s exposure. Due +# to mode switching on the still port, this will take +# longer than 6 seconds +camera.capture('dark.jpg') +''' diff --git a/Source/MotorControls.py b/Source/MotorControls.py new file mode 100644 index 0000000..557da6f --- /dev/null +++ b/Source/MotorControls.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +# Timelapse.py +# +# Copyright 2018 Bill Williams +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +''' +from time import sleep +from Dialog import * +from Mapping import * +from NotePage import * + +class MotorControls ( BasicNotepage ): + def BuildPage ( self ): + f = ttk.LabelFrame(self,text='Time lapse settings',padding=(5,5,5,5)) + f.grid(row=0,column=0,columnspan=4,sticky='NEWS',pady=5) + f.columnconfigure(2,weight=1) + f.columnconfigure(4,weight=1) + + Label(f,text='Default').grid(row=0,column=0,sticky='E') + self.LowLightCaptureButton = Button(f,text='Low Light',width=15, \ + command=self.CaptureLowLight) + self.LowLightCaptureButton.grid(row=1,column=0,sticky='W') + self.StartDelayCaptureButton = Button(f,text='Delay Capture',width=15, \ + command=self.StartDelayCapture) + self.StartDelayCaptureButton.grid(row=2,column=0,sticky='W') + + def CaptureLowLight ( self ): + self.camera.capture('foo.jpg') + pass + def StartDelayCapture ( self ): + pass + #### TODO: Implement Reset NEEDS LOTS OF WORK!! + def Reset ( self ): + pass + diff --git a/Source/PhotoParams.py b/Source/PhotoParams.py index 7b57984..036590d 100644 --- a/Source/PhotoParams.py +++ b/Source/PhotoParams.py @@ -20,7 +20,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301, USA. # -''' +''' try: from Tkinter import * except ImportError: diff --git a/Source/PiCameraApp.py b/Source/PiCameraApp.py index 299d214..decd8fb 100644 --- a/Source/PiCameraApp.py +++ b/Source/PiCameraApp.py @@ -1,6 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +''' +This program has been modified for use with MicroscoPy under the +terms of the published by the Free Software Foundation. + +Thank you to Bill Williams for building the framework for the +original Graphical User Interface, I hope the comments and +modications I have made as part of my final year MEng project +are useful! + +I have learnt Python and TKinter practically from scratch, and have +no extensive previous programming knowledge, in a short few weeks for +this project so apologies if my PEP 8 standards are not up to par. + +Benjamin Hutchings +University of Birmingham, UK. +''' + ''' PiCameraApp.py Copyright (C) 2015 - Bill Williams @@ -17,38 +34,24 @@ ''' ''' - 1.) LED control no longer works on RPI 3 Model B - The camera LED cannot currently be controlled when the module is - attached to a Raspberry Pi 3 Model B as the GPIO that controls the - LED has moved to a GPIO expander not directly accessible to the - ARM processor. - 2.) +-------------Import Standard Python Library Modules------------------ ''' -########### TODO: Standardize variable, class, member cases - -import time -import datetime -import webbrowser # display the Picamera documentation -import io -import time -import os -from time import time, sleep -from Tooltip import * -from AnnotationOverlay import * - -#~ try: -import RPi.GPIO -#~ except ImportError: - #~ RPiGPIO = False -''' -From PiCamera ver 1.3 documentation -https://picamera.readthedocs.io/en/release-1.13/recipes1.html -Be aware when you first use the LED property it will set the GPIO -library to Broadcom (BCM) mode with GPIO.setmode(GPIO.BCM) and disable -warnings with GPIO.setwarnings(False). The LED cannot be controlled when -the library is in BOARD mode. -''' +import time +import datetime +import webbrowser +import io +import time +import os +from time import time, sleep +from Tooltip import * +from AnnotationOverlay import * + + +# #~ try: +# import RPi.GPIO +# #~ except ImportError: +# #~ RPiGPIO = False try: import picamera @@ -56,15 +59,14 @@ import picamera.array except ImportError: raise ImportError("You do not seem to have picamera installed") - try: from Tkinter import * # Python 2.X except ImportError: from tkinter import * # Python 3.X try: - from tkColorChooser import askcolor + from tkColorChooser import askcolor except ImportError: - from tkinter.colorchooser import askcolor + from tkinter.colorchooser import askcolor try: import tkFileDialog as FileDialog except ImportError: @@ -83,7 +85,6 @@ import tkFont except ImportError: import tkinter.font - # sudo apt-get install python3-pil.imagetk import PIL from PIL import Image, ExifTags @@ -93,27 +94,42 @@ raise ("ImageTk not installed. If running Python 3.x\n" \ "Use: sudo apt-get install python3-pil.imagetk") -from AboutDialog import * -from PreferencesDialog import * -from AnnotationOverlay import * -from KeyboardShortcuts import * -from Mapping import * -from NotePage import * -from CameraUtils import * -from BasicControls import * -from FinerControl import * -from Exposure import * -from Timelapse import * -from Utils import * - -# -# Main PiCameraApp Window -# + + + +''' +------------------Import Classes from Custom Librarys------------------ +''' + +# Original Classes +from AboutDialog import * +from PreferencesDialog import * +from AnnotationOverlay import * +from KeyboardShortcuts import * +from Mapping import * +from NotePage import * +from CameraUtils import * +from BasicControls import * +from FinerControl import * +from Exposure import * +from Timelapse import * +from Utils import * + +# MicroscoPy 2.0 Packages +from MotorControls import * +from Livestream import * +from PositionalTracking import * + + +''' +----------------------Main PiCameraApp Window------------------------ +''' class PiCameraApp ( Frame ): # Some statics used elsewhere ExposureModeText = None def __init__(self, root, camera, title): + # Creates a Frame for the Main Window Frame.__init__(self, root) self.grid(padx=5,pady=5) @@ -121,6 +137,7 @@ def __init__(self, root, camera, title): self.ControlMapping = ControlMapping() + # Starts Camera Preview self.camera = camera self.camera.start_preview(fullscreen=False,window=(0,0,10,10)) @@ -135,7 +152,8 @@ def __init__(self, root, camera, title): ToolTip.LoadToolTips() - #----------- Icons for Menu and Buttons ------------------------ + '''-------------- Icons for Menu and Buttons -------------------''' + self.iconClose = GetPhotoImage("Assets/window-close.png") #self.iconClose = ImageTk.PhotoImage(PIL.Image.open("Assets/window-close.png")) self.iconPrefs = GetPhotoImage('Assets/prefs1_16x16.png') @@ -146,8 +164,8 @@ def __init__(self, root, camera, title): image = PIL.Image.open('Assets/video-icon-b.png') self.iconVideoBig = GetPhotoImage(image.resize((22,22))) self.iconVideo = GetPhotoImage(image.resize((16,16))) - - #------------ Notebook with all camera control pages ----------- + + '''-------------- Notebook with all control pages -------------''' frame1 = ttk.Frame(master,padding=(5,5,5,5)) frame1.grid(row=0,column=0,sticky="NSEW") frame1.rowconfigure(2,weight=1) @@ -160,37 +178,29 @@ def __init__(self, root, camera, title): n.columnconfigure(0,weight=1) n.enable_traversal() - self.BasicControlsFrame = BasicControls(n,camera) - self.ExposureFrame = Exposure(n,camera) - self.FinerControlFrame = FinerControl(n,camera) - #self.TimelapseFrame = Timelapse(n,camera) - - n.add(self.BasicControlsFrame ,text='Basic',underline=0) - n.add(self.ExposureFrame,text='Exposure',underline=0) - n.add(self.FinerControlFrame,text='Advanced',underline=0) - #n.add(self.TimelapseFrame,text='Time lapse',underline=0) + # Calls Notebook Pages from external classes + # PiCamera Classes + self.BasicControlsFrame = BasicControls(n,camera) + self.ExposureFrame = Exposure(n,camera) + self.FinerControlFrame = FinerControl(n,camera) + # MicroscoPy Classes + self.TimelapseFrame = Timelapse(n,camera) # Unfinished + self.MotorControlsFrame = MotorControls(n,camera) # Unfinished + self.LivestreamFrame = Livestream(n,camera) # Unfinished + self.PositonalTrackingFrame = PositionalTracking(n,camera) # Unfinished + + n.add(self.BasicControlsFrame , text='Basic', underline=0) + n.add(self.ExposureFrame, text='Exposure', underline=0) + n.add(self.FinerControlFrame, text='Advanced', underline=0) + n.add(self.TimelapseFrame, text='Time lapse', underline=0) + + n.add(self.MotorControlsFrame, text='Motor Positioning', underline=0) + n.add(self.LivestreamFrame, text='Livestream', underline=0) + n.add(self.PositonalTrackingFrame, text='Object Tracking', underline=0) + self.FinerControlFrame.PassControlFrame(self.BasicControlsFrame) - # ----------------------Paned Window --------------------------- - # Start of Image Canvas preview, camera captures, - # Paned Window VERTICAL - # TopFrame - # Preview ImageCanvas row Weight=1 - # ButtonFrame - # Preview Buttons - # BottomFrame - #### TODO: Add title to BottomFrame 'Captured Image/Video' - # PanedWindow HORIZONTAL row 0, col 0 - # LeftFrame - # Camera setups, EXIF Text - #### TODO: ButtonFrame - #### TODO: Clear, Save as File, Save as Python - # RightFrame - # Current Photo Canvas - # ButtonFrame - # Picture / Video buttons - self.pw = ttk.PanedWindow(master,orient=VERTICAL,style='TPanedwindow', takefocus=True) self.pw.grid(row=0,column=1,sticky="NSEW") @@ -202,15 +212,14 @@ def __init__(self, root, camera, title): self.TopFrame.rowconfigure(0,weight=1) self.TopFrame.columnconfigure(1,weight=1) - #### TODO: Create Canvas Class to handle generic cursors, etc self.ImageCanvas = Canvas(self.TopFrame,width=256,height=256, background=self.ControlMapping.FocusColor,cursor='diamond_cross') self.ImageCanvas.grid(row=0,column=0,columnspan=2,sticky="NEWS") self.ImageCanvas.itemconfigure('nopreview',state='hidden') - self.ImageCanvas.bind("",self.CanvasMouseMove) - self.ImageCanvas.bind("",self.CanvasMouseMove) - self.ImageCanvas.bind("",self.CanvasEnterLeave) - self.ImageCanvas.bind("",self.CanvasEnterLeave) + self.ImageCanvas.bind("", self.CanvasMouseMove) + self.ImageCanvas.bind("", self.CanvasMouseMove) + self.ImageCanvas.bind("", self.CanvasEnterLeave) + self.ImageCanvas.bind("", self.CanvasEnterLeave) ButtonFrame = ttk.Frame(self.TopFrame,padding=(5,5,5,5),relief=SUNKEN) ButtonFrame.grid(row=1,column=0,columnspan=2,sticky="NEWS") @@ -329,13 +338,13 @@ def __init__(self, root, camera, title): self.photoCanvas.create_text(0,0, fill='red',tags=('text','objs'),anchor='nw') - self.photoCanvas.bind('',self.PhotoCanvasResize) - self.photoCanvas.bind("",self.photoCanvasScrollStart) - self.photoCanvas.bind("",self.photoCanvasScrollMove) - self.photoCanvas.bind("",self.photoCanvasMove) - self.photoCanvas.bind("",self.photoCanvasButtonUp) - self.photoCanvas.bind("",self.photoCanvasEnterLeave) - self.photoCanvas.bind("",self.photoCanvasEnterLeave) + self.photoCanvas.bind('', self.PhotoCanvasResize) + self.photoCanvas.bind("", self.photoCanvasScrollStart) + self.photoCanvas.bind("", self.photoCanvasScrollMove) + self.photoCanvas.bind("", self.photoCanvasMove) + self.photoCanvas.bind("", self.photoCanvasButtonUp) + self.photoCanvas.bind("", self.photoCanvasEnterLeave) + self.photoCanvas.bind("", self.photoCanvasEnterLeave) self.InPhotoZoom = False # hack - # self.PhotoState = 'none', 'picture', 'zoom', 'video' ??? @@ -564,14 +573,17 @@ def __init__(self, root, camera, title): root.protocol("WM_DELETE_WINDOW", lambda e=None:self.quitProgram(e)) self.UpdateAnnotationText() + def ResetCameraSetups ( self, event ): if MessageBox.askyesno("PiCamera", \ "Reset camera settings to default values?"): self.BasicControlsFrame.Reset() self.ExposureFrame.Reset() self.FinerControlFrame.Reset() + def SaveCameraSetups ( self, val ): pass + def ShowHideImageAttributesPane ( self, ShowIt): if ShowIt: if self.CurrentImage: self.photoPanedWindow.insert(0,self.LeftFrame) @@ -579,15 +591,18 @@ def ShowHideImageAttributesPane ( self, ShowIt): try: self.photoPanedWindow.forget(self.LeftFrame) except TclError: pass # Already forgotten! + def ViewImageCursor ( self, event ): if self.viewImageCursor.get() is True: self.photoCanvas.itemconfigure('cursors',state='normal') else: self.photoCanvas.itemconfigure('cursors',state='hidden') + def ViewImageAttributesPane ( self, event ): if not event == 'Menu': # Must change variable state ourselves self.viewImageAttributesPane.set(not self.viewImageAttributesPane.get()) self.ShowHideImageAttributesPane(self.viewImageAttributesPane.get()) + def ViewPreviewPane ( self, event ): if not event == 'Menu': # Must change variable state ourselves self.viewPreviewPane.set(not self.viewPreviewPane.get()) @@ -601,8 +616,10 @@ def ViewPreviewPane ( self, event ): self.pw.insert(0,self.TopFrame) else: self.pw.forget(self.TopFrame) + def TextboxResize ( self, event ): pass #print event.width + def SavePictureorVideo ( self, event ): #### TODO: Create a picture class that maintains the state of # the current image. This would include the state of the camera @@ -638,6 +655,7 @@ def SavePictureorVideo ( self, event ): # What options are available from PILLOW? self.CurrentImage.save(path) print ("Not JPEG save") + def photoCanvasScrollStart ( self, event ): if self.CurrentImage: if event.state & 0x0004 == 0x0004: # Ctrl key @@ -650,11 +668,13 @@ def photoCanvasScrollStart ( self, event ): else: #self.photoCanvas.config(cursor='hand1') self.photoCanvas.scan_mark(event.x,event.y) + def photoCanvasScrollMove ( self, event ): if self.CurrentImage: if not self.InPhotoZoom: self.photoCanvas.scan_dragto(event.x,event.y,gain=3) self.photoCanvasMove(event) + def photoCanvasButtonUp ( self, event ): self.photoCanvas.config(cursor='diamond_cross') if self.InPhotoZoom: @@ -671,6 +691,7 @@ def photoCanvasButtonUp ( self, event ): #self.photoCanvas.config(scrollregion=(int(coords[0]), #int(coords[1]),int(coords[2]),int(coords[3]))) self.photoCanvas.delete("zoom") + def photoCanvasMove ( self, event ): if not self.CurrentImage: return @@ -708,6 +729,7 @@ def photoCanvasMove ( self, event ): y1 = coords[1] y2 = y self.photoCanvas.coords('zoom',x1,y1,x2,y2) + def WheelScrollPhotoCanvas ( self, event ): if event.state & 0x0004 == 0x0004: # Ctrl key if not self.CurrentImage: return @@ -717,6 +739,7 @@ def WheelScrollPhotoCanvas ( self, event ): else: self.photoZoomScale *= (1.0/1.1) self.LoadImageFromStream(self.photoZoomScale) + def TakePicture ( self, event ): if self.InCaptureVideo: return @@ -733,6 +756,7 @@ def TakePicture ( self, event ): self.photoCanvas.itemconfigure('capture',state='normal') self.photoCanvas.itemconfigure('capture',text='Capture in progress...') self.after(500,self.CapturePicture(photoFormat)) + def CapturePicture ( self, photoFormat ): try: if photoFormat == 'jpeg': #'/home/pi/Pictures/Image.jpeg' @@ -752,6 +776,7 @@ def CapturePicture ( self, photoFormat ): self.LoadImageFromStream ( 1.0 ) self.photoCanvas.itemconfigure('objs',state='normal') self.photoCanvas.tag_raise("objs") # raise Z order to topmost + def LoadImageFromStream ( self, zoom ): if self.photo: del self.photo @@ -836,12 +861,14 @@ def ToggleVideo ( self, event ): else: try: os.remove(self.TempFile) except: pass + def UpdateCaptureInProgress ( self ): if not self.InCaptureVideo: return # keep updating video capture time delta = time() - self.time self.photoCanvas.itemconfigure('capture',text='Recording %.2f sec'%delta) self.after(50,self.UpdateCaptureInProgress) # call again + def ClearPicture ( self, event ): self.ShowHideImageAttributesPane(False) self.CameraUtils.ClearTextBox() @@ -854,16 +881,20 @@ def ClearPicture ( self, event ): self.photoCanvas.itemconfigure('cross',state='normal') self.photoCanvas.itemconfigure('objs',state='hidden') self.photoCanvas.config(scrollregion=(0,0,1,1)) + def FlipPictureH ( self, event ): if self.CurrentImage: pass #self.CurrentImage.transpose(ImageTk.FLIP_LEFT_RIGHT) + def FlipPictureV ( self, event ): if self.CurrentImage: pass #self.CurrentImage.transpose(ImageTk.FLIP_TOP_BOTTOM) + def ViewProperties ( self, event ): pass + def PhotoCanvasResize ( self, event ): size = (event.width,event.height) x = self.photoCanvas.canvasx(event.x) @@ -889,6 +920,7 @@ def PhotoCanvasResize ( self, event ): self.photoCanvas.coords('cross1',0,0,0+size[0],0+size[1]) self.photoCanvas.coords('cross2',0+size[0],0,0,0+size[1]) self.photoCanvas.coords('capture',0+size[0]/2,0+size[1]/2) + def photoCanvasEnterLeave ( self, event ): self.XYText.set('X: Y:') if not self.CurrentImage: return @@ -900,6 +932,7 @@ def photoCanvasEnterLeave ( self, event ): self.photoCanvas.itemconfigure('objs',state=state1) else: self.photoCanvas.itemconfigure('objs',state='hidden') + def CanvasMouseMove ( self, event ): res = self.camera.resolution canvas = self.CanvasSize @@ -913,6 +946,7 @@ def CanvasMouseMove ( self, event ): #self.statusText.set('%d X: %d Y: W: %d H: %d' % \ #(event.x,event.y,canvas[2],canvas[3])) + def CanvasEnterLeave ( self, event ): pass def SetPreviewOn ( self ): @@ -945,25 +979,32 @@ def SetPreviewOn ( self ): self.WindowSize.state(['disabled'] \ if not ( self.PreviewOn.get() and self.ShowOnScreen.get() ) \ else ['!disabled']) + def AlphaChanged(self, newVal): val = int(float(newVal)) self.camera.preview.alpha = val self.alpha.focus_set() + def ToggleHFlip ( self ): self.HFlipState = not self.HFlipState #self.camera.preview.hflip = not self.camera.preview.hflip self.camera.hflip = not self.camera.hflip + def ToggleVFlip ( self ): self.VFlipState = not self.VFlipState self.camera.vflip = not self.camera.vflip #self.camera.preview.vflip = not self.camera.preview.vflip + def RotateCamera ( self ): r = (self.camera.rotation + 90) % 360 self.camera.rotation = r + def SetPreviewLocation ( self ): self.SetPreviewOn(); + def WindowSizeChanged ( self, newVal ): self.SetPreviewOn(); + def LoseFocus ( self, event ): ''' The Combobox is a problem. @@ -1041,18 +1082,22 @@ def UpdateAnnotationText ( self ): else: text = text + " (" + t + ")" self.camera.annotate_text = text self.after(1000,self.UpdateAnnotationText) + def SystemPreferences ( self, event ): PreferencesDialog(self,title='PiCamera Preferences',camera=self.camera, minwidth=400,minheight=550,okonly=False) self.ControlMapping.SetControlMapping() + def AnnotationOverlay ( self, event ): AnnotationOverlayDialog(self,title='Annotation / Overlay', camera=self.camera,okonly=False) + def ViewStatusBar ( self, event ): if self.ViewStatusBarBoolean.get() is True: self.StatusBarFrame.grid() else: self.StatusBarFrame.grid_remove() + def KeyboardShortcuts ( self, event ): KeyboardShortcutsDialog(self,title='Keyboard shortcuts', \ resizable=True,minwidth=400,minheight=300) # minwidth does not work by itself, minwidth=300) diff --git a/Source/PositionalTracking.py b/Source/PositionalTracking.py new file mode 100644 index 0000000..e88bf45 --- /dev/null +++ b/Source/PositionalTracking.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +# Timelapse.py +# +# Copyright 2018 Bill Williams +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +''' +from time import sleep +from Dialog import * +from Mapping import * +from NotePage import * + +class PositionalTracking ( BasicNotepage ): + def BuildPage ( self ): + f = ttk.LabelFrame(self,text='Time lapse settings',padding=(5,5,5,5)) + f.grid(row=0,column=0,columnspan=4,sticky='NEWS',pady=5) + f.columnconfigure(2,weight=1) + f.columnconfigure(4,weight=1) + + Label(f,text='Default').grid(row=0,column=0,sticky='E') + self.LowLightCaptureButton = Button(f,text='Low Light',width=15, \ + command=self.CaptureLowLight) + self.LowLightCaptureButton.grid(row=1,column=0,sticky='W') + self.StartDelayCaptureButton = Button(f,text='Delay Capture',width=15, \ + command=self.StartDelayCapture) + self.StartDelayCaptureButton.grid(row=2,column=0,sticky='W') + + def CaptureLowLight ( self ): + self.camera.capture('foo.jpg') + pass + def StartDelayCapture ( self ): + pass + #### TODO: Implement Reset NEEDS LOTS OF WORK!! + def Reset ( self ): + pass + +''' + What controls are needed for this page? + Photo captures: + Type of Time lapse + Burst + Timed + etc + Whether the image settings stay the same for each picture - Checkbox + Whether the Video port is used or not (faster) - Checkbox + Filename (Textbox entry) + Start + Immediately + Delay XXX YYY SEC, MIN HR + On a specific date/time + Delay between each shot.... or at a specific time each day, etc.... + e.g., Every XX YYY where XX is a number YYY is SEC, MIN, HR, DAY + e.g., On every MIN, 1/2 HR, HOUR + e.g., Every Day at XX:XX Time + When does the capture end + After XXX shots XXX from 1 to 1000? + After XXX minutes, Hours, Days + On XXXX date + Append a number or a date/time to 'Filename' - or both + Use Drop down ComboBox + e.g. Bill_1.jpg, Bill_2.jpg, ... etc + or Bill_Date_Time.jpg, Bill_Date_Time.jpg, ... etc + or both Bill_Date_Time_1.jpg, Bill_Date_Time_2.jpg, ... etc + What about video captures? +''' + + +''' + Examples from the picamera documentation + https://picamera.readthedocs.io/en/release-1.13/recipes1.html + +The following script provides a brief example of configuring these settings: + +from time import sleep +from picamera import PiCamera + +camera = PiCamera(resolution=(1280, 720), framerate=30) +# Set ISO to the desired value +camera.iso = 100 +# Wait for the automatic gain control to settle +sleep(2) +# Now fix the values +camera.shutter_speed = camera.exposure_speed +camera.exposure_mode = 'off' +g = camera.awb_gains +camera.awb_mode = 'off' +camera.awb_gains = g +# Finally, take several photos with the fixed settings +camera.capture_sequence(['image%02d.jpg' % i for i in range(10)]) + +from time import sleep +from picamera import PiCamera + +camera = PiCamera() +camera.start_preview() +sleep(2) +for filename in camera.capture_continuous('img{counter:03d}.jpg'): + print('Captured %s' % filename) + sleep(300) # wait 5 minutes +''' + +''' +from time import sleep +from picamera import PiCamera +from datetime import datetime, timedelta + +def wait(): + # Calculate the delay to the start of the next hour + next_hour = (datetime.now() + timedelta(hour=1)).replace( + minute=0, second=0, microsecond=0) + delay = (next_hour - datetime.now()).seconds + sleep(delay) + +camera = PiCamera() +camera.start_preview() +wait() +for filename in camera.capture_continuous('img{timestamp:%Y-%m-%d-%H-%M}.jpg'): + print('Captured %s' % filename) + wait() + + +3.7. Capturing in low light +Using similar tricks to those in Capturing consistent images, the Pi’s +camera can capture images in low light conditions. The primary objective +is to set a high gain, and a long exposure time to allow the camera to +gather as much light as possible. However, the shutter_speed attribute +is constrained by the camera’s framerate so the first thing we need to +do is set a very slow framerate. The following script captures an image +with a 6 second exposure time (the maximum the Pi’s V1 camera module is +capable of; the V2 camera module can manage 10 second exposures): + +from picamera import PiCamera +from time import sleep +from fractions import Fraction + +# Force sensor mode 3 (the long exposure mode), set +# the framerate to 1/6fps, the shutter speed to 6s, +# and ISO to 800 (for maximum gain) +camera = PiCamera( + resolution=(1280, 720), + framerate=Fraction(1, 6), + sensor_mode=3) +camera.shutter_speed = 6000000 +camera.iso = 800 +# Give the camera a good long time to set gains and +# measure AWB (you may wish to use fixed AWB instead) +sleep(30) +camera.exposure_mode = 'off' +# Finally, capture an image with a 6s exposure. Due +# to mode switching on the still port, this will take +# longer than 6 seconds +camera.capture('dark.jpg') +''' diff --git a/Source/Timelapse.py b/Source/Timelapse.py index cf55d1c..2bffe13 100644 --- a/Source/Timelapse.py +++ b/Source/Timelapse.py @@ -1,26 +1,3 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -''' -# Timelapse.py -# -# Copyright 2018 Bill Williams -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301, USA. -# -''' from time import sleep from Dialog import * from Mapping import * @@ -28,142 +5,81 @@ class Timelapse ( BasicNotepage ): def BuildPage ( self ): - f = ttk.LabelFrame(self,text='Time lapse settings',padding=(5,5,5,5)) - f.grid(row=0,column=0,columnspan=4,sticky='NEWS',pady=5) - f.columnconfigure(2,weight=1) - f.columnconfigure(4,weight=1) - - Label(f,text='Default').grid(row=0,column=0,sticky='E') - self.LowLightCaptureButton = Button(f,text='Low Light',width=15, \ - command=self.CaptureLowLight) - self.LowLightCaptureButton.grid(row=1,column=0,sticky='W') - self.StartDelayCaptureButton = Button(f,text='Delay Capture',width=15, \ - command=self.StartDelayCapture) - self.StartDelayCaptureButton.grid(row=2,column=0,sticky='W') + #------------------------Timings------------------------ + # f = ttk.LabelFrame(self,text='Timings',padding=(5,5,5,5)) + # f.grid(row=0,column=0,columnspan=4,sticky='NEWS',pady=5) + # f.columnconfigure(2,weight=1) + # f.columnconfigure(4,weight=1) + + # Label(f,text='The total number of images captured will depend on the').grid(row=0,column=0,sticky='W') + # Label(f,text='interval between frames and total recording length.').grid(row=1,column=0,sticky='W') + + # self.DelayLabel = Label(f,text='Delay between Frames: ') + # self.DelayLabel.grid(row=2,column=0,sticky='W') + + # self.TotalTimeLabel = Label(f,text='Total Recording Time: ', width=15) + # self.TotalTimeLabel.grid(row=3,column=0,sticky='W') + + #------------------------Pathways------------------------ + # f = ttk.LabelFrame(self,text='Pathways',padding=(5,5,5,5)) + # f.grid(row=1,column=0,columnspan=4,sticky='NEWS',pady=5) + # f.columnconfigure(2,weight=1) + # f.columnconfigure(4,weight=1) + + # Label(f,text='Please backup any frames required for future use from the designated output').grid(row=0,column=0,sticky='E') + # Label(f,text='folder because the \'Frammes dir\' will be wiped when a new timelapse is initiated.').grid(row=1,column=0,sticky='E') + + # self.FramesPathway = Label(f,text='Frames dir:', width=15) + # self.FramesPathway.grid(row=2,column=0,sticky='W') + + # self.VideoPathway = Label(f,text='Frames dir:', width=15) + # self.VideoPathway.grid(row=2,column=0,sticky='W') + + # self.LowLightCaptureButton.grid(row=2,column=0,sticky='W') + + + # #------------------------Video output------------------------ + # f = ttk.LabelFrame(self,text='Video Output',padding=(5,5,5,5)) + # f.grid(row=1,column=0,columnspan=4,sticky='NEWS',pady=5) + # f.columnconfigure(2,weight=1) + # f.columnconfigure(4,weight=1) + + # Label(f,text='By default the video will be saved as an .mp4 file. Please ensure there is not').grid(row=0,column=0,sticky='E') + # Label(f,text='another .mp4 file with the same desired name in the chosen output folder.').grid(row=1,column=0,sticky='E') + + # self.FramesPathway = Label(f,text='Frames dir:', width=15,) + # self.FramesPathway.grid(row=2,column=0,sticky='W') + + # self.VideoPathway = Label(f,text='Video Output dir:', width=15) + # self.VideoPathway.grid(row=3,column=0,sticky='W') + + # self.VideoPathway = Label(f,text='Video fps:', width=15) + # self.VideoPathway.grid(row=4,column=0,sticky='W') + + + # #----------------------Start Timelapse----------------------- + # f = ttk.LabelFrame(self,text='Initiate Timelapse!',padding=(5,5,5,5)) + # f.grid(row=1,column=0,columnspan=4,sticky='NEWS',pady=5) + # f.columnconfigure(2,weight=1) + # f.columnconfigure(4,weight=1) + + # Label(f,text='Please check your settings and then press to begin!').grid(row=0,column=0,sticky='W') + + # self.StartTimelapseButton = Button(f,text='Start Timelapse!', width=15, + # command=self.StartTimelapse) + # self.StartTimelapseButton.grid(row=2,column=0,sticky='W') + + + + #------------------------Functions------------------------ def CaptureLowLight ( self ): self.camera.capture('foo.jpg') pass def StartDelayCapture ( self ): pass - #### TODO: Implement Reset NEEDS LOTS OF WORK!! def Reset ( self ): pass -''' - What controls are needed for this page? - Photo captures: - Type of Time lapse - Burst - Timed - etc - Whether the image settings stay the same for each picture - Checkbox - Whether the Video port is used or not (faster) - Checkbox - Filename (Textbox entry) - Start - Immediately - Delay XXX YYY SEC, MIN HR - On a specific date/time - Delay between each shot.... or at a specific time each day, etc.... - e.g., Every XX YYY where XX is a number YYY is SEC, MIN, HR, DAY - e.g., On every MIN, 1/2 HR, HOUR - e.g., Every Day at XX:XX Time - When does the capture end - After XXX shots XXX from 1 to 1000? - After XXX minutes, Hours, Days - On XXXX date - Append a number or a date/time to 'Filename' - or both - Use Drop down ComboBox - e.g. Bill_1.jpg, Bill_2.jpg, ... etc - or Bill_Date_Time.jpg, Bill_Date_Time.jpg, ... etc - or both Bill_Date_Time_1.jpg, Bill_Date_Time_2.jpg, ... etc - What about video captures? -''' - - -''' - Examples from the picamera documentation - https://picamera.readthedocs.io/en/release-1.13/recipes1.html - -The following script provides a brief example of configuring these settings: - -from time import sleep -from picamera import PiCamera - -camera = PiCamera(resolution=(1280, 720), framerate=30) -# Set ISO to the desired value -camera.iso = 100 -# Wait for the automatic gain control to settle -sleep(2) -# Now fix the values -camera.shutter_speed = camera.exposure_speed -camera.exposure_mode = 'off' -g = camera.awb_gains -camera.awb_mode = 'off' -camera.awb_gains = g -# Finally, take several photos with the fixed settings -camera.capture_sequence(['image%02d.jpg' % i for i in range(10)]) - -from time import sleep -from picamera import PiCamera - -camera = PiCamera() -camera.start_preview() -sleep(2) -for filename in camera.capture_continuous('img{counter:03d}.jpg'): - print('Captured %s' % filename) - sleep(300) # wait 5 minutes -''' - -''' -from time import sleep -from picamera import PiCamera -from datetime import datetime, timedelta - -def wait(): - # Calculate the delay to the start of the next hour - next_hour = (datetime.now() + timedelta(hour=1)).replace( - minute=0, second=0, microsecond=0) - delay = (next_hour - datetime.now()).seconds - sleep(delay) - -camera = PiCamera() -camera.start_preview() -wait() -for filename in camera.capture_continuous('img{timestamp:%Y-%m-%d-%H-%M}.jpg'): - print('Captured %s' % filename) - wait() - - -3.7. Capturing in low light -Using similar tricks to those in Capturing consistent images, the Pi’s -camera can capture images in low light conditions. The primary objective -is to set a high gain, and a long exposure time to allow the camera to -gather as much light as possible. However, the shutter_speed attribute -is constrained by the camera’s framerate so the first thing we need to -do is set a very slow framerate. The following script captures an image -with a 6 second exposure time (the maximum the Pi’s V1 camera module is -capable of; the V2 camera module can manage 10 second exposures): - -from picamera import PiCamera -from time import sleep -from fractions import Fraction - -# Force sensor mode 3 (the long exposure mode), set -# the framerate to 1/6fps, the shutter speed to 6s, -# and ISO to 800 (for maximum gain) -camera = PiCamera( - resolution=(1280, 720), - framerate=Fraction(1, 6), - sensor_mode=3) -camera.shutter_speed = 6000000 -camera.iso = 800 -# Give the camera a good long time to set gains and -# measure AWB (you may wish to use fixed AWB instead) -sleep(30) -camera.exposure_mode = 'off' -# Finally, capture an image with a 6s exposure. Due -# to mode switching on the still port, this will take -# longer than 6 seconds -camera.capture('dark.jpg') -''' + def StartTimelapse ( self ): + pass \ No newline at end of file diff --git a/Source/tests/cursor example.py b/Source/tests/cursor example.py new file mode 100644 index 0000000..d45ca96 --- /dev/null +++ b/Source/tests/cursor example.py @@ -0,0 +1,12 @@ +from tkinter import * +import tkinter + +top = tkinter.Tk() + +B1 = tkinter.Button(top, text ="circle", relief=RAISED,\ + cursor="circle") +B2 = tkinter.Button(top, text ="plus", relief=RAISED,\ + cursor="plus") +B1.pack() +B2.pack() +top.mainloop() \ No newline at end of file