├── .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 |
- Initial release. Only tested under Python 2.7X
- Tested using the RPI V1 camera module
|
16 | | 0.2 | - Tested using Python ver 2.7.13 and Python ver 3.5.3. If there are any problems please send me new issues.
- Awaiting a V2 camera module for test.
- User interface update. Reorganized controls, added new icons.
- Added tooltips. The file **/Assets/Tooltips.txt** can be modified by the user to add his/her own tips
- Additional camera functionality in accordance with https://picamera.readthedocs.io/en/release-1.13/.
- New dialogs for Preferences, Video and Image capture formats, and Annotation.
- Support for many of the image effect parameters.
- Code for the Camera programming tabs **Basic**, **Exposure**, and **Advanced** moved to separate files.
|
17 | | | |
18 |
19 |
20 |
21 | 
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 | 
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
--------------------------------------------------------------------------------