commit 6de0b97d804329c8bb03004171d1d8be4d4d7669 Author: Jelly_Candy Date: Wed Oct 14 13:21:06 2026 +0800 update:upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32376aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Xmake cache +.xmake/ +build/ +images/ +output/ +.vscode/ + +# MacOS Cache +.DS_Store + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2a6e4c6 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,467 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +struct Options { + std::string inputDir = "占位符字段"; + std::string outputFile = "占位符字段.yml"; + std::string diagnosticsDir = "占位符字段"; + + // 内角点数量,不是黑白方格数量 + int cols = 占位符字段; + int rows = 占位符字段; + + // 每个方格的实际边长,单位 mm + double squareSize = 占位符字段; + + int minImages = 10; + bool visualize = false; + bool saveDiagnostics = true; +}; + +static void printUsage(const char* program) +{ + std::cout + << "OpenCV C++ Camera Calibrator\n\n" + << "Usage:\n" + << " " << program << " [options]\n\n" + << "Options:\n" + << " --input DIR Image directory (default: images)\n" + << " --output FILE Output YAML (default: " + "output/camera_intrinsics.yml)\n" + << " --cols N Inner corners per row (default: 9)\n" + << " --rows N Inner corners per column (default: 6)\n" + << " --square MM Square side length in mm (default: 25)\n" + << " --min-images N Minimum valid images (default: 10)\n" + << " --visualize Show detected corners interactively\n" + << " --no-diagnostics Do not save diagnostic images\n" + << " --help Show this help\n\n" + << "Example for a 10x7-square board with 25 mm squares:\n" + << " " << program + << " --input images --cols 9 --rows 6 --square 25\n"; +} + +static bool parseInt(const std::string& s, int& value) +{ + try { + size_t pos = 0; + int v = std::stoi(s, &pos); + if (pos != s.size()) + return false; + value = v; + return true; + } catch (...) { + return false; + } +} + +static bool parseDouble(const std::string& s, double& value) +{ + try { + size_t pos = 0; + double v = std::stod(s, &pos); + if (pos != s.size() || !std::isfinite(v)) + return false; + value = v; + return true; + } catch (...) { + return false; + } +} + +static bool parseArgs(int argc, char** argv, Options& opt) +{ + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + + if (arg == "--help") { + printUsage(argv[0]); + return false; + } else if (arg == "--visualize") { + opt.visualize = true; + } else if (arg == "--no-diagnostics") { + opt.saveDiagnostics = false; + } else if (arg == "--input" || + arg == "--output" || + arg == "--cols" || + arg == "--rows" || + arg == "--square" || + arg == "--min-images") { + + if (i + 1 >= argc) { + std::cerr << "Missing value for " << arg << '\n'; + return false; + } + + const std::string value = argv[++i]; + + if (arg == "--input") { + opt.inputDir = value; + } else if (arg == "--output") { + opt.outputFile = value; + } else if (arg == "--cols") { + if (!parseInt(value, opt.cols)) { + std::cerr << "Invalid --cols value\n"; + return false; + } + } else if (arg == "--rows") { + if (!parseInt(value, opt.rows)) { + std::cerr << "Invalid --rows value\n"; + return false; + } + } else if (arg == "--square") { + if (!parseDouble(value, opt.squareSize)) { + std::cerr << "Invalid --square value\n"; + return false; + } + } else if (arg == "--min-images") { + if (!parseInt(value, opt.minImages)) { + std::cerr << "Invalid --min-images value\n"; + return false; + } + } + } else { + std::cerr << "Unknown option: " << arg << '\n'; + return false; + } + } + + if (opt.cols < 2 || opt.rows < 2 || + opt.squareSize <= 0 || opt.minImages < 3) { + std::cerr << "Invalid parameters. Check cols, rows, square " + "and min-images.\n"; + return false; + } + + return true; +} + +static std::string lower(std::string s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return s; +} + +static bool isImageFile(const fs::path& path) +{ + const std::string ext = lower(path.extension().string()); + + static const std::set extensions = { + ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff" + }; + + return extensions.count(ext) > 0; +} + +struct ViewData { + std::string filename; + std::vector corners; + cv::Mat image; +}; + +int main(int argc, char** argv) +{ + Options opt; + + if (!parseArgs(argc, argv, opt)) { + return 1; + } + + const cv::Size boardSize(opt.cols, opt.rows); + + std::cout << "=== Camera Calibration ===\n" + << "Input directory : " << opt.inputDir << '\n' + << "Output file : " << opt.outputFile << '\n' + << "Inner corners : " << opt.cols + << " x " << opt.rows << '\n' + << "Square size : " << opt.squareSize << " mm\n" + << "Minimum images : " << opt.minImages << "\n\n"; + + if (!fs::exists(opt.inputDir) || + !fs::is_directory(opt.inputDir)) { + std::cerr << "Input directory does not exist: " + << opt.inputDir << '\n'; + return 2; + } + + std::vector files; + + for (const auto& entry : fs::directory_iterator(opt.inputDir)) { + if (entry.is_regular_file() && isImageFile(entry.path())) { + files.push_back(entry.path()); + } + } + + std::sort(files.begin(), files.end()); + + if (files.empty()) { + std::cerr << "No supported images found in " + << opt.inputDir << '\n'; + return 2; + } + + std::cout << "Found " << files.size() << " candidate images.\n"; + + std::vector views; + cv::Size imageSize; + int readFailures = 0; + int cornerFailures = 0; + int sizeFailures = 0; + + const cv::TermCriteria subpixCriteria( + cv::TermCriteria::EPS | cv::TermCriteria::MAX_ITER, + 40, 0.001 + ); + + for (const auto& path : files) { + cv::Mat image = cv::imread(path.string(), cv::IMREAD_COLOR); + + if (image.empty()) { + std::cout << "[READ FAIL] " << path.filename().string() + << '\n'; + ++readFailures; + continue; + } + + if (imageSize.empty()) { + imageSize = image.size(); + } else if (image.size() != imageSize) { + std::cout << "[SIZE MISMATCH] " + << path.filename().string() << " is " + << image.cols << "x" << image.rows + << "; expected " << imageSize.width + << "x" << imageSize.height << '\n'; + ++sizeFailures; + continue; + } + + cv::Mat gray; + cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY); + + std::vector corners; + + const bool found = cv::findChessboardCorners( + gray, + boardSize, + corners, + cv::CALIB_CB_ADAPTIVE_THRESH | + cv::CALIB_CB_NORMALIZE_IMAGE + ); + + if (!found || corners.size() != + static_cast(opt.cols * opt.rows)) { + std::cout << "[NO CORNERS] " + << path.filename().string() << '\n'; + ++cornerFailures; + continue; + } + + cv::cornerSubPix( + gray, + corners, + cv::Size(11, 11), + cv::Size(-1, -1), + subpixCriteria + ); + + ViewData view; + view.filename = path.filename().string(); + view.corners = corners; + view.image = image; + views.push_back(view); + + cv::Mat display = image.clone(); + cv::drawChessboardCorners( + display, boardSize, corners, true + ); + + std::cout << "[OK] " << view.filename << '\n'; + + if (opt.saveDiagnostics) { + fs::create_directories(opt.diagnosticsDir); + fs::path outPath = + fs::path(opt.diagnosticsDir) / + (path.stem().string() + "_corners.png"); + cv::imwrite(outPath.string(), display); + } + + if (opt.visualize) { + cv::imshow("Detected checkerboard", display); + const int key = cv::waitKey(0); + if (key == 27) { + cv::destroyAllWindows(); + break; + } + } + } + + if (opt.visualize) + cv::destroyAllWindows(); + + std::cout << "\n=== Detection Summary ===\n" + << "Total files : " << files.size() << '\n' + << "Valid views : " << views.size() << '\n' + << "Read failures : " << readFailures << '\n' + << "Corner failures : " << cornerFailures << '\n' + << "Size mismatches : " << sizeFailures << '\n'; + + if (views.size() < static_cast(opt.minImages)) { + std::cerr << "\nNot enough valid images. Need at least " + << opt.minImages << ", got " << views.size() + << ".\n"; + return 3; + } + + // 棋盘格位于 Z=0 平面,所有点使用同一单位。 + // squareSize 以 mm 输入,因此 tvecs 的单位也是 mm。 + std::vector objectTemplate; + + for (int row = 0; row < opt.rows; ++row) { + for (int col = 0; col < opt.cols; ++col) { + objectTemplate.emplace_back( + static_cast(col * opt.squareSize), + static_cast(row * opt.squareSize), + 0.0f + ); + } + } + + std::vector> objectPoints; + std::vector> imagePoints; + + for (const auto& view : views) { + objectPoints.push_back(objectTemplate); + imagePoints.push_back(view.corners); + } + + cv::Mat cameraMatrix = cv::initCameraMatrix2D( + objectPoints, imagePoints, imageSize + ); + + cv::Mat distCoeffs = cv::Mat::zeros(8, 1, CV_64F); + + std::vector rvecs, tvecs; + + const double rms = cv::calibrateCamera( + objectPoints, + imagePoints, + imageSize, + cameraMatrix, + distCoeffs, + rvecs, + tvecs, + 0, + cv::TermCriteria( + cv::TermCriteria::EPS | cv::TermCriteria::MAX_ITER, + 100, 1e-9 + ) + ); + + double totalSquaredError = 0.0; + size_t totalPoints = 0; + std::vector perViewErrors; + + std::cout << "\n=== Per-image Reprojection Error ===\n"; + std::cout << std::fixed << std::setprecision(4); + + for (size_t i = 0; i < views.size(); ++i) { + std::vector projected; + + cv::projectPoints( + objectPoints[i], + rvecs[i], + tvecs[i], + cameraMatrix, + distCoeffs, + projected + ); + + double squaredError = 0.0; + + for (size_t j = 0; j < projected.size(); ++j) { + const cv::Point2f delta = + imagePoints[i][j] - projected[j]; + + squaredError += delta.dot(delta); + } + + const double viewRms = std::sqrt( + squaredError / projected.size() + ); + + perViewErrors.push_back(viewRms); + totalSquaredError += squaredError; + totalPoints += projected.size(); + + std::cout << views[i].filename << " : " + << viewRms << " px\n"; + } + + const double globalRms = std::sqrt( + totalSquaredError / totalPoints + ); + + fs::create_directories( + fs::path(opt.outputFile).parent_path().empty() + ? fs::path(".") + : fs::path(opt.outputFile).parent_path() + ); + + cv::FileStorage out( + opt.outputFile, cv::FileStorage::WRITE + ); + + if (!out.isOpened()) { + std::cerr << "Cannot write output file: " + << opt.outputFile << '\n'; + return 4; + } + + out << "image_width" << imageSize.width; + out << "image_height" << imageSize.height; + out << "board_inner_corners_cols" << opt.cols; + out << "board_inner_corners_rows" << opt.rows; + out << "square_size_mm" << opt.squareSize; + out << "camera_matrix" << cameraMatrix; + out << "distortion_coefficients" << distCoeffs; + out << "opencv_rms" << rms; + out << "global_reprojection_error_px" << globalRms; + + out << "per_view_errors_px" << "["; + for (size_t i = 0; i < views.size(); ++i) { + out << "{" + << "filename" << views[i].filename + << "error_px" << perViewErrors[i] + << "}"; + } + out << "]"; + + out.release(); + + std::cout << "\n=== Calibration Result ===\n" + << "Image size: " << imageSize.width + << " x " << imageSize.height << '\n' + << "Camera matrix:\n" << cameraMatrix << '\n' + << "Distortion coefficients:\n" + << distCoeffs << '\n' + << "OpenCV RMS: " << rms << " px\n" + << "Global reprojection error: " + << globalRms << " px\n" + << "Saved to: " << opt.outputFile << '\n'; + + return 0; +} diff --git a/xmake.lua b/xmake.lua new file mode 100644 index 0000000..2ec0c04 --- /dev/null +++ b/xmake.lua @@ -0,0 +1,81 @@ +add_rules("mode.debug", "mode.release") + +set_languages("c++17") + +set_policy("generator.compile_commands", true) + +add_requires("cmake::OpenCV", {alias = "opencv", system = true}) + +target("camera_calibrator") + set_kind("binary") + add_packages("opencv") + add_files("src/*.cpp") +-- +-- If you want to known more usage about xmake, please see https://xmake.io +-- +-- ## FAQ +-- +-- You can enter the project directory firstly before building project. +-- +-- $ cd projectdir +-- +-- 1. How to build project? +-- +-- $ xmake +-- +-- 2. How to configure project? +-- +-- $ xmake f -p [macosx|linux|iphoneos ..] -a [x86_64|i386|arm64 ..] -m [debug|release] +-- +-- 3. Where is the build output directory? +-- +-- The default output directory is `./build` and you can configure the output directory. +-- +-- $ xmake f -o outputdir +-- $ xmake +-- +-- 4. How to run and debug target after building project? +-- +-- $ xmake run [targetname] +-- $ xmake run -d [targetname] +-- +-- 5. How to install target to the system directory or other output directory? +-- +-- $ xmake install +-- $ xmake install -o installdir +-- +-- 6. Add some frequently-used compilation flags in xmake.lua +-- +-- @code +-- -- add debug and release modes +-- add_rules("mode.debug", "mode.release") +-- +-- -- add macro definition +-- add_defines("NDEBUG", "_GNU_SOURCE=1") +-- +-- -- set warning all as error +-- set_warnings("all", "error") +-- +-- -- set language: c99, c++11 +-- set_languages("c99", "c++11") +-- +-- -- set optimization: none, faster, fastest, smallest +-- set_optimize("fastest") +-- +-- -- add include search directories +-- add_includedirs("/usr/include", "/usr/local/include") +-- +-- -- add link libraries and search directories +-- add_links("tbox") +-- add_linkdirs("/usr/local/lib", "/usr/lib") +-- +-- -- add system link libraries +-- add_syslinks("z", "pthread") +-- +-- -- add compilation and link flags +-- add_cxflags("-stdnolib", "-fno-strict-aliasing") +-- add_ldflags("-L/usr/local/lib", "-lpthread", {force = true}) +-- +-- @endcode +-- +