├── .gitignore ├── ImportPhotos.py ├── LICENSE ├── README.md ├── __init__.py ├── code ├── MouseClick.py └── PhotosViewer.py ├── i18n ├── ImportPhotos_fr.qm └── ImportPhotos_fr.ts ├── icons ├── ImportImage.svg ├── SelectImage.svg ├── arrowLeft.png ├── arrowRight.png ├── edges.PNG ├── example.png ├── export.svg ├── icon.png ├── mActionPan.svg ├── mActionZoomFullExtent.svg ├── mActionZoomToSelected.svg ├── method-draw-image.svg ├── photos.qml ├── redband.PNG ├── rotate.png ├── sync_views.svg └── tonorth.png ├── install_packages ├── install_pip_packages.bat ├── py3-env.bat └── requirements.txt ├── metadata.txt ├── resources.py ├── resources.qrc ├── runuifiles.bat └── ui ├── impphotos.py └── impphotos.ui /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | deploy.bat 3 | .idea/ -------------------------------------------------------------------------------- /ImportPhotos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | ImportPhotos 5 | A QGIS plugin 6 | Import photos 7 | last update : 04/01/2023 8 | begin : February 2018 9 | copyright : (C) 2019 by KIOS Research Center 10 | email : mariosmsk@gmail.com 11 | ***************************************************************************/ 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | import json 23 | import os 24 | import uuid 25 | 26 | from qgis.PyQt import uic 27 | from qgis.PyQt.QtCore import QFileInfo 28 | from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt, QTextCodec 29 | from qgis.PyQt.QtGui import QIcon, QGuiApplication 30 | from qgis.PyQt.QtWidgets import QAction, QFileDialog, QMessageBox, QInputDialog, QLabel 31 | from qgis.PyQt.QtWidgets import QDialog 32 | from qgis.core import * 33 | from qgis.gui import QgsRuleBasedRendererWidget 34 | 35 | # Initialize Qt resources from file resources.py 36 | from . import resources 37 | # Import the code for the dialog 38 | from .code.MouseClick import MouseClick 39 | from pathlib import Path 40 | 41 | # Import python module 42 | CHECK_MODULE = '' 43 | try: 44 | import exifread 45 | 46 | CHECK_MODULE = 'exifread' 47 | except: 48 | CHECK_MODULE = '' 49 | 50 | try: 51 | if CHECK_MODULE == '': 52 | from PIL import Image 53 | from PIL.ExifTags import TAGS 54 | 55 | CHECK_MODULE = 'PIL' 56 | except: 57 | CHECK_MODULE = '' 58 | 59 | FORM_CLASS, _ = uic.loadUiType(os.path.join( 60 | os.path.dirname(__file__), 'ui/impphotos.ui')) 61 | 62 | FIELDS = ['fid', 'ID', 'Name', 'Date', 'Time', 'Lon', 'Lat', 'Altitude', 'North', 'Azimuth', 'Cam. Maker', 63 | 'Cam. Model', 'Title', 'Comment', 'Path', 'RelPath', 'Timestamp', 'Images', 'Link', 'Description'] 64 | 65 | SUPPORTED_PHOTOS_EXTENSIONS = ['jpg', 'jpeg', 'JPG', 'JPEG'] 66 | 67 | SUPPORTED_OUTPUT_FILE_EXTENSIONS = { 68 | "GeoPackage (*.gpkg *.GPKG)": ".gpkg", 69 | "ESRI Shapefile (*.shp *.SHP)": ".shp", 70 | "GeoJSON (*.geojson *.GEOJSON)": ".geojson", 71 | "Comma Separated Value (*.csv *.CSV)": ".csv", 72 | "Keyhole Markup Language (*.kml *.KML)": ".kml", 73 | "Mapinfo TAB (*.tab *.TAB)": ".tab" 74 | } 75 | 76 | EXTENSION_DRIVERS = { 77 | ".gpkg": "GPKG", 78 | ".shp": "ESRI Shapefile", 79 | ".geojson": "GeoJSON", 80 | ".csv": "CSV", 81 | ".kml": "KML", 82 | ".tab": "MapInfo File" 83 | } 84 | 85 | CODEC = QTextCodec.codecForName("UTF-8") 86 | 87 | 88 | # Import ui file 89 | class ImportPhotosDialog(QDialog, FORM_CLASS): 90 | def __init__(self, parent=None): 91 | # """Constructor.""" 92 | QDialog.__init__(self, None, Qt.WindowStaysOnTopHint) 93 | super(ImportPhotosDialog, self).__init__(parent) 94 | self.setupUi(self) 95 | 96 | 97 | class ImportPhotos: 98 | """QGIS Plugin Implementation.""" 99 | 100 | def __init__(self, iface): 101 | """Constructor. 102 | 103 | :param iface: An interface instance that will be passed to this class 104 | which provides the hook by which you can manipulate the QGIS 105 | application at run time. 106 | :type iface: QgsInterface 107 | """ 108 | # Save reference to the QGIS interface 109 | self.iface = iface 110 | self.canvas = self.iface.mapCanvas() 111 | self.project_instance = QgsProject.instance() 112 | # initialize plugin directory 113 | self.plugin_dir = os.path.dirname(__file__) 114 | # initialize locale 115 | locale = QSettings().value('locale/userLocale')[0:2] 116 | locale_path = os.path.join( 117 | self.plugin_dir, 118 | 'i18n', 119 | 'ImportPhotos_{}.qm'.format(locale)) 120 | 121 | if os.path.exists(locale_path): 122 | self.translator = QTranslator() 123 | self.translator.load(locale_path) 124 | 125 | # Declare instance attributes 126 | self.actions = [] 127 | self.menu = self.tr('&ImportPhotos') 128 | # TODO: We are going to let the user set this up in a future iteration 129 | self.toolbar = self.iface.addToolBar('ImportPhotos') 130 | self.toolbar.setObjectName('ImportPhotos') 131 | # Renderer that will be set after the import process 132 | self.layer_renderer = None 133 | 134 | # noinspection PyMethodMayBeStatic 135 | def tr(self, message): 136 | """Get the translation for a string using Qt translation API. 137 | 138 | We implement this ourselves since we do not inherit QObject. 139 | 140 | :param message: String for translation. 141 | :type message: str, QString 142 | 143 | :returns: Translated version of message. 144 | :rtype: QString 145 | """ 146 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass 147 | return QCoreApplication.translate('ImportPhotos', message) 148 | 149 | def add_action( 150 | self, 151 | icon_path, 152 | text, 153 | callback, 154 | checkable=False, 155 | enabled_flag=True, 156 | add_to_menu=True, 157 | add_to_toolbar=True, 158 | status_tip=None, 159 | whats_this=None, 160 | parent=None): 161 | """Add a toolbar icon to the toolbar. 162 | 163 | :param icon_path: Path to the icon for this action. Can be a resource 164 | path (e.g. ':/plugins/foo/bar.png') or a normal file system path. 165 | :type icon_path: str 166 | 167 | :param text: Text that should be shown in menu items for this action. 168 | :type text: str 169 | 170 | :param callback: Function to be called when the action is triggered. 171 | :type callback: function 172 | 173 | :param enabled_flag: A flag indicating if the action should be enabled 174 | by default. Defaults to True. 175 | :type enabled_flag: bool 176 | 177 | :param add_to_menu: Flag indicating whether the action should also 178 | be added to the menu. Defaults to True. 179 | :type add_to_menu: bool 180 | 181 | :param add_to_toolbar: Flag indicating whether the action should also 182 | be added to the toolbar. Defaults to True. 183 | :type add_to_toolbar: bool 184 | 185 | :param status_tip: Optional text to show in a popup when mouse pointer 186 | hovers over the action. 187 | :type status_tip: str 188 | 189 | :param parent: Parent widget for the new action. Defaults None. 190 | :type parent: QWidget 191 | 192 | :param whats_this: Optional text to show in the status bar when the 193 | mouse pointer hovers over the action. 194 | 195 | :returns: The action that was created. Note that the action is also 196 | added to self.actions list. 197 | :rtype: QAction 198 | """ 199 | 200 | # Create the dialog (after translation) and keep reference 201 | 202 | icon = QIcon(icon_path) 203 | action = QAction(icon, text, parent) 204 | action.triggered.connect(callback) 205 | action.setEnabled(enabled_flag) 206 | 207 | if status_tip is not None: 208 | action.setStatusTip(status_tip) 209 | 210 | if whats_this is not None: 211 | action.setWhatsThis(whats_this) 212 | 213 | if add_to_toolbar: 214 | self.toolbar.addAction(action) 215 | 216 | if add_to_menu: 217 | self.iface.addPluginToMenu( 218 | self.menu, 219 | action) 220 | if checkable: 221 | action.setCheckable(checkable) 222 | 223 | self.actions.append(action) 224 | 225 | return action 226 | 227 | def initGui(self): 228 | """Create the menu entries and toolbar icons inside the QGIS GUI.""" 229 | icon_path = ':/plugins/ImportPhotos/icons/ImportImage.svg' 230 | self.add_action( 231 | icon_path, 232 | text=self.tr('Import Photos'), 233 | callback=self.run, 234 | parent=self.iface.mainWindow()) 235 | icon_path = ':/plugins/ImportPhotos/icons/SelectImage.svg' 236 | self.clickPhotos = self.add_action( 237 | icon_path, 238 | checkable=True, 239 | text=self.tr('Click Photos'), 240 | callback=self.setMouseClickMapTool, 241 | parent=self.iface.mainWindow()) 242 | icon_path = ':/plugins/ImportPhotos/icons/sync_views.svg' 243 | self.add_action( 244 | icon_path, 245 | text=self.tr('Update Photos'), 246 | callback=self.update_photos, 247 | parent=self.iface.mainWindow()) 248 | icon_path = ':/plugins/ImportPhotos/icons/export.svg' 249 | self.add_action( 250 | icon_path, 251 | text=self.tr('Bulk Export'), 252 | callback=self.bulk_export, 253 | parent=self.iface.mainWindow()) 254 | 255 | self.dlg = ImportPhotosDialog() 256 | self.dlg.ok.clicked.connect(self.import_photos) 257 | self.dlg.closebutton.clicked.connect(self.dlg.close) 258 | self.dlg.toolButtonImport.clicked.connect(self.toolButtonImport) 259 | self.dlg.toolButtonOut.clicked.connect(self.toolButtonOut) 260 | self.dlg.toolButtonRelative.clicked.connect(self.toolButtonRelative) 261 | 262 | # Add QgsRuleBasedRendererWidget 263 | # temp_layer is a class variable because we need to keep its reference 264 | # so the RendererWidget does not crash QGIS 265 | # If it's not a class variable, then it goes out of scope after this method 266 | # and as mentioned, QGIS crashes because it tries to access it. 267 | self.temp_layer = QgsVectorLayer( 268 | 'Point?crs=epsg:4326&field=ID:string&field=Name:string&' 269 | 'field=Date:date&field=Time:text&field=Lon:double&field=Lat:double' 270 | '&field=Altitude:double&field=Cam.Mak:string&field=Cam.Mod:string' 271 | '&field=Title:string&field=Comment:string&field=Path:string' 272 | '&field=RelPath:string&field=Timestamp:string&field=Images:string' 273 | '&field=Link:string&field=Description:string', 274 | 'temp_layer', 275 | 'memory') 276 | self.temp_layer.setRenderer(QgsFeatureRenderer.defaultRenderer(QgsWkbTypes.PointGeometry)) 277 | self.temp_layer.loadNamedStyle(os.path.join(self.plugin_dir, 'icons', "photos.qml")) 278 | renderer_widget = QgsRuleBasedRendererWidget( 279 | self.temp_layer, QgsStyle.defaultStyle(), 280 | self.temp_layer.renderer()) 281 | renderer_widget.setObjectName("renderer_widget") 282 | self.dlg.gridLayout.addWidget(QLabel("Output layer style"), 4, 0) 283 | self.dlg.gridLayout.addWidget(renderer_widget, 4, 2) 284 | 285 | self.toolMouseClick = MouseClick(self.canvas, self) 286 | 287 | def setMouseClickMapTool(self): 288 | 289 | # Set photos layer as active layer 290 | for layer in self.project_instance.mapLayers().values(): 291 | if layer.type() == QgsMapLayerType.VectorLayer and layer.fields().names() == FIELDS: 292 | self.iface.setActiveLayer(layer) 293 | break 294 | 295 | self.canvas.setMapTool(self.toolMouseClick) 296 | 297 | def unload(self): 298 | """Removes the plugin menu item and icon from QGIS GUI.""" 299 | for action in self.actions: 300 | self.iface.removePluginMenu( 301 | self.tr('&ImportPhotos'), 302 | action) 303 | self.iface.removeToolBarIcon(action) 304 | # remove the toolbar 305 | del self.toolbar 306 | 307 | def run(self): 308 | if CHECK_MODULE == '': 309 | self.showMessage( 310 | self.tr('Python Modules'), 311 | self.tr('Please install python module "exifread" or "PIL".'), 312 | 'Warning') 313 | return 314 | 315 | self.dlg.out.setText('') 316 | self.dlg.imp.setText('') 317 | self.dlg.canvas_extent.setChecked(False) 318 | self.dlg.show() 319 | 320 | def toolButtonOut(self): 321 | 322 | outputPath, selected_extension_filter = QFileDialog.getSaveFileName( 323 | self.dlg, 324 | self.tr("Save output layer"), os.path.expanduser('~'), 325 | ";;".join(list(SUPPORTED_OUTPUT_FILE_EXTENSIONS.keys()))) 326 | 327 | if outputPath: 328 | extension = SUPPORTED_OUTPUT_FILE_EXTENSIONS[selected_extension_filter] 329 | if os.path.splitext(outputPath)[1] == '': 330 | # Add extension to filepath if user did not specify it 331 | self.dlg.out.setText(outputPath + extension) 332 | else: 333 | # Set extension with the specified filter 334 | self.dlg.out.setText(os.path.splitext(outputPath)[0] + extension) 335 | 336 | def get_path_relative_to_project_root(self, abs_path): 337 | project_folder = QFileInfo( 338 | self.project_instance.fileName()).absolutePath() 339 | try: 340 | rel_path = os.path.relpath( 341 | path=os.path.normpath(abs_path), start=project_folder) 342 | except ValueError: 343 | # On Windows, when path and start are on different drives. 344 | rel_path = os.path.normpath(abs_path) 345 | return rel_path 346 | 347 | def toolButtonRelative(self): 348 | directory_path = QFileDialog.getExistingDirectory( 349 | self.dlg, self.tr('Select a folder:'), 350 | os.path.expanduser('~'), QFileDialog.ShowDirsOnly) 351 | 352 | if directory_path: 353 | self.selected_folder = directory_path[:] 354 | self.dlg.relativeroot.setText(directory_path) 355 | 356 | def toolButtonImport(self): 357 | directory_path = QFileDialog.getExistingDirectory( 358 | self.dlg, self.tr('Select a folder'), 359 | os.path.expanduser('~'), QFileDialog.ShowDirsOnly) 360 | 361 | if directory_path: 362 | self.selected_folder = directory_path[:] 363 | self.dlg.imp.setText(directory_path) 364 | 365 | def import_photos(self): 366 | self.layer_renderer = self.dlg.findChild(QgsRuleBasedRendererWidget, "renderer_widget").renderer() 367 | 368 | file_not_found = False 369 | if self.dlg.imp.text() == '' and not os.path.isdir(self.dlg.imp.text()): # should have been or? 370 | file_not_found = True 371 | msg = self.tr('Please select a directory photos.') 372 | if self.dlg.out.text() == '' and not os.path.isabs(self.dlg.out.text()): # should have been or? 373 | file_not_found = True 374 | msg = self.tr('Please define output file location.') 375 | 376 | if file_not_found: 377 | self.showMessage('Warning', msg, 'Warning') 378 | return 379 | 380 | if self.dlg.relativeroot.text() == '': 381 | self.relativeroot = self.dlg.imp.text() 382 | else: 383 | self.relativeroot = self.dlg.relativeroot.text() 384 | 385 | self.webroot = self.dlg.webroot.text() # Will be checked later if it is '' 386 | 387 | # get paths of photos 388 | self.photos_to_import = [] 389 | for root, dirs, files in os.walk(self.dlg.imp.text()): 390 | for filename in files: 391 | if filename.lower().endswith(tuple(SUPPORTED_PHOTOS_EXTENSIONS)): 392 | self.photos_to_import.append(os.path.join(root, filename)) 393 | 394 | if len(self.photos_to_import) == 0: 395 | self.showMessage('Warning', self.tr('No photos were found!'), 'Warning') 396 | return 397 | 398 | # Set up for url: 399 | 400 | self.dlg.close() 401 | self.call_import_photos() 402 | # QGuiApplication.setOverrideCursor(Qt.WaitCursor) 403 | # photos_to_import.sort() 404 | # try: 405 | # result = self.import_photos_task(photos_to_import) 406 | # self.completed(result) 407 | # except Exception as e: 408 | # self.showMessage(self.tr('Unexpected Error'), str(e), 'Warning') 409 | # QGuiApplication.restoreOverrideCursor() 410 | 411 | def call_import_photos(self): 412 | # self.import_photos_task('', '') 413 | # self.completed('') 414 | self.taskPhotos = QgsTask.fromFunction('ImportPhotos', self.import_photos_task, 415 | on_finished=self.completed, wait_time=4) 416 | QgsApplication.taskManager().addTask(self.taskPhotos) 417 | 418 | def stopped(self, task): 419 | QgsMessageLog.logMessage( 420 | 'Task "{name}" was canceled'.format( 421 | name=task.description()), 422 | 'ImportPhotos', Qgis.Info) 423 | 424 | def import_photos_task(self, task, wait_time): 425 | self.temp_photos_layer = self.project_instance.addMapLayer( 426 | QgsVectorLayer("Point?crs=epsg:4326", None, "memory"), False) 427 | 428 | imported_photos_counter = 0 429 | out_of_bounds_photos_counter = 0 430 | no_location_photos_counter = 0 431 | editing_started = self.temp_photos_layer.startEditing() 432 | 433 | self.photos = [] 434 | self.photos_names = [] 435 | for root, dirs, files in os.walk(self.selected_folder): 436 | for name in files: 437 | if name.lower().endswith(tuple(SUPPORTED_PHOTOS_EXTENSIONS)): 438 | self.photos.append(os.path.join(root, name)) 439 | 440 | self.initphotos = len(self.photos) 441 | if editing_started: 442 | # Import new pictures 443 | attribute_fields_set = False 444 | 445 | for count, photo_path in enumerate(self.photos_to_import): 446 | try: 447 | if not os.path.isdir(photo_path) and photo_path.lower().endswith( 448 | tuple(SUPPORTED_PHOTOS_EXTENSIONS)): 449 | geo_info = self.get_geo_infos_from_photo(photo_path) 450 | if geo_info and geo_info["properties"]["Lat"] and geo_info["properties"]["Lon"]: 451 | geo_info = json.dumps(geo_info) 452 | fields = QgsJsonUtils.stringToFields(geo_info, CODEC) 453 | 454 | if not attribute_fields_set: 455 | attribute_fields_set = True 456 | for field in fields.toList(): 457 | self.temp_photos_layer.addAttribute(field) 458 | 459 | feature = QgsJsonUtils.stringToFeatureList( 460 | geo_info, fields, CODEC)[0] 461 | 462 | self.temp_photos_layer.addFeature(feature) 463 | imported_photos_counter += 1 464 | elif geo_info == 'out': 465 | out_of_bounds_photos_counter += 1 466 | elif geo_info is False: 467 | no_location_photos_counter += 1 468 | except: 469 | pass 470 | 471 | if not editing_started or not self.temp_photos_layer.commitChanges(): 472 | self.project_instance.removeMapLayer(self.temp_photos_layer) 473 | title = self.tr('Import Photos') 474 | msg = "{}\n\n{} {}".format( 475 | self.tr("Import Failed."), 476 | self.tr("Details:"), 477 | "\n".join(self.temp_photos_layer.commitErrors())) 478 | self.showMessage(title, msg, 'Warning') 479 | self.result = False, len( 480 | self.photos_to_import), imported_photos_counter, out_of_bounds_photos_counter, no_location_photos_counter 481 | 482 | # Save vector layer as a Shapefile 483 | driver = EXTENSION_DRIVERS[os.path.splitext(self.dlg.out.text())[1]] 484 | error_code, error_message = QgsVectorFileWriter.writeAsVectorFormat( 485 | self.temp_photos_layer, self.dlg.out.text(), "utf-8", 486 | QgsCoordinateReferenceSystem(self.temp_photos_layer.crs().authid()), 487 | driver) 488 | 489 | if error_code != 0: 490 | self.project_instance.removeMapLayer(self.temp_photos_layer) 491 | self.showMessage(self.tr('Writing output file error'), error_message, 'Warning') 492 | return False, len( 493 | self.photos_to_import), imported_photos_counter, out_of_bounds_photos_counter, no_location_photos_counter 494 | 495 | self.project_instance.removeMapLayer(self.temp_photos_layer) 496 | self.setMouseClickMapTool() 497 | 498 | self.result = True, len( 499 | self.photos_to_import), imported_photos_counter, out_of_bounds_photos_counter, no_location_photos_counter 500 | 501 | def completed(self, result): 502 | 503 | import_ok, photos_to_import_number, imported_photos_counter, out_of_bounds_photos_counter, no_location_photos_counter = self.result 504 | no_location_photos_counter = no_location_photos_counter + photos_to_import_number - imported_photos_counter - out_of_bounds_photos_counter 505 | 506 | if import_ok: 507 | if imported_photos_counter == 0: 508 | title = self.tr('ImportPhotos') 509 | msg = '{}\n\n{}\n {}'.format( 510 | self.tr('Import Completed.'), 511 | self.tr('Details:'), 512 | self.tr('No new photos were added.')) 513 | else: 514 | title = self.tr('ImportPhotos') 515 | msg = '{}\n\n{}\n {} {}\n {} {}\n {} {}\n'.format( 516 | self.tr('Import Completed.'), 517 | self.tr('Details:'), 518 | str(int(imported_photos_counter)), 519 | self.tr('photo(s) added without error.'), 520 | str(int(no_location_photos_counter)), 521 | self.tr('photo(s) skipped (because of missing location).'), 522 | str(int(out_of_bounds_photos_counter)), 523 | self.tr('photo(s) skipped (because not in canvas extent).')) 524 | self.showMessage(title, msg, self.tr('Information')) 525 | 526 | self.layerPhotos_final = QgsVectorLayer( 527 | self.dlg.out.text(), 528 | os.path.basename(self.dlg.out.text()).split(".")[0], 529 | "ogr") 530 | 531 | self.layerPhotos_final.setReadOnly(False) 532 | self.layerPhotos_final.setRenderer(self.layer_renderer.clone()) 533 | self.layerPhotos_final.reload() 534 | self.layerPhotos_final.triggerRepaint() 535 | self.project_instance.addMapLayer(self.layerPhotos_final) 536 | 537 | expression = """ 538 |
| [% Name %] | 541 |
|---|
29 |  30 | 31 |  32 | 33 | Tutorial on youtube:
34 | [](https://www.youtube.com/watch?v=Y3R8gHJUrrk)
35 |
36 | QGIS 3
37 | Mac Users. Requires the following Python Modules to be installed: UnixImageIO, FreeType, PIL Please visit: http://www.kyngchaos.com/software/python
38 |
39 | ## Updated version
40 |
41 |
42 |
43 |
44 | # Contributors #
45 | * Marios S. Kyriakou, [KIOS Research and Innovation Center of Excellence (KIOS CoE)](https://www.kios.ucy.ac.cy/)
46 | * George A. Christou, [KIOS Research and Innovation Center of Excellence (KIOS CoE)](https://www.kios.ucy.ac.cy/)
47 | * Panayiotis S. Kolios, [KIOS Research and Innovation Center of Excellence (KIOS CoE)](https://www.kios.ucy.ac.cy/)
48 | * Demetris G. Eliades, [KIOS Research and Innovation Center of Excellence (KIOS CoE)](https://www.kios.ucy.ac.cy/)
49 |
50 | * [QGIS Cyprus](https://www.facebook.com/qgiscyprus/)
51 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | ImportPhotos
5 | A QGIS plugin
6 | Import photos jpegs
7 | -------------------
8 | begin : 2018-08-25
9 | git sha : $Format:%H$
10 | copyright : (C) 2018 by KIOS Research Center
11 | email : mariosmsk@gmail.com
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | # noinspection PyPep8Naming
25 | def classFactory(iface): # pylint: disable=invalid-name
26 | """Load ImportPhotos class from file ImportPhotos.
27 |
28 | :param iface: A QGIS interface instance.
29 | :type iface: QgsInterface
30 | """
31 | #
32 | from .ImportPhotos import ImportPhotos
33 | return ImportPhotos(iface)
34 |
--------------------------------------------------------------------------------
/code/MouseClick.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | ImportPhotos
5 | A QGIS plugin
6 | Import photos
7 | last update : 04/01/2023
8 | begin : February 2018
9 | copyright : (C) 2019 by KIOS Research Center
10 | email : mariosmsk@gmail.com
11 | ***************************************************************************/
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 | import os.path
23 |
24 | from qgis.PyQt.QtCore import (Qt, pyqtSignal, QCoreApplication, QFileInfo, QRectF)
25 | from qgis.PyQt.QtGui import (QPixmap, QImage)
26 | from qgis.core import (QgsRectangle, QgsProject)
27 | from qgis.gui import (QgsMapTool)
28 |
29 | from .PhotosViewer import PhotoWindow
30 |
31 |
32 | # Mouseclik import file
33 | class MouseClick(QgsMapTool):
34 | afterLeftClick = pyqtSignal()
35 | afterRightClick = pyqtSignal()
36 | afterDoubleClick = pyqtSignal()
37 |
38 | def __init__(self, canvas, drawSelf):
39 | QgsMapTool.__init__(self, canvas)
40 | self.canvas = canvas
41 | self.drawSelf = drawSelf
42 | self.drawSelf.rb = None
43 | self.photosDLG = None
44 |
45 | def canvasPressEvent(self, event):
46 | if event.button() == 1:
47 | # sigeal : keep photo viewer on top of other windows
48 | if self.photosDLG is not None:
49 | self.photosDLG.setWindowFlags(Qt.WindowStaysOnTopHint)
50 | self.drawSelf.refresh()
51 |
52 | def canvasMoveEvent(self, event):
53 | pass
54 |
55 | # sigeal : display photo on click instead of double-click
56 | # def canvasReleaseEvent(self, event):
57 | def canvasDoubleClickEvent(self, event):
58 | pass
59 |
60 | # sigeal : display photo on click instead of double-click
61 | # def canvasDoubleClickEvent(self, event):
62 | def canvasReleaseEvent(self, event):
63 | layers = self.canvas.layers()
64 | p = self.toMapCoordinates(event.pos())
65 | w = self.canvas.mapUnitsPerPixel() * 10
66 | try:
67 | rect = QgsRectangle(p.x() - w, p.y() - w, p.x() + w, p.y() + w)
68 | except:
69 | return
70 | layersSelected = []
71 | for layer in layers:
72 | if layer.type():
73 | continue
74 | fields = [field.name().upper() for field in layer.fields()]
75 | if 'PATH' or 'PHOTO' in fields:
76 | lRect = self.canvas.mapSettings().mapToLayerCoordinates(layer, rect)
77 | layer.selectByRect(lRect)
78 | selected_features = layer.selectedFeatures()
79 | if selected_features != []:
80 | layersSelected.append(layer)
81 | ########## SHOW PHOTOS ############
82 | feature = selected_features[0]
83 | self.drawSelf.featureIndex = feature.id()
84 | activeLayerChanged = not hasattr(self.drawSelf, 'layerActive') or (
85 | self.drawSelf.layerActive != layer)
86 | self.drawSelf.layerActive = layer
87 | self.drawSelf.fields = fields
88 | self.drawSelf.maxlen = len(self.drawSelf.layerActive.name())
89 | self.drawSelf.layerActiveName = layer.name()
90 | self.drawSelf.iface.setActiveLayer(layer)
91 |
92 | if self.drawSelf.maxlen > 13:
93 | self.drawSelf.maxlen = 14
94 | self.drawSelf.layerActiveName = self.drawSelf.layerActive.name() + '...'
95 |
96 | if 'PATH' in fields:
97 | imPath = feature.attributes()[feature.fieldNameIndex('Path')]
98 | elif 'PHOTO' in fields:
99 | imPath = feature.attributes()[feature.fieldNameIndex('photo')]
100 | else:
101 | return
102 |
103 | self.drawSelf.prj = QgsProject.instance()
104 | try:
105 | if not os.path.exists(imPath):
106 | if self.drawSelf.prj.fileName() and 'RELPATH' in fields:
107 | imPath = os.path.join(QFileInfo(self.drawSelf.prj.fileName()).absolutePath(),
108 | feature.attributes()[feature.fieldNameIndex('RelPath')])
109 | else:
110 | c = self.drawSelf.noImageFound()
111 | if c:
112 | return
113 | except:
114 | c = self.drawSelf.noImageFound()
115 | if c:
116 | return
117 |
118 | self.drawSelf.getImage = QImage(imPath)
119 |
120 | if self.photosDLG is None or activeLayerChanged:
121 | self.photosDLG = PhotoWindow(self.drawSelf)
122 | self.photosDLG.viewer.scene.clear()
123 | pixmap = QPixmap.fromImage(self.drawSelf.getImage)
124 | self.photosDLG.viewer.scene.addPixmap(pixmap)
125 | self.photosDLG.viewer.setSceneRect(QRectF(pixmap.rect()))
126 | self.photosDLG.viewer.resizeEvent([])
127 |
128 | try:
129 | dateTrue = str(feature.attributes()[feature.fieldNameIndex('Date')].toString('yyyy-MM-dd'))
130 | except:
131 | dateTrue = str(feature.attributes()[feature.fieldNameIndex('Date')])
132 | try:
133 | timeTrue = str(feature.attributes()[feature.fieldNameIndex('Time')].toString('hh:mm:ss'))
134 | except:
135 | timeTrue = str(feature.attributes()[feature.fieldNameIndex('Time')])
136 |
137 | try:
138 | name_ = feature.attributes()[feature.fieldNameIndex('Name')]
139 | name_ = name_[:-4]
140 | except:
141 | try:
142 | name_ = feature.attributes()[feature.fieldNameIndex('filename')]
143 | except:
144 | name_ = ''
145 |
146 | try:
147 | self.photosDLG.infoPhoto1.setText(self.tr('Date: ') + dateTrue)
148 | self.photosDLG.infoPhoto2.setText(self.tr('Time: ') + timeTrue[0:8])
149 | except:
150 | pass
151 | self.photosDLG.infoPhoto3.setText(self.tr('Layer: ') + self.drawSelf.layerActiveName)
152 | try:
153 | name_ = feature.attributes()[feature.fieldNameIndex('Description')]
154 | except:
155 | pass
156 |
157 | self.photosDLG.add_window_place.setText(name_)
158 |
159 | azimuth = feature.attributes()[feature.fieldNameIndex('Azimuth')]
160 |
161 | if type(azimuth) is str:
162 | try:
163 | azimuth = float(azimuth)
164 | except:
165 | pass
166 | if type(azimuth) is float:
167 | if azimuth > 0:
168 | self.photosDLG.rotate_azimuth.setEnabled(True)
169 | self.photosDLG.showNormal()
170 | return
171 | self.photosDLG.rotate_azimuth.setEnabled(False)
172 | self.photosDLG.showNormal()
173 | return
174 |
175 | def deactivate(self):
176 | self.drawSelf.clickPhotos.setChecked(False)
177 |
178 | def isZoomTool(self):
179 | return False
180 |
181 | def isTransient(self):
182 | return False
183 |
184 | def isEditTool(self):
185 | return True
186 |
187 | # noinspection PyMethodMayBeStatic
188 | def tr(self, message):
189 | """Get the translation for a string using Qt translation API.
190 |
191 | We implement this ourselves since we do not inherit QObject.
192 |
193 | :param message: String for translation.
194 | :type message: str, QString
195 |
196 | :returns: Translated version of message.
197 | :rtype: QString
198 | """
199 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
200 | return QCoreApplication.translate('PhotoWindow', message)
201 |
--------------------------------------------------------------------------------
/code/PhotosViewer.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | ImportPhotos
5 | A QGIS plugin
6 | Import photos
7 | last update : 04/01/2023
8 | begin : February 2018
9 | copyright : (C) 2019 by KIOS Research Center
10 | email : mariosmsk@gmail.com
11 | ***************************************************************************/
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 | from qgis.PyQt.QtWidgets import (QGraphicsView, QGraphicsScene, QVBoxLayout, QHBoxLayout, QWidget,
23 | QLineEdit, QLabel, QSizePolicy, QPushButton, QFrame, QMenuBar, QAction, qApp,
24 | QFileDialog, QMessageBox)
25 | from qgis.PyQt.QtCore import (QFileInfo, Qt, pyqtSignal, QRectF, QRect, QSize, QCoreApplication)
26 | from qgis.PyQt.QtGui import (QPainterPath, QIcon, QPixmap, QImage, QFont)
27 | import os.path
28 |
29 | # Filtering opencv
30 | opencv = False
31 | try:
32 | import cv2
33 | import numpy as np
34 | from matplotlib import pyplot as plt
35 |
36 | opencv = True
37 | except:
38 | opencv = False
39 |
40 |
41 | class PhotosViewer(QGraphicsView):
42 | afterLeftClick = pyqtSignal(float, float)
43 | afterLeftClickReleased = pyqtSignal(float, float)
44 | afterDoubleClick = pyqtSignal(float, float)
45 | keyPressed = pyqtSignal(int)
46 |
47 | def __init__(self, selfwindow):
48 | QGraphicsView.__init__(self)
49 |
50 | self.selfwindow = selfwindow
51 | self.panSelect = False
52 | self.zoomSelect = False
53 | self.rotate_value = 0
54 | self.rotate_azimuth_value = 0
55 |
56 | self.zoom_data = []
57 | size = 36
58 | self.scene = QGraphicsScene()
59 | if len(self.selfwindow.allpictures) > 1:
60 | self.leftClick = QPushButton(self)
61 | self.leftClick.setIcon(QIcon(':/plugins/ImportPhotos/icons/arrowLeft.png'))
62 | self.leftClick.clicked.connect(self.selfwindow.leftClickButton)
63 | self.leftClick.setToolTip(self.tr('Show previous photo'))
64 | self.leftClick.setStyleSheet("QPushButton{border: 0px; background: transparent;}")
65 | self.leftClick.setIconSize(QSize(size, size))
66 | self.leftClick.setFocusPolicy(Qt.NoFocus)
67 |
68 | self.rightClick = QPushButton(self)
69 | self.rightClick.setIcon(QIcon(':/plugins/ImportPhotos/icons/arrowRight.png'))
70 | self.rightClick.clicked.connect(self.selfwindow.rightClickButton)
71 | self.rightClick.setToolTip(self.tr('Show next photo'))
72 | self.rightClick.setStyleSheet("QPushButton{border: 0px; background: transparent;}")
73 | self.rightClick.setIconSize(QSize(size, size))
74 | self.rightClick.setFocusPolicy(Qt.NoFocus)
75 |
76 | self.setScene(self.scene)
77 | self.setMouseTracking(False)
78 | self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
79 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
80 | self.setDragMode(QGraphicsView.NoDrag)
81 |
82 | def mousePressEvent(self, event):
83 | sc_pos = self.mapToScene(event.pos())
84 | if self.panSelect:
85 | self.setDragMode(QGraphicsView.ScrollHandDrag)
86 | if self.zoomSelect:
87 | self.setDragMode(QGraphicsView.RubberBandDrag)
88 | self.afterLeftClick.emit(sc_pos.x(), sc_pos.y())
89 | QGraphicsView.mousePressEvent(self, event)
90 |
91 | def mouseDoubleClickEvent(self, event):
92 | sc_pos = self.mapToScene(event.pos())
93 | if self.zoomSelect or self.panSelect:
94 | self.zoom_data = []
95 | self.fitInView(self.sceneRect(), Qt.KeepAspectRatio)
96 | self.afterDoubleClick.emit(sc_pos.x(), sc_pos.y())
97 | QGraphicsView.mouseDoubleClickEvent(self, event)
98 |
99 | def mouseReleaseEvent(self, event):
100 | QGraphicsView.mouseReleaseEvent(self, event)
101 | sc_pos = self.mapToScene(event.pos())
102 | if self.zoomSelect:
103 | view_bb = self.sceneRect()
104 | if self.zoom_data:
105 | view_bb = self.zoom_data
106 | selection_bb = self.scene.selectionArea().boundingRect().intersected(view_bb)
107 | self.scene.setSelectionArea(QPainterPath())
108 | if selection_bb.isValid() and (selection_bb != view_bb):
109 | self.zoom_data = selection_bb
110 | self.fitInView(self.zoom_data, Qt.KeepAspectRatio)
111 | self.setDragMode(QGraphicsView.NoDrag)
112 | self.afterLeftClickReleased.emit(sc_pos.x(), sc_pos.y())
113 |
114 | def resizeEvent(self, event):
115 | self.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio)
116 |
117 | if len(self.selfwindow.allpictures) > 1:
118 | loc = self.viewport().geometry()
119 | newloc = list(loc.getRect())
120 | self.left_newloc = newloc[:]
121 | self.left_newloc[0] = self.left_newloc[0] # x
122 | self.left_newloc[1] = self.left_newloc[3] / 2.4 # y
123 | self.left_newloc[2] = self.left_newloc[2] / 5 # width
124 | self.left_newloc[3] = self.left_newloc[3] / 5 # height
125 | self.leftClick.setGeometry(QRect(*(map(round, self.left_newloc))))
126 | newloc[0] = newloc[2] - newloc[2] / 5 # x
127 | newloc[1] = newloc[3] / 2.4 # y
128 | newloc[2] = newloc[2] / 5 # width
129 | newloc[3] = newloc[3] / 5 # height
130 | self.rightClick.setGeometry(QRect(*(map(round, newloc))))
131 |
132 | # Fix rotate for the next photo
133 | self.rotate(-self.rotate_value)
134 | self.rotate_value = 0
135 |
136 | # Fix azimuth rotate for the next photo
137 | if self.rotate_azimuth_value > 0:
138 | self.rotate(-self.rotate_azimuth_value)
139 | self.rotate_azimuth_value = 0
140 |
141 | def keyPressEvent(self, e):
142 | if e.key() == Qt.Key_Right:
143 | self.selfwindow.rightClickButton()
144 |
145 | if e.key() == Qt.Key_Left:
146 | self.selfwindow.leftClickButton()
147 |
148 | if e.key() == Qt.Key_Escape:
149 | if self.selfwindow.isFullScreen():
150 | self.selfwindow.showMaximized()
151 | return
152 |
153 | if e.key() == Qt.Key_F11:
154 | if self.selfwindow.isFullScreen():
155 | self.selfwindow.showMaximized()
156 | else:
157 | self.selfwindow.showFullScreen()
158 |
159 | if e.key() == Qt.Key_Escape:
160 | self.selfwindow.close()
161 |
162 | # noinspection PyMethodMayBeStatic
163 | def tr(self, message):
164 | """Get the translation for a string using Qt translation API.
165 |
166 | We implement this ourselves since we do not inherit QObject.
167 |
168 | :param message: String for translation.
169 | :type message: str, QString
170 |
171 | :returns: Translated version of message.
172 | :rtype: QString
173 | """
174 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
175 | return QCoreApplication.translate('PhotosViewer', message)
176 |
177 |
178 | class PhotoWindow(QWidget):
179 | def __init__(self, drawSelf):
180 | super(PhotoWindow, self).__init__()
181 | self.drawSelf = drawSelf
182 |
183 | # Update for photo
184 | self.allpictures = {}
185 | self.allpicturesdates = {}
186 | self.allpicturestimes = {}
187 | self.allpicturesImpath = {} # feature id / picture path
188 | self.allpicturesAzimuth = {}
189 | self.allpicturesName = {}
190 | self.allpicturesLink = {}
191 | for i, f in enumerate(self.drawSelf.layerActive.getFeatures()):
192 | attributes = f.attributes()
193 | if 'PATH' in self.drawSelf.fields:
194 | imPath = attributes[f.fieldNameIndex('Path')]
195 | elif 'PHOTO' in self.drawSelf.fields:
196 | imPath = attributes[f.fieldNameIndex('photo')]
197 | else:
198 | imPath = ''
199 | try:
200 | dateTrue = str(attributes[f.fieldNameIndex('Date')].toString('yyyy-MM-dd'))
201 | except:
202 | dateTrue = str(attributes[f.fieldNameIndex('Date')])
203 | try:
204 | timeTrue = str(attributes[f.fieldNameIndex('Time')].toString('hh:mm:ss'))
205 | except:
206 | timeTrue = str(attributes[f.fieldNameIndex('Time')])
207 | try:
208 | name_ = attributes[f.fieldNameIndex('Name')]
209 | name_ = name_[:-4]
210 | except:
211 | try:
212 | name_ = attributes[f.fieldNameIndex('filename')]
213 | except:
214 | name_ = ''
215 |
216 | if not os.path.exists(imPath):
217 | try:
218 | if self.drawSelf.prj.fileName() and 'RELPATH' in self.drawSelf.fields:
219 | imPath = os.path.join(
220 | QFileInfo(self.drawSelf.prj.fileName()).absolutePath(),
221 | attributes[f.fieldNameIndex('RelPath')])
222 | except:
223 | imPath = ''
224 | try:
225 | azimuth = attributes[f.fieldNameIndex('Azimuth')]
226 | except:
227 | azimuth = None
228 |
229 | try:
230 | link = attributes[f.fieldNameIndex('Link')]
231 | except:
232 | link = None
233 |
234 | self.allpictures[f.id()] = name_
235 | self.allpicturesdates[f.id()] = dateTrue
236 | self.allpicturestimes[f.id()] = timeTrue
237 | self.allpicturesImpath[f.id()] = imPath
238 | self.allpicturesAzimuth[f.id()] = azimuth
239 | self.allpicturesName[f.id()] = name_
240 | self.allpicturesLink[f.id()] = link
241 |
242 | self.viewer = PhotosViewer(self)
243 |
244 | ######################################################################################
245 |
246 | self.setWindowTitle('Photo')
247 | self.setWindowIcon(QIcon(':/plugins/ImportPhotos/icons/icon.png'))
248 |
249 | menu_bar = QMenuBar(self)
250 | menu_bar.setGeometry(QRect(0, 0, 10000, 26))
251 |
252 | file_menu = menu_bar.addMenu(self.tr('File'))
253 | self.saveas = file_menu.addAction(self.tr('Save As'))
254 | self.saveas.triggered.connect(self.saveas_call)
255 |
256 | filters_menu = menu_bar.addMenu(self.tr('Filters'))
257 |
258 | self.gray_filter_status = False
259 | self.gray_filter_btn = filters_menu.addAction(self.tr('Gray Filter'))
260 | self.gray_filter_btn.setCheckable(True)
261 | self.gray_filter_btn.triggered.connect(self.gray_filter_call)
262 |
263 | self.mirror_filter_status = False
264 | self.mirror_filter_btn = filters_menu.addAction(self.tr('Mirror Filter'))
265 | self.mirror_filter_btn.setCheckable(True)
266 | self.mirror_filter_btn.triggered.connect(self.mirror_filter_call)
267 |
268 | self.mono_filter_status = False
269 | self.mono_filter_btn = filters_menu.addAction(self.tr('Mono Filter'))
270 | self.mono_filter_btn.setCheckable(True)
271 | self.mono_filter_btn.triggered.connect(self.mono_filter_call)
272 |
273 | try:
274 | if opencv:
275 | opencv_menu = menu_bar.addMenu(self.tr('Opencv'))
276 | bands_menu = menu_bar.addMenu(self.tr('Bands'))
277 |
278 | self.opencv_filt_status = {'Edges': False, 'Red': False, 'Green': False, 'Blue': False,
279 | '2DConvolution': False, 'Median': False, 'Gaussian': False,
280 | 'Gaussian Highpass': False}
281 | self.edges_filter_btn = opencv_menu.addAction(self.tr('Edges Filter'))
282 | self.edges_filter_btn.setCheckable(True)
283 | self.edges_filter_btn.triggered.connect(self.edges_filter_call)
284 |
285 | self.red_filter_btn = bands_menu.addAction(self.tr('Red Band'))
286 | self.red_filter_btn.setCheckable(True)
287 | self.red_filter_btn.triggered.connect(self.red_filter_call)
288 |
289 | self.blue_filter_btn = bands_menu.addAction(self.tr('Blue Band'))
290 | self.blue_filter_btn.setCheckable(True)
291 | self.blue_filter_btn.triggered.connect(self.blue_filter_call)
292 |
293 | self.green_filter_btn = bands_menu.addAction(self.tr('Green Band'))
294 | self.green_filter_btn.setCheckable(True)
295 | self.green_filter_btn.triggered.connect(self.green_filter_call)
296 |
297 | self.averaging_filter_btn = opencv_menu.addAction(self.tr('2D Convolution Filter'))
298 | self.averaging_filter_btn.setCheckable(True)
299 | self.averaging_filter_btn.triggered.connect(self.averaging_filter_call)
300 |
301 | self.median_filter_btn = opencv_menu.addAction(self.tr('Median Filter'))
302 | self.median_filter_btn.setCheckable(True)
303 | self.median_filter_btn.triggered.connect(self.median_filter_call)
304 |
305 | self.gaussian_filter_btn = opencv_menu.addAction(self.tr('Gaussian Filter'))
306 | self.gaussian_filter_btn.setCheckable(True)
307 | self.gaussian_filter_btn.triggered.connect(self.gaussian_filter_call)
308 |
309 | self.gaussian_high_filter_btn = opencv_menu.addAction(self.tr('Gaussian Highpass'))
310 | self.gaussian_high_filter_btn.setCheckable(True)
311 | self.gaussian_high_filter_btn.triggered.connect(self.gaussian_high_filter_call)
312 | except:
313 | pass
314 | # # Add Filter buttons
315 | sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
316 |
317 | self.add_window_place = QLabel(self) # temporary
318 | self.add_window_place.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum))
319 | self.add_window_place.setFrameShape(QFrame.NoFrame)
320 | self.add_window_place.setOpenExternalLinks(True) # To make link clickable
321 |
322 | self.infoPhoto1 = QLabel(self)
323 | self.infoPhoto1.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum))
324 | self.infoPhoto1.setStyleSheet("background-color: lightgray;") # Light gray close to white
325 | self.infoPhoto1.setFrameShape(QFrame.Box)
326 |
327 | self.infoPhoto2 = QLabel(self)
328 | self.infoPhoto2.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum))
329 | self.infoPhoto2.setStyleSheet("background-color: lightgray;") # Light gray close to white
330 | self.infoPhoto2.setFrameShape(QFrame.Box)
331 |
332 | self.infoPhoto3 = QLabel(self)
333 | self.infoPhoto3.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum))
334 | self.infoPhoto3.setStyleSheet("background-color: lightgray;") # Light gray close to white
335 | self.infoPhoto3.setFrameShape(QFrame.Box)
336 |
337 | self.extent = QPushButton(self)
338 | self.extent.setSizePolicy(sizePolicy)
339 | self.extent.setIcon(QIcon(':/plugins/ImportPhotos/icons/mActionZoomFullExtent.svg'))
340 | self.extent.clicked.connect(self.extentbutton)
341 |
342 | self.zoom = QPushButton(self)
343 | self.zoom.setSizePolicy(sizePolicy)
344 | self.zoom.setIcon(QIcon(':/plugins/ImportPhotos/icons/method-draw-image.svg'))
345 | self.zoom.clicked.connect(self.zoombutton)
346 |
347 | self.pan = QPushButton(self)
348 | self.pan.setSizePolicy(sizePolicy)
349 | self.pan.setIcon(QIcon(':/plugins/ImportPhotos/icons/mActionPan.svg'))
350 | self.pan.clicked.connect(self.panbutton)
351 |
352 | self.zoom_to_select = QPushButton(self)
353 | self.zoom_to_select.setSizePolicy(sizePolicy)
354 | self.zoom_to_select.setIcon(QIcon(':/plugins/ImportPhotos/icons/mActionZoomToSelected.svg'))
355 | self.zoom_to_select.clicked.connect(self.zoom_to_selectbutton)
356 |
357 | self.rotate_option = QPushButton(self)
358 | self.rotate_option.setSizePolicy(sizePolicy)
359 | self.rotate_option.setIcon(QIcon(':/plugins/ImportPhotos/icons/rotate.png'))
360 | self.rotate_option.clicked.connect(self.rotatebutton)
361 |
362 | self.rotate_azimuth = QPushButton(self)
363 | self.rotate_azimuth.setSizePolicy(sizePolicy)
364 | self.rotate_azimuth.setIcon(QIcon(':/plugins/ImportPhotos/icons/tonorth.png'))
365 | self.rotate_azimuth.clicked.connect(self.rotate_azimuthbutton)
366 |
367 | self.hide_arrow = QPushButton(self)
368 | self.hide_arrow.setSizePolicy(sizePolicy)
369 | self.hide_arrow.setIcon(QIcon(':/plugins/ImportPhotos/icons/arrowRight.png'))
370 | self.hide_arrow.clicked.connect(self.hide_arrow_button)
371 | if len(self.allpictures) > 1:
372 | self.hide_arrow.setEnabled(True)
373 | else:
374 | self.hide_arrow.setEnabled(False)
375 |
376 | # Add tips on buttons
377 | self.extent.setToolTip(self.tr('Extent photo'))
378 | self.zoom.setToolTip(self.tr('Select area to zoom'))
379 | self.pan.setToolTip(self.tr('Pan'))
380 | self.zoom_to_select.setToolTip(self.tr('Zoom to selected photo'))
381 | self.rotate_option.setToolTip(self.tr('Rotate 45°'))
382 | self.rotate_azimuth.setToolTip(self.tr('Rotate to azimuth'))
383 | self.hide_arrow.setToolTip(self.tr('Hide arrows'))
384 |
385 | # Arrange layout
386 | VBlayout = QVBoxLayout(self)
387 | HBlayout = QHBoxLayout()
388 | HBlayout2 = QHBoxLayout()
389 | HBlayoutTop = QHBoxLayout()
390 | HBlayoutTop.setAlignment(Qt.AlignCenter)
391 | HBlayoutTop.addWidget(self.add_window_place)
392 | HBlayout2.addWidget(self.viewer)
393 | HBlayout.setAlignment(Qt.AlignCenter)
394 | HBlayout.addWidget(self.infoPhoto1)
395 | HBlayout.addWidget(self.infoPhoto2)
396 | HBlayout.addWidget(self.infoPhoto3)
397 | HBlayout.addWidget(self.extent)
398 | HBlayout.addWidget(self.zoom)
399 | HBlayout.addWidget(self.pan)
400 | HBlayout.addWidget(self.rotate_option)
401 | HBlayout.addWidget(self.rotate_azimuth)
402 | HBlayout.addWidget(self.zoom_to_select)
403 | HBlayout.addWidget(self.hide_arrow)
404 |
405 | spacelabel = QHBoxLayout()
406 | spacelabel.addWidget(QLabel(self))
407 | VBlayout.addLayout(spacelabel)
408 |
409 | VBlayout.addLayout(HBlayoutTop)
410 | VBlayout.addLayout(HBlayout2)
411 | VBlayout.addLayout(HBlayout)
412 |
413 | def gray_filter_call(self):
414 | if self.gray_filter_btn.isChecked():
415 | self.gray_filter_status = True
416 | self.update_filters('filters_tab')
417 | else:
418 | self.gray_filter_status = False
419 | self.gray_filter_btn.setChecked(False)
420 | self.updateWindow()
421 |
422 | def mirror_filter_call(self):
423 | if self.mirror_filter_btn.isChecked():
424 | self.mirror_filter_status = True
425 | self.update_filters('filters_tab')
426 | else:
427 | self.mirror_filter_status = False
428 | self.mirror_filter_btn.setChecked(False)
429 | self.updateWindow()
430 |
431 | def mono_filter_call(self):
432 | if self.mono_filter_btn.isChecked():
433 | self.mono_filter_status = True
434 | self.update_filters('filters_tab')
435 | else:
436 | self.mono_filter_status = False
437 | self.mono_filter_btn.setChecked(False)
438 | self.updateWindow()
439 |
440 | def averaging_filter_call(self):
441 | if self.averaging_filter_btn.isChecked():
442 | self.opencv_filt_status['2DConvolution'] = True
443 | self.update_filters('averaging')
444 | else:
445 | self.opencv_filt_status['2DConvolution'] = False
446 | self.averaging_filter_btn.setChecked(False)
447 | self.updateWindow()
448 |
449 | def median_filter_call(self):
450 | if self.median_filter_btn.isChecked():
451 | self.opencv_filt_status['Median'] = True
452 | self.update_filters('median')
453 | else:
454 | self.opencv_filt_status['Median'] = False
455 | self.median_filter_btn.setChecked(False)
456 | self.updateWindow()
457 |
458 | def gaussian_filter_call(self):
459 | if self.gaussian_filter_btn.isChecked():
460 | self.opencv_filt_status['Gaussian'] = True
461 | self.update_filters('gaussian')
462 | else:
463 | self.opencv_filt_status['Gaussian'] = False
464 | self.gaussian_filter_btn.setChecked(False)
465 | self.updateWindow()
466 |
467 | def gaussian_high_filter_call(self):
468 | if self.gaussian_high_filter_btn.isChecked():
469 | self.opencv_filt_status['Gaussian Highpass'] = True
470 | self.update_filters('fourrier')
471 | else:
472 | self.opencv_filt_status['Gaussian Highpass'] = False
473 | self.gaussian_high_filter_btn.setChecked(False)
474 | self.updateWindow()
475 |
476 | def red_filter_call(self):
477 | if self.red_filter_btn.isChecked():
478 | self.opencv_filt_status['Red'] = True
479 | self.update_filters('red')
480 | else:
481 | self.opencv_filt_status['Red'] = False
482 | self.red_filter_btn.setChecked(False)
483 | self.updateWindow()
484 |
485 | def blue_filter_call(self):
486 | if self.blue_filter_btn.isChecked():
487 | self.opencv_filt_status['Blue'] = True
488 | self.update_filters('blue')
489 | else:
490 | self.opencv_filt_status['Blue'] = False
491 | self.blue_filter_btn.setChecked(False)
492 | self.updateWindow()
493 |
494 | def green_filter_call(self):
495 | if self.green_filter_btn.isChecked():
496 | self.opencv_filt_status['Green'] = True
497 | self.update_filters('green')
498 | else:
499 | self.opencv_filt_status['Green'] = False
500 | self.green_filter_btn.setChecked(False)
501 | self.updateWindow()
502 |
503 | def edges_filter_call(self):
504 | if self.edges_filter_btn.isChecked():
505 | self.opencv_filt_status['Edges'] = True
506 | self.update_filters('edges')
507 | else:
508 | self.opencv_filt_status['Edges'] = False
509 | self.edges_filter_btn.setChecked(False)
510 | self.updateWindow()
511 |
512 | def update_filters(self, filter):
513 | if opencv:
514 | if filter != 'fourrier':
515 | self.opencv_filt_status['Gaussian Highpass'] = False
516 | self.gaussian_high_filter_btn.setChecked(False)
517 | if filter != 'median':
518 | self.opencv_filt_status['Median'] = False
519 | self.median_filter_btn.setChecked(False)
520 | if filter != 'gaussian':
521 | self.opencv_filt_status['Gaussian'] = False
522 | self.gaussian_filter_btn.setChecked(False)
523 | if filter != 'averaging':
524 | self.opencv_filt_status['2DConvolution'] = False
525 | self.averaging_filter_btn.setChecked(False)
526 | if filter != 'blue':
527 | self.opencv_filt_status['Blue'] = False
528 | self.blue_filter_btn.setChecked(False)
529 | if filter != 'red':
530 | self.opencv_filt_status['Red'] = False
531 | self.red_filter_btn.setChecked(False)
532 | if filter != 'green':
533 | self.opencv_filt_status['Green'] = False
534 | self.green_filter_btn.setChecked(False)
535 | if filter != 'edges':
536 | self.opencv_filt_status['Edges'] = False
537 | self.edges_filter_btn.setChecked(False)
538 |
539 | if filter != 'filters_tab':
540 | self.gray_filter_status = False
541 | self.gray_filter_btn.setChecked(False)
542 | if filter != 'filters_tab':
543 | self.mirror_filter_status = False
544 | self.mirror_filter_btn.setChecked(False)
545 | if filter != 'filters_tab':
546 | self.mono_filter_status = False
547 | self.mono_filter_btn.setChecked(False)
548 |
549 | def saveas_call(self):
550 | self.outputPath = QFileDialog.getSaveFileName(None, self.tr('Save Image'), os.path.join(
551 | os.path.join(os.path.expanduser('~')), 'Desktop'), '.png')
552 | self.outputPath = self.outputPath[0]
553 | if self.outputPath == '':
554 | return
555 | self.drawSelf.getImage.save(self.outputPath + '.png')
556 | self.showMessage(title='ImportPhotos',
557 | msg=self.tr('Save image at "') + self.outputPath + '.png' + self.tr('" succesfull.'),
558 | button='OK', icon='Info')
559 |
560 | def showMessage(self, title, msg, button, icon):
561 | msgBox = QMessageBox()
562 | if icon == 'Warning':
563 | msgBox.setIcon(QMessageBox.Warning)
564 | if icon == 'Info':
565 | msgBox.setIcon(QMessageBox.Information)
566 | msgBox.setWindowTitle(title)
567 | msgBox.setText(msg)
568 | msgBox.setStandardButtons(QMessageBox.Ok)
569 | font = QFont()
570 | font.setPointSize(9)
571 | msgBox.setFont(font)
572 | msgBox.setWindowFlags(Qt.CustomizeWindowHint | Qt.WindowStaysOnTopHint | Qt.WindowCloseButtonHint)
573 | buttonY = msgBox.button(QMessageBox.Ok)
574 | buttonY.setText(button)
575 | buttonY.setFont(font)
576 | msgBox.exec_()
577 |
578 | def hide_arrow_button(self):
579 | icon_right = QIcon(':/plugins/ImportPhotos/icons/arrowRight.png')
580 | if self.viewer.leftClick.icon().isNull():
581 | self.viewer.leftClick.setIcon(QIcon(':/plugins/ImportPhotos/icons/arrowLeft.png'))
582 | self.viewer.rightClick.setIcon(icon_right)
583 | self.hide_arrow.setIcon(icon_right)
584 | self.hide_arrow.setToolTip(self.tr('Hide arrows'))
585 | else:
586 | self.viewer.leftClick.setIcon(QIcon(''))
587 | self.viewer.rightClick.setIcon(QIcon(''))
588 | self.hide_arrow.setToolTip(self.tr('Show arrows'))
589 | self.hide_arrow.setIcon(icon_right)
590 |
591 | def leftClickButton(self):
592 | lastId = list(self.allpicturesImpath.keys())[-1]
593 | it = iter(self.allpicturesImpath)
594 |
595 | prevKey = lastId
596 | for key in it:
597 | if key == self.drawSelf.featureIndex:
598 | self.drawSelf.featureIndex = prevKey
599 | break
600 | prevKey = key
601 | self.updateWindow()
602 |
603 | def rightClickButton(self):
604 | firstId = list(self.allpicturesImpath.keys())[0]
605 | it = iter(self.allpicturesImpath)
606 | for key in it:
607 | if key == self.drawSelf.featureIndex:
608 | self.drawSelf.featureIndex = next(it, firstId)
609 | break
610 | self.updateWindow()
611 |
612 | def updateWindow(self):
613 | imPath = self.allpicturesImpath[self.drawSelf.featureIndex]
614 | try:
615 | if not os.path.exists(imPath):
616 | c = self.drawSelf.noImageFound()
617 | imPath = ''
618 | except:
619 | c = self.drawSelf.noImageFound()
620 | imPath = ''
621 |
622 | self.viewer.scene.clear()
623 | self.drawSelf.getImage = QImage(imPath)
624 |
625 | if self.gray_filter_status:
626 | self.drawSelf.getImage = self.drawSelf.getImage.convertToFormat(QImage.Format_Grayscale8)
627 | if self.mirror_filter_status:
628 | self.drawSelf.getImage = self.drawSelf.getImage.mirrored(True, False)
629 | if self.mono_filter_status:
630 | self.drawSelf.getImage = self.drawSelf.getImage.convertToFormat(QImage.Format_Mono)
631 |
632 | if opencv:
633 | if self.opencv_filt_status['2DConvolution']:
634 | ## Average filter
635 | img = cv2.imread(imPath)
636 | kernel = np.ones((5, 5), np.float32) / 25
637 | filt = cv2.filter2D(img, -1, kernel)
638 |
639 | if self.opencv_filt_status['Red']:
640 | ## RED
641 | img = np.array(cv2.imread(imPath))
642 | filt = np.zeros(img.shape, dtype='uint8')
643 | filt[:, :, 2] = img[:, :, 2]
644 | if self.opencv_filt_status['Blue']:
645 | ## BLUE
646 | img = np.array(cv2.imread(imPath))
647 | filt = np.zeros(img.shape, dtype='uint8')
648 | filt[:, :, 0] = img[:, :, 0]
649 | if self.opencv_filt_status['Green']:
650 | ## GREEN
651 | img = np.array(cv2.imread(imPath))
652 | filt = np.zeros(img.shape, dtype='uint8')
653 | filt[:, :, 1] = img[:, :, 1]
654 |
655 | if self.opencv_filt_status['Edges']:
656 | ## Edges filter
657 | img = cv2.imread(imPath, 0)
658 | filt = cv2.Canny(img, 100, 200)
659 |
660 | if self.opencv_filt_status['Median']:
661 | img = cv2.imread(imPath)
662 | filt = cv2.medianBlur(img, 5)
663 |
664 | if self.opencv_filt_status['Gaussian']:
665 | img = cv2.imread(imPath)
666 | filt = cv2.GaussianBlur(img, (5, 5), 0)
667 |
668 | if self.opencv_filt_status['Gaussian Highpass']:
669 | from scipy import ndimage
670 | data = np.array(cv2.imread(imPath))
671 | lowpass = ndimage.gaussian_filter(data, 3)
672 | filt = data - lowpass
673 |
674 | for value in self.opencv_filt_status:
675 | if self.opencv_filt_status[value] == True:
676 | # Fix for all opencv filters
677 | height, width = filt.shape[:2]
678 | try:
679 | rgb = cv2.cvtColor(filt, cv2.COLOR_GRAY2RGB)
680 | except:
681 | rgb = cv2.cvtColor(filt, cv2.COLOR_BGR2RGB)
682 |
683 | self.drawSelf.getImage = QImage(rgb, width, height, QImage.Format_RGB888)
684 | break
685 |
686 | pixmap = QPixmap.fromImage(self.drawSelf.getImage)
687 | self.viewer.scene.addPixmap(pixmap)
688 | self.viewer.setSceneRect(QRectF(pixmap.rect()))
689 | self.drawSelf.layerActive.selectByIds([self.drawSelf.featureIndex])
690 |
691 | self.viewer.resizeEvent([])
692 | self.extentbutton()
693 | self.infoPhoto1.setText(
694 | self.tr('Date: ') + self.allpicturesdates[self.drawSelf.featureIndex])
695 | self.infoPhoto2.setText(
696 | self.tr('Time: ') + self.allpicturestimes[self.drawSelf.featureIndex][0:8])
697 | self.infoPhoto3.setText(self.tr('Layer: ') + self.drawSelf.layerActiveName)
698 | link = self.allpicturesLink[self.drawSelf.featureIndex]
699 | header = self.allpicturesName[self.drawSelf.featureIndex]
700 | if link is not None:
701 | header = f'{header}'
702 | self.add_window_place.setText(header)
703 | azimuth = self.allpicturesAzimuth[self.drawSelf.featureIndex]
704 | if type(azimuth) is str:
705 | try:
706 | azimuth = float(azimuth)
707 | except:
708 | pass
709 | if type(azimuth) is float:
710 | if azimuth > 0:
711 | self.rotate_azimuth.setEnabled(True)
712 | return
713 | self.rotate_azimuth.setEnabled(False)
714 |
715 | def rotatebutton(self):
716 | self.viewer.rotate(90)
717 | self.viewer.rotate_value = self.viewer.rotate_value + 90
718 | if self.viewer.rotate_value == 360:
719 | self.viewer.rotate_value = 0
720 |
721 | def rotate_azimuthbutton(self):
722 | if self.viewer.rotate_azimuth_value == 0:
723 | azimuth = self.allpicturesAzimuth[self.drawSelf.featureIndex]
724 | if type(azimuth) is str:
725 | azimuth = float(azimuth)
726 | self.viewer.rotate(azimuth)
727 | self.viewer.rotate_azimuth_value = azimuth
728 | return
729 | if self.viewer.rotate_azimuth_value > 0:
730 | self.viewer.rotate(-self.viewer.rotate_azimuth_value)
731 | self.viewer.rotate_azimuth_value = 0
732 |
733 | def zoom_to_selectbutton(self):
734 | self.drawSelf.iface.actionZoomToSelected().trigger()
735 |
736 | def panbutton(self):
737 | self.viewer.panSelect = True
738 | self.viewer.zoomSelect = False
739 | self.viewer.setCursor(Qt.OpenHandCursor)
740 | self.viewer.setDragMode(QGraphicsView.ScrollHandDrag)
741 |
742 | def zoombutton(self):
743 | self.viewer.panSelect = False
744 | self.viewer.zoomSelect = True
745 | self.viewer.setCursor(Qt.CrossCursor)
746 | self.viewer.setDragMode(QGraphicsView.RubberBandDrag)
747 |
748 | def extentbutton(self):
749 | self.viewer.zoom_data = []
750 | self.viewer.fitInView(self.viewer.sceneRect(), Qt.KeepAspectRatio)
751 | self.viewer.panSelect = False
752 | self.viewer.zoomSelect = False
753 | self.viewer.setCursor(Qt.ArrowCursor)
754 | self.viewer.setDragMode(QGraphicsView.NoDrag)
755 |
756 | # noinspection PyMethodMayBeStatic
757 | def tr(self, message):
758 | """Get the translation for a string using Qt translation API.
759 |
760 | We implement this ourselves since we do not inherit QObject.
761 |
762 | :param message: String for translation.
763 | :type message: str, QString
764 |
765 | :returns: Translated version of message.
766 | :rtype: QString
767 | """
768 | # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
769 | return QCoreApplication.translate('PhotoWindow', message)
770 |
--------------------------------------------------------------------------------
/i18n/ImportPhotos_fr.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KIOS-Research/ImportPhotos/3196e97fe15f5f740a6f037fd507aca185fc50e9/i18n/ImportPhotos_fr.qm
--------------------------------------------------------------------------------
/i18n/ImportPhotos_fr.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |