Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projection adjustments #118

Merged
merged 46 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a21cf5c
prepare for saving computed camera parameters
krupkat Dec 30, 2023
e820109
enable saving camera parameters per pano
krupkat Dec 30, 2023
c986187
prepare for pano rotation
krupkat Dec 30, 2023
c5d9b83
basic rotation implementation
krupkat Dec 30, 2023
6b5880d
add helpers for (un)projecting points
krupkat Dec 31, 2023
7360b83
add rotate mode in sidebar
krupkat Dec 31, 2023
a3720e0
wip rotation widget
krupkat Dec 31, 2023
2db4666
add horizontal and vertical handles to rotation widget
krupkat Dec 31, 2023
2222593
wip code for rotation by dragging
krupkat Dec 31, 2023
a05732f
wip mouse cursor selection for rotation widget
krupkat Jan 1, 2024
7539758
better algorithm for distance to polyline
krupkat Jan 1, 2024
7b29ac7
fix for filtering points that cannot be unwarped
krupkat Jan 1, 2024
53df3dc
add auto detection of middle image for rotation widget
krupkat Jan 1, 2024
460e9e6
working pitch/yaw rotation
krupkat Jan 1, 2024
b15ef19
wip roll implementation
krupkat Jan 1, 2024
956a687
filter out polylines outside viewport
krupkat Jan 1, 2024
000c2d3
apply rotation when exiting rotate mode
krupkat Jan 1, 2024
171246d
auto apply rotation after dragging
krupkat Jan 1, 2024
85d02e9
version 0.18.0 prerelease
krupkat Jan 2, 2024
b333eb9
fix for build on ubuntu 20
krupkat Jan 2, 2024
a4d063b
gitignore + nix shell update
krupkat Feb 3, 2024
8e2c2da
compute roll axis at the image center
krupkat Feb 3, 2024
7c1345c
fix vertical projections
krupkat Feb 3, 2024
be79b32
allow cropping on previews
krupkat Feb 3, 2024
828bfd4
save crop rects with each pano
krupkat Feb 3, 2024
9d46591
switching between crop / rotate
krupkat Feb 3, 2024
31dadcc
move full res button
krupkat Feb 3, 2024
9e18d51
fix roll axis computation
krupkat Feb 3, 2024
c424966
better flow rotate -> crop
krupkat Feb 4, 2024
338a776
improve rotation widget handles positioning
krupkat Feb 4, 2024
c196e24
fix crop interactions
krupkat Feb 4, 2024
9f82305
add pitch and yaw axis computation
krupkat Feb 4, 2024
b5d2fa8
add roll handle
krupkat Feb 4, 2024
a2c43e7
add pitch + yaw rotation speed computation
krupkat Feb 4, 2024
0a1810c
fix mouse cursor selection when rotating
krupkat Feb 4, 2024
7830a96
add reset rotation action
krupkat Feb 4, 2024
b46de02
add reset crop action
krupkat Feb 4, 2024
cc497cc
split preview panel and widgets code (drag+rotate)
krupkat Feb 4, 2024
bb82b1b
update help message
krupkat Feb 4, 2024
30a6449
add rotate + crop menu options + shortcuts
krupkat Feb 5, 2024
8c2d510
split widgets (rotate + crop)
krupkat Feb 5, 2024
35fe980
tidy
krupkat Feb 5, 2024
1932cff
format
krupkat Feb 5, 2024
ae06ee8
more tidy
krupkat Feb 5, 2024
5448994
add tests
krupkat Feb 5, 2024
b5a9299
better drag directions + selecting mouse cursor
krupkat Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ CMakeSettings.json
_site
exiv2/
Xpano.app
.cache
.cache
.envrc
.direnv
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,15 @@ set(XPANO_SOURCES
"xpano/gui/panels/warning_pane.cc"
"xpano/gui/pano_gui.cc"
"xpano/gui/shortcut.cc"
"xpano/gui/widgets/drag.cc"
"xpano/gui/widgets/rotate.cc"
"xpano/pipeline/options.cc"
"xpano/pipeline/stitcher_pipeline.cc"
"xpano/utils/config.cc"
"xpano/utils/disjoint_set.cc"
"xpano/utils/exiv2.cc"
"xpano/utils/imgui_.cc"
"xpano/utils/opencv.cc"
"xpano/utils/path.cc"
"xpano/utils/resource.cc"
"xpano/utils/sdl_.cc"
Expand Down
1 change: 1 addition & 0 deletions misc/build/nix/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ pkgs.mkShell {
exiv2
dbus
(python3.withPackages (pkgs: with pkgs; [ pyyaml ]))
clang-tools_15
];
}
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ add_executable(StitcherTest
../xpano/pipeline/stitcher_pipeline.cc
../xpano/utils/disjoint_set.cc
../xpano/utils/exiv2.cc
../xpano/utils/opencv.cc
../xpano/utils/path.cc)

target_link_libraries(StitcherTest
Expand Down
66 changes: 66 additions & 0 deletions tests/stitcher_pipeline_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,49 @@ TEST_CASE("Stitcher pipeline defaults") {
CHECK(total_pixels == non_zero_pixels);
}

TEST_CASE("Stitcher pipeline defaults [extra results]") {
xpano::pipeline::StitcherPipeline<kReturnFuture> stitcher;

auto loading_task = stitcher.RunLoading(kInputs, {}, {});
auto result = loading_task.future.get();
auto progress = loading_task.progress->Report();
CHECK(progress.tasks_done == progress.num_tasks);

CHECK(result.images.size() == 10);
CHECK(result.matches.size() == 17);
REQUIRE(result.panos.size() == 2);
REQUIRE_THAT(result.panos[0].ids, Equals<int>({1, 2, 3, 4, 5}));
REQUIRE_THAT(result.panos[1].ids, Equals<int>({6, 7, 8}));

// preview
auto stitching_task0 = stitcher.RunStitching(result, {.pano_id = 0});

auto stitch_result0 = stitching_task0.future.get();
progress = stitching_task0.progress->Report();
CHECK(progress.tasks_done == progress.num_tasks);

CHECK(stitch_result0.pano.has_value());
CHECK(stitch_result0.auto_crop.has_value());
CHECK(stitch_result0.mask.has_value());
CHECK_FALSE(stitch_result0.export_path.has_value());
REQUIRE(stitch_result0.cameras.has_value());
CHECK(stitch_result0.cameras->cameras.size() == 5);

// full resolution
auto stitching_task1 =
stitcher.RunStitching(result, {.pano_id = 1, .full_res = true});
auto stitch_result1 = stitching_task1.future.get();
progress = stitching_task1.progress->Report();
CHECK(progress.tasks_done == progress.num_tasks);

CHECK(stitch_result1.pano.has_value());
CHECK(stitch_result1.auto_crop.has_value());
CHECK(stitch_result1.mask.has_value());
CHECK_FALSE(stitch_result1.export_path.has_value());
REQUIRE(stitch_result1.cameras.has_value());
CHECK(stitch_result1.cameras->cameras.size() == 3);
}

TEST_CASE("Stitcher pipeline single pano matching") {
xpano::pipeline::StitcherPipeline<kReturnFuture> stitcher;
auto loading_task = stitcher.RunLoading(
Expand Down Expand Up @@ -280,6 +323,29 @@ const std::vector<std::filesystem::path> kInputsWithExifMetadata = {
"data/image08.jpg",
};

TEST_CASE("Export [extra results]") {
const std::filesystem::path tmp_path =
xpano::tests::TmpPath().replace_extension("jpg");

xpano::pipeline::StitcherPipeline<kReturnFuture> stitcher;
auto loading_task = stitcher.RunLoading(kInputsWithExifMetadata, {}, {});
auto data = loading_task.future.get();
REQUIRE(data.panos.size() == 1);
auto stitch_result =
stitcher.RunStitching(data, {.pano_id = 0, .export_path = tmp_path})
.future.get();

CHECK(stitch_result.pano.has_value());
CHECK(stitch_result.auto_crop.has_value());
CHECK(stitch_result.mask.has_value());
CHECK(stitch_result.export_path.has_value());
REQUIRE(stitch_result.cameras.has_value());
CHECK(stitch_result.cameras->cameras.size() == 3);

REQUIRE(std::filesystem::exists(tmp_path));
std::filesystem::remove(tmp_path);
}

TEST_CASE("ExportWithMetadata") {
const std::filesystem::path tmp_path =
xpano::tests::TmpPath().replace_extension("jpg");
Expand Down
82 changes: 54 additions & 28 deletions xpano/algorithm/algorithm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@
#include <opencv2/photo.hpp>
#include <opencv2/stitching.hpp>
#include <opencv2/stitching/detail/matchers.hpp>
#include <opencv2/stitching/detail/warpers.hpp>
#include <opencv2/stitching/warpers.hpp>

#include "xpano/algorithm/auto_crop.h"
#include "xpano/algorithm/blenders.h"
#include "xpano/algorithm/image.h"
#include "xpano/algorithm/stitcher.h"
#include "xpano/algorithm/warpers.h"
#include "xpano/utils/disjoint_set.h"
#include "xpano/utils/rect.h"
#include "xpano/utils/threadpool.h"
Expand All @@ -38,6 +41,7 @@ void InsertInOrder(int value, std::vector<int>* vec) {
auto iter = std::lower_bound(vec->begin(), vec->end(), value);
vec->insert(iter, value);
}

cv::Ptr<cv::WarperCreator> PickWarper(ProjectionOptions options) {
cv::Ptr<cv::WarperCreator> warper_creator;
switch (options.type) {
Expand Down Expand Up @@ -74,25 +78,32 @@ cv::Ptr<cv::WarperCreator> PickWarper(ProjectionOptions options) {
return warper_creator;
}

std::optional<cv::RotateFlags> GetRotationFlags(
WaveCorrectionType wave_correction,
cv::detail::WaveCorrectKind wave_correction_auto) {
// have to rotate clockwise in case vertical wave correction was either
// selected by user or autodetected
switch (wave_correction) {
case WaveCorrectionType::kOff:
[[fallthrough]];
case WaveCorrectionType::kHorizontal:
return {};
case WaveCorrectionType::kVertical:
return cv::ROTATE_90_CLOCKWISE;
case WaveCorrectionType::kAuto:
cv::Ptr<cv::WarperCreator> PickWarperPortrait(ProjectionOptions options) {
// When vertical wave correction is detected / selected, the portrait
// variants of projections are used (if implemented)
cv::Ptr<cv::WarperCreator> warper_creator;
switch (options.type) {
case ProjectionType::kPerspective:
warper_creator = cv::makePtr<stitcher::PlanePortraitWarper>();
break;
case ProjectionType::kCylindrical:
warper_creator = cv::makePtr<stitcher::CylindricalPortraitWarper>();
break;
case ProjectionType::kSpherical:
warper_creator = cv::makePtr<stitcher::SphericalPortraitWarper>();
break;
case ProjectionType::kCompressedRectilinear:
warper_creator = cv::makePtr<cv::CompressedRectilinearPortraitWarper>(
options.a_param, options.b_param);
break;
case ProjectionType::kPanini:
warper_creator = cv::makePtr<cv::PaniniPortraitWarper>(options.a_param,
options.b_param);
break;
default:
break;
}
if (wave_correction_auto == cv::detail::WAVE_CORRECT_VERT) {
return cv::ROTATE_90_CLOCKWISE;
}
return {};
return warper_creator;
}

cv::Ptr<cv::FeatureDetector> PickFeaturesFinder(FeatureType feature) {
Expand Down Expand Up @@ -242,9 +253,11 @@ std::vector<Pano> FindPanos(const std::vector<Match>& matches,
}

StitchResult Stitch(const std::vector<cv::Mat>& images,
const std::optional<Cameras>& cameras,
StitchUserOptions user_options, StitchOptions options) {
auto stitcher = stitcher::Stitcher::Create(cv::Stitcher::PANORAMA);
stitcher->SetWarper(PickWarper(user_options.projection));
stitcher->SetPortraitWarper(PickWarperPortrait(user_options.projection));
stitcher->SetFeaturesFinder(PickFeaturesFinder(user_options.feature));
stitcher->SetFeaturesMatcher(cv::makePtr<cv::detail::BestOf2NearestMatcher>(
false, user_options.match_conf));
Expand All @@ -259,7 +272,17 @@ StitchResult Stitch(const std::vector<cv::Mat>& images,
stitcher->SetProgressMonitor(options.progress_monitor);

cv::Mat pano;
auto status = stitcher->Stitch(images, pano);
stitcher::Status status;

if (cameras &&
cameras->wave_correction_user == user_options.wave_correction &&
cameras->cameras.size() == images.size()) {
stitcher->SetWaveCorrectKind(cameras->wave_correction_auto);
stitcher->SetTransform(images, cameras->cameras);
status = stitcher->ComposePanorama(pano);
} else {
status = stitcher->Stitch(images, pano);
}

if (status != stitcher::Status::kSuccess) {
return {status, {}, {}};
Expand All @@ -270,16 +293,10 @@ StitchResult Stitch(const std::vector<cv::Mat>& images,
stitcher->ResultMask().copyTo(mask);
}

if (auto rotate = GetRotationFlags(user_options.wave_correction,
stitcher->WaveCorrectKind());
rotate) {
cv::rotate(pano, pano, *rotate);
if (options.return_pano_mask) {
cv::rotate(mask, mask, *rotate);
}
}

return {status, pano, mask};
auto result_cameras =
Cameras{stitcher->Cameras(), user_options.wave_correction,
stitcher->WaveCorrectKind(), stitcher->GetWarpHelper()};
return {status, pano, mask, std::move(result_cameras)};
}

int StitchTasksCount(int num_images) {
Expand Down Expand Up @@ -338,4 +355,13 @@ Pano SinglePano(int size) {
return pano;
}

Cameras Rotate(const Cameras& cameras, const cv::Mat& rotation_matrix) {
Cameras rotated = cameras;
for (auto& camera : rotated.cameras) {
camera.R = rotation_matrix * camera.R;
}

return rotated;
}

} // namespace xpano::algorithm
15 changes: 15 additions & 0 deletions xpano/algorithm/algorithm.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,20 @@

namespace xpano::algorithm {

struct Cameras {
std::vector<cv::detail::CameraParams> cameras;
WaveCorrectionType wave_correction_user; // set by user
cv::detail::WaveCorrectKind wave_correction_auto; // computed by OpenCV
stitcher::WarpHelper warp_helper;
};

struct Pano {
std::vector<int> ids;
bool exported = false;
std::optional<utils::RectRRf> crop;
std::optional<utils::RectRRf> auto_crop;
std::optional<Cameras> cameras;
std::optional<Cameras> backup_cameras;
};

struct Match {
Expand All @@ -47,6 +58,7 @@ struct StitchResult {
stitcher::Status status;
cv::Mat pano;
cv::Mat mask;
Cameras cameras;
};

struct StitchOptions {
Expand All @@ -56,6 +68,7 @@ struct StitchOptions {
};

StitchResult Stitch(const std::vector<cv::Mat>& images,
const std::optional<Cameras>& cameras,
StitchUserOptions user_options, StitchOptions options);

int StitchTasksCount(int num_images);
Expand All @@ -67,4 +80,6 @@ std::optional<utils::RectRRf> FindLargestCrop(const cv::Mat& mask);
cv::Mat Inpaint(const cv::Mat& pano, const cv::Mat& mask,
InpaintingOptions options);

Cameras Rotate(const Cameras& cameras, const cv::Mat& rotation_matrix);

} // namespace xpano::algorithm