mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-21 10:37:18 +02:00
[ui] create thumbnails asynchronously
This commit is contained in:
parent
220bcfb9e2
commit
60c8e779d5
3 changed files with 98 additions and 47 deletions
|
@ -116,23 +116,10 @@ class MeshroomApp(QApplication):
|
||||||
pwd = os.path.dirname(__file__)
|
pwd = os.path.dirname(__file__)
|
||||||
self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))
|
self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))
|
||||||
|
|
||||||
# User specified thumbnail directory
|
# Initialize thumbnail cache:
|
||||||
thumbnailDir = os.getenv('MESHROOM_THUMBNAIL_DIR')
|
# - read related environment variables
|
||||||
if thumbnailDir is not None:
|
# - clean cache directory and make sure it exists on disk
|
||||||
ThumbnailCache.thumbnailDir = thumbnailDir
|
ThumbnailCache.initialize()
|
||||||
|
|
||||||
# User specifed time limit for thumbnails on disk (expressed in days)
|
|
||||||
thumbnailTimeLimit = os.getenv('MESHROOM_THUMBNAIL_TIME_LIMIT')
|
|
||||||
if thumbnailTimeLimit is not None:
|
|
||||||
ThumbnailCache.storageTimeLimit = float(thumbnailTimeLimit)
|
|
||||||
|
|
||||||
# User specifed maximum number of thumbnails on disk
|
|
||||||
thumbnailMaxNumberOnDisk = os.getenv('MESHROOM_MAX_THUMBNAILS_ON_DISK')
|
|
||||||
if thumbnailMaxNumberOnDisk is not None:
|
|
||||||
ThumbnailCache.maxThumbnailsOnDisk = int(thumbnailMaxNumberOnDisk)
|
|
||||||
|
|
||||||
# Clean thumbnail directory
|
|
||||||
ThumbnailCache.clean()
|
|
||||||
|
|
||||||
# QML engine setup
|
# QML engine setup
|
||||||
qmlDir = os.path.join(pwd, "qml")
|
qmlDir = os.path.join(pwd, "qml")
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
|
from meshroom.common import Signal
|
||||||
|
|
||||||
from PySide2.QtCore import QObject, Slot, QSize, QUrl, Qt
|
from PySide2.QtCore import QObject, Slot, QSize, QUrl, Qt
|
||||||
from PySide2.QtGui import QImageReader, QImageWriter
|
from PySide2.QtGui import QImageReader, QImageWriter
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pathlib
|
from pathlib import Path
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
from multiprocessing.pool import ThreadPool
|
||||||
|
|
||||||
|
|
||||||
class ThumbnailCache(QObject):
|
class ThumbnailCache(QObject):
|
||||||
"""
|
"""ThumbnailCache provides an abstraction for the thumbnail cache on disk, available in QML.
|
||||||
ThumbnailCache provides an abstraction for the thumbnail cache on disk, available in QML.
|
|
||||||
|
|
||||||
For a given image file, it ensures the corresponding thumbnail exists (by creating it if necessary)
|
For a given image file, it ensures the corresponding thumbnail exists (by creating it if necessary)
|
||||||
and gives access to it.
|
and gives access to it.
|
||||||
|
Since creating thumbnails can be long (as it requires to read the full image from disk)
|
||||||
|
it is performed asynchronously to avoid blocking the main thread.
|
||||||
|
|
||||||
The default cache location is a subdirectory of the user's home directory:
|
The default cache location is a subdirectory of the user's home directory:
|
||||||
~/Meshroom/thumbnails.
|
~/Meshroom/thumbnails.
|
||||||
|
@ -34,7 +38,7 @@ class ThumbnailCache(QObject):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Thumbnail cache directory
|
# Thumbnail cache directory
|
||||||
thumbnailDir = os.path.join(pathlib.Path.home(), 'Meshroom', 'thumbnails')
|
thumbnailDir = os.path.join(Path.home(), 'Meshroom', 'thumbnails')
|
||||||
|
|
||||||
# Thumbnail dimensions limit (the actual dimensions of a thumbnail will depend on the aspect ratio)
|
# Thumbnail dimensions limit (the actual dimensions of a thumbnail will depend on the aspect ratio)
|
||||||
thumbnailSize = QSize(100, 100)
|
thumbnailSize = QSize(100, 100)
|
||||||
|
@ -45,39 +49,90 @@ class ThumbnailCache(QObject):
|
||||||
# Maximum number of thumbnails in the cache directory
|
# Maximum number of thumbnails in the cache directory
|
||||||
maxThumbnailsOnDisk = 100000
|
maxThumbnailsOnDisk = 100000
|
||||||
|
|
||||||
@Slot(QUrl, result=QUrl)
|
# Signal to notify listeners that a thumbnail was created and written on disk
|
||||||
def thumbnail(self, imgSource):
|
# This signal has one argument: the url of the image that the thumbnail is associated to
|
||||||
"""
|
thumbnailCreated = Signal(QUrl)
|
||||||
Retrieve the filepath of the thumbnail corresponding to a given image.
|
|
||||||
If the thumbnail does not exist on disk, it is created.
|
# Thread pool for running createThumbnail asynchronously on a fixed number of worker threads
|
||||||
|
pool = ThreadPool()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize():
|
||||||
|
"""Initialize static fields in cache class and cache directory on disk."""
|
||||||
|
# User specified thumbnail directory
|
||||||
|
dir = os.getenv('MESHROOM_THUMBNAIL_DIR')
|
||||||
|
if dir is not None:
|
||||||
|
ThumbnailCache.thumbnailDir = dir
|
||||||
|
|
||||||
|
# User specifed time limit for thumbnails on disk (expressed in days)
|
||||||
|
timeLimit = os.getenv('MESHROOM_THUMBNAIL_TIME_LIMIT')
|
||||||
|
if timeLimit is not None:
|
||||||
|
ThumbnailCache.storageTimeLimit = float(timeLimit)
|
||||||
|
|
||||||
|
# User specifed maximum number of thumbnails on disk
|
||||||
|
maxOnDisk = os.getenv('MESHROOM_MAX_THUMBNAILS_ON_DISK')
|
||||||
|
if maxOnDisk is not None:
|
||||||
|
ThumbnailCache.maxThumbnailsOnDisk = int(maxOnDisk)
|
||||||
|
|
||||||
|
# Clean thumbnail directory
|
||||||
|
ThumbnailCache.clean()
|
||||||
|
|
||||||
|
# Make sure the thumbnail directory exists before writing into it
|
||||||
|
os.makedirs(ThumbnailCache.thumbnailDir, exist_ok=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def thumbnailPath(imgPath):
|
||||||
|
"""Use SHA1 hashing to associate a unique thumbnail to an image.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
imgSource (QUrl): the filepath to the input image
|
imgPath (str): filepath to the input image
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
QUrl: the filepath to the corresponding thumbnail
|
str: filepath to the corresponding thumbnail
|
||||||
|
"""
|
||||||
|
digest = hashlib.sha1(imgPath.encode('utf-8')).hexdigest()
|
||||||
|
path = os.path.join(ThumbnailCache.thumbnailDir, f'{digest}.jpg')
|
||||||
|
return path
|
||||||
|
|
||||||
|
@Slot(QUrl, result=QUrl)
|
||||||
|
def thumbnail(self, imgSource):
|
||||||
|
"""Retrieve the filepath of the thumbnail corresponding to a given image.
|
||||||
|
|
||||||
|
If the thumbnail does not exist on disk, it will be created asynchronously.
|
||||||
|
When this is done, the createdThumbnail signal is emitted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
imgSource (QUrl): location of the input image
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
QUrl: location of the corresponding thumbnail if it exists, otherwise None
|
||||||
"""
|
"""
|
||||||
# Safety check
|
|
||||||
if not imgSource.isValid():
|
if not imgSource.isValid():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
imgPath = imgSource.toLocalFile()
|
imgPath = imgSource.toLocalFile()
|
||||||
|
path = ThumbnailCache.thumbnailPath(imgPath)
|
||||||
# Use SHA1 hashing to associate a unique thumbnail to the image
|
|
||||||
digest = hashlib.sha1(imgPath.encode('utf-8')).hexdigest()
|
|
||||||
path = os.path.join(ThumbnailCache.thumbnailDir, f'{digest}.jpg')
|
|
||||||
source = QUrl.fromLocalFile(path)
|
source = QUrl.fromLocalFile(path)
|
||||||
|
|
||||||
# Check if thumbnail already exists
|
# Check if thumbnail already exists
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
# Update last modification time
|
# Update last modification time
|
||||||
pathlib.Path(path).touch(exist_ok=True)
|
Path(path).touch(exist_ok=True)
|
||||||
return source
|
return source
|
||||||
|
|
||||||
# Thumbnail does not exist, therefore we create it:
|
# Thumbnail does not exist
|
||||||
# 1. read the image
|
# create it in a worker thread to avoid UI freeze
|
||||||
# 2. scale it to thumbnail dimensions
|
ThumbnailCache.pool.apply_async(self.createThumbnail, args=(imgSource,))
|
||||||
# 3. write it in the cache
|
return None
|
||||||
|
|
||||||
|
def createThumbnail(self, imgSource):
|
||||||
|
"""Load an image, resize it to thumbnail dimensions and save the result in the cache directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
imgSource (QUrl): location of the input image
|
||||||
|
"""
|
||||||
|
imgPath = imgSource.toLocalFile()
|
||||||
|
path = ThumbnailCache.thumbnailPath(imgPath)
|
||||||
logging.debug(f'[ThumbnailCache] Creating thumbnail {path} for image {imgPath}')
|
logging.debug(f'[ThumbnailCache] Creating thumbnail {path} for image {imgPath}')
|
||||||
|
|
||||||
# Initialize image reader object
|
# Initialize image reader object
|
||||||
|
@ -89,10 +144,7 @@ class ThumbnailCache(QObject):
|
||||||
img = reader.read()
|
img = reader.read()
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
logging.error(f'[ThumbnailCache] Error when reading image: {reader.errorString()}')
|
logging.error(f'[ThumbnailCache] Error when reading image: {reader.errorString()}')
|
||||||
return None
|
return
|
||||||
|
|
||||||
# Make sure the thumbnail directory exists before writing into it
|
|
||||||
os.makedirs(ThumbnailCache.thumbnailDir, exist_ok=True)
|
|
||||||
|
|
||||||
# Scale image while preserving aspect ratio
|
# Scale image while preserving aspect ratio
|
||||||
thumbnail = img.scaled(ThumbnailCache.thumbnailSize, aspectMode=Qt.KeepAspectRatio)
|
thumbnail = img.scaled(ThumbnailCache.thumbnailSize, aspectMode=Qt.KeepAspectRatio)
|
||||||
|
@ -102,14 +154,13 @@ class ThumbnailCache(QObject):
|
||||||
success = writer.write(thumbnail)
|
success = writer.write(thumbnail)
|
||||||
if not success:
|
if not success:
|
||||||
logging.error(f'[ThumbnailCache] Error when writing thumbnail: {writer.errorString()}')
|
logging.error(f'[ThumbnailCache] Error when writing thumbnail: {writer.errorString()}')
|
||||||
return None
|
|
||||||
|
|
||||||
return source
|
# Notify listeners
|
||||||
|
self.thumbnailCreated.emit(imgSource)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean():
|
def clean():
|
||||||
"""
|
"""Scan the thumbnail directory and:
|
||||||
Scan the thumbnail directory and:
|
|
||||||
1. remove all thumbnails that have not been used for more than storageTimeLimit days
|
1. remove all thumbnails that have not been used for more than storageTimeLimit days
|
||||||
2. ensure that the number of thumbnails on disk does not exceed maxThumbnailsOnDisk.
|
2. ensure that the number of thumbnails on disk does not exceed maxThumbnailsOnDisk.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -31,6 +31,19 @@ Item {
|
||||||
property var metadata: metadataStr ? JSON.parse(viewpoint.get("metadata").value) : {}
|
property var metadata: metadataStr ? JSON.parse(viewpoint.get("metadata").value) : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSourceChanged: {
|
||||||
|
thumbnail.source = ThumbnailCache.thumbnail(root.source)
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ThumbnailCache
|
||||||
|
function onThumbnailCreated(imgSource) {
|
||||||
|
if (imgSource == root.source) {
|
||||||
|
thumbnail.source = ThumbnailCache.thumbnail(root.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
id: imageMA
|
id: imageMA
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
@ -77,9 +90,9 @@ Item {
|
||||||
border.color: isCurrentItem ? imageLabel.palette.highlight : Qt.darker(imageLabel.palette.highlight)
|
border.color: isCurrentItem ? imageLabel.palette.highlight : Qt.darker(imageLabel.palette.highlight)
|
||||||
border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
|
border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
|
||||||
Image {
|
Image {
|
||||||
|
id: thumbnail
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 4
|
anchors.margins: 4
|
||||||
source: ThumbnailCache.thumbnail(root.source)
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
autoTransform: true
|
autoTransform: true
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue