1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# python-gphoto2 - Python interface to libgphoto2 5# http://github.com/jim-easterbrook/python-gphoto2 6# Copyright (C) 2019 "sdaau" 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 3 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, see <http://www.gnu.org/licenses/>. 20 21# another camera config gui, with load/save settings to file, and live view 22# started: sdaau 2019, on with python3-gphoto2 and `sudo -H pip2 install gphoto2`, Ubuntu 18.04 23# uses camera-config-gui.py, and code from focus-gui.py, time_lapse.py 24 25""" 26NOTE that properties are reported by the camera, depending on the camera state! On Canon S3 IS: 27For instance, if camera is not in capture mode, then upon --save-cam-conf-json, the properties "imgsettings" and "capturesettings" (which otherwise contain other properties as children) will return error. 28If camera is in capture mode, and prop "shootingmode" is "Auto" - then "iso" has "Auto" option, "exposurecompensation" and "flashcompensation" is read-write, "aperture" and "shutterspeed" is read-only 29If camera is in capture mode, and prop "shootingmode" is "Manual" - then "iso" loses "Auto" option, "exposurecompensation" and "flashcompensation" become read-only, "aperture" and "shutterspeed" become read-write 30NOTE: some properties may take a while to update, see [waiting for variables to update, and wait_for_event causing picture to be taken? · Issue #75 · jim-easterbrook/python-gphoto2 · GitHub](https://github.com/jim-easterbrook/python-gphoto2/issues/75) 31Upon loading json files on camera that change some modes, you might get an error, in which case you can try loading the same file again 32NOTE: if doing live view on Canon, while capture=0, then still an image is returned - but it is the last captured image (except if immediately after camera startup, when capture=0 is ignored, and live view captures regardless). However, sometimes live view may freeze even with capture=1 - in which case, switching Camera Output from Off to LCD/Video Out and back to Off helps. 33NOTE: Switching from viewing full res image capture, to lower resolution live view preview, and vice versa, should automatically fit the view. But if the view gets "lost": in that case, hit Ctrl-F for fit to view once, then can do Ctrl-Z for original scale (or mouse wheel to zoom in/out the preview). 34""" 35 36from __future__ import print_function 37 38import io 39import logging 40import math 41import sys, os 42import re 43import argparse 44import json, codecs 45from collections import OrderedDict 46from datetime import datetime 47import time 48import tzlocal # sudo -H pip install tzlocal # pip2, pip3 49my_timezone = tzlocal.get_localzone() 50 51from PIL import Image, ImageDraw, ImageFont 52 53from PyQt5 import QtCore, QtWidgets, QtGui 54from PyQt5.QtCore import Qt, pyqtSignal, QPoint, QRect 55from PyQt5.QtGui import QIcon 56from PyQt5.QtWidgets import QFileDialog 57 58import gphoto2 as gp 59 60THISSCRIPTDIR = os.path.dirname(os.path.realpath(__file__)) 61sys.path.insert(1,THISSCRIPTDIR) 62ccgoo = __import__('camera-config-gui') 63NOCAMIMG = "cam-conf-no-cam.png" 64APPNAME = "cam-conf-view-gui.py" 65 66patTrailDigSpace = re.compile(r'(?<=\.)(\d+?)(0+)(?=[^\d]|$)') # SO: 32348435 67 68# blacklist by properties' names, since on a Canon S3 IS, serialnumber etc are read-write 69# make it react by regex too, as on a Canon S3 IS there is read-write: 70# 'd04a' = '0' (PTP Property 0xd04a)), 'd034' = '1548282664' (UNIX Time)) ... 71# which should likely not be changed; most of ^d0.* properties are read-only or duplicates, 72# but still good to ignore them all in one go: 73BLACKLISTPROPSREGEX = [ 74 'opcode', 'datetime', 'serialnumber', 'manufacturer', 'cameramodel', 'deviceversion', 'vendorextension', r'^d0' 75] 76BLACKLISTPROPSREGEX = [re.compile(x) for x in BLACKLISTPROPSREGEX] 77 78PROPNAMESTOSETFIRST = [ "shootingmode" ] 79MINFPS=0.5 # just for the text in status bar 80SLEEPWAITCHANGE=2 # amount of seconds to wait for variable update after first pass, when loading json file 81 82def get_camera_model(camera_config): 83 # get the camera model 84 OK, camera_model = gp.gp_widget_get_child_by_name( 85 camera_config, 'cameramodel') 86 if OK < gp.GP_OK: 87 OK, camera_model = gp.gp_widget_get_child_by_name( 88 camera_config, 'model') 89 if OK >= gp.GP_OK: 90 camera_model = camera_model.get_value() 91 else: 92 camera_model = '' 93 return camera_model 94 95def get_gphoto2_CameraWidgetType_string(innumenum): 96 switcher = { 97 0: "GP_WIDGET_WINDOW", 98 1: "GP_WIDGET_SECTION", 99 2: "GP_WIDGET_TEXT", 100 3: "GP_WIDGET_RANGE", 101 4: "GP_WIDGET_TOGGLE", 102 5: "GP_WIDGET_RADIO", 103 6: "GP_WIDGET_MENU", 104 7: "GP_WIDGET_BUTTON", 105 8: "GP_WIDGET_DATE" 106 } 107 return switcher.get(innumenum, "Invalid camwidget type") 108 109class PropCount(object): 110 def __init__(self): 111 self.numro = 0 112 self.numrw = 0 113 self.numtot = 0 114 self.numexc = 0 115 def __str__(self): 116 return "{},{},{},{}".format(self.numtot,self.numrw,self.numro,self.numexc) 117 118def get_formatted_ts(inunixts=None): 119 if inunixts is None: 120 unixts = time.time() 121 else: 122 unixts = inunixts 123 unixts = round(unixts,6) 124 tzlocalts = tzlocal.get_localzone().localize(datetime.utcfromtimestamp(unixts), is_dst=None).replace(microsecond=0) 125 isots = tzlocalts.isoformat(' ') 126 fsuffts = tzlocalts.strftime('%Y%m%d_%H%M%S') # file suffix timestamp 127 return (unixts, isots, fsuffts) 128 129def get_stamped_filename(infilename, inunixts): 130 unixts, isots, fsuffts = get_formatted_ts( inunixts ) 131 outfilename = re.sub(r"_\d{8}_\d{6}", "", infilename) # remove any of our timestamps 132 # "split" at first period (if any), and insert (or append) our timestamp there: 133 outfilename = re.sub(r'^([^.\n]*)([.]*)(.*)$', r'\1_'+fsuffts+r'\2\3', outfilename) 134 return outfilename 135 136def get_camera_config_children(childrenarr, savearr, propcount): 137 for child in childrenarr: 138 tmpdict = OrderedDict() 139 propcount.numtot += 1 140 if child.get_readonly(): 141 propcount.numro += 1 142 else: 143 propcount.numrw += 1 144 tmpdict['idx'] = str(propcount) 145 tmpdict['ro'] = child.get_readonly() 146 tmpdict['name'] = child.get_name() 147 tmpdict['label'] = child.get_label() 148 tmpdict['type'] = child.get_type() 149 tmpdict['typestr'] = get_gphoto2_CameraWidgetType_string( tmpdict['type'] ) 150 if ((tmpdict['type'] == gp.GP_WIDGET_RADIO) or (tmpdict['type'] == gp.GP_WIDGET_MENU)): 151 tmpdict['count_choices'] = child.count_choices() 152 tmpchoices = [] 153 for choice in child.get_choices(): 154 tmpchoices.append(choice) 155 tmpdict['choices'] = ",".join(tmpchoices) 156 if (child.count_children() > 0): 157 tmpdict['children'] = [] 158 get_camera_config_children(child.get_children(), tmpdict['children'], propcount) 159 else: 160 # NOTE: camera HAS to be "into preview mode to raise mirror", otherwise at this point can get "gphoto2.GPhoto2Error: [-2] Bad parameters" for get_value 161 try: 162 tmpdict['value'] = child.get_value() 163 except Exception as ex: 164 tmpdict['value'] = "{} {}".format( type(ex).__name__, ex.args) 165 propcount.numexc += 1 166 savearr.append(tmpdict) 167 168def get_camera_config_object(camera_config, inunixts=None): 169 retdict = OrderedDict() 170 retdict['camera_model'] = get_camera_model(camera_config) 171 if inunixts is None: myunixts = time.time() 172 else: myunixts = inunixts 173 unixts, isots, fsuffts = get_formatted_ts( myunixts ) 174 retdict['ts_taken_on'] = unixts 175 retdict['date_taken_on'] = isots 176 propcount = PropCount() 177 retarray = [] 178 retdict['children'] = [] 179 get_camera_config_children(camera_config.get_children(), retdict['children'], propcount) 180 excstr = "no errors - OK." if (propcount.numexc == 0) else "{} errors - bad (please check if camera mirror is up)!".format(propcount.numexc) 181 print("Parsed camera config: {} properties total, of which {} read-write and {} read-only; with {}".format(propcount.numtot, propcount.numrw, propcount.numro, excstr)) 182 return retdict 183 184def put_camera_capture_preview_mirror(camera, camera_config, camera_model): 185 if camera_model == 'unknown': 186 # find the capture size class config item 187 # need to set this on my Canon 350d to get preview to work at all 188 OK, capture_size_class = gp.gp_widget_get_child_by_name( 189 camera_config, 'capturesizeclass') 190 if OK >= gp.GP_OK: 191 # set value 192 value = capture_size_class.get_choice(2) 193 capture_size_class.set_value(value) 194 # set config 195 camera.set_config(camera_config) 196 else: 197 # put camera into preview mode to raise mirror 198 ret = gp.gp_camera_capture_preview(camera) # OK, camera_file 199 #print(ret) # [0, <Swig Object of type 'CameraFile *' at 0x7fb5a0044a40>] 200 201def start_capture_view(): 202 camera = gp.Camera() 203 hasCamInited = False 204 try: 205 camera.init() # prints: WARNING: gphoto2: (b'gp_context_error') b'Could not detect any camera' if logging set up 206 hasCamInited = True 207 except Exception as ex: 208 lastException = ex 209 print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args)) 210 if hasCamInited: 211 camera_config = camera.get_config() 212 camera_model = get_camera_model(camera_config) 213 put_camera_capture_preview_mirror(camera, camera_config, camera_model) 214 print("Started capture view (extended lens/raised mirror) on camera: {}".format(camera_model)) 215 sys.exit(0) 216 else: # camera not inited 217 print("Sorry, no camera present, cannot execute command; exiting.") 218 sys.exit(1) 219 220def stop_capture_view(): 221 camera = gp.Camera() 222 hasCamInited = False 223 try: 224 camera.init() 225 hasCamInited = True 226 except Exception as ex: 227 lastException = ex 228 print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args)) 229 if hasCamInited: 230 camera_config = camera.get_config() 231 camera_model = get_camera_model(camera_config) 232 # https://github.com/gphoto/gphoto2/issues/195 233 OK, capture = gp.gp_widget_get_child_by_name( camera_config, 'capture' ) 234 if OK >= gp.GP_OK: 235 capture.set_value(0) 236 camera.set_config(camera_config) 237 print("Stopped capture view (retracted lens/released mirror) on camera: {} ({} {})".format(camera_model, OK, capture)) 238 sys.exit(0) 239 else: # camera not inited 240 print("Sorry, no camera present, cannot execute command; exiting.") 241 sys.exit(1) 242 243# mostly from time_lapse.py (_send_file is from focus-gui.py) 244def do_capture_image(camera): 245 # adjust camera configuratiuon 246 cfg = camera.get_config() 247 capturetarget_cfg = cfg.get_child_by_name('capturetarget') 248 capturetarget = capturetarget_cfg.get_value() 249 capturetarget_cfg.set_value('Internal RAM') 250 camera.set_config(cfg) 251 # do capture 252 path = camera.capture(gp.GP_CAPTURE_IMAGE) 253 print('capture cam path: {} {}'.format(path.folder, path.name)) 254 camera_file = camera.file_get( 255 path.folder, path.name, gp.GP_FILE_TYPE_NORMAL) 256 # saving of image implied in current directory: 257 camera_file.save(path.name) 258 camera.file_delete(path.folder, path.name) 259 # reset configuration 260 capturetarget_cfg.set_value(capturetarget) 261 camera.set_config(cfg) 262 return path.name 263 264 265def get_json_filters(args): 266 jsonfilters = [] 267 if hasattr(args, 'include_names_json'): 268 splitnames = args.include_names_json.split(",") 269 for iname in splitnames: 270 splitnameeq = iname.split("=") # 1-element array if = not present; 2-element array if present 271 splitnameeq = list(filter(None, splitnameeq)) 272 jsonfilters.append(splitnameeq) 273 jsonfilters = list(filter(None, jsonfilters)) 274 return jsonfilters 275 276def copy_json_filter_props(inarr, outarr, inpropcount, outpropcount, jsonfilters): 277 for inchild in inarr: 278 inpropcount.numtot += 1 279 if inchild['ro'] == 1: 280 inpropcount.numro += 1 281 else: 282 inpropcount.numrw += 1 283 shouldCopy = False 284 if len(jsonfilters)>0: 285 for jfilter in jsonfilters: 286 if len(jfilter) == 2: 287 key = jfilter[0] ; val = jfilter[1] 288 if ( str(inchild[key]) == val ): 289 shouldCopy = True 290 elif len(jfilter) == 1: 291 if inchild["name"] == jfilter[0]: 292 shouldCopy = True 293 else: # len(jsonfilters) == 0: ; no filters, pass everything 294 shouldCopy = True 295 # copy with flatten hierarchy - only copy dicts that have 'value' (else they have 'children'): 296 if ( ('value' in inchild.keys()) and (inchild['value'] is not None) ): 297 if shouldCopy: 298 outpropcount.numtot += 1 299 if inchild['ro'] == 1: 300 outpropcount.numro += 1 301 else: 302 outpropcount.numrw += 1 303 if 'Error' in str(inchild['value']): 304 outpropcount.numexc += 1 305 outarr.append(inchild) 306 else: # if the child dict has no 'value' then it has 'children' 307 if 'children' in inchild.keys(): 308 copy_json_filter_props(inchild['children'], outarr, inpropcount, outpropcount, jsonfilters) 309 310 311def do_GetSaveCamConfJson(camera, jsonfile, inunixts=None): 312 camera_config = camera.get_config() # may print: WARNING: gphoto2: (b'_get_config [config.c:7649]') b"Type of property 'Owner Name' expected: 0x4002 got: 0x0000" 313 camconfobj = get_camera_config_object(camera_config, inunixts) 314 with open(jsonfile, 'wb') as f: 315 json.dump(camconfobj, codecs.getwriter('utf-8')(f), ensure_ascii=False, indent=2, separators=(',', ': ')) 316 print("Saved config to {}".format(jsonfile)) 317 318 319def getSaveCamConfJson(args): 320 if (not(args.save_cam_conf_json)): 321 print("getSaveCamConfJson: Sorry, unusable/empty output .json filename; aborting") 322 sys.exit(1) 323 jsonfile = os.path.realpath(args.save_cam_conf_json) 324 print("getSaveCamConfJson: saving to {}".format(jsonfile)) 325 camera = gp.Camera() 326 hasCamInited = False 327 try: 328 camera.init() 329 hasCamInited = True 330 except Exception as ex: 331 lastException = ex 332 print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args)) 333 if hasCamInited: 334 do_GetSaveCamConfJson(camera, jsonfile) 335 print("Exiting.") 336 sys.exit(0) 337 else: # camera not inited 338 print("Sorry, no camera present, cannot execute command; exiting.") 339 sys.exit(1) 340 341def copyFilterCamConfJson(args): 342 if (not(args.load_cam_conf_json) or not(args.save_cam_conf_json)): 343 print("copyFilterCamConfJson: Sorry, unusable/empty input or output .json filename; aborting") 344 sys.exit(1) 345 injsonfile = os.path.realpath(args.load_cam_conf_json) 346 outjsonfile = os.path.realpath(args.save_cam_conf_json) 347 print("loadSetCamConfJson: input load from {}, output save to {}".format(injsonfile, outjsonfile)) 348 # no need for camera here 349 # open and parse injsonfile as object 350 with open(injsonfile) as in_data_file: 351 indatadict = json.load(in_data_file, object_pairs_hook=OrderedDict) 352 # check filters 353 jsonfilters = get_json_filters(args) 354 jsonfilterslen = len(jsonfilters) 355 if jsonfilterslen > 0: 356 print("Got {} json filters; including only properties names(/values) in filters in output file".format(jsonfilterslen)) 357 else: 358 print("Got no ({}) json filters; copying input (flattened) to output file".format(jsonfilterslen)) 359 retdict = OrderedDict() 360 retdict['camera_model'] = indatadict['camera_model'] 361 retdict['orig_ts_taken_on'] = indatadict['ts_taken_on'] 362 retdict['orig_date_taken_on'] = indatadict['date_taken_on'] 363 unixts, isots, fsuffts = get_formatted_ts( time.time() ) 364 retdict['ts_taken_on'] = unixts 365 retdict['date_taken_on'] = isots 366 inpropcount = PropCount() 367 outpropcount = PropCount() 368 retdict['children'] = [] 369 copy_json_filter_props(indatadict['children'], retdict['children'], inpropcount, outpropcount, jsonfilters) 370 print("Processed input props: {} ro, {} rw, {} total; got output props: {} ro, {} rw, {} total".format( 371 inpropcount.numro, inpropcount.numrw, inpropcount.numtot, outpropcount.numro, outpropcount.numrw, outpropcount.numtot 372 )) 373 # save 374 with open(outjsonfile, 'wb') as f: 375 json.dump(retdict, codecs.getwriter('utf-8')(f), ensure_ascii=False, indent=2, separators=(',', ': ')) 376 print("Saved filtered copy to output file {}".format(outjsonfile)) 377 sys.exit(0) 378 379def do_LoadSetCamConfJson(camera, injsonfile): 380 camera_config = camera.get_config() 381 # open and parse injsonfile as object 382 with open(injsonfile) as in_data_file: 383 indatadict = json.load(in_data_file, object_pairs_hook=OrderedDict) 384 print("Getting flattened object (removes hierarchy/nodes with only children and without value) from {} ...".format(injsonfile)) 385 inpropcount = PropCount() 386 outpropcount = PropCount() 387 jsonfilters = [] # empty here 388 flatproparray = [] 389 copy_json_filter_props(indatadict['children'], flatproparray, inpropcount, outpropcount, jsonfilters) 390 print("Flattened {} ro, {} rw, {} total input props to: {} ro, {} rw, {} total flat props".format( 391 inpropcount.numro, inpropcount.numrw, inpropcount.numtot, outpropcount.numro, outpropcount.numrw, outpropcount.numtot 392 )) 393 print("Applying at most {} read/write props (ignoring {} read-only ones, out of {} total props) to camera:".format( 394 outpropcount.numrw, outpropcount.numro, len(flatproparray) 395 )) 396 numappliedprops = 0 397 usedlabels = [] ; usednames = [] 398 # NOTE - some of the camera properties depend on others; 399 # say for shootingmode=Manual, some vars are r/w, but for shootingmode=Auto, same vars become r/o 400 # thus we split off flatproparray in two parts, executed first the one, then the other 401 flatproparrayfirst = [] 402 for iname in PROPNAMESTOSETFIRST: # SO: 54343917 403 # find the object having the name iname 404 foundidx = -1 405 for ix, idict in enumerate(flatproparray): 406 if idict.get('name') == iname: 407 foundidx = ix 408 break 409 if foundidx > -1: 410 # remove dict object via pop at index, save removed object 411 remdict = flatproparray.pop(foundidx) 412 # add removed object to newarr: 413 flatproparrayfirst.append(remdict) 414 print("First pass (of applying cam settings):") 415 usednamesfirst = [] 416 passedpropsfirst = 0 417 # no need to check duplicates or blacklisted here: 418 for ix, tprop in enumerate(flatproparrayfirst): 419 if tprop['ro'] == 1: 420 print(" {:3d}: (ignoring read-only prop '{}' ({}))".format(ix+1, tprop['name'], tprop['label'] )) 421 else: 422 numappliedprops += 1 423 print(" {:3d}: Applying prop {}/{}: '{}' = '{}' ({}))".format(ix+1, numappliedprops, outpropcount.numrw, tprop['name'], tprop['value'], tprop['label'])) 424 usedlabels.append(tprop['label']) 425 usednamesfirst.append(tprop['name']) 426 OK, gpprop = gp.gp_widget_get_child_by_name( camera_config, "{}".format(tprop['name']) ) 427 if OK >= gp.GP_OK: 428 if ( type(tprop['value']).__name__ == "unicode" ): 429 gpprop.set_value( "{}".format(tprop['value']) ) 430 else: 431 gpprop.set_value( tprop['value'] ) 432 passedpropsfirst = ix+1 433 print(" applying props: {}.".format(",".join(usednamesfirst))) 434 camera.set_config(camera_config) 435 # sleeping for 5 sec seems enough to allow changes between auto and manual shootingmode, without taking a picture... but 1 sec is not; 2 seems enough.. 436 time.sleep(SLEEPWAITCHANGE) 437 print("Second pass (of applying cam settings):") 438 for ix, tprop in enumerate(flatproparray): 439 propnum = passedpropsfirst + ix + 1 440 if tprop['ro'] == 1: 441 print(" {:3d}: (ignoring read-only prop '{}' ({}))".format(propnum, tprop['name'], tprop['label'] )) 442 # only look for exact duplicates - rest, if needed, can be blacklisted: 443 elif ( tprop['label'] in usedlabels ): 444 print(" {:3d}: (ignoring duplicate label prop '{}' ({}))".format(propnum, tprop['name'], tprop['label'] )) 445 elif ( any([pat.match(tprop['name']) for pat in BLACKLISTPROPSREGEX]) ): 446 print(" {:3d}: (ignoring blacklisted name prop '{}' ({}))".format(propnum, tprop['name'], tprop['label'] )) 447 else: 448 numappliedprops += 1 449 print(" {:3d}: Applying prop {}/{}: '{}' = '{}' ({}))".format(propnum, numappliedprops, outpropcount.numrw, tprop['name'], tprop['value'], tprop['label'])) 450 usedlabels.append(tprop['label']) 451 usednames.append(tprop['name']) 452 OK, gpprop = gp.gp_widget_get_child_by_name( camera_config, "{}".format(tprop['name']) ) 453 if OK >= gp.GP_OK: 454 if ( type(tprop['value']).__name__ == "unicode" ): 455 gpprop.set_value( "{}".format(tprop['value']) ) 456 else: 457 gpprop.set_value( tprop['value'] ) 458 print(" applying props: {} (+ {}).".format( ",".join(usednames), ",".join(usednamesfirst) )) 459 camera.set_config(camera_config) 460 print("Applied {} properties from file {} to camera.".format(numappliedprops, injsonfile)) 461 462def loadSetCamConfJson(args): 463 if (not(args.load_cam_conf_json)): 464 print("loadSetCamConfJson: Sorry, unusable/empty output .json filename; aborting") 465 sys.exit(1) 466 injsonfile = os.path.realpath(args.load_cam_conf_json) 467 print("loadSetCamConfJson: loading from {}".format(injsonfile)) 468 camera = gp.Camera() 469 ctx = gp.Context() 470 hasCamInited = False 471 try: 472 camera.init(ctx) 473 hasCamInited = True 474 except Exception as ex: 475 lastException = ex 476 print("No camera: {} {}; ".format( type(lastException).__name__, lastException.args)) 477 if hasCamInited: 478 do_LoadSetCamConfJson(camera, injsonfile) 479 print("Exiting.") 480 sys.exit(0) 481 else: 482 print("Sorry, no camera present, cannot execute command; exiting.") 483 sys.exit(1) 484 485 486# SO:35514531 - see also SO:46934526, 40683840, 9475772, https://github.com/baoboa/pyqt5/blob/master/examples/widgets/imageviewer.py 487class PhotoViewer(QtWidgets.QGraphicsView): 488 photoClicked = QtCore.pyqtSignal(QtCore.QPoint) 489 490 def __init__(self, parent): 491 super(PhotoViewer, self).__init__(parent) 492 self.parent = parent 493 self.isMouseOver = False 494 self.ZOOMFACT = 1.25 495 self._zoom = 0 496 self._zoomfactor = 1 497 self._empty = True 498 self._scene = QtWidgets.QGraphicsScene(self) 499 self._photo = QtWidgets.QGraphicsPixmapItem() 500 self._scene.addItem(self._photo) 501 self.setScene(self._scene) 502 self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 503 self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 504 self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) 505 self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) 506 self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(30, 30, 30))) 507 self.setFrameShape(QtWidgets.QFrame.NoFrame) 508 509 def hasPhoto(self): 510 return not self._empty 511 512 def printUnityFactor(self): 513 rect = QtCore.QRectF(self._photo.pixmap().rect()) 514 unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) 515 viewrect = self.viewport().rect() 516 scenerect = self.transform().mapRect(rect) 517 factor = min(viewrect.width() / scenerect.width(), 518 viewrect.height() / scenerect.height()) 519 print("puf factor {} vr_w {} sr_w {} u_w {} vr_h {} sr_h {} u_h {} ".format(factor, viewrect.width(), scenerect.width(), unity.width(), viewrect.height(), scenerect.height(), unity.height() )) 520 521 def fitInView(self, scale=True): 522 rect = QtCore.QRectF(self._photo.pixmap().rect()) 523 if not rect.isNull(): 524 self.setSceneRect(rect) 525 if self.hasPhoto(): 526 unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) 527 self.scale(1 / unity.width(), 1 / unity.height()) 528 viewrect = self.viewport().rect() 529 scenerect = self.transform().mapRect(rect) 530 factor = min(viewrect.width() / scenerect.width(), 531 viewrect.height() / scenerect.height()) 532 # here, view scaled to fit: 533 self._zoomfactor = factor 534 self._zoom = math.log( self._zoomfactor, self.ZOOMFACT ) 535 self.scale(factor, factor) 536 self.parent.updateStatusBar() 537 if (self.isMouseOver): # should be true on wheel, regardless 538 self.setDragState() 539 540 def setPhoto(self, pixmap=None): 541 if pixmap and not pixmap.isNull(): 542 self._empty = False 543 self._photo.setPixmap(pixmap) 544 else: 545 self._empty = True 546 self._photo.setPixmap(QtGui.QPixmap()) 547 548 def resetZoom(self): 549 if self.hasPhoto(): 550 self._zoom = 0 551 self._zoomfactor = 1.0 552 unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1)) 553 self.scale(1 / unity.width(), 1 / unity.height()) 554 self.parent.updateStatusBar() 555 if (self.isMouseOver): 556 self.setDragState() 557 558 def zoomPlus(self): 559 if self.hasPhoto(): 560 factor = self.ZOOMFACT # 1.25 561 self._zoomfactor = self._zoomfactor * self.ZOOMFACT 562 self._zoom += 1 563 self.scale(factor, factor) 564 self.parent.updateStatusBar() 565 self.setDragState() 566 567 def zoomMinus(self): 568 if self.hasPhoto(): 569 factor = 1.0/self.ZOOMFACT #0.8 570 self._zoomfactor = self._zoomfactor / self.ZOOMFACT 571 self._zoom -= 1 572 self.scale(factor, factor) 573 self.parent.updateStatusBar() 574 self.setDragState() 575 576 def wheelEvent(self, event): 577 if self.hasPhoto(): 578 if event.angleDelta().y() > 0: 579 self.zoomPlus() 580 else: 581 self.zoomMinus() 582 583 def mousePressEvent(self, event): 584 if self._photo.isUnderMouse(): 585 self.photoClicked.emit(QtCore.QPoint(event.pos())) 586 super(PhotoViewer, self).mousePressEvent(event) 587 588 def getCanDrag(self): 589 return ((self.horizontalScrollBar().maximum() > 0) or (self.verticalScrollBar().maximum() > 0)) 590 591 def setDragState(self): 592 # here we mostly want to take case of the mouse cursor/pointer - and show the hand only when dragging is possible 593 canDrag = self.getCanDrag() 594 if (canDrag): 595 self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag) 596 else: 597 self.setDragMode(QtWidgets.QGraphicsView.NoDrag) 598 599 def enterEvent(self, event): 600 self.isMouseOver = True 601 self.setDragState() 602 return super(PhotoViewer, self).enterEvent(event) 603 604 def leaveEvent(self, event): 605 self.isMouseOver = False 606 # no need for setDragState - is autohandled, as we leave 607 return super(PhotoViewer, self).enterEvent(event) 608 609# monkeypatch ccgoo.RangeWidget - it reacted only on self.sliderReleased.connect(self.new_value), 610# which reacts only if slider dragged first, making it impossible to control from say VNC 611class MyRangeWidget(QtWidgets.QSlider): 612 def __init__(self, config_changed, config, parent=None): 613 QtWidgets.QSlider.__init__(self, Qt.Horizontal, parent) 614 self.config_changed = config_changed 615 self.config = config 616 if self.config.get_readonly(): 617 self.setDisabled(True) 618 assert self.config.count_children() == 0 619 lo, hi, self.inc = self.config.get_range() 620 value = self.config.get_value() 621 self.setRange(int(lo * self.inc), int(hi * self.inc)) 622 self.setValue(int(value * self.inc)) 623 self.valueChanged.connect(self.new_value) 624 625 def mousePressEvent(self, e): 626 if e.button() == Qt.LeftButton: 627 e.accept() 628 x = e.pos().x() 629 value = (self.maximum() - self.minimum()) * x / self.width() + self.minimum() 630 self.setValue(value) 631 else: 632 return super().mousePressEvent(self, e) 633 634 def new_value(self): 635 value = float(self.value()) * self.inc 636 self.config.set_value(value) 637 self.config_changed() 638ccgoo.RangeWidget = MyRangeWidget 639 640class MainWindow(QtWidgets.QMainWindow): 641 quit_action = None 642 new_image_sig = QtCore.pyqtSignal(object) 643 644 def _reset_config(self): 645 if self.hasCamInited: 646 if self.old_capturetarget is not None: 647 # find the capture target item 648 OK, capture_target = gp.gp_widget_get_child_by_name( 649 self.camera_config, 'capturetarget') 650 if OK >= gp.GP_OK: 651 # set config 652 capture_target.set_value(self.old_capturetarget) 653 self.camera.set_config(self.camera_config) 654 self.old_capturetarget = None 655 656 def closeEvent(self, event): 657 if self.hasCamInited: 658 self.running = False 659 self._reset_config() 660 self.camera.exit() 661 self.settings.setValue("geometry", self.saveGeometry()) 662 self.settings.setValue("windowState", self.saveState()) 663 return super(MainWindow, self).closeEvent(event) 664 665 def _set_config(self): 666 if self.hasCamInited: 667 # find the capture target item 668 OK, capture_target = gp.gp_widget_get_child_by_name( 669 self.camera_config, 'capturetarget') 670 if OK >= gp.GP_OK: 671 if self.old_capturetarget is None: 672 self.old_capturetarget = capture_target.get_value() 673 choice_count = capture_target.count_choices() 674 for n in range(choice_count): 675 choice = capture_target.get_choice(n) 676 if 'internal' in choice.lower(): 677 # set config 678 capture_target.set_value(choice) 679 self.camera.set_config(self.camera_config) 680 break 681 # find the image format config item 682 # camera dependent - 'imageformat' is 'imagequality' on some 683 OK, image_format = gp.gp_widget_get_child_by_name( 684 self.camera_config, 'imageformat') 685 if OK >= gp.GP_OK: 686 # get current setting 687 value = image_format.get_value() 688 # make sure it's not raw 689 if 'raw' in value.lower(): 690 print('Cannot preview raw images') 691 return False 692 return True 693 else: 694 return False 695 696 def set_splitter(self, inqtsplittertype, inwidget1, inwidget2): 697 if (hasattr(self,'splitter1')): 698 self.mainwid.layout().removeWidget(self.splitter1) 699 self.splitter1.close() 700 self.splitter1 = QtWidgets.QSplitter(inqtsplittertype) 701 self.splitter1.addWidget(inwidget1) 702 self.splitter1.addWidget(inwidget2) 703 self.splitter1.setSizes([600, 600]); # equal splitter at start 704 self.mainwid.layout().addWidget(self.splitter1) 705 self.mainwid.layout().update() 706 707 def create_main_menu(self): 708 self.mainMenu = self.menuBar() 709 self.fileMenu = self.mainMenu.addMenu('&File') 710 # actions 711 self.load_action = QtWidgets.QAction('&Load settings', self) 712 self.load_action.setShortcuts(['Ctrl+L']) 713 self.load_action.triggered.connect(self.load_settings) 714 self.save_action = QtWidgets.QAction('&Save settings', self) 715 self.save_action.setShortcuts(['Ctrl+S']) 716 self.save_action.triggered.connect(self.save_settings) 717 self.fileMenu.addAction(self.load_action) 718 self.fileMenu.addAction(self.save_action) 719 self.fileMenu.addAction(self.quit_action) 720 self.viewMenu = self.mainMenu.addMenu('&View') 721 self.zoomorig_action = QtWidgets.QAction('&Zoom original', self) 722 self.zoomorig_action.setShortcuts(['Ctrl+Z']) 723 self.zoomorig_action.triggered.connect(self.zoom_original) 724 self.zoomfitview_action = QtWidgets.QAction('Zoom to &fit view', self) 725 self.zoomfitview_action.setShortcuts(['Ctrl+F']) 726 self.zoomfitview_action.triggered.connect(self.zoom_fit_view) 727 self.zoomplus_action = QtWidgets.QAction('Zoom plu&s', self) 728 self.zoomplus_action.setShortcuts(['+']) 729 self.zoomplus_action.triggered.connect(self.zoom_plus) 730 self.zoomminus_action = QtWidgets.QAction('Zoom &minus', self) 731 self.zoomminus_action.setShortcuts(['-']) 732 self.zoomminus_action.triggered.connect(self.zoom_minus) 733 self.switchlayout_action = QtWidgets.QAction('Switch &Layout', self) 734 self.switchlayout_action.setShortcuts(['Ctrl+A']) 735 self.switchlayout_action.triggered.connect(self.switch_splitter_layout) 736 self.dopreview_action = QtWidgets.QAction('Do &Preview', self) 737 self.dopreview_action.setShortcuts(['Ctrl+X']) 738 self.dopreview_action.triggered.connect(self._do_preview) 739 self.repeatpreview_action = QtWidgets.QAction('&Repeat Preview', self) 740 self.repeatpreview_action.setShortcuts(['Ctrl+R']) 741 self.repeatpreview_action.setCheckable(True) 742 self.repeatpreview_action.setChecked(False) 743 self.repeatpreview_action.triggered.connect(self.continuous) 744 self.docapture_action = QtWidgets.QAction('Capture &Image', self) 745 self.docapture_action.setShortcuts(['Ctrl+I']) 746 self.docapture_action.triggered.connect(self._capture_image) 747 self.viewMenu.addAction(self.switchlayout_action) 748 self.viewMenu.addAction(self.dopreview_action) 749 self.viewMenu.addAction(self.repeatpreview_action) 750 self.viewMenu.addAction(self.docapture_action) 751 self.viewMenu.addAction(self.zoomorig_action) 752 self.viewMenu.addAction(self.zoomfitview_action) 753 self.viewMenu.addAction(self.zoomplus_action) 754 self.viewMenu.addAction(self.zoomminus_action) 755 756 def replicate_ccgoo_main_window(self): 757 # main widget 758 self.widget = QtWidgets.QWidget() 759 self.widget.setLayout(QtWidgets.QGridLayout()) 760 self.widget.layout().setColumnStretch(0, 1) 761 # 'apply' button 762 if self.args.config_apply_btn == 1: 763 self.apply_button = QtWidgets.QPushButton('apply changes') 764 self.apply_button.setEnabled(False) 765 self.apply_button.clicked.connect(self.apply_changes) 766 self.widget.layout().addWidget(self.apply_button, 1, 1) 767 768 def eventFilter(self, source, event): 769 return super(MainWindow, self).eventFilter(source, event) 770 771 def replicate_fg_viewer(self): 772 self.image_display = PhotoViewer(self) 773 self.frameviewlayout.addWidget(self.image_display) 774 self.new_image_sig.connect(self.new_image) 775 776 def updateStatusBar(self): 777 msgstr = "" 778 if self.lastException: 779 msgstr = "No camera: {} {}; ".format( type(self.lastException).__name__, self.lastException.args) 780 if self.hasCamInited: 781 msgstr = "Camera model: {} ; ".format(self.camera_model if (self.camera_model) else "No info") 782 if self.lastImageSize: 783 msgstr += "last imgsize: {} x {} ".format(self.lastImageSize[0], self.lastImageSize[1]) 784 zoomstr = "zoom: {:.3f} {:.3f}".format(self.image_display._zoom, self.image_display._zoomfactor) 785 def replacer(m): 786 retstr = m.group(1).replace(r'0', ' ')+' '*len(m.group(2)) 787 return retstr 788 zoomstr = patTrailDigSpace.sub(lambda m: replacer(m), zoomstr) 789 msgstr += zoomstr 790 if self.fps: 791 msgstr += " ; {}".format(self.fps) 792 self.fps = "" 793 if self.singleStatusMsg: 794 msgstr += " ({})".format(self.singleStatusMsg) 795 self.singleStatusMsg = "" 796 self.statusBar().showMessage(msgstr) 797 798 def checkCreateNoCamImg(self): 799 nocamimgpath = os.path.join(THISSCRIPTDIR, NOCAMIMG) 800 if (not(os.path.isfile(nocamimgpath))): 801 # create gradient background with text: 802 nocamsize = (320, 240) 803 nocamsizec = (nocamsize[0]/2, nocamsize[1]/2) 804 colA = (80, 80, 80) ; colB = (210, 30, 30) ; # colB is center 805 nocamimg = Image.new('RGB', nocamsize, color=0xFF) 806 gradoffset = (-50, 20) 807 nocammaxd = math.sqrt((nocamsizec[0]+abs(gradoffset[0]))**2 + (nocamsizec[1]+abs(gradoffset[1]))**2) 808 for ix in range(nocamsize[0]): 809 for iy in range(nocamsize[1]): 810 dist = math.sqrt( (ix-nocamsizec[0]-gradoffset[0])**2 + (iy-nocamsizec[1]-gradoffset[1])**2 ) 811 distnorm = dist/nocammaxd 812 r = colA[0]*distnorm + colB[0]*(1.0-distnorm) 813 g = colA[1]*distnorm + colB[1]*(1.0-distnorm) 814 b = colA[2]*distnorm + colB[2]*(1.0-distnorm) 815 nocamimg.putpixel((ix, iy), (int(r), int(g), int(b)) ) 816 d = ImageDraw.Draw(nocamimg) 817 d.text((20,nocamsizec[1]-4), "No camera (unknown)", fill=(240,240,240)) 818 nocamimg.save(nocamimgpath) 819 820 821 def __init__(self, args): 822 self.args = args 823 self.current_splitter_style=0 824 self.lastImageSize = None 825 self.lastImageType = None # 0 - preview; 1 - capture image 826 self.timestamp = None 827 self.fps = "" 828 self.lastException = None 829 self.lastOpenPath = None 830 self.lastSavePath = None 831 self.singleStatusMsg = "" 832 self.hasCamInited = False 833 self.do_init = QtCore.QEvent.registerEventType() 834 self.do_next = QtCore.QEvent.registerEventType() 835 self.running = False 836 QtWidgets.QMainWindow.__init__(self) 837 self.settings = QtCore.QSettings("MyCompany", APPNAME) 838 if not self.settings.value("geometry") == None: 839 self.restoreGeometry(self.settings.value("geometry")) 840 if not self.settings.value("windowState") == None: 841 self.restoreState(self.settings.value("windowState")) 842 self.setWindowTitle("Camera config {}".format(APPNAME)) 843 self.setMinimumWidth(1000) 844 self.setMinimumHeight(600) 845 # quit shortcut 846 self.quit_action = QtWidgets.QAction('&Quit', self) 847 self.quit_action.setShortcuts(['Ctrl+Q', 'Ctrl+W']) 848 self.quit_action.setStatusTip('Exit application') 849 self.quit_action.triggered.connect(QtWidgets.qApp.closeAllWindows) 850 self.addAction(self.quit_action) 851 # main menu 852 self.create_main_menu() 853 # replicate main window from camera-config-gui-oo 854 self.replicate_ccgoo_main_window() 855 # frames 856 self.frameview = QtWidgets.QFrame(self) 857 self.frameview.setFrameShape(QtWidgets.QFrame.StyledPanel) 858 self.frameviewlayout = QtWidgets.QGridLayout(self.frameview) 859 self.frameviewlayout.setSpacing(0); 860 self.frameviewlayout.setContentsMargins(0,0,0,0); 861 862 self.checkCreateNoCamImg() 863 864 self.replicate_fg_viewer() 865 866 self.frameconf = QtWidgets.QFrame(self) 867 self.frameconf.setFrameShape(QtWidgets.QFrame.StyledPanel) 868 self.frameconf.setStyleSheet("padding: 0;") # nope 869 self.frameconflayout = QtWidgets.QHBoxLayout(self.frameconf) 870 self.frameconflayout.setSpacing(0); 871 self.frameconflayout.setContentsMargins(0,0,0,0); 872 self.scrollconf = QtWidgets.QScrollArea(self) 873 self.scrollconf.setWidgetResizable(False) 874 # self.contentconf is just used for init here; afterward is replaced by self.widget (self.configsection) 875 self.contentconf = QtWidgets.QWidget() 876 self.contentconf.setLayout(QtWidgets.QGridLayout()) 877 self.contentconf.layout().setColumnStretch(0, 1) 878 self.scrollconf.setWidget(self.contentconf) 879 self.frameconflayout.addWidget(self.scrollconf) 880 881 self.mainwid = QtWidgets.QWidget() 882 self.mainwid.setLayout(QtWidgets.QGridLayout()) 883 self.setCentralWidget(self.mainwid) 884 self.set_splitter(Qt.Horizontal, self.frameview, self.frameconf) 885 886 self.camera = gp.Camera() 887 self.ctx = gp.Context() 888 QtWidgets.QApplication.postEvent( 889 self, QtCore.QEvent(self.do_init), Qt.LowEventPriority - 1) 890 891 def event(self, event): 892 if ( (event.type() != self.do_init) and (event.type() != self.do_next) ): 893 return QtWidgets.QMainWindow.event(self, event) 894 event.accept() 895 if event.type() == self.do_init: 896 QtWidgets.QApplication.setOverrideCursor(Qt.WaitCursor) 897 try: 898 self.initialise() 899 finally: 900 QtWidgets.QApplication.restoreOverrideCursor() 901 return True 902 elif event.type() == self.do_next: 903 self._do_continuous() 904 return True 905 906 def _do_continuous(self): 907 if not self.running: 908 self._reset_config() 909 return 910 if self.hasCamInited: 911 self._do_preview() 912 else: 913 self._do_preview_camnotinit() 914 # post event to trigger next capture 915 QtWidgets.QApplication.postEvent( 916 self, QtCore.QEvent(self.do_next), Qt.LowEventPriority - 1) 917 918 @QtCore.pyqtSlot() 919 def continuous(self): 920 if self.running: 921 self.running = False 922 self.updateStatusBar() # clear last singleStatusMsg - clears fps upon stop 923 return 924 self.running = True 925 self._do_continuous() 926 927 def CameraHandlerInit(self): 928 # get camera config tree 929 self.hasCamInited = False 930 self.lastException = None 931 try: 932 self.camera.init(self.ctx) 933 self.hasCamInited = True 934 except Exception as ex: 935 self.lastException = ex 936 if type(ex) == gp.GPhoto2Error: 937 nocamimgpath = os.path.join(THISSCRIPTDIR, NOCAMIMG) 938 temppixmap = QtGui.QPixmap(nocamimgpath) 939 self.image_display.setPhoto(temppixmap) 940 if self.hasCamInited: 941 self.camera_config = self.camera.get_config() 942 # from CameraHandler::Init: 943 self.old_capturetarget = None 944 # get the camera model 945 self.camera_model = get_camera_model(self.camera_config) 946 put_camera_capture_preview_mirror(self.camera, self.camera_config, self.camera_model) 947 self.updateStatusBar() 948 949 def recreate_section_widget(self): 950 if ( hasattr(self, 'configsection') and (self.configsection) ): 951 self.widget.layout().removeWidget(self.configsection) 952 self.configsection = ccgoo.SectionWidget(self.config_changed, self.camera_config) 953 self.widget.layout().addWidget(self.configsection, 0, 0, 1, 3) 954 self.scrollconf.setWidget(self.widget) 955 956 def initialise(self): 957 self.CameraHandlerInit() 958 if self.hasCamInited: 959 # create corresponding tree of tab widgets 960 self.setWindowTitle(self.camera_config.get_label()) 961 self.recreate_section_widget() 962 963 def config_changed(self): 964 if self.args.config_apply_btn == 1: 965 self.apply_button.setEnabled(True) 966 else: 967 def handler(): 968 self.apply_changes() 969 timer.stop() 970 timer.deleteLater() 971 timer = QtCore.QTimer() 972 timer.timeout.connect(handler) 973 timer.start(0) 974 975 def reconstruct_config_section(self): 976 # assumes first setup already done, so there are existing tabs: 977 tabs = self.configsection.children()[1] 978 lastindex = tabs.currentIndex() 979 # get also the values of self.scrollconf scrollbars 980 lastconfhscroll = (self.scrollconf.horizontalScrollBar().value(), self.scrollconf.horizontalScrollBar().minimum(), self.scrollconf.horizontalScrollBar().singleStep(), self.scrollconf.horizontalScrollBar().pageStep(), self.scrollconf.horizontalScrollBar().maximum()) 981 lastconfvscroll = (self.scrollconf.verticalScrollBar().value(), self.scrollconf.verticalScrollBar().minimum(), self.scrollconf.verticalScrollBar().singleStep(), self.scrollconf.verticalScrollBar().pageStep(), self.scrollconf.verticalScrollBar().maximum()) 982 # Pre-reconstruct 983 self.camera_config = self.camera.get_config() 984 self.replicate_ccgoo_main_window() 985 self.recreate_section_widget() 986 tabs = self.configsection.children()[1] 987 tabs.setCurrentIndex(lastindex) 988 # Post-reconstruct 989 self.scrollconf.horizontalScrollBar().setValue(lastconfhscroll[0]) 990 self.scrollconf.verticalScrollBar().setValue(lastconfvscroll[0]) 991 992 def apply_changes(self): 993 self.camera.set_config(self.camera_config) 994 # here we'd need to reconstruct, to get the proper config values 995 self.reconstruct_config_section() 996 997 def load_settings(self): 998 self.updateStatusBar() # clear last singleStatusMsg 999 if self.lastOpenPath is not None: 1000 startpath, startfile = os.path.split(self.lastOpenPath) 1001 elif self.lastSavePath is not None: 1002 startpath, startfile = os.path.split(self.lastSavePath) 1003 else: # both None 1004 startpath = os.getcwd() 1005 startfile = "" 1006 options = QFileDialog.Options() 1007 options |= QFileDialog.DontUseNativeDialog 1008 fileName, _ = QFileDialog.getOpenFileName(self, "Load Camera Settings", os.path.join(startpath, startfile), "JSON Text Files (*.json);;All Files (*)", options=options) 1009 if fileName: 1010 do_LoadSetCamConfJson(self.camera, fileName) 1011 # update GUI also 1012 self.camera_config = self.camera.get_config() 1013 # avoid GUI context 1014 def handler(): 1015 self.apply_changes() 1016 timer.stop() 1017 timer.deleteLater() 1018 timer = QtCore.QTimer() 1019 timer.timeout.connect(handler) 1020 timer.start(0) 1021 self.lastOpenPath = fileName 1022 self.singleStatusMsg = "loaded file to cam config; see terminal stdout for more" 1023 self.updateStatusBar() 1024 1025 def save_settings(self): 1026 self.updateStatusBar() # clear last singleStatusMsg 1027 if self.lastSavePath is not None: 1028 startpath, startfile = os.path.split(self.lastSavePath) 1029 else: # both None 1030 startpath = os.getcwd() 1031 startfile = "{}.json".format( re.sub(r'\s+', '', self.camera_model) ) 1032 unixts = time.time() 1033 startfile = get_stamped_filename(startfile, unixts) 1034 options = QFileDialog.Options() 1035 options |= QFileDialog.DontUseNativeDialog 1036 fileName, _ = QFileDialog.getSaveFileName(self, "Save Camera Settings", os.path.join(startpath, startfile), "JSON Text Files (*.json);;All Files (*)", options=options) 1037 if fileName: 1038 do_GetSaveCamConfJson(self.camera, fileName, unixts) 1039 self.lastSavePath = fileName 1040 self.singleStatusMsg = "saved cam config to file; see terminal stdout for more" 1041 self.updateStatusBar() 1042 1043 def zoom_original(self): 1044 self.image_display.resetZoom() 1045 1046 def zoom_fit_view(self): 1047 self.image_display.fitInView() 1048 1049 def zoom_plus(self): 1050 self.image_display.zoomPlus() 1051 1052 def zoom_minus(self): 1053 self.image_display.zoomMinus() 1054 1055 def set_splitter_layout_style(self): 1056 if self.current_splitter_style == 0: 1057 self.set_splitter(Qt.Horizontal, self.frameview, self.frameconf) 1058 elif self.current_splitter_style == 1: 1059 self.set_splitter(Qt.Vertical, self.frameview, self.frameconf) 1060 elif self.current_splitter_style == 2: 1061 self.set_splitter(Qt.Horizontal, self.frameconf, self.frameview) 1062 elif self.current_splitter_style == 3: 1063 self.set_splitter(Qt.Vertical, self.frameconf, self.frameview) 1064 1065 def switch_splitter_layout(self): 1066 self.current_splitter_style = (self.current_splitter_style + 1) % 4 1067 self.set_splitter_layout_style() 1068 1069 def _do_preview(self): 1070 # capture preview image 1071 OK, camera_file = gp.gp_camera_capture_preview(self.camera) 1072 if OK < gp.GP_OK: 1073 print('Failed to capture preview') 1074 self.running = False 1075 return 1076 self._send_file(camera_file) 1077 1078 def _do_preview_camnotinit(self): 1079 # since cam not inited here, just load the cam-conf-no-cam.png/NOCAMIMG 1080 nocamimgpath = os.path.join(THISSCRIPTDIR, NOCAMIMG) 1081 image = Image.open(nocamimgpath) 1082 image.load() 1083 self.singleStatusMsg = "No cam img: {} x {}".format(image.size[0], image.size[1]) 1084 self.new_image_sig.emit(image) 1085 1086 def _capture_image(self): 1087 startts = time.time() 1088 self.running = False 1089 self.repeatpreview_action.setChecked(False) # needed just for the menu item 1090 self.singleStatusMsg = "Capturing image - wait ..." 1091 self.updateStatusBar() # clear last singleStatusMsg 1092 imgfilename = do_capture_image(self.camera) 1093 imgpathname = os.path.realpath(imgfilename) 1094 image = Image.open(imgpathname) 1095 image.load() 1096 self.new_image_sig.emit(image) 1097 self.lastImageType = 1 1098 self.image_display.fitInView() 1099 endts = time.time() 1100 self.singleStatusMsg = "Captured image: {} [{:.3f} s]".format(imgpathname, endts-startts) 1101 self.updateStatusBar() 1102 1103 def _send_file(self, camera_file): 1104 file_data = camera_file.get_data_and_size() 1105 image = Image.open(io.BytesIO(file_data)) 1106 image.load() 1107 self.new_image_sig.emit(image) 1108 if self.lastImageType != 0: 1109 self.image_display.fitInView() # reset previous offsets.. 1110 self.image_display.resetZoom() # .. then back to zoom_original for preview img 1111 self.lastImageType = 0 1112 1113 @QtCore.pyqtSlot(object) 1114 def new_image(self, image): 1115 self.lastImageSize = image.size 1116 w, h = image.size 1117 image_data = image.tobytes('raw', 'RGB') 1118 self.q_image = QtGui.QImage(image_data, w, h, QtGui.QImage.Format_RGB888) 1119 self._draw_image() 1120 if self.timestamp is not None: 1121 tstampnow = time.time() 1122 tdelta = tstampnow - self.timestamp 1123 fps = 1.0/tdelta 1124 self.fps = "(<{} fps)".format(MINFPS) if (fps<MINFPS) else "{:7.2f} fps".format(fps) 1125 self.timestamp = tstampnow 1126 else: 1127 self.timestamp = time.time() 1128 self.updateStatusBar() 1129 1130 def _draw_image(self): 1131 if not self.q_image: 1132 return 1133 self.pixmap = QtGui.QPixmap.fromImage(self.q_image) 1134 self.image_display.setPhoto(self.pixmap) 1135 1136 1137def main(): 1138 # set up logging 1139 # note that: '%(filename)s:%(lineno)d' just prints 'port_log.py:127' 1140 logging.basicConfig( 1141 format='%(asctime)s %(levelname)s: %(name)s: %(message)s', level=logging.WARNING) 1142 callback_obj = gp.check_result(gp.use_python_logging()) 1143 1144 # command line argument parser 1145 parser = argparse.ArgumentParser(description="{} - interact with camera via python-gphoto2. Called without command line arguments, it will start a Qt GUI.".format(APPNAME)) 1146 parser.add_argument('--save-cam-conf-json', default=argparse.SUPPRESS, help='Get and save the camera configuration to .json file, if standalone (together with --load-cam-conf-json, can be used for copying filtered json files). The string argument is the filename, action is aborted if standalone and no camera online, or if the argument is empty. Overwrites existing files without prompting. Note that if camera is online, but mirror is not raised, process will complete with errors and fewer properties collected in json file (default: suppress)') # "%(default)s" 1147 parser.add_argument('--load-cam-conf-json', default=argparse.SUPPRESS, help='Load and set the camera configuration from .json file, if standalone (together with --save-cam-conf-json, can be used for copying filtered json files). The string argument is the filename, action is aborted if standalone and no camera online, or if the argument is empty (default: suppress)') # "%(default)s" 1148 parser.add_argument('--include-names-json', default=argparse.SUPPRESS, help='Comma separated list of property names to be filtered/included. When using --load-cam-conf-json with --save-cam-conf-json, a json copy with flattening of hierarchy (removal of nodes with children and without value) is performed; in that case --include-names-json can be used to include only certain properties in the output. Can also use `ro=0` or `ro=1` as filtering criteria. If empty ignored (default: suppress)') # "%(default)s" 1149 parser.add_argument('--start-capture-view', default='', help='Command - start capture view (extend lens/raise mirror) on the camera, then exit', action='store_const', const=start_capture_view) # "%(default)s" 1150 parser.add_argument('--stop-capture-view', default='', help='Command - stop capture view (retract lens/release mirror) on the camera, then exit', action='store_const', const=stop_capture_view) # "%(default)s" 1151 parser.add_argument('--config-apply-btn', type=int, default=0, help='GUI option: 0: do not create apply button, update on change; 1: create apply button, update on its click (default: %(default)s)') # "" 1152 args = parser.parse_args() # in case of --help, this also prints help and exits before Qt window is raised 1153 if (args.start_capture_view): args.start_capture_view() 1154 elif (args.stop_capture_view): args.stop_capture_view() 1155 elif (hasattr(args, 'save_cam_conf_json') and not(hasattr(args, 'load_cam_conf_json'))): 1156 getSaveCamConfJson(args) 1157 elif (hasattr(args, 'load_cam_conf_json') and not(hasattr(args, 'save_cam_conf_json'))): 1158 loadSetCamConfJson(args) 1159 elif (hasattr(args, 'load_cam_conf_json') and hasattr(args, 'save_cam_conf_json')): 1160 copyFilterCamConfJson(args) 1161 1162 # start Qt Gui 1163 app = QtWidgets.QApplication([APPNAME]) # SO: 18133302 1164 main = MainWindow(args) 1165 main.show() 1166 sys.exit(app.exec_()) 1167 1168 1169if __name__ == "__main__": 1170 main() 1171 1172