├── .gitignore ├── README.md ├── Source ├── AboutDialog.py ├── AnnotationOverlay.py ├── Assets │ ├── ColorPicker1.png │ ├── PiCamera.png │ ├── RPI-symbol.png │ ├── Save.png │ ├── Tooltips.txt │ ├── camera-icon.png │ ├── cancel.png │ ├── cancel_22x22.png │ ├── computer-monitor.png │ ├── files.png │ ├── flip.png │ ├── folders.png │ ├── gpl.txt │ ├── help.png │ ├── keyboard.gif │ ├── ok.png │ ├── ok_22x22.png │ ├── prefs1_16x16.png │ ├── prefs_16x16.png │ ├── reset.png │ ├── rotate.png │ ├── video-icon-b.png │ ├── web_16x16.png │ ├── web_22x22.png │ └── window-close.png ├── BasicControls.py ├── CameraUtils.py ├── ConfigFile.py ├── CreateScript.py ├── Dialog.py ├── Exposure.py ├── FinerControl.py ├── ImageEffects.py ├── KeyboardShortcuts.py ├── Mapping.py ├── NotePage.py ├── PhotoParams.py ├── PiCameraApp.py ├── PreferencesDialog.py ├── Timelapse.py ├── Tooltip.py ├── Utils.py └── VideoParams.py ├── _config.yml └── gpl.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # My local stuff 2 | Temp/ 3 | Assets/Pi.gif 4 | Assets/video.jpeg 5 | Assets/ok.png 6 | Assets/web_22x22.png 7 | Source/__TMP__.mp4 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *,cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Synopsis 2 | 3 | PiCameraApp: A graphical user interface (GUI) for the Picamera library written in Python using Tkinter / ttk. 4 | 5 | ## Motivation 6 | 7 | While developing a camera interface to a 32x32 RGB matrix, I was constantly programming the Picamera in code to test options. I decided to develop a GUI that provides an interface to all of the Picamera's API. Since I haven't done much GUI programming in Linux, I used the Tkinter API. 8 | 9 | Note: I am an old (old, old, old, ..., so very old) Windows programmer going back to the days of Windows 2.1 (Petzold). Both the Python language as well as Linux on the Raspberry Pi are new to me, so please forgive unintentional (or blatant) misuses of the API or Python coding 'standards'. 10 | 11 | ## Version History 12 | 13 | | Version | Notes | 14 | | :--------- | :----------------------------------------------------- | 15 | | 0.1 | | 16 | | 0.2 | | 17 | | | | 18 | 19 |
  • 20 | 21 | ![mainscreen0 2](https://user-images.githubusercontent.com/3778024/36648609-43091bc0-1a5b-11e8-97c8-be0db1249a32.png) 22 | 23 | ## Installation 24 | 25 | Download the zip file and extract to a directory of your choosing. To run, open a terminal, change to the directory containing the source files, and enter **sudo python PiCameraApp.py** or **sudo python3 PiCameraApp.py**. 26 | 27 | ## Known Issues 28 | 29 | | Issue | Description / Workaround | 30 | | :--------- | :----------------------------------------------------- | 31 | | LED | The led_pin parameter can be used to specify the GPIO pin which should be used to control the camera’s LED via the led attribute. If this is not specified, it should default to the correct value for your Pi platform. At present, the camera’s LED cannot be controlled on the Pi 3 (the GPIOs used to control the camera LED were re-routed to GPIO expander on the Pi 3). There are circumstances in which the camera firmware may override an existing LED setting. For example, in the case that the firmware resets the camera (as can happen with a CSI-2 timeout), the LED may also be reset. If you wish to guarantee that the LED remain off at all times, you may prefer to use the disable_camera_led option in config.txt (this has the added advantage that sudo privileges and GPIO access are not required, at least for LED control). Thanks https://picamera.readthedocs.io/en/release-1.13/| 32 | | Sensor Mode | Use this with discretion. In any mode other than Mode 0 (Auto), I've experienced sudden 'freezes' of the application forcing a complete reboot. | 33 | | framerate_range and H264 video | The App would raise an exception when attempting to cature H264 video when framerate_range was selected. The exception complained the framerate_delta could not be specified with framerate_range??? Until I resolve this bug, I don't allow capturing H264 videos with framerate_range selected. | 34 | | framerate and framerate_delta error checking | There are cases where the code may not catch an exception. Avoid setting framerate and framerate_delta values that could add to numbers less than or equal to zero. A future update will fix this issue. 35 | | JPEG image parameters | The JPEG image capture parameter 'Restart' is not supported with this release. | 36 | | H264 video parameters | The H264 video capture parameter 'Intra Period' is not supported with this release. | 37 | | Other video paramaters | 'bitrate' and 'quality' are not supported in this release. | 38 | | Image Effects parameters | The Image Effect parameters for 'colorbalance', 'film', 'solarize', and 'film' are not supported with this release. | 39 | | EXIF data display | The python exif module does not support all EXIF metadata. Find a better solution. | 40 | | Image flip buttons | The two image flip buttons on the bottom image pane are disabled. These are meant to 'flip' the PIL image that is displayed. To flip or rotate the camera image, use the buttons on the top preview pane. | 41 | | | | 42 | 43 | ## TODO List (future enhancements) 44 | 45 | | TODO | Description | 46 | | :--------- | :----------------------------------------------------- | 47 | | Save Camera State | Allow the user to save and restore the current camera programming state. | 48 | | Output Samples | Allow the user to generate a simple Python script that will program the camera and take a still image or video. | 49 | | INI File | Have a configuration file that saves / restores Preferences | 50 | | Time Delay | Support programming the camera to take still (or videos of length **time**), starting **start time**, then every **time** sec, delaying **time** sec until **number** or **end time** is reached. | 51 | | GPIO Support | Better suport the LED GPIO - this is still buggy (or not fully understood). Also, allow the user to specify GPIO pin(s) that can be toggled (or held high or low) while a still image or video capture is in progress. | 52 | | Better error checking | Reorgainze code | 53 | | | | 54 | 55 | ## API Reference 56 | 57 | PiCameraApp has been developed using Python ver 2.7.13 and Python ver 3.5.3. In addition, it uses the following additonal Python libraries. See the PiCameraApp About dialog for exact versions used. 58 | 59 | | Library | Usage | 60 | | :--------- | :-------------------------------------------------- | 61 | | picamera | The python interface to the PiCamera hardware. See https://picamera.readthedocs.io/en/release-1.13/install.html | 62 | | RPi.GPIO | Required to toggle the LED on the camera. Can get it at http://www.raspberrypi-spy.co.uk/2012/05/install-rpi-gpio-python-library/ | 63 | | PIL / Pillow | The Pillow fork of the Python Image Library. One issue is with PIL ImageTk under Python 3.x. It was not installed on my RPI. If you have similar PIL Import Errors use: **sudo apt-get install python3-pil.imagetk**. | 64 | | | | 65 | 66 | ![about](https://user-images.githubusercontent.com/3778024/36648694-71283a1c-1a5c-11e8-9c85-ec1f07218cca.png) 67 | 68 | ## License 69 | 70 | 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 3 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 71 | 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, see http://www.gnu.org/licenses/. 72 | -------------------------------------------------------------------------------- /Source/AboutDialog.py: -------------------------------------------------------------------------------- 1 | # -*- coding: iso-8859-15 -*- 2 | ''' 3 | AboutDialog.py 4 | Copyright (C) 2015 - Bill Williams 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | ''' 16 | from platform import * 17 | try: 18 | from Tkinter import * 19 | except ImportError: 20 | from tkinter import * 21 | try: 22 | import ttk 23 | from ttk import * 24 | except ImportError: 25 | from tkinter import ttk 26 | 27 | from Dialog import * 28 | from Utils import * 29 | from Mapping import * 30 | 31 | import PIL 32 | from PIL import Image, ImageTk, ExifTags 33 | 34 | NoRequire = False 35 | try: 36 | from pkg_resources import require 37 | except ImportError: 38 | print ( "Cannot import 'require' from 'pkg_resources'" ) 39 | NoRequire = True 40 | 41 | from NotePage import BasicNotepage 42 | 43 | # 44 | # General About Dialog. 45 | # 46 | class AboutDialog ( Dialog ): 47 | def BuildDialog ( self ): 48 | #self.MainFrame.columnconfigure(0,weight=1) 49 | #self.MainFrame.rowconfigure(1,weight=1) 50 | 51 | image = PIL.Image.open('Assets/PiCamera.png') 52 | image = image.resize((50,106), Image.ANTIALIAS) 53 | photo = ImageTk.PhotoImage(image) 54 | img = Label(self.MainFrame,image=photo) 55 | img.image = photo 56 | img.grid(row=0,column=0,sticky='W') 57 | 58 | l4 = Label(self.MainFrame,text='PiCamera ver 0.2 ', 59 | font=('Helvetica',20,'bold italic'), \ 60 | foreground='blue') #,anchor='center') 61 | l4.grid(row=0,column=1,sticky='W') #'EW') 62 | 63 | n = Notebook(self.MainFrame,padding=(5,5,5,5)) 64 | n.grid(row=1,column=0,columnspan=2,sticky='NSEW') 65 | n.columnconfigure(0,weight=1) 66 | n.rowconfigure(0,weight=1) 67 | 68 | AboutPage = About(n, camera=self._camera) 69 | LicensePage = License(n) 70 | CreditsPage = Credits(n) 71 | 72 | n.add(AboutPage,text='About',underline=0) 73 | n.add(LicensePage,text='License',underline=0) 74 | n.add(CreditsPage,text='Credits',underline=0) 75 | 76 | # Handle PiCameraApp About 77 | class About ( BasicNotepage ): 78 | def BuildPage ( self ): 79 | Label(self,text='PiCamera Application', 80 | anchor='center',font=('Helvetica',14),foreground='blue') \ 81 | .grid(row=0,column=0,) 82 | 83 | Label(self,text='Copyright (C) 2015 - 2018', 84 | anchor='center',font=('Helvetica',12)).grid(row=1,column=0,) 85 | Label(self,text='Bill Williams (github.com/Billwilliams1952/)', 86 | anchor='center',font=('Helvetica',12)).grid(row=2,column=0,) 87 | 88 | Separator(self,orient='horizontal').grid(row=3,column=0, 89 | columnspan=2,sticky='NSEW',pady=10) 90 | 91 | rev = self.camera.revision 92 | if rev == "ov5647": camType = "V1" 93 | elif rev == "imx219" : camType = "V2" 94 | else: camType = "Unknown" 95 | Label(self,text="Camera revision: " + rev + " (" + camType + \ 96 | " module)",font=('Helvetica',14)).grid(row=4,column=0,sticky='NSEW') 97 | 98 | # Only on PI for PiCamera! 99 | txt = linux_distribution() 100 | if txt[0]: 101 | os = 'Linux OS: %s %s' % ( txt[0].title(), txt[1] ) 102 | else: 103 | os = 'Unknown Linux OS' 104 | Label(self,text=os).grid(row=5,column=0,sticky='NSEW') 105 | 106 | l = Label(self,text='Python version: %s' % python_version()) 107 | l.grid(row=6,column=0,sticky='NSEW') 108 | 109 | if NoRequire: 110 | PiVer = "Picamera library version unknown" 111 | PILVer = "Pillow (PIL) library version unknown" 112 | RPIVer = "GPIO library version unknown" 113 | else: 114 | PiVer = "PiCamera library version %s" % require('picamera')[0].version 115 | PILVer = "Pillow (PIL) library version %s" % require('Pillow')[0].version 116 | RPIVer = "GPIO library version %s" % require('rpi.gpio')[0].version 117 | 118 | Label(self,text=PiVer).grid(row=7,column=0,sticky='NSEW') 119 | Label(self,text=PILVer).grid(row=8,column=0,sticky='NSEW') 120 | Label(self,text=RPIVer).grid(row=9,column=0,sticky='NSEW') 121 | s = processor() 122 | if s: 123 | txt = 'Processor type: %s (%s)' % (processor(), machine()) 124 | else: 125 | txt = 'Processor type: %s' % machine() 126 | Label(self,text=txt).grid(row=10,column=0,sticky='NSEW') 127 | Label(self,text='Platform: %s' % platform()).grid(row=11, \ 128 | column=0,sticky='NSEW') 129 | 130 | # Handle GPL License 131 | class License ( BasicNotepage ): 132 | def BuildPage ( self ): 133 | self.sb = Scrollbar(self,orient='vertical') 134 | self.sb.grid(row=0,column=1,sticky='NEWS') 135 | self.text = Text(self,height=15,width=50,wrap='word', 136 | yscrollcommand=self.sb.set) 137 | self.text.grid(row=0,column=0,sticky='NEWS') 138 | self.text.bind("",lambda e : "break") # ignore all keypress 139 | # Note: return "break" from event handler to ignore 140 | self.sb.config(command=self.text.yview) 141 | try: 142 | with open('Assets/gpl.txt') as f: self.text.insert(END,f.read()) 143 | except IOError: 144 | self.text.insert(END,"\n\n\n\t\tError reading file 'Assets/gpl.txt'") 145 | 146 | # Handle Credits 147 | class Credits ( BasicNotepage ): 148 | def BuildPage ( self ): 149 | f = MyLabelFrame(self,'Thanks To',0,0) 150 | string = \ 151 | "Tooltip implementation courtesy of:\n" \ 152 | " code.activestate.com/recipes/576688-tooltip-for-tkinter/\n" \ 153 | "Tooltip information courtesy of:\n" \ 154 | " picamera.readthedocs.io/en/release-1.13/api_camera.html\n" \ 155 | "Various free icons courtesy of:\n" \ 156 | " iconfinder.com/icons/ and icons8.com/icon/\n" \ 157 | "" 158 | Label(f,text=string,style='DataLabel.TLabel').grid(row=0,column=0,sticky='NSEW') 159 | -------------------------------------------------------------------------------- /Source/AnnotationOverlay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # AnnotationOverlay.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | try: 25 | from Tkinter import * 26 | except ImportError: 27 | from tkinter import * 28 | try: 29 | from tkColorChooser import askcolor 30 | except ImportError: 31 | from tkinter.colorchooser import askcolor 32 | try: 33 | import tkFileDialog as FileDialog 34 | except ImportError: 35 | import tkinter.filedialog as FileDialog 36 | try: 37 | import tkMessageBox as MessageBox 38 | except ImportError: 39 | import tkinter.messagebox as MessageBox 40 | try: 41 | import ttk 42 | from ttk import * 43 | except ImportError: 44 | from tkinter import ttk 45 | #from ttk import * 46 | try: 47 | import tkFont as Font 48 | except ImportError: 49 | import tkinter.font as Font 50 | 51 | import datetime as dt 52 | from Dialog import * 53 | from Mapping import * 54 | from NotePage import * 55 | from Utils import * 56 | from Tooltip import * 57 | from NotePage import BasicNotepage 58 | from PreferencesDialog import * 59 | 60 | try: 61 | import picamera 62 | from picamera import * 63 | import picamera.array 64 | except ImportError: 65 | raise ImportError("You do not seem to have picamera installed") 66 | 67 | class AnnotationOverlayDialog ( Dialog ): 68 | def BuildDialog ( self ): 69 | n = Notebook(self.MainFrame,padding=(5,5,5,5)) 70 | n.grid(row=0,column=0,sticky='NSEW') 71 | n.columnconfigure(0,weight=1) 72 | n.rowconfigure(0,weight=1) 73 | 74 | self.Annotation = AnnotationPage(n,camera=self._camera,cancel=self.CancelButton) 75 | self.Overlay = OverlayPage(n,camera=self._camera,cancel=self.CancelButton) 76 | #self.EXIF = EXIFPage(n,camera=self._camera,cancel=self.CancelButton) 77 | 78 | n.add(self.Annotation,text='Annotation',underline=0) 79 | n.add(self.Overlay,text='Overlay',underline=0) 80 | #n.add(self.EXIF,text='EXIF',underline=0) 81 | 82 | def OkPressed ( self ): 83 | self.Annotation.SaveChanges() 84 | self.Overlay.SaveChanges() 85 | return True 86 | 87 | def CancelPressed ( self ): 88 | return tkMessageBox.askyesno("Annotation/Overlay","Exit without saving changes?") 89 | 90 | class AnnotationPage ( BasicNotepage ): 91 | UseText = False 92 | Text = 'Text' 93 | Timestamp = False 94 | FrameNum = False 95 | Textsize = 32 96 | ColorYValue = 1.0 97 | UseForeColor = False 98 | ForeColor = picamera.Color('white') 99 | UseBackColor = False 100 | BackColor = None 101 | @staticmethod 102 | # Called if Reset Camera is clicked 103 | def Reset (): 104 | UseText = False 105 | Text = 'Text' 106 | Timestamp = False 107 | FrameNum = False 108 | Textsize = 32 109 | ColorYValue = 1.0 110 | UseForeColor = False 111 | ForeColor = picamera.Color('white') 112 | UseBackColor = False 113 | BackColor = None 114 | def BuildPage ( self ): 115 | f = MyLabelFrame(self,'Annotation text',0,0) 116 | self.EnableAnnotateText = MyBooleanVar(AnnotationPage.UseText) 117 | self.NoAnnotateTextRadio = MyRadio(f,'None (Default)',False,self.EnableAnnotateText, 118 | self.AnnotationTextRadio,0,0,'W',tip=400) 119 | MyRadio(f,'Text:',True,self.EnableAnnotateText,self.AnnotationTextRadio, 120 | 0,1,'W',tip=401) 121 | 122 | self.AddTimestamp = MyBooleanVar(AnnotationPage.Timestamp) 123 | self.AddTimestampButton = ttk.Checkbutton(f,text='Add Timestamp to overlay', 124 | variable=self.AddTimestamp,command=self.AddTimestampButtonChecked) 125 | self.AddTimestampButton.grid(row=2,column=0,padx=5,columnspan=3,sticky='W') 126 | ToolTip(self.AddTimestampButton, msg=402) 127 | 128 | okCmd = (self.register(self.ValidateAnnotationText),'%P') 129 | self.AnnotateTextEntry = Entry(f,width=30,validate='all',validatecommand=okCmd) 130 | self.AnnotateTextEntry.insert(0, AnnotationPage.Text) 131 | self.AnnotateTextEntry.grid(row=0,column=2,sticky='W') 132 | ToolTip(self.AnnotateTextEntry, msg=403) 133 | 134 | self.AnnotateFrame = MyBooleanVar(AnnotationPage.FrameNum) 135 | self.AnnotateFrameButton = ttk.Checkbutton(f,text='Add Frame Number to overlay', 136 | variable=self.AnnotateFrame,command=self.AnnotateFrameButtonChecked) 137 | self.AnnotateFrameButton.grid(row=1,column=0,columnspan=3,sticky='W',padx=5) 138 | ToolTip(self.AnnotateFrameButton,msg=411) 139 | 140 | f1 = ttk.Frame(f) 141 | f1.grid(row=3,column=0,columnspan=3,sticky='W') 142 | Label(f1,text='Text size:').grid(row=0,column=0,sticky='W',padx=5,pady=3) 143 | self.Size = ttk.Label(f1,text='%d' % AnnotationPage.Textsize, 144 | style='DataLabel.TLabel') 145 | self.Size.grid(row=0,column=2,sticky='W',padx=5) 146 | self.AnnotateTextSize = ttk.Scale(f1,from_=6,to=160,length=175, 147 | command=self.AnnotateTextSizeChanged,orient='horizontal') 148 | self.AnnotateTextSize.grid(row=0,column=1,sticky='W') 149 | self.AnnotateTextSize.set(AnnotationPage.Textsize) 150 | ToolTip(self.AnnotateTextSize, msg=404) 151 | 152 | f = MyLabelFrame(self,'Foreground / background colors',2,0)#,'NEWS',5) 153 | b = MyBooleanVar(AnnotationPage.UseBackColor) 154 | self.NoBackColorRadio = MyRadio(f,'Background (None default)',False,b, 155 | self.AnnotationBackgroundColor,1,0,'W',tip=405) 156 | MyRadio(f,'Set:',True,b,self.AnnotationBackgroundColor,1,1,'W',tip=406) 157 | image = PIL.Image.open('Assets/ColorPicker1.png') 158 | self.colorimage = ImageTk.PhotoImage(image.resize((16,16),Image.ANTIALIAS)) 159 | self.chooseBackColor = ttk.Button(f,text='Color',image=self.colorimage, 160 | command=self.ChooseBackcolorClick,width=7,padding=(5,5,5,5)) 161 | self.chooseBackColor.grid(row=1,column=2,sticky='W') 162 | ToolTip(self.chooseBackColor,407) 163 | 164 | b = MyBooleanVar(AnnotationPage.UseForeColor) 165 | self.WhiteDefaultRadio = MyRadio(f,'Foreground (White default)',False,b, 166 | self.AnnotationForegroundColor,0,0,'W',tip=408) 167 | MyRadio(f,'Set:',True,b,self.AnnotationForegroundColor,0,1,'W',tip=409) 168 | self.Ylabel = ttk.Label(f,text='Y: %f' % AnnotationPage.ColorYValue, 169 | style='DataLabel.TLabel') 170 | self.Ylabel.grid(row=0,column=3,sticky='W',padx=5) 171 | self.Ycolor = ttk.Scale(f,from_=0.0,to=1.0,orient='horizontal', 172 | command=self.YValueChanged) 173 | self.Ycolor.grid(row=0,column=2,sticky='W',pady=5) 174 | self.Ycolor.set(AnnotationPage.ColorYValue) 175 | ToolTip(self.Ycolor,410) 176 | 177 | self.AnnotationBackgroundColor(False) 178 | self.AnnotationForegroundColor(False) 179 | self.BackColor = picamera.Color('Black') 180 | def AnnotationTextRadio ( self, EnableAddText ): 181 | if EnableAddText: 182 | self.AnnotateTextEntry.config(state='normal') 183 | self.AnnotateTextEntry.focus_set() 184 | state = '!disabled' 185 | self.camera.annotate_text = self.AnnotateTextEntry.get() 186 | else: 187 | state = 'disabled' 188 | self.camera.annotate_text = '' 189 | self.AnnotateTextEntry.config(state=state) 190 | AnnotationPage.UseText = EnableAddText 191 | def ValidateAnnotationText ( self, TextToAdd ): 192 | # A Hack because I want to continuously update the timestamp 193 | # if displayed 194 | if self.EnableAnnotateText.get() is False: TextToAdd = "" 195 | AnnotationPage.Text = TextToAdd 196 | if self.AddTimestamp.get(): 197 | try: # in case a formatting error... 198 | t = dt.datetime.now().strftime(PreferencesDialog.DefaultTimestampFormat) 199 | except: 200 | t = "Bad format" # Should never get here... 201 | if TextToAdd == "": TextToAdd = t 202 | else: TextToAdd = TextToAdd + " (" + t + ")" 203 | self.camera.annotate_text = TextToAdd 204 | return True 205 | def AnnotateFrameButtonChecked ( self ): 206 | self.camera.annotate_frame_num = self.AnnotateFrame.get() 207 | AnnotationPage.AnnotateFrameNum = self.AnnotateFrame.get() 208 | self.ValidateAnnotationText(self.AnnotateTextEntry.get()) 209 | AnnotationPage.FrameNum = self.AnnotateFrame.get() 210 | def AddTimestampButtonChecked ( self ): 211 | AnnotationPage.Timestamp = self.AddTimestamp.get() 212 | self.ValidateAnnotationText(self.AnnotateTextEntry.get()) 213 | if self.AddTimestamp.get(): 214 | self.after(1000,self.AddTimestampButtonChecked) 215 | def AnnotationBackgroundColor ( self, AddColor ): 216 | if AddColor: 217 | self.camera.annotate_background = self.BackColor 218 | self.chooseBackColor.config(state='!disabled') 219 | self.chooseBackColor.focus_set() 220 | else: 221 | self.camera.annotate_background = None 222 | self.chooseBackColor.config(state='disabled') 223 | def ChooseBackcolorClick ( self ): 224 | # pass a String for the color - not a Value! 225 | result = askcolor(parent=self,color=str(self.BackColor), 226 | title='Annotation Background color') 227 | # [0] is (R,G,B) tuple, [1] is hex value of color 228 | if result[0] == None: return # Cancel 229 | self.BackColor = picamera.Color(result[1]) 230 | self.camera.annotate_background = picamera.Color(result[1]) 231 | def AnnotationForegroundColor ( self, AddColor ): 232 | if AddColor: 233 | self.camera.annotate_foreground = \ 234 | picamera.Color(y=float(self.Ycolor.get()), u=0, v=0) 235 | self.Ycolor.state(['!disabled']) 236 | self.Ycolor.focus_set() 237 | else: 238 | self.Ycolor.state(['disabled']) 239 | self.camera.annotate_foreground = picamera.Color('white') 240 | def YValueChanged ( self, val ): 241 | self.camera.annotate_foreground = picamera.Color(y=float(val), u=0, v=0) 242 | AnnotationPage.YValue = float(val) 243 | self.Ylabel.config(text='Y: %.2f' % float(val)) 244 | def AnnotateTextSizeChanged ( self, newVal ): 245 | self.camera.annotate_text_size = int(float(newVal)) 246 | self.Size.config(text='%d' % int(float(newVal))) 247 | AnnotationPage.Textsize = int(float(newVal)) 248 | self.AnnotateTextSize.focus_set() 249 | def SaveChanges ( self ): 250 | pass 251 | 252 | class OverlayPage ( BasicNotepage ): 253 | @staticmethod 254 | # Called if Reset Camera is clicked 255 | def Reset (): 256 | pass 257 | def BuildPage ( self ): 258 | Label(self,text="NOTHING HERE YET!!").grid(row=0,column=0,sticky='W'); 259 | def SaveChanges ( self ): 260 | pass 261 | -------------------------------------------------------------------------------- /Source/Assets/ColorPicker1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/ColorPicker1.png -------------------------------------------------------------------------------- /Source/Assets/PiCamera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/PiCamera.png -------------------------------------------------------------------------------- /Source/Assets/RPI-symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/RPI-symbol.png -------------------------------------------------------------------------------- /Source/Assets/Save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/Save.png -------------------------------------------------------------------------------- /Source/Assets/Tooltips.txt: -------------------------------------------------------------------------------- 1 | 2 | # This is a Tooltips text file. Feel free to edit this fle to personalize 3 | # your tooltips for particular controls. 4 | 5 | # Copyright 2018 Bill Williams 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | 15 | # Format is 16 | # Tooltip ID number - DO NOT CHANGE! : Tooltip text to display $ 17 | # 18 | # Blank lines are ignored, and any line starting with a '#' is ignored. 19 | # All whitespace before and after the Tooltip ID Number and text is removed before processing. 20 | # Text will continue to be appended until the line ends with a dollar sign. 21 | # The '(C)' text is replaced with: 22 | # 'Thanks to: picamera.readthedocs.io/en/release-1.13/api_camera.html' 23 | #e.g., 24 | # 35 : This is some tooltip text. $ 25 | # 36 : This text here spans three lines. The amount of text is only limited by python 26 | # and could be huge. This can go on until a the dollar sign is 27 | # found. $ 28 | # 36 : The next tooltip. Use a newline \n to force a newline in the displayed text. $ 29 | 30 | # ----------------------- TODOs ------------------------ 31 | # ADD: An optional aspect number to control the size of the tooltip 32 | # ADD: Common text. Embed in text by 33 | # e.g. 34 | # 300 : This is some text.\n\nNow add common text here. <25000> $ 35 | # 25000 : This is special text that may be referenced multiple times. $ 36 | 37 | # ------------- Main Window Controls ------------- 38 | 39 | 1 : Enable/disable preview image. $ 40 | 2 : Set alpha for preview window. $ 41 | 3 : Flip camera image vertically. $ 42 | 4 : Flip camera image horizontally. $ 43 | 5 : Toggle between showing preview on screen or in the PiCamera preview window. $ 44 | 6 : Adjust size of preview image on screen. $ 45 | 7 : Scroll image vertically. $ 46 | 8 : Scroll image horizontally. $ 47 | 9 : Take picture using camera settings (Ctrl+P). $ 48 | 10 : Start/Stop video capture using camera settings (Ctrl+V). $ 49 | 11 : Clear displayed picture (Ctrl+C). $ 50 | 12 : Flip displayed pictured horizontally.\n\n IN WORK $ 51 | 13 : Flip displayed picture vertically.\n\n IN WORK $ 52 | 14 : Rotate camera image. Valid values are 0, 90, 180, 270. $ 53 | 54 | #-------------- Status Bar ------------------ 55 | 40: Current X/Y location of mouse on image. $ 56 | 41: Current Analog Gain (AG) and Digital Gain (DG) from Exposure Mode settings. $ 57 | 42: Current Red Gain (RG) and Blue Gain (BG) from Auto White Balance settings. $ 58 | 43: Currently programmed Exposure Setting (ES) set under Shutter Speed. $ 59 | 44: Currently programmed Frame rate (FPS). $ 60 | 45: General messages / status. $ 61 | 62 | # -------------- OK / Cancel in Dialogs -------------- 63 | 50 : Close the window. If changes were made, then save the changes. $ 64 | 51 : Close the windows without saving any changes. $ 65 | 52 : Display specific help for this window. $ 66 | 67 | # ------------- Basic Controls Tab ---------------- 68 | 69 | # Select port for image capture 70 | 100 : Use Camera Still Port to capture images. This port is slower but 71 | produces better quality pictures. $ 72 | 101 : Use Video Port to capture images. Allows rapid image captures up to the 73 | rate of video frames. The tradeoff is that the image quality is not as good.\n\n 74 | Note: Exif metadata will not be included in JPEG output. This is due to an 75 | underlying firmware limitation. $ 76 | 102 : Turn On/Off the LED via GPIO.\n\n 77 | If a GPIO library is available (only RPi.GPIO is currently supported), 78 | and if the python process has the necessary privileges (typically this 79 | means running as root via sudo), this control can be used to set the 80 | state of the camera's LED.\n\nAt present, the camera's LED cannot be controlled 81 | on the Pi 3 (the GPIOs used to control the camera LED were re-routed to GPIO 82 | expander on the Pi 3). (C) $ 83 | 103 : Enable/disable removeal of video noise by applying a denoise algorithm to 84 | video recordings. $ 85 | 104 : Enable/disable removeal of image noise by applying a denoise algorithm to 86 | image captures. $ 87 | 105 : Enable/disable video stabilzation $ 88 | 89 | # Picture / Video Capture 90 | 120 : Enable selection of the image/video image size from the drop down list.\n\n 91 | This attribute, in combination with framerate, determines the mode that the 92 | camera operates in. The actual sensor framerate and resolution used by the camera 93 | is influenced, but not directly set, by the selection. (C) $ 94 | 121 : List of standard image/video sizes. $ 95 | 122 : Enable selection of a non-standard image/video size using the two drop down lists. $ 96 | 123 : List of new widths for the image/video.\n\nThe size is set in 32 byte boundaries. $ 97 | 124 : List of new heights for the image/video.\n\nThe size is set in 16 byte boundaries. $ 98 | 125 : Programmed width of image/video in pixels. $ 99 | 126 : Programmed height of image/video in pixels. $ 100 | 101 | # Zoom region before 102 | 130 : Adjust the new inital X point for the preview image zoom. $ 103 | 131 : Adjust the new inital Y point for the preview image zoom. $ 104 | 132 : Adjust the new width of the zoomed area of the preview image. $ 105 | 133 : Adjust the new height of the zoomed area of the preview image. $ 106 | 134 : Reset back to full size on the preview image. $ 107 | 108 | # Resize after 109 | 140 : Do not resize image after capture. $ 110 | 141 : Resize the image after capture. Use drop down lists to select the new width/height.\n\n 111 | Exif metadata will not be included in JPEG output. This is due to an underlying firmware 112 | limitation. $ 113 | 142 : List of new widths for the resized image.\n\nThe size is set in 32 byte boundaries. $ 114 | 143 : List of new heights for the resized image.\n\nThe size is set in 16 byte boundaries. $ 115 | 116 | # Quick adjustments 117 | 150 : Adjust image brightness.\n\nRange 0 (dark) to 100 (full bright). $ 118 | 151 : Adjust image contrast.\n\nRange -100 to 100. $ 119 | 152 : Adjust image color saturation.\n\nRange -100 to 100. Default is 0. $ 120 | 153 : Adjust image sharpness (a measure of the amount of post-processing to 121 | reduce or increase image sharpness).\n\nRange -100 to 100 (max sharpness). Default is 0. $ 122 | 154 : Reset quick adjustments to nominal values. $ 123 | 124 | # Preprogrammed image effects 125 | 160 : No image effect applied. $ 126 | 161 : Select image effect from drop down list. $ 127 | 162 : List of available image effects to apply to the image. $ 128 | 163 : Edit effect parameters for selected effect. $ 129 | 130 | # LED / Flash Mode 131 | 180 : Flash mode is OFF. $ 132 | 181 : Flash mode is automatically set based on exposure parameters. $ 133 | 182 : Select Flash mode from the drop down list. $ 134 | 183 : List of manuualy selectable Flash modes.\n\n 135 | Note: You must define which GPIO pins the camera is to use for flash and privacy 136 | indicators. This is done within the Device Tree configuration which is considered 137 | an advanced topic. Specifically, you need to define pins FLASH_0_ENABLE and 138 | optionally FLASH_0_INDICATOR (for the privacy indicator). More information 139 | can be found in\n\n 140 | https://picamera.readthedocs.io/en/release-1.13/recipes2.html#flash-configuration $ 141 | 142 | # ------------- Exposure Tab ---------------- 143 | # Metering Mode 144 | 200 : List of available metering modes. All modes set up two regions, a 145 | center region, and an outer region. The major difference between each mode is 146 | the size of the center region.\n\nThe 'backlit' mode has the largest central region 147 | (30% of the width), while 'spot' has the smallest (10% of the width).\n\n 148 | The default value is 'average'. (C) $ 149 | 150 | # Exposure Mode 151 | 205 : Fully automated exposure mode. \n\nNOTE: Users should wait for the Analog and 152 | Digital gain values to settle before taking a picture or video. $ 153 | 206 : Preset Exposure. Select from the drop down list.\n\nNOTE: Users should wait for 154 | the Analog and Digital gain values to settle before taking a picture or video. $ 155 | 207 : Manually set ISO. Select from the drop down list.\n\nNOTE: Users should wait 156 | for the Analog and Digital gain values to settle before taking a picture or video. $ 157 | 208 : Exposure OFF - fixed analog and digital gains.\n\nNOTE: Setting to OFF after gains have 158 | settled is a good way to ensure multiple pictures have the same exposure. $ 159 | 209 : List of preset exposure types. $ 160 | 210 : List of preset ISO values.\n\nOn the V1 camera module, non-zero ISO values attempt 161 | to fix overall gain at various levels. For example, ISO 100 attempts to provide an 162 | overall gain of 1.0, ISO 200 attempts to provide overall gain of 2.0, etc. 163 | The algorithm prefers analog gain over digital gain to reduce noise.\n\n 164 | On the V2 camera module, ISO 100 attempts to produce overall gain of ~1.84, 165 | and ISO 800 attempts to produce overall gain of ~14.72 (the V2 camera module was 166 | calibrated against the ISO film speed standard).$ 167 | 211 : Current analog gain.\n\nNOTE: Users should wait for this value to settle before taking 168 | a picture or video. $ 169 | 212 : Current digital gain.\n\nNOTE: Users should wait for this value to settle before taking 170 | a picture or video. $ 171 | 213 : Actual ISO. This value is caculated by (Analog_Gain / Digital_Gain) * 100.0 $ 172 | 214 : Apparent ISO. If Exposure Mode is Auto or one of the Preset Exposures, then this 173 | value is Auto, else it is the Manually set value. $ 174 | 175 | # Exposure compensation - DRC 176 | # Add radio buttons Disable / Enable 177 | 230 : Adjust the camera's exposure compensation level.\n\n 178 | The default value is 0. Larger values result in brighter images, smaller (negative) values 179 | result in dimmer images. $ 180 | 231 : Disable exposure compensation (default). $ 181 | 232 : Enable exposure compensation. Adjust the amount using the slider. $ 182 | 233 : Disable Dynamic Range Compression (off). $ 183 | 234 : Select the strength of the dynamic range compression applied to the camera's output 184 | using the drop down list.\n\nWARNING: Enabling DRC will override fixed white balance gains 185 | (set under Auto white balance settings). $ 186 | 235 : List of available DRC strengths to apply to the camera's output. $ 187 | 188 | # Auto White Balance Settings 189 | 250 : Fully automated Auto White Balance (AWB).\n\nNOTE: Users should wait for the Red and 190 | Blue gains to settle before taking a picture or video. $ 191 | 251 : Enable selection of preset scenes for AWB from the drop down list.\n\nNOTE: Users 192 | should wait for the Red and Blue gains to settle before taking a picture or video. $ 193 | 252 : AWB is OFF. The user may manually set the values for Red Gain and Blue Gain.\n\n 194 | NOTE: Setting AWB to OFF after the gains have settled is a good way to ensure multiple 195 | pictures have the same exposure. $ 196 | 253 : List of available preset AWB scenes.\n\nNOTE: Users 197 | should wait for the Red and Blue gains to settle before taking a picture or video. $ 198 | 254 : The current value of the Red Gain.\n\nWhen AWB is OFF, then the user may adjust 199 | the value of the Red Gain.\n\nEach gain must be between 0.0 and 8.0. 200 | Typical values are between 0.9 and 1.9. $ 201 | 255 : The current value of the Blue Gain.\n\nWhen AWB is OFF, then the user may adjust 202 | the value of the Blue Gain.\n\nEach gain must be between 0.0 and 8.0. 203 | Typical values are between 0.9 and 1.9. $ 204 | 205 | # Shutter Speed 206 | 300 : Shutter speed will be automatically determined by the auto-exposure algorithm. 207 | Faster shutter times naturally require greater amounts of illumination and vice versa. $ 208 | 301 : The current exposure speed speed of the camera. If you have set shutter speed to a 209 | non-zero value, then exposure speed and shutter speed should be equal. If shutter speed 210 | is Auto, then this value is the actual exposure speed currently in use.$ 211 | 302 : Manually set the shutter speed using the edit field and drop down selection. 212 | In later firmwares, this attribute is limited by the value of the framerate attribute.\n\n 213 | For example, if the Frame rate is set to 30.0 fps, the shutter speed cannot be slower than 214 | 33,333µs (1 / Frame rate).\n\n When shutter speed is used, the exposure speed value 215 | matches the shutter speed value. $ 216 | 303 : Enter the shutter speed value.\n\nNote: The programmed value is limited to either:\n\n 217 | a.) 1 / (fixed framerate + framerate delta), if fixed framerate.\n 218 | b.) 1 / (framerate range 'From' value), if framerate range. $ 219 | 304 : Select the shutter speed multiplier.\n\nNote: The programmed value is limited to 220 | either:\n\n 221 | a.) 1 / (fixed framerate + framerate delta), if fixed framerate.\n 222 | b.) 1 / (framerate range 'From' value), if framerate range. $ 223 | 224 | # Framerate 225 | 310 : The currently programmed frame rate.\n\n 226 | If an error is detected in the selected framerate values, the color of the messages will be Red 227 | else Blue for no errors detected. $ 228 | 311 : Enable a fixed framerate. $ 229 | 312 : Enter the fixed framerate.\n\n 230 | The edit field allows the use of the '/' character to specify a fraction, e.g. 231 | entering 1/6 is interpreted as 0.16666667, or 15/2.5 is 6.0.\n\n 232 | The lowest framerate value is fixed at 1/6 (or a 6 second maximum shutter speed), 233 | the highest framerate value is fixed at 90. $ 234 | 313 : Enable a framerate range. $ 235 | 314 : Enter the framerate range 'From' value.\n\n 236 | The edit field allows the use of the '/' character to specify a fraction, e.g. 237 | entering 1/6 is interpreted as 0.16666667, or 15/2.5 is 6.0.\n\n 238 | The lowest framerate range 'From' value is fixed at 1/6 (or a 6 second maximum shutter speed), 239 | the highest framerate range 'From' value must be less than the framerate range 'To' value. $ 240 | 315 : Enter a framerate delta that is applied to the fixed framerate. \n\n 241 | The edit field allows the use of the '/' character to specify a fraction, e.g. 242 | entering 1/6 is interpreted as 0.16666667, or 15/2.5 is 6.0.\n\n 243 | The lowest framerate delta is fixed at -10, the highest 244 | framerate delta is fixed at 10.$ 245 | 316 : Enter the framerate range 'To' value.\n\n 246 | The edit field allows the use of the '/' character to specify a fraction, e.g. 247 | entering 1/6 is interpreted as 0.16666667, or 15/2.5 is 6.0.\n\n 248 | The lowest framerate range 'To' value must be greater than the framerate range 'From' 249 | value, the highest framerate range 'To' value is fixed at 90. $ 250 | 251 | # ------------- Advanced Tab ---------------- 252 | 350 : Select Auto sensor mode (default).\n\nThis is an advanced property which can be 253 | used to control the camera's sensor 254 | mode. By default, mode 0 is used which allows the camera to automatically select 255 | an input mode based on the requested resolution and framerate. $ 256 | 351 : Select a sensor mode from the drop down list.\n\n 257 | Valid values are currently between Mode 1 and Mode 7. The set of valid sensor modes 258 | (along with the heuristic used to select one automatically) are detailed in the 259 | Sensor Modes section of the documentation. (C) $ 260 | 352 : List of valid sensor modes from mode 1 to mode 7.\n\n 261 | NOTE: At the time of writing, setting this property does nothing unless the camera 262 | has been initialized with a sensor mode other than 0. Furthermore, some mode 263 | transitions appear to require setting the property twice (in a row). 264 | This appears to be a firmware limitation. $ 265 | 360: Set the clock mode to 'reset'.\n\n 266 | This is an advanced property which can be used to control the nature of the frame 267 | timestamps available from the frame property. When this is “reset” (the default) 268 | each frame's timestamp will be relative to the start of the recording. (C) $ 269 | 361: Set the clock mode to 'raw'.\n\n 270 | This is an advanced property which can be used to control the nature of the frame 271 | timestamps available from the frame property. When this is 272 | “raw”, each frame's timestamp will be relative to the last initialization of the camera. 273 | (C) $ 274 | 362 : The camera's timestamp is a 64-bit integer representing the number of microseconds 275 | since the last system boot. When the camera's clock_mode is 'raw' the values returned 276 | by this attribute are comparable to those from the frame timestamp attribute. (C) $ 277 | 363 : Statistics will be calculated from the preceding preview frame (this also 278 | applies when the preview is not visible). $ 279 | 364 : Statistics will be calculated from the captured image itself.\n\n 280 | The advantages to calculating scene statistics from the captured image are that 281 | time between startup and capture is reduced as only the AGC (automatic gain control) 282 | has to converge. The downside is that processing time for captures increases and 283 | that white balance and gain won't necessarily match the preview.\n\n 284 | Warning: Enabling the still statistics pass will override fixed white balance gains. (C) $ 285 | 370 : Remove any color effect applied to the camera. $ 286 | 371 : Set the color effect applied to the camera using the three sliders.\n\n 287 | For example, to make the image black and white set both U and V slider values to 128 288 | (center of the slider range). $ 289 | 372 : Adjust the luminance ('Y') applied to the color effect. This has about the same affect as 290 | adjusting the Brightness slider on the Basic Controls tab. $ 291 | 373 : Adjust the 'U' chromiance value of the color effect. Range is from 0 to 255. $ 292 | 374 : Adjust the 'V' chromiance value of the color effect. Range is from 0 to 255. $ 293 | 375 : The current YUV value of the color effect applied to the camera. \n\n 294 | 'Y' is the Brightness, 'U' and 'V' are the chromiance values from the sliders. $ 295 | 376 : The RGB value of the color effect applied to the camera.\n\n 296 | RGB is calculated from YUV as follows:\n 297 | Red = Clamp(Y + 1.370705 * (V-128))\n 298 | Green = Clamp(Y - 0.337633 * (U-128) - 0.698001 * (V-128))\n 299 | Blue = Clamp(Y + 1.732446 * (U-128)) $ 300 | 377 : Current color effect. The effect is also shown in the preview window. $ 301 | 302 | # ------------- Timelapse Tab ---------------- 303 | 1000 : NOTHING $ 304 | 305 | #-------------- Photo Params Dialog ----------------- 306 | 2000 : Defines the quality of the JPEG encoder as an integer ranging from 1 to 100. 307 | Defaults to 85. \n\n 308 | Note: JPEG quality is not a percentage and definitions of quality vary widely. (C) $ 309 | 2001 : Current value of JPEG encoder quality. $ 310 | 2010 : Defines the restart interval for the JPEG encoder as a number of JPEG MCUs. 311 | The actual restart interval used will be a multiple of the number of MCUs per row 312 | in the resulting image. (C) $ 313 | 2015 : No thumbnail is included in the EXIF metadata. $ 314 | 2020 : Defines the size and quality of the thumbnail to embed in the Exif metadata. 315 | Specifying None disables thumbnail generation. Otherwise, specify a tuple of 316 | (width, height, quality). Defaults to (64, 48, 35). (C) $ 317 | 2021 : Select the width of the embedded thumbnail image.\n\n 318 | For best results, the width to height ratio should closely match the image width to height 319 | ratio. $ 320 | 2022 : Select the height of the embedded thumbnail image.\n\n 321 | For best results, the width to height ratio should closely match the image width to height 322 | ratio. $ 323 | 2023 : Defines the quality of the thumbnail. Defaults to 40.\n\n 324 | See JPEG quality for more information. $ 325 | 2030 : No Bayer data is included in Exif metadata. $ 326 | 2031 : Raw bayer data from the camera's sensor is included in the Exif metadata. $ 327 | 2100 : Enable / disable including EXIF metadata when the quick image is saved.\n\n 328 | Note: EXIF data is always included in picamera JPEG files. This option pertains to 329 | saving the image that is displayed in the photo viewer pane. The PILLOW library is 330 | used to save the image. At this point, the EXIF data can be included or excluded 331 | from the saved image.\n\n 332 | Any JPEG files that are saved immediately using picamera functions will always have EXIF 333 | data. $ 334 | 2110 : Enter text that is included in the JPEG EXIF metadata. The data is saved under 335 | the tag 'EXIF.UserComment'. $ 336 | 337 | #-------------- Video Params Dialog ----------------- 338 | 3000 : Select the H.264 profile to use for encoding. Defaults to ‘high', but can be 339 | one of ‘baseline', ‘main', ‘extended', ‘high', or ‘constrained'. $ 340 | 3001 : The H.264 level to use for encoding. Defaults to ‘4', but can be any H.264 level 341 | up to ‘4.2'. $ 342 | 3002 : The key frame rate (the rate at which I-frames are inserted in the output).\n\n 343 | Select None (default).\n 344 | Select Single initial I-frame, and then only P-frames subsequently.\n 345 | Select Frames between I-frames. The number of frames is entered in the edit field.\n\n 346 | Note: split_recording() will fail in this mode. (C) $ 347 | 3003 : The number of frames between I-Frames. $ 348 | 3004 : Select the key frame format (the way in which I-frames will be inserted into 349 | the output stream). Defaults to None, but can be one of ‘cyclic', ‘adaptive', ‘both', 350 | or ‘cyclicrows'. $ 351 | 3005 : The encoder should output SPS/PPS headers within the stream to 352 | ensure GOPs (groups of pictures) are self describing. This is important for streaming 353 | applications where the client may wish to seek within the stream, and enables the use 354 | of split_recording(). (C) $ 355 | 3006 : Do not output SPS/PPS headers within the stream. $ 356 | 3007 : The encoder should include “Supplemental Enhancement Information” within the 357 | output stream. $ 358 | 3008 : Do not include “Supplemental Enhancement Information” data within output 359 | stream. $ 360 | 3009 : The encoder includes the camera's framerate in the SPS header. $ 361 | 3010 : Do not include the camera's framerate in the SPS header. $ 362 | 3011 : No motion vector estimation data is output. $ 363 | 3012 : Motion vector estimation data is output to the selected file. $ 364 | 3013 : Select a file to which to save motion vector estimation data. $ 365 | 3014 : Currently selected file for saving motion vector estimation data. $ 366 | 367 | #-------------- Image Effects Dialog --------------- 368 | 4000 : Control the quantization steps for the image. Valid values are 2 to 32, 369 | and the default is 4. $ 370 | 4001 : Current quantization level. $ 371 | 4010 : Set the size of the blur kernel. Valid values are 1 or 2. $ 372 | 4011 : Current blur kernel size. $ 373 | 4020 : Select the quadrant of the U/V space from which to retain chroma.\n\n 374 | 0=green, 1=red/yellow, 2=blue, 3=purple.\n\nThere is no default; this effect 375 | does nothing until parameters are set. $ 376 | 4030 : Select the direction of the colorswap.\n\n 377 | Swap RGB to BGR or swap RGB to BRG. $ 378 | 379 | # ------------- Annotation Dialog ---------------- 380 | 400 : Disable text annotation on the preview or image. $ 381 | 401 : Enable text annotation on the preview or image. Enter the text in the edit field. $ 382 | 402 : Enable / disable adding a timestamp on the preview and image. The timestamp format 383 | may be modified by selecting File | Preferences... then selecting the Interface tab. $ 384 | 403 : Text to annotate on the preview and image. $ 385 | 404 : Adjust text annotation size from 6 to 150. The default is 32. $ 386 | 405 : No background color is applied to the annotation text output. $ 387 | 406 : Select of background color for the annotation text output. $ 388 | 407 : Display a color chooser dialog for the background color. $ 389 | 408 : Use the default white color for the annotation text output. $ 390 | 409 : Select of text color for the annotation text output. The underlying 391 | firmware does not directly support setting all components of the text color, 392 | only the Y' component of a Y'UV tuple. This is roughly (but not precisely) 393 | analogous to the “brightness” of a color, so you may choose to think of 394 | this as setting how bright the annotation text will be relative to its 395 | background. (C) $ 396 | 410 : Adjust the relative 'brightness' of the annotation text color from 397 | black to white.\n\nThe Y component of the YUV color tuple is varied from 398 | 0.0 to 1.0 while keeping the U and V components 0. $ 399 | 411 : Enables / disables annotating the frame number. $ 400 | 401 | #----------------- Preferences Dialog ------------------- 402 | # General tab 403 | 6000 : Select the default directory for saving images. $ 404 | 6001 : Current default directory for saving images. $ 405 | 6002 : Select the default directory for saving videos. $ 406 | 6003 : Current default directory for saving videos. $ 407 | 6004 : Select the default directory for saving text data, sample scrpts, etc. $ 408 | 6005 : Current default directory for saving text data, sample scrpts, etc. $ 409 | 6010 : List of available image save formats. $ 410 | 6011 : Edit image capture parameters for the selected image format.$ 411 | 6020 : List of available video save formats. $ 412 | 6021 : Edit video capture parameters for the selected video format.$ 413 | 414 | 6050 : Enter the default date/time format string to be used for the timestamp. $ 415 | 6051 : Current default timestamp format. $ 416 | 6052 : Get web help on date/time format commands. $ 417 | 418 | 6060 : Enable/disable adding timestamp to photo names when saving. $ 419 | 6061 : Enable/disable adding timestamp to video names when saving. $ 420 | 421 | # Interface tab 422 | 6100 : List of available 'themes' to apply to the PiCameraApp user interface. $ 423 | 6110 : Enable/disable tooltips. $ 424 | 6111 : Enable/disable showing the tip number in the tooltips.\n\nThis is a debug 425 | option. $ 426 | 6112 : Select the amount of delay before the tooltip is shown once the mouse starts 427 | hovering over a control/widget. $ 428 | 6113 : Current amount of tooltip delay. $ 429 | 430 | # Other tab 431 | -------------------------------------------------------------------------------- /Source/Assets/camera-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/camera-icon.png -------------------------------------------------------------------------------- /Source/Assets/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/cancel.png -------------------------------------------------------------------------------- /Source/Assets/cancel_22x22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/cancel_22x22.png -------------------------------------------------------------------------------- /Source/Assets/computer-monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/computer-monitor.png -------------------------------------------------------------------------------- /Source/Assets/files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/files.png -------------------------------------------------------------------------------- /Source/Assets/flip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/flip.png -------------------------------------------------------------------------------- /Source/Assets/folders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/folders.png -------------------------------------------------------------------------------- /Source/Assets/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/help.png -------------------------------------------------------------------------------- /Source/Assets/keyboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/keyboard.gif -------------------------------------------------------------------------------- /Source/Assets/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/ok.png -------------------------------------------------------------------------------- /Source/Assets/ok_22x22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/ok_22x22.png -------------------------------------------------------------------------------- /Source/Assets/prefs1_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/prefs1_16x16.png -------------------------------------------------------------------------------- /Source/Assets/prefs_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/prefs_16x16.png -------------------------------------------------------------------------------- /Source/Assets/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/reset.png -------------------------------------------------------------------------------- /Source/Assets/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/rotate.png -------------------------------------------------------------------------------- /Source/Assets/video-icon-b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/video-icon-b.png -------------------------------------------------------------------------------- /Source/Assets/web_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/web_16x16.png -------------------------------------------------------------------------------- /Source/Assets/web_22x22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/web_22x22.png -------------------------------------------------------------------------------- /Source/Assets/window-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Billwilliams1952/PiCameraApp/61802b367d620aafb6b4e0bb84ea1ebd0dbd42c0/Source/Assets/window-close.png -------------------------------------------------------------------------------- /Source/BasicControls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # BasicControls.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | 25 | import os 26 | from collections import OrderedDict 27 | import RPi.GPIO 28 | 29 | # If no RPi.GPIO, then disable the ability to toggle the camera LED 30 | RPiGPIO = True 31 | 32 | try: 33 | import ttk 34 | from ttk import * 35 | except ImportError: 36 | from tkinter import ttk 37 | from tkinter.ttk import * 38 | 39 | # We are running PILLOW, the fork of PIL 40 | import PIL 41 | from PIL import Image, ImageTk, ExifTags 42 | 43 | from Dialog import * 44 | from Mapping import * 45 | from NotePage import * 46 | from Utils import * 47 | from VideoParams import * 48 | from PhotoParams import * 49 | from ImageEffects import * 50 | 51 | class BasicControls ( BasicNotepage ): 52 | def BuildPage ( self ): 53 | #### TODO: Add Rotation. Cleanup and organize controls 54 | # Add handling of Image Effect Params 55 | 56 | #----------- Select port for image captures ------------ 57 | f1 = MyLabelFrame(self,'Select port for image captures',0,0,span=2) 58 | self.UseVidPort = MyBooleanVar(False) 59 | self.UseRadio = MyRadio(f1,'Use Still Port',False,self.UseVidPort, 60 | self.UseVideoPort,0,0,'W',tip=100) 61 | MyRadio(f1,'Use Video Port',True,self.UseVidPort, 62 | self.UseVideoPort,0,1,'W',tip=101) 63 | f2 = ttk.Frame(f1) # Sub frame 64 | f2.grid(row=1,column=0,columnspan=4,sticky='NSEW') 65 | self.VideoDenoise = MyBooleanVar(True) 66 | b = ttk.Checkbutton(f2,text='Video denoise',variable=self.VideoDenoise, 67 | command=self.VideoDenoiseChecked) 68 | b.grid(row=1,column=0,sticky='NW',padx=5) 69 | ToolTip(b,msg=103) 70 | self.VideoStab = MyBooleanVar(False) 71 | b = ttk.Checkbutton(f2,text='Video stabilization',variable=self.VideoStab, 72 | command=self.VideoStabChecked) 73 | b.grid(row=1,column=1,sticky='NW') 74 | ToolTip(b, msg=105) 75 | self.ImageDenoise = MyBooleanVar(True) 76 | b = ttk.Checkbutton(f2,text='Image denoise',variable=self.ImageDenoise, 77 | command=self.ImageDenoiseChecked) 78 | b.grid(row=1,column=2,sticky='NW',padx=10) 79 | ToolTip(b, msg=104) 80 | 81 | #--------------- Picture/Video Capture Size --------------- 82 | f = MyLabelFrame(self,'Picture/Video capture size in pixels',1,0) 83 | #f.columnconfigure(0,weight=1) 84 | f1 = ttk.Frame(f) # Sub frames to frame f 85 | f1.grid(row=1,column=0,sticky='NSEW') 86 | f1.columnconfigure(1,weight=1) 87 | self.UseFixedResolutions = BooleanVar() 88 | self.UseFixedResolutions.set(True) 89 | self.UseFixedResRadio = ttk.Radiobutton(f1,text='Use fixed:', 90 | variable=self.UseFixedResolutions, 91 | value=True,command=self.UseFixedResRadios,padding=(5,5,5,5)) 92 | ToolTip(self.UseFixedResRadio,120) 93 | self.UseFixedResRadio.grid(row=0,column=0,sticky='NW') 94 | self.FixedResolutionsCombo = Combobox(f1,state='readonly',width=25) 95 | self.FixedResolutionsCombo.bind('<>', 96 | self.FixedResolutionChanged) 97 | self.FixedResolutionsCombo.grid(row=0,column=1,columnspan=3,sticky='W') 98 | ToolTip(self.FixedResolutionsCombo,121) 99 | #------------ Capture Width and Height ---------------- 100 | # OrderedDict is used to ensure the keys stay in the same order as 101 | # entered. I want the combobox to display in this order 102 | #### TODO: Must check resolution and framerate and disable the Video 103 | # button if we exceed limits of the modes 104 | # Framerate 1-30 fps up to 1920x1080 16:9 aspect ratio 105 | # Framerate 1-15 fps up to 2592 x 1944 4:3 aspect ratio 106 | # Framerate 0.1666 to 1 fps up to 2592 x 1944 4:3 aspect ratio 107 | # Framerate 1-42 fps up t0 1296 x 972 4:3 aspect ratio 108 | # Framerate 1-49 fps up to 1296 x 730 16:9 aspect ratio 109 | # Framerate 42.1 - 60 fps to 640 x 480 4:3 aspect ratio 110 | # Framerate 60.1 - 90 fps to 640 x 480 4:3 aspect ratio 111 | self.StandardResolutions = OrderedDict([ \ 112 | ('CGA', (320,200)), ('QVGA', (320,240)), 113 | ('VGA', (640,480)), ('PAL', (768,576)), 114 | ('480p', (720,480)), ('576p', (720,576)), 115 | ('WVGA', (800,480)), ('SVGA', (800,600)), 116 | ('FWVGA', (854,480)), ('WSVGA', (1024,600)), 117 | ('XGA', (1024,768)), ('HD 720', (1280,720)), 118 | ('WXGA_1', (1280,768)), ('WXGA_2', (1280,800)), 119 | ('SXGA', (1280,1024)), ('SXGA+', (1400,1050)), 120 | ('UXGA', (1600,1200)), ('WSXGA+', (1680,1050)), 121 | ('HD 1080', (1920,1080)), ('WUXGA', (1920,1200)), 122 | ('2K', (2048,1080)), ('QXGA', (2048, 1536)), 123 | ('WQXGA', (2560,1600)), ('MAX Resolution', (2592,1944)), 124 | ]) 125 | vals = [] 126 | for key in self.StandardResolutions.keys(): 127 | vals.append('%s: (%dx%d)' % (key, # Tabs not working?!! 128 | self.StandardResolutions[key][0], 129 | self.StandardResolutions[key][1])) 130 | self.FixedResolutionsCombo['values'] = vals 131 | self.FixedResolutionsCombo.current(10) 132 | 133 | f2 = ttk.Frame(f) # subframe to frame f 134 | f2.grid(row=2,column=0,sticky='NSEW') 135 | f2.columnconfigure(2,weight=1) 136 | f2.columnconfigure(4,weight=1) 137 | b2 = ttk.Radiobutton(f2,text='Roll your own:', 138 | variable=self.UseFixedResolutions, 139 | value=False,command=self.UseFixedResRadios,padding=(5,5,5,5)) 140 | b2.grid(row=1,column=0,sticky='NW') 141 | ToolTip(b2,122) 142 | 143 | Label(f2,text="Width:",anchor=E).grid(column=1,row=1,sticky='E',ipadx=3,ipady=3) 144 | Widths = [] 145 | for i in range(1,82): 146 | Widths.append(32 * i) # Widths can be in 32 byte increments 147 | self.cb = MyComboBox ( f2, Widths, current=10, 148 | callback=self.ResolutionChanged, width=5, row=1, col=2, 149 | sticky='W', state='disabled', tip=123) 150 | 151 | Label(f2,text="Height:",anchor=E).grid(column=3,row=1,sticky='W',ipadx=5,ipady=3) 152 | Heights = [] 153 | for i in range(1,123): 154 | Heights.append(16 * i) # heights in 16 byte increments 155 | self.cb1 = MyComboBox ( f2, Heights, current=10, 156 | callback=self.ResolutionChanged, width=5, row=1, col=4, 157 | sticky='W', state='disabled', tip=124) 158 | 159 | ttk.Label(f2,text='Actual:').grid(row=2,column=1,sticky='E') 160 | self.WidthLabel = ttk.Label(f2,style='DataLabel.TLabel') 161 | self.WidthLabel.grid(row=2,column=2,sticky='W') 162 | ToolTip(self.WidthLabel,125) 163 | ttk.Label(f2,text='Actual:').grid(row=2,column=3,sticky='E') 164 | self.HeightLabel = ttk.Label(f2,style='DataLabel.TLabel') 165 | self.HeightLabel.grid(row=2,column=4,sticky='W') 166 | ToolTip(self.HeightLabel,126) 167 | 168 | Separator(f,orient=HORIZONTAL).grid(pady=5,row=3,column=0, 169 | columnspan=4,sticky='EW') 170 | 171 | #--------------- Zoom Region Before ---------------- 172 | f4 = MyLabelFrame(f,'Zoom region of interest before taking '+ 173 | 'picture/video',4,0) 174 | #f4.columnconfigure(1,weight=1) 175 | #f4.columnconfigure(3,weight=1) 176 | Label(f4,text='X:').grid(row=0,column=0,sticky='E') 177 | self.Xzoom = ttk.Scale(f4,from_=0.0,to=0.94,orient='horizontal') 178 | self.Xzoom.grid(row=0,column=1,sticky='W',padx=5,pady=3) 179 | self.Xzoom.set(0.0) 180 | ToolTip(self.Xzoom,130) 181 | Label(f4,text='Y:').grid(row=0,column=2,sticky='E') 182 | self.Yzoom = ttk.Scale(f4,from_=0.0,to=0.94,orient='horizontal') 183 | self.Yzoom.grid(row=0,column=3,sticky='W',padx=5,pady=3) 184 | self.Yzoom.set(0.0) 185 | ToolTip(self.Yzoom,131) 186 | Label(f4,text='Width:').grid(row=1,column=0,sticky='E') 187 | self.Widthzoom = ttk.Scale(f4,from_=0.05,to=1.0,orient='horizontal') 188 | self.Widthzoom.grid(row=1,column=1,sticky='W',padx=5,pady=3) 189 | self.Widthzoom.set(1.0) 190 | ToolTip(self.Widthzoom,132) 191 | Label(f4,text='Height:').grid(row=1,column=2,sticky='E') 192 | self.Heightzoom = ttk.Scale(f4,from_=0.05,to=1.0,orient='horizontal') 193 | self.Heightzoom.grid(row=1,column=3,sticky='W',padx=5,pady=3) 194 | self.Heightzoom.set(1.0) 195 | ToolTip(self.Heightzoom,133) 196 | # WLW THIS IS A PROBLEM 197 | image = PIL.Image.open('Assets/reset.png') #.resize((16,16)) 198 | self.resetImage = GetPhotoImage(image.resize((16,16))) 199 | self.ResetZoomButton = ttk.Button(f4,image=self.resetImage, 200 | command=self.ZoomReset) 201 | self.ResetZoomButton.grid(row=0,column=4,rowspan=2,sticky='W') 202 | ToolTip(self.ResetZoomButton,134) 203 | 204 | self.Xzoom.config(command=lambda newval, 205 | widget=self.Xzoom:self.Zoom(newval,widget)) 206 | self.Yzoom.config(command=lambda newval, 207 | widget=self.Yzoom:self.Zoom(newval,widget)) 208 | self.Widthzoom.config(command=lambda newval, 209 | widget=self.Widthzoom:self.Zoom(newval,widget)) 210 | self.Heightzoom.config(command=lambda newval, 211 | widget=self.Heightzoom:self.Zoom(newval,widget)) 212 | 213 | Separator(f,orient=HORIZONTAL).grid(pady=5,row=5,column=0, 214 | columnspan=3,sticky='EW') 215 | 216 | #--------------- Resize Image After ---------------- 217 | f4 = MyLabelFrame(f,'Resize image after taking picture/video',6,0) 218 | #f4.columnconfigure(3,weight=1) 219 | #f4.columnconfigure(5,weight=1) 220 | 221 | b = MyBooleanVar(False) 222 | self.ResizeAfterNone = MyRadio(f4,'None (Default)',False,b, 223 | self.AllowImageResizeAfter,0,0,'W',pad=(0,5,0,5), tip=140) 224 | MyRadio(f4,'Resize',True,b,self.AllowImageResizeAfter, 225 | 0,1,'W',pad=(5,5,0,5),tip=141) 226 | 227 | Label(f4,text="Width:",anchor=E).grid(column=2,row=0,sticky='E',ipadx=3,ipady=3) 228 | self.resizeWidthAfterCombo = MyComboBox ( f4, Widths, current=10, 229 | callback=self.ResizeAfterChanged, width=5, row=0, col=3, 230 | sticky='W', state='disabled', tip=142) 231 | 232 | Label(f4,text="Height:",anchor=E).grid(column=4,row=0,sticky='W',ipadx=5,ipady=3) 233 | self.resizeHeightAfterCombo = MyComboBox ( f4, Heights, current=10, 234 | callback=self.ResizeAfterChanged, width=5, row=0, col=5, 235 | sticky='W', state='disabled', tip=143) 236 | 237 | self.resizeAfter = None 238 | 239 | #--------------- Quick Adjustments ---------------- 240 | f = MyLabelFrame(self,'Quick adjustments',2,0) 241 | #f.columnconfigure(2,weight=1) 242 | #-Brightness 243 | self.brightLabel, self.brightness, val = \ 244 | self.SetupLabelCombo(f,'Brightness:',0,0,0, 100, 245 | self.CameraBrightnessChanged, self.camera.brightness ) 246 | self.CameraBrightnessChanged(val) 247 | ToolTip(self.brightness,msg=150) 248 | #-Contrast 249 | self.contrastLabel, self.contrast, val = \ 250 | self.SetupLabelCombo(f,'Contrast:',0,3,-100, 100, 251 | self.ContrastChanged, self.camera.contrast ) 252 | self.ContrastChanged(val) 253 | ToolTip(self.contrast,msg=151) 254 | #-Saturation 255 | self.saturationLabel, self.saturation, val = \ 256 | self.SetupLabelCombo(f,'Saturation:',1,0,-100, 100, 257 | self.SaturationChanged, self.camera.saturation, label='Sat' ) 258 | self.SaturationChanged(val) 259 | ToolTip(self.saturation,msg=152) 260 | #-Sharpness 261 | self.sharpnessLabel, self.sharpness, val = \ 262 | self.SetupLabelCombo(f,'Sharpness:',1,3,-100, 100, 263 | self.SharpnessChanged, self.camera.sharpness ) 264 | self.SharpnessChanged(val) 265 | ToolTip(self.sharpness,msg=153) 266 | #-Reset 267 | #self.ResetGeneralButton = Button(f,image=self.resetImage,width=5, 268 | #command=self.ResetGeneralSliders) 269 | #self.ResetGeneralButton.grid(row=4,column=2,sticky='W',padx=5) 270 | #ToolTip(self.ResetGeneralButton,msg=154) 271 | 272 | #--------------- Image Effects ---------------- 273 | f = MyLabelFrame(self,'Preprogrammed image effects',3,0) 274 | #f.columnconfigure(2,weight=1) 275 | 276 | v = MyBooleanVar(False) 277 | self.NoEffectsRadio = MyRadio(f,'None (Default)',False,v, 278 | self.EffectsChecked,0,0,'W',tip=160) 279 | MyRadio(f,'Select effect:',True,v,self.EffectsChecked,0,1,'W', 280 | tip=161) 281 | 282 | self.effects = Combobox(f,height=15,width=10,state='readonly')#,width=15) 283 | self.effects.grid(row=0,column=2,sticky='W') 284 | effects = list(self.camera.IMAGE_EFFECTS.keys()) # python 3 workaround 285 | effects.remove('none') 286 | effects.sort() #cmp=lambda x,y: cmp(x.lower(),y.lower())) # not python 3 287 | self.effects['values'] = effects 288 | self.effects.current(0) 289 | self.effects.bind('<>',self.EffectsChanged) 290 | ToolTip(self.effects, msg=162) 291 | 292 | self.ModParams = ttk.Button(f,text='Params...', 293 | command=self.ModifyEffectsParamsPressed,underline=0,padding=(5,3,5,3),width=8) 294 | self.ModParams.grid(row=0,column=3,sticky=EW,padx=5) 295 | ToolTip(self.ModParams, msg=163) 296 | self.EffectsChecked(False) 297 | ''' 298 | Add additional controls if JPG is selected 299 | Certain file formats accept additional options which can be specified as keyword 300 | arguments. Currently, only the 'jpeg' encoder accepts additional options, which are: 301 | 302 | quality - Defines the quality of the JPEG encoder as an integer ranging from 1 to 100. 303 | Defaults to 85. Please note that JPEG quality is not a percentage and 304 | definitions of quality vary widely. 305 | restart - Defines the restart interval for the JPEG encoder as a number of JPEG MCUs. 306 | The actual restart interval used will be a multiple of the number of MCUs per row in the resulting image. 307 | thumbnail - Defines the size and quality of the thumbnail to embed in the Exif 308 | metadata. Specifying None disables thumbnail generation. Otherwise, 309 | specify a tuple of (width, height, quality). Defaults to (64, 48, 35). 310 | bayer - If True, the raw bayer data from the camera’s sensor is included in the 311 | Exif metadata. 312 | ''' 313 | #--------------- Flash Mode --------------- 314 | f = MyLabelFrame(self,'LED and Flash mode',4,0,span=4) 315 | #f.columnconfigure(3,weight=1) 316 | self.LedOn = MyBooleanVar(True) 317 | self.LedButton = ttk.Checkbutton(f,text='Led On (via GPIO pins)', 318 | variable=self.LedOn, command=self.LedOnChecked) 319 | self.LedButton.grid(row=0,column=0,sticky='NW',pady=5, columnspan=2) 320 | ToolTip(self.LedButton,msg=102) 321 | Label(f,text='Flash Mode:').grid(row=1,column=0,sticky='W') 322 | b = MyStringVar('off') 323 | self.FlashModeOffRadio = MyRadio(f,'Off (Default)','off',b, 324 | self.FlashModeButton,1,1,'W',tip=180) 325 | MyRadio(f,'Auto','auto',b,self.FlashModeButton,1,2,'W',tip=181) 326 | MyRadio(f,'Select:','set',b,self.FlashModeButton,1,3,'W',tip=182) 327 | # Use invoke() on radio button to force a command 328 | self.FlashModeCombo = Combobox(f,state='readonly',width=10) 329 | self.FlashModeCombo.grid(row=1,column=4,sticky='W') 330 | self.FlashModeCombo.bind('<>',self.FlashModeChanged) 331 | modes = list(self.camera.FLASH_MODES.keys()) 332 | modes.remove('off') # these two are handled by radio buttons 333 | modes.remove('auto') 334 | modes.sort() #cmp=lambda x,y: cmp(x.lower(),y.lower())) 335 | self.FlashModeCombo['values'] = modes 336 | self.FlashModeCombo.current(0) 337 | self.FlashModeCombo.config(state='disabled') 338 | ToolTip(self.FlashModeCombo,183) 339 | 340 | self.FixedResolutionChanged(None) 341 | 342 | def Reset ( self ): 343 | # Use widget.invoke() to simulate a button/radiobutton press 344 | self.UseRadio.invoke() 345 | self.LedOn.set(True) 346 | self.VideoStab.set(False) # Doesn't call the function! 347 | self.VideoDenoise.set(True) 348 | self.ImageDenoise.set(True) 349 | self.ResetGeneralSliders() 350 | self.UseFixedResRadio.invoke() 351 | self.FixedResolutionsCombo.current(10) # Set to 1280 x 1024 352 | self.ResetZoomButton.invoke() 353 | self.ResizeAfterNone.invoke() 354 | self.NoEffectsRadio.invoke() 355 | self.effects.current(0) 356 | self.UseRadio.focus_set() 357 | self.FlashModeOffRadio.invoke() 358 | def UseVideoPort ( self , val): 359 | pass #self.camera.use_video_port = val 360 | def LedOnChecked ( self ): 361 | self.camera.led = self.LedOn.get() 362 | def SetupLabelCombo ( self, parent, textname, rownum, colnum, 363 | minto, maxto, callback, cameraVal, label=''): 364 | l = Label(parent,text=textname) 365 | l.grid(row=rownum,column=colnum*3,sticky='E',pady=2)#,padx=2) 366 | label = Label(parent,width=4,anchor=E)#,relief=SUNKEN, background='#f0f0ff') 367 | label.grid(row=rownum,column=colnum*3+1) 368 | #label.config(font=('Helvetica',12)) 369 | # create the scale WITHOUT a callback. Then set the scale. 370 | scale = ttk.Scale(parent,from_=minto,to=maxto,orient='horizontal') 371 | scale.grid(row=rownum,column=colnum*3+2,sticky='W',padx=5,pady=3) 372 | val = cameraVal 373 | scale.set(val) # this would attempt to call any callback 374 | scale.config(command=callback) # now supply the callback 375 | return label, scale, val 376 | def UpdateMe( self, newVal, label ): 377 | val = int(float(newVal)) 378 | label.config(text='%d' % val, 379 | foreground='red' if val < 0 else 'blue' if val > 0 else 'black' ) 380 | return val 381 | def CameraBrightnessChanged ( self, newVal ): 382 | self.brightness.focus_set() 383 | self.camera.brightness = self.UpdateMe(newVal,self.brightLabel) 384 | def ContrastChanged ( self, newVal ): 385 | self.contrast.focus_set() 386 | self.camera.contrast = self.UpdateMe(newVal,self.contrastLabel) 387 | def SaturationChanged ( self, newVal ): 388 | self.saturation.focus_set() 389 | self.camera.saturation = self.UpdateMe(newVal,self.saturationLabel) 390 | def SharpnessChanged ( self, newVal ): 391 | self.sharpness.focus_set() 392 | self.camera.sharpness = self.UpdateMe(newVal,self.sharpnessLabel) 393 | def ResetGeneralSliders ( self ): 394 | self.brightness.set(50) 395 | self.contrast.set(0) 396 | self.saturation.set(0) 397 | self.sharpness.set(0) 398 | #self.ResetGeneralButton.focus_set() 399 | def UpdateWidthHeightLabels ( self ): 400 | res = self.camera.resolution # in case a different default value 401 | self.WidthLabel.config(text='%d' % int(res[0])) 402 | self.HeightLabel.config(text='%d' % int(res[1])) 403 | def ResolutionChanged(self,event): 404 | self.camera.resolution = (int(self.cb.get()),int(self.cb1.get())) 405 | self.UpdateWidthHeightLabels() 406 | def FixedResolutionChanged ( self, event ): 407 | key = self.FixedResolutionsCombo.get().split(':')[0] 408 | self.camera.resolution = self.StandardResolutions[key] 409 | self.UpdateWidthHeightLabels() 410 | def UseFixedResRadios ( self ): 411 | states = {False:'disabled', True:'readonly'} 412 | useFixedRes = self.UseFixedResolutions.get() 413 | if useFixedRes: 414 | self.FixedResolutionChanged(None) 415 | self.FixedResolutionsCombo.focus_set() 416 | else: 417 | self.ResolutionChanged(None) 418 | self.cb.focus_set() 419 | self.FixedResolutionsCombo.config(state=states[useFixedRes]) 420 | self.cb.config(state=states[not useFixedRes]) 421 | self.cb1.config(state=states[not useFixedRes]) 422 | def Zoom ( self, newVal, scale ): 423 | self.camera.zoom = (float(self.Xzoom.get()),float(self.Yzoom.get()), 424 | float(self.Widthzoom.get()),float(self.Heightzoom.get())) 425 | scale.focus_set() 426 | def SetZoom ( self, x, y, w, h ): 427 | self.Xzoom.set(x) 428 | self.Yzoom.set(y) 429 | self.Widthzoom.set(w) 430 | self.Heightzoom.set(h) 431 | def ZoomReset ( self ): 432 | self.Xzoom.set(0.0) 433 | self.Yzoom.set(0.0) 434 | self.Widthzoom.set(1.0) 435 | self.Heightzoom.set(1.0) 436 | def AllowImageResizeAfter ( self, allowResizeAfter ): 437 | if allowResizeAfter: 438 | state = 'readonly' 439 | self.ResizeAfterChanged(None) 440 | self.resizeWidthAfterCombo.focus_set() 441 | else: 442 | self.resizeAfter = None 443 | state = 'disabled' 444 | self.resizeWidthAfterCombo.config(state=state) 445 | self.resizeHeightAfterCombo.config(state=state) 446 | def ResizeAfterChanged ( self, event ): 447 | self.resizeAfter = ( int(self.resizeWidthAfterCombo.get()), 448 | int(self.resizeHeightAfterCombo.get()) ) 449 | def GetResizeAfter ( self ): 450 | return self.resizeAfter 451 | def EffectsChecked ( self, EffectsEnabled ): 452 | if EffectsEnabled == True: 453 | self.effects.config(state='readonly') 454 | self.EffectsChanged(None) 455 | self.effects.focus_set() 456 | else: 457 | self.effects.config(state='disabled') 458 | self.ModParams.config(state='disabled') 459 | self.camera.image_effect = 'none' 460 | def EffectsChanged ( self, event ): 461 | self.camera.image_effect = self.effects.get() 462 | if self.camera.image_effect in ['solarize', 'colorpoint', 463 | 'colorbalance', 'colorswap', 'posterise', 'blur', 'film', 464 | 'watercolor']: 465 | self.ModParams.config(state='!disabled') 466 | params = Effects1Page.EffectParam[self.camera.image_effect] 467 | if params != -1: # We have something to program 468 | self.camera.image_effect_params = \ 469 | Effects1Page.EffectParam[self.camera.image_effect] 470 | else: 471 | self.ModParams.config(state='disabled') 472 | def ModifyEffectsParamsPressed ( self ): 473 | ImageEffectsDialog(self,title='Image Effects Parameters', 474 | camera=self.camera,okonly=False) 475 | def ImageDenoiseChecked ( self ): 476 | self.camera.image_denoise = self.ImageDenoise.get() 477 | def VideoDenoiseChecked ( self ): 478 | self.camera.video_denoise = self.VideoDenoise.get() 479 | def VideoStabChecked ( self ): 480 | self.camera.video_stabilization = self.VideoStab.get() 481 | def FlashModeButton ( self, FlashMode ): 482 | if FlashMode == 'set': 483 | self.FlashModeCombo.config(state='readonly') 484 | self.FlashModeCombo.focus_set() 485 | self.FlashModeChanged(None) 486 | else: 487 | self.FlashModeCombo.config(state='disabled') 488 | self.camera.flash_mode = FlashMode 489 | def FlashModeChanged ( self, event ): 490 | self.camera.flash_mode = self.FlashModeCombo.get() 491 | 492 | -------------------------------------------------------------------------------- /Source/CameraUtils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | CameraUtils.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | import picamera 16 | from picamera import * 17 | import picamera.array 18 | try: 19 | from Tkinter import * 20 | except ImportError: 21 | from tkinter import * 22 | try: 23 | from tkColorChooser import askcolor 24 | except ImportError: 25 | from tkinter.colorchooser import askcolor 26 | try: 27 | import tkFileDialog 28 | except ImportError: 29 | import tkinter.filedialog 30 | try: 31 | import tkMessageBox 32 | except ImportError: 33 | import tkinter.messagebox 34 | try: 35 | import ttk 36 | from ttk import * 37 | except ImportError: 38 | from tkinter import ttk 39 | #from ttk import * 40 | try: 41 | import tkFont 42 | except ImportError: 43 | import tkinter.font 44 | 45 | import PIL 46 | from PIL import Image, ImageTk, ExifTags 47 | 48 | from Utils import OnOff, EvenOdd 49 | from PreferencesDialog import * 50 | 51 | # 52 | # Class to handle formatting and otuputting the camera settings and EXIF tags 53 | # 54 | class CameraUtils: 55 | def __init__ ( self, camera, BasicControls): 56 | self.camera = camera 57 | self.TextBox = None 58 | self.BasicControls = BasicControls 59 | self.EXIFAdded = False 60 | self.even = True 61 | 62 | def SetupCameraSettingsTextbox ( self, textbox ): 63 | boldFont = Font(textbox,textbox.cget("font")) 64 | boldFont.configure(weight="bold") 65 | boldUnderlineFont = Font(textbox,textbox.cget("font")) 66 | boldUnderlineFont.configure(weight="bold",underline=True) 67 | textbox.tag_configure("Bold",font=boldFont) 68 | textbox.tag_configure("Title",font=("Arial",12,"bold")) 69 | textbox.tag_configure("Section",font=("Arial",11,"bold italic")) 70 | textbox.tag_configure("KeyboardCommand",font=boldFont,foreground='blue') 71 | textbox.tag_configure("CmdKey",font=boldUnderlineFont) 72 | textbox.tag_configure("odd",background='white') 73 | textbox.tag_configure("even",background='#f0f0ff') 74 | self.text = textbox 75 | 76 | def AddCmdKey ( self, text ): 77 | if self.outfile: 78 | self.outfile.write(text) 79 | self.outfile.write('\n') 80 | else: 81 | strs = text.split(':') 82 | bg = EvenOdd(self.even) 83 | self.text.insert(END," ",("KeyboardCommand",bg)) 84 | self.text.insert(END,strs[0],("KeyboardCommand",bg)) 85 | self.text.insert(END,strs[1],(bg)) 86 | self.text.insert(END,'\n',(bg)) 87 | self.even = not self.even 88 | 89 | def WriteString ( self, string, formatstring = "" ): 90 | if self.outfile: 91 | self.outfile.write(string) 92 | self.outfile.write('\n') 93 | else: 94 | self.text.insert(END,string,formatstring) 95 | self.text.insert(END,'\n',formatstring) 96 | 97 | def FillCameraSettingTextBox ( self, parent, writetofile = False ): 98 | if writetofile: 99 | # Get file to write, create it (delete if exist) 100 | self.outfile = tkFileDialog.asksaveasfile(mode='w',defaultextension="*.txt") 101 | if not self.outfile: 102 | self.ClearTextBox() 103 | else: 104 | self.outfile = None 105 | 106 | self.WriteString("Camera setups","Title") 107 | 108 | self.WriteString("Preferences","Section") 109 | 110 | self.AddCmdKey('Photo format:\t\t\'%s\'' % PreferencesDialog.DefaultPhotoFormat) 111 | # Output params based on photo format type.... 112 | if PreferencesDialog.DefaultPhotoFormat == 'jpeg': 113 | pass 114 | 115 | self.WriteString("Basic","Section") 116 | 117 | self.AddCmdKey('Use video port:\t\t%s' % OnOff(self.BasicControls.UseVidPort.get())) 118 | self.AddCmdKey('Stabilization:\t\t%s' % OnOff(self.camera.video_stabilization)) 119 | self.AddCmdKey('Video denoise:\t\t%s' % OnOff(self.camera.video_denoise)) 120 | self.AddCmdKey('Image denoise:\t\t%s' % OnOff(self.camera.image_denoise)) 121 | self.AddCmdKey('Resolution:\t\t%d x %d pixels'%self.camera.resolution) 122 | zoom = self.camera.zoom 123 | if zoom[0] == 0 and zoom[1] == 0 and zoom[2] == 1.0 and zoom[3] == 1.0: 124 | self.AddCmdKey('Zoom:\t\tnone') 125 | else: 126 | self.AddCmdKey('Zoom:\t\t(X %.3f Y %.3f W %.3f H %.3f)'%zoom) 127 | resize = self.BasicControls.GetResizeAfter() 128 | if resize == None: 129 | self.AddCmdKey('Resize:\t\tnone') 130 | else: 131 | self.AddCmdKey('Resize:\t\t%d x %d pixels' % resize) 132 | self.AddCmdKey('Brightness:\t\t%d' % self.camera.brightness) 133 | self.AddCmdKey('Contrast:\t\t%d' % self.camera.contrast) 134 | self.AddCmdKey('Saturation:\t\t%d' % self.camera.saturation) 135 | self.AddCmdKey('Sharpness:\t\t%d' % self.camera.sharpness) 136 | self.AddCmdKey('Image effect:\t\t%s' % self.camera.image_effect) 137 | params = self.camera.image_effect_params 138 | if params == None: 139 | self.AddCmdKey('Image params:\t\tnone') 140 | else: 141 | self.AddCmdKey('Image params:\t\t') 142 | self.AddCmdKey('Rotation:\t\t%d degrees' % self.camera.rotation) 143 | self.AddCmdKey('Flash mode:\t\t%s' % self.camera.flash_mode) 144 | 145 | self.WriteString("Exposure","Section") 146 | 147 | self.AddCmdKey('Metering mode:\t\t%s' % self.camera.meter_mode) 148 | self.AddCmdKey('Exposure mode:\t\t%s' % self.camera.exposure_mode) 149 | effiso = int(100.0 * self.camera.analog_gain/self.camera.digital_gain) 150 | if self.camera.iso == 0: 151 | self.AddCmdKey('ISO:\t\tAuto (Effective %d)' % effiso) 152 | else: 153 | self.AddCmdKey('ISO:\t\t%d (Effective %d)' % (self.camera.iso,effiso)) 154 | self.AddCmdKey('Analog gain:\t\t%.3f' % self.camera.analog_gain) 155 | self.AddCmdKey('Digital gain:\t\t%.3f' % self.camera.digital_gain) 156 | self.AddCmdKey('Exposure comp:\t\t%s' % self.camera.exposure_compensation) 157 | self.AddCmdKey('Shutter speed:\t\t%d usec' % \ 158 | (self.camera.exposure_speed if self.camera.shutter_speed == 0 \ 159 | else self.camera.shutter_speed) ) 160 | self.AddCmdKey('Exposure speed:\t\t%d usec' % self.camera.exposure_speed) 161 | self.AddCmdKey('Frame rate:\t\t%.3f fps' % self.camera.framerate) 162 | 163 | self.WriteString("Advanced","Section") 164 | 165 | self.AddCmdKey('AWB mode:\t\t%s' % self.camera.awb_mode) 166 | self.AddCmdKey('AWB Gains:\t\tRed %.3f Blue %.3f' % self.camera.awb_gains) 167 | self.AddCmdKey('DRC strength:\t\t%s' % self.camera.drc_strength) 168 | if self.camera.color_effects == None: 169 | self.AddCmdKey('Color effects:\t\tnone') 170 | else: 171 | self.AddCmdKey('Color effects:\t\t(U %d V %d)' % self.camera.color_effects) 172 | self.AddCmdKey('Sensor mode:\t\t%d' % self.camera.sensor_mode) 173 | 174 | 175 | self.WriteString("Annotate/EXIF metadata","Section") 176 | 177 | text = self.camera.annotate_text 178 | if len(text) == 0: 179 | self.AddCmdKey('Annotation:\t\tnone') 180 | else: 181 | self.AddCmdKey('Annotate text:\t\'%s\'' % text) 182 | self.AddCmdKey('Annotate text size:\t\t%d' % \ 183 | self.camera.annotate_text_size) 184 | self.AddCmdKey( \ 185 | 'Annotate foreground color:\tR %d G %d B %d' % \ 186 | self.camera.annotate_foreground.rgb_bytes) 187 | if self.camera.annotate_background == None: 188 | self.AddCmdKey('Annotate background color:\tnone') 189 | else: 190 | self.AddCmdKey(\ 191 | 'Annotate background color:\tR %d G %d B %d' % \ 192 | self.camera.annotate_background.rgb_bytes) 193 | self.AddCmdKey('Annotate frame num:\t\t%s'% \ 194 | OnOff(self.camera.annotate_frame_num)) 195 | 196 | # Don't close file here (if open), wait to write EXIF tags 197 | 198 | def AddEXIFTags ( self, currentImage ): 199 | if self.EXIFAdded or not currentImage: return 200 | 201 | self.even = True 202 | self.WriteString("\nEXIF Tags","Title") 203 | # ExifTool reads correctly..... should we call that? 204 | # import exifread ???? 205 | #----------- This does not read all tags -------------- 206 | try: 207 | exif = { 208 | PIL.ExifTags.TAGS[k] : v 209 | for k, v in currentImage._getexif().items() 210 | if k in PIL.ExifTags.TAGS 211 | } 212 | for key in exif.keys(): 213 | text = '%s:\t\t%s' % (key,exif[key]) 214 | self.AddCmdKey(text) 215 | except: 216 | self.WriteString('Exif tags not supported!') 217 | 218 | self.CloseFile() 219 | self.EXIFAdded = True 220 | 221 | def ClearTextBox ( self ): 222 | self.text.delete("1.0",END) 223 | self.EXIFAdded = False 224 | 225 | def CloseFile ( self ): 226 | if self.outfile: 227 | self.outfile.close() 228 | self.outfile = None 229 | -------------------------------------------------------------------------------- /Source/ConfigFile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | ConfigFile.py 5 | Copyright (C) 2015 - Bill Williams 6 | 7 | Read/Write the PiCamera INI file 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation, either version 3 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | ''' 19 | try: 20 | from configparser import ConfigParser 21 | except ImportError: 22 | from ConfigParser import ConfigParser # ver. < 3.0 23 | 24 | -------------------------------------------------------------------------------- /Source/CreateScript.py: -------------------------------------------------------------------------------- 1 | PiCameraLoaded = True 2 | try: 3 | import picamera 4 | from picamera import * 5 | import picamera.array 6 | except ImportError: 7 | raise ImportError("You do not seem to have PiCamera installed") 8 | PiCameraLoaded = False 9 | 10 | def OutputPythonScript ( camera ): 11 | # Open file 12 | # Loop through options and change 13 | pass 14 | 15 | 16 | -------------------------------------------------------------------------------- /Source/Dialog.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Dialog.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | try: 16 | from Tkinter import * 17 | except ImportError: 18 | from tkinter import * 19 | try: 20 | from tkColorChooser import askcolor 21 | except ImportError: 22 | from tkinter.colorchooser import askcolor 23 | try: 24 | import tkFileDialog 25 | except ImportError: 26 | import tkinter.filedialog 27 | try: 28 | import tkMessageBox 29 | except ImportError: 30 | import tkinter.messagebox 31 | try: 32 | import ttk 33 | from ttk import * 34 | except ImportError: 35 | from tkinter import ttk 36 | #from ttk import * 37 | try: 38 | import tkFont 39 | except ImportError: 40 | import tkinter.font 41 | 42 | import PIL 43 | from PIL import Image, ImageTk, ExifTags 44 | 45 | from Utils import UnderConstruction 46 | from Tooltip import * 47 | 48 | # 49 | # Generic Dialog CLass - All dialogs inherit from this one 50 | # 51 | class Dialog: 52 | def __init__ ( self, parent, modal=True, title='No title supplied', 53 | showtitlebar=True, centerTo='default', okonly=True, 54 | help=False, resizable=False, minwidth=None, minheight=None, 55 | camera=None, data = None ): 56 | self._parent = parent 57 | self.modal = modal 58 | self._window = Toplevel() 59 | self._window.minsize(minwidth,minheight) 60 | self.CancelButton = None 61 | 62 | if resizable is False: 63 | self._window.resizable(width=False,height=False) 64 | 65 | self._window.rowconfigure(0,weight=1) 66 | self._window.columnconfigure(0,weight=1) 67 | self._window.title(title) 68 | self._centerTo = centerTo 69 | 70 | # Should be: Need to fix..... 71 | # self._mainFrame 72 | # self.LayoutFrame 73 | # Supplied to User for Layout 74 | # self._buttonFrame 75 | # Help Cancel Ok 76 | self.MainFrame = ttk.Frame(self._window,padding=(5,5,5,5)) 77 | self.MainFrame.grid(row=0,column=0,columnspan=3,sticky='NSEW') 78 | 79 | self._camera = camera 80 | self.data = data 81 | 82 | self.okimage = ImageTk.PhotoImage(file='Assets/ok_22x22.png') 83 | self.OkButton = ttk.Button(self._window,text='Close' if okonly else 'Ok', 84 | command=lambda:self._Ok(None),image=self.okimage,compound='left') 85 | self.OkButton.grid(row=1,column=2,padx=10,pady=5) 86 | self.OkButton.focus_set() 87 | ToolTip(self.OkButton,50) 88 | self._window.bind( '', self._Ok ) 89 | 90 | if okonly is False: 91 | self.cancelimage = ImageTk.PhotoImage(file='Assets/cancel_22x22.png') 92 | self.CancelButton = ttk.Button(self._window,text='Cancel', 93 | command=lambda:self._Cancel(None),image=self.cancelimage, 94 | compound='left',state='disabled') 95 | self.CancelButton.grid(row=1,column=1,pady=5) 96 | ToolTip(self.CancelButton,51) 97 | self._window.bind( '', self._Cancel ) 98 | 99 | if help is True: 100 | b = ttk.Button(self._window,text='Help',command=lambda:self._Help(None)) 101 | b.grid(row=1,column=0,sticky='W',padx=10,pady=5) 102 | ToolTip(b,52) 103 | self._window.bind( '', self._Help ) 104 | 105 | self.BuildDialog() # Overriden function 106 | 107 | self._window.after(10,self._Position) # better way found! 108 | self._window.overrideredirect(not showtitlebar) 109 | self._window.transient(self._parent) # no icon 110 | 111 | if modal is True: # must close this dialog to return to parent 112 | self._window.grab_set() 113 | self._parent.wait_window(self._window) 114 | 115 | def BuildDialog ( self ): # Always Override 116 | UnderConstruction ( self.MainFrame ) 117 | def OkPressed ( self ): return True # Optional Override 118 | def CancelPressed ( self ): return True # Optional Override 119 | def HelpPressed ( self ): # Optional Override 120 | tkMessageBox.showwarning("Help","No Help available!") 121 | 122 | # Remap these so the dialog doesn't have to worry about the 123 | # 'event' parameter 124 | def _Ok ( self, event ): 125 | if self.OkPressed(): self._window.destroy() 126 | def _Cancel ( self, event ): 127 | if self.CancelPressed() : self._window.destroy() 128 | def _Help ( self, event ): 129 | self.HelpPressed() 130 | 131 | def _Position ( self ): 132 | if self._centerTo == 'default': return 133 | # handle center window and center screen 134 | if self._centerTo == 'parent': 135 | parentwidth = self._parent.winfo_width() 136 | parentheight = self._parent.winfo_height() 137 | locX = self._parent.winfo_x() 138 | locY = self._parent.winfo_y() 139 | else: # center to 'screen' 140 | parentwidth = self._parent.winfo_screenwidth() 141 | parentheight = self._parent.winfo_screenheight() 142 | locX = 0 143 | locY = 0 144 | width = self._window.winfo_width() 145 | height = self._window.winfo_height() 146 | x = locX + parentwidth/2 - width / 2 147 | y = locY + parentheight / 2 - height / 2 148 | self._window.geometry('%dx%d+%d+%d' % (width,height,x,y)) 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /Source/Exposure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # Exposure.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | 25 | try: 26 | import ttk 27 | from ttk import * 28 | except ImportError: 29 | from tkinter import ttk 30 | from tkinter.ttk import * 31 | 32 | from PiCameraApp import * 33 | from Dialog import * 34 | from Mapping import * 35 | from NotePage import * 36 | from Utils import * 37 | 38 | class Exposure ( BasicNotepage ): 39 | def BuildPage ( self ): 40 | f = ttk.Frame(self) 41 | f.grid(row=0,column=0,sticky='NSEW') 42 | f.columnconfigure(1,weight=1) 43 | 44 | #------------------- Metering Mode -------------------- 45 | l = Label(f,text='Metering mode:') 46 | l.grid(row=0,column=0,sticky='W',pady=5) 47 | self.MeteringModeCombo = Combobox(f,state='readonly',width=20) 48 | self.MeteringModeCombo.grid(row=0,column=1,columnspan=3,sticky='W') 49 | l = list(self.camera.METER_MODES.keys()) 50 | l.sort() 51 | self.MeteringModeCombo['values'] = l 52 | self.MeteringModeCombo.current(0) 53 | self.MeteringModeCombo.bind('<>',self.MeteringModeChanged) 54 | ToolTip(self.MeteringModeCombo,200) 55 | 56 | #------------------- Exposure Mode -------------------- 57 | self.ExposureModeText = None 58 | f = ttk.LabelFrame(self,text='Exposure mode (Equivalent film ISO)',padding=(5,5,5,5)) 59 | f.grid(row=1,column=0,sticky='NSW',pady=5) # was 4, columnspan=3, 60 | #f.columnconfigure(1,weight=1) 61 | 62 | self.ExposureModeVar = MyStringVar('auto') 63 | self.AutoExposureRadio = MyRadio(f,'Full auto (Default)','auto', 64 | self.ExposureModeVar, 65 | self.ExposureModeButton,0,0,'W',tip=205) 66 | MyRadio(f,'Preset exposures:','set',self.ExposureModeVar, 67 | self.ExposureModeButton,1,0,'W',tip=206) 68 | MyRadio(f,'Manually set ISO:','iso',self.ExposureModeVar, 69 | self.ExposureModeButton,2,0,'W',tip=207) 70 | MyRadio(f,'Off (Gains set at current value)','off',self.ExposureModeVar, 71 | self.ExposureModeButton,3,0,'W',span=2,tip=208) #was 2 72 | 73 | self.ExpModeCombo = Combobox(f,state='readonly',width=10) 74 | ToolTip(self.ExpModeCombo,209) 75 | self.ExpModeCombo.grid(row=1,column=1,sticky='W') #sticky='EW') 76 | self.ExpModeCombo.bind('<>',self.ExpModeChanged) 77 | exp = list(self.camera.EXPOSURE_MODES.keys()) 78 | exp.remove('off') # these two are handled by radio buttons 79 | exp.remove('auto') 80 | exp.sort() #cmp=lambda x,y: cmp(x.lower(),y.lower())) 81 | self.ExpModeCombo['values'] = exp 82 | self.ExpModeCombo.current(1) 83 | 84 | self.IsoCombo = Combobox(f,state='readonly',width=10) 85 | ToolTip(self.IsoCombo,210) 86 | self.IsoCombo.grid(row=2,column=1,sticky='W') #sticky='EW') 87 | self.IsoCombo.bind('<>',self.IsoChanged) 88 | self.IsoCombo['values'] = [100,200,320,400,500,640,800] 89 | self.IsoCombo.current(3) 90 | 91 | Separator(f,orient=HORIZONTAL).grid(pady=5,row=4,column=0, 92 | columnspan=2,sticky='EW') # was 3 93 | 94 | f1 = ttk.Frame(f) 95 | f1.grid(row=5,column=0,sticky='NS',columnspan=2) # was 2 96 | l = Label(f1,text='Analog gain:').grid(row=0,column=0,sticky='W') 97 | self.AnalogGain = ttk.Label(f1,style='DataLabel.TLabel') 98 | self.AnalogGain.grid(row=0,column=1,sticky=W,pady=2,padx=5) 99 | ToolTip(self.AnalogGain,211) 100 | l = Label(f1,text='Digital gain:').grid(row=0,column=2,sticky='W') 101 | self.DigitalGain = ttk.Label(f1,style='DataLabel.TLabel') 102 | self.DigitalGain.grid(row=0,column=3,sticky=W,pady=2,padx=5) 103 | ToolTip(self.DigitalGain,212) 104 | l = Label(f1,text='Actual ISO:').grid(row=1,column=0,sticky='W') 105 | self.EffIso = ttk.Label(f1,style='DataLabel.TLabel') 106 | self.EffIso.grid(row=1,column=1,sticky=W,pady=2,padx=5) 107 | ToolTip(self.EffIso,213) 108 | l = Label(f1,text='Apparent ISO:').grid(row=1,column=2,sticky='W') 109 | self.MeasIso = ttk.Label(f1,style='DataLabel.TLabel') 110 | self.MeasIso.grid(row=1,column=3,sticky=W,pady=2,padx=5) 111 | ToolTip(self.MeasIso,214) 112 | 113 | # -------------- Right frame --------------- 114 | f = ttk.LabelFrame(self,text='Exposure Compensation / DRC', 115 | padding=(5,5,5,5),width=150) 116 | f.grid(row=1,column=2,sticky='NS',pady=5) # was 4, columnspan=3, 117 | b = MyBooleanVar(True) 118 | self.AutoExposureRadio = MyRadio(f,'None (Default)',True,b, 119 | self.ExposureCompButton,0,0,'W',span=2,tip=231) 120 | MyRadio(f,'Amount:',False,b, 121 | self.ExposureCompButton,1,0,'W',tip=232) 122 | self.fstop = ttk.Label(f,width=16,padding=(5,5,5,5),style='DataLabel.TLabel') 123 | self.fstop.grid(row=2,column=1,sticky='W') 124 | self.ExpCompSlider = ttk.Scale(f,from_=-25,to=25,length=100, 125 | command=self.ExpComboSliderChanged,orient='horizontal') 126 | self.ExpCompSlider.grid(row=1,column=1,sticky='EW',pady=5) 127 | self.ExpCompSlider.set(0) 128 | ToolTip(self.ExpCompSlider,230) 129 | 130 | Separator(f,orient=HORIZONTAL).grid(pady=5,row=3,column=0, 131 | columnspan=2,sticky='EW') # was 3 132 | 133 | l = Label(f,text='Dynamic Range Compression') \ 134 | .grid(row=4,column=0,sticky='W',columnspan=2) 135 | b = MyBooleanVar(False) 136 | self.DisableDRCRadio = MyRadio(f,'Disabled (Default)',False,b, 137 | self.DrcChecked,5,0,'W',pad=(0,5,10,5),tip=233,span=2) 138 | MyRadio(f,'Enabled',True,b,self.DrcChecked,6,0,'W', 139 | pad=(0,5,10,5),tip=234) 140 | self.DrcCombo = Combobox(f,state='readonly',width=5) 141 | self.DrcCombo.grid(row=6,column=1,sticky='EW') 142 | ToolTip(self.DrcCombo,235) 143 | vals = self.camera.DRC_STRENGTHS 144 | vals = list(vals.keys()) 145 | vals.remove('off') # Handled by radio button 146 | self.DrcCombo['values'] = vals 147 | self.DrcCombo.current(0) 148 | self.DrcCombo.bind('<>',self.DrcStrengthChanged) 149 | 150 | #------------------- Auto White Balance -------------------- 151 | f = ttk.LabelFrame(self,text='Auto white balance settings',padding=(5,5,5,5)) 152 | f.grid(row=2,column=0,columnspan=5,sticky='NEWS',pady=5) 153 | #f.columnconfigure(2,weight=1) 154 | #f.columnconfigure(4,weight=1) 155 | 156 | self.AWBText = None 157 | self.AutoAWB = MyStringVar('auto') 158 | self.AWBRadio = MyRadio(f,'Auto','auto',self.AutoAWB,self.AutoAWBChecked, 159 | 0,0,'NW',tip=250) 160 | MyRadio(f,'Select:','sel',self.AutoAWB,self.AutoAWBChecked,1,0,'NW',tip=251) 161 | MyRadio(f,'Off','off',self.AutoAWB,self.AutoAWBChecked,2,0,'NW',tip=252) 162 | 163 | Label(f,text='Default').grid(row=0,column=1,sticky='E') 164 | Label(f,text='Mode:').grid(row=1,column=1,sticky='E',pady=5) 165 | self.awb = Combobox(f,state='readonly',width=12) 166 | ToolTip(self.awb,253) 167 | self.awb.grid(row=1,column=2,columnspan=1,sticky='W') 168 | self.awb.bind('<>',self.AWBModeChanged) 169 | modes = list(self.camera.AWB_MODES.keys()) 170 | modes.sort() #cmp=lambda x,y: cmp(x.lower(),y.lower())) 171 | modes.remove('off') # these two are handled by the radiobuttons 172 | modes.remove('auto') 173 | self.awb['values'] = modes 174 | self.awb.current(0) 175 | 176 | okCmd = (self.register(self.ValidateGains),'%P') 177 | Label(f,text='Red gain:').grid(row=2,column=1,sticky=E) 178 | self.RedGain = StringVar() 179 | self.RedEntry = Entry(f,textvariable=self.RedGain,width=10, 180 | validate='all',validatecommand=okCmd) 181 | self.RedEntry.grid(row=2,column=2,sticky='W') 182 | ToolTip(self.RedEntry,254) 183 | 184 | Label(f,text='Blue gain:').grid(row=2,column=3,sticky=W) 185 | self.BlueGain = StringVar() 186 | self.BlueEntry = Entry(f,textvariable=self.BlueGain,width=10, 187 | validate='all',validatecommand=okCmd) 188 | self.BlueEntry.grid(row=2,column=4,sticky='W') 189 | ToolTip(self.BlueEntry,255) 190 | 191 | #------------------- Shutter Speed -------------------- 192 | self.ShutterSpeedText = None 193 | self.Multiplier = 1 194 | f = ttk.LabelFrame(self,text='Shutter speed',padding=(5,5,5,5)) 195 | f.grid(row=3,column=0,columnspan=4,sticky='NEWS',pady=5) 196 | #f.columnconfigure(2,weight=1) 197 | self.ShutterSpeedAuto = MyBooleanVar(True) 198 | self.AutoShutterRadio = MyRadio(f,'Auto (Default)',True,self.ShutterSpeedAuto, 199 | self.ShutterSpeedButton,0,0,'W',span=2,tip=300) 200 | l = Label(f,text='Current Exposure:') 201 | l.grid(row=0,column=1,sticky=W,pady=5) 202 | self.ExposureSpeed = ttk.Label(f,style='DataLabel.TLabel') 203 | self.ExposureSpeed.grid(row=0,column=2,sticky=W) 204 | ToolTip(self.ExposureSpeed,301) 205 | MyRadio(f,'Set shutter speed:',False,self.ShutterSpeedAuto, 206 | self.ShutterSpeedButton,1,0,'W',tip=302) 207 | okCmd = (self.register(self.ValidateShutterSpeed),'%P') 208 | self.ShutterSpeed = StringVar() 209 | self.ShutterSpeedEntry = Entry(f,textvariable=self.ShutterSpeed,width=7, 210 | validate='all',validatecommand=okCmd) 211 | self.ShutterSpeedEntry.grid(row=1,column=1,sticky='EW') 212 | ToolTip(self.ShutterSpeedEntry,303) 213 | self.ShutterSpeedCombo = Combobox(f,state='readonly',width=6) 214 | self.ShutterSpeedCombo.grid(row=1,column=2,columnspan=1,sticky='W') 215 | self.ShutterSpeedCombo['values'] = ['usec','msec','sec'] 216 | self.ShutterSpeedCombo.current(0) 217 | self.ShutterSpeedCombo.bind('<>',self.ShutterSpeedComboChanged) 218 | ToolTip(self.ShutterSpeedCombo,304) 219 | self.SlowestShutterSpeed = ttk.Label(f,style='RedMessage.TLabel') 220 | self.SlowestShutterSpeed.grid(row=2,column=0,columnspan=4,sticky='W') 221 | 222 | #------------------- Frame Rate -------------------- 223 | self.FPSText = None 224 | f = MyLabelFrame(self,'Frame rate',4,0,span=4) 225 | #f.columnconfigure(2,weight=1) 226 | 227 | l = Label(f,text='Current frame rate:').grid(row=0,column=0,sticky='W') 228 | self.FrameRate = ttk.Label(f,style='DataLabel.TLabel') 229 | self.FrameRate.grid(row=0,column=1,columnspan=3,sticky='W') 230 | ToolTip(self.FrameRate,310) 231 | 232 | self.FixedFrameRateBool = MyBooleanVar(True) 233 | self.ffr = MyRadio(f,'Fixed frame rate:',True,self.FixedFrameRateBool, 234 | self.FixedFrameRateChecked,1,0,'W',tip=311) 235 | okCmd = (self.register(self.ValidateFixedRange),'%P') 236 | self.FixedFramerateText = MyStringVar("30.0") 237 | self.FixedFramerateEntry = Entry(f,width=6,validate='all', 238 | validatecommand=okCmd,textvariable=self.FixedFramerateText) 239 | self.FixedFramerateEntry.grid(row=1,column=1,sticky='W') 240 | ToolTip(self.FixedFramerateEntry,312) 241 | l = Label(f,text='FPS').grid(row=1,column=2,sticky='W') 242 | 243 | Label(f,text='Delta:').grid(row=1,column=3,sticky='E',padx=(5,0)) 244 | okCmd = (self.register(self.ValidateFramerateDelta),'%P') 245 | self.FramerateDeltaText = MyStringVar("0.0") 246 | self.FramerateDelta = Entry(f,width=6,validate='all', 247 | validatecommand=okCmd,textvariable=self.FramerateDeltaText) 248 | self.FramerateDelta.grid(row=1,column=4,sticky='W') 249 | ToolTip(self.FramerateDelta,315) 250 | Label(f,text='FPS').grid(row=1,column=5,sticky='W') 251 | 252 | MyRadio(f,'Frame rate range:',False, 253 | self.FixedFrameRateBool, 254 | self.FixedFrameRateChecked,2,0,'W',tip=313) 255 | #Label(f,text='Frame rate range:').grid(row=2,column=0,sticky='W') 256 | ok1Cmd = (self.register(self.ValidateFramerateRangeFrom),'%P') 257 | self.FramerateRangeFromText = MyStringVar("1/6") 258 | self.FramerateFrom = Entry(f,width=6,validate='all', 259 | textvariable=self.FramerateRangeFromText) 260 | self.FramerateFrom.grid(row=2,column=1,sticky='W') 261 | ToolTip(self.FramerateFrom,314) 262 | Label(f,text='FPS').grid(row=2,column=2,sticky='W') 263 | Label(f,text='To:').grid(row=2,column=3,sticky='E') 264 | self.FramerateRangeToText = MyStringVar("30.0") 265 | ok2Cmd = (self.register(self.ValidateFramerateRangeTo),'%P') 266 | self.FramerateTo = Entry(f,width=6,validate='all', 267 | validatecommand=ok2Cmd,textvariable=self.FramerateRangeToText) 268 | self.FramerateTo.grid(row=2,column=4,sticky='W') 269 | ToolTip(self.FramerateTo,316) 270 | l = Label(f,text='FPS').grid(row=2,column=5,sticky='W') 271 | 272 | self.FramerateFrom.config(validatecommand=ok1Cmd) 273 | 274 | self.AutoExposureRadio.invoke() 275 | self.DrcChecked(False) 276 | self.CheckGains() 277 | self.ExposureModeButton('auto') 278 | self.AutoAWBChecked('auto') 279 | self.ShowAWBGains() 280 | self.AutoShutterRadio.invoke() 281 | self.ExpModeCombo.focus_set() 282 | self.ffr.invoke() 283 | self.UpdateFrameRate() 284 | 285 | #### TODO: Implement Reset IN WORK 286 | def Reset ( self ): 287 | self.MeteringModeCombo.current(0) 288 | self.ExposureModeVar.set('auto') 289 | self.ExposureModeButton('auto') # invoke not working here?? 290 | self.AWBRadio.invoke() 291 | self.AutoShutterRadio.invoke() 292 | self.ExpCompSlider.set(0) 293 | self.ShutterSpeedCombo.current(0) 294 | self.MeteringModeCombo.focus_set() 295 | self.DisableDRCRadio.invoke() 296 | self.DisableDRCRadio.focus_set() 297 | self.FixedFramerateText.set("30") 298 | self.FramerateDeltaText.set("0") 299 | self.FramerateRangeFromText.set("1/6") 300 | self.FramerateRangeToText.set("30") 301 | self.ffr.invoke() 302 | def SetVariables ( self, ExposureModeText, AWBText, 303 | ShutterSpeedText, FPSText): 304 | self.ExposureModeText = ExposureModeText 305 | self.AWBText = AWBText 306 | self.ShutterSpeedText = ShutterSpeedText 307 | self.FPSText = FPSText 308 | def MeteringModeChanged ( self, event ): 309 | self.camera.meter_mode = self.MeteringModeCombo.get() 310 | def ExposureModeButton ( self, ExposureMode ): 311 | self.ExpCompSlider.state(['!disabled']) 312 | if ExposureMode == 'auto' or ExposureMode == 'off': 313 | self.ExpModeCombo.config(state='disabled') 314 | self.IsoCombo.config(state='disabled') 315 | self.camera.iso = 0 # auto ISO 316 | if ExposureMode == 'off': 317 | self.ExpCompSlider.state(['disabled']) 318 | self.camera.exposure_mode = ExposureMode 319 | elif ExposureMode == 'set': 320 | self.camera.iso = 0 # auto ISO 321 | self.ExpModeCombo.config(state='readonly') 322 | self.ExpModeCombo.focus_set() 323 | self.IsoCombo.config(state='disabled') 324 | self.ExpModeChanged(None) 325 | else: # mode = 'iso' 326 | self.ExpModeCombo.config(state='disabled') 327 | self.IsoCombo.config(state='readonly') 328 | self.IsoCombo.focus_set() 329 | self.IsoChanged(None) 330 | def ExpModeChanged ( self, event ): 331 | self.camera.exposure_mode = self.ExpModeCombo.get() 332 | def IsoChanged ( self, event ): 333 | val = int(self.IsoCombo.get()) 334 | self.camera.iso = val 335 | def CheckGains ( self ): 336 | ag = self.camera.analog_gain 337 | dg = self.camera.digital_gain 338 | self.AnalogGain.config(text= '%.3f' % ag) 339 | self.DigitalGain.config(text= '%.3f' % dg) 340 | if self.ExposureModeText: 341 | self.ExposureModeText.set('AG: %.3f DG: %.3f' % (ag, dg)) 342 | if not dg == 0: 343 | self.EffIso.config(text='%d' % int(ag / dg * 100.0)) 344 | else: 345 | self.EffIso.config(text="Unknown! Digital Gain is 0") 346 | self.MeasIso.config(text= \ 347 | 'Auto' if self.camera.iso == 0 else str(self.camera.iso) ) 348 | self.after(300,self.CheckGains) 349 | def ValidateGains ( self, EntryIfAllowed ): 350 | if EntryIfAllowed == '' or EntryIfAllowed == '.': 351 | val = 0.0 # special cases handled here 352 | else: 353 | try: val = float(EntryIfAllowed) 354 | except: val = -1.0 355 | return val >= 0.0 and val <= 8.0 356 | def UpdateGains ( self ): 357 | def ToFloat ( val ): return float(0.0 if val == '' or val == '.' else val) 358 | self.camera.awb_gains = (ToFloat(self.RedEntry.get()), 359 | ToFloat(self.BlueEntry.get())) 360 | def DrcChecked ( self, DrcEnabled ): 361 | if DrcEnabled == False: 362 | self.camera.drc_strength = 'off' 363 | self.DrcCombo.config(state = 'disabled') 364 | else: 365 | self.DrcStrengthChanged(None) 366 | self.DrcCombo.config(state = 'readonly') 367 | self.DrcCombo.focus_set() 368 | def DrcStrengthChanged ( self, event ): 369 | self.camera.drc_strength = self.DrcCombo.get() 370 | def AutoAWBChecked ( self, AwbMode ): 371 | if AwbMode == 'auto' or AwbMode == 'sel': 372 | self.camera.awb_mode = 'auto' if AwbMode == 'auto' else self.awb.get() 373 | self.ShowAWBGains() 374 | if AwbMode == 'sel': self.awb.focus_set() 375 | else: # 'off' 376 | gains = self.camera.awb_gains 377 | self.camera.awb_mode = 'off' 378 | self.RedGain.set('%.3f' % gains[0]) 379 | self.RedEntry.focus_set() 380 | self.BlueGain.set('%.3f' % gains[1]) 381 | self.camera.awb_gains = gains 382 | self.awb.config(state='readonly' if AwbMode == 'sel' else 'disabled') 383 | self.RedEntry.config(state='normal' if AwbMode == 'off' else 'disabled') 384 | self.BlueEntry.config(state='normal' if AwbMode == 'off' else 'disabled') 385 | def ShowAWBGains ( self ): 386 | if not self.AutoAWB.get() == 'off': 387 | gains = self.camera.awb_gains 388 | self.RedGain.set('%.3f' % gains[0]) 389 | self.BlueGain.set('%.3f' % gains[1]) 390 | if self.AWBText: 391 | self.AWBText.set('RG: %.3f BG: %.3f' % (gains[0],gains[1])) 392 | else: 393 | self.UpdateGains() 394 | self.after(300,self.ShowAWBGains) 395 | def AWBModeChanged ( self, event ): 396 | self.camera.awb_mode = self.awb.get() 397 | def ExposureCompButton ( self, val ): 398 | if val is True: 399 | self.ExpCompSlider.state(['disabled']) 400 | self.camera.exposure_compensation = 0 401 | else: 402 | self.ExpCompSlider.state(['!disabled']) 403 | self.ExpCompSlider.focus_set() 404 | self.camera.exposure_compensation = int(self.ExpCompSlider.get()) 405 | def ExpComboSliderChanged ( self, newVal ): 406 | val = float(newVal) 407 | if val == 0.0: 408 | self.fstop.config(text = 'None (Default)') 409 | else: 410 | self.fstop.config(text = '%s %.2f fstops' % ( 411 | 'Close' if val < 0.0 else 'Open', abs(val) / 6.0) ) 412 | self.camera.exposure_compensation = int(val) 413 | self.ExpCompSlider.focus_set() 414 | def ShutterSpeedButton ( self, val ): 415 | if self.ShutterSpeedAuto.get() == True: 416 | self.camera.shutter_speed = 0 417 | self.ShutterSpeedEntry.config(state='disabled') 418 | self.ShutterSpeedCombo.state(['disabled']) 419 | self.ShutterSpeedCombo.current(0) 420 | self.after(300,self.CheckShutterSpeed) 421 | else: 422 | self.camera.shutter_speed = int(self.ShutterSpeed.get()) 423 | self.ShutterSpeedEntry.config(state='normal') #'!disabled') 424 | self.ShutterSpeedCombo.state(['!disabled']) 425 | self.ShutterSpeedEntry.focus_set() 426 | def CheckShutterSpeed ( self ): 427 | val = self.camera.exposure_speed 428 | txt = USECtoSec(val) 429 | self.ExposureSpeed.config(text=txt) 430 | self.ShutterSpeed.set(str(val)) 431 | if self.ShutterSpeedText: 432 | self.ShutterSpeedText.set('ES: '+txt) 433 | if self.ShutterSpeedAuto.get() is True: 434 | self.after(300,self.CheckShutterSpeed) 435 | def ValidateShutterSpeed ( self, EntryIfAllowed ): 436 | if self.ShutterSpeedAuto.get() is True: 437 | return True 438 | try: val = int(self.Multiplier * float(EntryIfAllowed)) 439 | except: val = -1 440 | if self.camera.framerate == 0: 441 | r = self.camera.framerate_range 442 | ul = int(1.0e6 / r[0]) # Lower limit on range 443 | ll = 1 # int(1.0e6 / r[1]) 444 | else: 445 | ul = int(1.0e6 / (self.camera.framerate + self.camera.framerate_delta)) 446 | ll = 1 # 1 usec is the fastest speed 447 | if val >= ll and val <= ul: 448 | self.camera.shutter_speed = val 449 | txt = USECtoSec(val) 450 | self.ExposureSpeed.config(text=txt) 451 | if self.ShutterSpeedText: 452 | self.ShutterSpeedText.set('ES: '+txt) 453 | return True 454 | def ShutterSpeedComboChanged (self, event ): 455 | self.Multiplier = int(pow(10,3 * self.ShutterSpeedCombo.current())) 456 | self.ValidateShutterSpeed(self.ShutterSpeedEntry.get()) 457 | def FixedFrameRateChecked ( self, val ): 458 | if val is True: # Fixed frame rate 459 | state = 'normal' 460 | state1 = 'disabled' 461 | self.FixedFramerateEntry.focus_set() 462 | self.ValidateFixedRange(self.FixedFramerateText.get()) 463 | else: # Frame rate range 464 | state = 'disabled' 465 | state1 = 'normal' 466 | self.ValidateFramerateRangeFrom(self.FramerateRangeFromText.get()) 467 | self.ValidateFramerateRangeTo(self.FramerateRangeToText.get()) 468 | self.FramerateFrom.focus_set() 469 | self.FixedFramerateEntry.config(state=state) 470 | self.FramerateDelta.config(state=state) 471 | self.FramerateFrom.config(state=state1) 472 | self.FramerateTo.config(state=state1) 473 | def ValidateEntry ( self, entry, minVal, maxVal ): 474 | ''' 475 | Change how the edit fields work. Allow a '/' in the edit field to 476 | denote a fraction. 477 | Split on '/' and evaluate each side. Note, if ' /' then 478 | num is evalulated since 'den' is empty 479 | ''' 480 | vals = entry.split('/',1) # entry is text 481 | val = vals[0].strip() 482 | try: num = float(val) 483 | except: num = None 484 | else: 485 | if len(vals) > 1: 486 | val = vals[1].strip() 487 | if val: 488 | try: den = float(val) 489 | except: num = None 490 | else: 491 | if den > 0: num = num / den 492 | else: num = None 493 | if num is not None: 494 | num = num if num >= minVal and num <= maxVal else None 495 | self.FrameRate.config(style='RedMessage.TLabel' if num is None \ 496 | else 'DataLabel.TLabel') 497 | return num 498 | def ValidateFixedRange ( self, EntryIfAllowed ): 499 | rate = self.ValidateEntry(EntryIfAllowed, 1.0/6.0, 90.0 ) 500 | if rate != None: 501 | self.camera.framerate = rate 502 | self.ValidateShutterSpeed(None) 503 | return True 504 | def ValidateRange ( self, fromText, toText ): 505 | fromVal = self.ValidateEntry(fromText, 1.0/6.0, 90.0 ) 506 | toVal = self.ValidateEntry(toText, 1.0/6.0, 90.0 ) 507 | if fromVal != None and toVal != None and \ 508 | fromVal >= 1.0/6.0 and fromVal < toVal and toVal <= 90.0: 509 | self.camera.framerate_range = (fromVal,toVal) 510 | self.ValidateShutterSpeed(None) 511 | self.FrameRate.config(style='DataLabel.TLabel') 512 | else: 513 | self.FrameRate.config(style='RedMessage.TLabel') 514 | def ValidateFramerateRangeFrom ( self, text ): 515 | self.ValidateRange(text,self.FramerateRangeToText.get()) 516 | return True 517 | def ValidateFramerateRangeTo ( self, text ): 518 | self.ValidateRange(self.FramerateRangeFromText.get(),text) 519 | return True 520 | def ValidateFramerateDelta ( self, text ): 521 | # Can delta be negative - YEP!! 522 | delta = self.ValidateEntry(text, -10.0, 10.0 ) 523 | if delta != None: 524 | self.camera.framerate_delta = delta 525 | self.ValidateShutterSpeed(None) 526 | return True 527 | def UpdateFrameRate ( self ): 528 | if self.camera.framerate == 0: 529 | r = self.camera.framerate_range 530 | txt = 'Auto fps' 531 | txt1 = '%.3f to %.3f fps' % (r[0],r[1]) 532 | t = USECtoSec(int(1.0e6 / r[0])) 533 | else: 534 | r = self.camera.framerate + self.camera.framerate_delta 535 | txt = '%.3f fps' % r 536 | txt1 = txt 537 | t = USECtoSec(int(1.0e6 / r)) 538 | self.FrameRate.config(text=txt1) 539 | if self.FPSText: self.FPSText.set(txt) 540 | self.SlowestShutterSpeed.config(text='Slowest shutter speed: %s' % t) 541 | self.after(300,self.UpdateFrameRate) 542 | -------------------------------------------------------------------------------- /Source/FinerControl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # FinerControl.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | 25 | from Dialog import * 26 | from Mapping import * 27 | from NotePage import * 28 | from Utils import * 29 | 30 | class FinerControl ( BasicNotepage ): 31 | def BuildPage ( self ): 32 | #--------------- Color Effects --------------- 33 | f = MyLabelFrame(self,'Color effects (Luminance and Chrominance - YUV420)', 34 | 2,0,span=4) 35 | f.columnconfigure(1,weight=1) 36 | b = MyBooleanVar(False) 37 | self.NoColorEffectsRadio = MyRadio(f,'No added color effect (Default)', 38 | False,b,self.AddColorEffect,0,0,'W',span=2,tip=370) 39 | MyRadio(f,'Add color effect',True,b,self.AddColorEffect,1,0,'W',tip=371) 40 | # Added Luminance (Brightness) 41 | self.lLabel = ttk.Label(f,text='Luminance:',padding=(20,2,0,2)) 42 | self.lLabel.grid(row=2,column=0,sticky='W') 43 | self.lScale = ttk.Scale(f,from_=0,to=100, \ 44 | orient='horizontal',length=100) 45 | self.lScale.grid(row=2,column=1,sticky='W',pady=2) 46 | self.lScale.set(self.camera.brightness) 47 | self.lScale.config(command=self.lValueChanged) 48 | ToolTip(self.lScale,372) 49 | self.uLabel = ttk.Label(f,text='U chrominance:',padding=(20,2,0,2)) 50 | self.uLabel.grid(row=3,column=0,sticky='W') 51 | self.uScale = ttk.Scale(f,from_=0,to=255, \ 52 | orient='horizontal',length=100) 53 | self.uScale.grid(row=3,column=1,sticky='W',pady=2) 54 | self.uScale.set(128) 55 | ToolTip(self.uScale,373) 56 | self.uScale.config(command=self.uValueChanged) 57 | self.vLabel = ttk.Label(f,text='V chrominance:',padding=(20,2,0,2)) 58 | self.vLabel.grid(row=4,column=0,sticky='W') 59 | self.vScale = ttk.Scale(f,from_=0,to=255, \ 60 | orient='horizontal',length=100) 61 | self.vScale.grid(row=4,column=1,sticky='W',pady=2) 62 | self.vScale.set(128) 63 | self.vScale.config(command=self.vValueChanged) 64 | ToolTip(self.vScale,374) 65 | self.YUV = ttk.Label(f,width=20,style='DataLabel.TLabel') 66 | self.YUV.grid(row=5,column=0,sticky='W') 67 | ToolTip(self.YUV,375) 68 | self.RGB = ttk.Label(f,style='DataLabel.TLabel') 69 | self.RGB.grid(row=5,column=1,columnspan=2,sticky='W') 70 | ToolTip(self.RGB,376) 71 | 72 | self.Color = Canvas(f,width=10,height=32) 73 | self.Color.grid(row=6,column=0,columnspan=2,sticky="EW") 74 | self.colorbg = self.Color.cget('background') 75 | ToolTip(self.Color,377) 76 | 77 | #--------------- Sensor Mode --------------- 78 | f = MyLabelFrame(self,'Sensor mode',3,0,span=4) 79 | f.columnconfigure(1,weight=1) 80 | # See PiCamera documentation - nothing happens unless the camera 81 | # is already initialized to a value other than 0 (Auto) 82 | l = ttk.Label(f,text='Sensor mode changes may not work! Some bugs', 83 | style='RedMessage.TLabel') 84 | l.grid(row=3,column=0,columnspan=2,sticky='W') 85 | 86 | b = MyBooleanVar(True) 87 | # Select input mode based of Resolution and framerate:', 88 | self.SensorModeAutoRadio = MyRadio(f,'Auto (Default mode 0)', 89 | True,b,self.AutoSensorModeRadio,1,0,'NW',span=2,tip=350) 90 | MyRadio(f,'Select Mode:',False,b, 91 | self.AutoSensorModeRadio,2,0,'NW',tip=351) 92 | self.SensorModeCombo = Combobox(f,state='disabled',width=35) 93 | self.SensorModeCombo.grid(row=2,column=1,sticky='W') 94 | self.SensorModeCombo['values'] = [ \ 95 | 'Mode 1: to 1920x1080 1-30 fps ', 96 | 'Mode 2: to 2592x1944 1-15 fps Image', 97 | 'Mode 3: to 2592x1944 0.1666-1 fps Image', 98 | 'Mode 4: to 1296x972 1-42 fps', 99 | 'Mode 5: to 1296x730 1-49 fps', 100 | 'Mode 6: to 640x480 42.1-60 fps', 101 | 'Mode 7: to 640x480 60.1-90 fps', 102 | ] 103 | self.SensorModeCombo.current(0) 104 | self.SensorModeCombo.bind('<>',self.SensorModeChanged) 105 | ToolTip(self.SensorModeCombo,352) 106 | 107 | f = MyLabelFrame(self,'Clock Mode',4,0,span=4) 108 | b = MyStringVar('reset') 109 | self.ClockReset = MyRadio(f,'Reset (default)', 110 | 'reset',b,self.ClockResetRadio,0,0,'W',tip=360) 111 | MyRadio(f,'Raw','raw',b,self.ClockResetRadio,0,1,'W',tip=361) 112 | 113 | f = MyLabelFrame(self,'Timestamp',5,0) 114 | ttk.Label(f,text="Current Timestamp:").grid(row=0,column=0,sticky='W') 115 | self.Timestamp = MyStringVar(int(self.camera.timestamp)) 116 | l = ttk.Label(f,textvariable=self.Timestamp,foreground='#0000FF') 117 | l.grid(row=0,column=1,sticky='W') 118 | ToolTip(l,362) 119 | 120 | f = MyLabelFrame(self,'Still Stats',6,0) 121 | self.StillStats = MyBooleanVar(self.camera.still_stats) 122 | MyRadio(f,'Off (default)', 123 | False,b,self.StillStatsChanged,0,0,'NW',tip=363) 124 | MyRadio(f,'On',True,b,self.StillStatsChanged,0,1,'NW',tip=364) 125 | 126 | self.AddColorEffect(False) 127 | self.UpdateTimestamp() 128 | 129 | #### TODO: Implement Reset - IN WORK 130 | def Reset ( self ): 131 | self.lScale.set(50) 132 | self.uScale.set(128) 133 | self.vScale.set(128) 134 | self.NoColorEffectsRadio.invoke() 135 | self.SensorModeAutoRadio.invoke() 136 | def PassControlFrame ( self, BasicControlsFrame ): 137 | self.BasicControlsFrame = BasicControlsFrame 138 | def AddColorEffect ( self, EnableColorEffect ): 139 | if EnableColorEffect == True: 140 | self.uvValueChanged() 141 | s = '!disabled' 142 | self.Color.grid() # show them 143 | self.YUV.grid() 144 | self.RGB.grid() 145 | self.lScale.focus_set() 146 | else: 147 | self.camera.color_effects = None 148 | s = 'disabled' 149 | self.Color.grid_remove() # hide them 150 | self.YUV.grid_remove() 151 | self.RGB.grid_remove() 152 | self.uScale.state([s]) # why is this different? 153 | self.vScale.state([s]) 154 | self.lScale.state([s]) 155 | def uvValueChanged ( self ): 156 | def Clamp ( color ): 157 | return 0 if color <= 0 else 255 if color >= 255 else int(color) 158 | y = int(255 * float(self.camera.brightness) / 100.0) 159 | u = int(self.uScale.get()) 160 | v = int(self.vScale.get()) 161 | self.camera.color_effects = (u,v) 162 | # Y'UV420 to RGB - see Wikipedia - conversion for Android 163 | red = Clamp(y + 1.370705 * (v-128)) 164 | green = Clamp(y - 0.337633 * (u-128) - 0.698001 * (v-128)) 165 | blue = Clamp(y + 1.732446 * (u-128)) 166 | self.YUV.config(text='Y: %03d U: %03d V: %03d' % (y,u,v)) 167 | self.RGB.config(text='R: %03d G: %03d B: %03d' % (red, green, blue)) 168 | self.Color.config(background='#%02x%02x%02x' % (red,green,blue)) 169 | def lValueChanged ( self, newVal ): 170 | self.lScale.focus_set() 171 | self.camera.brightness = (int)((float)(newVal)) 172 | self.BasicControlsFrame.brightness.set(newVal) 173 | self.uvValueChanged() 174 | def uValueChanged ( self, newVal ): 175 | self.uScale.focus_set() 176 | self.uvValueChanged() 177 | def vValueChanged ( self, newVal ): 178 | self.vScale.focus_set() 179 | self.uvValueChanged() 180 | def AutoSensorModeRadio ( self, AutoSensor ): 181 | if AutoSensor == True: 182 | # Bug in the firmware. Cannot seem to get back to mode 0 183 | # after changing to any other mode! 184 | self.camera.sensor_mode = 0 185 | self.camera.sensor_mode = 0 # doesn't work! 186 | self.SensorModeCombo.config(state='disabled') 187 | else: 188 | self.SensorModeCombo.config(state='readonly') 189 | self.SensorModeCombo.focus_set() 190 | self.SensorModeChanged(None) 191 | def SensorModeChanged ( self, event ): 192 | mode = int(self.SensorModeCombo.current()) + 1 193 | self.camera.sensor_mode = mode 194 | def ClockResetRadio ( self, val ): 195 | print (val) 196 | self.camera.clock_mode = val 197 | def UpdateTimestamp ( self ): 198 | self.Timestamp.set(int(self.camera.timestamp)) 199 | self.after(1000,self.UpdateTimestamp) 200 | def StillStatsChanged ( self, val ): 201 | print(val) 202 | self.camera.still_stats = val 203 | 204 | 205 | -------------------------------------------------------------------------------- /Source/ImageEffects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # ImageEffects.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | 25 | try: 26 | from Tkinter import * 27 | except ImportError: 28 | from tkinter import * 29 | try: 30 | from tkColorChooser import askcolor 31 | except ImportError: 32 | from tkinter.colorchooser import askcolor 33 | try: 34 | import tkFileDialog as FileDialog 35 | except ImportError: 36 | import tkinter.filedialog as FileDialog 37 | try: 38 | import tkMessageBox as MessageBox 39 | except ImportError: 40 | import tkinter.messagebox as MessageBox 41 | try: 42 | import ttk 43 | from ttk import * 44 | except ImportError: 45 | from tkinter import ttk 46 | #from ttk import * 47 | try: 48 | import tkFont 49 | except ImportError: 50 | import tkinter.font 51 | 52 | import datetime as dt 53 | from Dialog import * 54 | from Mapping import * 55 | from NotePage import * 56 | from Utils import * 57 | from Tooltip import * 58 | from NotePage import BasicNotepage 59 | 60 | try: 61 | import picamera 62 | from picamera import * 63 | import picamera.array 64 | except ImportError: 65 | raise ImportError("You do not seem to have picamera installed") 66 | 67 | ''' 68 | When queried, the image_effect_params property either returns None 69 | (for effects which have no configurable parameters, or if no parameters 70 | have been configured), or a tuple of numeric values up to six elements long. 71 | 72 | When set, the property changes the parameters of the current effect as 73 | a sequence of numbers, or a single number. Attempting to set parameters 74 | on an effect which does not support parameters, or providing an 75 | incompatible set of parameters for an effect will raise a 76 | PiCameraValueError exception. 77 | 78 | The effects which have parameters, and what combinations those 79 | parameters can take is as follows: 80 | 81 | Effect Parameters Description 82 | 'solarize' yuv, x0, y1, y2, y3 yuv controls whether data is processed 83 | as RGB (0) or YUV(1). Input values from 0 to x0 - 1 are 84 | remapped linearly onto the range 0 to y0. Values from x0 to 85 | 255 are remapped linearly onto the range y1 to y2. 86 | x0, y0, y1, y2 Same as above, but yuv defaults to 0 87 | (process as RGB). 88 | yuv Same as above, but x0, y0, y1, y2 89 | default to 128, 128, 128, 0 respectively. 90 | 91 | 'colorpoint' quadrant quadrant specifies which quadrant of the U/V 92 | space to retain chroma from: 0=green, 1=red/yellow, 93 | 2=blue, 3=purple. There is no default; this effect does 94 | nothing until parameters are set. 95 | 96 | 'colorbalance' lens, r, g, b, u, v lens specifies the lens shading 97 | strength (0.0 to 256.0, where 0.0 indicates lens shading 98 | has no effect). r, g, b are multipliers for their 99 | respective color channels (0.0 to 256.0). u and v are 100 | offsets added to the U/V plane (0 to 255). 101 | lens, r, g, b Same as above but u are defaulted to 0. 102 | lens, r, b Same as above but g also defaults to to 1.0. 103 | 104 | 'colorswap' dir If dir is 0, swap RGB to BGR. If dir is 1, swap RGB to BRG. 105 | 106 | 'posterise' steps Control the quantization steps for the image. 107 | Valid values are 2 to 32, and the default is 4. 108 | 109 | 'blur' size Specifies the size of the kernel. Valid values are 1 or 2. 110 | 111 | 'film' strength, u, v strength specifies the strength of effect. 112 | u and v are offsets added to the U/V plane (0 to 255). 113 | 114 | 'watercolor' u, v u and v specify offsets to add to the U/V plane (0 to 255). 115 | No parameters indicates no U/V effect. 116 | ''' 117 | 118 | class ImageEffectsDialog ( Dialog ): 119 | def BuildDialog ( self ): 120 | n = Notebook(self.MainFrame,padding=(5,5,5,5)) 121 | n.grid(row=0,column=0,sticky='NSEW') 122 | n.columnconfigure(0,weight=1) 123 | n.rowconfigure(0,weight=1) 124 | 125 | self.Effects1 = Effects1Page(n,camera=self._camera,cancel=self.CancelButton) 126 | self.Effects2 = Effects2Page(n,camera=self._camera,cancel=self.CancelButton) 127 | 128 | n.add(self.Effects1,text='Effects Page 1',underline=0) 129 | n.add(self.Effects2,text='Effects Page 2',underline=0) 130 | 131 | def OkPressed ( self ): 132 | self.Effects1.SaveChanges() 133 | self.Effects2.SaveChanges() 134 | return True 135 | 136 | def CancelPressed ( self ): 137 | return MessageBox.askyesno("Effects Parameters","Exit without saving changes?") 138 | 139 | class Effects1Page ( BasicNotepage ): 140 | # -1 means I haven't done anything with the values yet... 141 | EffectParam = { 'posterise' : 4, 'blur' : 1, 'colorpoint' : 0, 142 | 'colorswap' : 0, 'solarize' : -1, 'colorbalance' : -1, 143 | 'film' : -1, 'watercolor' : -1 } 144 | @staticmethod 145 | # Called if Reset Camera is clicked 146 | def Reset (): 147 | EffectParam = { 'posterise' : 4, 'blur' : 1, 'colorpoint' : 0, 148 | 'colorswap' : 0, 'solarize' : -1, 'colorbalance' : -1, 149 | 'film' : -1, 'watercolor' : -1 } 150 | def BuildPage ( self ): 151 | Label(self,text="Blur kernel size:").grid(row=0,column=0,sticky='W',pady=5); 152 | self.BlurAmt = ttk.Label(self,text="%d" % Effects1Page.EffectParam['blur'], 153 | style='DataLabel.TLabel') 154 | self.BlurAmt.grid(row=0,column=2,sticky='W') 155 | ToolTip(self.BlurAmt,4011) 156 | self.BlurKernelSize = ttk.Scale(self,from_=1,to=2,orient='horizontal', 157 | command=self.BlurKernelSizeChanged) 158 | self.BlurKernelSize.grid(row=0,column=1,sticky='W',pady=5) 159 | self.BlurKernelSize.set(Effects1Page.EffectParam['blur']) 160 | ToolTip(self.BlurKernelSize,4010) 161 | 162 | Label(self,text="Colorbalance:").grid(row=1,column=0,sticky='W',pady=5); 163 | Label(self,text="IN WORK").grid(row=1,column=1,sticky='W',pady=5); 164 | 165 | Label(self,text="Colorpoint U/V quadrant:").grid(row=2,column=0,sticky='W'); 166 | self.Quadrant = Combobox(self,state='readonly',width=15) 167 | self.Quadrant.bind('<>',self.QuadrantChanged) 168 | self.Quadrant.grid(row=2,column=1,sticky='W',columnspan=2) 169 | self.QuadrantList = ['Green','Red/Yellow','Blue','Purple'] 170 | self.Quadrant['values'] = self.QuadrantList 171 | self.Quadrant.current(Effects1Page.EffectParam['colorpoint']) 172 | ToolTip(self.Quadrant,4020) 173 | 174 | Label(self,text="Colorswap direction:").grid(row=3,column=0,sticky='W',pady=5); 175 | self.Direction = Combobox(self,state='readonly',width=15) 176 | self.Direction.bind('<>',self.DirectionChanged) 177 | self.Direction.grid(row=3,column=1,sticky='W',columnspan=2) 178 | self.DirectionList = ['Swap RGB to BGR', 'Swap BGR to RGB'] 179 | self.Direction['values'] = self.DirectionList 180 | self.Direction.current(Effects1Page.EffectParam['colorswap']) 181 | ToolTip(self.Direction,4030) 182 | 183 | Label(self,text="Film:").grid(row=4,column=0,sticky='W',pady=5); 184 | Label(self,text="IN WORK").grid(row=4,column=1,sticky='W',pady=5); 185 | 186 | Label(self,text="Posterize quantization steps:").grid(row=5,column=0,sticky='W',pady=5); 187 | self.PosterizeAmt = ttk.Label(self,text="%d" % Effects1Page.EffectParam['posterise'], 188 | style='DataLabel.TLabel') 189 | self.PosterizeAmt.grid(row=5,column=2,sticky='W') 190 | ToolTip(self.PosterizeAmt,4001) 191 | self.Posterize = ttk.Scale(self,from_=2,to=32,orient='horizontal', 192 | command=self.PosterizeChanged) 193 | self.Posterize.grid(row=5,column=1,sticky='W',pady=5) 194 | self.Posterize.set(Effects1Page.EffectParam['posterise']) 195 | ToolTip(self.Posterize,4000) 196 | 197 | Label(self,text="Solarize:").grid(row=6,column=0,sticky='W',pady=5); 198 | Label(self,text="IN WORK").grid(row=6,column=1,sticky='W',pady=5); 199 | 200 | Label(self,text="Watercolor:").grid(row=7,column=0,sticky='W',pady=5); 201 | Label(self,text="IN WORK").grid(row=7,column=1,sticky='W',pady=5); 202 | 203 | def PosterizeChanged ( self, val ): 204 | if self.camera.image_effect == 'posterise': 205 | self.camera.image_effect_params = ( int(float(val)) ) 206 | Effects1Page.EffectParam['posterise'] = int(float(val)) 207 | self.PosterizeAmt.config(text="%d" % Effects1Page.EffectParam['posterise']) 208 | def BlurKernelSizeChanged ( self, val ): 209 | if self.camera.image_effect == 'blur': 210 | self.camera.image_effect_params = ( int(float(val)) ) 211 | Effects1Page.EffectParam['blur'] = int(float(val)) 212 | self.BlurAmt.config(text="%d" % Effects1Page.EffectParam['blur']) 213 | def QuadrantChanged ( self, val ): 214 | if self.camera.image_effect == 'colorpoint': 215 | self.camera.image_effect_params = ( self.Quadrant.current() ) 216 | Effects1Page.EffectParam['colorpoint'] = self.Quadrant.current() 217 | def DirectionChanged ( self, val ): 218 | if self.camera.image_effect == 'colorswap': 219 | self.camera.image_effect_params = ( self.Direction.current() ) 220 | Effects1Page.EffectParam['colorswap'] = self.Direction.current() 221 | def SaveChanges ( self ): 222 | pass 223 | 224 | class Effects2Page ( BasicNotepage ): 225 | @staticmethod 226 | # Called if Reset Camera is clicked 227 | def Reset (): 228 | pass 229 | def BuildPage ( self ): 230 | Label(self,text="NOTHING HERE YET!!").grid(row=0,column=0,sticky='W'); 231 | def SaveChanges ( self ): 232 | pass 233 | -------------------------------------------------------------------------------- /Source/KeyboardShortcuts.py: -------------------------------------------------------------------------------- 1 | ''' 2 | KeyboardShortcuts.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | try: 16 | from Tkinter import * 17 | except ImportError: 18 | from tkinter import * 19 | try: 20 | from tkColorChooser import askcolor 21 | except ImportError: 22 | from tkinter.colorchooser import askcolor 23 | try: 24 | import tkFileDialog 25 | except ImportError: 26 | import tkinter.filedialog 27 | try: 28 | import tkMessageBox 29 | except ImportError: 30 | import tkinter.messagebox 31 | try: 32 | import ttk 33 | from ttk import * 34 | except ImportError: 35 | from tkinter import ttk 36 | #from ttk import * 37 | try: 38 | import tkFont 39 | from tkFont import Font 40 | except ImportError: 41 | import tkinter.font 42 | from tkinter.font import Font 43 | 44 | from Dialog import * 45 | 46 | import PIL 47 | from PIL import Image, ImageTk, ExifTags 48 | 49 | from Utils import EvenOdd 50 | 51 | # 52 | # Display formatted textbox of supported keyboard shortcuts 53 | # 54 | class KeyboardShortcutsDialog ( Dialog ): 55 | def BuildDialog ( self ): 56 | def AddCmdKey ( text ): 57 | bg = EvenOdd(self.even) 58 | strs = text.split(':') 59 | self.text.insert(END,strs[0],("KeyboardCommand","Indent",bg)) 60 | strs = strs[1].split('~') 61 | self.text.insert(END,strs[0],bg) 62 | if len(strs) > 1: 63 | self.text.insert(END,strs[1][0],("CmdKey",bg)) 64 | self.text.insert(END,strs[1][1:],(bg)) 65 | self.even = not self.even 66 | 67 | self.MainFrame.rowconfigure(0,weight=1) 68 | self.MainFrame.columnconfigure(0,weight=1) 69 | 70 | self.iconKeyboard = ImageTk.PhotoImage(PIL.Image.open("Assets/keyboard.gif")) 71 | Label(self.MainFrame,image=self.iconKeyboard).grid(row=0,column=0,sticky='W') 72 | 73 | self.sb = Scrollbar(self.MainFrame,orient='vertical') 74 | self.sb.grid(row=1,column=1,sticky='NS') 75 | self.text = Text(self.MainFrame,height=30,width=60,wrap='word', 76 | yscrollcommand=self.sb.set) 77 | self.text.grid(row=1,column=0,sticky='NSEW') 78 | self.text.bind("",lambda e : "break") # ignore all keypress 79 | self.sb.config(command=self.text.yview) 80 | 81 | ## Tags for various text formats 82 | boldFont = Font(self.text,self.text.cget("font")) 83 | boldFont.configure(weight="bold") 84 | boldUnderlineFont = Font(self.text,self.text.cget("font")) 85 | boldUnderlineFont.configure(weight="bold",underline=True) 86 | self.text.tag_configure("Bold",font=boldFont) 87 | self.text.tag_configure("Title",font=("Arial",14,"bold")) 88 | self.text.tag_configure("KeyboardCommand",font=boldFont,foreground='blue') 89 | self.text.tag_configure("Center",justify = CENTER) 90 | self.text.tag_configure("Indent",lmargin1='1c',lmargin2='1c') 91 | self.text.tag_configure("CmdKey",font=boldUnderlineFont) 92 | self.text.tag_configure("odd",background='white') 93 | self.text.tag_configure("even",background='#f0f0ff') 94 | self.even = True 95 | 96 | self.text.insert(END,"\nKeyboard shortcuts\n\n",("Center","Title")) 97 | 98 | self.text.insert(END,"Photo viewer pane\n\n","Bold") 99 | self.even = True 100 | AddCmdKey("LeftMouse:\t\t\t\tPan image\n") 101 | AddCmdKey("Ctrl+MouseWheel Up/Down:\t\t\t\tZoom in/out\n") 102 | AddCmdKey("Ctrl+LeftMouse:\t\t\t\tShow zoomed area on preview\n") 103 | 104 | self.text.insert(END,"\nNotepad traversal shorcuts\n\n","Bold") 105 | self.even = True 106 | AddCmdKey("Ctrl+TAB:\t\tSelects the next tab.\n") 107 | AddCmdKey("Ctrl+SHIFT+TAB:\t\t\tSelects the previous tab.\n") 108 | AddCmdKey("ALT+B:\t\tSelect ~Basic commands tab\n") 109 | AddCmdKey("ALT+E:\t\tSelect ~Exposure commands tab\n") 110 | AddCmdKey("ALT+C:\t\tSelect Finer ~Control commands tab\n") 111 | AddCmdKey("ALT+A:\t\tSelect ~Annotate/EXIF Metadata commands tab\n") 112 | AddCmdKey("ALT+T:\t\tSelect ~Time lapse commands tab\n") 113 | 114 | self.text.insert(END,"\nPhoto / Video commands\n\n","Bold") 115 | self.even = True 116 | AddCmdKey("Right Click:\tPopup menu\n") 117 | AddCmdKey("Ctrl+P:\t\tTake ~picture\n") 118 | AddCmdKey("Ctrl+V:\t\tToggle capture ~video on/off\n") 119 | AddCmdKey("Ctrl+C:\t\t~Clear picture or video in pane\n") 120 | AddCmdKey("Ctrl+R:\t\t~Reset all camera setups to defaults\n") 121 | 122 | self.text.insert(END,"\nUser Interface\n\n","Bold") 123 | self.even = True 124 | AddCmdKey("TAB:\t\tSelects the next active control.\n") 125 | AddCmdKey("Shift+TAB:\t\tSelects the previous active control.\n") 126 | AddCmdKey("Alt+F:\t\tDrop down ~File menu\n") 127 | AddCmdKey("Alt+V:\t\tDrop down ~View menu\n") 128 | AddCmdKey("Alt+P:\t\tDrop down ~Photo menu\n") 129 | AddCmdKey("Alt+H:\t\tDrop down ~Help menu\n") 130 | AddCmdKey("Ctrl+Shift+C:\t\tShow/hide ~Cursors\n") 131 | AddCmdKey("Ctrl+Shift+A:\t\tShow/hide Image ~Attributes pane\n") 132 | AddCmdKey("Ctrl+Shift+P:\t\tShow/hide ~Preview pane\n") 133 | 134 | self.text.insert(END,"\nGeneral commands\n\n","Bold") 135 | self.even = True 136 | AddCmdKey("Ctrl+Q:\t\t~Quit program\n") 137 | -------------------------------------------------------------------------------- /Source/Mapping.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Mapping.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | try: 16 | from Tkinter import * 17 | except ImportError: 18 | from tkinter import * 19 | try: 20 | from tkColorChooser import askcolor 21 | except ImportError: 22 | from tkinter.colorchooser import askcolor 23 | try: 24 | import tkFileDialog as FileDialog 25 | except ImportError: 26 | import tkinter.filedialog as FileDialog 27 | try: 28 | import tkMessageBox as MessageBox 29 | except ImportError: 30 | import tkinter.messagebox as MessageBox 31 | try: 32 | import ttk 33 | from ttk import * 34 | except ImportError: 35 | from tkinter import ttk 36 | from tkinter.ttk import * 37 | try: 38 | import tkFont as Font 39 | except ImportError: 40 | import tkinter.font as Font 41 | 42 | # 43 | # colors : https://wiki.tcl.tk/37701 44 | # 45 | 46 | class ControlMapping ( ): 47 | def __init__ ( self ): 48 | self.FocusColor = '#f0f0ff' 49 | self.NoFocusMouseOverColor = 'lightyellow' 50 | self.NoFocusNoMouseOverColor = 'white' 51 | self.SetControlMapping() 52 | def SetControlMapping ( self ): 53 | #Style().configure('.', font=('Helvetica', 12)) # all 54 | Style().configure('RedMessage.TLabel',font=('Arial',10,"italic"),foreground='red') 55 | # These dont work since they're overriden by the map later on... ??? 56 | Style().configure('Error.TEntry',background='red',foreground='white') 57 | Style().configure('OK.TEntry',background='white',foreground='black') 58 | Style().configure('DataLabel.TLabel',foreground='blue',font=('Arial',10)) 59 | Style().configure('StatusBar.TLabel',background=self.FocusColor,relief=SUNKEN) 60 | Style().configure('TMenu',background='white',activeforeground='lightblue') 61 | ''' 62 | Can't seem to get a foucs highlight around some of the controls. 63 | So I'm using color changes to show focus/tab stops. 64 | Also, the Scale does not seem to get focus by clicking on it 65 | Need to force focus when the user clicks on it 66 | ''' 67 | Style().map('TPanedwindow', 68 | background = [ 69 | ('!active','#f0f0ff'), 70 | ], 71 | ) 72 | Style().map('TCombobox', 73 | fieldbackground = [ 74 | ('focus','!disabled',self.FocusColor), 75 | ('!focus','active','!disabled',self.NoFocusMouseOverColor), 76 | ('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 77 | #('disabled','lightgray'), # Use foreground 78 | ], 79 | foreground = [ 80 | ('disabled','gray'), 81 | ('!disabled', 'black') 82 | ], 83 | selectbackground = [ 84 | ('focus','!disabled',self.FocusColor), 85 | ('!focus','active','!disabled',self.NoFocusMouseOverColor), 86 | ('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 87 | #('disabled','lightgray'), 88 | ], 89 | selectforeground = [ 90 | ('!focus','black'), 91 | ('readonly','black'), 92 | ('focus','black'), 93 | ('disabled','black'), 94 | ('!disabled', 'black') 95 | ], 96 | ) # close map 97 | 98 | Style().map('TEntry',# This one is just for 'look and feel' 99 | fieldbackground = [ 100 | ('focus','!disabled', '!invalid' ,self.FocusColor), 101 | ('!focus','active','!disabled','!invalid', self.NoFocusMouseOverColor), 102 | ('!focus','!active','!disabled','!invalid',self.NoFocusNoMouseOverColor), 103 | ('invalid', '#FF0000') 104 | #('disabled','lightgray'), 105 | ], 106 | foreground = [ 107 | ('disabled', '!invalid', 'gray'), 108 | ('!disabled', '!invalid', 'black') 109 | #('invalid', self.NoFocusNoMouseOverColor) 110 | ], 111 | #background = [ 112 | #('focus','!disabled',self.FocusColor), 113 | #('!focus','active','!disabled',self.NoFocusMouseOverColor'), 114 | #('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 115 | ##('disabled','lightgray'), 116 | #], 117 | #selectbackground = [ 118 | #('focus','!disabled',self.FocusColor), 119 | #('!focus','active','!disabled',self.NoFocusMouseOverColor), 120 | #('!focus','!active','!disabled',self.NoFocusNoMouseOverColor). 121 | ##('disabled','lightgray'), 122 | #], 123 | selectforeground = [ 124 | ('!focus','black'), 125 | ('focus','white'), 126 | ], 127 | ) # close map 128 | 129 | #Style().map('TMenubutton',# This one is just for 'look and feel' 130 | #fieldbackground = [ 131 | #('focus','!disabled',self.FocusColor), 132 | #('!focus','active','!disabled',self.NoFocusMouseOverColor), 133 | #('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 134 | ##('disabled','lightgray'), 135 | #], 136 | #foreground = [ 137 | #('disabled','gray'), 138 | #('!disabled', 'black') 139 | #], 140 | #background = [ 141 | #('focus','!disabled',self.FocusColor), 142 | #('!focus','active','!disabled',self.NoFocusMouseOverColor), 143 | #('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 144 | ##('disabled','lightgray'), 145 | #], 146 | #selectbackground = [ 147 | #('focus','!disabled',self.FocusColor), 148 | #('!focus','active','!disabled',self.NoFocusMouseOverColor), 149 | #('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 150 | ##('disabled','lightgray'), 151 | #], 152 | #selectforeground = [ 153 | #('!focus','black'), 154 | #('focus','white'), 155 | #], 156 | #) # close map 157 | 158 | Style().map("Horizontal.TScale", 159 | troughcolor = [ 160 | ('focus','!disabled',self.FocusColor), 161 | ('!focus','active','!disabled',self.NoFocusMouseOverColor), 162 | ('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 163 | ('disabled','lightgray'), 164 | ], 165 | ) # close map 166 | 167 | Style().map("Vertical.TScale", 168 | troughcolor = [ 169 | ('focus','!disabled',self.FocusColor), 170 | ('!focus','active','!disabled',self.NoFocusMouseOverColor), 171 | ('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 172 | ('disabled','lightgray'), 173 | ], 174 | ) # close map 175 | 176 | Style().map("TMenu", 177 | background = [ 178 | ('focus','!disabled',self.FocusColor), 179 | ('!focus','active','!disabled',self.NoFocusMouseOverColor), 180 | ('!focus','!active','!disabled',self.NoFocusNoMouseOverColor), 181 | ('disabled','lightgray'), 182 | ], 183 | ) # close map 184 | 185 | -------------------------------------------------------------------------------- /Source/NotePage.py: -------------------------------------------------------------------------------- 1 | ''' 2 | NotePage.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | 16 | try: 17 | from Tkinter import * 18 | except ImportError: 19 | from tkinter import * 20 | 21 | try: 22 | from tkColorChooser import askcolor 23 | except ImportError: 24 | from tkinter.colorchooser import askcolor 25 | try: 26 | import tkFileDialog 27 | except ImportError: 28 | import tkinter.filedialog 29 | try: 30 | import tkMessageBox as MessageBox 31 | except ImportError: 32 | import tkinter.messagebox as MessageBox 33 | try: 34 | import ttk 35 | from ttk import * 36 | except ImportError: 37 | from tkinter import ttk 38 | #from ttk import * 39 | try: 40 | import tkFont 41 | except ImportError: 42 | import tkinter.font 43 | 44 | from Utils import UnderConstruction 45 | 46 | # 47 | # Base Class for all NotePad pages. 48 | # 49 | class BasicNotepage ( Frame ): 50 | def __init__(self, parent, camera=None, cancel=None, ok=None, 51 | rowconfig=False, colconfig=True, data = None ): 52 | ttk.Frame.__init__(self, parent,padding=(10,10,10,10)) 53 | self.grid(sticky='NSEW') 54 | if colconfig is True: 55 | self.columnconfigure(0,weight=1) 56 | if rowconfig is True: 57 | self.rowconfigure(0,weight=1) 58 | self.camera = camera 59 | self.parent = parent 60 | self.CancelButton = cancel 61 | self.OkButton = ok 62 | self.data = data 63 | self.init = True # disable SomethingChanged 64 | self.BuildPage() 65 | self.init = False 66 | self.Changed = False 67 | def BuildPage ( self ): # MUST Overide this! 68 | UnderConstruction(self) 69 | def SomethingChanged ( self, val ): # Can override but call super! 70 | if self.init: return 71 | self.Changed = True 72 | if self.CancelButton != None: 73 | self.CancelButton.config(state='normal') #'!disabled') 74 | if self.OkButton != None: 75 | self.OkButton.config(text='Save') 76 | def SaveChanges ( self ): # MUST override this! 77 | MessageBox.showwarning("SaveChanges", "SaveChanges not implemented!") 78 | -------------------------------------------------------------------------------- /Source/PhotoParams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # PhotoParams.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | try: 25 | from Tkinter import * 26 | except ImportError: 27 | from tkinter import * 28 | try: 29 | from tkColorChooser import askcolor 30 | except ImportError: 31 | from tkinter.colorchooser import askcolor 32 | try: 33 | import tkFileDialog 34 | except ImportError: 35 | import tkinter.filedialog 36 | try: 37 | import tkMessageBox as MessageBox 38 | except ImportError: 39 | import tkinter.messagebox as MessageBox 40 | try: 41 | import ttk 42 | from ttk import * 43 | except ImportError: 44 | from tkinter import ttk 45 | #from ttk import * 46 | try: 47 | import tkFont 48 | except ImportError: 49 | import tkinter.font 50 | 51 | from Dialog import * 52 | from Mapping import * 53 | from NotePage import * 54 | from Utils import * 55 | from Tooltip import * 56 | 57 | # Handle all Photo parameters 58 | ''' 59 | Certain file formats accept additional options which can be specified as 60 | keyword arguments. Currently, only the 'jpeg' encoder accepts additional 61 | options, which are: 62 | 63 | quality Defines the quality of the JPEG encoder as an integer ranging 64 | from 1 to 100. Defaults to 85. Please note that JPEG quality is not a 65 | percentage and definitions of quality vary widely. 66 | restart Defines the restart interval for the JPEG encoder as a number 67 | of JPEG MCUs. The actual restart interval used will be a multiple of the 68 | number of MCUs per row in the resulting image. 69 | thumbnail Defines the size and quality of the thumbnail to embed in 70 | the Exif metadata. Specifying None disables thumbnail generation. 71 | Otherwise, specify a tuple of (width, height, quality). 72 | Defaults to (64, 48, 35). 73 | bayer If True, the raw bayer data from the camera’s sensor is included 74 | in the Exif metadata. 75 | ''' 76 | 77 | class PhotoParamsDialog ( Dialog ): 78 | def BuildDialog ( self ): 79 | n = Notebook(self.MainFrame,padding=(5,5,5,5)) 80 | n.grid(row=0,column=0,sticky='NSEW') 81 | #n.columnconfigure(0,weight=1) 82 | #n.rowconfigure(0,weight=1) 83 | 84 | self.JPEGpage = JPEG(n,cancel=self.CancelButton,ok=self.OkButton) 85 | self.OtherFormatspage = OtherFormats(n,cancel=self.CancelButton, 86 | ok=self.OkButton) 87 | 88 | n.add(self.JPEGpage,text='JPEG',underline=0) 89 | n.add(self.OtherFormatspage,text='Other formats',underline=0) 90 | 91 | def OkPressed ( self ): 92 | self.JPEGpage.SaveChanges() 93 | self.OtherFormatspage.SaveChanges() 94 | return True 95 | 96 | def CancelPressed ( self ): 97 | return MessageBox.askyesno("Photo Params","Exit without saving changes?") 98 | 99 | class JPEG ( BasicNotepage ): 100 | Quality = 85 101 | Restart = None 102 | Thumbnail = (64, 48, 40) # (width, height, quality) or None 103 | Bayer = False 104 | IncludeEXIF = True 105 | UserComment = "" 106 | @staticmethod 107 | # Called if Reset Camera is clicked 108 | def Reset (): 109 | Quality = 85 110 | Restart = None 111 | Thumbnail = (64, 48, 40) # or None or (width, height, quality) 112 | Bayer = False 113 | IncludeEXIF = True 114 | UserComment = "" 115 | def BuildPage ( self ): 116 | frame = MyLabelFrame(self, "JPEG Parameters", 0, 0) 117 | Label(frame,text="Quality:").grid(row=0,column=0,sticky='W'); 118 | self.Quality = ttk.Scale(frame,from_=1,to=100,orient='horizontal') 119 | self.Quality.grid(row=0,column=1,sticky='W',pady=5) 120 | self.Quality.set(JPEG.Quality) 121 | ToolTip(self.Quality,2000) 122 | 123 | self.QualityAmt = IntVar() 124 | self.QualityAmt.set(JPEG.Quality) 125 | l = ttk.Label(frame,textvariable=self.QualityAmt,style='DataLabel.TLabel') 126 | l.grid(row=0,column=2,sticky='W') 127 | ToolTip(l,2001) 128 | # NOW enable callback - else we get an error on self.QualityAmt 129 | self.Quality.config(command=self.QualityChanged) 130 | 131 | Label(frame,text="Restart:").grid(row=1,column=0,sticky='W'); 132 | l = Label(frame,text="Unclear what to do here!") 133 | l.grid(row=1,column=1,columnspan=2,sticky='W'); 134 | ToolTip(l,2010) 135 | 136 | f = ttk.Frame(frame) 137 | f.grid(row=2,column=0,columnspan=3,sticky='EW') 138 | 139 | Label(f,text="Thumbnail:").grid(row=0,column=0,sticky='W'); 140 | self.ThumbnailNone = MyBooleanVar(JPEG.Thumbnail == None) 141 | r = MyRadio(f,"None",True,self.ThumbnailNone,self.ThumbnailChanged, 142 | 0,1,'W',tip=2015) 143 | MyRadio(f,"Set size/quality",False,self.ThumbnailNone,self.ThumbnailChanged, 144 | 0,2,'W',tip=2020) 145 | 146 | self.ThumbnailEditFrame = ttk.Frame(f,padding=(15,0,0,0)) 147 | self.ThumbnailEditFrame.grid(row=1,column=0,columnspan=3,sticky='EW') 148 | Label(self.ThumbnailEditFrame,text="Width:").grid(row=0,column=0,sticky='E'); 149 | self.WidthCombo = Combobox(self.ThumbnailEditFrame,state='readonly',width=3) 150 | self.WidthCombo.bind('<>',self.SomethingChanged) 151 | self.WidthCombo.grid(row=0,column=1,sticky='W') 152 | self.WidthComboList = [16,32,48,64,80,96,112,128] 153 | self.WidthCombo['values'] = self.WidthComboList 154 | if JPEG.Thumbnail is None: 155 | self.WidthCombo.current(0) 156 | else: 157 | self.WidthCombo.current(int(JPEG.Thumbnail[0]/16) - 1) 158 | ToolTip(self.WidthCombo,2021) 159 | 160 | ttk.Label(self.ThumbnailEditFrame,text="Height:",padding=(5,0,0,0)).grid(row=0,column=2,sticky='E'); 161 | self.HeightCombo = Combobox(self.ThumbnailEditFrame,state='readonly',width=3) 162 | self.HeightCombo.bind('<>',self.SomethingChanged) 163 | self.HeightCombo.grid(row=0,column=3,sticky='W') 164 | self.HeightCombo['values'] = self.WidthComboList 165 | if JPEG.Thumbnail is None: 166 | self.HeightCombo.current(0) 167 | else: 168 | self.HeightCombo.current(int(JPEG.Thumbnail[1]/16) - 1) 169 | ToolTip(self.HeightCombo,2022) 170 | 171 | ttk.Label(self.ThumbnailEditFrame,text="Quality:",padding=(5,0,0,0)).grid(row=0,column=4,sticky='E'); 172 | self.QualityCombo = ttk.Combobox(self.ThumbnailEditFrame,state='readonly',width=3) 173 | self.QualityCombo.bind('<>',self.SomethingChanged) 174 | self.QualityCombo.grid(row=0,column=5,sticky='W') 175 | self.QualityComboList = [30,40,50,60,70,80,90] 176 | self.QualityCombo['values'] = self.QualityComboList 177 | if JPEG.Thumbnail is None: 178 | self.QualityCombo.current(0) 179 | else: 180 | self.QualityCombo.current(int((JPEG.Thumbnail[2]-30) / 10)) 181 | ToolTip(self.QualityCombo,2023) 182 | 183 | Label(f,text="Bayer:").grid(row=2,column=0,sticky='W'); 184 | self.Bayer = MyBooleanVar(JPEG.Bayer) 185 | MyRadio(f,"Off (default)",False,self.Bayer,self.SomethingChanged, 186 | 2,1,'W',tip=2030) 187 | MyRadio(f,"On",True,self.Bayer,self.SomethingChanged,2,2,'W', 188 | tip=2031) 189 | 190 | frame = MyLabelFrame(self, "EXIF Metadata", 1, 0) 191 | self.AddExifBoolean = MyBooleanVar(JPEG.IncludeEXIF) 192 | self.AddExifButton = ttk.Checkbutton(frame, 193 | text='Add EXIF metadata when saving photo', 194 | variable=self.AddExifBoolean, 195 | command=lambda e=None:self.SomethingChanged(e)) 196 | self.AddExifButton.grid(row=0,column=0,columnspan=2,sticky='W',padx=5) 197 | ToolTip(self.AddExifButton,msg=2100) 198 | 199 | Label(frame,text="Add a user comment to the EXIF metadata") \ 200 | .grid(row=1,column=0,sticky='W') 201 | self.AddCommentStr = MyStringVar(JPEG.UserComment) 202 | okCmd = (self.register(self.SomethingChanged),'%P') 203 | e = Entry(frame,textvariable=self.AddCommentStr,validate='key', 204 | validatecommand=okCmd) 205 | e.grid(row=2,column=0,columnspan=2,sticky='EW') 206 | ToolTip(e,msg=2110) 207 | 208 | self.ThumbnailChanged(None) 209 | def QualityChanged ( self, val ): 210 | self.QualityAmt.set(int(float(val))) 211 | self.SomethingChanged(None) 212 | def ThumbnailChanged ( self, val ): 213 | if self.ThumbnailNone.get() is True: 214 | self.ThumbnailEditFrame.grid_remove() 215 | else: 216 | self.ThumbnailEditFrame.grid() 217 | self.SomethingChanged(None) 218 | def SaveChanges ( self ): 219 | JPEG.Quality = self.QualityAmt.get() 220 | JPEG.Bayer = self.Bayer.get() 221 | JPEG.IncludeEXIF = self.AddExifBoolean.get() 222 | JPEG.UserComment = self.AddCommentStr.get() 223 | if self.ThumbnailNone.get() == True: 224 | JPEG.Thumbnail = None 225 | else: 226 | JPEG.Thumbnail = ( \ 227 | int(self.WidthCombo.get()) , 228 | int(self.HeightCombo.get()) , 229 | int(self.QualityCombo.get()) ) 230 | 231 | class OtherFormats ( BasicNotepage ): 232 | @staticmethod 233 | # Called if Reset Camera is clicked 234 | def Reset (): 235 | pass 236 | def SaveChanges ( self ): 237 | pass 238 | 239 | -------------------------------------------------------------------------------- /Source/PreferencesDialog.py: -------------------------------------------------------------------------------- 1 | ''' 2 | PreferencesDialog.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | import os 16 | import datetime 17 | import webbrowser # display the Picamera documentation 18 | try: 19 | from Tkinter import * 20 | except ImportError: 21 | from tkinter import * 22 | try: 23 | from tkColorChooser import askcolor 24 | except ImportError: 25 | from tkinter.colorchooser import askcolor 26 | try: 27 | import tkFileDialog as FileDialog 28 | except ImportError: 29 | import tkinter.filedialog as FileDialog 30 | try: 31 | import tkMessageBox 32 | except ImportError: 33 | import tkinter.messagebox 34 | try: 35 | import ttk 36 | from ttk import * 37 | except ImportError: 38 | from tkinter import ttk 39 | #from ttk import * 40 | try: 41 | import tkFont 42 | except ImportError: 43 | import tkinter.font 44 | 45 | from Dialog import * 46 | from Mapping import * 47 | from NotePage import * 48 | from Utils import * 49 | from Tooltip import * 50 | from PhotoParams import * 51 | from VideoParams import * 52 | 53 | try: 54 | import PIL 55 | from PIL import Image, ImageTk 56 | except ImportError: 57 | import PIL 58 | from PIL import Image #, ImageTk 59 | 60 | # 61 | # All PiCameraApp global preferences ae handled here 62 | # 63 | class PreferencesDialog ( Dialog ): 64 | # Static variables 65 | DefaultPhotoDir = "/home/pi/Pictures" 66 | DefaultVideoDir = "/home/pi/Videos" 67 | DefaultFilesDir = "/home/pi/Documents" 68 | DefaultPhotoFormat = 'jpeg' 69 | DefaultVideoFormat = 'h264' 70 | DefaultTimestampFormat = "%m-%d-%Y-%H:%M:%S" 71 | PhotoTimestamp = False 72 | VideoTimestamp = False 73 | @staticmethod 74 | # Called if Reset Camera is clicked 75 | def Reset (): 76 | DefaultPhotoDir = "/home/pi/Pictures" 77 | DefaultVideoDir = "/home/pi/Videos" 78 | DefaultFilesDir = "/home/pi/Documents" 79 | DefaultPhotoFormat = 'jpeg' 80 | DefaultVideoFormat = 'h264' 81 | DefaultTimestampFormat = "%m-%d-%Y-%H:%M:%S" 82 | PhotoTimestamp = False 83 | VideoTimestamp = False 84 | 85 | def BuildDialog ( self ): 86 | self.MainFrame.columnconfigure(0,weight=1) 87 | self.MainFrame.rowconfigure(0,weight=1) 88 | n = Notebook(self.MainFrame,padding=(5,5,5,5),width=30,height=200) 89 | n.grid(row=0,column=0,sticky='NEWS') 90 | 91 | self.GeneralPage = General(n,camera=self._camera,cancel=self.CancelButton,data=self) 92 | self.InterfacePage = Interface(n,camera=self._camera,cancel=self.CancelButton) 93 | self.OtherPage = Other(n,camera=self._camera,cancel=self.CancelButton) 94 | 95 | n.add(self.GeneralPage,text='General',underline=0) 96 | n.add(self.InterfacePage,text='Interface',underline=0) 97 | n.add(self.OtherPage,text='Other',underline=0) 98 | 99 | def OkPressed ( self ): 100 | #self.GeneralPage.SaveChanges() 101 | #self.InterfacePage.SaveChanges() 102 | #self.OtherPage.SaveChanges() 103 | return True 104 | 105 | def CancelPressed ( self ): 106 | if tkMessageBox.askyesno("PiCamera Preferences","Exit without saving changes?"): 107 | return True 108 | 109 | # Handle PiCameraApp General preferences 110 | class General ( BasicNotepage ): 111 | def BuildPage ( self ): 112 | # Setup default folder to save pictures and videos 113 | f = MyLabelFrame(self,'Set default directories',0,0) 114 | 115 | self.iconCameraBig = PIL.Image.open('Assets/camera-icon.png') 116 | self.iconCameraBig = ImageTk.PhotoImage(self.iconCameraBig.resize((22,22),Image.ANTIALIAS)) 117 | self.iconVideoBig = PIL.Image.open('Assets/video-icon-b.png') 118 | self.iconVideoBig = ImageTk.PhotoImage(self.iconVideoBig.resize((22,22),Image.ANTIALIAS)) 119 | self.iconFiles = PIL.Image.open('Assets/files.png') 120 | self.iconFiles = ImageTk.PhotoImage(self.iconFiles.resize((22,22),Image.ANTIALIAS)) 121 | 122 | b = ttk.Button(f,text="Photos...",image=self.iconCameraBig,compound='left', 123 | command=self.SelectPhotoDirectory,width=7) 124 | b.grid(row=0,column=0,sticky='W',pady=(5,5)) 125 | ToolTip(b,6000) 126 | self.PhotoDirLabel = Label(f,foreground='#0000FF', 127 | text=PreferencesDialog.DefaultPhotoDir,anchor=W) 128 | self.PhotoDirLabel.grid(row=0,column=1,sticky='EW',padx=10); 129 | ToolTip(self.PhotoDirLabel,6001) 130 | 131 | b = ttk.Button(f,text="Videos...",image=self.iconVideoBig,compound='left', 132 | command=self.SelectVideoDirectory,width=7) 133 | b.grid(row=1,column=0,sticky='W') 134 | ToolTip(b,6002) 135 | self.VideoDirLabel = Label(f,foreground='#0000FF', 136 | text=PreferencesDialog.DefaultVideoDir,anchor=W) 137 | self.VideoDirLabel.grid(row=1,column=1,sticky='EW',padx=10); 138 | ToolTip(self.VideoDirLabel,6003) 139 | 140 | b = ttk.Button(f,text="Files...",image=self.iconFiles,compound='left', 141 | command=self.SelectFilesDirectory,width=7) 142 | b.grid(row=2,column=0,sticky='W',pady=(5,5)) 143 | ToolTip(b,6004) 144 | self.FilesDirLabel = Label(f,foreground='#0000FF', 145 | text=PreferencesDialog.DefaultFilesDir,anchor=W) 146 | self.FilesDirLabel.grid(row=2,column=1,sticky='EW',padx=10); 147 | ToolTip(self.FilesDirLabel,6005) 148 | 149 | f = MyLabelFrame(self,'Photo/Video capture formats',1,0) 150 | 151 | ttk.Label(f,text='Photo capture format',padding=(5,5,5,5)) \ 152 | .grid(row=0,column=0,sticky='W') 153 | self.photoCaptureFormatCombo = Combobox(f,height=15,width=8, 154 | state='readonly')#,width=15) 155 | self.photoCaptureFormatCombo.grid(row=0,column=1,sticky='EW') 156 | self.photoFormats = ['jpeg','png','bmp', 157 | 'gif','yuv','rgb','rgba','bgr','bgra','raw'] 158 | self.photoCaptureFormatCombo['values'] = self.photoFormats 159 | self.photoCaptureFormatCombo.current( \ 160 | self.photoFormats.index(PreferencesDialog.DefaultPhotoFormat)) 161 | self.photoCaptureFormatCombo.bind('<>', 162 | self.photoCaptureFormatChanged) 163 | ToolTip(self.photoCaptureFormatCombo, msg=6010) 164 | self.ModFormatParams = ttk.Button(f,text='Params...', 165 | command=self.ModifyFormatParamPressed, 166 | underline=0,padding=(5,3,5,3),width=8) 167 | self.ModFormatParams.grid(row=0,column=2,sticky='W',padx=5) 168 | ToolTip(self.ModFormatParams, msg=6011) 169 | 170 | ttk.Label(f,text='Video capture format',padding=(5,5,5,5)) \ 171 | .grid(row=1,column=0,sticky='W') 172 | self.VideoCaptureFormatCombo = Combobox(f,height=15,width=8, 173 | state='readonly')#,width=15) 174 | self.VideoCaptureFormatCombo.grid(row=1,column=1,sticky='EW') 175 | self.videoFormats = ['h264','mjpeg','yuv', 176 | 'rgb','rgba','bgr','bgra'] 177 | self.VideoCaptureFormatCombo['values'] = self.videoFormats 178 | self.VideoCaptureFormatCombo.current( \ 179 | self.videoFormats.index(PreferencesDialog.DefaultVideoFormat)) 180 | self.VideoCaptureFormatCombo.bind('<>', 181 | self.VideoCaptureFormatChanged) 182 | ToolTip(self.VideoCaptureFormatCombo,6020) 183 | self.ModVideoFormatParams = ttk.Button(f,text='Params...', 184 | command=self.ModifyVideoFormatParamPressed, 185 | underline=0,padding=(5,3,5,3),width=8) 186 | self.ModVideoFormatParams.grid(row=1,column=2,sticky='W',padx=5) 187 | ToolTip(self.ModVideoFormatParams,6021) 188 | # Save / Restore camera settings? This may be a bit to do 189 | 190 | f = MyLabelFrame(self,'Photo/Video naming',2,0) 191 | Label(f,text='Timestamp format:') \ 192 | .grid(row=0,column=0,sticky='W') 193 | okCmd = (self.register(self.ValidateTimestamp),'%P') 194 | self.TimeStamp = MyStringVar(PreferencesDialog.DefaultTimestampFormat) 195 | e = Entry(f,width=20,validate='all', 196 | textvariable=self.TimeStamp) 197 | e.grid(row=0,column=1,sticky='W') 198 | ToolTip(e,6050) 199 | 200 | image = PIL.Image.open('Assets/help.png') 201 | self.helpimage = ImageTk.PhotoImage(image.resize((16,16))) 202 | b = ttk.Button(f,image=self.helpimage,width=10, 203 | command=self.FormatHelp,padding=(2,2,2,2)) 204 | b.grid(row=0,column=2,padx=5) 205 | ToolTip(b,6052) 206 | 207 | Label(f,text='Sample timestamp:').grid(row=1,column=0,sticky='W') 208 | self.TimestampLabel = MyStringVar(datetime.datetime.now() \ 209 | .strftime(PreferencesDialog.DefaultTimestampFormat)) 210 | self.tsl = Label(f,textvariable=self.TimestampLabel,foreground='#0000FF') 211 | self.tsl.grid(row=1,column=1,columnspan=2,sticky='W') 212 | ToolTip(self.tsl,6051) 213 | self.after(1000,self.UpdateTimestamp) 214 | 215 | self.PhotoTimestampVar = MyBooleanVar(PreferencesDialog.PhotoTimestamp) 216 | self.PhotoTimestamp = Checkbutton(f,text='Include timestamp in photo name', 217 | variable=self.PhotoTimestampVar, command=self.PhotoTimestampChecked) 218 | self.PhotoTimestamp.grid(row=2,column=0,columnspan=2,sticky='W') 219 | ToolTip(self.PhotoTimestamp,6060) 220 | 221 | self.VideoTimestampVar = MyBooleanVar(PreferencesDialog.VideoTimestamp) 222 | self.VideoTimestamp = Checkbutton(f,text='Include timestamp in video name', 223 | variable=self.VideoTimestampVar, command=self.VideoTimestampChecked) 224 | self.VideoTimestamp.grid(row=3,column=0,columnspan=2,sticky='W') 225 | ToolTip(self.VideoTimestamp,6061) 226 | 227 | e.config(validatecommand=okCmd) 228 | ''' 229 | Configuration files in python 230 | There are several ways to do this depending on the file format required. 231 | ConfigParser [.ini format] 232 | Write a file like so: 233 | from ConfigParser import SafeConfigParser 234 | config = SafeConfigParser() 235 | config.read('config.ini') 236 | config.add_section('main') 237 | config.set('main', 'key1', 'value1') 238 | config.set('main', 'key2', 'value2') 239 | config.set('main', 'key3', 'value3') 240 | ''' 241 | self.photoCaptureFormatChanged(None) 242 | self.VideoCaptureFormatChanged(None) 243 | def ChangeDirectory ( self, defaultDir, label, text ): 244 | oldDir = os.getcwd() 245 | try: 246 | os.chdir(defaultDir) 247 | # I hate that it doesn't allow you set set the initial directory! 248 | dirname = FileDialog.askdirectory()#self, initialdir="/home/pi/Pictures", 249 | #title='Select Photo Directory') 250 | if dirname: 251 | defaultDir = dirname 252 | label.config(text=dirname) 253 | except: print ( "Preferences dialog error setting %s directory" % text) 254 | finally: os.chdir(oldDir) 255 | def SelectPhotoDirectory ( self ): 256 | self.ChangeDirectory(PreferencesDialog.DefaultPhotoDir,self.PhotoDirLabel,"Photo") 257 | def SelectVideoDirectory ( self ): 258 | self.ChangeDirectory(PreferencesDialog.DefaultVideoDir,self.VideoDirLabel,"Video") 259 | def SelectFilesDirectory ( self ): 260 | self.ChangeDirectory(PreferencesDialog.DefaultFilesDir,self.FilesDirLabel,"Files") 261 | def photoCaptureFormatChanged ( self, val ): 262 | photoformat = self.photoCaptureFormatCombo.get() 263 | PreferencesDialog.DefaultPhotoFormat = photoformat 264 | self.ModFormatParams.config(state = \ 265 | 'normal' if photoformat == 'jpeg' else 'disabled' ) 266 | def ModifyFormatParamPressed ( self ): 267 | PhotoParamsDialog(self,title='Photo Capture Parameters', 268 | minwidth=350,minheight=100,okonly=False) 269 | # Hack. The modal flag is corrupted when calling another dialog? 270 | if self.data.modal is True: 271 | self.data._window.grab_set() 272 | self.data._parent.wait_window(self.data._window) 273 | def VideoCaptureFormatChanged ( self, val ): 274 | videoformat = self.VideoCaptureFormatCombo.get() 275 | PreferencesDialog.DefaultVideoFormat = videoformat 276 | def ModifyVideoFormatParamPressed ( self ): 277 | VideoParamsDialog(self,title='Video Capture Parameters', 278 | okonly=False, data=PreferencesDialog.DefaultFilesDir) 279 | # Hack. The modal flag is corrupted when calling another dialog? 280 | if self.data.modal is True: 281 | self.data._window.grab_set() 282 | self.data._parent.wait_window(self.data._window) 283 | def ValidateTimestamp ( self, text ): 284 | self.CheckTimestamp(text) 285 | return True 286 | def CheckTimestamp ( self, text ): 287 | try: 288 | self.TimestampLabel.set(datetime.datetime.now().strftime(text)) 289 | self.tsl.config(foreground='#0000FF') 290 | PreferencesDialog.DefaultTimestampFormat = text 291 | except: 292 | self.TimestampLabel.set("Error in format string") 293 | self.tsl.config(foreground='#FF0000') 294 | def UpdateTimestamp ( self ): 295 | self.CheckTimestamp(self.TimeStamp.get()) 296 | self.after(1000,self.UpdateTimestamp) 297 | def PhotoTimestampChecked ( self ): 298 | PreferencesDialog.PhotoTimestamp = self.PhotoTimestampVar.get() 299 | def VideoTimestampChecked ( self ): 300 | PreferencesDialog.VideoTimestamp = self.VideoTimestampVar.get() 301 | def FormatHelp ( self ): 302 | webbrowser.open_new_tab('https://docs.python.org/2/library/datetime.html') 303 | def SaveChanges ( self ): 304 | pass 305 | 306 | # Handle PiCameraApp Interface preferences 307 | class Interface ( BasicNotepage ): 308 | def BuildPage ( self ): 309 | self.iconMonitor = ImageTk.PhotoImage( \ 310 | PIL.Image.open("Assets/computer-monitor.png").resize((64,64),Image.ANTIALIAS)) 311 | Label(self,image=self.iconMonitor).grid(row=0,column=0,sticky='W') 312 | 313 | f = MyLabelFrame(self,'Interface themes',0,1) 314 | f.columnconfigure(1,weight=1) 315 | Label(f,text='Set theme').grid(row=0,column=0,sticky='W',pady=(5,5)) 316 | self.themes = Combobox(f,height=10,state='readonly') 317 | self.themes.grid(row=0,column=1,sticky='W',padx=(10,0)) 318 | self.themes['values'] = Style().theme_names() 319 | self.themes.set(Style().theme_use()) 320 | self.themes.bind('<>',self.ThemesSelected) 321 | ToolTip(self.themes,6100) 322 | 323 | f = MyLabelFrame(self,'Tooltips',1,0,span=2) 324 | self.ShowTooltips = MyBooleanVar(ToolTip.ShowToolTips) 325 | self.ShowTipsButton = ttk.Checkbutton(f,text='Show those annoying tooltips', 326 | variable=self.ShowTooltips, command=self.ShowTooltipsChecked)#,padding=(5,5,5,5)) 327 | self.ShowTipsButton.grid(row=1,column=0,columnspan=2,sticky='W') 328 | ToolTip(self.ShowTipsButton,6110) 329 | 330 | self.ShowTipNumber = MyBooleanVar(ToolTip.ShowTipNumber) 331 | self.ShowTipNumButton = ttk.Checkbutton(f,text='Show tip number in tip (debug)', 332 | variable=self.ShowTipNumber, command=self.ShowTooltipNumChecked, 333 | padding=(25,5,5,5)) 334 | self.ShowTipNumButton.grid(row=2,column=0,columnspan=2,sticky='W') 335 | ToolTip(self.ShowTipNumButton,6111) 336 | 337 | ttk.Label(f,text='Delay before tip',padding=(25,0,0,0)).grid(row=3,column=0,sticky='W') 338 | scale = ttk.Scale(f,value=ToolTip.ShowTipDelay, 339 | from_=0.1,to=5.0,orient='horizontal', 340 | command=self.TipDelayChanged) 341 | scale.grid(row=3,column=1,sticky='W') 342 | ToolTip(scale,6112) 343 | self.DelayText = MyStringVar("") 344 | l = Label(f,textvariable=self.DelayText,foreground='#0000FF') 345 | l.grid(row=3,column=2,sticky='W') 346 | ToolTip(l,6113) 347 | 348 | self.TipDelayChanged(ToolTip.ShowTipDelay) # Force display update 349 | def ThemesSelected ( self, event ): 350 | Style().theme_use(self.themes.get()) 351 | def ShowTooltipsChecked ( self ): 352 | ToolTip.ShowToolTips = self.ShowTooltips.get() 353 | def ShowTooltipNumChecked ( self ): 354 | ToolTip.ShowTipNumber = self.ShowTipNumber.get() 355 | def TipDelayChanged (self, val ): 356 | ToolTip.ShowTipDelay = float(val) 357 | self.DelayText.set('{:.1f} sec'.format(float(val))) 358 | def SaveChanges ( self ): 359 | pass 360 | 361 | # Handle PiCameraApp Other preferences 362 | class Other ( BasicNotepage ): 363 | pass 364 | 365 | 366 | -------------------------------------------------------------------------------- /Source/Timelapse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # Timelapse.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | from time import sleep 25 | from Dialog import * 26 | from Mapping import * 27 | from NotePage import * 28 | 29 | class Timelapse ( BasicNotepage ): 30 | def BuildPage ( self ): 31 | f = ttk.LabelFrame(self,text='Time lapse settings',padding=(5,5,5,5)) 32 | f.grid(row=0,column=0,columnspan=4,sticky='NEWS',pady=5) 33 | f.columnconfigure(2,weight=1) 34 | f.columnconfigure(4,weight=1) 35 | 36 | Label(f,text='Default').grid(row=0,column=0,sticky='E') 37 | self.LowLightCaptureButton = Button(f,text='Low Light',width=15, \ 38 | command=self.CaptureLowLight) 39 | self.LowLightCaptureButton.grid(row=1,column=0,sticky='W') 40 | self.StartDelayCaptureButton = Button(f,text='Delay Capture',width=15, \ 41 | command=self.StartDelayCapture) 42 | self.StartDelayCaptureButton.grid(row=2,column=0,sticky='W') 43 | 44 | def CaptureLowLight ( self ): 45 | self.camera.capture('foo.jpg') 46 | pass 47 | def StartDelayCapture ( self ): 48 | pass 49 | #### TODO: Implement Reset NEEDS LOTS OF WORK!! 50 | def Reset ( self ): 51 | pass 52 | 53 | ''' 54 | What controls are needed for this page? 55 | Photo captures: 56 | Type of Time lapse 57 | Burst 58 | Timed 59 | etc 60 | Whether the image settings stay the same for each picture - Checkbox 61 | Whether the Video port is used or not (faster) - Checkbox 62 | Filename (Textbox entry) 63 | Start 64 | Immediately 65 | Delay XXX YYY SEC, MIN HR 66 | On a specific date/time 67 | Delay between each shot.... or at a specific time each day, etc.... 68 | e.g., Every XX YYY where XX is a number YYY is SEC, MIN, HR, DAY 69 | e.g., On every MIN, 1/2 HR, HOUR 70 | e.g., Every Day at XX:XX Time 71 | When does the capture end 72 | After XXX shots XXX from 1 to 1000? 73 | After XXX minutes, Hours, Days 74 | On XXXX date 75 | Append a number or a date/time to 'Filename' - or both 76 | Use Drop down ComboBox 77 | e.g. Bill_1.jpg, Bill_2.jpg, ... etc 78 | or Bill_Date_Time.jpg, Bill_Date_Time.jpg, ... etc 79 | or both Bill_Date_Time_1.jpg, Bill_Date_Time_2.jpg, ... etc 80 | What about video captures? 81 | ''' 82 | 83 | 84 | ''' 85 | Examples from the picamera documentation 86 | https://picamera.readthedocs.io/en/release-1.13/recipes1.html 87 | 88 | The following script provides a brief example of configuring these settings: 89 | 90 | from time import sleep 91 | from picamera import PiCamera 92 | 93 | camera = PiCamera(resolution=(1280, 720), framerate=30) 94 | # Set ISO to the desired value 95 | camera.iso = 100 96 | # Wait for the automatic gain control to settle 97 | sleep(2) 98 | # Now fix the values 99 | camera.shutter_speed = camera.exposure_speed 100 | camera.exposure_mode = 'off' 101 | g = camera.awb_gains 102 | camera.awb_mode = 'off' 103 | camera.awb_gains = g 104 | # Finally, take several photos with the fixed settings 105 | camera.capture_sequence(['image%02d.jpg' % i for i in range(10)]) 106 | 107 | from time import sleep 108 | from picamera import PiCamera 109 | 110 | camera = PiCamera() 111 | camera.start_preview() 112 | sleep(2) 113 | for filename in camera.capture_continuous('img{counter:03d}.jpg'): 114 | print('Captured %s' % filename) 115 | sleep(300) # wait 5 minutes 116 | ''' 117 | 118 | ''' 119 | from time import sleep 120 | from picamera import PiCamera 121 | from datetime import datetime, timedelta 122 | 123 | def wait(): 124 | # Calculate the delay to the start of the next hour 125 | next_hour = (datetime.now() + timedelta(hour=1)).replace( 126 | minute=0, second=0, microsecond=0) 127 | delay = (next_hour - datetime.now()).seconds 128 | sleep(delay) 129 | 130 | camera = PiCamera() 131 | camera.start_preview() 132 | wait() 133 | for filename in camera.capture_continuous('img{timestamp:%Y-%m-%d-%H-%M}.jpg'): 134 | print('Captured %s' % filename) 135 | wait() 136 | 137 | 138 | 3.7. Capturing in low light 139 | Using similar tricks to those in Capturing consistent images, the Pi’s 140 | camera can capture images in low light conditions. The primary objective 141 | is to set a high gain, and a long exposure time to allow the camera to 142 | gather as much light as possible. However, the shutter_speed attribute 143 | is constrained by the camera’s framerate so the first thing we need to 144 | do is set a very slow framerate. The following script captures an image 145 | with a 6 second exposure time (the maximum the Pi’s V1 camera module is 146 | capable of; the V2 camera module can manage 10 second exposures): 147 | 148 | from picamera import PiCamera 149 | from time import sleep 150 | from fractions import Fraction 151 | 152 | # Force sensor mode 3 (the long exposure mode), set 153 | # the framerate to 1/6fps, the shutter speed to 6s, 154 | # and ISO to 800 (for maximum gain) 155 | camera = PiCamera( 156 | resolution=(1280, 720), 157 | framerate=Fraction(1, 6), 158 | sensor_mode=3) 159 | camera.shutter_speed = 6000000 160 | camera.iso = 800 161 | # Give the camera a good long time to set gains and 162 | # measure AWB (you may wish to use fixed AWB instead) 163 | sleep(30) 164 | camera.exposure_mode = 'off' 165 | # Finally, capture an image with a 6s exposure. Due 166 | # to mode switching on the still port, this will take 167 | # longer than 6 seconds 168 | camera.capture('dark.jpg') 169 | ''' 170 | -------------------------------------------------------------------------------- /Source/Tooltip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Tooltip.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # Tooltip implementation courtesy of 9 | # https://code.activestate.com/recipes/576688-tooltip-for-tkinter/ 10 | # 11 | # This program is free software; you can redistribute it and/or modify 12 | # it under the terms of the GNU General Public License as published by 13 | # the Free Software Foundation; either version 2 of the License, or 14 | # (at your option) any later version. 15 | # 16 | # This program is distributed in the hope that it will be useful, 17 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | # GNU General Public License for more details. 20 | # 21 | # You should have received a copy of the GNU General Public License 22 | # along with this program; if not, write to the Free Software 23 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 24 | # MA 02110-1301, USA. 25 | # 26 | # 27 | from time import time, localtime, strftime 28 | try: 29 | from Tkinter import * 30 | except ImportError: 31 | from tkinter import * 32 | 33 | try: 34 | from tkColorChooser import askcolor 35 | except ImportError: 36 | from tkinter.colorchooser import askcolor 37 | try: 38 | import tkFileDialog 39 | except ImportError: 40 | import tkinter.filedialog 41 | try: 42 | import tkMessageBox 43 | except ImportError: 44 | import tkinter.messagebox 45 | 46 | try: 47 | import ttk 48 | from ttk import * 49 | except ImportError: 50 | from tkinter import ttk 51 | #from ttk import * 52 | try: 53 | import tkFont 54 | except ImportError: 55 | import tkinter.font 56 | 57 | class ToolTip( Toplevel ): 58 | ShowToolTips = True 59 | ShowTipNumber = False 60 | ShowTipDelay = 1.0 # 1 second initial delay 61 | TipLines = [] # All the tip text in lines 62 | @staticmethod 63 | def LoadToolTips ( ): # Perhaps allow a reload of tips? 64 | tipsFile = open("Assets/Tooltips.txt", "r") 65 | if tipsFile: 66 | ToolTip.TipLines = tipsFile.read().split('\n') 67 | tipsFile.close() 68 | else: 69 | print ( "Error opening file 'Assets/Tooltips.txt'" ) 70 | @staticmethod 71 | def GetTooltipText ( ID ): 72 | append = False 73 | tip = "" 74 | for text in ToolTip.TipLines: 75 | text = text.strip() 76 | text = text.replace('(C)', 77 | '\n\nThanks to: picamera.readthedocs.io/en/release-1.13/api_camera.html') 78 | if append: 79 | # Buggy - spaces are being lost in the text 80 | if text.endswith('\\n') is True: tip = tip + text 81 | else: tip = tip + ' ' + text # add a space for next line 82 | if tip.endswith('$') is True: 83 | return tip.replace('$','').replace("\\n","\n") 84 | else: 85 | if len(text) is 0 or text[0] is '#': continue 86 | ID_Tip = text.split(':',1) # only the first colon is a split 87 | try: 88 | if int(ID_Tip[0].strip()) == ID: # find a better way 89 | # We have a match 90 | # Check if the text continues on multiple lines 91 | tip = ID_Tip[1].strip() 92 | append = False if tip.endswith('$') else True 93 | if append is False: 94 | return tip.replace("\\n","\n").replace('$','') 95 | except: pass 96 | return 'Tooltip text for ID %d not found.' % ID 97 | """ 98 | Provides a ToolTip widget for Tkinter. 99 | To apply a ToolTip to any Tkinter widget, simply pass the widget to the 100 | ToolTip constructor 101 | """ 102 | def __init__( self, wdgt, msg=None, msgFunc=None, follow=1 ): 103 | """ 104 | Initialize the ToolTip 105 | Arguments: 106 | wdgt: The widget to which this ToolTip is assigned 107 | msg: A static string message assigned to the ToolTip 108 | if msg istype integer - search for text in TipLines 109 | msgFunc: A function that retrieves a string to use as the ToolTip text 110 | delay: The delay in seconds before the ToolTip appears(may be float) 111 | follow: If True, the ToolTip follows motion, otherwise hides 112 | """ 113 | self.wdgt = wdgt 114 | # The parent of the ToolTip is the parent of the ToolTips widget 115 | self.parent = self.wdgt.master 116 | # Initalise the Toplevel 117 | Toplevel.__init__( self, self.parent, bg='black', padx=1, pady=1 ) 118 | self.withdraw() # Hide initially 119 | # The ToolTip Toplevel should have no frame or title bar 120 | self.overrideredirect( True ) 121 | # The msgVar will contain the text displayed by the ToolTip 122 | self.msgVar = StringVar() 123 | self.TipID = None 124 | self.TipNumText = "" 125 | try: 126 | if msg is None: 127 | self.msgVar.set('No tooltip provided') 128 | elif type(msg) is int: # lookup tooltip text in file 129 | self.TipID = msg 130 | self.msgVar.set(ToolTip.GetTooltipText(msg)) 131 | self.TipNumText = "Tip number %d\n\n" % self.TipID 132 | else: # assume a string is passed 133 | self.msgVar.set( msg ) 134 | except: 135 | self.msgVar.set('ERROR getting tooltip') 136 | self.msgFunc = msgFunc # call this function to return tip text 137 | self.follow = follow # move tip if mouse moves 138 | self.visible = 0 139 | self.lastMotion = 0 140 | # The test of the ToolTip is displayed in a Message widget 141 | Message( self, textvariable=self.msgVar, bg='#FFFFDD', 142 | aspect=250 ).grid() 143 | # Add bindings to the widget. This will NOT override bindings 144 | # that the widget already has 145 | self.wdgt.bind( '', self.spawn, '+' ) 146 | self.wdgt.bind( '', self.hide, '+' ) 147 | self.wdgt.bind( '', self.move, '+' ) 148 | 149 | def spawn( self, event=None ): 150 | """ 151 | Spawn the ToolTip. This simply makes the ToolTip eligible for display. 152 | Usually this is caused by entering the widget 153 | Arguments: 154 | event: The event that called this funciton 155 | """ 156 | self.visible = 1 157 | # The after function takes a time argument in miliseconds 158 | self.after( int( ToolTip.ShowTipDelay * 1000 ), self.show ) 159 | 160 | def show( self ): 161 | """ 162 | Displays the ToolTip if the time delay has been long enough 163 | """ 164 | if ToolTip.ShowToolTips is False: return 165 | text = self.msgVar.get() 166 | if ToolTip.ShowTipNumber is True and self.TipID is not None: 167 | # check if text is not there, if so add it 168 | if self.TipNumText not in text: 169 | self.msgVar.set(self.TipNumText+text) 170 | else: 171 | text.replace(self.TipNumText,"") 172 | self.msgVar.set(text) 173 | 174 | if self.visible == 1 and time() - self.lastMotion > ToolTip.ShowTipDelay: 175 | self.visible = 2 176 | if self.visible == 2: 177 | self.deiconify() 178 | 179 | def move( self, event ): 180 | """ 181 | Processes motion within the widget. 182 | Arguments: 183 | event: The event that called this function 184 | """ 185 | self.lastMotion = time() 186 | # If the follow flag is not set, motion within the widget will 187 | # make the ToolTip dissapear 188 | if self.follow == False: 189 | self.withdraw() 190 | self.visible = 1 191 | # Offset the ToolTip 10x10 pixels southeast of the pointer 192 | self.geometry( '+%i+%i' % ( event.x_root+10, event.y_root+10 ) ) 193 | # Try to call the message function. Will not change the message 194 | # if the message function is None or the message function fails 195 | try: self.msgVar.set( self.msgFunc() ) 196 | except: pass 197 | self.after( int( ToolTip.ShowTipDelay * 1000 ), self.show ) 198 | 199 | def hide( self, event=None ): 200 | """ 201 | Hides the ToolTip. Usually this is caused by leaving the widget 202 | Arguments: 203 | event: The event that called this function 204 | """ 205 | self.visible = 0 206 | self.withdraw() 207 | -------------------------------------------------------------------------------- /Source/Utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Utils.py 3 | Copyright (C) 2015 - Bill Williams 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | ''' 15 | 16 | Python2x = True 17 | 18 | try: 19 | from Tkinter import * 20 | except ImportError: 21 | from tkinter import * 22 | Python2x = False 23 | 24 | try: 25 | from tkColorChooser import askcolor 26 | except ImportError: 27 | from tkinter.colorchooser import askcolor 28 | try: 29 | import tkFileDialog 30 | except ImportError: 31 | import tkinter.filedialog 32 | try: 33 | import tkMessageBox 34 | except ImportError: 35 | import tkinter.messagebox 36 | try: 37 | import ttk 38 | from ttk import * 39 | except ImportError: 40 | from tkinter import ttk 41 | #from ttk import * 42 | try: 43 | import tkFont 44 | from tkFont import * 45 | except ImportError: 46 | import tkinter.font 47 | from tkinter.font import * 48 | 49 | import PIL 50 | from PIL import Image, ImageTk, ExifTags 51 | 52 | from Dialog import * 53 | from Tooltip import * 54 | 55 | # 56 | # General utility functions 57 | # 58 | def UnderConstruction ( window ): 59 | Label(window,text='UNDER CONSTRUCTION',font=('Arial',14,('bold')), 60 | anchor='center').grid(row=0,column=0,sticky='EW') 61 | 62 | def OnOff ( val ): return 'On' if val else 'Off' 63 | 64 | def EvenOdd ( val ): return 'even' if val else 'odd' 65 | 66 | # Add BooleanVar in here 67 | def MyRadio ( f, txt, varValue, varName, cmd=None, row=0, col=0, stick='W', 68 | span=1, pad=(5,5,5,5), tip='No Tip number provided'): 69 | #def MyRadio ( f, txt, varValue, varName = None, cmd=None, row=0, col=0, stick='W', 70 | # span=1, pad=(5,5,5,5), tip='No Tip number provided'): 71 | # if varName is None: 72 | # # Determine type of var from varValue and create one 73 | # if type(varValue) is int: 74 | # varName = MyIntVar(varValue) 75 | # elif type(varValue) is boolean: 76 | # varName = MyBooleanVar(varValue) 77 | # elif type(varValue) is str: 78 | # varName = MyStringVar(varValue) 79 | if cmd is None: 80 | r = ttk.Radiobutton(f,text=txt,value=varValue,variable=varName, 81 | padding=pad) 82 | else: 83 | r = ttk.Radiobutton(f,text=txt,value=varValue,variable=varName, 84 | command=lambda:cmd(varValue),padding=pad) 85 | r.grid(row=row,column=col,sticky=stick, columnspan=span) 86 | ToolTip(r,msg=tip) 87 | return r # , varName # return RadioButton and BooleanVar 88 | 89 | ''' 90 | params = [ ['text', True or False, row, col, sticky, rowspan, tipNum] 91 | ['text', True or False, row, col, sticky, rowspan, tipNum] ] 92 | Command = the function to call if pressed, pass True or False 93 | Create two radio buttons, return the first one, and the BooleanVar 94 | associated with the two. If command is not None, then call the command 95 | with the same value passed in the list 96 | 97 | Return, first radio created, BooleanVar created 98 | ''' 99 | def CreateRadioButtonBoolean ( parent, params, command=None ): 100 | pass 101 | 102 | ''' 103 | params = [ ['text', 'value', row, col, sticky, rowspan, tipNum] 104 | ['text', 'value', row, col, sticky, rowspan, tipNum] ] 105 | Command = the function to call if pressed, pass True or False 106 | Create two radio buttons, return the first one, and the BooleanVar 107 | associated with the two. If command is not None, then call the command 108 | with the same value passed in the list 109 | 110 | Return, first radio created, StringVar created 111 | ''' 112 | def CreateRadioButtonSet ( parent, params, command=None ): 113 | pass 114 | 115 | def MyComboBox ( parent, values, current, callback, width=5, row=0, col=0, 116 | sticky='EW', state='disabled', tip='No Tip number provided' ): 117 | combo = ttk.Combobox(parent,state=state,width=width) 118 | combo.grid(row=row,column=col,sticky=sticky) 119 | combo.bind('<>',callback) 120 | combo['values'] = values 121 | combo.current(current) 122 | ToolTip(combo,tip) 123 | return combo 124 | 125 | def MySliderBar ( parent ): 126 | pass 127 | 128 | def MyEditField ( parent ): 129 | pass 130 | 131 | def MyLabel ( parent, text, row, col, span ): 132 | pass 133 | 134 | def MyButton ( parent ): 135 | pass 136 | 137 | def MyLabelFrame ( f, txt, row, col, stick='NEWS', py=5, span=1, pad=(5,5,5,5)): 138 | l = ttk.LabelFrame(f,text=txt,padding=pad) 139 | l.grid(row=row,column=col,sticky=stick,columnspan=span,pady=py) 140 | return l 141 | 142 | def MyBooleanVar ( setTo ): 143 | b = BooleanVar() 144 | b.set(setTo) 145 | return b 146 | 147 | def MyIntVar ( setTo ): 148 | b = IntVar() 149 | b.set(setTo) 150 | return b 151 | 152 | def MyStringVar ( setTo ): 153 | s = StringVar() 154 | s.set(setTo) 155 | return s 156 | 157 | def GetPhotoImage ( filename ): 158 | # Get the image - test whether python 2x or 3x 159 | if Python2x: 160 | if isinstance(filename,basestring): 161 | return ImageTk.PhotoImage(PIL.Image.open(filename)) 162 | else: 163 | return ImageTk.PhotoImage(filename) 164 | else: 165 | if isinstance(filename,str): 166 | return ImageTk.PhotoImage(PIL.Image.open(filename)) 167 | else: 168 | return ImageTk.PhotoImage(filename) 169 | 170 | def USECtoSec ( usec ): 171 | # return a text value formatted 172 | if usec < 1000: 173 | return '%d usec' % usec 174 | elif usec < 1000000: 175 | return '%.3f msec' % (float(usec) / 1000.0) 176 | else: 177 | return '%.3f sec' % (float(usec) / 1000000.0) 178 | -------------------------------------------------------------------------------- /Source/VideoParams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' 4 | # VideoParams.py 5 | # 6 | # Copyright 2018 Bill Williams 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation; either version 2 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with this program; if not, write to the Free Software 20 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 21 | # MA 02110-1301, USA. 22 | # 23 | ''' 24 | try: 25 | from Tkinter import * 26 | except ImportError: 27 | from tkinter import * 28 | try: 29 | from tkColorChooser import askcolor 30 | except ImportError: 31 | from tkinter.colorchooser import askcolor 32 | try: 33 | import tkFileDialog as FileDialog 34 | except ImportError: 35 | import tkinter.filedialog as FileDialog 36 | try: 37 | import tkMessageBox as MessageBox 38 | except ImportError: 39 | import tkinter.messagebox as MessageBox 40 | try: 41 | import ttk 42 | from ttk import * 43 | except ImportError: 44 | from tkinter import ttk 45 | #from ttk import * 46 | try: 47 | import tkFont 48 | except ImportError: 49 | import tkinter.font 50 | 51 | import os 52 | 53 | from Dialog import * 54 | from Mapping import * 55 | from NotePage import * 56 | from Utils import * 57 | from Tooltip import * 58 | from NotePage import BasicNotepage 59 | 60 | # Handle all Video parameters 61 | ''' 62 | Thanks to: picamera.readthedocs.io/en/release-1.13/api_camera.html 63 | Certain formats accept additional options which can be specified as 64 | keyword arguments. The 'h264' format accepts the following additional 65 | options: 66 | 67 | profile - The H.264 profile to use for encoding. Defaults to ‘high’, 68 | but can be one of ‘baseline’, ‘main’, ‘extended’, ‘high’, or ‘constrained’. 69 | 70 | level - The H.264 level to use for encoding. Defaults to ‘4’, but can 71 | be any H.264 level up to ‘4.2’. 72 | 73 | intra_period - The key frame rate (the rate at which I-frames are 74 | inserted in the output). Defaults to None, but can be any 32-bit 75 | integer value representing the number of frames between successive 76 | I-frames. The special value 0 causes the encoder to produce a single 77 | initial I-frame, and then only P-frames subsequently. Note that 78 | split_recording() will fail in this mode. 79 | 80 | intra_refresh - The key frame format (the way in which I-frames will be 81 | inserted into the output stream). Defaults to None, but can be one 82 | of ‘cyclic’, ‘adaptive’, ‘both’, or ‘cyclicrows’. 83 | 84 | inline_headers - When True, specifies that the encoder should output 85 | SPS/PPS headers within the stream to ensure GOPs (groups of pictures) 86 | are self describing. This is important for streaming applications 87 | where the client may wish to seek within the stream, and enables the 88 | use of split_recording(). Defaults to True if not specified. 89 | 90 | sei - When True, specifies the encoder should include 91 | “Supplemental Enhancement Information” within the output stream. 92 | Defaults to False if not specified. 93 | 94 | sps_timing - When True the encoder includes the camera’s framerate in 95 | the SPS header. Defaults to False if not specified. 96 | 97 | motion_output - Indicates the output destination for motion vector 98 | estimation data. When None (the default), motion data is not output. 99 | Otherwise, this can be a filename string, a file-like object, or a 100 | writeable buffer object (as with the output parameter). 101 | 102 | All encoded formats accept the following additional options: 103 | 104 | bitrate - The bitrate at which video will be encoded. Defaults to 105 | 17000000 (17Mbps) if not specified. The maximum value depends on the 106 | selected H.264 level and profile. Bitrate 0 indicates the encoder 107 | should not use bitrate control (the encoder is limited by the quality 108 | only). 109 | 110 | quality - Specifies the quality that the encoder should attempt to 111 | maintain. For the 'h264' format, use values between 10 and 40 where 10 112 | is extremely high quality, and 40 is extremely low (20-25 is usually 113 | a reasonable range for H.264 encoding). For the mjpeg format, use JPEG 114 | quality values between 1 and 100 (where higher values are higher 115 | quality). Quality 0 is special and seems to be a “reasonable quality” 116 | default. 117 | 118 | quantization - Deprecated alias for quality. 119 | ''' 120 | 121 | from Mapping import * 122 | from PreferencesDialog import * 123 | 124 | class VideoParamsDialog ( Dialog ): 125 | def BuildDialog ( self ): 126 | n = Notebook(self.MainFrame,padding=(5,5,5,5)) 127 | n.grid(row=0,column=0,sticky='NSEW') 128 | #n.columnconfigure(0,weight=1) 129 | #n.rowconfigure(0,weight=1) 130 | 131 | self.H264page = H264(n,cancel=self.CancelButton,ok=self.OkButton, 132 | colconfig=False,data=self.data) 133 | self.AllFormatspage = AllFormats(n,cancel=self.CancelButton, 134 | ok=self.OkButton,colconfig=False) 135 | 136 | n.add(self.H264page,text='H264',underline=0) 137 | n.add(self.AllFormatspage,text='All formats',underline=0) 138 | 139 | def OkPressed ( self ): 140 | self.H264page.SaveChanges() 141 | self.AllFormatspage.SaveChanges() 142 | return True 143 | 144 | def CancelPressed ( self ): 145 | return MessageBox.askyesno("Video Params","Exit without saving changes?") 146 | 147 | class H264 ( BasicNotepage ): 148 | Profile = 'high' 149 | Level = '4' 150 | IntraPeriod = None # or a 32 bit integer, 0 is special 151 | IntraRefresh = None # Need to check for None and select text 152 | InlineHeaders = True 153 | SEI = False 154 | SPSTiming = False 155 | MotionOutput = None # or a filename 156 | @staticmethod 157 | # Called if Reset Camera is clicked 158 | def Reset (): 159 | Profile = 'high' 160 | Level = '4' 161 | IntraPeriod = None # or integer between 0 to 32767 ??? 162 | IntraRefresh = None 163 | InlineHeaders = True 164 | SEI = False 165 | SPSTiming = False 166 | MotionOutput = None 167 | def BuildPage ( self ): 168 | Label(self,text="Profile:").grid(row=0,column=0,sticky='W'); 169 | self.Profiles = Combobox(self,state='readonly',width=12) 170 | self.Profiles.bind('<>',self.SomethingChanged) 171 | self.Profiles.grid(row=0,column=1,sticky='W',columnspan=2,pady=3) 172 | self.profileList = ['high (default)','baseline','main','extended','constrained'] 173 | self.Profiles['values'] = self.profileList 174 | if H264.Profile == 'high': 175 | self.Profiles.current(0) 176 | else: 177 | self.Profiles.current(self.profileList.index(H264.Profile)) 178 | ToolTip(self.Profiles,3000) 179 | 180 | Label(self,text="Level:").grid(row=1,column=0,sticky='W'); 181 | self.Levels = Combobox(self,state='readonly',width=12) 182 | self.Levels.bind('<>',self.SomethingChanged) 183 | self.Levels.grid(row=1,column=1,sticky='W',columnspan=2) 184 | self.LevelsList = ['1','1b','1.1','1.2','1.3','2','2.1', 185 | '2.2', '3', '3.1', '3.2', '4 (default)','4.1','4.2'] 186 | self.Levels['values'] = self.LevelsList 187 | if H264.Level == '4': 188 | self.Levels.current(11) 189 | else: 190 | self.Levels.current(self.LevelsList.index(H264.Level)) 191 | ToolTip(self.Levels,3001) 192 | 193 | f = MyLabelFrame(self,"Intra-Period:",row=2,col=0,span=3,pad=(5,10,5,10)); 194 | self.FrameCount = StringVar() 195 | self.FrameCount.set('') 196 | okCmd = (self.register(self.ValidateFrameCount),'%P') 197 | Label(f,text="Count:").grid(row=0,column=2,sticky='W',padx=5); 198 | self.FrameCountEdit = Entry(f,textvariable=self.FrameCount,width=6, 199 | validate='all',validatecommand=okCmd) 200 | self.FrameCountEdit.grid(row=0,column=3,sticky='W') 201 | ToolTip(self.FrameCountEdit,3003) 202 | self.IntraPeriod = Combobox(f,state='readonly',width=33) 203 | self.IntraPeriod.bind('<>',self.IntraPeriodChanged) 204 | self.IntraPeriod.grid(row=0,column=0,sticky='W',columnspan=2) 205 | self.IntraPeriodList = ['None (default)','Initial I Frame, then P frames', 206 | 'Specify framecount between each I frame'] 207 | self.IntraPeriod['values'] = self.IntraPeriodList 208 | # Check if None, select 0, else select matching text 209 | self.FrameCount.set('1') 210 | if H264.IntraPeriod == None: 211 | self.IntraPeriod.current(0) 212 | elif H264.IntraPeriod == 0: 213 | self.IntraPeriod.current(1) 214 | else: 215 | self.IntraPeriod.current(2) 216 | self.FrameCount.set(str(H264.IntraPeriod)) 217 | ToolTip(self.IntraPeriod,3002) 218 | # Check if None (select radio), or 0, (select radio) 219 | # or put integer value into edit field 220 | 221 | Label(self,text="Intra-Refresh:").grid(row=3,column=0,sticky='W') 222 | self.IntraRefresh = Combobox(self,state='readonly',width=12) 223 | self.IntraRefresh.bind('<>',self.SomethingChanged) 224 | self.IntraRefresh.grid(row=3,column=1,sticky='W',columnspan=2) 225 | self.IntraRefreshList = ['None (default)','cyclic', 'adaptive', 226 | 'both', 'cyclicrows'] 227 | self.IntraRefresh['values'] = self.IntraRefreshList 228 | # Check if None, select 0, else select matching text 229 | if H264.IntraRefresh == None: 230 | self.IntraRefresh.current(0) 231 | else: 232 | self.IntraRefresh.current(self.IntraRefreshList.index(H264.IntraRefresh)) 233 | ToolTip(self.IntraRefresh,3004) 234 | 235 | Label(self,text="Inline Headers:").grid(row=4,column=0,sticky='W') 236 | self.InlineHeaders = MyBooleanVar(H264.InlineHeaders) 237 | MyRadio(self,"On (default)",True,self.InlineHeaders,self.SomethingChanged,4,1,'W', 238 | tip=3005) 239 | MyRadio(self,"Off",False,self.InlineHeaders,self.SomethingChanged,4,2,'W', 240 | tip=3006) 241 | 242 | Label(self,text="SEI:").grid(row=5,column=0,sticky='W'); 243 | self.SEI = MyBooleanVar(H264.SEI) 244 | MyRadio(self,"On",True,self.SEI,self.SomethingChanged,5,1,'w', 245 | tip=3007) 246 | MyRadio(self,"Off (default)",False,self.SEI,self.SomethingChanged,5,2,'w', 247 | tip=3008) 248 | 249 | Label(self,text="SPS Timing:").grid(row=6,column=0,sticky='W'); 250 | self.SPSTiming = MyBooleanVar(H264.SPSTiming) 251 | MyRadio(self,"On",True,self.SPSTiming,self.SomethingChanged,6,1,'W', 252 | tip=3009) 253 | MyRadio(self,"Off (default)",False,self.SPSTiming,self.SomethingChanged,6,2,'W', 254 | tip=3010) 255 | 256 | Label(self,text="Motion Output:").grid(row=7,column=0,sticky='W'); 257 | # Check if None, select radio, else place filename text field. 258 | # Have a button to select file 259 | self.MotionOutputFile = MyBooleanVar(H264.MotionOutput is None) 260 | r = MyRadio(self,"None",True,self.MotionOutputFile,self.MotionOutputChanged,7,1,'W', 261 | tip=3011) 262 | MyRadio(self,"To file:",False,self.MotionOutputFile,self.MotionOutputChanged,7,2,'W', 263 | tip=3012) 264 | self.SelectMotionOutputFile = ttk.Button(self,text='File...', 265 | command=self.SelectMotionOutputFilePressed, 266 | underline=0,padding=(5,3,5,3),width=8) 267 | self.SelectMotionOutputFile.grid(row=7,column=3,sticky='W',padx=5) 268 | ToolTip(self.SelectMotionOutputFile,3013) 269 | if H264.MotionOutput == None: 270 | self.MotionOutputFilename = "" 271 | else: 272 | self.MotionOutputFilename = H264.MotionOutput 273 | self.MotionFilename = ttk.Label(self,text=self.MotionOutputFilename, 274 | style='DataLabel.TLabel') 275 | self.MotionFilename.grid(row=8,column=2,sticky='W') 276 | ToolTip(self.MotionFilename,3014) 277 | 278 | self.IntraPeriodChanged(None) 279 | self.MotionOutputChanged(None) 280 | 281 | def IntraPeriodChanged ( self, val ): 282 | if self.IntraPeriod.current() == 2: 283 | self.FrameCountEdit.config(state='normal') 284 | self.ValidateFrameCount(self.FrameCount.get()) 285 | self.FrameCountEdit.focus_set() 286 | else: self.FrameCountEdit.config(state='disabled') 287 | self.SomethingChanged(None) 288 | def ValidateFrameCount ( self, textToValidate ): 289 | try: val = int(textToValidate) 290 | except: return False 291 | self.SomethingChanged(None) 292 | return True 293 | def MotionOutputChanged ( self, val ): 294 | self.SelectMotionOutputFile.config(state='disabled' \ 295 | if self.MotionOutputFile.get() else 'normal') 296 | self.SomethingChanged(None) 297 | def SelectMotionOutputFilePressed ( self ): 298 | if self.MotionOutputFilename == "": 299 | path = self.data 300 | filename = "Motion.mot" 301 | else: 302 | drive, path = os.path.splitdrive(self.MotionOutputFilename) 303 | path, filename = os.path.split(path) 304 | path = FileDialog.asksaveasfilename(title="Save motion data", 305 | initialdir=path,initialfile=filename, 306 | filetypes=[('Motion files', '.mot')] ) 307 | if path: 308 | self.MotionOutputFilename = path 309 | self.MotionFilename.config(text=path) 310 | self.SomethingChanged(None) 311 | def SaveChanges ( self ): 312 | H264.Profile = self.Profiles.get().replace('(default)','').strip() 313 | H264.Level = self.Levels.get().replace('(default)','').strip() 314 | 315 | H264.IntraPeriod = self.IntraPeriod.current() 316 | if H264.IntraPeriod == 0: H264.IntraPeriod = None 317 | elif H264.IntraPeriod == 1: H264.IntraPeriod = 0 318 | else: 319 | try: val = int(self.FrameCount.get()) 320 | except: val = 1 # Force a good number. Not the best... 321 | if val < 1: val = 1 322 | H264.IntraPeriod = val 323 | 324 | H264.IntraRefresh = self.IntraRefresh.current() 325 | if H264.IntraRefresh == 0: H264.IntraRefresh = None 326 | else: 327 | H264.IntraRefresh = self.IntraRefreshList[H264.IntraRefresh] 328 | 329 | H264.SEI = self.SEI.get() 330 | H264.SPSTiming = self.SPSTiming.get() 331 | H264.InlineHeaders = self.InlineHeaders.get() 332 | 333 | if self.MotionOutputFile.get() is True or not self.MotionOutputFilename: 334 | H264.MotionOutput = None 335 | else: 336 | H264.MotionOutput = self.MotionOutputFilename 337 | 338 | class AllFormats ( BasicNotepage ): 339 | BitRate = 17000000 # what are valid numbers? 340 | QualityH264 = 0 # 'reasonable' quality - H264 10-40 10 high, 20-25 ok 341 | QualityOther = 0 # 'reasonable' quality - MJPEG 1 to 100, 100 highest 342 | @staticmethod 343 | # Called if Reset Camera is clicked 344 | def Reset (): 345 | BitRate = 17000000 346 | QualityH264 = 0 347 | QualityOther = 0 348 | def BuildPage ( self ): 349 | Label(self,text="Bitrate:").grid(row=0,column=0,sticky='W'); 350 | Label(self,text="Quality:").grid(row=1,column=0,sticky='W'); 351 | def SaveChanges ( self ): 352 | pass 353 | 354 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman --------------------------------------------------------------------------------