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__)
|
||||
self.setWindowIcon(QIcon(os.path.join(pwd, "img/meshroom.svg")))
|
||||
|
||||
# User specified thumbnail directory
|
||||
thumbnailDir = os.getenv('MESHROOM_THUMBNAIL_DIR')
|
||||
if thumbnailDir is not None:
|
||||
ThumbnailCache.thumbnailDir = thumbnailDir
|
||||
|
||||
# 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()
|
||||
# Initialize thumbnail cache:
|
||||
# - read related environment variables
|
||||
# - clean cache directory and make sure it exists on disk
|
||||
ThumbnailCache.initialize()
|
||||
|
||||
# QML engine setup
|
||||
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.QtGui import QImageReader, QImageWriter
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import time
|
||||
import logging
|
||||
from multiprocessing.pool import ThreadPool
|
||||
|
||||
|
||||
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)
|
||||
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:
|
||||
~/Meshroom/thumbnails.
|
||||
|
@ -34,7 +38,7 @@ class ThumbnailCache(QObject):
|
|||
"""
|
||||
|
||||
# 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)
|
||||
thumbnailSize = QSize(100, 100)
|
||||
|
@ -45,39 +49,90 @@ class ThumbnailCache(QObject):
|
|||
# Maximum number of thumbnails in the cache directory
|
||||
maxThumbnailsOnDisk = 100000
|
||||
|
||||
@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 is created.
|
||||
# Signal to notify listeners that a thumbnail was created and written on disk
|
||||
# This signal has one argument: the url of the image that the thumbnail is associated to
|
||||
thumbnailCreated = Signal(QUrl)
|
||||
|
||||
# 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:
|
||||
imgSource (QUrl): the filepath to the input image
|
||||
imgPath (str): filepath to the input image
|
||||
|
||||
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():
|
||||
return None
|
||||
|
||||
imgPath = imgSource.toLocalFile()
|
||||
|
||||
# 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')
|
||||
path = ThumbnailCache.thumbnailPath(imgPath)
|
||||
source = QUrl.fromLocalFile(path)
|
||||
|
||||
# Check if thumbnail already exists
|
||||
if os.path.exists(path):
|
||||
# Update last modification time
|
||||
pathlib.Path(path).touch(exist_ok=True)
|
||||
Path(path).touch(exist_ok=True)
|
||||
return source
|
||||
|
||||
# Thumbnail does not exist, therefore we create it:
|
||||
# 1. read the image
|
||||
# 2. scale it to thumbnail dimensions
|
||||
# 3. write it in the cache
|
||||
# Thumbnail does not exist
|
||||
# create it in a worker thread to avoid UI freeze
|
||||
ThumbnailCache.pool.apply_async(self.createThumbnail, args=(imgSource,))
|
||||
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}')
|
||||
|
||||
# Initialize image reader object
|
||||
|
@ -89,10 +144,7 @@ class ThumbnailCache(QObject):
|
|||
img = reader.read()
|
||||
if img.isNull():
|
||||
logging.error(f'[ThumbnailCache] Error when reading image: {reader.errorString()}')
|
||||
return None
|
||||
|
||||
# Make sure the thumbnail directory exists before writing into it
|
||||
os.makedirs(ThumbnailCache.thumbnailDir, exist_ok=True)
|
||||
return
|
||||
|
||||
# Scale image while preserving aspect ratio
|
||||
thumbnail = img.scaled(ThumbnailCache.thumbnailSize, aspectMode=Qt.KeepAspectRatio)
|
||||
|
@ -102,14 +154,13 @@ class ThumbnailCache(QObject):
|
|||
success = writer.write(thumbnail)
|
||||
if not success:
|
||||
logging.error(f'[ThumbnailCache] Error when writing thumbnail: {writer.errorString()}')
|
||||
return None
|
||||
|
||||
return source
|
||||
# Notify listeners
|
||||
self.thumbnailCreated.emit(imgSource)
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
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) : {}
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
thumbnail.source = ThumbnailCache.thumbnail(root.source)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ThumbnailCache
|
||||
function onThumbnailCreated(imgSource) {
|
||||
if (imgSource == root.source) {
|
||||
thumbnail.source = ThumbnailCache.thumbnail(root.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: imageMA
|
||||
anchors.fill: parent
|
||||
|
@ -77,9 +90,9 @@ Item {
|
|||
border.color: isCurrentItem ? imageLabel.palette.highlight : Qt.darker(imageLabel.palette.highlight)
|
||||
border.width: imageMA.containsMouse || root.isCurrentItem ? 2 : 0
|
||||
Image {
|
||||
id: thumbnail
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: ThumbnailCache.thumbnail(root.source)
|
||||
asynchronous: true
|
||||
autoTransform: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue