diff --git a/meshroom/aliceVision/SphereDetection.py b/meshroom/aliceVision/SphereDetection.py index 8d0d394649..618344e469 100644 --- a/meshroom/aliceVision/SphereDetection.py +++ b/meshroom/aliceVision/SphereDetection.py @@ -1,4 +1,4 @@ -__version__ = "1.0" +__version__ = "2.0" from meshroom.core import desc from meshroom.core.utils import VERBOSE_LEVEL @@ -31,45 +31,43 @@ class SphereDetection(desc.CommandLineNode): description="Automatic detection of calibration spheres.", value=False, ), + desc.Circle( + name="sphereShape", + label="Sphere Shape", + description="The shape of the calibration sphere for every image.", + enabled=lambda node: not node.autoDetect.value, + group=lambda node: None if node.sphereFile.value else "allParams", + keyable=True, + keyType="viewId", + ), + desc.File( + name="sphereFile", + label="Sphere Shape File", + description="An input JSON file containing the shapes for every image. If provided, " + "the shapes provided with \"Sphere Shape\" will be ignored.", + semantic="shapeFile", + value="", + enabled=lambda node: not node.autoDetect.value, + group=lambda node: None if not node.sphereFile.value else "allParams", + ), + desc.BoolParam( + name="fillMissingSpheres", + label="Fill Missing Spheres", + description="Checked if a sphere position is to be written as detected although it " + "was not provided. In that case, the position of the last known sphere " + "will be used.", + value=False, + enabled=lambda node: not node.autoDetect.value, + ), desc.FloatParam( name="minScore", label="Minimum Score", description="Minimum score for the detection.", value=0.0, range=(0.0, 50.0, 0.01), + enabled=lambda node: node.autoDetect.value, advanced=True, ), - desc.GroupAttribute( - name="sphereCenter", - label="Sphere Center", - description="Center of the circle (XY offset to the center of the image in pixels).", - groupDesc=[ - desc.FloatParam( - name="x", - label="x", - description="X offset in pixels.", - value=0.0, - range=(-1000.0, 10000.0, 1.0), - ), - desc.FloatParam( - name="y", - label="y", - description="Y offset in pixels.", - value=0.0, - range=(-1000.0, 10000.0, 1.0), - ), - ], - enabled=lambda node: not node.autoDetect.value, - group=None, # skip group from command line - ), - desc.FloatParam( - name="sphereRadius", - label="Radius", - description="Sphere radius in pixels.", - value=500.0, - range=(0.0, 10000.0, 0.1), - enabled=lambda node: not node.autoDetect.value, - ), desc.ChoiceParam( name="verboseLevel", label="Verbose Level", @@ -84,6 +82,7 @@ class SphereDetection(desc.CommandLineNode): name="output", label="Output Path", description="Sphere detection information will be written here.", + semantic="shapeFile", value="{nodeCacheFolder}/detection.json", ) ] diff --git a/meshroom/multi-viewPhotometricStereo.mg b/meshroom/multi-viewPhotometricStereo.mg index 3daf1f9666..47c2974554 100644 --- a/meshroom/multi-viewPhotometricStereo.mg +++ b/meshroom/multi-viewPhotometricStereo.mg @@ -1,6 +1,6 @@ { "header": { - "releaseVersion": "2025.1.0", + "releaseVersion": "2026.1.0+develop", "fileVersion": "2.0", "nodesVersions": { "CameraInit": "12.0", @@ -17,7 +17,7 @@ "PrepareDenseScene": "3.1", "SfMFilter": "1.0", "SfMTransfer": "2.1", - "SphereDetection": "1.0", + "SphereDetection": "2.0", "StructureFromMotion": "3.3", "Texturing": "6.0" }, diff --git a/meshroom/photometricStereo.mg b/meshroom/photometricStereo.mg index 2040916223..a090af6c40 100644 --- a/meshroom/photometricStereo.mg +++ b/meshroom/photometricStereo.mg @@ -1,13 +1,13 @@ { "header": { - "releaseVersion": "2025.1.0", + "releaseVersion": "2026.1.0+develop", "fileVersion": "2.0", "nodesVersions": { "CameraInit": "12.0", "CopyFiles": "1.3", "LightingCalibration": "1.0", "PhotometricStereo": "1.0", - "SphereDetection": "1.0" + "SphereDetection": "2.0" }, "template": true }, diff --git a/src/aliceVision/lightingEstimation/lightingCalibration.cpp b/src/aliceVision/lightingEstimation/lightingCalibration.cpp index 102c914aa0..656e0b07c9 100644 --- a/src/aliceVision/lightingEstimation/lightingCalibration.cpp +++ b/src/aliceVision/lightingEstimation/lightingCalibration.cpp @@ -70,9 +70,25 @@ void lightCalibration(const sfmData::SfMData& sfmData, // Main tree bpt::ptree fileTree; + // Read the json file and initialize the tree bpt::read_json(inputFile, fileTree); + // Spheres tree + bpt::ptree spheresTree; + + // Initialize spheres tree + const auto shapesTreeOpt = fileTree.get_child_optional("shapes"); + if (shapesTreeOpt && !shapesTreeOpt->empty()) + { + const auto& firstShapeTree = shapesTreeOpt->begin()->second; + spheresTree = firstShapeTree.get_child("observations"); + } + else + { + ALICEVISION_THROW_ERROR("Cannot find sphere detection data in '" << inputFile << "'."); + } + for (const auto& [currentId, currentView] : viewMap) { ALICEVISION_LOG_INFO("View Id: " << currentView.getViewId()); @@ -81,19 +97,20 @@ void lightCalibration(const sfmData::SfMData& sfmData, if (!boost::algorithm::icontains(imagePath.stem().string(), "ambient")) { std::string sphereName = std::to_string(currentView.getViewId()); - auto sphereExists = (fileTree.get_child_optional(sphereName)).is_initialized(); + auto sphereExists = (spheresTree.get_child_optional(sphereName)).is_initialized(); if (sphereExists) { ALICEVISION_LOG_INFO(" - " << imagePath.string()); imageList.push_back(imagePath.string()); std::array currentSphereParams; - for (auto& currentSphere : fileTree.get_child(sphereName)) { - currentSphereParams[0] = currentSphere.second.get_child("").get("x", 0.0); - currentSphereParams[1] = currentSphere.second.get_child("").get("y", 0.0); - currentSphereParams[2] = currentSphere.second.get_child("").get("r", 0.0); + const auto& currentSphere = spheresTree.get_child(sphereName); + currentSphereParams[0] = currentSphere.get("center.x", 0.0) - (currentView.getImage().getWidth() / 2); + currentSphereParams[1] = currentSphere.get("center.y", 0.0) - (currentView.getImage().getHeight() / 2); + currentSphereParams[2] = currentSphere.get("radius", 0.0); } + allSpheresParams.push_back(currentSphereParams); IndexT intrinsicId = currentView.getIntrinsicId(); diff --git a/src/aliceVision/sphereDetection/sphereDetection.cpp b/src/aliceVision/sphereDetection/sphereDetection.cpp index e1f94a7f6c..cea676bf88 100644 --- a/src/aliceVision/sphereDetection/sphereDetection.cpp +++ b/src/aliceVision/sphereDetection/sphereDetection.cpp @@ -9,6 +9,9 @@ // Standard libs #include #include +#include + +#include // AliceVision image library #include @@ -32,19 +35,39 @@ #include // Boost JSON -#include #include // SFMData #include #include -// namespaces -namespace bpt = boost::property_tree; - namespace aliceVision { namespace sphereDetection { +void fillShapeTree(bpt::ptree& fileTree, const bpt::ptree& spheresTree) +{ + bpt::ptree shapesTree; + { + // Shape tree + bpt::ptree shapeTree; + shapeTree.put("name", "Manual Sphere Detection"); + shapeTree.put("type", "Circle"); + + // Shape properties tree + bpt::ptree shapeProperties; + shapeProperties.put("color", "green"); + shapeTree.add_child("properties", shapeProperties); + + // Shape observations tree + shapeTree.add_child("observations", spheresTree); + + // Add shape tree to shapes tree + shapesTree.push_back(std::make_pair("", shapeTree)); + } + + fileTree.add_child("shapes", shapesTree); +} + void modelExplore(Ort::Session& session) { // Define allocator @@ -173,8 +196,8 @@ Prediction predict(Ort::Session& session, const fs::path imagePath, const float void sphereDetection(const sfmData::SfMData& sfmData, Ort::Session& session, fs::path outputPath, const float minScore) { - // Main tree - bpt::ptree fileTree; + // Spheres tree + bpt::ptree spheresTree; for (auto& viewID : sfmData.getViews()) { @@ -191,62 +214,184 @@ void sphereDetection(const sfmData::SfMData& sfmData, Ort::Session& session, fs: // If there is no bounding box, then no sphere has been detected if (pred.bboxes.size() > 0) { - bpt::ptree spheresNode; - // We only take the best sphere in the picture const int i = 0; // Compute sphere coords from bbox coords const auto bbox = pred.bboxes.at(i); const float r = std::min(bbox.at(3) - bbox.at(1), bbox.at(2) - bbox.at(0)) / 2; - const float x = bbox.at(0) + r - pred.size.width / 2; - const float y = bbox.at(1) + r - pred.size.height / 2; + const float x = bbox.at(0) + r; + const float y = bbox.at(1) + r; // Create an unnamed node containing the sphere bpt::ptree sphereNode; - sphereNode.put("x", x); - sphereNode.put("y", y); - sphereNode.put("r", r); + sphereNode.put("center.x", x); + sphereNode.put("center.y", y); + sphereNode.put("radius", r); sphereNode.put("score", pred.scores.at(i)); sphereNode.put("type", "matte"); - // Add sphere to array - spheresNode.push_back(std::make_pair("", sphereNode)); - - fileTree.add_child(sphereName, spheresNode); + // Add sphere node to spheres tree + spheresTree.add_child(sphereName, sphereNode); } else { ALICEVISION_LOG_WARNING("No sphere detected for '" << imagePath << "'."); } } + + // Main tree + bpt::ptree fileTree; + fillShapeTree(fileTree, spheresTree); + + // Write JSON bpt::write_json(outputPath.string(), fileTree); } -void writeManualSphereJSON(const sfmData::SfMData& sfmData, const std::array& sphereParam, fs::path outputPath) +bool writeManualSphereJSON(const sfmData::SfMData& sfmData, + const std::vector& x, + const std::vector& y, + const std::vector& radius, + fs::path outputPath, + bool fillMissingSpheres) { - // Main tree - bpt::ptree fileTree; + auto xValues = aliceVision::utils::dictStringToStringMap(x); + auto yValues = aliceVision::utils::dictStringToStringMap(y); + auto radiusValues = aliceVision::utils::dictStringToStringMap(radius); + + // Spheres tree + bpt::ptree spheresTree; for (auto& viewID : sfmData.getViews()) { - ALICEVISION_LOG_DEBUG("View Id: " << viewID); - + ALICEVISION_LOG_DEBUG("View ID: " << viewID); const std::string sphereName = std::to_string(viewID.second->getViewId()); - bpt::ptree spheresNode; + std::vector sphereParams; + auto pos = xValues.find(sphereName); + if (pos == xValues.end()) + { + ALICEVISION_LOG_INFO("Sphere shape for view ID " << sphereName << " not found."); + + if (fillMissingSpheres) + { + ALICEVISION_LOG_INFO("Using sphere position from view ID " << xValues.rbegin()->first << "."); + sphereParams = {std::stof(xValues.rbegin()->second), std::stof(yValues.rbegin()->second), std::stof(radiusValues.rbegin()->second)}; + } + } + else + { + ALICEVISION_LOG_DEBUG("Sphere shape for view ID " << sphereName << " found."); + sphereParams = {std::stof(xValues.at(sphereName)), std::stof(yValues.at(sphereName)), std::stof(radiusValues.at(sphereName))}; + } + // Create an unnamed node containing the sphere - bpt::ptree sphereNode; - sphereNode.put("x", sphereParam[0]); - sphereNode.put("y", sphereParam[1]); - sphereNode.put("r", sphereParam[2]); - sphereNode.put("type", "matte"); + if (!sphereParams.empty()) + { + bpt::ptree sphereNode; + sphereNode.put("center.x", sphereParams[0]); + sphereNode.put("center.y", sphereParams[1]); + sphereNode.put("radius", sphereParams[2]); + sphereNode.put("type", "matte"); + + // Add sphere node to spheres tree + spheresTree.add_child(sphereName, sphereNode); + } + } + + // Shapes tree + bpt::ptree shapesTree; + { + // Shape tree + bpt::ptree shapeTree; + shapeTree.put("name", "Manual Sphere Detection"); + shapeTree.put("type", "Circle"); + + // Shape properties tree + bpt::ptree shapeProperties; + shapeProperties.put("color", "green"); + shapeTree.add_child("properties", shapeProperties); - // Add sphere to array - spheresNode.push_back(std::make_pair("", sphereNode)); + // Shape observations tree + shapeTree.add_child("observations", spheresTree); - fileTree.add_child(sphereName, spheresNode); + // Add shape tree to shapes tree + shapesTree.push_back(std::make_pair("", shapeTree)); } + + // Main tree + bpt::ptree fileTree; + fileTree.add_child("shapes", shapesTree); + + // Write JSON bpt::write_json(outputPath.string(), fileTree); + + return true; +} + +bool writeManualSphereJSON(const sfmData::SfMData& sfmData, const std::string& sphereFile, const std::string& outputPath, bool fillMissingSpheres) +{ + if (!fillMissingSpheres) + { + // Copy the file as is if since there is no need to check on missing spheres + fs::copy_file(sphereFile, outputPath); + return true; + } + + // Main tree + bpt::ptree fileTree; + + // Read the json file and initialize the tree + bpt::read_json(sphereFile, fileTree); + + // Spheres tree + bpt::ptree spheresTree; + + // Initialize spheres tree + const auto shapesTreeOpt = fileTree.get_child_optional("shapes"); + if (shapesTreeOpt && !shapesTreeOpt->empty()) + { + const auto& firstShapeTree = shapesTreeOpt->begin()->second; + spheresTree = firstShapeTree.get_child("observations"); + } + else + { + ALICEVISION_THROW_ERROR("Cannot find sphere detection data in '" << sphereFile << "'."); + } + + std::string lastSphereViewID = spheresTree.rbegin()->first; + std::vector sphereParams = {spheresTree.rbegin()->second.get("center.x", 0.0f), + spheresTree.rbegin()->second.get("center.y", 0.0f), + spheresTree.rbegin()->second.get("radius", 0.0f)}; + + ALICEVISION_LOG_INFO("Got last known sphere position: " << lastSphereViewID); + + for (auto& viewID : sfmData.getViews()) + { + ALICEVISION_LOG_DEBUG("View ID: " << viewID); + const std::string sphereName = std::to_string(viewID.second->getViewId()); + + auto sphereExists = (spheresTree.get_child_optional(sphereName)).is_initialized(); + if (!sphereExists) + { + ALICEVISION_LOG_INFO("Sphere exists"); + bpt::ptree sphereNode; + sphereNode.put("center.x", sphereParams[0]); + sphereNode.put("center.y", sphereParams[1]); + sphereNode.put("radius", sphereParams[2]); + sphereNode.put("type", "matte"); + + // Add sphere node to spheres tree + spheresTree.add_child(sphereName, sphereNode); + } + } + + fileTree.clear(); + fillShapeTree(fileTree, spheresTree); + + // Write JSON + bpt::write_json(outputPath, fileTree); + + return true; } } // namespace sphereDetection diff --git a/src/aliceVision/sphereDetection/sphereDetection.hpp b/src/aliceVision/sphereDetection/sphereDetection.hpp index d244d97420..33e9b90b8c 100644 --- a/src/aliceVision/sphereDetection/sphereDetection.hpp +++ b/src/aliceVision/sphereDetection/sphereDetection.hpp @@ -12,6 +12,9 @@ // ONNXRuntime #include +// Boost Property Tree +#include + // SFMData #include #include @@ -23,6 +26,7 @@ namespace sphereDetection { // namespaces namespace fs = std::filesystem; +namespace bpt = boost::property_tree; struct Prediction { @@ -31,6 +35,8 @@ struct Prediction cv::Size size; }; +void fillShapeTree(bpt::ptree& fileTree, const bpt::ptree& spheresTree); + /** * @brief Print inputs and outputs of neural network, and checks the requirements * @param session The ONNXRuntime session @@ -48,13 +54,33 @@ void modelExplore(Ort::Session& session); void sphereDetection(const sfmData::SfMData& sfmData, Ort::Session& session, fs::path outputPath, const float minScore); /** - * @brief Write JSON for a hand-detected sphere + * @brief Write a JSON file containing the shapes for the hand-detected spheres. + * + * @param sfmData Input SfMData. + * @param x Flat vector of strings containing "view ID":"x-coordinate" pairs for the hand-detected spheres. + * @param y Flat vector of strings containing "view ID":"y-coordinate" pairs for the hand-detected spheres. + * @param radius Flat vector of strings containing "view ID":"radius" pairs for the hand-detected spheres. + * @param outputPath Path to the output JSON file. + * @param fillMissingSpheres If enabled, view IDs for which no sphere has been hand-detected will use the location of the last detected sphere. + * @return True if the JSON file was correctly written, False otherwise. + */ +bool writeManualSphereJSON(const sfmData::SfMData& sfmData, + const std::vector& x, + const std::vector& y, + const std::vector& radius, + fs::path outputPath, + bool fillMissingSpheres); + +/** + * @brief Write a JSON file containing the shapes for the spheres, based on a provided JSON file that contains sphere locations. * - * @param sfmData Input .sfm file - * @param sphereParam Parameters of the hand-detected sphere - * @return outputPath Path to the JSON file + * @param sfmData Input SfMData. + * @param sphereFile A JSON file containing the locations of the detected spheres. + * @param outputPath Path to the output JSON file. + * @param fillMissingSpheres If enabled, view IDs for which no sphere has been hand-detected will use the location of the last detected sphere. + * @return True if the JSON file was correctly written, False otherwise. */ -void writeManualSphereJSON(const sfmData::SfMData& sfmData, const std::array& sphereParam, fs::path outputPath); +bool writeManualSphereJSON(const sfmData::SfMData& sfmData, const std::string& sphereFile, const std::string& outputPath, bool fillMissingSpheres); } // namespace sphereDetection } // namespace aliceVision diff --git a/src/aliceVision/utils/convert.hpp b/src/aliceVision/utils/convert.hpp index 3c84fd4341..aaaba0d34e 100644 --- a/src/aliceVision/utils/convert.hpp +++ b/src/aliceVision/utils/convert.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -20,5 +21,53 @@ inline std::string toStringZeroPadded(std::size_t i, std::size_t zeroPadding) return ss.str(); } +/** + * @brief Converts a flat vector of strings representing key-value pairs + * into a std::map of string-to-string. + * + * @param dict A vector of strings where each pair of elements (i, i+1) + * represents a key and value, possibly with formatting characters. + * @return std::map A map containing cleaned key-value pairs. + * + * @code + * std::vector dict = {"{key1:", "value1,", "key2:", "value2}"}; + * auto result = dictStringToStringMap(dict); + * // result => { {"key1", "value1"}, {"key2", "value2"} } + * @endcode + */ +inline std::map dictStringToStringMap(std::vector dict) +{ + std::map stringMap; + + for (std::size_t i = 0; i < dict.size(); i += 2) + { + std::string keyString = std::string(dict[i]); + std::string valueString = std::string(dict[i + 1]); + + if (keyString[0] == '{') + { + keyString = keyString.substr(1, keyString.size() - 1); + } + if (keyString[keyString.size() - 1] == ':') + { + keyString = keyString.substr(0, keyString.size() - 1); + } + + if (valueString[valueString.size() - 1] == ',' || valueString[valueString.size() - 1] == '}') + { + valueString = valueString.substr(0, valueString.size() - 1); + } + + // Ensure both strings are not empty after being parsed. + // If one of them is, do not add the pair to the map. + if (keyString.size() > 0 && valueString.size() > 0) + { + stringMap.insert(std::pair(keyString, valueString)); + } + } + + return stringMap; +} + } // namespace utils } // namespace aliceVision diff --git a/src/software/pipeline/main_sphereDetection.cpp b/src/software/pipeline/main_sphereDetection.cpp index 313244693d..5559677ecb 100644 --- a/src/software/pipeline/main_sphereDetection.cpp +++ b/src/software/pipeline/main_sphereDetection.cpp @@ -29,7 +29,7 @@ #include -#define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 +#define ALICEVISION_SOFTWARE_VERSION_MAJOR 2 #define ALICEVISION_SOFTWARE_VERSION_MINOR 0 namespace fs = std::filesystem; @@ -47,8 +47,9 @@ int aliceVision_main(int argc, char** argv) float inputMinScore; bool autoDetect; - Eigen::Vector2f sphereCenterOffset(0, 0); - double sphereRadius = 1.0; + std::vector x, y, radius; + std::string sphereFile; + bool fillMissingSpheres; // clang-format off po::options_description requiredParams("Required parameters"); @@ -66,12 +67,16 @@ int aliceVision_main(int argc, char** argv) optionalParams.add_options() ("minScore,s", po::value(&inputMinScore)->default_value(0.0), "Minimum detection score.") - ("x,x", po::value(&sphereCenterOffset(0))->default_value(0.0), - "Sphere's center offset X (pixels).") - ("y,y", po::value(&sphereCenterOffset(1))->default_value(0.0), - "Sphere's center offset Y (pixels).") - ("sphereRadius,r", po::value(&sphereRadius)->default_value(1.0), - "Sphere's radius (pixels)."); + ("x,x", po::value>(&x)->multitoken(), + "Sphere's center X (pixels).") + ("y,y", po::value>(&y)->multitoken(), + "Sphere's center Y (pixels).") + ("radius,r", po::value>(&radius)->multitoken(), + "Sphere's radius (pixels).") + ("sphereFile,f", po::value(&sphereFile)->default_value(""), + "File containing the positions for the spheres in all the images.") + ("fillMissingSpheres,m", po::value(&fillMissingSpheres)->default_value(true), + "True if a sphere position is to be written as detected although it was not provided. In that case, the position of the last known sphere will be used."); // clang-format on CmdLine cmdline("AliceVision sphereDetection"); @@ -113,12 +118,17 @@ int aliceVision_main(int argc, char** argv) } else { - std::array sphereParam; - sphereParam[0] = sphereCenterOffset(0); - sphereParam[1] = sphereCenterOffset(1); - sphereParam[2] = sphereRadius; - - sphereDetection::writeManualSphereJSON(sfmData, sphereParam, fsOutputPath); + if (sphereFile.empty()) + { + sphereDetection::writeManualSphereJSON(sfmData, x, y, radius, fsOutputPath, fillMissingSpheres); + } + else + { + if (!sphereDetection::writeManualSphereJSON(sfmData, sphereFile, outputPath, fillMissingSpheres)) + { + return EXIT_FAILURE; + } + } } ALICEVISION_LOG_INFO("Task done in (s): " + std::to_string(timer.elapsed()));