Skip to content

Commit b19d27a

Browse files
authored
Projection adjustments (#118)
* prepare for saving computed camera parameters * enable saving camera parameters per pano * prepare for pano rotation * basic rotation implementation * add helpers for (un)projecting points * add rotate mode in sidebar * wip rotation widget * add horizontal and vertical handles to rotation widget * wip code for rotation by dragging * wip mouse cursor selection for rotation widget * better algorithm for distance to polyline * fix for filtering points that cannot be unwarped * add auto detection of middle image for rotation widget * working pitch/yaw rotation * wip roll implementation * filter out polylines outside viewport * apply rotation when exiting rotate mode * auto apply rotation after dragging * version 0.18.0 prerelease * fix for build on ubuntu 20 * gitignore + nix shell update * compute roll axis at the image center * fix vertical projections * allow cropping on previews * save crop rects with each pano * switching between crop / rotate * move full res button * fix roll axis computation * better flow rotate -> crop * improve rotation widget handles positioning * fix crop interactions * add pitch and yaw axis computation * add roll handle * add pitch + yaw rotation speed computation * fix mouse cursor selection when rotating * add reset rotation action * add reset crop action * split preview panel and widgets code (drag+rotate) * update help message * add rotate + crop menu options + shortcuts * split widgets (rotate + crop) * tidy * format * more tidy * add tests * better drag directions + selecting mouse cursor
1 parent 72b4917 commit b19d27a

30 files changed

+1460
-283
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ CMakeSettings.json
1616
_site
1717
exiv2/
1818
Xpano.app
19-
.cache
19+
.cache
20+
.envrc
21+
.direnv

CMakeLists.txt

+3
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ set(XPANO_SOURCES
6767
"xpano/gui/panels/warning_pane.cc"
6868
"xpano/gui/pano_gui.cc"
6969
"xpano/gui/shortcut.cc"
70+
"xpano/gui/widgets/drag.cc"
71+
"xpano/gui/widgets/rotate.cc"
7072
"xpano/pipeline/options.cc"
7173
"xpano/pipeline/stitcher_pipeline.cc"
7274
"xpano/utils/config.cc"
7375
"xpano/utils/disjoint_set.cc"
7476
"xpano/utils/exiv2.cc"
7577
"xpano/utils/imgui_.cc"
78+
"xpano/utils/opencv.cc"
7679
"xpano/utils/path.cc"
7780
"xpano/utils/resource.cc"
7881
"xpano/utils/sdl_.cc"

misc/build/nix/default.nix

+1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ pkgs.mkShell {
1313
exiv2
1414
dbus
1515
(python3.withPackages (pkgs: with pkgs; [ pyyaml ]))
16+
clang-tools_15
1617
];
1718
}

tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ add_executable(StitcherTest
3232
../xpano/pipeline/stitcher_pipeline.cc
3333
../xpano/utils/disjoint_set.cc
3434
../xpano/utils/exiv2.cc
35+
../xpano/utils/opencv.cc
3536
../xpano/utils/path.cc)
3637

3738
target_link_libraries(StitcherTest

tests/stitcher_pipeline_test.cc

+66
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,49 @@ TEST_CASE("Stitcher pipeline defaults") {
9696
CHECK(total_pixels == non_zero_pixels);
9797
}
9898

99+
TEST_CASE("Stitcher pipeline defaults [extra results]") {
100+
xpano::pipeline::StitcherPipeline<kReturnFuture> stitcher;
101+
102+
auto loading_task = stitcher.RunLoading(kInputs, {}, {});
103+
auto result = loading_task.future.get();
104+
auto progress = loading_task.progress->Report();
105+
CHECK(progress.tasks_done == progress.num_tasks);
106+
107+
CHECK(result.images.size() == 10);
108+
CHECK(result.matches.size() == 17);
109+
REQUIRE(result.panos.size() == 2);
110+
REQUIRE_THAT(result.panos[0].ids, Equals<int>({1, 2, 3, 4, 5}));
111+
REQUIRE_THAT(result.panos[1].ids, Equals<int>({6, 7, 8}));
112+
113+
// preview
114+
auto stitching_task0 = stitcher.RunStitching(result, {.pano_id = 0});
115+
116+
auto stitch_result0 = stitching_task0.future.get();
117+
progress = stitching_task0.progress->Report();
118+
CHECK(progress.tasks_done == progress.num_tasks);
119+
120+
CHECK(stitch_result0.pano.has_value());
121+
CHECK(stitch_result0.auto_crop.has_value());
122+
CHECK(stitch_result0.mask.has_value());
123+
CHECK_FALSE(stitch_result0.export_path.has_value());
124+
REQUIRE(stitch_result0.cameras.has_value());
125+
CHECK(stitch_result0.cameras->cameras.size() == 5);
126+
127+
// full resolution
128+
auto stitching_task1 =
129+
stitcher.RunStitching(result, {.pano_id = 1, .full_res = true});
130+
auto stitch_result1 = stitching_task1.future.get();
131+
progress = stitching_task1.progress->Report();
132+
CHECK(progress.tasks_done == progress.num_tasks);
133+
134+
CHECK(stitch_result1.pano.has_value());
135+
CHECK(stitch_result1.auto_crop.has_value());
136+
CHECK(stitch_result1.mask.has_value());
137+
CHECK_FALSE(stitch_result1.export_path.has_value());
138+
REQUIRE(stitch_result1.cameras.has_value());
139+
CHECK(stitch_result1.cameras->cameras.size() == 3);
140+
}
141+
99142
TEST_CASE("Stitcher pipeline single pano matching") {
100143
xpano::pipeline::StitcherPipeline<kReturnFuture> stitcher;
101144
auto loading_task = stitcher.RunLoading(
@@ -280,6 +323,29 @@ const std::vector<std::filesystem::path> kInputsWithExifMetadata = {
280323
"data/image08.jpg",
281324
};
282325

326+
TEST_CASE("Export [extra results]") {
327+
const std::filesystem::path tmp_path =
328+
xpano::tests::TmpPath().replace_extension("jpg");
329+
330+
xpano::pipeline::StitcherPipeline<kReturnFuture> stitcher;
331+
auto loading_task = stitcher.RunLoading(kInputsWithExifMetadata, {}, {});
332+
auto data = loading_task.future.get();
333+
REQUIRE(data.panos.size() == 1);
334+
auto stitch_result =
335+
stitcher.RunStitching(data, {.pano_id = 0, .export_path = tmp_path})
336+
.future.get();
337+
338+
CHECK(stitch_result.pano.has_value());
339+
CHECK(stitch_result.auto_crop.has_value());
340+
CHECK(stitch_result.mask.has_value());
341+
CHECK(stitch_result.export_path.has_value());
342+
REQUIRE(stitch_result.cameras.has_value());
343+
CHECK(stitch_result.cameras->cameras.size() == 3);
344+
345+
REQUIRE(std::filesystem::exists(tmp_path));
346+
std::filesystem::remove(tmp_path);
347+
}
348+
283349
TEST_CASE("ExportWithMetadata") {
284350
const std::filesystem::path tmp_path =
285351
xpano::tests::TmpPath().replace_extension("jpg");

xpano/algorithm/algorithm.cc

+54-28
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@
2121
#include <opencv2/photo.hpp>
2222
#include <opencv2/stitching.hpp>
2323
#include <opencv2/stitching/detail/matchers.hpp>
24+
#include <opencv2/stitching/detail/warpers.hpp>
25+
#include <opencv2/stitching/warpers.hpp>
2426

2527
#include "xpano/algorithm/auto_crop.h"
2628
#include "xpano/algorithm/blenders.h"
2729
#include "xpano/algorithm/image.h"
2830
#include "xpano/algorithm/stitcher.h"
31+
#include "xpano/algorithm/warpers.h"
2932
#include "xpano/utils/disjoint_set.h"
3033
#include "xpano/utils/rect.h"
3134
#include "xpano/utils/threadpool.h"
@@ -38,6 +41,7 @@ void InsertInOrder(int value, std::vector<int>* vec) {
3841
auto iter = std::lower_bound(vec->begin(), vec->end(), value);
3942
vec->insert(iter, value);
4043
}
44+
4145
cv::Ptr<cv::WarperCreator> PickWarper(ProjectionOptions options) {
4246
cv::Ptr<cv::WarperCreator> warper_creator;
4347
switch (options.type) {
@@ -74,25 +78,32 @@ cv::Ptr<cv::WarperCreator> PickWarper(ProjectionOptions options) {
7478
return warper_creator;
7579
}
7680

77-
std::optional<cv::RotateFlags> GetRotationFlags(
78-
WaveCorrectionType wave_correction,
79-
cv::detail::WaveCorrectKind wave_correction_auto) {
80-
// have to rotate clockwise in case vertical wave correction was either
81-
// selected by user or autodetected
82-
switch (wave_correction) {
83-
case WaveCorrectionType::kOff:
84-
[[fallthrough]];
85-
case WaveCorrectionType::kHorizontal:
86-
return {};
87-
case WaveCorrectionType::kVertical:
88-
return cv::ROTATE_90_CLOCKWISE;
89-
case WaveCorrectionType::kAuto:
81+
cv::Ptr<cv::WarperCreator> PickWarperPortrait(ProjectionOptions options) {
82+
// When vertical wave correction is detected / selected, the portrait
83+
// variants of projections are used (if implemented)
84+
cv::Ptr<cv::WarperCreator> warper_creator;
85+
switch (options.type) {
86+
case ProjectionType::kPerspective:
87+
warper_creator = cv::makePtr<stitcher::PlanePortraitWarper>();
88+
break;
89+
case ProjectionType::kCylindrical:
90+
warper_creator = cv::makePtr<stitcher::CylindricalPortraitWarper>();
91+
break;
92+
case ProjectionType::kSpherical:
93+
warper_creator = cv::makePtr<stitcher::SphericalPortraitWarper>();
94+
break;
95+
case ProjectionType::kCompressedRectilinear:
96+
warper_creator = cv::makePtr<cv::CompressedRectilinearPortraitWarper>(
97+
options.a_param, options.b_param);
98+
break;
99+
case ProjectionType::kPanini:
100+
warper_creator = cv::makePtr<cv::PaniniPortraitWarper>(options.a_param,
101+
options.b_param);
102+
break;
103+
default:
90104
break;
91105
}
92-
if (wave_correction_auto == cv::detail::WAVE_CORRECT_VERT) {
93-
return cv::ROTATE_90_CLOCKWISE;
94-
}
95-
return {};
106+
return warper_creator;
96107
}
97108

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

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

261274
cv::Mat pano;
262-
auto status = stitcher->Stitch(images, pano);
275+
stitcher::Status status;
276+
277+
if (cameras &&
278+
cameras->wave_correction_user == user_options.wave_correction &&
279+
cameras->cameras.size() == images.size()) {
280+
stitcher->SetWaveCorrectKind(cameras->wave_correction_auto);
281+
stitcher->SetTransform(images, cameras->cameras);
282+
status = stitcher->ComposePanorama(pano);
283+
} else {
284+
status = stitcher->Stitch(images, pano);
285+
}
263286

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

273-
if (auto rotate = GetRotationFlags(user_options.wave_correction,
274-
stitcher->WaveCorrectKind());
275-
rotate) {
276-
cv::rotate(pano, pano, *rotate);
277-
if (options.return_pano_mask) {
278-
cv::rotate(mask, mask, *rotate);
279-
}
280-
}
281-
282-
return {status, pano, mask};
296+
auto result_cameras =
297+
Cameras{stitcher->Cameras(), user_options.wave_correction,
298+
stitcher->WaveCorrectKind(), stitcher->GetWarpHelper()};
299+
return {status, pano, mask, std::move(result_cameras)};
283300
}
284301

285302
int StitchTasksCount(int num_images) {
@@ -338,4 +355,13 @@ Pano SinglePano(int size) {
338355
return pano;
339356
}
340357

358+
Cameras Rotate(const Cameras& cameras, const cv::Mat& rotation_matrix) {
359+
Cameras rotated = cameras;
360+
for (auto& camera : rotated.cameras) {
361+
camera.R = rotation_matrix * camera.R;
362+
}
363+
364+
return rotated;
365+
}
366+
341367
} // namespace xpano::algorithm

xpano/algorithm/algorithm.h

+15
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,20 @@
2323

2424
namespace xpano::algorithm {
2525

26+
struct Cameras {
27+
std::vector<cv::detail::CameraParams> cameras;
28+
WaveCorrectionType wave_correction_user; // set by user
29+
cv::detail::WaveCorrectKind wave_correction_auto; // computed by OpenCV
30+
stitcher::WarpHelper warp_helper;
31+
};
32+
2633
struct Pano {
2734
std::vector<int> ids;
2835
bool exported = false;
36+
std::optional<utils::RectRRf> crop;
37+
std::optional<utils::RectRRf> auto_crop;
38+
std::optional<Cameras> cameras;
39+
std::optional<Cameras> backup_cameras;
2940
};
3041

3142
struct Match {
@@ -47,6 +58,7 @@ struct StitchResult {
4758
stitcher::Status status;
4859
cv::Mat pano;
4960
cv::Mat mask;
61+
Cameras cameras;
5062
};
5163

5264
struct StitchOptions {
@@ -56,6 +68,7 @@ struct StitchOptions {
5668
};
5769

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

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

83+
Cameras Rotate(const Cameras& cameras, const cv::Mat& rotation_matrix);
84+
7085
} // namespace xpano::algorithm

0 commit comments

Comments
 (0)