mediapipe/mediapipe/util/tracking/region_flow_computation_test.cc

305 lines
10 KiB
C++
Raw Normal View History

// 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>
#include "absl/flags/flag.h"
#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"
#include "mediapipe/framework/port/opencv_imgcodecs_inc.h"
#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.
ABSL_FLAG(bool, time_seed, false, "Activate to test thresholds");
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.
if (absl::GetFlag(FLAGS_time_seed)) {
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:
convert(CV_8UC3, cv::COLOR_RGB2BGR);
break;
case RegionFlowComputationOptions::FORMAT_GRAYSCALE:
convert(CV_8UC1, cv::COLOR_RGB2GRAY);
break;
case RegionFlowComputationOptions::FORMAT_RGBA:
convert(CV_8UC4, cv::COLOR_RGB2RGBA);
break;
case RegionFlowComputationOptions::FORMAT_BGRA:
convert(CV_8UC4, cv::COLOR_RGB2BGRA);
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