From aa5e2b76b614f45284f7c621f16d5fac6eec399b Mon Sep 17 00:00:00 2001 From: Julien-Haudegond <44610840+Julien-Haudegond@users.noreply.github.com> Date: Tue, 4 Aug 2020 17:27:13 +0200 Subject: [PATCH 1/9] [ui] Components: add a CSV Data container --- meshroom/ui/components/__init__.py | 2 + meshroom/ui/components/csvData.py | 114 +++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 meshroom/ui/components/csvData.py diff --git a/meshroom/ui/components/__init__.py b/meshroom/ui/components/__init__.py index 6c94a10ded..df9a5bb5ac 100755 --- a/meshroom/ui/components/__init__.py +++ b/meshroom/ui/components/__init__.py @@ -5,9 +5,11 @@ def registerTypes(): from meshroom.ui.components.edge import EdgeMouseArea from meshroom.ui.components.filepath import FilepathHelper from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController + from meshroom.ui.components.csvData import CsvData qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea") qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController") + qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData") diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py new file mode 100644 index 0000000000..2e87ab572a --- /dev/null +++ b/meshroom/ui/components/csvData.py @@ -0,0 +1,114 @@ +from meshroom.common.qt import QObjectListModel + +from PySide2.QtCore import QObject, Slot, Signal, Property +from PySide2.QtCharts import QtCharts + +import csv + +class CsvData(QObject): + """ + Store data from a CSV file + """ + def __init__(self): + super(CsvData, self).__init__() + self._filepath = "" + self._data = QObjectListModel(parent=self) # List of CsvColumn + self._ready = False + + @Slot(int, result=QObject) + def getColumn(self, index): + return self._data.at(index) + + def getFilepath(self): + return self._filepath + + def setFilepath(self, filepath): + if self._filepath == filepath: + return + self._filepath = filepath + self.updateData() + self.filepathChanged.emit() + + def setReady(self, ready): + if self._ready == ready: + return + self._ready = ready + self.readyChanged.emit() + + def updateData(self): + self.setReady(False) + self._data.setObjectList(self.read()) + if not self._data.isEmpty(): + self.setReady(True) + + def read(self): + """ + Read the CSV file and return a list containing CsvColumn objects + """ + if not self._filepath or not self._filepath.endswith(".csv"): + return [] + + csvRows = [] + with open(self._filepath, "r") as fp: + reader = csv.reader(fp) + for row in reader: + csvRows.append(row) + + dataList = [] + + # Create the objects in dataList + # with the first line elements as objects' title + for elt in csvRows[0]: + dataList.append(CsvColumn(elt)) + + # Populate the content attribute + for elt in csvRows[1:]: + for idx, value in enumerate(elt): + dataList[idx].appendValue(value) + + return dataList + + filepathChanged = Signal() + filepath = Property(str, getFilepath, setFilepath, notify=filepathChanged) + readyChanged = Signal() + ready = Property(bool, lambda self: self._ready, notify=readyChanged) + data = Property(QObject, lambda self: self._data, constant=True) + + +class CsvColumn(QObject): + """ + Store content of a CSV column + """ + def __init__(self, title=""): + super(CsvColumn, self).__init__() + self._title = title + self._content = [] + + def appendValue(self, value): + self._content.append(value) + + @Slot(result=str) + def getFirst(self): + if not self._content: + return "" + return self._content[0] + + @Slot(result=str) + def getLast(self): + if not self._content: + return "" + return self._content[-1] + + @Slot(QtCharts.QXYSeries) + def fillChartSerie(self, serie): + """ + Fill XYSerie used for displaying QML Chart + """ + if not serie: + return + serie.clear() + for index, value in enumerate(self._content): + serie.append(float(index), float(value)) + + title = Property(str, lambda self: self._title, constant=True) + content = Property("QStringList", lambda self: self._content, constant=True) \ No newline at end of file From e0c42fb42a5e83a3257bf12f36662d56a0364828 Mon Sep 17 00:00:00 2001 From: Julien-Haudegond <44610840+Julien-Haudegond@users.noreply.github.com> Date: Wed, 5 Aug 2020 11:16:45 +0200 Subject: [PATCH 2/9] [ui] Components: fix CsvData error --- meshroom/ui/components/csvData.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index 2e87ab572a..b22f8edabd 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -4,6 +4,7 @@ from PySide2.QtCharts import QtCharts import csv +import os class CsvData(QObject): """ @@ -45,7 +46,7 @@ def read(self): """ Read the CSV file and return a list containing CsvColumn objects """ - if not self._filepath or not self._filepath.endswith(".csv"): + if not self._filepath or not self._filepath.endswith(".csv") or not os.path.isfile(self._filepath): return [] csvRows = [] From 5a1660a2c00e1db92e325defece6cdf830c94d45 Mon Sep 17 00:00:00 2001 From: Julien-Haudegond <44610840+Julien-Haudegond@users.noreply.github.com> Date: Wed, 5 Aug 2020 11:54:46 +0200 Subject: [PATCH 3/9] [ui] Viewer: add LdrToHdrCalibration camera response graph --- .../ui/qml/Viewer/CameraResponseGraph.qml | 125 ++++++++++++++++++ meshroom/ui/qml/Viewer/Viewer2D.qml | 32 +++++ 2 files changed, 157 insertions(+) create mode 100644 meshroom/ui/qml/Viewer/CameraResponseGraph.qml diff --git a/meshroom/ui/qml/Viewer/CameraResponseGraph.qml b/meshroom/ui/qml/Viewer/CameraResponseGraph.qml new file mode 100644 index 0000000000..f149b83915 --- /dev/null +++ b/meshroom/ui/qml/Viewer/CameraResponseGraph.qml @@ -0,0 +1,125 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import QtPositioning 5.8 +import QtLocation 5.9 +import QtCharts 2.13 +import Charts 1.0 + +import Controls 1.0 +import Utils 1.0 +import DataObjects 1.0 + +FloatingPane { + id: root + + property var ldrHdrCalibrationNode: null + property color textColor: Colors.sysPalette.text + + clip: true + padding: 4 + + CsvData { + id: csvData + filepath: ldrHdrCalibrationNode ? ldrHdrCalibrationNode.attribute("response").value : "" + } + + // To avoid interaction with components in background + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: {} + onReleased: {} + onWheel: {} + } + + property bool ready: csvData.ready + Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenterOffset: -responseChart.width/2 + anchors.verticalCenterOffset: -responseChart.height/2 + + InteractiveChartView { + id: responseChart + width: root.width > 400 ? 400 : (root.width < 350 ? 350 : root.width) + height: width * 0.75 + + title: "Camera Response Function (CRF)" + legend.visible: false + antialiasing: true + + ValueAxis { + id: valueAxisX + labelFormat: "%i" + titleText: "Camera Brightness" + min: ready ? csvData.getColumn(0).getFirst() : 0 + max: ready ? csvData.getColumn(0).getLast() : 1 + } + ValueAxis { + id: valueAxisY + titleText: "Normalized Radiance" + min: 0.0 + max: 1.0 + } + + // We cannot use a Repeater with these Components so we need to instantiate them one by one + // Red curve + LineSeries { + axisX: valueAxisX + axisY: valueAxisY + name: ready ? csvData.getColumn(1).title : "" + color: name.toLowerCase() + + Component.onCompleted: if(ready) csvData.getColumn(1).fillChartSerie(this) + } + // Green curve + LineSeries { + axisX: valueAxisX + axisY: valueAxisY + name: ready ? csvData.getColumn(2).title : "" + color: name.toLowerCase() + + Component.onCompleted: if(ready) csvData.getColumn(2).fillChartSerie(this) + } + // Blue curve + LineSeries { + axisX: valueAxisX + axisY: valueAxisY + name: ready ? csvData.getColumn(3).title : "" + color: name.toLowerCase() + + Component.onCompleted: if(ready) csvData.getColumn(3).fillChartSerie(this) + } + } + + Item { + id: btnContainer + + anchors.bottom: responseChart.bottom + anchors.bottomMargin: 35 + anchors.left: responseChart.left + anchors.leftMargin: responseChart.width * 0.15 + + RowLayout { + ChartViewCheckBox { + text: "ALL" + color: textColor + checkState: legend.buttonGroup.checkState + onClicked: { + const _checked = checked + for(let i = 0; i < responseChart.count; ++i) { + responseChart.series(i).visible = _checked + } + } + } + + ChartViewLegend { + id: legend + chartView: responseChart + } + } + } + } +} diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 69efd6a9c6..e02faa2676 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -552,6 +552,19 @@ FocusScope { featuresViewer: featuresViewerLoader.item } } + + Loader { + id: ldrHdrCalibrationGraph + anchors.fill: parent + + property var activeNode: _reconstruction.activeNodes.get('LdrToHdrCalibration').node + active: activeNode && activeNode.isComputed + visible: displayLdrHdrCalibrationGraph.checked + + sourceComponent: CameraResponseGraph { + ldrHdrCalibrationNode: activeNode + } + } } FloatingPane { id: bottomToolbar @@ -628,6 +641,25 @@ FocusScope { visible: activeNode } + MaterialToolButton { + id: displayLdrHdrCalibrationGraph + property var activeNode: _reconstruction.activeNodes.get("LdrToHdrCalibration").node + property bool isComputed: activeNode && activeNode.isComputed + ToolTip.text: "Display Camera Response Function: " + (activeNode ? activeNode.label : "No Node") + text: MaterialIcons.timeline + font.pointSize: 11 + Layout.minimumWidth: 0 + checkable: true + checked: false + enabled: activeNode && activeNode.isComputed + visible: activeNode + + onIsComputedChanged: { + if(!isComputed) + checked = false + } + } + Label { id: resolutionLabel Layout.fillWidth: true From 385d1f7738bd386361f6f0e0532fde7cd8aa815b Mon Sep 17 00:00:00 2001 From: Julien-Haudegond <44610840+Julien-Haudegond@users.noreply.github.com> Date: Wed, 5 Aug 2020 12:23:23 +0200 Subject: [PATCH 4/9] [ui] Components: fix documentation --- meshroom/ui/components/csvData.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index b22f8edabd..6f5bb93c7b 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -7,10 +7,9 @@ import os class CsvData(QObject): - """ - Store data from a CSV file - """ + """Store data from a CSV file.""" def __init__(self): + """Initialize the object without any parameter.""" super(CsvData, self).__init__() self._filepath = "" self._data = QObjectListModel(parent=self) # List of CsvColumn @@ -43,9 +42,7 @@ def updateData(self): self.setReady(True) def read(self): - """ - Read the CSV file and return a list containing CsvColumn objects - """ + """Read the CSV file and return a list containing CsvColumn objects.""" if not self._filepath or not self._filepath.endswith(".csv") or not os.path.isfile(self._filepath): return [] @@ -77,10 +74,9 @@ def read(self): class CsvColumn(QObject): - """ - Store content of a CSV column - """ + """Store content of a CSV column.""" def __init__(self, title=""): + """Initialize the object with optional column title parameter.""" super(CsvColumn, self).__init__() self._title = title self._content = [] @@ -102,9 +98,7 @@ def getLast(self): @Slot(QtCharts.QXYSeries) def fillChartSerie(self, serie): - """ - Fill XYSerie used for displaying QML Chart - """ + """Fill XYSerie used for displaying QML Chart.""" if not serie: return serie.clear() From b43d59f347bf7f25b9163c8d865148181a957904 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 6 Aug 2020 19:23:22 +0200 Subject: [PATCH 5/9] [ui] CSV: case insensitive extension check --- meshroom/ui/components/csvData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index 6f5bb93c7b..a8337817de 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -43,7 +43,7 @@ def updateData(self): def read(self): """Read the CSV file and return a list containing CsvColumn objects.""" - if not self._filepath or not self._filepath.endswith(".csv") or not os.path.isfile(self._filepath): + if not self._filepath or not self._filepath.lower().endswith(".csv") or not os.path.isfile(self._filepath): return [] csvRows = [] From b9cdd3524199ccb2d7837e9a7b85b40761f1f2d3 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 6 Aug 2020 19:24:51 +0200 Subject: [PATCH 6/9] [ui] CsvData: use signal/slot connection --- meshroom/ui/components/csvData.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index a8337817de..f6b00c1133 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -14,6 +14,7 @@ def __init__(self): self._filepath = "" self._data = QObjectListModel(parent=self) # List of CsvColumn self._ready = False + self.filepathChanged.connect(self.updateData) @Slot(int, result=QObject) def getColumn(self, index): @@ -25,8 +26,8 @@ def getFilepath(self): def setFilepath(self, filepath): if self._filepath == filepath: return + self.setReady(False) self._filepath = filepath - self.updateData() self.filepathChanged.emit() def setReady(self, ready): @@ -37,9 +38,11 @@ def setReady(self, ready): def updateData(self): self.setReady(False) - self._data.setObjectList(self.read()) - if not self._data.isEmpty(): - self.setReady(True) + self._data.clear() + newColumns = self.read() + if newColumns: + self._data.setObjectList(newColumns) + self.setReady(True) def read(self): """Read the CSV file and return a list containing CsvColumn objects.""" From 9e7cb5875c6d22e957b7ff51d456018af4ad04b2 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 6 Aug 2020 19:26:06 +0200 Subject: [PATCH 7/9] [ui] Viewer2D: CRF display is active only when enabled --- meshroom/ui/qml/Viewer/Viewer2D.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index e02faa2676..aa3bfa0071 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -558,8 +558,7 @@ FocusScope { anchors.fill: parent property var activeNode: _reconstruction.activeNodes.get('LdrToHdrCalibration').node - active: activeNode && activeNode.isComputed - visible: displayLdrHdrCalibrationGraph.checked + active: activeNode && activeNode.isComputed && displayLdrHdrCalibrationGraph.checked sourceComponent: CameraResponseGraph { ldrHdrCalibrationNode: activeNode From de93f795fbe4d0140a74399f5449bcf2cc9dcefd Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Thu, 6 Aug 2020 19:26:30 +0200 Subject: [PATCH 8/9] [ui] CsvData: declare Qt parent --- meshroom/ui/components/csvData.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index f6b00c1133..6d00a42cd5 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -8,9 +8,9 @@ class CsvData(QObject): """Store data from a CSV file.""" - def __init__(self): + def __init__(self, parent=None): """Initialize the object without any parameter.""" - super(CsvData, self).__init__() + super(CsvData, self).__init__(parent=parent) self._filepath = "" self._data = QObjectListModel(parent=self) # List of CsvColumn self._ready = False @@ -60,7 +60,7 @@ def read(self): # Create the objects in dataList # with the first line elements as objects' title for elt in csvRows[0]: - dataList.append(CsvColumn(elt)) + dataList.append(CsvColumn(elt, parent=self._data)) # Populate the content attribute for elt in csvRows[1:]: @@ -78,9 +78,9 @@ def read(self): class CsvColumn(QObject): """Store content of a CSV column.""" - def __init__(self, title=""): + def __init__(self, title="", parent=None): """Initialize the object with optional column title parameter.""" - super(CsvColumn, self).__init__() + super(CsvColumn, self).__init__(parent=parent) self._title = title self._content = [] From baaccb0562c1db1c5419608d8fcbac3d0f9a73e8 Mon Sep 17 00:00:00 2001 From: Fabien Castan Date: Tue, 11 Aug 2020 23:55:22 +0200 Subject: [PATCH 9/9] [ui] CsvData: fix data update --- meshroom/ui/components/csvData.py | 9 ++++- .../ui/qml/Viewer/CameraResponseGraph.qml | 38 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index 6d00a42cd5..71c820412f 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -23,6 +23,10 @@ def getColumn(self, index): def getFilepath(self): return self._filepath + @Slot(result=int) + def getNbColumns(self): + return len(self._data) if self._ready else 0 + def setFilepath(self, filepath): if self._filepath == filepath: return @@ -42,7 +46,7 @@ def updateData(self): newColumns = self.read() if newColumns: self._data.setObjectList(newColumns) - self.setReady(True) + self.setReady(True) def read(self): """Read the CSV file and return a list containing CsvColumn objects.""" @@ -73,7 +77,8 @@ def read(self): filepath = Property(str, getFilepath, setFilepath, notify=filepathChanged) readyChanged = Signal() ready = Property(bool, lambda self: self._ready, notify=readyChanged) - data = Property(QObject, lambda self: self._data, constant=True) + data = Property(QObject, lambda self: self._data, notify=readyChanged) + nbColumns = Property(int, getNbColumns, notify=readyChanged) class CsvColumn(QObject): diff --git a/meshroom/ui/qml/Viewer/CameraResponseGraph.qml b/meshroom/ui/qml/Viewer/CameraResponseGraph.qml index f149b83915..0c266359be 100644 --- a/meshroom/ui/qml/Viewer/CameraResponseGraph.qml +++ b/meshroom/ui/qml/Viewer/CameraResponseGraph.qml @@ -34,7 +34,24 @@ FloatingPane { onWheel: {} } - property bool ready: csvData.ready + property bool crfReady: csvData.ready && csvData.nbColumns >= 4 + onCrfReadyChanged: { + if(crfReady) + { + redCurve.clear() + greenCurve.clear() + blueCurve.clear() + csvData.getColumn(1).fillChartSerie(redCurve) + csvData.getColumn(2).fillChartSerie(greenCurve) + csvData.getColumn(3).fillChartSerie(blueCurve) + } + else + { + redCurve.clear() + greenCurve.clear() + blueCurve.clear() + } + } Item { anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -54,8 +71,8 @@ FloatingPane { id: valueAxisX labelFormat: "%i" titleText: "Camera Brightness" - min: ready ? csvData.getColumn(0).getFirst() : 0 - max: ready ? csvData.getColumn(0).getLast() : 1 + min: crfReady ? csvData.getColumn(0).getFirst() : 0 + max: crfReady ? csvData.getColumn(0).getLast() : 1 } ValueAxis { id: valueAxisY @@ -67,30 +84,27 @@ FloatingPane { // We cannot use a Repeater with these Components so we need to instantiate them one by one // Red curve LineSeries { + id: redCurve axisX: valueAxisX axisY: valueAxisY - name: ready ? csvData.getColumn(1).title : "" + name: crfReady ? csvData.getColumn(1).title : "" color: name.toLowerCase() - - Component.onCompleted: if(ready) csvData.getColumn(1).fillChartSerie(this) } // Green curve LineSeries { + id: greenCurve axisX: valueAxisX axisY: valueAxisY - name: ready ? csvData.getColumn(2).title : "" + name: crfReady ? csvData.getColumn(2).title : "" color: name.toLowerCase() - - Component.onCompleted: if(ready) csvData.getColumn(2).fillChartSerie(this) } // Blue curve LineSeries { + id: blueCurve axisX: valueAxisX axisY: valueAxisY - name: ready ? csvData.getColumn(3).title : "" + name: crfReady ? csvData.getColumn(3).title : "" color: name.toLowerCase() - - Component.onCompleted: if(ready) csvData.getColumn(3).fillChartSerie(this) } }