mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-06 10:18:42 +02:00
Merge pull request #1266 from ludchieng/dev/colorChecker
New ColorChecker Detection and Correction
This commit is contained in:
commit
c876d2b20c
7 changed files with 391 additions and 2 deletions
79
meshroom/nodes/aliceVision/ColorCheckerCorrection.py
Normal file
79
meshroom/nodes/aliceVision/ColorCheckerCorrection.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
__version__ = "1.0"
|
||||||
|
|
||||||
|
from meshroom.core import desc
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
|
class ColorCheckerCorrection(desc.CommandLineNode):
|
||||||
|
commandLine = 'aliceVision_utils_colorCheckerCorrection {allParams}'
|
||||||
|
size = desc.DynamicNodeSize('input')
|
||||||
|
# parallelization = desc.Parallelization(blockSize=40)
|
||||||
|
# commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}'
|
||||||
|
|
||||||
|
documentation = '''
|
||||||
|
(BETA) \\
|
||||||
|
Performs color calibration from Macbeth color checker chart.
|
||||||
|
|
||||||
|
The node assumes all the images to process are sharing the same colorimetric properties.
|
||||||
|
All the input images will get the same correction.
|
||||||
|
|
||||||
|
If multiple color charts are submitted, only the first one will be taken in account.
|
||||||
|
'''
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
desc.File(
|
||||||
|
name='inputData',
|
||||||
|
label='Color checker data',
|
||||||
|
description='Position and colorimetric data of the color checker',
|
||||||
|
value='',
|
||||||
|
uid=[0],
|
||||||
|
),
|
||||||
|
desc.File(
|
||||||
|
name='input',
|
||||||
|
label='Input',
|
||||||
|
description='SfMData file input, image filenames or regex(es) on the image file path.\nsupported regex: \'#\' matches a single digit, \'@\' one or more digits, \'?\' one character and \'*\' zero or more.',
|
||||||
|
value='',
|
||||||
|
uid=[0],
|
||||||
|
),
|
||||||
|
desc.ChoiceParam(
|
||||||
|
name='extension',
|
||||||
|
label='Output File Extension',
|
||||||
|
description='Output Image File Extension.',
|
||||||
|
value='exr',
|
||||||
|
values=['exr', ''],
|
||||||
|
exclusive=True,
|
||||||
|
uid=[0],
|
||||||
|
),
|
||||||
|
desc.ChoiceParam(
|
||||||
|
name='storageDataType',
|
||||||
|
label='Storage Data Type for EXR output',
|
||||||
|
description='Storage image data type:\n'
|
||||||
|
' * float: Use full floating point (32 bits per channel)\n'
|
||||||
|
' * half: Use half float (16 bits per channel)\n'
|
||||||
|
' * halfFinite: Use half float, but clamp values to avoid non-finite values\n'
|
||||||
|
' * auto: Use half float if all values can fit, else use full float\n',
|
||||||
|
value='float',
|
||||||
|
values=['float', 'half', 'halfFinite', 'auto'],
|
||||||
|
exclusive=True,
|
||||||
|
uid=[0],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
desc.File(
|
||||||
|
name='outSfMData',
|
||||||
|
label='Output sfmData',
|
||||||
|
description='Output sfmData.',
|
||||||
|
value=lambda attr: (desc.Node.internalFolder + os.path.basename(attr.node.input.value)) if (os.path.splitext(attr.node.input.value)[1] in ['.abc', '.sfm']) else '',
|
||||||
|
uid=[],
|
||||||
|
group='', # do not export on the command line
|
||||||
|
),
|
||||||
|
desc.File(
|
||||||
|
name='output',
|
||||||
|
label='Output Folder',
|
||||||
|
description='Output Images Folder.',
|
||||||
|
value=desc.Node.internalFolder,
|
||||||
|
uid=[],
|
||||||
|
),
|
||||||
|
]
|
63
meshroom/nodes/aliceVision/ColorCheckerDetection.py
Normal file
63
meshroom/nodes/aliceVision/ColorCheckerDetection.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
__version__ = "1.0"
|
||||||
|
|
||||||
|
from meshroom.core import desc
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
|
class ColorCheckerDetection(desc.CommandLineNode):
|
||||||
|
commandLine = 'aliceVision_utils_colorCheckerDetection {allParams}'
|
||||||
|
size = desc.DynamicNodeSize('input')
|
||||||
|
# parallelization = desc.Parallelization(blockSize=40)
|
||||||
|
# commandLineRange = '--rangeStart {rangeStart} --rangeSize {rangeBlockSize}'
|
||||||
|
|
||||||
|
documentation = '''
|
||||||
|
(BETA) \\
|
||||||
|
Performs Macbeth color checker chart detection.
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- the detected color charts position and colors
|
||||||
|
- the associated tranform matrix from "theoric" to "measured"
|
||||||
|
assuming that the "theoric" Macbeth chart corners coordinates are:
|
||||||
|
(0, 0), (1675, 0), (1675, 1125), (0, 1125)
|
||||||
|
|
||||||
|
Dev notes:
|
||||||
|
- Fisheye/pinhole is not handled
|
||||||
|
- ColorCheckerViewer is unstable with multiple color chart within a same image
|
||||||
|
'''
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
desc.File(
|
||||||
|
name='input',
|
||||||
|
label='Input',
|
||||||
|
description='SfMData file input, image filenames or regex(es) on the image file path.\nsupported regex: \'#\' matches a single digit, \'@\' one or more digits, \'?\' one character and \'*\' zero or more.',
|
||||||
|
value='',
|
||||||
|
uid=[0],
|
||||||
|
),
|
||||||
|
desc.IntParam(
|
||||||
|
name='maxCount',
|
||||||
|
label='Max count by image',
|
||||||
|
description='Max color charts count to detect in a single image',
|
||||||
|
value=1,
|
||||||
|
range=(1, 3, 1),
|
||||||
|
uid=[0],
|
||||||
|
advanced=True,
|
||||||
|
),
|
||||||
|
desc.BoolParam(
|
||||||
|
name='debug',
|
||||||
|
label='Debug',
|
||||||
|
description='If checked, debug data will be generated',
|
||||||
|
value=False,
|
||||||
|
uid=[0],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
desc.File(
|
||||||
|
name='outputData',
|
||||||
|
label='Color checker data',
|
||||||
|
description='Output position and colorimetric data extracted from detected color checkers in the images',
|
||||||
|
value=desc.Node.internalFolder + '/ccheckers.json',
|
||||||
|
uid=[],
|
||||||
|
),
|
||||||
|
]
|
43
meshroom/ui/qml/Viewer/ColorCheckerEntity.qml
Normal file
43
meshroom/ui/qml/Viewer/ColorCheckerEntity.qml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import QtQuick 2.11
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// required for perspective transform
|
||||||
|
property real sizeX: 1675.0 // might be overrided in ColorCheckerViewer
|
||||||
|
property real sizeY: 1125.0 // might be overrided in ColorCheckerViewer
|
||||||
|
|
||||||
|
property var colors: null
|
||||||
|
property var window: null
|
||||||
|
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: canvas
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
width: parent.sizeX
|
||||||
|
height: parent.sizeY
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
border.width: Math.max(1, (4.0 / zoom))
|
||||||
|
border.color: "red"
|
||||||
|
|
||||||
|
transformOrigin: Item.TopLeft
|
||||||
|
transform: Matrix4x4 {
|
||||||
|
id: transformation
|
||||||
|
matrix: Qt.matrix4x4()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function transform(matrix) {
|
||||||
|
var m = matrix
|
||||||
|
transformation.matrix = Qt.matrix4x4(
|
||||||
|
m[0][0], m[0][1], 0, m[0][2],
|
||||||
|
m[1][0], m[1][1], 0, m[1][2],
|
||||||
|
0, 0, 1, 0,
|
||||||
|
m[2][0], m[2][1], 0, m[2][2] )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
meshroom/ui/qml/Viewer/ColorCheckerPane.qml
Normal file
42
meshroom/ui/qml/Viewer/ColorCheckerPane.qml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import QtQuick 2.11
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import Controls 1.0
|
||||||
|
|
||||||
|
FloatingPane {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var colors: null
|
||||||
|
|
||||||
|
clip: true
|
||||||
|
padding: 4
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: grid
|
||||||
|
|
||||||
|
spacing: 5
|
||||||
|
x: spacing
|
||||||
|
y: spacing
|
||||||
|
rows: 4
|
||||||
|
columns: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.colors
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: cell
|
||||||
|
width: root.width / grid.columns - grid.spacing * (grid.columns+1) / grid.columns
|
||||||
|
height: root.height / grid.rows - grid.spacing * (grid.rows+1) / grid.rows
|
||||||
|
color: Qt.rgba(modelData.r, modelData.g, modelData.b, 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
102
meshroom/ui/qml/Viewer/ColorCheckerViewer.qml
Normal file
102
meshroom/ui/qml/Viewer/ColorCheckerViewer.qml
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import QtQuick 2.11
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property url source: undefined
|
||||||
|
property var json: null
|
||||||
|
property var image: null
|
||||||
|
property var viewpoint: null
|
||||||
|
property real zoom: 1.0
|
||||||
|
|
||||||
|
// required for perspective transform
|
||||||
|
// Match theoretical values in AliceVision
|
||||||
|
// see https://github.com/alicevision/AliceVision/blob/68ab70bcbc3eb01b73dc8dea78c78d8b4778461c/src/software/utils/main_colorCheckerDetection.cpp#L47
|
||||||
|
readonly property real ccheckerSizeX: 1675.0
|
||||||
|
readonly property real ccheckerSizeY: 1125.0
|
||||||
|
|
||||||
|
// offset the cchecker top left corner to match the image top left corner
|
||||||
|
x: -image.width / 2 + ccheckerSizeX / 2
|
||||||
|
y: -image.height / 2 + ccheckerSizeY / 2
|
||||||
|
|
||||||
|
property var ccheckers: []
|
||||||
|
property int selectedCChecker: -1
|
||||||
|
|
||||||
|
Component.onCompleted: { readSourceFile(); }
|
||||||
|
onSourceChanged: { readSourceFile(); }
|
||||||
|
onViewpointChanged: { loadCCheckers(); }
|
||||||
|
property var updatePane: null
|
||||||
|
|
||||||
|
|
||||||
|
function getColors() {
|
||||||
|
if (ccheckers[selectedCChecker] === undefined)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (ccheckers[selectedCChecker].colors === undefined)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ccheckers[selectedCChecker].colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSourceFile() {
|
||||||
|
var xhr = new XMLHttpRequest;
|
||||||
|
// console.warn("readSourceFile: " + root.source)
|
||||||
|
xhr.open("GET", root.source);
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status == 200) {
|
||||||
|
try {
|
||||||
|
root.json = null;
|
||||||
|
// console.warn("readSourceFile: update json from " + root.source)
|
||||||
|
root.json = JSON.parse(xhr.responseText);
|
||||||
|
// console.warn("readSourceFile: root.json.checkers.length=" + root.json.checkers.length)
|
||||||
|
}
|
||||||
|
catch(exc)
|
||||||
|
{
|
||||||
|
console.warn("Failed to parse ColorCheckerDetection JSON file: " + source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadCCheckers();
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCCheckers() {
|
||||||
|
emptyCCheckers();
|
||||||
|
if (root.json === null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentImagePath = (root.viewpoint && root.viewpoint.attribute && root.viewpoint.attribute.childAttribute("path")) ? root.viewpoint.attribute.childAttribute("path").value : null
|
||||||
|
var viewId = (root.viewpoint && root.viewpoint.attribute && root.viewpoint.attribute.childAttribute("viewId")) ? root.viewpoint.attribute.childAttribute("viewId").value : null
|
||||||
|
for (var i = 0; i < root.json.checkers.length; i++) {
|
||||||
|
// Only load ccheckers for the current view
|
||||||
|
var checker = root.json.checkers[i]
|
||||||
|
if (checker.viewId == viewId ||
|
||||||
|
checker.imagePath == currentImagePath) {
|
||||||
|
var cpt = Qt.createComponent("ColorCheckerEntity.qml");
|
||||||
|
|
||||||
|
var obj = cpt.createObject(root, {
|
||||||
|
sizeX: root.ccheckerSizeX,
|
||||||
|
sizeY: root.ccheckerSizeY,
|
||||||
|
colors: root.json.checkers[i].colors
|
||||||
|
});
|
||||||
|
obj.transform(root.json.checkers[i].transform);
|
||||||
|
ccheckers.push(obj);
|
||||||
|
selectedCChecker = ccheckers.length-1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyCCheckers() {
|
||||||
|
for (var i = 0; i < ccheckers.length; i++)
|
||||||
|
ccheckers[i].destroy();
|
||||||
|
ccheckers = [];
|
||||||
|
selectedCChecker = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -342,6 +342,28 @@ FocusScope {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ColorCheckerViewer: display color checker detection results
|
||||||
|
// note: use a Loader to evaluate if a ColorCheckerDetection node exist and displayColorChecker checked at runtime
|
||||||
|
Loader {
|
||||||
|
id: colorCheckerViewerLoader
|
||||||
|
anchors.centerIn: parent
|
||||||
|
property var activeNode: _reconstruction.activeNodes.get("ColorCheckerDetection").node
|
||||||
|
active: (displayColorCheckerViewerLoader.checked && activeNode)
|
||||||
|
|
||||||
|
|
||||||
|
sourceComponent: ColorCheckerViewer {
|
||||||
|
visible: activeNode.isComputed && json !== undefined && imgContainer.image.status === Image.Ready
|
||||||
|
source: Filepath.stringToUrl(activeNode.attribute("outputData").value)
|
||||||
|
image: imgContainer.image
|
||||||
|
viewpoint: _reconstruction.selectedViewpoint
|
||||||
|
zoom: imgContainer.scale
|
||||||
|
|
||||||
|
updatePane: function() {
|
||||||
|
colorCheckerPane.colors = getColors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
@ -405,6 +427,17 @@ FocusScope {
|
||||||
metadata: visible ? root.metadata : {}
|
metadata: visible ? root.metadata : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ColorCheckerPane {
|
||||||
|
id: colorCheckerPane
|
||||||
|
width: 250
|
||||||
|
height: 170
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
visible: displayColorCheckerViewerLoader.checked && colorCheckerPane.colors !== null
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: msfmDataLoader
|
id: msfmDataLoader
|
||||||
|
|
||||||
|
@ -645,6 +678,30 @@ FocusScope {
|
||||||
enabled: activeNode && activeNode.attribute("useFisheye").value
|
enabled: activeNode && activeNode.attribute("useFisheye").value
|
||||||
visible: activeNode
|
visible: activeNode
|
||||||
}
|
}
|
||||||
|
MaterialToolButton {
|
||||||
|
id: displayColorCheckerViewerLoader
|
||||||
|
property var activeNode: _reconstruction.activeNodes.get('ColorCheckerDetection').node
|
||||||
|
ToolTip.text: "Display Color Checker: " + (activeNode ? activeNode.label : "No Node")
|
||||||
|
text: MaterialIcons.view_comfy //view_module grid_on gradient view_comfy border_all
|
||||||
|
font.pointSize: 11
|
||||||
|
Layout.minimumWidth: 0
|
||||||
|
checkable: true
|
||||||
|
enabled: activeNode && activeNode.isComputed && _reconstruction.selectedViewId != -1
|
||||||
|
checked: false
|
||||||
|
visible: activeNode
|
||||||
|
onEnabledChanged: {
|
||||||
|
if(enabled == false)
|
||||||
|
checked = false
|
||||||
|
}
|
||||||
|
onCheckedChanged: {
|
||||||
|
if(checked == true)
|
||||||
|
{
|
||||||
|
displaySfmDataGlobalStats.checked = false
|
||||||
|
displaySfmStatsView.checked = false
|
||||||
|
metadataCB.checked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MaterialToolButton {
|
MaterialToolButton {
|
||||||
id: displayLdrHdrCalibrationGraph
|
id: displayLdrHdrCalibrationGraph
|
||||||
|
@ -723,6 +780,7 @@ FocusScope {
|
||||||
if(checked == true) {
|
if(checked == true) {
|
||||||
displaySfmDataGlobalStats.checked = false
|
displaySfmDataGlobalStats.checked = false
|
||||||
metadataCB.checked = false
|
metadataCB.checked = false
|
||||||
|
displayColorCheckerViewerLoader.checked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -747,6 +805,7 @@ FocusScope {
|
||||||
if(checked == true) {
|
if(checked == true) {
|
||||||
displaySfmStatsView.checked = false
|
displaySfmStatsView.checked = false
|
||||||
metadataCB.checked = false
|
metadataCB.checked = false
|
||||||
|
displayColorCheckerViewerLoader.checked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -770,6 +829,7 @@ FocusScope {
|
||||||
{
|
{
|
||||||
displaySfmDataGlobalStats.checked = false
|
displaySfmDataGlobalStats.checked = false
|
||||||
displaySfmStatsView.checked = false
|
displaySfmStatsView.checked = false
|
||||||
|
displayColorCheckerViewerLoader.checked = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1070,10 +1070,10 @@ class Reconstruction(UIGraph):
|
||||||
vp = None
|
vp = None
|
||||||
if self.viewpoints:
|
if self.viewpoints:
|
||||||
vp = next((v for v in self.viewpoints if str(v.viewId.value) == self._selectedViewId), None)
|
vp = next((v for v in self.viewpoints if str(v.viewId.value) == self._selectedViewId), None)
|
||||||
self.setSelectedViewpoint(vp)
|
self._setSelectedViewpoint(vp)
|
||||||
self.selectedViewIdChanged.emit()
|
self.selectedViewIdChanged.emit()
|
||||||
|
|
||||||
def setSelectedViewpoint(self, viewpointAttribute):
|
def _setSelectedViewpoint(self, viewpointAttribute):
|
||||||
if self._selectedViewpoint:
|
if self._selectedViewpoint:
|
||||||
# Reconstruction has ownership of Viewpoint object - destroy it when not needed anymore
|
# Reconstruction has ownership of Viewpoint object - destroy it when not needed anymore
|
||||||
self._selectedViewpoint.deleteLater()
|
self._selectedViewpoint.deleteLater()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue