Meshroom/meshroom/ui/qml/ImageGallery/ImageGallery.qml
Candice Bentéjac 45bdeba343 [ui] Increase the "cacheBuffer" value of ImageGallery's GridView
The "cacheBuffer" property determines whether delegates are retained
outside the visible area of view. In the case of the ImageGallery,
it determines whether the images that are not currently visible in
the GridView (because we need to scroll up or down to be able to see
them) will remain in the cache or not. The default value is platform-
dependent (320 for Windows) and currently causes any image that is not
directly visible to be lost, even if it was previously loaded when it
appeared in the view: if we scroll up or down, we will necessarily need
to wait for the images to be loaded again.

10000 is an arbitrary value that seems to work correctly for most cases.
2022-12-22 16:09:52 +01:00

814 lines
32 KiB
QML

import QtQuick 2.14
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import QtQml.Models 2.2
import Qt.labs.qmlmodels 1.0
import Controls 1.0
import Utils 1.0
/**
* ImageGallery displays as a grid of Images a model containing Viewpoints objects.
* It manages a model of multiple CameraInit nodes as individual groups.
*/
Panel {
id: root
property variant cameraInits
property variant cameraInit
property int cameraInitIndex
property variant tempCameraInit
readonly property alias currentItem: grid.currentItem
readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : ""
readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined
readonly property int centerViewId: (_reconstruction && _reconstruction.sfmTransform) ? parseInt(_reconstruction.sfmTransform.attribute("transformation").value) : 0
property int defaultCellSize: 160
property bool readOnly: false
signal removeImageRequest(var attribute)
signal filesDropped(var drop, var augmentSfm)
title: "Image Gallery"
implicitWidth: (root.defaultCellSize + 2) * 2
QtObject {
id: m
property variant currentCameraInit: _reconstruction && _reconstruction.tempCameraInit ? _reconstruction.tempCameraInit : root.cameraInit
property variant viewpoints: currentCameraInit ? currentCameraInit.attribute('viewpoints').value : undefined
property variant intrinsics: currentCameraInit ? currentCameraInit.attribute('intrinsics').value : undefined
property bool readOnly: root.readOnly || displayHDR.checked
}
property variant parsedIntrinsic
property int numberOfIntrinsics : m.intrinsics ? m.intrinsics.count : 0
onNumberOfIntrinsicsChanged: {
parseIntr()
}
onCameraInitIndexChanged: {
parseIntr()
}
function changeCurrentIndex(newIndex) {
_reconstruction.cameraInitIndex = newIndex
}
function populate_model()
{
intrinsicModel.clear()
for (var intr in parsedIntrinsic) {
intrinsicModel.appendRow(parsedIntrinsic[intr])
}
}
function parseIntr(){
parsedIntrinsic = []
if(!m.intrinsics)
{
return
}
//Loop through all intrinsics
for(var i = 0; i < m.intrinsics.count; ++i){
var intrinsic = {}
//Loop through all attributes
for(var j=0; j < m.intrinsics.at(i).value.count; ++j){
var currentAttribute = m.intrinsics.at(i).value.at(j)
if(currentAttribute.type === "GroupAttribute"){
for(var k=0; k < currentAttribute.value.count; ++k){
intrinsic[currentAttribute.name + "." + currentAttribute.value.at(k).name] = currentAttribute.value.at(k)
}
}
else if(currentAttribute.type === "ListAttribute"){
// not needed for now
}
else{
intrinsic[currentAttribute.name] = currentAttribute
}
}
// Table Model needs to contain an entry for each column.
// In case of old file formats, some intrinsic keys that we display may not exist in the model.
// So, here we create an empty entry to enforce that the key exists in the model.
for(var n = 0; n < intrinsicModel.columnNames.length; ++n)
{
var name = intrinsicModel.columnNames[n]
if(!(name in intrinsic)) {
intrinsic[name] = {}
}
}
parsedIntrinsic[i] = intrinsic
}
populate_model()
}
headerBar: RowLayout {
SearchBar {
id: searchBar
width: 150
}
MaterialToolButton {
text: MaterialIcons.more_vert
font.pointSize: 11
padding: 2
checkable: true
checked: galleryMenu.visible
onClicked: galleryMenu.open()
Menu {
id: galleryMenu
y: parent.height
x: -width + parent.width
MenuItem {
text: "Edit Sensor Database..."
onTriggered: {
sensorDBDialog.open()
}
}
Menu {
title: "Advanced"
Action {
id: displayViewIdsAction
text: "Display View IDs"
checkable: true
}
}
}
}
}
SensorDBDialog {
id: sensorDBDialog
sensorDatabase: cameraInit ? Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value) : ""
readOnly: _reconstruction ? _reconstruction.computing : false
onUpdateIntrinsicsRequest: _reconstruction.rebuildIntrinsics(cameraInit)
}
ColumnLayout {
anchors.fill: parent
spacing: 4
GridView {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true
cacheBuffer: 10000 // Magic number that seems to work well, even with lots of images
visible: !intrinsicsFilterButton.checked
ScrollBar.vertical: ScrollBar {
minimumSize: 0.05
active : !intrinsicsFilterButton.checked
visible: !intrinsicsFilterButton.checked
}
focus: true
clip: true
cellWidth: thumbnailSizeSlider.value
cellHeight: cellWidth
highlightFollowsCurrentItem: true
keyNavigationEnabled: true
property bool updateSelectedViewFromGrid: true
// Update grid current item when selected view changes
Connections {
target: _reconstruction
onSelectedViewIdChanged: {
if (_reconstruction.selectedViewId > -1) {
grid.updateCurrentIndexFromSelectionViewId()
}
}
}
function makeCurrentItemVisible()
{
grid.positionViewAtIndex(grid.currentIndex, GridView.Visible)
}
function updateCurrentIndexFromSelectionViewId()
{
var idx = grid.model.find(_reconstruction.selectedViewId, "viewId")
if (idx >= 0 && grid.currentIndex != idx) {
grid.currentIndex = idx
}
}
onCurrentItemChanged: {
if (grid.updateSelectedViewFromGrid && grid.currentItem) {
_reconstruction.selectedViewId = grid.currentItem.viewpoint.get("viewId").value
}
}
model: SortFilterDelegateModel {
id: sortedModel
model: m.viewpoints
sortRole: "path.basename"
filters: displayViewIdsAction.checked ? filtersWithViewIds : filtersBasic
property var filtersBasic: [
{role: "path", value: searchBar.text},
{role: "viewId.isReconstructed", value: reconstructionFilter}
]
property var filtersWithViewIds: [
[
{role: "path", value: searchBar.text},
{role: "viewId.asString", value: searchBar.text}
],
{role: "viewId.isReconstructed", value: reconstructionFilter}
]
property var reconstructionFilter: undefined
// override modelData to return basename of viewpoint's path for sorting
function modelData(item, roleName_) {
var roleNameAndCmd = roleName_.split(".");
var roleName = roleName_;
var cmd = "";
if(roleNameAndCmd.length >= 2)
{
roleName = roleNameAndCmd[0];
cmd = roleNameAndCmd[1];
}
if(cmd == "isReconstructed")
return _reconstruction.isReconstructed(item.model.object);
var value = item.model.object.childAttribute(roleName).value;
if(cmd == "basename")
return Filepath.basename(value);
if (cmd == "asString")
return value.toString();
return value
}
delegate: ImageDelegate {
id: imageDelegate
viewpoint: object.value
width: grid.cellWidth
height: grid.cellHeight
readOnly: m.readOnly
displayViewId: displayViewIdsAction.checked
visible: !intrinsicsFilterButton.checked
isCurrentItem: GridView.isCurrentItem
onPressed: {
grid.currentIndex = DelegateModel.filteredIndex
}
function sendRemoveRequest()
{
if(!readOnly)
removeImageRequest(object)
}
onRemoveRequest: sendRemoveRequest()
Keys.onDeletePressed: sendRemoveRequest()
RowLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 2
spacing: 2
property bool valid: Qt.isQtObject(object) // object can be evaluated to null at some point during creation/deletion
property bool inViews: valid && _reconstruction && _reconstruction.sfmReport && _reconstruction.isInViews(object)
// Camera Initialization indicator
IntrinsicsIndicator {
intrinsic: parent.valid && _reconstruction ? _reconstruction.getIntrinsic(object) : null
metadata: imageDelegate.metadata
}
// Rig indicator
Loader {
id: rigIndicator
property int rigId: parent.valid ? object.childAttribute("rigId").value : -1
active: rigId >= 0
sourceComponent: ImageBadge {
property int rigSubPoseId: model.object.childAttribute("subPoseId").value
text: MaterialIcons.link
ToolTip.text: "<b>Rig: Initialized</b><br>" +
"Rig ID: " + rigIndicator.rigId + " <br>" +
"SubPose: " + rigSubPoseId
}
}
// Center of SfMTransform
Loader {
id: sfmTransformIndicator
active: viewpoint && (viewpoint.get("viewId").value == centerViewId)
sourceComponent: ImageBadge {
text: MaterialIcons.gamepad
ToolTip.text: "Camera used to define the center of the scene."
}
}
Item { Layout.fillWidth: true }
// Reconstruction status indicator
Loader {
active: parent.inViews
visible: active
sourceComponent: ImageBadge {
property bool reconstructed: _reconstruction.sfmReport && _reconstruction.isReconstructed(model.object)
text: reconstructed ? MaterialIcons.videocam : MaterialIcons.videocam_off
color: reconstructed ? Colors.green : Colors.red
ToolTip.text: "<b>Camera: " + (reconstructed ? "" : "Not ") + "Reconstructed</b>"
}
}
}
}
}
// Keyboard shortcut to change current image group
Keys.priority: Keys.BeforeItem
Keys.onPressed: {
if(event.modifiers & Qt.AltModifier)
{
if(event.key == Qt.Key_Right)
{
_reconstruction.cameraInitIndex = Math.min(root.cameraInits.count - 1, root.cameraInitIndex + 1)
event.accepted = true
}
else if(event.key == Qt.Key_Left)
{
_reconstruction.cameraInitIndex = Math.max(0, root.cameraInitIndex - 1)
event.accepted = true
}
}
else
{
if(event.key == Qt.Key_Right)
{
grid.moveCurrentIndexRight()
event.accepted = true
}
else if(event.key == Qt.Key_Left)
{
grid.moveCurrentIndexLeft()
event.accepted = true
}
else if(event.key == Qt.Key_Up)
{
grid.moveCurrentIndexUp()
event.accepted = true
}
else if(event.key == Qt.Key_Down)
{
grid.moveCurrentIndexDown()
event.accepted = true
}
else if (event.key == Qt.Key_Tab)
{
searchBar.forceActiveFocus()
event.accepted = true
}
}
}
// Explanatory placeholder when no image has been added yet
Column {
id: dropImagePlaceholder
anchors.centerIn: parent
visible: (m.viewpoints ? m.viewpoints.count == 0 : true) && !intrinsicsFilterButton.checked
spacing: 4
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: MaterialIcons.photo_library
font.pointSize: 24
font.family: MaterialIcons.fontFamily
}
Label {
text: "Drop Image Files / Folders"
}
}
// Placeholder when the filtered images list is empty
Column {
id: noImageImagePlaceholder
anchors.centerIn: parent
visible: (m.viewpoints ? m.viewpoints.count != 0 : false) && !dropImagePlaceholder.visible && grid.model.count == 0 && !intrinsicsFilterButton.checked
spacing: 4
Label {
anchors.horizontalCenter: parent.horizontalCenter
text: MaterialIcons.filter_none
font.pointSize: 24
font.family: MaterialIcons.fontFamily
}
Label {
text: "No images in this filtered view"
}
}
DropArea {
id: dropArea
anchors.fill: parent
enabled: !m.readOnly && !intrinsicsFilterButton.checked
keys: ["text/uri-list"]
// TODO: onEntered: call specific method to filter files based on extension
onDropped: {
var augmentSfm = augmentArea.hovered
root.filesDropped(drop, augmentSfm)
}
// Background opacifier
Rectangle {
visible: dropArea.containsDrag
anchors.fill: parent
color: root.palette.window
opacity: 0.8
}
ColumnLayout {
anchors.fill: parent
visible: dropArea.containsDrag
spacing: 1
Label {
id: addArea
property bool hovered: dropArea.drag.y < height
Layout.fillWidth: true
Layout.fillHeight: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "Add Images"
font.bold: true
background: Rectangle {
color: parent.hovered ? parent.palette.highlight : parent.palette.window
opacity: 0.8
border.color: parent.palette.highlight
}
}
// DropArea overlay
Label {
id: augmentArea
property bool hovered: visible && dropArea.drag.y >= y
Layout.fillWidth: true
Layout.preferredHeight: parent.height * 0.3
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: "Augment Reconstruction"
font.bold: true
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
visible: m.viewpoints ? m.viewpoints.count > 0 : false
background: Rectangle {
color: parent.hovered ? palette.highlight : palette.window
opacity: 0.8
border.color: parent.palette.highlight
}
}
}
}
MouseArea {
anchors.fill: parent
onPressed: {
if(mouse.button == Qt.LeftButton)
grid.forceActiveFocus()
mouse.accepted = false
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: intrinsicsFilterButton.checked
clip: true
TableView {
id : intrinsicTable
visible: intrinsicsFilterButton.checked
anchors.fill: parent
boundsMovement : Flickable.StopAtBounds
//Provide width for column
//Note no size provided for the last column (bool comp) so it uses its automated size
columnWidthProvider: function (column) { return intrinsicModel.columnWidths[column] }
model: intrinsicModel
delegate: IntrinsicDisplayDelegate{}
ScrollBar.horizontal: ScrollBar { id: sb }
ScrollBar.vertical : ScrollBar { id: sbv }
}
TableModel {
id : intrinsicModel
// Hardcoded default width per column
property var columnWidths: [105, 75, 75, 75, 125, 60, 60, 45, 45, 200, 60, 60]
property var columnNames: [
"intrinsicId",
"initialFocalLength",
"focalLength",
"type",
"width",
"height",
"sensorWidth",
"sensorHeight",
"serialNumber",
"principalPoint.x",
"principalPoint.y",
"locked"]
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[0]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[1]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[2]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[3]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[4]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[5]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[6]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[7]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[8]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[9]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[10]]} }
TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[11]]} }
//https://doc.qt.io/qt-5/qml-qt-labs-qmlmodels-tablemodel.html#appendRow-method
}
//CODE FOR HEADERS
//UNCOMMENT WHEN COMPATIBLE WITH THE RIGHT QT VERSION
// HorizontalHeaderView {
// id: horizontalHeader
// syncView: tableView
// anchors.left: tableView.left
// }
}
RowLayout {
Layout.fillHeight: false
visible: root.cameraInits ? root.cameraInits.count > 1 : false
Layout.alignment: Qt.AlignHCenter
spacing: 2
ToolButton {
text: MaterialIcons.navigate_before
font.family: MaterialIcons.fontFamily
ToolTip.text: "Previous Group (Alt+Left)"
ToolTip.visible: hovered
enabled: nodesCB.currentIndex > 0
onClicked: nodesCB.decrementCurrentIndex()
}
Label { id: groupLabel; text: "Group " }
ComboBox {
id: nodesCB
model: {
// Create an array from 1 to cameraInits.count for the
// display of group indices (real indices still are from
// 0 to cameraInits.count - 1)
var l = [];
if (root.cameraInits) {
for (var i = 1; i <= root.cameraInits.count; i++) {
l.push(i);
}
}
return l;
}
implicitWidth: 40
currentIndex: root.cameraInitIndex
onActivated: root.changeCurrentIndex(currentIndex)
}
Label { text: "/ " + (root.cameraInits ? root.cameraInits.count : "Unknown") }
ToolButton {
text: MaterialIcons.navigate_next
font.family: MaterialIcons.fontFamily
ToolTip.text: "Next Group (Alt+Right)"
ToolTip.visible: hovered
enabled: root.cameraInits ? nodesCB.currentIndex < root.cameraInits.count - 1 : false
onClicked: nodesCB.incrementCurrentIndex()
}
}
}
footerContent: RowLayout {
// Images count
id: footer
function resetButtons(){
inputImagesFilterButton.checked = false
estimatedCamerasFilterButton.checked = false
nonEstimatedCamerasFilterButton.checked = false
}
MaterialToolLabelButton {
id : inputImagesFilterButton
Layout.minimumWidth: childrenRect.width
ToolTip.text: grid.model.count + " Input Images"
iconText: MaterialIcons.image
label: (m.viewpoints ? m.viewpoints.count : 0)
padding: 3
checkable: true
checked: true
onCheckedChanged:{
if(checked) {
sortedModel.reconstructionFilter = undefined;
estimatedCamerasFilterButton.checked = false;
nonEstimatedCamerasFilterButton.checked = false;
intrinsicsFilterButton.checked = false;
}
}
}
// Estimated cameras count
MaterialToolLabelButton {
id : estimatedCamerasFilterButton
Layout.minimumWidth: childrenRect.width
ToolTip.text: label + " Estimated Cameras"
iconText: MaterialIcons.videocam
label: _reconstruction && _reconstruction.nbCameras ? _reconstruction.nbCameras.toString() : "-"
padding: 3
enabled: _reconstruction ? _reconstruction.cameraInit && _reconstruction.nbCameras : false
checkable: true
checked: false
onCheckedChanged:{
if(checked) {
sortedModel.reconstructionFilter = true;
inputImagesFilterButton.checked = false;
nonEstimatedCamerasFilterButton.checked = false;
intrinsicsFilterButton.checked = false;
}
}
onEnabledChanged:{
if(!enabled) {
if(checked) inputImagesFilterButton.checked = true;
checked = false
}
}
}
// Non estimated cameras count
MaterialToolLabelButton {
id : nonEstimatedCamerasFilterButton
Layout.minimumWidth: childrenRect.width
ToolTip.text: label + " Non Estimated Cameras"
iconText: MaterialIcons.videocam_off
label: _reconstruction && _reconstruction.nbCameras ? ((m.viewpoints ? m.viewpoints.count : 0) - _reconstruction.nbCameras.toString()).toString() : "-"
padding: 3
enabled: _reconstruction ? _reconstruction.cameraInit && _reconstruction.nbCameras : false
checkable: true
checked: false
onCheckedChanged:{
if(checked) {
sortedModel.reconstructionFilter = false;
inputImagesFilterButton.checked = false;
estimatedCamerasFilterButton.checked = false;
intrinsicsFilterButton.checked = false;
}
}
onEnabledChanged:{
if(!enabled) {
if(checked) inputImagesFilterButton.checked = true;
checked = false
}
}
}
MaterialToolLabelButton {
id : intrinsicsFilterButton
Layout.minimumWidth: childrenRect.width
ToolTip.text: label + " Number of intrinsics"
iconText: MaterialIcons.camera
label: _reconstruction ? (m.intrinsics ? m.intrinsics.count : 0) : "0"
padding: 3
enabled: m.intrinsics ? m.intrinsics.count > 0 : false
checkable: true
checked: false
onCheckedChanged:{
if(checked) {
inputImagesFilterButton.checked = false
estimatedCamerasFilterButton.checked = false
nonEstimatedCamerasFilterButton.checked = false
}
}
onEnabledChanged:{
if(!enabled) {
if(checked) inputImagesFilterButton.checked = true;
checked = false
}
}
}
Item { Layout.fillHeight: true; Layout.fillWidth: true }
MaterialToolLabelButton {
id: displayHDR
Layout.minimumWidth: childrenRect.width
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("LdrToHdrMerge").node : null
ToolTip.text: "Visualize HDR images: " + (activeNode ? activeNode.label : "No Node")
iconText: MaterialIcons.filter
label: activeNode ? activeNode.attribute("nbBrackets").value : ""
visible: activeNode
enabled: activeNode && activeNode.isComputed
property string nodeID: activeNode ? (activeNode.label + activeNode.isComputed) : ""
onNodeIDChanged: {
if(checked) {
open();
}
}
onEnabledChanged: {
// Reset the toggle to avoid getting stuck
// with the HDR node checked but disabled.
if(checked) {
checked = false;
close();
}
}
checkable: true
checked: false
onClicked: {
if(checked) {
open();
} else {
close();
}
}
function open() {
if(imageProcessing.checked)
imageProcessing.checked = false;
_reconstruction.setupTempCameraInit(activeNode, "outSfMData");
}
function close() {
_reconstruction.clearTempCameraInit();
}
}
MaterialToolButton {
id: imageProcessing
Layout.minimumWidth: childrenRect.width
property var activeNode: _reconstruction ? _reconstruction.activeNodes.get("ImageProcessing").node : null
font.pointSize: 15
padding: 0
ToolTip.text: "Preprocessed Images: " + (activeNode ? activeNode.label : "No Node")
text: MaterialIcons.wallpaper
visible: activeNode && activeNode.attribute("outSfMData").value
enabled: activeNode && activeNode.isComputed
property string nodeID: activeNode ? (activeNode.label + activeNode.isComputed) : ""
onNodeIDChanged: {
if(checked) {
open();
}
}
onEnabledChanged: {
// Reset the toggle to avoid getting stuck
// with the HDR node checked but disabled.
if(checked) {
checked = false;
close();
}
}
checkable: true
checked: false
onClicked: {
if(checked) {
open();
} else {
close();
}
}
function open() {
if(displayHDR.checked)
displayHDR.checked = false;
_reconstruction.setupTempCameraInit(activeNode, "outSfMData");
}
function close() {
_reconstruction.clearTempCameraInit();
}
}
Item { Layout.fillHeight: true; width: 1 }
// Thumbnail size icon and slider
MaterialToolButton {
Layout.minimumWidth: childrenRect.width
text: MaterialIcons.photo_size_select_large
ToolTip.text: "Thumbnails Scale"
padding: 0
anchors.margins: 0
font.pointSize: 11
onClicked: { thumbnailSizeSlider.value = defaultCellSize; }
}
Slider {
id: thumbnailSizeSlider
from: 70
value: defaultCellSize
to: 250
implicitWidth: 70
}
}
}