Merge pull request #1266 from ludchieng/dev/colorChecker

New ColorChecker Detection and Correction
This commit is contained in:
Fabien Castan 2021-04-26 16:42:55 +02:00 committed by GitHub
commit c876d2b20c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 391 additions and 2 deletions

View 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=[],
),
]

View 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=[],
),
]

View 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] )
}
}

View 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)
}
}
}
}
}

View 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;
}
}

View file

@ -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
}
}
}

View file

@ -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()