From fe1f6c1a76bfcb51f76292068792dcb6c626b270 Mon Sep 17 00:00:00 2001 From: Carl Philipp Klemm Date: Wed, 23 Jul 2025 18:07:23 +0200 Subject: [PATCH] inital commit --- CMakeLists.txt | 14 ++ main.cpp | 656 +++++++++++++++++++++++++++++++++++++++++++++++++ strnatcmp.h | 184 ++++++++++++++ 3 files changed, 854 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 main.cpp create mode 100644 strnatcmp.h diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d0edff9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 2.4) +project(scannassembler) + +include_directories("/usr/include/opencv4") + +add_executable(scannassembler main.cpp ) + +target_link_libraries( scannassembler -lopencv_core -lopencv_imgcodecs -lopencv_highgui -lopencv_aruco -lopencv_bgsegm -lopencv_calib3d -lopencv_ccalib -lopencv_datasets -lopencv_dnn -lopencv_dpm -lopencv_features2d -lopencv_flann -lopencv_freetype -lopencv_fuzzy -lopencv_hdf -lopencv_imgcodecs -lopencv_imgproc -lopencv_video -lopencv_videoio -lopencv_stitching -lopencv_xfeatures2d -lomp) +set_target_properties( scannassembler PROPERTIES COMPILE_FLAGS -m64 LINK_FLAGS -m64) +add_definitions(" -fopenmp -s -std=c++11 -Wall") + +set(EXECUTABLE_OUTPUT_PATH ./build) + +install(TARGETS scannassembler RUNTIME DESTINATION bin) diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..b917581 --- /dev/null +++ b/main.cpp @@ -0,0 +1,656 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "strnatcmp.h" + +bool verbose = false; + +class Config +{ +public: + std::vector inputImages; + std::string outputImage = "out.png"; + int gridSizeX = 11; + int gridSizeY = 10; + int sampleSize = 2; + float confidence = 1.5f; + int startSearch = 0; + bool warp = false; + float deltaX = -1; + float deltaY = -1; + bool illum = true; + bool seamAdjust = true; +}; + +void cd_to_exe_dir( char *argv[] ) +{ +std::string path = argv[0]; +int ii = path.length(); +while ( !( path[ii] == '/' || path[ii] == '\\' ) && ii > 0 ) +{ + ii--; +} +path.erase( ii, 100 ); +chdir( path.c_str() ); +} + +static void printUsage() +{ + std::cout<<"Usage: scannassembler [OPTION]... [FILES/DIRECTORY]...\n" + <<"This program assembles a regular grid of overlapping images into one large image. [FILES/DIRECTORY] is either a list of at least 4 files or a directory containing only images, with naturaly sorted file names.\n\n" + <<"Options:\n" + <<" -h, --help Display this help.\n" + <<" -o, --output Output image file name.\n" + <<" -v, --verbose Make application verbose.\n" + <<" -x, --xSize X size of grid.\n" + <<" -y, --ySize Y size of grid.\n" + <<" -c, --confidence Confidence of image feature matiching, default 1.2.\n" + <<" --start Image number where to start matching search\n" + <<" --warp Attempt to warp the image to correct for camera intrinsics. Defaults to off\n" + <<" --no-warp Disable warp.\n" + <<" -d, --delta [x] [y] Presett distance between images in pixels instead of automatic recognition\n" + <<" --no-seam blend only mode\n" + <<" --illum correct for differences in image brigeness. Defaults to on\n" + <<" --no-illum Disable image brigeness correction\n"; +} + +static int parseCmdArgs(int argc, char** argv, Config *config) +{ + if (argc == 1) + { + printUsage(); + return -1; + } + for (int i = 1; i < argc; i++) + { + std::string currentToken(argv[i]); + if (currentToken == "--help" || currentToken == "-h") + { + printUsage(); + return -1; + } + else if (currentToken == "--output" || currentToken == "-o") + { + i++; + if(argc > i) config->outputImage = argv[i]; + else return -1; + } + else if (currentToken == "--verbose" || currentToken == "-v") + { + verbose = true; + std::cout<<"Verbose mode on\n"; + } + else if (currentToken == "--xSize" || currentToken == "-x") + { + i++; + if(argc > i) config->gridSizeX = atoi(argv[i]); + else return -1; + } + else if (currentToken == "--ySize" || currentToken == "-y") + { + i++; + if(argc > i) config->gridSizeY = atoi(argv[i]); + else return -1; + } + else if (currentToken == "--confidence" || currentToken == "-c") + { + i++; + if(argc > i) config->confidence = atof(argv[i]); + else return -1; + } + else if (currentToken == "--start") + { + i++; + if(argc > i) config->startSearch = atoi(argv[i]); + else return -1; + } + else if (currentToken == "--delta" || currentToken == "-d") + { + if(argc > i+2) + { + config->deltaX = atof(argv[i+1]); + config->deltaY = atof(argv[i+2]); + i+=2; + } + else return -1; + } + else if (currentToken == "--warp") + { + config->warp = true; + } + else if (currentToken == "--no-warp") + { + config->warp = false; + } + else if (currentToken == "--illum") + { + config->illum = true; + } + else if (currentToken == "--no-illum") + { + config->illum = false; + } + else if (currentToken == "--no-seam") + { + config->seamAdjust = false; + } + else config->inputImages.push_back(argv[i]); + } + return 0; +} + +void showImages(std::vector images) +{ + cv::namedWindow( "Viewer", cv::WINDOW_AUTOSIZE ); + for(uint i = 0; i< images.size(); i++) + { + cv::imshow( "Viewer", images[i] ); + cv::waitKey(0); + } + cv::destroyWindow("Viewer"); +} + +void showImages(std::vector images) +{ + cv::namedWindow( "Viewer", cv::WINDOW_AUTOSIZE ); + for(uint i = 0; i< images.size(); i++) + { + cv::imshow( "Viewer", images[i] ); + cv::waitKey(0); + } + cv::destroyWindow("Viewer"); +} + +void showImages(cv::Mat image) +{ + cv::namedWindow( "Viewer", cv::WINDOW_AUTOSIZE ); + cv::imshow( "Viewer", image ); + cv::waitKey(0); + cv::destroyWindow("Viewer"); +} + +std::vector getSample(Config config, std::vector images, const int centralIndex, const int gridSizeX = 2, const int gridSizeY = 2) +{ + std::vector sampleImages; + if(images.size() < (uint)centralIndex+gridSizeX+gridSizeY*(config.gridSizeX)) return sampleImages; + + for(int y = 0; y < gridSizeY; y++) + { + for(int x = 0; x < gridSizeX; x++) + { + sampleImages.push_back(images[centralIndex+x+y*(config.gridSizeX)]); + } + } + return sampleImages; +} + +bool calculateCameraIntrinsics(std::vector sampleImages, std::vector* cameras, float confidence = 1.2f) +{ + std::vector features(sampleImages.size()); + + cv::Ptr featurefinder = cv::KAZE::create(); + for(uint i = 0; i < sampleImages.size(); i++) featurefinder->detectAndCompute(sampleImages[i], cv::Mat(), features[i].keypoints, features[i].descriptors); + + for(uint i = 0; i pairwiseMatches; + matcher(features, pairwiseMatches); + + std::vector ids = cv::detail::leaveBiggestComponent(features, pairwiseMatches, confidence); + if(ids.size() != sampleImages.size()) + { + std::cout<<"Match failure only "<size(); i++) + { + cv::Mat R; + cameras->at(i).R.convertTo(R, CV_32F); + cameras->at(i).R = R; + } + + + cv::detail::BundleAdjusterRay adjuster; + adjuster.setRefinementMask(cv::Mat::ones(3, 3, CV_8U)); + if(!adjuster(features, pairwiseMatches, *cameras)) + { + std::cout << "Camera parameters adjusting failed.\n"; + return false; + } + return true; +} + +std::vector processFileNames(std::vector fileNames, bool sort = true) +{ + if(fileNames.size() < 4) + { + if(verbose)std::cout<<"assuming directory\n"; + DIR *directory; + directory = opendir(fileNames[0].c_str()); + if(directory == NULL) + { + std::cout<<"Cant open directory "<d_type == DT_REG) fileNames.push_back(dirName + std::string(dirEntry->d_name)); + } + } + if(sort) std::sort(fileNames.begin(),fileNames.end(), compareNat); + if(verbose)for(unsigned i = 0; i < fileNames.size(); i++) std::cout< loadImages(std::vector fileNames) +{ + std::vector images; + for(uint i = 0; i < fileNames.size(); i++) + { + cv::Mat tmpImage = cv::imread(fileNames[i]); + if(tmpImage.data)images.push_back(tmpImage); + else + { + std::cout<<"can not read image "< lab_planes(3); + cv::split(lab_image, lab_planes); // now we have the L image in lab_planes[0] + + + + // apply the CLAHE algorithm to the L channel + cv::Ptr clahe = cv::createCLAHE(); + clahe->setClipLimit(clipLmt); + cv::Mat dst; + + clahe->apply(lab_planes[0], dst); + + // Merge the the color planes back into an Lab image + dst.copyTo(lab_planes[0]); + cv::merge(lab_planes, lab_image); + +// convert back to RGB + cv::Mat result; + cv::cvtColor(lab_image, result, cv::COLOR_Lab2BGR); + return result; +} + +cv::Mat accumulate( std::vector< cv::Mat > images, int type = -1 ) +{ + cv::Mat accBuffer = cv::Mat::zeros(images.at(0).size(), CV_64FC3); + for(uint i = 0; i< images.size(); i++) + { + cv::Mat tmp; + images[i].convertTo(tmp, CV_64FC3); + cv::add(tmp, accBuffer, accBuffer); + } + accBuffer = accBuffer / images.size(); + (type == -1) ? accBuffer.convertTo(accBuffer, images.at(0).type()) : accBuffer.convertTo(accBuffer, type); + return accBuffer; +} + +void removeMean(std::vector< cv::Mat >* images) +{ + cv::Mat acuumulated = accumulate(*images, CV_64FC3); + acuumulated = acuumulated - cv::mean(acuumulated); + for(uint i = 0; i< images->size(); i++) + { + cv::Mat tmp; + images->at(i).convertTo(tmp, CV_64FC3); + tmp = tmp - acuumulated; + tmp.convertTo(images->at(i), images->at(i).type()); + } +} + +cv::Point2f projectTopLeftPoint(cv::Size src_size, cv::InputArray K, cv::InputArray R, float flocal) +{ + float tl_uf = (std::numeric_limits::max)(); + float tl_vf = (std::numeric_limits::max)(); + + cv::detail::SphericalProjector projector; + + projector.scale = flocal; + + projector.setCameraParams(K,R); + + float u, v; + for (int y = 0; y < src_size.height; ++y) + { + for (int x = 0; x < src_size.width; ++x) + { + projector.mapForward(static_cast(x), static_cast(y), u, v); + tl_uf = (std::min)(tl_uf, u); + tl_vf = (std::min)(tl_vf, v); + } + } + + cv::Point2f result; + result.x = tl_uf; + result.y = tl_vf; + return result; +} + + + +cv::Point2f wheigtedMean( std::vector< std::vector > input ) +{ + //findDeltas + std::vector delta; + for(uint i = 0; i < input.size(); i++) + { + cv::Point2f tmp; + tmp.x = ((input[i][1].x-input[i][0].x)+(input[i][3].x-input[i][2].x))/2; + tmp.y = ((input[i][2].y-input[i][0].y)+(input[i][3].y-input[i][1].y))/2; + delta.push_back(tmp); + + } + + //compute Mean + cv::Point2f avDelta(0,0); + for(uint i = 0; i < delta.size(); i++) + { + avDelta.x = avDelta.x + delta[i].x; + avDelta.y = avDelta.y + delta[i].y; + } + avDelta.x = avDelta.x/delta.size(); + avDelta.y = avDelta.y/delta.size(); + std::cout<<"Mean Delta x: "< 0.2 || abs(delta[i].y-avDelta.y)/avDelta.y > 0.2 ) + { + std::cout<<"removing x: "< fileNames = processFileNames(config.inputImages); + if(fileNames.size() != (uint)config.gridSizeX*config.gridSizeY) + { + std::cout<<"This grid needs "< images = loadImages(fileNames); + if(images.size() != fileNames.size()) + { + images.size() == 0 ? std::cout<<"Could not read any images.\n" : std::cout<<"Could not read all images.\n"; + return -1; + } + + std::vector sizes(images.size()); + for(uint i = 0; i < images.size(); i++) sizes[i] = images[i].size(); + + cv::Point2f avDelta; + std::vector masks(images.size()); + std::vector imagesWarped(images.size()); + if(config.deltaX == -1 || config.deltaY == -1) + { + double avFocal = 1; + + //Caluclate camera Intrinsics + std::vector< std::vector > cameras; + { + std::vector samples; + int bottomLeftSampleImageIndex = config.startSearch; + int subsequentMatches = 0; + do + { + std::cout<<"trying set "< tmpCameras; + if(samples.size() != 0 && calculateCameraIntrinsics(samples, &tmpCameras, config.confidence)) + { + cameras.push_back(tmpCameras); + subsequentMatches++; + } + else subsequentMatches = 0; + + }while( samples.size() != 0 ); + } + if(cameras.size() == 0 ) + { + std::cout<<"Can not find match."; + return -1; + } + + //Caluclate mean focal length + for(uint i = 0; i < cameras.size(); i++) + { + for(uint u = 0; u < cameras[i].size(); u++) + { + avFocal += cameras[i][u].focal; + } + } + avFocal = avFocal/(cameras.size()*cameras[0].size()); + + //Caluclate Master Matrixes + cv::Mat_ avCameraMatrix = cv::Mat::zeros(3,3,CV_64F); + cv::Mat_ avRotationMatrix = cv::Mat::zeros(3,3,CV_64F); + for(uint i = 0; i< cameras.size(); i++) + { + for(uint u = 0; u < cameras[i].size(); u++) + { + cv::add(avCameraMatrix, cameras[i][u].K(), avCameraMatrix); + cv::add(avRotationMatrix, cameras[i][u].R, avRotationMatrix); + } + } + avCameraMatrix = avCameraMatrix/(cameras.size()*cameras[0].size()); + avRotationMatrix = avRotationMatrix/(cameras.size()*cameras[0].size()); + std::cout<<"Master Camera Intrinsics :\nK:\n" << avCameraMatrix << "\nR:\n" < > sampleCorners(cameras.size()); + for(uint i = 0; i < cameras.size(); i++) + { + sampleCorners[i] = std::vector(cameras[i].size()); + for(uint u = 0; u < cameras[i].size(); u++) + { + cv::Mat_ K; + cameras[i][u].K().convertTo(K, CV_32F); + cv::Mat_ R; + cameras[i][u].R.convertTo(R, CV_32F); + cv::Mat uxmap, uymap; + + sampleCorners[i][u] = projectTopLeftPoint(images[0].size(), K, R, avFocal); + std::cout<<"corner of set "< warperCreator = cv::makePtr(); + cv::Ptr warper = warperCreator->create(avFocal); + cv::Mat_ avCameraMatrix32f; + cv::Mat_ avRotationMatrix32f; + avCameraMatrix.convertTo(avCameraMatrix32f, CV_32F); + avRotationMatrix.convertTo(avRotationMatrix32f, CV_32F); + for (uint i = 0; i < images.size(); i++) + { + if(config.warp) warper->warp(images[i], avCameraMatrix32f, avRotationMatrix32f, cv::INTER_LINEAR, cv::BORDER_REFLECT, imagesWarped[i]); + images[i].release(); + } + //warp masks + for (uint i = 0; i < imagesWarped.size(); i++) + { + masks[i].create(sizes[i], CV_8U); + masks[i].setTo(cv::Scalar::all(255)); + warper->warp(masks[i], avCameraMatrix32f, avRotationMatrix32f, cv::INTER_LINEAR, cv::BORDER_REFLECT, masks[i]); + } + + } + else + { + for (uint i = 0; i < images.size(); i++) + { + masks[i].create(sizes[i], CV_8U); + masks[i].setTo(cv::Scalar::all(255)); + imagesWarped[i] = images[i]; + images[i].release(); + } + + } + } + else + { + avDelta.x = config.deltaX; + avDelta.y = config.deltaY; + for (uint i = 0; i < images.size(); i++) + { + imagesWarped[i] = images[i]; + masks[i].create(images[i].size(), CV_8U); + masks[i].setTo(cv::Scalar::all(255)); + images[i].release(); + } + } + + + //calculate Corners of all images + std::vector corners(imagesWarped.size()); + for(uint i = 0; i < imagesWarped.size(); i++) + { + cv::Point2i postionOfCurrentImage; + postionOfCurrentImage.x = i % (config.gridSizeX); + postionOfCurrentImage.y = (i - i % (config.gridSizeX))/config.gridSizeX; + if(verbose)std::cout<<"postition of image "< imagesWarped32f(imagesWarped.size()); + std::vector masksUmat(imagesWarped.size()); + for (uint i = 0; i < imagesWarped.size(); i++) + { + imagesWarped[i].convertTo(imagesWarped32f[i], CV_32F); + masks[i].copyTo(masksUmat[i]); + } + cv::detail::VoronoiSeamFinder seamFinder; + seamFinder.find(imagesWarped32f, corners, masksUmat); + for (uint i = 0; i < imagesWarped.size(); i++) + { + masksUmat[i].copyTo(masks[i]); + masksUmat[i].release(); + imagesWarped32f[i].release(); + } + imagesWarped32f.clear(); + masksUmat.clear(); + } + + + if(config.illum) + { + std::cout<<"Adjusting Illumination\n"; + for(uint i = 0; i < imagesWarped.size(); i++) imagesWarped[i] = illumCorrection(imagesWarped[i], 3); + } + std::cout<<"Removeing Mean\n"; + removeMean(&imagesWarped); + //showImages(accumulate(images)); + + std::cout<<"Composing\n"; + //compose + cv::Ptr blender; + blender = cv::detail::Blender::createDefault(cv::detail::Blender::Blender::MULTI_BAND, false); + cv::detail::MultiBandBlender* mb = dynamic_cast(blender.get()); + mb->setNumBands(5); + blender->prepare(corners, sizes); + for (uint i = 0; i < imagesWarped.size(); i++) + { + blender->feed(imagesWarped[i], masks[i], corners[i]); + } + cv::Mat result; + cv::Mat result_mask; + blender->blend(result, result_mask); + + result.convertTo(result, CV_8UC3); + cv::imwrite(config.outputImage, result); + + return 0; +} diff --git a/strnatcmp.h b/strnatcmp.h new file mode 100644 index 0000000..db21489 --- /dev/null +++ b/strnatcmp.h @@ -0,0 +1,184 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/*********************************************************************** + * Now let's make some changes: + * + * 1. std::string support + * 2. usable with std::sort and std::vectors + * + * ********************************************************************/ + +#ifndef __STRNATCMP_HPP +#define __STRNATCMP_HPP + +#include /* size_t */ +#include +#include + +typedef char nat_char; + +int strnatcmp(nat_char const *a, nat_char const *b); +int strnatcasecmp(nat_char const *a, nat_char const *b); + + +static inline int nat_isdigit(nat_char a){ + return isdigit((unsigned char) a); +} + + +static inline int nat_isspace(nat_char a){ + return isspace((unsigned char) a); +} + + +static inline nat_char nat_toupper(nat_char a){ + return toupper((unsigned char) a); +} + + +static int compare_right(nat_char const *a, nat_char const *b){ + int bias = 0; + + /* The longest run of digits wins. That aside, the greatest + value wins, but we can't know that it will until we've scanned + both numbers to know that they have the same magnitude, so we + remember it in BIAS. */ + for (;; a++, b++) { + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return bias; + if (!nat_isdigit(*a)) + return -1; + if (!nat_isdigit(*b)) + return +1; + if (*a < *b) { + if (!bias) + bias = -1; + } else if (*a > *b) { + if (!bias) + bias = +1; + } else if (!*a && !*b) + return bias; + } + + return 0; +} + + +static int compare_left(nat_char const *a, nat_char const *b){ + /* Compare two left-aligned numbers: the first to have a + different value wins. */ + for (;; a++, b++) { + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return 0; + if (!nat_isdigit(*a)) + return -1; + if (!nat_isdigit(*b)) + return +1; + if (*a < *b) + return -1; + if (*a > *b) + return +1; + } + + return 0; +} + + +static int strnatcmp0(nat_char const *a, nat_char const *b, int fold_case){ + int ai, bi; + nat_char ca, cb; + int fractional, result; + + ai = bi = 0; + while (1) { + ca = a[ai]; cb = b[bi]; + + /* skip over leading spaces or zeros */ + while (nat_isspace(ca)) + ca = a[++ai]; + + while (nat_isspace(cb)) + cb = b[++bi]; + + /* process run of digits */ + if (nat_isdigit(ca) && nat_isdigit(cb)) { + fractional = (ca == '0' || cb == '0'); + + if (fractional) { + if ((result = compare_left(a+ai, b+bi)) != 0) + return result; + } else { + if ((result = compare_right(a+ai, b+bi)) != 0) + return result; + } + } + + if (!ca && !cb) { + /* The strings compare the same. Perhaps the caller + will want to call strcmp to break the tie. */ + return 0; + } + + if (fold_case) { + ca = nat_toupper(ca); + cb = nat_toupper(cb); + } + + if (ca < cb) + return -1; + + if (ca > cb) + return +1; + + ++ai; ++bi; + } +} + +/*The following doesn't consider case of string*/ +int strnatcmp(nat_char const *a, nat_char const *b) { + return strnatcmp0(a, b, 0); +} + + +/* Compare, recognizing numeric string and ignoring case. + * This one is the general case where lower-case and upper- + * case are considered as same. + * */ +int strnatcasecmp(nat_char const *a, nat_char const *b) { + return strnatcmp0(a, b, 1); +} + +//You can use this function with std::sort and vector +bool compareNat(const std::string& a,const std::string& b){ + if(strnatcasecmp(a.c_str(),b.c_str())==-1) return true; + else return false; + } + +//You can use this function with qsort +int compareNatq(const void *a, const void *b){ + char const *pa = *(char const **)a, *pb = *(char const **)b; + return strnatcasecmp(pa, pb); +} + + +#endif