diff --git a/README.md b/README.md index 5435efe30a..e4bce2fa60 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,25 @@ Presentation of the Meshroom software with a focus on using it for Match Moving. The project is released under MPLv2, see [**COPYING.md**](COPYING.md). +## Citation + +If you use this project for research, please cite: + ``` + @inproceedings{alicevision2021, + title={{A}liceVision {M}eshroom: An open-source {3D} reconstruction pipeline}, + authors={Carsten Griwodz, Simone Gasparini, Lilian Calvet, Pierre Gurdjos, Fabien Castan, Benoit Maujean, Gregoire De Lillo, Yann Lanthony}, + booktitle={Proc. 12th ACM Multimed. Syst. Conf. - MMSys '21}, + doi = {10.1145/3458305.3478443} + publisher = {ACM Press}, + year = {2021} + } + ``` + ## Get the project -See [**INSTALL.md**](INSTALL.md) to setup the project and pre-requisites. +You can [download pre-compiled binaries for the latest release](https://github.com/alicevision/meshroom/releases). + +If you want to build it yourself, see [**INSTALL.md**](INSTALL.md) to setup the project and pre-requisites. Get the source code and install runtime requirements: ```bash @@ -93,6 +109,22 @@ You may need to adjust the folder `/usr/lib/nvidia-340` with the correct driver python bin/meshroom_batch --input INPUT_IMAGES_FOLDER --output OUTPUT_FOLDER ``` +## Start Meshroom without building AliceVision + +To use Meshroom (ui) without building AliceVision +* Download a [release](https://github.com/alicevision/meshroom/releases) +* Checkout corresponding Meshroom (ui) version/tag to avoid versions incompatibilities +* `LD_LIBRARY_PATH=~/foo/Meshroom-2021.1.0/aliceVision/lib/ PATH=$PATH:~/foo/Meshroom-2021.1.0/aliceVision/bin/ PYTHONPATH=$PWD python3 meshroom/ui` + +## Start and Debug Meshroom in an IDE + +PyCharm Community is free IDE which can be used. To start and debug a project with that IDE, +right-click on `Meshroom/ui/__main__.py` > `Debug`, then `Edit Configuration`, in `Environment variables` : +* If you want to use aliceVision built by yourself add: `PATH=$PATH:/foo/build/Linux-x86_64/` +* If you want to use aliceVision release add: `LD_LIBRARY_PATH=/foo/Meshroom-2021.1.0/aliceVision/lib/;PATH=$PATH:/foo/Meshroom-2021.1.0/aliceVision/bin/` (Make sure that you are on the branch matching the right version) + +![image](https://user-images.githubusercontent.com/937836/127321375-3bf78e73-569d-414a-8649-de0307adf794.png) + ## FAQ diff --git a/meshroom/core/attribute.py b/meshroom/core/attribute.py index be1aec8255..81134918e1 100644 --- a/meshroom/core/attribute.py +++ b/meshroom/core/attribute.py @@ -71,24 +71,29 @@ def node(self): def root(self): return self._root() if self._root else None - def absoluteName(self): - return '{}.{}.{}'.format(self.node.graph.name, self.node.name, self._name) + def getName(self): + """ Attribute name """ + return self._name def getFullName(self): - """ Name inside the Graph: nodeName.name """ + """ Name inside the Graph: groupName.name """ if isinstance(self.root, ListAttribute): return '{}[{}]'.format(self.root.getFullName(), self.root.index(self)) elif isinstance(self.root, GroupAttribute): - return '{}.{}'.format(self.root.getFullName(), self._name) - return '{}.{}'.format(self.node.name, self._name) + return '{}.{}'.format(self.root.getFullName(), self.getName()) + return self.getName() + + def getFullNameToNode(self): + """ Name inside the Graph: nodeName.groupName.name """ + return '{}.{}'.format(self.node.name, self.getFullName()) + + def getFullNameToGraph(self): + """ Name inside the Graph: graphName.nodeName.groupName.name """ + return '{}.{}'.format(self.node.graph.name, self.getFullNameToNode()) def asLinkExpr(self): """ Return link expression for this Attribute """ - return "{" + self.getFullName() + "}" - - def getName(self): - """ Attribute name """ - return self._name + return "{" + self.getFullNameToNode() + "}" def getType(self): return self.attributeDesc.__class__.__name__ @@ -102,6 +107,22 @@ def getBaseType(self): def getLabel(self): return self._label + def getFullLabel(self): + """ Full Label includes the name of all parent groups, e.g. 'groupLabel subGroupLabel Label' """ + if isinstance(self.root, ListAttribute): + return self.root.getFullLabel() + elif isinstance(self.root, GroupAttribute): + return '{} {}'.format(self.root.getFullLabel(), self.getLabel()) + return self.getLabel() + + def getFullLabelToNode(self): + """ Label inside the Graph: nodeLabel groupLabel Label """ + return '{} {}'.format(self.node.label, self.getFullLabel()) + + def getFullLabelToGraph(self): + """ Label inside the Graph: graphName nodeLabel groupLabel Label """ + return '{} {}'.format(self.node.graph.name, self.getFullLabelToNode()) + def getEnabled(self): if isinstance(self.desc.enabled, types.FunctionType): try: @@ -265,7 +286,12 @@ def updateInternals(self): name = Property(str, getName, constant=True) fullName = Property(str, getFullName, constant=True) + fullNameToNode = Property(str, getFullNameToNode, constant=True) + fullNameToGraph = Property(str, getFullNameToGraph, constant=True) label = Property(str, getLabel, constant=True) + fullLabel = Property(str, getFullLabel, constant=True) + fullLabelToNode = Property(str, getFullLabelToNode, constant=True) + fullLabelToGraph = Property(str, getFullLabelToGraph, constant=True) type = Property(str, getType, constant=True) baseType = Property(str, getType, constant=True) isReadOnly = Property(bool, _isReadOnly, constant=True) diff --git a/meshroom/core/graph.py b/meshroom/core/graph.py index bf4f053ef2..edfc755feb 100644 --- a/meshroom/core/graph.py +++ b/meshroom/core/graph.py @@ -415,7 +415,7 @@ def nodeOutEdges(self, node): def removeNode(self, nodeName): """ Remove the node identified by 'nodeName' from the graph - and return in and out edges removed by this operation in two dicts {dstAttr.getFullName(), srcAttr.getFullName()} + and return in and out edges removed by this operation in two dicts {dstAttr.getFullNameToNode(), srcAttr.getFullNameToNode()} """ node = self.node(nodeName) inEdges = {} @@ -425,10 +425,10 @@ def removeNode(self, nodeName): with GraphModification(self): for edge in self.nodeOutEdges(node): self.removeEdge(edge.dst) - outEdges[edge.dst.getFullName()] = edge.src.getFullName() + outEdges[edge.dst.getFullNameToNode()] = edge.src.getFullNameToNode() for edge in self.nodeInEdges(node): self.removeEdge(edge.dst) - inEdges[edge.dst.getFullName()] = edge.src.getFullName() + inEdges[edge.dst.getFullNameToNode()] = edge.src.getFullNameToNode() node.alive = False self._nodes.remove(node) @@ -583,7 +583,7 @@ def addEdge(self, srcAttr, dstAttr): if srcAttr.node.graph != self or dstAttr.node.graph != self: raise RuntimeError('The attributes of the edge should be part of a common graph.') if dstAttr in self.edges.keys(): - raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.getFullName())) + raise RuntimeError('Destination attribute "{}" is already connected.'.format(dstAttr.getFullNameToNode())) edge = Edge(srcAttr, dstAttr) self.edges.add(edge) self.markNodesDirty(dstAttr.node) @@ -600,7 +600,7 @@ def addEdges(self, *edges): @changeTopology def removeEdge(self, dstAttr): if dstAttr not in self.edges.keys(): - raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.getFullName())) + raise RuntimeError('Attribute "{}" is not connected'.format(dstAttr.getFullNameToNode())) edge = self.edges.pop(dstAttr) self.markNodesDirty(dstAttr.node) dstAttr.valueChanged.emit() @@ -1202,4 +1202,3 @@ def loadGraph(filepath): graph.load(filepath) graph.update() return graph - diff --git a/meshroom/ui/commands.py b/meshroom/ui/commands.py index f0a2324d3c..4ae6a4f376 100755 --- a/meshroom/ui/commands.py +++ b/meshroom/ui/commands.py @@ -197,10 +197,10 @@ def undoImpl(self): class SetAttributeCommand(GraphCommand): def __init__(self, graph, attribute, value, parent=None): super(SetAttributeCommand, self).__init__(graph, parent) - self.attrName = attribute.getFullName() + self.attrName = attribute.getFullNameToNode() self.value = value self.oldValue = attribute.getExportValue() - self.setText("Set Attribute '{}'".format(attribute.getFullName())) + self.setText("Set Attribute '{}'".format(attribute.getFullNameToNode())) def redoImpl(self): if self.value == self.oldValue: @@ -215,8 +215,8 @@ def undoImpl(self): class AddEdgeCommand(GraphCommand): def __init__(self, graph, src, dst, parent=None): super(AddEdgeCommand, self).__init__(graph, parent) - self.srcAttr = src.getFullName() - self.dstAttr = dst.getFullName() + self.srcAttr = src.getFullNameToNode() + self.dstAttr = dst.getFullNameToNode() self.setText("Connect '{}'->'{}'".format(self.srcAttr, self.dstAttr)) if src.baseType != dst.baseType: @@ -233,8 +233,8 @@ def undoImpl(self): class RemoveEdgeCommand(GraphCommand): def __init__(self, graph, edge, parent=None): super(RemoveEdgeCommand, self).__init__(graph, parent) - self.srcAttr = edge.src.getFullName() - self.dstAttr = edge.dst.getFullName() + self.srcAttr = edge.src.getFullNameToNode() + self.dstAttr = edge.dst.getFullNameToNode() self.setText("Disconnect '{}'->'{}'".format(self.srcAttr, self.dstAttr)) def redoImpl(self): @@ -250,7 +250,7 @@ class ListAttributeAppendCommand(GraphCommand): def __init__(self, graph, listAttribute, value, parent=None): super(ListAttributeAppendCommand, self).__init__(graph, parent) assert isinstance(listAttribute, ListAttribute) - self.attrName = listAttribute.getFullName() + self.attrName = listAttribute.getFullNameToNode() self.index = None self.count = 1 self.value = value if value else None @@ -276,10 +276,10 @@ def __init__(self, graph, attribute, parent=None): super(ListAttributeRemoveCommand, self).__init__(graph, parent) listAttribute = attribute.root assert isinstance(listAttribute, ListAttribute) - self.listAttrName = listAttribute.getFullName() + self.listAttrName = listAttribute.getFullNameToNode() self.index = listAttribute.index(attribute) self.value = attribute.getExportValue() - self.setText("Remove {}".format(attribute.getFullName())) + self.setText("Remove {}".format(attribute.getFullNameToNode())) def redoImpl(self): listAttribute = self.graph.attribute(self.listAttrName) diff --git a/meshroom/ui/graph.py b/meshroom/ui/graph.py index 31e995a90c..871125802d 100644 --- a/meshroom/ui/graph.py +++ b/meshroom/ui/graph.py @@ -618,14 +618,14 @@ def clearDataFrom(self, nodes): @Slot(Attribute, Attribute) def addEdge(self, src, dst): if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute): - with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullName())): + with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullNameToNode())): self.appendAttribute(dst) self._addEdge(src, dst.at(-1)) else: self._addEdge(src, dst) def _addEdge(self, src, dst): - with self.groupedGraphModification("Connect '{}'->'{}'".format(src.getFullName(), dst.getFullName())): + with self.groupedGraphModification("Connect '{}'->'{}'".format(src.getFullNameToNode(), dst.getFullNameToNode())): if dst in self._graph.edges.keys(): self.removeEdge(self._graph.edge(dst)) self.push(commands.AddEdgeCommand(self._graph, src, dst)) @@ -633,7 +633,7 @@ def _addEdge(self, src, dst): @Slot(Edge) def removeEdge(self, edge): if isinstance(edge.dst.root, ListAttribute): - with self.groupedGraphModification("Remove Edge and Delete {}".format(edge.dst.getFullName())): + with self.groupedGraphModification("Remove Edge and Delete {}".format(edge.dst.getFullNameToNode())): self.push(commands.RemoveEdgeCommand(self._graph, edge)) self.removeAttribute(edge.dst) else: diff --git a/meshroom/ui/qml/ImageGallery/ImageGallery.qml b/meshroom/ui/qml/ImageGallery/ImageGallery.qml index 1658db1fe2..cedbd8230e 100644 --- a/meshroom/ui/qml/ImageGallery/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery/ImageGallery.qml @@ -1,8 +1,9 @@ -import QtQuick 2.7 +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 @@ -16,6 +17,7 @@ Panel { 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 : "" @@ -23,7 +25,6 @@ Panel { readonly property int centerViewId: (_reconstruction && _reconstruction.sfmTransform) ? parseInt(_reconstruction.sfmTransform.attribute("transformation").value) : 0 property int defaultCellSize: 160 - property int currentIndex: 0 property bool readOnly: false signal removeImageRequest(var attribute) @@ -32,17 +33,70 @@ Panel { title: "Images" implicitWidth: (root.defaultCellSize + 2) * 2 - function changeCurrentIndex(newIndex) { - _reconstruction.cameraInitIndex = newIndex - } - QtObject { id: m property variant currentCameraInit: _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() + } + + 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 { MaterialToolButton { text: MaterialIcons.more_vert @@ -91,7 +145,13 @@ Panel { Layout.fillWidth: true Layout.fillHeight: true - ScrollBar.vertical: ScrollBar { minimumSize: 0.05 } + visible: !intrinsicsFilterButton.checked + + ScrollBar.vertical: ScrollBar { + minimumSize: 0.05 + active : !intrinsicsFilterButton.checked + visible: !intrinsicsFilterButton.checked + } focus: true clip: true @@ -99,35 +159,72 @@ Panel { cellHeight: cellWidth highlightFollowsCurrentItem: true keyNavigationEnabled: true + property bool updateSelectedViewFromGrid: true // Update grid current item when selected view changes Connections { target: _reconstruction onSelectedViewIdChanged: { - var idx = grid.model.find(_reconstruction.selectedViewId, "viewId") - if(idx >= 0) - grid.currentIndex = idx + 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 + } + } + onCurrentIndexChanged: { + if(grid.updateSelectedViewFromGrid) { + _reconstruction.selectedViewId = grid.currentItem.viewpoint.get("viewId").value } } model: SortFilterDelegateModel { id: sortedModel model: m.viewpoints - sortRole: "path" - // TODO: provide filtering on reconstruction status - // filterRole: _reconstruction.sfmReport ? "reconstructed" : "" - // filterValue: true / false - // in modelData: - // if(filterRole == roleName) - // return _reconstruction.isReconstructed(item.model.object) + sortRole: "path.basename" + property var filterRoleType: "" + filterRole: _reconstruction.sfmReport ? filterRoleType : "" + filterValue: false + + function updateFilter(role, value) { + grid.updateSelectedViewFromGrid = false + sortedModel.filterRoleType = role + sortedModel.filterValue = value + grid.updateCurrentIndexFromSelectionViewId() + grid.updateSelectedViewFromGrid = true + } + onFilterRoleChanged: { + grid.makeCurrentItemVisible() + } + onFilterValueChanged: { + grid.makeCurrentItemVisible() + } // override modelData to return basename of viewpoint's path for sorting - function modelData(item, roleName) { + 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(roleName == sortRole) + + if(cmd == "basename") return Filepath.basename(value) - else - return value + + return value } delegate: ImageDelegate { @@ -138,14 +235,10 @@ Panel { height: grid.cellHeight readOnly: m.readOnly displayViewId: displayViewIdsAction.checked + visible: !intrinsicsFilterButton.checked isCurrentItem: GridView.isCurrentItem - onIsCurrentItemChanged: { - if(isCurrentItem) - _reconstruction.selectedViewId = viewpoint.get("viewId").value - } - onPressed: { grid.currentIndex = DelegateModel.filteredIndex if(mouse.button == Qt.LeftButton) @@ -223,18 +316,51 @@ Panel { Keys.onPressed: { if(event.modifiers & Qt.AltModifier) { - event.accepted = true if(event.key == Qt.Key_Right) - root.changeCurrentIndex(Math.min(root.cameraInits.count - 1, root.currentIndex + 1)) + { + _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 + { + grid.updateSelectedViewFromGrid = false + if(event.key == Qt.Key_Right) + { + grid.moveCurrentIndexRight() + // grid.setCurrentIndex(Math.min(grid.model.count - 1, grid.currentIndex + 1)) + event.accepted = true + } else if(event.key == Qt.Key_Left) - root.changeCurrentIndex(Math.max(0, root.currentIndex - 1)) + { + grid.moveCurrentIndexLeft() + // grid.setCurrentIndex(Math.max(0, grid.currentIndex - 1)) + 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 + } + grid.updateSelectedViewFromGrid = true } } // Explanatory placeholder when no image has been added yet Column { + id: dropImagePlaceholder anchors.centerIn: parent - visible: grid.model.count == 0 + visible: (m.viewpoints ? m.viewpoints.count == 0 : true) && !intrinsicsFilterButton.checked spacing: 4 Label { anchors.horizontalCenter: parent.horizontalCenter @@ -246,11 +372,27 @@ Panel { 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 + enabled: !m.readOnly && !intrinsicsFilterButton.checked keys: ["text/uri-list"] // TODO: onEntered: call specific method to filter files based on extension onDropped: { @@ -308,6 +450,74 @@ Panel { } } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + visible: intrinsicsFilterButton.checked + + 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", + "pxInitialFocalLength", + "pxFocalLength.x", + "pxFocalLength.y", + "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]]} } + TableModelColumn { display: function(modelIndex){return parsedIntrinsic[modelIndex.row][intrinsicModel.columnNames[12]]} } + //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.count > 1 @@ -327,7 +537,7 @@ Panel { id: nodesCB model: root.cameraInits.count implicitWidth: 40 - currentIndex: root.currentIndex + currentIndex: root.cameraInitIndex onActivated: root.changeCurrentIndex(currentIndex) } Label { text: "/ " + (root.cameraInits.count - 1) } @@ -336,7 +546,7 @@ Panel { font.family: MaterialIcons.fontFamily ToolTip.text: "Next Group (Alt+Right)" ToolTip.visible: hovered - enabled: root.currentIndex < root.cameraInits.count - 1 + enabled: root.cameraInitIndex < root.cameraInits.count - 1 onClicked: nodesCB.incrementCurrentIndex() } } @@ -344,22 +554,120 @@ Panel { footerContent: RowLayout { // Images count - MaterialToolLabel { + 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: grid.model.count.toString() - // enabled: grid.model.count > 0 - // margin: 4 + label: (m.viewpoints ? m.viewpoints.count : 0) + padding: 3 + + checkable: true + checked: true + + onCheckedChanged:{ + if(checked) { + sortedModel.updateFilter("", true) + estimatedCamerasFilterButton.checked = false + nonEstimatedCamerasFilterButton.checked = false + intrinsicsFilterButton.checked = false; + } + } } - // cameras count - MaterialToolLabel { + // Estimated cameras count + MaterialToolLabelButton { + id : estimatedCamerasFilterButton Layout.minimumWidth: childrenRect.width ToolTip.text: label + " Estimated Cameras" iconText: MaterialIcons.videocam - label: _reconstruction ? _reconstruction.nbCameras.toString() : "0" - // margin: 4 - // enabled: _reconstruction.cameraInit && _reconstruction.nbCameras + label: _reconstruction.nbCameras ? _reconstruction.nbCameras.toString() : "-" + padding: 3 + + enabled: _reconstruction.cameraInit && _reconstruction.nbCameras + checkable: true + checked: false + + onCheckedChanged:{ + if(checked) { + sortedModel.updateFilter("viewId.isReconstructed", 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.nbCameras ? ((m.viewpoints ? m.viewpoints.count : 0) - _reconstruction.nbCameras.toString()).toString() : "-" + padding: 3 + + enabled: _reconstruction.cameraInit && _reconstruction.nbCameras + checkable: true + checked: false + + onCheckedChanged:{ + if(checked) { + sortedModel.updateFilter("viewId.isReconstructed", 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 } diff --git a/meshroom/ui/qml/ImageGallery/IntrinsicDisplayDelegate.qml b/meshroom/ui/qml/ImageGallery/IntrinsicDisplayDelegate.qml new file mode 100644 index 0000000000..bcc320abad --- /dev/null +++ b/meshroom/ui/qml/ImageGallery/IntrinsicDisplayDelegate.qml @@ -0,0 +1,205 @@ +import QtQuick 2.9 +import QtQuick.Layouts 1.3 +import QtQuick.Controls 2.2 +import MaterialIcons 2.2 +import Utils 1.0 + +RowLayout { + id: root + + Layout.fillWidth: true + + property variant attribute: model.display + property int rowIndex: model.row + property int columnIndex: model.column + property bool readOnly: false + property string toolTipText: { + if(!attribute || Object.keys(attribute).length === 0) + return "" + return attribute.fullLabel + } + + Pane { + Layout.minimumWidth: loaderComponent.width + Layout.minimumHeight: loaderComponent.height + Layout.fillWidth: true + + padding: 0 + + hoverEnabled: true + + // Tooltip to replace headers for now (header incompatible atm) + ToolTip.delay: 10 + ToolTip.timeout: 5000 + ToolTip.visible: hovered + ToolTip.text: toolTipText + + Rectangle { + width: parent.width + height: loaderComponent.height + + color: rowIndex % 2 ? palette.window : Qt.darker(palette.window, 1.1) + border.width: 2 + border.color: Qt.darker(palette.window, 1.2) + clip: true + Loader { + id: loaderComponent + active: !!model.display // convert to bool with "!!" + sourceComponent: { + if(!model.display) + return undefined + switch(model.display.type) + { + case "ChoiceParam": return choice_component + case "IntParam": return int_component + case "FloatParam": return float_component + case "BoolParam": return bool_component + case "StringParam": return textField_component + default: return undefined + } + } + } + } + } + + Component { + id: textField_component + TextInput{ + text: model.display.value + width: intrinsicModel.columnWidths[columnIndex] + horizontalAlignment: TextInput.AlignRight + color: 'white' + + padding: 12 + + selectByMouse: true + selectionColor: 'white' + selectedTextColor: Qt.darker(palette.window, 1.1) + + onEditingFinished: _reconstruction.setAttribute(attribute, text) + onAccepted: { + _reconstruction.setAttribute(attribute, text) + } + Component.onDestruction: { + if(activeFocus) + _reconstruction.setAttribute(attribute, text) + } + } + } + + Component { + id: int_component + + TextInput{ + text: model.display.value + width: intrinsicModel.columnWidths[columnIndex] + horizontalAlignment: TextInput.AlignRight + color: 'white' + + padding: 12 + + selectByMouse: true + selectionColor: 'white' + selectedTextColor: Qt.darker(palette.window, 1.1) + + IntValidator { + id: intValidator + } + + validator: intValidator + + onEditingFinished: _reconstruction.setAttribute(attribute, Number(text)) + onAccepted: { + _reconstruction.setAttribute(attribute, Number(text)) + } + Component.onDestruction: { + if(activeFocus) + _reconstruction.setAttribute(attribute, Number(text)) + } + } + } + + Component { + id: choice_component + ComboBox { + id: combo + model: attribute.desc.values + width: intrinsicModel.columnWidths[columnIndex] + + flat : true + + topInset: 7 + leftInset: 6 + rightInset: 6 + bottomInset: 7 + + Component.onCompleted: currentIndex = find(attribute.value) + onActivated: _reconstruction.setAttribute(attribute, currentText) + + Connections { + target: attribute + onValueChanged: combo.currentIndex = combo.find(attribute.value) + } + } + } + + Component { + id: bool_component + CheckBox { + checked: attribute ? attribute.value : false + padding: 12 + onToggled: _reconstruction.setAttribute(attribute, !attribute.value) + } + } + + Component { + id: float_component + TextInput{ + readonly property real formattedValue: model.display.value.toFixed(2) + property string displayValue: String(formattedValue) + + text: displayValue + width: intrinsicModel.columnWidths[columnIndex] + horizontalAlignment: TextInput.AlignRight + + color: 'white' + padding: 12 + + selectByMouse: true + selectionColor: 'white' + selectedTextColor: Qt.darker(palette.window, 1.1) + + enabled: !readOnly + + clip: true; + + autoScroll: activeFocus + + //Use this function to ensure the left part is visible + //while keeping the trick for formatting the text + //Timing issues otherwise + onActiveFocusChanged: { + if(activeFocus) text = String(model.display.value) + else text = String(formattedValue) + cursorPosition = 0 + } + + DoubleValidator { + id: doubleValidator + locale: 'C' // use '.' decimal separator disregarding the system locale + } + + validator: doubleValidator + + onEditingFinished: _reconstruction.setAttribute(attribute, Number(text)) + onAccepted: { + _reconstruction.setAttribute(attribute, Number(text)) + } + Component.onDestruction: { + if(activeFocus) + _reconstruction.setAttribute(attribute, Number(text)) + } + } + } + +} diff --git a/meshroom/ui/qml/ImageGallery/qmldir b/meshroom/ui/qml/ImageGallery/qmldir index f6bbf7dbcf..5132661071 100644 --- a/meshroom/ui/qml/ImageGallery/qmldir +++ b/meshroom/ui/qml/ImageGallery/qmldir @@ -2,3 +2,6 @@ module ImageGallery ImageGallery 1.0 ImageGallery.qml ImageDelegate 1.0 ImageDelegate.qml +ImageIntrinsicDelegate 1.0 ImageIntrinsicDelegate.qml +ImageIntrinsicViewer 1.0 ImageIntrinsicViewer.qml +IntrinsicDisplayDelegate 1.0 IntrinsicDisplayDelegate.qml diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index cdd5642ae0..ab6807dc36 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -67,7 +67,7 @@ Item { cameraInits: root.cameraInits cameraInit: reconstruction.cameraInit tempCameraInit: reconstruction.tempCameraInit - currentIndex: reconstruction.cameraInitIndex + cameraInitIndex: reconstruction.cameraInitIndex onRemoveImageRequest: reconstruction.removeAttribute(attribute) onFilesDropped: reconstruction.handleFilesDrop(drop, augmentSfm ? null : cameraInit) } diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 4e635b2a55..a1c6a8fb74 --- a/start.sh +++ b/start.sh @@ -1,3 +1,12 @@ -#!/bin/sh -export PYTHONPATH="$(dirname "$(readlink -f "${BASH_SOURCE[0]}" )" )" -python meshroom/ui +#!/bin/bash +export MESHROOM_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}" )" )" +export PYTHONPATH=$MESHROOM_ROOT:$PYTHONPATH + +# using existing alicevision release +#export LD_LIBRARY_PATH=/foo/Meshroom-2021.1.0/aliceVision/lib/ +#export PATH=$PATH:/foo/Meshroom-2021.1.0/aliceVision/bin/ + +# using alicevision built source +#export PATH=$PATH:/foo/build/Linux-x86_64/ + +python "$MESHROOM_ROOT/meshroom/ui"