Skip to content

Commit f00d510

Browse files
authored
Feature better auto crop (#48)
* switch auto crop code to right-open intervals so that size is computed correctly * better auto crop algorithm expands left/right based on area increase * improve algorithm with multiple start locations, add real test case * keep pano selected on full-res failure * add crop shadow * tidy
1 parent e7793f2 commit f00d510

File tree

8 files changed

+129
-61
lines changed

8 files changed

+129
-61
lines changed

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ target_include_directories(AutoCropTest PRIVATE
1818
".."
1919
)
2020

21+
copy_file(AutoCropTest ${CMAKE_CURRENT_SOURCE_DIR}/data/mask.png)
22+
2123
add_executable(StitcherTest
2224
stitcher_pipeline_test.cc
2325
../xpano/algorithm/algorithm.cc

tests/auto_crop_test.cc

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <catch2/catch_test_macros.hpp>
44
#include <opencv2/core.hpp>
5+
#include <opencv2/imgcodecs.hpp>
56

67
#include "xpano/utils/vec.h"
78

@@ -22,47 +23,47 @@ TEST_CASE("Auto crop full mask / even size") {
2223
auto result = FindLargestCrop(mask);
2324
REQUIRE(result.has_value());
2425
CHECK(result->start == Point2i{0, 0});
25-
CHECK(result->end == Point2i{19, 9});
26+
CHECK(result->end == Point2i{20, 10});
2627
}
2728

2829
TEST_CASE("Auto crop full mask / odd size") {
2930
cv::Mat mask(10, 21, CV_8U, cv::Scalar(kMaskValueOn));
3031
auto result = FindLargestCrop(mask);
3132
REQUIRE(result.has_value());
3233
CHECK(result->start == Point2i{0, 0});
33-
CHECK(result->end == Point2i{20, 9});
34+
CHECK(result->end == Point2i{21, 10});
3435
}
3536

3637
TEST_CASE("Auto crop single column mask") {
3738
cv::Mat mask(10, 1, CV_8U, cv::Scalar(kMaskValueOn));
3839
auto result = FindLargestCrop(mask);
3940
REQUIRE(result.has_value());
4041
CHECK(result->start == Point2i{0, 0});
41-
CHECK(result->end == Point2i{0, 9});
42+
CHECK(result->end == Point2i{1, 10});
4243
}
4344

4445
TEST_CASE("Auto crop two columns mask") {
4546
cv::Mat mask(10, 2, CV_8U, cv::Scalar(kMaskValueOn));
4647
auto result = FindLargestCrop(mask);
4748
REQUIRE(result.has_value());
4849
CHECK(result->start == Point2i{0, 0});
49-
CHECK(result->end == Point2i{1, 9});
50+
CHECK(result->end == Point2i{2, 10});
5051
}
5152

5253
TEST_CASE("Auto crop single row mask") {
5354
cv::Mat mask(1, 20, CV_8U, cv::Scalar(kMaskValueOn));
5455
auto result = FindLargestCrop(mask);
5556
REQUIRE(result.has_value());
5657
CHECK(result->start == Point2i{0, 0});
57-
CHECK(result->end == Point2i{19, 0});
58+
CHECK(result->end == Point2i{20, 1});
5859
}
5960

6061
TEST_CASE("Auto crop two rows mask") {
6162
cv::Mat mask(2, 20, CV_8U, cv::Scalar(kMaskValueOn));
6263
auto result = FindLargestCrop(mask);
6364
REQUIRE(result.has_value());
6465
CHECK(result->start == Point2i{0, 0});
65-
CHECK(result->end == Point2i{19, 1});
66+
CHECK(result->end == Point2i{20, 2});
6667
}
6768

6869
TEST_CASE("Auto crop mask with rows set") {
@@ -73,7 +74,7 @@ TEST_CASE("Auto crop mask with rows set") {
7374
auto result = FindLargestCrop(mask);
7475
REQUIRE(result.has_value());
7576
CHECK(result->start == Point2i{0, 5});
76-
CHECK(result->end == Point2i{19, 5});
77+
CHECK(result->end == Point2i{20, 6});
7778
}
7879

7980
SECTION("two rows") {
@@ -82,7 +83,7 @@ TEST_CASE("Auto crop mask with rows set") {
8283
auto result = FindLargestCrop(mask);
8384
REQUIRE(result.has_value());
8485
CHECK(result->start == Point2i{0, 5});
85-
CHECK(result->end == Point2i{19, 6});
86+
CHECK(result->end == Point2i{20, 7});
8687
}
8788
}
8889

@@ -91,10 +92,8 @@ TEST_CASE("Auto crop mask with empty column") {
9192
mask.col(5) = 0;
9293
auto result = FindLargestCrop(mask);
9394
REQUIRE(result.has_value());
94-
// Algorithm will stop when encountering empty column 5
95-
// this is to simplify the implementation
9695
CHECK(result->start == Point2i{6, 0});
97-
CHECK(result->end == Point2i{13, 9});
96+
CHECK(result->end == Point2i{20, 10});
9897
}
9998

10099
TEST_CASE("Auto crop empty matrix") {
@@ -114,7 +113,7 @@ II->1 1 1 1 1 1
114113
1 1 0 1 1 1
115114
IV
116115
*/
117-
TEST_CASE("Auto crop complex case") {
116+
TEST_CASE("Auto crop complex case I") {
118117
cv::Mat mask(6, 6, CV_8U, cv::Scalar(kMaskValueOn));
119118
mask.at<unsigned char>(0, 4) = 0;
120119
mask.at<unsigned char>(1, 2) = 0;
@@ -126,7 +125,35 @@ TEST_CASE("Auto crop complex case") {
126125
auto result = FindLargestCrop(mask);
127126
REQUIRE(result.has_value());
128127
CHECK(result->start == Point2i{1, 2});
129-
CHECK(result->end == Point2i{4, 4});
128+
CHECK(result->end == Point2i{5, 5});
129+
}
130+
131+
/* X
132+
1 1 1 1 1 1 X
133+
1 1 1 1 1 1
134+
1 1 0 1 1 1
135+
0 1 1 1 1 1
136+
1 1 1 1 1 1
137+
1 1 1 1 1 1 X
138+
X
139+
*/
140+
TEST_CASE("Auto crop complex case II") {
141+
cv::Mat mask(6, 6, CV_8U, cv::Scalar(kMaskValueOn));
142+
mask.at<unsigned char>(2, 2) = 0;
143+
mask.at<unsigned char>(3, 0) = 0;
144+
145+
auto result = FindLargestCrop(mask);
146+
REQUIRE(result.has_value());
147+
CHECK(result->start == Point2i{3, 0});
148+
CHECK(result->end == Point2i{6, 6});
149+
}
150+
151+
TEST_CASE("Real life example") {
152+
auto mask = cv::imread("mask.png", cv::IMREAD_UNCHANGED);
153+
auto result = FindLargestCrop(mask);
154+
REQUIRE(result.has_value());
155+
CHECK(result->start == Point2i{67, 659});
156+
CHECK(result->end == Point2i{5985, 2950});
130157
}
131158

132159
// NOLINTEND(readability-magic-numbers)

tests/data/mask.png

41.5 KB
Loading

xpano/algorithm/auto_crop.cc

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
#include <vector>
66

77
#include <opencv2/core.hpp>
8-
#include <spdlog/spdlog.h>
98

9+
#include "xpano/constants.h"
1010
#include "xpano/utils/rect.h"
1111
#include "xpano/utils/vec.h"
1212

@@ -18,6 +18,8 @@ struct Line {
1818
int end;
1919
};
2020

21+
bool IsLineValid(const Line& line) { return line.start < line.end; }
22+
2123
int Length(const Line& line) { return line.end - line.start; }
2224

2325
bool IsSet(unsigned char value) { return value == kMaskValueOn; }
@@ -27,19 +29,19 @@ std::optional<Line> FindLongestLineInColumn(cv::Mat column) {
2729
std::optional<Line> longest_line;
2830

2931
if (IsSet(column.at<unsigned char>(0, 0))) {
30-
current_line = Line{0, 0};
32+
current_line = Line{0, 1};
3133
}
3234

3335
for (int i = 1; i < column.rows; i++) {
3436
auto prev = IsSet(column.at<unsigned char>(i - 1, 0));
3537
auto current = IsSet(column.at<unsigned char>(i, 0));
3638

3739
if (!prev && current) {
38-
current_line = Line{i, i};
40+
current_line = Line{i, i + 1};
3941
}
4042

4143
if (prev && current) {
42-
current_line->end = i;
44+
current_line->end = i + 1;
4345
}
4446

4547
if (prev && !current) {
@@ -59,62 +61,77 @@ std::optional<Line> FindLongestLineInColumn(cv::Mat column) {
5961
return longest_line;
6062
}
6163

64+
std::optional<utils::RectPPi> FindLargestCrop(const std::vector<Line>& lines,
65+
const Line& invalid_line,
66+
int seed) {
67+
if (!IsLineValid(lines[seed])) {
68+
return {};
69+
}
70+
auto current_rect =
71+
utils::RectPPi{{seed, lines[seed].start}, {seed + 1, lines[seed].end}};
72+
auto largest_rect = current_rect;
73+
74+
if (lines.size() == 1) {
75+
return largest_rect;
76+
}
77+
78+
int left = seed - 1;
79+
int right = seed + static_cast<int>(lines.size() % 2);
80+
auto left_line = lines[left];
81+
auto right_line = lines[right];
82+
83+
while (IsLineValid(left_line) || IsLineValid(right_line)) {
84+
auto left_rect = utils::RectPPi{
85+
{left, std::max(left_line.start, current_rect.start[1])},
86+
{current_rect.end[0], std::min(left_line.end, current_rect.end[1])}};
87+
88+
auto right_rect = utils::RectPPi{
89+
{current_rect.start[0],
90+
std::max(right_line.start, current_rect.start[1])},
91+
{right + 1, std::min(right_line.end, current_rect.end[1])}};
92+
93+
if (utils::Area(left_rect) > utils::Area(right_rect)) {
94+
current_rect = left_rect;
95+
left_line = (left == 0) ? invalid_line : lines[--left];
96+
} else {
97+
current_rect = right_rect;
98+
right_line = (right == lines.size() - 1) ? invalid_line : lines[++right];
99+
}
100+
101+
if (utils::Area(current_rect) >= utils::Area(largest_rect)) {
102+
largest_rect = current_rect;
103+
}
104+
}
105+
106+
return largest_rect;
107+
}
108+
62109
} // namespace
63110

64111
// Approximage solution only.
65112
// Full solution would be https://stackoverflow.com/questions/2478447
66-
// This algorithm starts in the middle and expands the rectangle simultaneously
67-
// to the left and right. For the sake of simplicity if an empty column is
68-
// encountered, returns the current largest rectangle.
113+
// This algorithm starts in multiple sampled locations and expands
114+
// the rectangles in the direction with the larger area.
69115
std::optional<utils::RectPPi> FindLargestCrop(const cv::Mat& mask) {
116+
if (mask.empty()) {
117+
return {};
118+
}
70119
Line invalid_line = {mask.rows, 0};
71120
std::vector<Line> lines(mask.cols);
72121
for (int i = 0; i < mask.cols; i++) {
73122
auto longest_line = FindLongestLineInColumn(mask.col(i));
74123
lines[i] = longest_line.value_or(invalid_line);
75124
}
76125

77-
int half_size = mask.cols / 2;
78-
auto is_line_valid = [](const Line& line) { return line.start <= line.end; };
79-
if (mask.cols == 0 || !is_line_valid(lines[half_size])) {
80-
return {};
81-
}
82-
auto current_rect = utils::RectPPi{{half_size, lines[half_size].start},
83-
{half_size, lines[half_size].end}};
84-
auto largest_rect = current_rect;
126+
std::optional<utils::RectPPi> largest_rect;
85127

86-
int left_start = half_size - 1;
87-
int right_start = half_size + mask.cols % 2;
88-
for (int i = 0; i < half_size; i++) {
89-
auto left = left_start - i;
90-
auto right = right_start + i;
91-
auto left_line = lines[left];
92-
auto right_line = lines[right];
93-
94-
if (!is_line_valid(left_line)) {
95-
spdlog::warn("Auto crop: empty panorama at x = {}", left);
96-
return largest_rect;
97-
}
128+
int num_samples = 1 + mask.cols / kAutoCropSamplingDistance;
129+
for (int i = 0; i < num_samples; i++) {
130+
int start = (i + 1) * mask.cols / (num_samples + 1);
131+
auto current_rect = FindLargestCrop(lines, invalid_line, start);
98132

99-
if (!is_line_valid(right_line)) {
100-
spdlog::warn("Auto crop: empty panorama at x = {}", right);
101-
return largest_rect;
102-
}
103-
104-
current_rect.start[0] = left;
105-
current_rect.end[0] = right;
106-
107-
if (int top = std::max(left_line.start, right_line.start);
108-
top > current_rect.start[1]) {
109-
current_rect.start[1] = top;
110-
}
111-
112-
if (int bottom = std::min(left_line.end, right_line.end);
113-
bottom < current_rect.end[1]) {
114-
current_rect.end[1] = bottom;
115-
}
116-
117-
if (utils::Area(current_rect) >= utils::Area(largest_rect)) {
133+
if (current_rect && (!largest_rect || utils::Area(*current_rect) >=
134+
utils::Area(*largest_rect))) {
118135
largest_rect = current_rect;
119136
}
120137
}

xpano/constants.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,6 @@ constexpr float kDefaultPaniniB = 1.0f;
6969
const std::string kConfigFilename = "config.txt";
7070

7171
constexpr int kCropEdgeTolerance = 10;
72+
constexpr int kAutoCropSamplingDistance = 512;
7273

7374
} // namespace xpano

xpano/gui/panels/preview_pane.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ auto CropRectPP(const utils::RectPVf& image, const utils::RectRRf& crop_rect) {
3939

4040
void Overlay(const utils::RectRRf& crop_rect, const utils::RectPVf& image) {
4141
auto rect = CropRectPP(image, crop_rect);
42+
43+
const auto transparent_color = ImColor(0, 0, 0, 128);
44+
auto start = ImVec2(image.start[0], image.start[1]);
45+
auto end = ImVec2(rect.start[0], image.start[1] + image.size[1]);
46+
ImGui::GetWindowDrawList()->AddRectFilled(start, end, transparent_color);
47+
start = ImVec2(rect.end[0], image.start[1]);
48+
end = ImVec2(image.start[0] + image.size[0], image.start[1] + image.size[1]);
49+
ImGui::GetWindowDrawList()->AddRectFilled(start, end, transparent_color);
50+
start = ImVec2(rect.start[0], image.start[1]);
51+
end = ImVec2(rect.end[0], rect.start[1]);
52+
ImGui::GetWindowDrawList()->AddRectFilled(start, end, transparent_color);
53+
start = ImVec2(rect.start[0], rect.end[1]);
54+
end = ImVec2(rect.end[0], image.start[1] + image.size[1]);
55+
ImGui::GetWindowDrawList()->AddRectFilled(start, end, transparent_color);
56+
4257
const auto crop_color = ImColor(255, 255, 255, 255);
4358
ImGui::GetWindowDrawList()->AddRect(utils::ImVec(rect.start),
4459
utils::ImVec(rect.end), crop_color, 0.0f,

xpano/gui/pano_gui.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ auto ResolveStitchingResultFuture(
162162
*status_message = {fmt::format("Failed to stitch pano {}", result.pano_id),
163163
algorithm::ToString(result.status)};
164164
spdlog::info(*status_message);
165-
plot_pane->Reset();
165+
if (!result.full_res) {
166+
plot_pane->Reset();
167+
}
166168
return {};
167169
}
168170

xpano/pipeline/stitcher_pipeline.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ StitchingResult StitcherPipeline::RunStitchingPipeline(
118118
progress_.NotifyTaskDone();
119119

120120
if (status != cv::Stitcher::OK) {
121-
return StitchingResult{.pano_id = options.pano_id, .status = status};
121+
return StitchingResult{
122+
.pano_id = options.pano_id,
123+
.full_res = options.full_res,
124+
.status = status,
125+
};
122126
}
123127

124128
std::optional<utils::RectRRf> auto_crop;

0 commit comments

Comments
 (0)