diff --git a/src/aliceVision/track/TracksHandler.hpp b/src/aliceVision/track/TracksHandler.hpp index 6bd04b802c..7fa8262a56 100644 --- a/src/aliceVision/track/TracksHandler.hpp +++ b/src/aliceVision/track/TracksHandler.hpp @@ -21,6 +21,11 @@ class TracksHandler return _mapTracks; } + track::TracksMap & getAllTracksMutable() + { + return _mapTracks; + } + const track::TracksPerView & getTracksPerView() const { return _mapTracksPerView; diff --git a/src/software/utils/CMakeLists.txt b/src/software/utils/CMakeLists.txt index 3e5217797d..362872cf61 100644 --- a/src/software/utils/CMakeLists.txt +++ b/src/software/utils/CMakeLists.txt @@ -147,6 +147,32 @@ if(ALICEVISION_BUILD_SFM) aliceVision_cmdline Boost::program_options ) + + # Output intrinsics transformed images + alicevision_add_software(aliceVision_exportImages + SOURCE main_exportImages.cpp + FOLDER ${FOLDER_SOFTWARE_UTILS} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_image + aliceVision_sfmData + aliceVision_sfmDataIO + Boost::program_options + Boost::boost + ) + + # Output intrinsics transformed sfmdata + alicevision_add_software(aliceVision_intrinsicsTransforming + SOURCE main_intrinsicsTransforming.cpp + FOLDER ${FOLDER_SOFTWARE_UTILS} + LINKS aliceVision_system + aliceVision_cmdline + aliceVision_sfmData + aliceVision_sfmDataIO + aliceVision_track + Boost::program_options + Boost::boost + ) endif() # SfM quality evaluation diff --git a/src/software/utils/main_exportImages.cpp b/src/software/utils/main_exportImages.cpp new file mode 100644 index 0000000000..be23d58697 --- /dev/null +++ b/src/software/utils/main_exportImages.cpp @@ -0,0 +1,321 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 AliceVision contributors. +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include + +// These constants define the current software version. +// They must be updated when the command line is changed. +#define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 +#define ALICEVISION_SOFTWARE_VERSION_MINOR 0 + +using namespace aliceVision; + +namespace po = boost::program_options; +namespace fs = std::filesystem; + +using NameFunction = std::function; + +template +std::string to_string_with_zero_padding(const T& value, std::size_t total_length) +{ + auto str = std::to_string(value); + if (str.length() < total_length) + { + str.insert(str.front() == '-' ? 1 : 0, total_length - str.length(), '0'); + } + + return str; +} + +template +void ImageIntrinsicsTransform(const image::Image& imageIn, + const camera::IntrinsicBase & intrinsicSource, + const camera::IntrinsicBase & intrinsicOutput, + image::Image& image_ud, + T fillcolor, + const oiio::ROI& roi = oiio::ROI()) +{ + // There is distortion + const Vec2 center(imageIn.width() * 0.5, imageIn.height() * 0.5); + + int widthRoi = intrinsicOutput.w(); + int heightRoi = intrinsicOutput.h(); + int xOffset = 0; + int yOffset = 0; + if (roi.defined()) + { + widthRoi = roi.width(); + heightRoi = roi.height(); + xOffset = roi.xbegin; + yOffset = roi.ybegin; + } + + image_ud.resize(widthRoi, heightRoi, true, fillcolor); + const image::Sampler2d sampler; + +#pragma omp parallel for + for (int y = 0; y < heightRoi; ++y) + { + for (int x = 0; x < widthRoi; ++x) + { + const Vec2 undisto_pix(x + xOffset, y + yOffset); + + // compute coordinates with distortion + const Vec3 intermediate = intrinsicOutput.backProjectUnit(undisto_pix); + if (intermediate.z() < -0.2) continue; + const Vec2 disto_pix = intrinsicSource.project(intermediate.homogeneous(), true); + + // pick pixel if it is in the image domain + if (imageIn.contains(disto_pix(1), disto_pix(0))) + { + image_ud(y, x) = sampler(imageIn, disto_pix(1), disto_pix(0)); + } + } + } +} + +void processImage(const std::string& dstFileName, + const camera::IntrinsicBase & outputIntrinsic, + const camera::IntrinsicBase & sourceIntrinsic, + const oiio::ParamValueList& metadata, + const std::string& srcFileName, + bool evCorrection, + float exposureCompensation) +{ + image::Image image; + image::Image image_ud; + + readImage(srcFileName, image, image::EImageColorSpace::LINEAR); + + // exposure correction + if (evCorrection) + { + for (int pix = 0; pix < image.width() * image.height(); ++pix) + { + image(pix)[0] *= exposureCompensation; + image(pix)[1] *= exposureCompensation; + image(pix)[2] *= exposureCompensation; + } + } + + // undistort the image and save it + ImageIntrinsicsTransform(image, sourceIntrinsic, outputIntrinsic, image_ud, image::RGBAfColor(0.0)); + + //Write the result + writeImage(dstFileName, image_ud, image::ImageWriteOptions(), metadata); +} + +bool process(const sfmData::SfMData & input, + const sfmData::SfMData & target, + const NameFunction & namingFunction, + bool evCorrection, + size_t rangeStart, + size_t rangeEnd) +{ + rangeEnd = std::min(input.getViews().size(), rangeEnd); + + // for exposure correction + const double medianCameraExposure = input.getMedianCameraExposureSetting().getExposure(); + + for (int posImage = rangeStart; posImage < rangeEnd; posImage++) + { + auto viewsIt = input.getViews().begin(); + std::advance(viewsIt, posImage); + + //Retrieve view + IndexT viewId = viewsIt->first; + const sfmData::View & view = *viewsIt->second; + + //Retrieve intrinsic + IndexT intrinsicId = view.getIntrinsicId(); + const auto & intrinsic = input.getIntrinsic(intrinsicId); + + //Make sure the target sfm contains the same thing + const auto & targetIntrinsics = target.getIntrinsics(); + if (targetIntrinsics.find(intrinsicId) == targetIntrinsics.end()) + { + continue; + } + + const auto & targetIntrinsic = target.getIntrinsic(intrinsicId); + + //Retrieve image + std::string srcFileName = view.getImage().getImagePath(); + oiio::ParamValueList metadata = image::readImageMetadata(srcFileName); + + + // add exposure values to images metadata + const double cameraExposure = view.getImage().getCameraExposureSetting().getExposure(); + const double ev = std::log2(1.0 / cameraExposure); + const float exposureCompensation = float(medianCameraExposure / cameraExposure); + metadata.push_back(oiio::ParamValue("AliceVision:EV", float(ev))); + metadata.push_back(oiio::ParamValue("AliceVision:EVComp", exposureCompensation)); + + //Process Image + std::string outFileName = namingFunction(view); + + ALICEVISION_LOG_INFO("Process image " << srcFileName); + processImage(outFileName, + targetIntrinsic, + intrinsic, + metadata, + srcFileName, + evCorrection, + exposureCompensation); + } + + return true; +} + +int aliceVision_main(int argc, char* argv[]) +{ + // command-line parameters + std::string verboseLevel = system::EVerboseLevel_enumToString(system::Logger::getDefaultVerboseLevel()); + std::string inputSfmDataFilename; + std::string targetSfmDataFilename; + std::string outFolder; + std::string outImageFileTypeName = image::EImageFileType_enumToString(image::EImageFileType::EXR); + std::string namingMode = "frameid"; + int rangeStart = -1; + int rangeSize = 1; + bool evCorrection = false; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&inputSfmDataFilename)->required(), + "Input SfMData file.") + ("target,t", po::value(&targetSfmDataFilename)->required(), + "Target SfMData file.") + ("output,o", po::value(&outFolder)->required(), + "Output folder."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("outputFileType", po::value(&outImageFileTypeName)->default_value(outImageFileTypeName), + image::EImageFileType_informations().c_str()) + ("rangeStart", po::value(&rangeStart)->default_value(rangeStart), + "Range image index start.") + ("rangeSize", po::value(&rangeSize)->default_value(rangeSize), + "Range size.") + ("evCorrection", po::value(&evCorrection)->default_value(evCorrection), + "Correct exposure value.") + ("namingMode", po::value(&namingMode)->default_value(namingMode), + "naming mode."); + // clang-format on + + CmdLine cmdline("AliceVision prepareDenseScene"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + // set output file type + image::EImageFileType outputFileType = image::EImageFileType_stringToEnum(outImageFileTypeName); + + // Create output dir + if (!utils::exists(outFolder)) + { + fs::create_directory(outFolder); + } + + sfmDataIO::ESfMData flagsPart = sfmDataIO::ESfMData( + sfmDataIO::ESfMData::VIEWS | + sfmDataIO::ESfMData::INTRINSICS | + sfmDataIO::ESfMData::EXTRINSICS + ); + + // Read the input SfM scene + sfmData::SfMData inputSfmData; + if (!sfmDataIO::load(inputSfmData, inputSfmDataFilename, flagsPart)) + { + ALICEVISION_LOG_ERROR("The input SfMData file '" << inputSfmDataFilename << "' cannot be read."); + return EXIT_FAILURE; + } + + // Read the target SfM scene + sfmData::SfMData targetSfmData; + if (!sfmDataIO::load(targetSfmData, targetSfmDataFilename, flagsPart)) + { + ALICEVISION_LOG_ERROR("The target SfMData file '" << targetSfmDataFilename << "' cannot be read."); + return EXIT_FAILURE; + } + + int rangeEnd = inputSfmData.getViews().size(); + + // set range + if (rangeStart != -1) + { + if (rangeStart < 0 || rangeSize < 0) + { + ALICEVISION_LOG_ERROR("Range is incorrect"); + return EXIT_FAILURE; + } + + if (rangeStart + rangeSize > inputSfmData.getViews().size()) + { + rangeSize = inputSfmData.getViews().size() - rangeStart; + } + + rangeEnd = rangeStart + rangeSize; + + if (rangeSize <= 0) + { + ALICEVISION_LOG_WARNING("Nothing to compute."); + return EXIT_SUCCESS; + } + } + else + { + rangeStart = 0; + } + + NameFunction namingFunction; + + if (namingMode == "frameid") + { + namingFunction = [&outputFileType, outFolder](const sfmData::View & view) + { + const std::string baseFilename = to_string_with_zero_padding(view.getFrameId(), 10); + const std::string ext = image::EImageFileType_enumToString(outputFileType); + return (fs::path(outFolder) / (baseFilename + "." + ext)).string(); + }; + } + else if (namingMode == "viewid") + { + namingFunction = [&outputFileType, outFolder](const sfmData::View & view) + { + const std::string baseFilename = std::to_string(view.getViewId()); + const std::string ext = image::EImageFileType_enumToString(outputFileType); + return (fs::path(outFolder) / (baseFilename + "." + ext)).string(); + }; + } + else + { + namingFunction = [&outputFileType, outFolder](const sfmData::View & view) + { + const fs::path imagePath = fs::path(view.getImage().getImagePath()); + const std::string baseFilename = imagePath.stem().string(); + const std::string ext = image::EImageFileType_enumToString(outputFileType); + return (fs::path(outFolder) / (baseFilename + "." + ext)).string(); + }; + } + + if (!process(inputSfmData, targetSfmData, namingFunction, evCorrection, rangeStart, rangeEnd)) + { + ALICEVISION_LOG_ERROR("Process failed"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/software/utils/main_intrinsicsTransforming.cpp b/src/software/utils/main_intrinsicsTransforming.cpp new file mode 100644 index 0000000000..1980f6e4e8 --- /dev/null +++ b/src/software/utils/main_intrinsicsTransforming.cpp @@ -0,0 +1,318 @@ +// This file is part of the AliceVision project. +// Copyright (c) 2025 AliceVision contributors. +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// These constants define the current software version. +// They must be updated when the command line is changed. +#define ALICEVISION_SOFTWARE_VERSION_MAJOR 1 +#define ALICEVISION_SOFTWARE_VERSION_MINOR 0 + +using namespace aliceVision; + +namespace po = boost::program_options; +namespace fs = std::filesystem; + +/** + * @brief create a new SfmData where all the non pinhole stuff are removed + * For example, distortion/undistortion are removed + * @param sfmData the original sfmData + * @param outputSfmData the result sfmData + * @param fakeFov if one intrinsic is non pinhole, what is the required fov for the "fake" camera + * @return true if everything worked +*/ +bool convertToPinhole(const sfmData::SfMData & sfmData, + sfmData::SfMData & outputSfmData, + double fakeFov) +{ + outputSfmData.getIntrinsics().clear(); + + // Loop over all input intrinsics + for (const auto & [intrinsicId, intrinsicPtr] : sfmData.getIntrinsics()) + { + const auto & originalIntrinsic = *intrinsicPtr; + + bool isPinhole = camera::isPinhole(originalIntrinsic.getType()); + + //BY default, create a fake camera with a given fov + double hw = double(originalIntrinsic.w()) * 0.5; + double fx = hw / std::tan(fakeFov * 0.5); + double fy = fx; + double cx = 0.0; + double cy = 0.0; + + if (isPinhole) + { + //IF pinhone, recreate without distortion + const auto & pinhole = dynamic_cast(originalIntrinsic); + fx = pinhole.getScale().x(); + fy = pinhole.getScale().y(); + cx = pinhole.getOffset().x(); + cy = pinhole.getOffset().y(); + } + + std::shared_ptr fakecam = camera::createIntrinsic( + camera::EINTRINSIC::PINHOLE_CAMERA, + camera::DISTORTION_NONE, + camera::UNDISTORTION_NONE, + intrinsicPtr->w(), + intrinsicPtr->h(), + fx, fy, cx, cy + ); + + outputSfmData.getIntrinsics().insert({intrinsicId, fakecam}); + } + + return true; +} + +/** + * @brief create a new SfmData where all the intrinsics are converted to equirectangular + * For example, distortion/undistortion are removed + * @param sfmData the original sfmData + * @param outputSfmData the result sfmData + * @return true if everything worked +*/ +bool convertToEquirectangular(const sfmData::SfMData & sfmData, + sfmData::SfMData & outputSfmData) +{ + outputSfmData.getIntrinsics().clear(); + + // Loop over all input intrinsics + for (const auto & [intrinsicId, intrinsicPtr] : sfmData.getIntrinsics()) + { + const auto & originalIntrinsic = *intrinsicPtr; + + size_t minSize = std::min(originalIntrinsic.w(), originalIntrinsic.h()); + double fx = double(minSize) / M_PI; + double fy = fx; + + std::shared_ptr fakecam = camera::createIntrinsic( + camera::EINTRINSIC::EQUIRECTANGULAR_CAMERA, + camera::DISTORTION_NONE, + camera::UNDISTORTION_NONE, + minSize * 2, minSize, + fx, fy, 0, 0 + ); + + outputSfmData.getIntrinsics().insert({intrinsicId, fakecam}); + } + + return true; +} + +/** + * @brief Convert all obsrvation to simulate that they were observed using the new intrinsics + * @param sfmData the original sfmData + * @param outputSfmData the result sfmData + * @return true if everything worked +*/ +bool convertObservations(const sfmData::SfMData & sfmData, + sfmData::SfMData & outputSfmData) +{ + for (auto & [idLandmark, landmark] : outputSfmData.getLandmarks()) + { + //Copy observations and erase + const auto observationsCopy = landmark.getObservations(); + landmark.getObservations().clear(); + + for (const auto & [idView, obs] : observationsCopy) + { + //Ignore non reconstructed views + const auto & view = sfmData.getView(idView); + + //Undistort observation + IndexT intrinsicId = view.getIntrinsicId(); + if (intrinsicId == UndefinedIndexT) + { + continue; + } + + const auto & inputIntrinsic = sfmData.getIntrinsic(intrinsicId); + const auto & outputIntrinsic = outputSfmData.getIntrinsic(intrinsicId); + + const Vec3 intermediate = inputIntrinsic.backProjectUnit(obs.getCoordinates()); + const Vec2 undistorted = outputIntrinsic.project(intermediate.homogeneous(), false); + + if (undistorted.x() < 0 || undistorted.y() < 0) + { + continue; + } + + if (undistorted.x() >= outputIntrinsic.w() || undistorted.y() >= outputIntrinsic.h()) + { + continue; + } + + sfmData::Observation outputObservation = obs; + outputObservation.setCoordinates(undistorted); + landmark.getObservations()[idView] = outputObservation; + } + } + return true; +} + +/** + * @brief Convert all tracks to simulate that they were observed using the new intrinsics + * @param inputSfmData the original sfmData + * @param outputSfmData the sfmData with updated intrinsics + * @return true if everything worked +*/ +bool convertTracks(const sfmData::SfMData & inputSfmData, + const sfmData::SfMData & outputSfmData, + track::TracksHandler& tracksHandler) +{ + track::TracksMap & tracksMap = tracksHandler.getAllTracksMutable(); + const track::TracksPerView tpv = tracksHandler.getTracksPerView(); + + for (const auto & [viewId, trackids]: tpv) + { + const auto & view = inputSfmData.getView(viewId); + IndexT intrinsicId = view.getIntrinsicId(); + if (intrinsicId == UndefinedIndexT) + { + continue; + } + + const auto & inputIntrinsic = inputSfmData.getIntrinsic(intrinsicId); + const auto & outputIntrinsic = outputSfmData.getIntrinsic(intrinsicId); + + for (const auto & trackId : trackids) + { + auto & track = tracksMap.at(trackId); + auto & item = track.featPerView[viewId]; + + const Vec3 intermediate = inputIntrinsic.backProjectUnit(item.coords); + const Vec2 undistorted = outputIntrinsic.project(intermediate.homogeneous(), false); + + item.coords = undistorted; + } + } + + return true; +} + +int aliceVision_main(int argc, char* argv[]) +{ + // command-line parameters + std::string verboseLevel = system::EVerboseLevel_enumToString(system::Logger::getDefaultVerboseLevel()); + std::string inputSfmDataFilename; + std::string outputSfmDataFilename; + std::string inputTracksFilename; + std::string outputTracksFilename; + std::string cameraTypeStr; + double fakeFov = 90.0; + + // clang-format off + po::options_description requiredParams("Required parameters"); + requiredParams.add_options() + ("input,i", po::value(&inputSfmDataFilename)->required(), + "Input SfMData file.") + ("output,o", po::value(&outputSfmDataFilename)->required(), + "Output folder."); + + po::options_description optionalParams("Optional parameters"); + optionalParams.add_options() + ("fakeFov", po::value(&fakeFov)->default_value(fakeFov), + "Virtual FOV if output is pinhole and input is not.") + ("type", po::value(&cameraTypeStr)->default_value(cameraTypeStr), + "Default camera model type (pinhole, equidistant, equirectangular).") + ("inputTracks", po::value(&inputTracksFilename)->required(), + "Input Tracks file.") + ("outputTracks", po::value(&outputTracksFilename)->required(), + "Output Tracks file."); + // clang-format on + + CmdLine cmdline("AliceVision prepareDenseScene"); + cmdline.add(requiredParams); + cmdline.add(optionalParams); + if (!cmdline.execute(argc, argv)) + { + return EXIT_FAILURE; + } + + camera::EINTRINSIC cameraType = camera::EINTRINSIC::PINHOLE_CAMERA; + if (!cameraTypeStr.empty()) + { + cameraType = camera::EINTRINSIC_stringToEnum(cameraTypeStr); + } + + sfmDataIO::ESfMData flagsPart = sfmDataIO::ESfMData( + sfmDataIO::ESfMData::ALL + ); + + // Read the input SfM scene + sfmData::SfMData inputSfmData; + if (!sfmDataIO::load(inputSfmData, inputSfmDataFilename, flagsPart)) + { + ALICEVISION_LOG_ERROR("The input SfMData file '" << inputSfmDataFilename << "' cannot be read."); + return EXIT_FAILURE; + } + + sfmData::SfMData outputSfmData(inputSfmData); + if (cameraType == camera::EINTRINSIC::PINHOLE_CAMERA) + { + if (!convertToPinhole(inputSfmData, outputSfmData, fakeFov)) + { + ALICEVISION_LOG_ERROR("There was an error converting intrinsics"); + return EXIT_FAILURE; + } + } + else if (cameraType == camera::EINTRINSIC::EQUIRECTANGULAR_CAMERA) + { + if (!convertToEquirectangular(inputSfmData, outputSfmData)) + { + ALICEVISION_LOG_ERROR("There was an error converting intrinsics"); + return EXIT_FAILURE; + } + } + else + { + ALICEVISION_LOG_ERROR("Invalid camera model"); + return EXIT_FAILURE; + } + + if (!convertObservations(inputSfmData, outputSfmData)) + { + ALICEVISION_LOG_ERROR("There was an error converting observations"); + return EXIT_FAILURE; + } + + if (!inputTracksFilename.empty()) + { + // Load tracks + ALICEVISION_LOG_INFO("Load tracks"); + track::TracksHandler tracksHandler; + if (!tracksHandler.load(inputTracksFilename, inputSfmData.getViewsKeys())) + { + ALICEVISION_LOG_ERROR("The input tracks file '" + inputTracksFilename + "' cannot be read."); + return EXIT_FAILURE; + } + + if (!convertTracks(inputSfmData, outputSfmData, tracksHandler)) + { + ALICEVISION_LOG_ERROR("There was an error converting tracks"); + return EXIT_FAILURE; + } + } + + ALICEVISION_LOG_INFO("Export SfM: " << outputSfmDataFilename); + if (!sfmDataIO::save(outputSfmData, outputSfmDataFilename, flagsPart)) + { + ALICEVISION_LOG_ERROR("The output SfMData file '" << outputSfmDataFilename << "' cannot be written."); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}