mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-03 00:38:41 +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 {
|
||||
|
@ -405,6 +427,17 @@ FocusScope {
|
|||
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 {
|
||||
id: msfmDataLoader
|
||||
|
||||
|
@ -645,6 +678,30 @@ FocusScope {
|
|||
enabled: activeNode && activeNode.attribute("useFisheye").value
|
||||
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 {
|
||||
id: displayLdrHdrCalibrationGraph
|
||||
|
@ -723,6 +780,7 @@ FocusScope {
|
|||
if(checked == true) {
|
||||
displaySfmDataGlobalStats.checked = false
|
||||
metadataCB.checked = false
|
||||
displayColorCheckerViewerLoader.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -747,6 +805,7 @@ FocusScope {
|
|||
if(checked == true) {
|
||||
displaySfmStatsView.checked = false
|
||||
metadataCB.checked = false
|
||||
displayColorCheckerViewerLoader.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -770,6 +829,7 @@ FocusScope {
|
|||
{
|
||||
displaySfmDataGlobalStats.checked = false
|
||||
displaySfmStatsView.checked = false
|
||||
displayColorCheckerViewerLoader.checked = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1070,10 +1070,10 @@ class Reconstruction(UIGraph):
|
|||
vp = None
|
||||
if self.viewpoints:
|
||||
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()
|
||||
|
||||
def setSelectedViewpoint(self, viewpointAttribute):
|
||||
def _setSelectedViewpoint(self, viewpointAttribute):
|
||||
if self._selectedViewpoint:
|
||||
# Reconstruction has ownership of Viewpoint object - destroy it when not needed anymore
|
||||
self._selectedViewpoint.deleteLater()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue