Merge pull request #539 from alicevision/dev/featuresViewer

Features Viewer
This commit is contained in:
Fabien Castan 2019-07-25 20:58:18 +02:00 committed by GitHub
commit 059ca3685f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 318 additions and 28 deletions

View file

@ -0,0 +1,66 @@
import QtQuick 2.10
import QtQuick.Controls 2.10
import Utils 1.0
/**
* ColorChart is a color picker based on a set of predefined colors.
* It takes the form of a ToolButton that pops-up its palette when pressed.
*/
ToolButton {
id: root
property var colors: ["red", "green", "blue"]
property int currentIndex: 0
signal colorPicked(var colorIndex)
background: Rectangle {
color: root.colors[root.currentIndex]
border.width: hovered ? 1 : 0
border.color: Colors.sysPalette.midlight
}
onPressed: palettePopup.open()
// Popup for the color palette
Popup {
id: palettePopup
padding: 4
// content width is missing side padding (hence the + padding*2)
implicitWidth: colorChart.contentItem.width + padding*2
// center the current color
y: - (root.height - padding) / 2
x: - colorChart.currentItem.x - padding
// Colors palette
ListView {
id: colorChart
implicitHeight: contentItem.childrenRect.height
implicitWidth: contentWidth
orientation: ListView.Horizontal
spacing: 2
currentIndex: root.currentIndex
model: root.colors
// display each color as a ToolButton with a custom background
delegate: ToolButton {
padding: 0
width: root.width
height: root.height
background: Rectangle {
color: modelData
// display border of current/selected item
border.width: hovered || index === colorChart.currentIndex ? 1 : 0
border.color: Colors.sysPalette.midlight
}
onClicked: {
colorPicked(index);
palettePopup.close();
}
}
}
}
}

View file

@ -1,5 +1,6 @@
module Controls
ColorChart 1.0 ColorChart.qml
FloatingPane 1.0 FloatingPane.qml
Group 1.0 Group.qml
MessageDialog 1.0 MessageDialog.qml

View file

@ -14,4 +14,7 @@ QtObject {
readonly property color yellow: "#FFEB3B"
readonly property color red: "#F44336"
readonly property color blue: "#03A9F4"
readonly property color cyan: "#00BCD4"
readonly property color pink: "#E91E63"
readonly property color lime: "#CDDC39"
}

View file

@ -0,0 +1,111 @@
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import Utils 1.0
import Controls 1.0
/**
* FeaturesInfoOverlay is an overlay that displays info and
* provides controls over a FeaturesViewer component.
*/
FloatingPane {
id: root
property int pluginStatus: Loader.Null
property Item featuresViewer: null
property var featureExtractionNode: null
ColumnLayout {
// Header
RowLayout {
// FeatureExtraction node name
Label {
text: featureExtractionNode.label
Layout.fillWidth: true
}
// Settings menu
Loader {
active: root.pluginStatus === Loader.Ready
sourceComponent: MaterialToolButton {
text: MaterialIcons.settings
font.pointSize: 10
onClicked: settingsMenu.popup(width, 0)
Menu {
id: settingsMenu
padding: 4
implicitWidth: 210
RowLayout {
Label {
text: "Display Mode:"
}
ComboBox {
id: displayModeCB
flat: true
Layout.fillWidth: true
model: featuresViewer.displayModes
onActivated: featuresViewer.displayMode = currentIndex
}
}
}
}
}
}
// Error message if AliceVision plugin is unavailable
Label {
visible: root.pluginStatus === Loader.Error
text: "AliceVision plugin is required to display Features"
color: Colors.red
}
// Feature types
ListView {
implicitHeight: contentHeight
implicitWidth: contentItem.childrenRect.width
model: featuresViewer !== null ? featuresViewer.model : 0
delegate: RowLayout {
id: featureType
property var viewer: featuresViewer.itemAt(index)
spacing: 4
// Visibility toogle
MaterialToolButton {
text: featureType.viewer.visible ? MaterialIcons.visibility : MaterialIcons.visibility_off
onClicked: featureType.viewer.visible = !featureType.viewer.visible
font.pointSize: 10
opacity: featureType.viewer.visible ? 1.0 : 0.6
}
// ColorChart picker
ColorChart {
implicitWidth: 12
implicitHeight: implicitWidth
colors: featuresViewer.colors
currentIndex: featureType.viewer.colorIndex
// offset FeaturesViewer color set when changing the color of one feature type
onColorPicked: featuresViewer.colorOffset = colorIndex - index
}
// Feature type name
Label {
text: featureType.viewer.describerType + (featureType.viewer.loading ? "" : ": " + featureType.viewer.features.length)
}
// Feature loading status
Loader {
active: featureType.viewer.loading
sourceComponent: BusyIndicator {
padding: 0
implicitWidth: 12
implicitHeight: 12
running: true
}
}
}
}
}
}

View file

@ -0,0 +1,40 @@
import QtQuick 2.11
import AliceVision 1.0 as AliceVision
import Utils 1.0
/**
* FeaturesViewer displays the extracted feature points of a View.
* Requires QtAliceVision plugin.
*/
Repeater {
id: root
/// ViewID to display the features of
property int viewId
/// Folder containing the features files
property string folder
/// The list of describer types to load
property alias describerTypes: root.model
/// List of available display modes
readonly property var displayModes: ['Points', 'Squares', 'Oriented Squares']
/// Current display mode index
property int displayMode: 0
/// The list of colors used for displaying several describers
property var colors: [Colors.blue, Colors.red, Colors.yellow, Colors.green, Colors.orange, Colors.cyan, Colors.pink, Colors.lime]
/// Offset the color list
property int colorOffset: 0
model: root.describerTypes
// instantiate one FeaturesViewer by describer type
delegate: AliceVision.FeaturesViewer {
readonly property int colorIndex: (index+root.colorOffset)%root.colors.length
describerType: modelData
folder: root.folder
viewId: root.viewId
color: root.colors[colorIndex]
displayMode: root.displayMode
}
}

View file

@ -77,8 +77,39 @@ FocusScope {
visible: image.status === Image.Loading
}
// FeatureViewer: display view extracted feature points
// note: requires QtAliceVision plugin - use a Loader to evaluate plugin avaibility at runtime
Loader {
id: featuresViewerLoader
active: displayFeatures.checked
// handle rotation/position based on available metadata
rotation: {
var orientation = metadata ? metadata["Orientation"] : 0
switch(orientation) {
case "6": return 90;
case "8": return -90;
default: return 0;
}
}
x: rotation === 90 ? image.paintedWidth : 0
y: rotation === -90 ? image.paintedHeight : 0
Component.onCompleted: {
// instantiate and initialize a FeaturesViewer component dynamically using Loader.setSource
setSource("FeaturesViewer.qml", {
'active': Qt.binding(function() { return displayFeatures.checked; }),
'viewId': Qt.binding(function() { return _reconstruction.selectedViewId; }),
'model': Qt.binding(function() { return _reconstruction.featureExtraction.attribute("describerTypes").value; }),
'folder': Qt.binding(function() { return Filepath.stringToUrl(_reconstruction.featureExtraction.attribute("output").value); }),
})
}
}
}
// Busy indicator
BusyIndicator {
anchors.centerIn: parent
@ -147,6 +178,21 @@ FocusScope {
metadata: visible ? root.metadata : {}
}
Loader {
id: featuresOverlay
anchors.bottom: bottomToolbar.top
anchors.left: parent.left
anchors.margins: 2
active: displayFeatures.checked
sourceComponent: FeaturesInfoOverlay {
featureExtractionNode: _reconstruction.featureExtraction
pluginStatus: featuresViewerLoader.status
featuresViewer: featuresViewerLoader.item
}
}
FloatingPane {
id: bottomToolbar
anchors.bottom: parent.bottom
@ -163,6 +209,13 @@ FocusScope {
text: (image.status == Image.Ready ? image.scale.toFixed(2) : "1.00") + "x"
state: "xsmall"
}
MaterialToolButton {
id: displayFeatures
font.pointSize: 11
ToolTip.text: "Display Features"
checkable: true
text: MaterialIcons.scatter_plot
}
Item {
Layout.fillWidth: true

View file

@ -133,8 +133,8 @@ Item {
// Load reconstructed model
Button {
readonly property var outputAttribute: _reconstruction.endNode ? _reconstruction.endNode.attribute("outputMesh") : null
readonly property bool outputReady: outputAttribute && _reconstruction.endNode.globalStatus === "SUCCESS"
readonly property var outputAttribute: _reconstruction.texturing ? _reconstruction.texturing.attribute("outputMesh") : null
readonly property bool outputReady: outputAttribute && _reconstruction.texturing.globalStatus === "SUCCESS"
readonly property int outputMediaIndex: viewer3D.library.find(outputAttribute)
text: "Load Model"
@ -142,7 +142,7 @@ Item {
anchors.bottomMargin: 10
anchors.horizontalCenter: parent.horizontalCenter
visible: outputReady && outputMediaIndex == -1
onClicked: viewer3D.view(_reconstruction.endNode.attribute("outputMesh"))
onClicked: viewer3D.view(_reconstruction.texturing.attribute("outputMesh"))
}
}
}

View file

@ -595,6 +595,14 @@ ApplicationWindow {
{
_reconstruction.sfm = node;
}
else if(node.nodeType === "FeatureExtraction")
{
_reconstruction.featureExtraction = node;
}
else if(node.nodeType === "CameraInit")
{
_reconstruction.cameraInit = node;
}
for(var i=0; i < node.attributes.count; ++i)
{
var attr = node.attributes.at(i)

View file

@ -161,19 +161,31 @@ class Reconstruction(UIGraph):
def __init__(self, graphFilepath='', parent=None):
super(Reconstruction, self).__init__(graphFilepath, parent)
self._buildingIntrinsics = False
self._cameraInit = None
self._cameraInits = QObjectListModel(parent=self)
self._endNode = None
self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable)
self.graphChanged.connect(self.onGraphChanged)
self._liveSfmManager = LiveSfmManager(self)
# SfM result
# initialize member variables for key steps of the 3D reconstruction pipeline
# - CameraInit
self._cameraInit = None # current CameraInit node
self._cameraInits = QObjectListModel(parent=self) # all CameraInit nodes
self._buildingIntrinsics = False
self.intrinsicsBuilt.connect(self.onIntrinsicsAvailable)
# - Feature Extraction
self._featureExtraction = None
self.cameraInitChanged.connect(self.updateFeatureExtraction)
# - SfM
self._sfm = None
self._views = None
self._poses = None
self._selectedViewId = None
self._liveSfmManager = LiveSfmManager(self)
# - Texturing
self._texturing = None
# react to internal graph changes to update those variables
self.graphChanged.connect(self.onGraphChanged)
if graphFilepath:
self.onGraphChanged()
@ -211,8 +223,9 @@ class Reconstruction(UIGraph):
def onGraphChanged(self):
""" React to the change of the internal graph. """
self._liveSfmManager.reset()
self.featureExtraction = None
self.sfm = None
self.endNode = None
self.texturing = None
self.updateCameraInits()
if not self._graph:
return
@ -249,6 +262,10 @@ class Reconstruction(UIGraph):
camInit = self._cameraInits[idx] if self._cameraInits else None
self.cameraInit = camInit
def updateFeatureExtraction(self):
""" Set the current FeatureExtraction node based on the current CameraInit node. """
self.featureExtraction = self.lastNodeOfType('FeatureExtraction', self.cameraInit) if self.cameraInit else None
def lastSfmNode(self):
""" Retrieve the last SfM node from the initial CameraInit node. """
return self.lastNodeOfType("StructureFromMotion", self._cameraInit, Status.SUCCESS)
@ -493,21 +510,8 @@ class Reconstruction(UIGraph):
self._sfm.chunks[0].statusChanged.disconnect(self.updateViewsAndPoses)
self._sfm.destroyed.disconnect(self._unsetSfm)
self._setSfm(node)
self.setEndNode(self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS))
def setEndNode(self, node=None):
if self._endNode == node:
return
if self._endNode:
try:
self._endNode.destroyed.disconnect(self.setEndNode)
except RuntimeError:
# self._endNode might have been destroyed at this point, causing PySide2 to throw a RuntimeError
pass
self._endNode = node
if self._endNode:
self._endNode.destroyed.connect(self.setEndNode)
self.endNodeChanged.emit()
self.texturing = self.lastNodeOfType("Texturing", self._sfm, Status.SUCCESS)
@Slot(QObject, result=bool)
def isInViews(self, viewpoint):
@ -566,12 +570,16 @@ class Reconstruction(UIGraph):
sfmChanged = Signal()
sfm = Property(QObject, getSfm, setSfm, notify=sfmChanged)
featureExtractionChanged = Signal()
featureExtraction = makeProperty(QObject, "_featureExtraction", featureExtractionChanged, resetOnDestroy=True)
sfmReportChanged = Signal()
# convenient property for QML binding re-evaluation when sfm report changes
sfmReport = Property(bool, lambda self: len(self._poses) > 0, notify=sfmReportChanged)
sfmAugmented = Signal(Node, Node)
endNodeChanged = Signal()
endNode = Property(QObject, lambda self: self._endNode, setEndNode, notify=endNodeChanged)
texturingChanged = Signal()
texturing = makeProperty(QObject, "_texturing", notify=texturingChanged)
nbCameras = Property(int, reconstructedCamerasCount, notify=sfmReportChanged)