update:upload
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
# Xmake cache
|
||||
.xmake/
|
||||
build/
|
||||
images/
|
||||
output/
|
||||
.vscode/
|
||||
|
||||
# MacOS Cache
|
||||
.DS_Store
|
||||
|
||||
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<char>(std::tolower(c));
|
||||
});
|
||||
return s;
|
||||
}
|
||||
|
||||
static bool isImageFile(const fs::path& path)
|
||||
{
|
||||
const std::string ext = lower(path.extension().string());
|
||||
|
||||
static const std::set<std::string> extensions = {
|
||||
".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff"
|
||||
};
|
||||
|
||||
return extensions.count(ext) > 0;
|
||||
}
|
||||
|
||||
struct ViewData {
|
||||
std::string filename;
|
||||
std::vector<cv::Point2f> 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<fs::path> 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<ViewData> 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<cv::Point2f> 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<size_t>(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<size_t>(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<cv::Point3f> objectTemplate;
|
||||
|
||||
for (int row = 0; row < opt.rows; ++row) {
|
||||
for (int col = 0; col < opt.cols; ++col) {
|
||||
objectTemplate.emplace_back(
|
||||
static_cast<float>(col * opt.squareSize),
|
||||
static_cast<float>(row * opt.squareSize),
|
||||
0.0f
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<cv::Point3f>> objectPoints;
|
||||
std::vector<std::vector<cv::Point2f>> 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<cv::Mat> 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<double> 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<cv::Point2f> 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;
|
||||
}
|
||||
@@ -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
|
||||
--
|
||||
|
||||
Reference in New Issue
Block a user