Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ui] Viewer: add Camera Response Function graph #1020

Merged
merged 9 commits into from
Aug 13, 2020
2 changes: 2 additions & 0 deletions meshroom/ui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
117 changes: 117 additions & 0 deletions meshroom/ui/components/csvData.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from meshroom.common.qt import QObjectListModel

from PySide2.QtCore import QObject, Slot, Signal, Property
from PySide2.QtCharts import QtCharts

import csv
import os

class CsvData(QObject):
"""Store data from a CSV file."""
def __init__(self, parent=None):
"""Initialize the object without any parameter."""
super(CsvData, self).__init__(parent=parent)
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):
return self._data.at(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
self.setReady(False)
self._filepath = filepath
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.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."""
if not self._filepath or not self._filepath.lower().endswith(".csv") or not os.path.isfile(self._filepath):
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, parent=self._data))

# 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, notify=readyChanged)
nbColumns = Property(int, getNbColumns, notify=readyChanged)


class CsvColumn(QObject):
"""Store content of a CSV column."""
def __init__(self, title="", parent=None):
"""Initialize the object with optional column title parameter."""
super(CsvColumn, self).__init__(parent=parent)
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)
139 changes: 139 additions & 0 deletions meshroom/ui/qml/Viewer/CameraResponseGraph.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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 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
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: crfReady ? csvData.getColumn(0).getFirst() : 0
max: crfReady ? 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 {
id: redCurve
axisX: valueAxisX
axisY: valueAxisY
name: crfReady ? csvData.getColumn(1).title : ""
color: name.toLowerCase()
}
// Green curve
LineSeries {
id: greenCurve
axisX: valueAxisX
axisY: valueAxisY
name: crfReady ? csvData.getColumn(2).title : ""
color: name.toLowerCase()
}
// Blue curve
LineSeries {
id: blueCurve
axisX: valueAxisX
axisY: valueAxisY
name: crfReady ? csvData.getColumn(3).title : ""
color: name.toLowerCase()
}
}

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
}
}
}
}
}
31 changes: 31 additions & 0 deletions meshroom/ui/qml/Viewer/Viewer2D.qml
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,18 @@ FocusScope {
featuresViewer: featuresViewerLoader.item
}
}

Loader {
id: ldrHdrCalibrationGraph
anchors.fill: parent

property var activeNode: _reconstruction.activeNodes.get('LdrToHdrCalibration').node
active: activeNode && activeNode.isComputed && displayLdrHdrCalibrationGraph.checked

sourceComponent: CameraResponseGraph {
ldrHdrCalibrationNode: activeNode
}
}
}
FloatingPane {
id: bottomToolbar
Expand Down Expand Up @@ -628,6 +640,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
Expand Down