-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1020 from alicevision/dev/camResponseGraph
[ui] Viewer: add Camera Response Function graph
- Loading branch information
Showing
4 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters