2019-12-07 00:33:11 +01:00
|
|
|
// Copyright 2019 The MediaPipe Authors.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
#include "mediapipe/util/tracking/region_flow_computation.h"
|
|
|
|
|
|
|
|
#include <math.h>
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
#include <memory>
|
|
|
|
#include <random>
|
|
|
|
#include <string>
|
|
|
|
|
2021-03-25 23:01:44 +01:00
|
|
|
#include "absl/flags/flag.h"
|
2019-12-07 00:33:11 +01:00
|
|
|
#include "absl/time/clock.h"
|
|
|
|
#include "mediapipe/framework/deps/file_path.h"
|
|
|
|
#include "mediapipe/framework/port/file_helpers.h"
|
|
|
|
#include "mediapipe/framework/port/gtest.h"
|
|
|
|
#include "mediapipe/framework/port/logging.h"
|
|
|
|
#include "mediapipe/framework/port/opencv_core_inc.h"
|
2022-11-18 17:51:30 +01:00
|
|
|
#include "mediapipe/framework/port/opencv_imgcodecs_inc.h"
|
2019-12-07 00:33:11 +01:00
|
|
|
#include "mediapipe/framework/port/opencv_imgproc_inc.h"
|
|
|
|
#include "mediapipe/framework/port/status.h"
|
|
|
|
#include "mediapipe/framework/port/vector.h"
|
|
|
|
#include "mediapipe/util/tracking/region_flow.h"
|
|
|
|
#include "mediapipe/util/tracking/region_flow.pb.h"
|
|
|
|
|
|
|
|
// To ensure that the selected thresholds are robust, it is recommend
|
|
|
|
// to run this test mutiple times with time seed, if changes are made.
|
2021-03-25 23:01:44 +01:00
|
|
|
ABSL_FLAG(bool, time_seed, false, "Activate to test thresholds");
|
2019-12-07 00:33:11 +01:00
|
|
|
|
|
|
|
namespace mediapipe {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
using RandomEngine = std::mt19937_64;
|
|
|
|
|
|
|
|
struct FlowDirectionParam {
|
|
|
|
TrackingOptions::FlowDirection internal_direction;
|
|
|
|
TrackingOptions::FlowDirection output_direction;
|
|
|
|
};
|
|
|
|
|
|
|
|
class RegionFlowComputationTest
|
|
|
|
: public ::testing::TestWithParam<FlowDirectionParam> {
|
|
|
|
protected:
|
|
|
|
virtual void SetUp() {
|
|
|
|
// Setup tracking direction options based on testing parameter.
|
|
|
|
const auto& param = GetParam();
|
|
|
|
auto* tracking_options = base_options_.mutable_tracking_options();
|
|
|
|
tracking_options->set_internal_tracking_direction(param.internal_direction);
|
|
|
|
tracking_options->set_output_flow_direction(param.output_direction);
|
|
|
|
|
|
|
|
// Load bee image.
|
|
|
|
data_dir_ = file::JoinPath("./", "/mediapipe/util/tracking/testdata/");
|
|
|
|
|
|
|
|
std::string png_data;
|
|
|
|
MEDIAPIPE_CHECK_OK(
|
|
|
|
file::GetContents(data_dir_ + "stabilize_test.png", &png_data));
|
|
|
|
std::vector<char> buffer(png_data.begin(), png_data.end());
|
|
|
|
original_frame_ = cv::imdecode(cv::Mat(buffer), 1);
|
|
|
|
ASSERT_FALSE(original_frame_.empty());
|
|
|
|
ASSERT_EQ(original_frame_.type(), CV_8UC3);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Creates a movie in the specified format by displacing original_frame_ to
|
|
|
|
// random positions.
|
|
|
|
void MakeMovie(int frames, RegionFlowComputationOptions::ImageFormat format,
|
|
|
|
std::vector<cv::Mat>* movie,
|
|
|
|
std::vector<Vector2_f>* positions);
|
|
|
|
|
|
|
|
// Outputs allocated resized input frame.
|
|
|
|
void GetResizedFrame(int width, int height, cv::Mat* result) const;
|
|
|
|
|
|
|
|
// Runs frame pair test using RGB, RGBA or grayscale input.
|
|
|
|
void RunFramePairTest(RegionFlowComputationOptions::ImageFormat format);
|
|
|
|
|
|
|
|
protected:
|
|
|
|
RegionFlowComputationOptions base_options_;
|
|
|
|
|
|
|
|
private:
|
|
|
|
std::string data_dir_;
|
|
|
|
cv::Mat original_frame_;
|
|
|
|
};
|
|
|
|
|
|
|
|
std::vector<FlowDirectionParam> FlowDirectionCombinations() {
|
|
|
|
return {{TrackingOptions::FORWARD, TrackingOptions::FORWARD},
|
|
|
|
{TrackingOptions::FORWARD, TrackingOptions::BACKWARD},
|
|
|
|
{TrackingOptions::BACKWARD, TrackingOptions::FORWARD},
|
|
|
|
{TrackingOptions::BACKWARD, TrackingOptions::BACKWARD}};
|
|
|
|
}
|
|
|
|
|
|
|
|
INSTANTIATE_TEST_SUITE_P(FlowDirection, RegionFlowComputationTest,
|
|
|
|
::testing::ValuesIn(FlowDirectionCombinations()));
|
|
|
|
|
|
|
|
void RegionFlowComputationTest::MakeMovie(
|
|
|
|
int num_frames, RegionFlowComputationOptions::ImageFormat format,
|
|
|
|
std::vector<cv::Mat>* movie, std::vector<Vector2_f>* positions) {
|
|
|
|
CHECK(positions != nullptr);
|
|
|
|
CHECK(movie != nullptr);
|
|
|
|
|
|
|
|
const int border = 40;
|
|
|
|
int frame_width = original_frame_.cols - 2 * border;
|
|
|
|
int frame_height = original_frame_.rows - 2 * border;
|
|
|
|
ASSERT_GT(frame_width, 0);
|
|
|
|
ASSERT_GT(frame_height, 0);
|
|
|
|
|
|
|
|
// First generate random positions.
|
|
|
|
int seed = 900913; // google.
|
2021-02-27 09:21:16 +01:00
|
|
|
if (absl::GetFlag(FLAGS_time_seed)) {
|
2019-12-07 00:33:11 +01:00
|
|
|
seed = ToUnixMillis(absl::Now()) % (1 << 16);
|
|
|
|
LOG(INFO) << "Using time seed: " << seed;
|
|
|
|
}
|
|
|
|
|
|
|
|
RandomEngine random(seed);
|
|
|
|
std::uniform_int_distribution<> uniform_dist(-10, 10);
|
|
|
|
positions->resize(num_frames);
|
|
|
|
(*positions)[0] = Vector2_f(border, border);
|
|
|
|
for (int f = 1; f < num_frames; ++f) {
|
|
|
|
Vector2_f pos = (*positions)[f - 1] +
|
|
|
|
Vector2_f(uniform_dist(random), uniform_dist(random));
|
|
|
|
|
|
|
|
// Clamp to valid positions.
|
|
|
|
pos.x(std::max<int>(0, pos.x()));
|
|
|
|
pos.y(std::max<int>(0, pos.y()));
|
|
|
|
pos.x(std::min<int>(2 * border, pos.x()));
|
|
|
|
pos.y(std::min<int>(2 * border, pos.y()));
|
|
|
|
|
|
|
|
(*positions)[f] = pos;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create movie by copying.
|
|
|
|
movie->resize(num_frames);
|
|
|
|
cv::Mat original_frame = original_frame_;
|
|
|
|
|
|
|
|
auto convert = [&](int channel_format, int conversion_code) {
|
|
|
|
original_frame.create(original_frame_.rows, original_frame_.cols,
|
|
|
|
channel_format);
|
|
|
|
cv::cvtColor(original_frame_, original_frame, conversion_code);
|
|
|
|
};
|
|
|
|
|
|
|
|
switch (format) {
|
|
|
|
case RegionFlowComputationOptions::FORMAT_RGB:
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RegionFlowComputationOptions::FORMAT_BGR:
|
2020-04-07 04:10:12 +02:00
|
|
|
convert(CV_8UC3, cv::COLOR_RGB2BGR);
|
2019-12-07 00:33:11 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case RegionFlowComputationOptions::FORMAT_GRAYSCALE:
|
2020-04-07 04:10:12 +02:00
|
|
|
convert(CV_8UC1, cv::COLOR_RGB2GRAY);
|
2019-12-07 00:33:11 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case RegionFlowComputationOptions::FORMAT_RGBA:
|
2020-04-07 04:10:12 +02:00
|
|
|
convert(CV_8UC4, cv::COLOR_RGB2RGBA);
|
2019-12-07 00:33:11 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case RegionFlowComputationOptions::FORMAT_BGRA:
|
2020-04-07 04:10:12 +02:00
|
|
|
convert(CV_8UC4, cv::COLOR_RGB2BGRA);
|
2019-12-07 00:33:11 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
for (int f = 0; f < num_frames; ++f) {
|
|
|
|
(*movie)[f].create(frame_height, frame_width, original_frame.type());
|
|
|
|
const auto& pos = (*positions)[f];
|
|
|
|
cv::Mat tmp_view(original_frame, cv::Range(pos.y(), pos.y() + frame_height),
|
|
|
|
cv::Range(pos.x(), pos.x() + frame_width));
|
|
|
|
tmp_view.copyTo((*movie)[f]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void RegionFlowComputationTest::GetResizedFrame(int width, int height,
|
|
|
|
cv::Mat* result) const {
|
|
|
|
CHECK(result != nullptr);
|
|
|
|
cv::resize(original_frame_, *result, cv::Size(width, height));
|
|
|
|
}
|
|
|
|
|
|
|
|
void RegionFlowComputationTest::RunFramePairTest(
|
|
|
|
RegionFlowComputationOptions::ImageFormat format) {
|
|
|
|
std::vector<cv::Mat> movie;
|
|
|
|
std::vector<Vector2_f> positions;
|
|
|
|
const int num_frames = 10;
|
|
|
|
MakeMovie(num_frames, format, &movie, &positions);
|
|
|
|
|
|
|
|
const int frame_width = movie[0].cols;
|
|
|
|
const int frame_height = movie[0].rows;
|
|
|
|
|
|
|
|
base_options_.set_image_format(format);
|
|
|
|
|
|
|
|
RegionFlowComputation flow_computation(base_options_, frame_width,
|
|
|
|
frame_height);
|
|
|
|
|
|
|
|
for (int i = 0; i < num_frames; ++i) {
|
|
|
|
flow_computation.AddImage(movie[i], 0);
|
|
|
|
|
|
|
|
if (i > 0) {
|
|
|
|
float inliers = 0;
|
|
|
|
std::unique_ptr<RegionFlowFrame> region_flow_frame(
|
|
|
|
flow_computation.RetrieveRegionFlow());
|
|
|
|
// Get flow vector based on actual motion applied to frames. Direction
|
|
|
|
// based on output flow direction.
|
|
|
|
Vector2_f flow_vector;
|
|
|
|
switch (base_options_.tracking_options().output_flow_direction()) {
|
|
|
|
case TrackingOptions::BACKWARD:
|
|
|
|
flow_vector = positions[i] - positions[i - 1];
|
|
|
|
break;
|
|
|
|
case TrackingOptions::FORWARD:
|
|
|
|
flow_vector = positions[i - 1] - positions[i];
|
|
|
|
break;
|
|
|
|
case TrackingOptions::CONSECUTIVELY:
|
|
|
|
FAIL();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We expect all flow vectors to be similar to flow_vector.
|
|
|
|
for (const auto& region_flow : region_flow_frame->region_flow()) {
|
|
|
|
for (const auto& feature : region_flow.feature()) {
|
|
|
|
// Half a pixel error is very reasonable.
|
|
|
|
if (fabs(flow_vector.x() - FeatureFlow(feature).x()) < 0.5f &&
|
|
|
|
fabs(flow_vector.y() - FeatureFlow(feature).y()) < 0.5f) {
|
|
|
|
++inliers;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// 95% of all features should be tracked reliably.
|
|
|
|
EXPECT_GE(inliers / region_flow_frame->num_total_features(), 0.95f);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_P(RegionFlowComputationTest, FramePairTest) {
|
|
|
|
// The output flow direction should be either forward or backward.
|
|
|
|
EXPECT_NE(base_options_.tracking_options().output_flow_direction(),
|
|
|
|
TrackingOptions::CONSECUTIVELY);
|
|
|
|
// Test on grayscale input.
|
|
|
|
RunFramePairTest(RegionFlowComputationOptions::FORMAT_GRAYSCALE);
|
|
|
|
// Test on RGB input.
|
|
|
|
RunFramePairTest(RegionFlowComputationOptions::FORMAT_RGB);
|
|
|
|
// Test on BGR input.
|
|
|
|
RunFramePairTest(RegionFlowComputationOptions::FORMAT_BGR);
|
|
|
|
// Test on RGBA input.
|
|
|
|
RunFramePairTest(RegionFlowComputationOptions::FORMAT_RGBA);
|
|
|
|
// Test on BGRA input.
|
|
|
|
RunFramePairTest(RegionFlowComputationOptions::FORMAT_BGRA);
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_P(RegionFlowComputationTest, ResolutionTests) {
|
|
|
|
// Test all kinds of resolutions (disregard resulting flow).
|
|
|
|
// Square test, synthetic tracks.
|
|
|
|
for (int dim = 1; dim <= 50; ++dim) {
|
|
|
|
RegionFlowComputationOptions options = base_options_;
|
|
|
|
// Force synthetic features, as images are too small to track anything.
|
|
|
|
options.set_use_synthetic_zero_motion_tracks_all_frames(true);
|
|
|
|
RegionFlowComputation flow_computation(options, dim, dim);
|
|
|
|
|
|
|
|
cv::Mat input_frame;
|
|
|
|
GetResizedFrame(dim, dim, &input_frame);
|
|
|
|
// Add frame several times.
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
|
|
flow_computation.AddImage(input_frame, 0);
|
|
|
|
|
|
|
|
// Don't care about the result here, simply test for segfaults.
|
|
|
|
delete flow_computation.RetrieveRegionFlow();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Larger frames with tracking.
|
|
|
|
for (int dim = 50; dim <= 100; ++dim) {
|
|
|
|
RegionFlowComputation flow_computation(base_options_, dim, dim);
|
|
|
|
|
|
|
|
cv::Mat input_frame;
|
|
|
|
GetResizedFrame(dim, dim, &input_frame);
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
|
|
flow_computation.AddImage(input_frame, 0);
|
|
|
|
delete flow_computation.RetrieveRegionFlow();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Different aspect ratios, first frame synthetic only.
|
|
|
|
for (int y = 1; y <= 50; y += 3) {
|
|
|
|
for (int x = 1; x <= 100; x += 7) {
|
|
|
|
RegionFlowComputationOptions options = base_options_;
|
|
|
|
options.set_use_synthetic_zero_motion_tracks_first_frame(true);
|
|
|
|
RegionFlowComputation flow_computation(options, x, y);
|
|
|
|
|
|
|
|
cv::Mat input_frame;
|
|
|
|
GetResizedFrame(x, y, &input_frame);
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
|
|
flow_computation.AddImage(input_frame, 0);
|
|
|
|
delete flow_computation.RetrieveRegionFlow();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
} // namespace mediapipe
|