What I did is create a postgres/ postgis database of all my pictures that includes a column thumbnail of the images at a reasonable scale. I can then create a layer that shows the picture locations as in the figure below.

I then made a plugin that when I click on the canvas it selects the nearest picture and displays it in a separate window with some meta data.

The core code of the plugin is:
# -*- coding: utf-8 -*-
"""
/***************************************************************************
PictureLinker
A QGIS plugin
Shows picture of points
Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
-------------------
begin : 2020-09-14
git sha : $Format:%H$
copyright : (C) 2020 by Bruno Vermeulen
email : [email protected]
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
import os
# if using Linux then add a path to the site-packages
if os.name == 'posix':
import sys
import_path = os.path.expanduser('~/.local/share/QGIS/QGIS3/profiles/default/python/site-packages')
sys.path.insert(0, import_path)
from qgis.core import (
QgsProject, QgsDistanceArea, QgsFeatureRequest, QgsPointXY,
)
from qgis.gui import (
QgsMapTool, QgsMapToolEmitPoint, QgsVertexMarker,
)
from qgis.PyQt.QtCore import Qt, QSettings, QTranslator, QCoreApplication
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction
from .pyqt_picture import PictureShow
# Initialize Qt resources from file resources.py
from .resources import qInitResources, qCleanupResources
qInitResources()
pictures_layer = 'picture year'
d = QgsDistanceArea()
d.setEllipsoid('WGS84')
class PicLayer():
def __init__(self):
layer = QgsProject.instance().mapLayersByName(pictures_layer)[0]
self._features = list(layer.getFeatures(QgsFeatureRequest()))
@property
def nearest_feature(self):
return self._nearest_feature
def select_nearest_picture(self, point):
''' Calculates the distance from point and all pictures and selects the
one with the minimum distance. Returns point either of closest
picture or original point if not picture is found
argument: point: QgsPointXY
returns: point: QgsPointXY
'''
min_distance = float('inf')
self._nearest_feature = None
for feature in self._features:
distance = d.measureLine(feature.geometry().asPoint(), point)
if distance < min_distance:
min_distance = distance
self._nearest_feature = feature
if self._nearest_feature:
return self._nearest_feature.geometry().asPoint()
else:
return point
class SelectPicMapTool(QgsMapToolEmitPoint):
def __init__(self, canvas):
self.canvas = canvas
QgsMapToolEmitPoint.__init__(self, self.canvas)
self.pic_layer = PicLayer()
self.marker = None
self.picshow = PictureShow()
def reset(self):
self.canvas.scene().removeItem(self.marker)
def canvasPressEvent(self, e):
point = self.toMapCoordinates(e.pos())
point = QgsPointXY(point.x(), point.y())
self.reset()
point = self.pic_layer.select_nearest_picture(point)
print( #open console to see result
f'picture id: {self.pic_layer.nearest_feature.attributes()[2]} - '
f'year taken: {self.pic_layer.nearest_feature.attributes()[3]:.0f} - '
f'x: {point.x():.6f}, y: {point.y():.6f}'
)
self.show_marker(point)
self.show_picture()
def show_marker(self, point):
self.marker = QgsVertexMarker(self.canvas)
self.marker.setColor(Qt.yellow)
self.marker.setIconSize(6)
self.marker.setIconType(QgsVertexMarker.ICON_CROSS) # or ICON_BOX, ICON_X
self.marker.setPenWidth(3)
self.marker.setCenter(point)
self.marker.show()
def show_picture(self):
self.picshow.cntr_select_pic(
self.pic_layer.nearest_feature.attributes()[2])
self.picshow.show_picture()
def deactivate(self):
self.reset()
self.picshow.cntr_quit()
class PictureLinker:
"""QGIS Plugin ImplementationL"""
def __init__(self, iface):
self.iface = iface
self.plugin_dir = os.path.dirname(__file__)
locale = QSettings().value('locale/userLocale')[0:2]
locale_path = os.path.join(
self.plugin_dir,
'i18n',
'PictureLinker_{}.qm'.format(locale))
if os.path.exists(locale_path):
self.translator = QTranslator()
self.translator.load(locale_path)
QCoreApplication.installTranslator(self.translator)
self.actions = []
self.menu = self.tr(u'&Picture linker')
def tr(self, message):
return QCoreApplication.translate('PictureLinker', message)
def add_action(self,
icon_path,
text,
callback,
checkable=True,
enabled_flag=True,
add_to_menu=True,
add_to_toolbar=True,
status_tip=None,
whats_this=None,
parent=None):
icon = QIcon(icon_path)
action = QAction(icon, text, parent)
action.triggered.connect(callback)
action.setEnabled(enabled_flag)
if checkable:
action.setCheckable(True)
if status_tip is not None:
action.setStatusTip(status_tip)
if whats_this is not None:
action.setWhatsThis(whats_this)
if add_to_toolbar:
self.iface.addToolBarIcon(action)
if add_to_menu:
self.iface.addPluginToMenu(
self.menu,
action)
self.actions.append(action)
return action
def initGui(self):
icon_path = ':/plugins/picture_linker/icon.png'
self.add_action(
icon_path,
text=self.tr('Show pictures'),
callback=self.run,
parent=self.iface.mainWindow())
def unload(self):
"""Removes the plugin menu item and icon from QGIS GUI."""
for action in self.actions:
self.iface.removePluginMenu(
self.tr(u'&Picture linker'),
action
)
self.iface.removeToolBarIcon(action)
def run(self):
try:
self.select_pic.deactivate()
except AttributeError:
pass
if self.actions[0].isChecked():
canvas = self.iface.mapCanvas()
self.select_pic = SelectPicMapTool(canvas)
canvas.setMapTool(self.select_pic)
else:
self.select_pic.deactivate()
self.iface.mapCanvas().unsetMapTool(self.select_pic)
Note 1:there is a module pyqt_picture
that deals with extracting the thumbnail picture from the database and displays the picture
Note 2: The thumbnail size is (600, 600) and for 25,000 pictures the database size is about 2 Gigabyte, so quite managable