Project import generated by Copybara.

GitOrigin-RevId: d91373b4d4d10abef49cab410caa6aadf0875049
This commit is contained in:
MediaPipe Team
2019-12-06 15:57:20 -08:00
committed by mgyong
parent 137867d088
commit d16cc3be5b
137 changed files with 41547 additions and 30 deletions
+1
View File
@@ -165,6 +165,7 @@ cc_library(
deps = [
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/port:logging",
"//mediapipe/framework/port:status",
],
alwayslink = 1,
)
@@ -13,11 +13,12 @@
// limitations under the License.
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/port/canonical_errors.h"
#include "mediapipe/framework/port/logging.h"
namespace mediapipe {
// Attach the header from one stream to another stream.
// Attach the header from a stream or side input to another stream.
//
// The header stream (tag HEADER) must not have any packets in it.
//
@@ -25,17 +26,53 @@ namespace mediapipe {
// calculator to not need a header or to accept a separate stream with
// a header, that would be more future proof.
//
// Example usage 1:
// node {
// calculator: "AddHeaderCalculator"
// input_stream: "DATA:audio"
// input_stream: "HEADER:audio_header"
// output_stream: "audio_with_header"
// }
//
// Example usage 2:
// node {
// calculator: "AddHeaderCalculator"
// input_stream: "DATA:audio"
// input_side_packet: "HEADER:audio_header"
// output_stream: "audio_with_header"
// }
//
class AddHeaderCalculator : public CalculatorBase {
public:
static ::mediapipe::Status GetContract(CalculatorContract* cc) {
cc->Inputs().Tag("HEADER").SetNone();
bool has_side_input = false;
bool has_header_stream = false;
if (cc->InputSidePackets().HasTag("HEADER")) {
cc->InputSidePackets().Tag("HEADER").SetAny();
has_side_input = true;
}
if (cc->Inputs().HasTag("HEADER")) {
cc->Inputs().Tag("HEADER").SetNone();
has_header_stream = true;
}
if (has_side_input == has_header_stream) {
return mediapipe::InvalidArgumentError(
"Header must be provided via exactly one of side input and input "
"stream");
}
cc->Inputs().Tag("DATA").SetAny();
cc->Outputs().Index(0).SetSameAs(&cc->Inputs().Tag("DATA"));
return ::mediapipe::OkStatus();
}
::mediapipe::Status Open(CalculatorContext* cc) override {
const Packet& header = cc->Inputs().Tag("HEADER").Header();
Packet header;
if (cc->InputSidePackets().HasTag("HEADER")) {
header = cc->InputSidePackets().Tag("HEADER");
}
if (cc->Inputs().HasTag("HEADER")) {
header = cc->Inputs().Tag("HEADER").Header();
}
if (!header.IsEmpty()) {
cc->Outputs().Index(0).SetHeader(header);
}
@@ -14,8 +14,10 @@
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/calculator_runner.h"
#include "mediapipe/framework/port/canonical_errors.h"
#include "mediapipe/framework/port/gmock.h"
#include "mediapipe/framework/port/gtest.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/framework/port/status_matchers.h"
#include "mediapipe/framework/timestamp.h"
#include "mediapipe/framework/tool/validate_type.h"
@@ -24,7 +26,7 @@ namespace mediapipe {
class AddHeaderCalculatorTest : public ::testing::Test {};
TEST_F(AddHeaderCalculatorTest, Works) {
TEST_F(AddHeaderCalculatorTest, HeaderStream) {
CalculatorGraphConfig::Node node;
node.set_calculator("AddHeaderCalculator");
node.add_input_stream("HEADER:header_stream");
@@ -96,4 +98,62 @@ TEST_F(AddHeaderCalculatorTest, NoPacketsOnHeaderStream) {
ASSERT_FALSE(runner.Run().ok());
}
TEST_F(AddHeaderCalculatorTest, InputSidePacket) {
CalculatorGraphConfig::Node node;
node.set_calculator("AddHeaderCalculator");
node.add_input_stream("DATA:data_stream");
node.add_output_stream("merged_stream");
node.add_input_side_packet("HEADER:header");
CalculatorRunner runner(node);
// Set header and add 5 packets.
runner.MutableSidePackets()->Tag("HEADER") =
Adopt(new std::string("my_header"));
for (int i = 0; i < 5; ++i) {
Packet packet = Adopt(new int(i)).At(Timestamp(i * 1000));
runner.MutableInputs()->Tag("DATA").packets.push_back(packet);
}
// Run calculator.
MP_ASSERT_OK(runner.Run());
ASSERT_EQ(1, runner.Outputs().NumEntries());
// Test output.
EXPECT_EQ(std::string("my_header"),
runner.Outputs().Index(0).header.Get<std::string>());
const std::vector<Packet>& output_packets = runner.Outputs().Index(0).packets;
ASSERT_EQ(5, output_packets.size());
for (int i = 0; i < 5; ++i) {
const int val = output_packets[i].Get<int>();
EXPECT_EQ(i, val);
EXPECT_EQ(Timestamp(i * 1000), output_packets[i].Timestamp());
}
}
TEST_F(AddHeaderCalculatorTest, UsingBothSideInputAndStream) {
CalculatorGraphConfig::Node node;
node.set_calculator("AddHeaderCalculator");
node.add_input_stream("HEADER:header_stream");
node.add_input_stream("DATA:data_stream");
node.add_output_stream("merged_stream");
node.add_input_side_packet("HEADER:header");
CalculatorRunner runner(node);
// Set both headers and add 5 packets.
runner.MutableSidePackets()->Tag("HEADER") =
Adopt(new std::string("my_header"));
runner.MutableSidePackets()->Tag("HEADER") =
Adopt(new std::string("my_header"));
for (int i = 0; i < 5; ++i) {
Packet packet = Adopt(new int(i)).At(Timestamp(i * 1000));
runner.MutableInputs()->Tag("DATA").packets.push_back(packet);
}
// Run should fail because header can only be provided one way.
EXPECT_EQ(runner.Run().code(), ::mediapipe::InvalidArgumentError("").code());
}
} // namespace mediapipe
@@ -330,22 +330,27 @@ void PacketResamplerCalculator::UpdateNextOutputTimestampWithJitter() {
return ::mediapipe::OkStatus();
}
LOG_IF(WARNING, frame_time_usec_ <
(cc->InputTimestamp() - last_packet_.Timestamp()).Value())
<< "Adding jitter is meaningless when upsampling.";
if (frame_time_usec_ <
(cc->InputTimestamp() - last_packet_.Timestamp()).Value()) {
LOG_FIRST_N(WARNING, 2)
<< "Adding jitter is not very useful when upsampling.";
}
const int64 curr_diff =
(next_output_timestamp_ - cc->InputTimestamp()).Value();
const int64 last_diff =
(next_output_timestamp_ - last_packet_.Timestamp()).Value();
if (curr_diff * last_diff > 0) {
return ::mediapipe::OkStatus();
while (true) {
const int64 last_diff =
(next_output_timestamp_ - last_packet_.Timestamp()).Value();
RET_CHECK_GT(last_diff, 0.0);
const int64 curr_diff =
(next_output_timestamp_ - cc->InputTimestamp()).Value();
if (curr_diff > 0.0) {
break;
}
OutputWithinLimits(cc, (std::abs(curr_diff) > last_diff
? last_packet_
: cc->Inputs().Get(input_data_id_).Value())
.At(next_output_timestamp_));
UpdateNextOutputTimestampWithJitter();
}
OutputWithinLimits(cc, (std::abs(curr_diff) > std::abs(last_diff)
? last_packet_
: cc->Inputs().Get(input_data_id_).Value())
.At(next_output_timestamp_));
UpdateNextOutputTimestampWithJitter();
return ::mediapipe::OkStatus();
}
@@ -501,8 +501,11 @@ void ImageCroppingCalculator::GetOutputDimensions(CalculatorContext* cc,
row_max = std::max(row_max, transformed_points_[i * 2 + 1]);
}
*dst_width = std::round((col_max - col_min) * src_width);
*dst_height = std::round((row_max - row_min) * src_height);
int width = static_cast<int>(std::round((col_max - col_min) * src_width));
int height = static_cast<int>(std::round((row_max - row_min) * src_height));
// Minimum output dimension 1x1 prevents creation of textures with 0x0.
*dst_width = std::max(1, width);
*dst_height = std::max(1, height);
}
} // namespace mediapipe
+28
View File
@@ -983,3 +983,31 @@ cc_test(
"//mediapipe/framework/port:parse_text_proto",
],
)
cc_library(
name = "detections_to_timed_box_list_calculator",
srcs = ["detections_to_timed_box_list_calculator.cc"],
visibility = ["//visibility:public"],
deps = [
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/formats:detection_cc_proto",
"//mediapipe/framework/formats:location_data_cc_proto",
"//mediapipe/framework/port:ret_check",
"//mediapipe/framework/port:status",
"//mediapipe/util/tracking:box_tracker",
],
alwayslink = 1,
)
cc_library(
name = "detection_unique_id_calculator",
srcs = ["detection_unique_id_calculator.cc"],
visibility = ["//visibility:public"],
deps = [
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/formats:detection_cc_proto",
"//mediapipe/framework/port:ret_check",
"//mediapipe/framework/port:status",
],
alwayslink = 1,
)
@@ -0,0 +1,110 @@
// 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/framework/calculator_framework.h"
#include "mediapipe/framework/formats/detection.pb.h"
#include "mediapipe/framework/port/status.h"
namespace mediapipe {
namespace {
constexpr char kDetectionsTag[] = "DETECTIONS";
constexpr char kDetectionListTag[] = "DETECTION_LIST";
// Each detection processed by DetectionUniqueIDCalculator will be assigned an
// unique id that starts from 1. If a detection already has an ID other than 0,
// the ID will be overwritten.
static int64 detection_id = 0;
inline int GetNextDetectionId() { return ++detection_id; }
} // namespace
// Assign a unique id to detections.
// Note that the calculator will consume the input vector of Detection or
// DetectionList. So the input stream can not be connected to other calculators.
//
// Example config:
// node {
// calculator: "DetectionUniqueIdCalculator"
// input_stream: "DETECTIONS:detections"
// output_stream: "DETECTIONS:output_detections"
// }
class DetectionUniqueIdCalculator : public CalculatorBase {
public:
static ::mediapipe::Status GetContract(CalculatorContract* cc) {
RET_CHECK(cc->Inputs().HasTag(kDetectionListTag) ||
cc->Inputs().HasTag(kDetectionsTag))
<< "None of the input streams are provided.";
if (cc->Inputs().HasTag(kDetectionListTag)) {
RET_CHECK(cc->Outputs().HasTag(kDetectionListTag));
cc->Inputs().Tag(kDetectionListTag).Set<DetectionList>();
cc->Outputs().Tag(kDetectionListTag).Set<DetectionList>();
}
if (cc->Inputs().HasTag(kDetectionsTag)) {
RET_CHECK(cc->Outputs().HasTag(kDetectionsTag));
cc->Inputs().Tag(kDetectionsTag).Set<std::vector<Detection>>();
cc->Outputs().Tag(kDetectionsTag).Set<std::vector<Detection>>();
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status Open(CalculatorContext* cc) override {
cc->SetOffset(::mediapipe::TimestampDiff(0));
return ::mediapipe::OkStatus();
}
::mediapipe::Status Process(CalculatorContext* cc) override;
};
REGISTER_CALCULATOR(DetectionUniqueIdCalculator);
::mediapipe::Status DetectionUniqueIdCalculator::Process(
CalculatorContext* cc) {
if (cc->Inputs().HasTag(kDetectionListTag) &&
!cc->Inputs().Tag(kDetectionListTag).IsEmpty()) {
auto result =
cc->Inputs().Tag(kDetectionListTag).Value().Consume<DetectionList>();
if (result.ok()) {
auto detection_list = std::move(result).ValueOrDie();
for (Detection& detection : *detection_list->mutable_detection()) {
detection.set_detection_id(GetNextDetectionId());
}
cc->Outputs()
.Tag(kDetectionListTag)
.Add(detection_list.release(), cc->InputTimestamp());
}
}
if (cc->Inputs().HasTag(kDetectionsTag) &&
!cc->Inputs().Tag(kDetectionsTag).IsEmpty()) {
auto result = cc->Inputs()
.Tag(kDetectionsTag)
.Value()
.Consume<std::vector<Detection>>();
if (result.ok()) {
auto detections = std::move(result).ValueOrDie();
for (Detection& detection : *detections) {
detection.set_detection_id(GetNextDetectionId());
}
cc->Outputs()
.Tag(kDetectionsTag)
.Add(detections.release(), cc->InputTimestamp());
}
}
return ::mediapipe::OkStatus();
}
} // namespace mediapipe
@@ -39,6 +39,8 @@ constexpr char kKeypointLabel[] = "KEYPOINT";
// The ratio of detection label font height to the height of detection bounding
// box.
constexpr double kLabelToBoundingBoxRatio = 0.1;
// Perserve 2 decimal digits.
constexpr float kNumScoreDecimalDigitsMultipler = 100;
} // namespace
@@ -235,18 +237,26 @@ void DetectionsToRenderDataCalculator::AddLabels(
std::string label_str = detection.label().empty()
? absl::StrCat(detection.label_id(i))
: detection.label(i);
const float rounded_score =
std::round(detection.score(i) * kNumScoreDecimalDigitsMultipler) /
kNumScoreDecimalDigitsMultipler;
std::string label_and_score =
absl::StrCat(label_str, options.text_delimiter(), detection.score(i),
absl::StrCat(label_str, options.text_delimiter(), rounded_score,
options.text_delimiter());
label_and_scores.push_back(label_and_score);
}
std::vector<std::string> labels;
if (options.render_detection_id()) {
const std::string detection_id_str =
absl::StrCat("Id: ", detection.detection_id());
labels.push_back(detection_id_str);
}
if (options.one_label_per_line()) {
labels.swap(label_and_scores);
labels.insert(labels.end(), label_and_scores.begin(),
label_and_scores.end());
} else {
labels.push_back(absl::StrJoin(label_and_scores, ""));
}
// Add the render annotations for "label(_id),score".
for (int i = 0; i < labels.size(); ++i) {
auto label = labels.at(i);
@@ -53,4 +53,7 @@ message DetectionsToRenderDataCalculatorOptions {
// instances of this calculator are present in the graph, this value
// should be unique among them.
optional string scene_class = 7 [default = "DETECTION"];
// If true, renders the detection id in the first line before the labels.
optional bool render_detection_id = 8 [default = false];
}
@@ -0,0 +1,110 @@
// 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/framework/calculator_framework.h"
#include "mediapipe/framework/formats/detection.pb.h"
#include "mediapipe/framework/formats/location_data.pb.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/util/tracking/box_tracker.h"
namespace mediapipe {
namespace {
constexpr char kDetectionsTag[] = "DETECTIONS";
constexpr char kDetectionListTag[] = "DETECTION_LIST";
constexpr char kBoxesTag[] = "BOXES";
} // namespace
// A calculator that converts Detection proto to TimedBoxList proto for
// tracking.
//
// Please note that only Location Data formats of RELATIVE_BOUNDING_BOX are
// supported.
//
// Example config:
// node {
// calculator: "DetectionsToTimedBoxListCalculator"
// input_stream: "DETECTIONS:detections"
// output_stream: "BOXES:boxes"
// }
class DetectionsToTimedBoxListCalculator : public CalculatorBase {
public:
static ::mediapipe::Status GetContract(CalculatorContract* cc) {
RET_CHECK(cc->Inputs().HasTag(kDetectionListTag) ||
cc->Inputs().HasTag(kDetectionsTag))
<< "None of the input streams are provided.";
if (cc->Inputs().HasTag(kDetectionListTag)) {
cc->Inputs().Tag(kDetectionListTag).Set<DetectionList>();
}
if (cc->Inputs().HasTag(kDetectionsTag)) {
cc->Inputs().Tag(kDetectionsTag).Set<std::vector<Detection>>();
}
cc->Outputs().Tag(kBoxesTag).Set<TimedBoxProtoList>();
return ::mediapipe::OkStatus();
}
::mediapipe::Status Open(CalculatorContext* cc) override {
cc->SetOffset(TimestampDiff(0));
return ::mediapipe::OkStatus();
}
::mediapipe::Status Process(CalculatorContext* cc) override;
private:
void ConvertDetectionToTimedBox(const Detection& detection,
TimedBoxProto* box, CalculatorContext* cc);
};
REGISTER_CALCULATOR(DetectionsToTimedBoxListCalculator);
::mediapipe::Status DetectionsToTimedBoxListCalculator::Process(
CalculatorContext* cc) {
auto output_timed_box_list = absl::make_unique<TimedBoxProtoList>();
if (cc->Inputs().HasTag(kDetectionListTag)) {
const auto& detection_list =
cc->Inputs().Tag(kDetectionListTag).Get<DetectionList>();
for (const auto& detection : detection_list.detection()) {
TimedBoxProto* box = output_timed_box_list->add_box();
ConvertDetectionToTimedBox(detection, box, cc);
}
}
if (cc->Inputs().HasTag(kDetectionsTag)) {
const auto& detections =
cc->Inputs().Tag(kDetectionsTag).Get<std::vector<Detection>>();
for (const auto& detection : detections) {
TimedBoxProto* box = output_timed_box_list->add_box();
ConvertDetectionToTimedBox(detection, box, cc);
}
}
cc->Outputs().Tag(kBoxesTag).Add(output_timed_box_list.release(),
cc->InputTimestamp());
return ::mediapipe::OkStatus();
}
void DetectionsToTimedBoxListCalculator::ConvertDetectionToTimedBox(
const Detection& detection, TimedBoxProto* box, CalculatorContext* cc) {
const auto& relative_bounding_box =
detection.location_data().relative_bounding_box();
box->set_left(relative_bounding_box.xmin());
box->set_right(relative_bounding_box.xmin() + relative_bounding_box.width());
box->set_top(relative_bounding_box.ymin());
box->set_bottom(relative_bounding_box.ymin() +
relative_bounding_box.height());
box->set_id(detection.detection_id());
box->set_time_msec(cc->InputTimestamp().Microseconds() / 1000);
}
} // namespace mediapipe
+210
View File
@@ -37,6 +37,67 @@ proto_library(
deps = ["//mediapipe/framework:calculator_proto"],
)
proto_library(
name = "motion_analysis_calculator_proto",
srcs = ["motion_analysis_calculator.proto"],
deps = [
"//mediapipe/framework:calculator_proto",
"//mediapipe/util/tracking:motion_analysis_proto",
],
)
proto_library(
name = "flow_packager_calculator_proto",
srcs = ["flow_packager_calculator.proto"],
deps = [
"//mediapipe/framework:calculator_proto",
"//mediapipe/util/tracking:flow_packager_proto",
],
)
proto_library(
name = "box_tracker_calculator_proto",
srcs = ["box_tracker_calculator.proto"],
visibility = ["//visibility:public"],
deps = [
"//mediapipe/framework:calculator_proto",
"//mediapipe/util/tracking:box_tracker_proto",
],
)
mediapipe_cc_proto_library(
name = "motion_analysis_calculator_cc_proto",
srcs = ["motion_analysis_calculator.proto"],
cc_deps = [
"//mediapipe/framework:calculator_cc_proto",
"//mediapipe/util/tracking:motion_analysis_cc_proto",
],
visibility = ["//visibility:public"],
deps = [":motion_analysis_calculator_proto"],
)
mediapipe_cc_proto_library(
name = "flow_packager_calculator_cc_proto",
srcs = ["flow_packager_calculator.proto"],
cc_deps = [
"//mediapipe/framework:calculator_cc_proto",
"//mediapipe/util/tracking:flow_packager_cc_proto",
],
visibility = ["//visibility:public"],
deps = [":flow_packager_calculator_proto"],
)
mediapipe_cc_proto_library(
name = "box_tracker_calculator_cc_proto",
srcs = ["box_tracker_calculator.proto"],
cc_deps = [
"//mediapipe/framework:calculator_cc_proto",
"//mediapipe/util/tracking:box_tracker_cc_proto",
],
visibility = ["//visibility:public"],
deps = [":box_tracker_calculator_proto"],
)
mediapipe_cc_proto_library(
name = "flow_to_image_calculator_cc_proto",
srcs = ["flow_to_image_calculator.proto"],
@@ -131,6 +192,94 @@ cc_library(
alwayslink = 1,
)
cc_library(
name = "motion_analysis_calculator",
srcs = ["motion_analysis_calculator.cc"],
visibility = ["//visibility:public"],
deps = [
":motion_analysis_calculator_cc_proto",
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/formats:image_frame",
"//mediapipe/framework/formats:image_frame_opencv",
"//mediapipe/framework/formats:video_stream_header",
"//mediapipe/framework/port:integral_types",
"//mediapipe/framework/port:logging",
"//mediapipe/framework/port:ret_check",
"//mediapipe/framework/port:status",
"//mediapipe/util/tracking:camera_motion",
"//mediapipe/util/tracking:camera_motion_cc_proto",
"//mediapipe/util/tracking:frame_selection_cc_proto",
"//mediapipe/util/tracking:motion_analysis",
"//mediapipe/util/tracking:motion_estimation",
"//mediapipe/util/tracking:motion_models",
"//mediapipe/util/tracking:region_flow_cc_proto",
"@com_google_absl//absl/strings",
],
alwayslink = 1,
)
cc_library(
name = "flow_packager_calculator",
srcs = ["flow_packager_calculator.cc"],
visibility = ["//visibility:public"],
deps = [
":flow_packager_calculator_cc_proto",
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/port:integral_types",
"//mediapipe/framework/port:logging",
"//mediapipe/util/tracking:camera_motion_cc_proto",
"//mediapipe/util/tracking:flow_packager",
"//mediapipe/util/tracking:region_flow_cc_proto",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
],
alwayslink = 1,
)
cc_library(
name = "box_tracker_calculator",
srcs = ["box_tracker_calculator.cc"],
visibility = ["//visibility:public"],
deps = [
":box_tracker_calculator_cc_proto",
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/formats:image_frame",
"//mediapipe/framework/formats:image_frame_opencv",
"//mediapipe/framework/formats:video_stream_header", # fixdeps: keep -- required for exobazel build.
"//mediapipe/framework/port:integral_types",
"//mediapipe/framework/port:logging",
"//mediapipe/framework/port:parse_text_proto",
"//mediapipe/framework/port:ret_check",
"//mediapipe/framework/port:status",
"//mediapipe/framework/tool:options_util",
"//mediapipe/util/tracking",
"//mediapipe/util/tracking:box_tracker",
"//mediapipe/util/tracking:tracking_visualization_utilities",
"@com_google_absl//absl/strings",
],
alwayslink = 1,
)
cc_library(
name = "tracked_detection_manager_calculator",
srcs = ["tracked_detection_manager_calculator.cc"],
visibility = ["//visibility:public"],
deps = [
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework/formats:detection_cc_proto",
"//mediapipe/framework/formats:location_data_cc_proto",
"//mediapipe/framework/formats:rect_cc_proto",
"//mediapipe/framework/port:status",
"//mediapipe/util/tracking",
"//mediapipe/util/tracking:box_tracker",
"//mediapipe/util/tracking:tracked_detection",
"//mediapipe/util/tracking:tracked_detection_manager",
"//mediapipe/util/tracking:tracking_visualization_utilities",
"@com_google_absl//absl/container:node_hash_map",
],
alwayslink = 1,
)
filegroup(
name = "test_videos",
srcs = [
@@ -201,3 +350,64 @@ cc_test(
"//mediapipe/framework/port:parse_text_proto",
],
)
MEDIAPIPE_DEPS = [
"//mediapipe/calculators/video:box_tracker_calculator",
"//mediapipe/calculators/video:flow_packager_calculator",
"//mediapipe/calculators/video:motion_analysis_calculator",
"//mediapipe/framework/stream_handler:fixed_size_input_stream_handler",
"//mediapipe/framework/stream_handler:sync_set_input_stream_handler",
]
mediapipe_binary_graph(
name = "parallel_tracker_binarypb",
graph = "testdata/parallel_tracker_graph.pbtxt",
output_name = "testdata/parallel_tracker.binarypb",
visibility = ["//visibility:public"],
deps = MEDIAPIPE_DEPS,
)
mediapipe_binary_graph(
name = "tracker_binarypb",
graph = "testdata/tracker_graph.pbtxt",
output_name = "testdata/tracker.binarypb",
visibility = ["//visibility:public"],
deps = MEDIAPIPE_DEPS,
)
cc_test(
name = "tracking_graph_test",
size = "small",
srcs = ["tracking_graph_test.cc"],
copts = ["-DPARALLEL_INVOKER_ACTIVE"] + select({
"//mediapipe:apple": [],
"//mediapipe:android": [],
"//conditions:default": [],
}),
data = [
":testdata/lenna.png",
":testdata/parallel_tracker.binarypb",
":testdata/tracker.binarypb",
],
deps = [
":box_tracker_calculator",
":box_tracker_calculator_cc_proto",
":flow_packager_calculator",
":motion_analysis_calculator",
"//mediapipe/framework:calculator_cc_proto",
"//mediapipe/framework:calculator_framework",
"//mediapipe/framework:packet",
"//mediapipe/framework/deps:file_path",
"//mediapipe/framework/formats:image_frame",
"//mediapipe/framework/port:advanced_proto",
"//mediapipe/framework/port:core_proto",
"//mediapipe/framework/port:file_helpers",
"//mediapipe/framework/port:gtest_main",
"//mediapipe/framework/port:opencv_highgui",
"//mediapipe/framework/port:status",
"//mediapipe/framework/stream_handler:fixed_size_input_stream_handler",
"//mediapipe/framework/stream_handler:sync_set_input_stream_handler",
"//mediapipe/util/tracking:box_tracker_cc_proto",
"//mediapipe/util/tracking:tracking_cc_proto",
],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
// 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.
syntax = "proto2";
package mediapipe;
import "mediapipe/framework/calculator.proto";
import "mediapipe/util/tracking/box_tracker.proto";
message BoxTrackerCalculatorOptions {
extend CalculatorOptions {
optional BoxTrackerCalculatorOptions ext = 268767860;
}
optional BoxTrackerOptions tracker_options = 1;
// Initial position to be tracked. Can also be supplied as side packet or
// as input stream.
optional TimedBoxProtoList initial_position = 2;
// If set and VIZ stream is present, renders tracking data into the
// visualization.
optional bool visualize_tracking_data = 3 [default = false];
// If set and VIZ stream is present, renders the box state
// into the visualization.
optional bool visualize_state = 4 [default = false];
// If set and VIZ stream is present, renders the internal box state
// into the visualization.
optional bool visualize_internal_state = 5 [default = false];
// Size of the track data cache during streaming mode. This allows to buffer
// track_data's for fast forward tracking, i.e. any TimedBox received
// via input stream START_POS can be tracked towards the current track head
// (i.e. last received TrackingData). Measured in number of frames.
optional int32 streaming_track_data_cache_size = 6 [default = 0];
// Add a transition period of N frames to smooth the jump from original
// tracking to reset start pos with motion compensation. The transition will
// be a linear decay of original tracking result. 0 means no transition.
optional int32 start_pos_transition_frames = 7 [default = 0];
}
@@ -0,0 +1,281 @@
// 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 <stdio.h>
#include <fstream>
#include <memory>
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "mediapipe/calculators/video/flow_packager_calculator.pb.h"
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/port/integral_types.h"
#include "mediapipe/framework/port/logging.h"
#include "mediapipe/util/tracking/camera_motion.pb.h"
#include "mediapipe/util/tracking/flow_packager.h"
#include "mediapipe/util/tracking/region_flow.pb.h"
namespace mediapipe {
using mediapipe::CameraMotion;
using mediapipe::FlowPackager;
using mediapipe::RegionFlowFeatureList;
using mediapipe::TrackingData;
using mediapipe::TrackingDataChunk;
// A calculator that packages input CameraMotion and RegionFlowFeatureList
// into a TrackingData and optionally writes TrackingDataChunks to file.
//
// Input stream:
// FLOW: Input region flow (proto RegionFlowFeatureList).
// CAMERA: Input camera stream (proto CameraMotion, optional).
//
// Input side packets:
// CACHE_DIR: Optional caching directory tracking files are written to.
//
// Output streams.
// TRACKING: Output tracking data (proto TrackingData, per frame
// optional).
// TRACKING_CHUNK: Output tracking chunks (proto TrackingDataChunk,
// per chunk, optional), output at the first timestamp
// of each chunk.
// COMPLETE: Optional output packet sent on PreStream to
// to signal downstream calculators that all data has been
// processed and calculator is closed. Can be used to indicate
// that all data as been written to CACHE_DIR.
class FlowPackagerCalculator : public CalculatorBase {
public:
~FlowPackagerCalculator() override = default;
static ::mediapipe::Status GetContract(CalculatorContract* cc);
::mediapipe::Status Open(CalculatorContext* cc) override;
::mediapipe::Status Process(CalculatorContext* cc) override;
::mediapipe::Status Close(CalculatorContext* cc) override;
// Writes passed chunk to disk.
void WriteChunk(const TrackingDataChunk& chunk) const;
// Initializes next chunk for tracking beginning from last frame of
// current chunk (Chunking is design with one frame overlap).
void PrepareCurrentForNextChunk(TrackingDataChunk* chunk);
private:
FlowPackagerCalculatorOptions options_;
// Caching options.
bool use_caching_ = false;
bool build_chunk_ = false;
std::string cache_dir_;
int chunk_idx_ = -1;
TrackingDataChunk tracking_chunk_;
int frame_idx_ = 0;
Timestamp prev_timestamp_;
std::unique_ptr<FlowPackager> flow_packager_;
};
REGISTER_CALCULATOR(FlowPackagerCalculator);
::mediapipe::Status FlowPackagerCalculator::GetContract(
CalculatorContract* cc) {
if (!cc->Inputs().HasTag("FLOW")) {
return tool::StatusFail("No input flow was specified.");
}
cc->Inputs().Tag("FLOW").Set<RegionFlowFeatureList>();
if (cc->Inputs().HasTag("CAMERA")) {
cc->Inputs().Tag("CAMERA").Set<CameraMotion>();
}
if (cc->Outputs().HasTag("TRACKING")) {
cc->Outputs().Tag("TRACKING").Set<TrackingData>();
}
if (cc->Outputs().HasTag("TRACKING_CHUNK")) {
cc->Outputs().Tag("TRACKING_CHUNK").Set<TrackingDataChunk>();
}
if (cc->Outputs().HasTag("COMPLETE")) {
cc->Outputs().Tag("COMPLETE").Set<bool>();
}
if (cc->InputSidePackets().HasTag("CACHE_DIR")) {
cc->InputSidePackets().Tag("CACHE_DIR").Set<std::string>();
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status FlowPackagerCalculator::Open(CalculatorContext* cc) {
options_ = cc->Options<FlowPackagerCalculatorOptions>();
flow_packager_.reset(new FlowPackager(options_.flow_packager_options()));
use_caching_ = cc->InputSidePackets().HasTag("CACHE_DIR");
build_chunk_ = use_caching_ || cc->Outputs().HasTag("TRACKING_CHUNK");
if (use_caching_) {
cache_dir_ = cc->InputSidePackets().Tag("CACHE_DIR").Get<std::string>();
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status FlowPackagerCalculator::Process(CalculatorContext* cc) {
InputStream* flow_stream = &(cc->Inputs().Tag("FLOW"));
const RegionFlowFeatureList& flow = flow_stream->Get<RegionFlowFeatureList>();
const Timestamp timestamp = flow_stream->Value().Timestamp();
const CameraMotion* camera_motion = nullptr;
if (cc->Inputs().HasTag("CAMERA")) {
InputStream* camera_stream = &(cc->Inputs().Tag("CAMERA"));
camera_motion = &camera_stream->Get<CameraMotion>();
}
std::unique_ptr<TrackingData> tracking_data(new TrackingData());
flow_packager_->PackFlow(flow, camera_motion, tracking_data.get());
if (build_chunk_) {
if (chunk_idx_ < 0) { // Lazy init, determine first start.
chunk_idx_ =
timestamp.Value() / 1000 / options_.caching_chunk_size_msec();
tracking_chunk_.set_first_chunk(true);
}
CHECK_GE(chunk_idx_, 0);
TrackingDataChunk::Item* item = tracking_chunk_.add_item();
item->set_frame_idx(frame_idx_);
item->set_timestamp_usec(timestamp.Value());
if (frame_idx_ > 0) {
item->set_prev_timestamp_usec(prev_timestamp_.Value());
}
if (cc->Outputs().HasTag("TRACKING")) {
// Need to copy as output is requested.
*item->mutable_tracking_data() = *tracking_data;
} else {
item->mutable_tracking_data()->Swap(tracking_data.get());
}
const int next_chunk_msec =
options_.caching_chunk_size_msec() * (chunk_idx_ + 1);
if (timestamp.Value() / 1000 >= next_chunk_msec) {
if (cc->Outputs().HasTag("TRACKING_CHUNK")) {
cc->Outputs()
.Tag("TRACKING_CHUNK")
.Add(new TrackingDataChunk(tracking_chunk_),
Timestamp(tracking_chunk_.item(0).timestamp_usec()));
}
if (use_caching_) {
WriteChunk(tracking_chunk_);
}
PrepareCurrentForNextChunk(&tracking_chunk_);
}
}
if (cc->Outputs().HasTag("TRACKING")) {
cc->Outputs()
.Tag("TRACKING")
.Add(tracking_data.release(), flow_stream->Value().Timestamp());
}
prev_timestamp_ = timestamp;
++frame_idx_;
return ::mediapipe::OkStatus();
}
::mediapipe::Status FlowPackagerCalculator::Close(CalculatorContext* cc) {
if (frame_idx_ > 0) {
tracking_chunk_.set_last_chunk(true);
if (cc->Outputs().HasTag("TRACKING_CHUNK")) {
cc->Outputs()
.Tag("TRACKING_CHUNK")
.Add(new TrackingDataChunk(tracking_chunk_),
Timestamp(tracking_chunk_.item(0).timestamp_usec()));
}
if (use_caching_) {
WriteChunk(tracking_chunk_);
}
}
if (cc->Outputs().HasTag("COMPLETE")) {
cc->Outputs().Tag("COMPLETE").Add(new bool(true), Timestamp::PreStream());
}
return ::mediapipe::OkStatus();
}
void FlowPackagerCalculator::WriteChunk(const TrackingDataChunk& chunk) const {
if (chunk.item_size() == 0) {
LOG(ERROR) << "Write chunk called with empty tracking data."
<< "This can only occur if the spacing between frames "
<< "is larger than the requested chunk size. Try increasing "
<< "the chunk size";
return;
}
auto format_runtime =
absl::ParsedFormat<'d'>::New(options_.cache_file_format());
std::string chunk_file;
if (format_runtime) {
chunk_file =
cache_dir_ + "/" + absl::StrFormat(*format_runtime, chunk_idx_);
} else {
LOG(ERROR) << "chache_file_format wrong. fall back to chunk_%04d.";
chunk_file = cache_dir_ + "/" + absl::StrFormat("chunk_%04d", chunk_idx_);
}
std::string data;
chunk.SerializeToString(&data);
const char* temp_filename = tempnam(cache_dir_.c_str(), nullptr);
std::ofstream out_file(temp_filename);
if (!out_file) {
LOG(ERROR) << "Could not open " << temp_filename;
} else {
out_file.write(data.data(), data.size());
}
if (rename(temp_filename, chunk_file.c_str()) != 0) {
LOG(ERROR) << "Failed to rename to " << chunk_file;
}
LOG(INFO) << "Wrote chunk : " << chunk_file;
}
void FlowPackagerCalculator::PrepareCurrentForNextChunk(
TrackingDataChunk* chunk) {
CHECK(chunk);
if (chunk->item_size() == 0) {
LOG(ERROR) << "Called with empty chunk. Unexpected.";
return;
}
chunk->set_first_chunk(false);
// Buffer last item for next chunk.
TrackingDataChunk::Item last_item;
last_item.Swap(chunk->mutable_item(chunk->item_size() - 1));
chunk->Clear();
chunk->add_item()->Swap(&last_item);
++chunk_idx_;
}
} // namespace mediapipe
@@ -0,0 +1,36 @@
// 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.
syntax = "proto2";
package mediapipe;
import "mediapipe/framework/calculator.proto";
import "mediapipe/util/tracking/flow_packager.proto";
message FlowPackagerCalculatorOptions {
extend CalculatorOptions {
optional FlowPackagerCalculatorOptions ext = 271236147;
}
optional mediapipe.FlowPackagerOptions flow_packager_options = 1;
// Chunk size for caching files that are written to the externally specified
// caching directory. Specified in msec.
// Note that each chunk always contains at its end the first frame of the
// next chunk (to enable forward tracking across chunk boundaries).
optional int32 caching_chunk_size_msec = 2 [default = 2500];
optional string cache_file_format = 3 [default = "chunk_%04d"];
}
@@ -0,0 +1,988 @@
// 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 <cmath>
#include <fstream>
#include <memory>
#include "absl/strings/numbers.h"
#include "absl/strings/str_split.h"
#include "absl/strings/string_view.h"
#include "mediapipe/calculators/video/motion_analysis_calculator.pb.h"
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/formats/image_frame_opencv.h"
#include "mediapipe/framework/formats/video_stream_header.h"
#include "mediapipe/framework/port/integral_types.h"
#include "mediapipe/framework/port/logging.h"
#include "mediapipe/framework/port/ret_check.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/util/tracking/camera_motion.h"
#include "mediapipe/util/tracking/camera_motion.pb.h"
#include "mediapipe/util/tracking/frame_selection.pb.h"
#include "mediapipe/util/tracking/motion_analysis.h"
#include "mediapipe/util/tracking/motion_estimation.h"
#include "mediapipe/util/tracking/motion_models.h"
#include "mediapipe/util/tracking/region_flow.pb.h"
namespace mediapipe {
using mediapipe::AffineAdapter;
using mediapipe::CameraMotion;
using mediapipe::FrameSelectionResult;
using mediapipe::Homography;
using mediapipe::HomographyAdapter;
using mediapipe::LinearSimilarityModel;
using mediapipe::MixtureHomography;
using mediapipe::MixtureRowWeights;
using mediapipe::MotionAnalysis;
using mediapipe::ProjectViaFit;
using mediapipe::RegionFlowComputationOptions;
using mediapipe::RegionFlowFeatureList;
using mediapipe::SalientPointFrame;
using mediapipe::TranslationModel;
const char kOptionsTag[] = "OPTIONS";
// A calculator that performs motion analysis on an incoming video stream.
//
// Input streams: (at least one of them is required).
// VIDEO: The input video stream (ImageFrame, sRGB, sRGBA or GRAY8).
// SELECTION: Optional input stream to perform analysis only on selected
// frames. If present needs to contain camera motion
// and features.
//
// Input side packets:
// CSV_FILE: Read motion models as homographies from CSV file. Expected
// to be defined in the frame domain (un-normalized).
// Should store 9 floats per row.
// Specify number of homographies per frames via option
// meta_models_per_frame. For values > 1, MixtureHomographies
// are created, for value == 1, a single Homography is used.
// DOWNSAMPLE: Optionally specify downsampling factor via input side packet
// overriding value in the graph settings.
// Output streams (all are optional).
// FLOW: Sparse feature tracks in form of proto RegionFlowFeatureList.
// CAMERA: Camera motion as proto CameraMotion describing the per frame-
// pair motion. Has VideoHeader from input video.
// SALIENCY: Foreground saliency (objects moving different from the
// background) as proto SalientPointFrame.
// VIZ: Visualization stream as ImageFrame, sRGB, visualizing
// features and saliency (set via
// analysis_options().visualization_options())
// DENSE_FG: Dense foreground stream, describing per-pixel foreground-
// ness as confidence between 0 (background) and 255
// (foreground). Output is ImageFrame (GRAY8).
// VIDEO_OUT: Optional output stream when SELECTION is used. Output is input
// VIDEO at the selected frames. Required VIDEO to be present.
// GRAY_VIDEO_OUT: Optional output stream for downsampled, grayscale video.
// Requires VIDEO to be present and SELECTION to not be used.
class MotionAnalysisCalculator : public CalculatorBase {
// TODO: Activate once leakr approval is ready.
// typedef com::google::android::libraries::micro::proto::Data HomographyData;
public:
~MotionAnalysisCalculator() override = default;
static ::mediapipe::Status GetContract(CalculatorContract* cc);
::mediapipe::Status Open(CalculatorContext* cc) override;
::mediapipe::Status Process(CalculatorContext* cc) override;
::mediapipe::Status Close(CalculatorContext* cc) override;
private:
// Outputs results to Outputs() if MotionAnalysis buffered sufficient results.
// Otherwise no-op. Set flush to true to force output of all buffered data.
void OutputMotionAnalyzedFrames(bool flush, CalculatorContext* cc);
// Lazy init function to be called on Process.
::mediapipe::Status InitOnProcess(InputStream* video_stream,
InputStream* selection_stream);
// Parses CSV file contents to homographies.
bool ParseModelCSV(const std::string& contents,
std::deque<Homography>* homographies);
// Turns list of 9-tuple floating values into set of homographies.
bool HomographiesFromValues(const std::vector<float>& homog_values,
std::deque<Homography>* homographies);
// Appends CameraMotions and features from homographies.
// Set append_identity to true to add an identity transform to the beginning
// of the each list *in addition* to the motions derived from homographies.
void AppendCameraMotionsFromHomographies(
const std::deque<Homography>& homographies, bool append_identity,
std::deque<CameraMotion>* camera_motions,
std::deque<RegionFlowFeatureList>* features);
// Helper function to subtract current metadata motion from features. Used
// for hybrid estimation case.
void SubtractMetaMotion(const CameraMotion& meta_motion,
RegionFlowFeatureList* features);
// Inverse of above function to add back meta motion and replace
// feature location with originals after estimation.
void AddMetaMotion(const CameraMotion& meta_motion,
const RegionFlowFeatureList& meta_features,
RegionFlowFeatureList* features, CameraMotion* motion);
MotionAnalysisCalculatorOptions options_;
int frame_width_ = -1;
int frame_height_ = -1;
int frame_idx_ = 0;
// Buffers incoming video frame packets (if visualization output is requested)
std::vector<Packet> packet_buffer_;
// Buffers incoming timestamps until MotionAnalysis is ready to output via
// above OutputMotionAnalyzedFrames.
std::vector<Timestamp> timestamp_buffer_;
// Input indicators for each stream.
bool selection_input_ = false;
bool video_input_ = false;
// Output indicators for each stream.
bool region_flow_feature_output_ = false;
bool camera_motion_output_ = false;
bool saliency_output_ = false;
bool visualize_output_ = false;
bool dense_foreground_output_ = false;
bool video_output_ = false;
bool grayscale_output_ = false;
bool csv_file_input_ = false;
// Inidicates if saliency should be computed.
bool with_saliency_ = false;
// Set if hybrid meta analysis - see proto for details.
bool hybrid_meta_analysis_ = false;
// Concatenated motions for each selected frame. Used in case
// hybrid estimation is requested to fallback to valid models.
std::deque<CameraMotion> selected_motions_;
// Normalized homographies from CSV file or metadata.
std::deque<Homography> meta_homographies_;
std::deque<CameraMotion> meta_motions_;
std::deque<RegionFlowFeatureList> meta_features_;
// Offset into above meta_motions_ and features_ when using
// hybrid meta analysis.
int hybrid_meta_offset_ = 0;
std::unique_ptr<MotionAnalysis> motion_analysis_;
std::unique_ptr<MixtureRowWeights> row_weights_;
};
REGISTER_CALCULATOR(MotionAnalysisCalculator);
::mediapipe::Status MotionAnalysisCalculator::GetContract(
CalculatorContract* cc) {
if (cc->Inputs().HasTag("VIDEO")) {
cc->Inputs().Tag("VIDEO").Set<ImageFrame>();
}
// Optional input stream from frame selection calculator.
if (cc->Inputs().HasTag("SELECTION")) {
cc->Inputs().Tag("SELECTION").Set<FrameSelectionResult>();
}
RET_CHECK(cc->Inputs().HasTag("VIDEO") || cc->Inputs().HasTag("SELECTION"))
<< "Either VIDEO, SELECTION must be specified.";
if (cc->Outputs().HasTag("FLOW")) {
cc->Outputs().Tag("FLOW").Set<RegionFlowFeatureList>();
}
if (cc->Outputs().HasTag("CAMERA")) {
cc->Outputs().Tag("CAMERA").Set<CameraMotion>();
}
if (cc->Outputs().HasTag("SALIENCY")) {
cc->Outputs().Tag("SALIENCY").Set<SalientPointFrame>();
}
if (cc->Outputs().HasTag("VIZ")) {
cc->Outputs().Tag("VIZ").Set<ImageFrame>();
}
if (cc->Outputs().HasTag("DENSE_FG")) {
cc->Outputs().Tag("DENSE_FG").Set<ImageFrame>();
}
if (cc->Outputs().HasTag("VIDEO_OUT")) {
cc->Outputs().Tag("VIDEO_OUT").Set<ImageFrame>();
}
if (cc->Outputs().HasTag("GRAY_VIDEO_OUT")) {
// We only output grayscale video if we're actually performing full region-
// flow analysis on the video.
RET_CHECK(cc->Inputs().HasTag("VIDEO") &&
!cc->Inputs().HasTag("SELECTION"));
cc->Outputs().Tag("GRAY_VIDEO_OUT").Set<ImageFrame>();
}
if (cc->InputSidePackets().HasTag("CSV_FILE")) {
cc->InputSidePackets().Tag("CSV_FILE").Set<std::string>();
}
if (cc->InputSidePackets().HasTag("DOWNSAMPLE")) {
cc->InputSidePackets().Tag("DOWNSAMPLE").Set<float>();
}
if (cc->InputSidePackets().HasTag(kOptionsTag)) {
cc->InputSidePackets().Tag(kOptionsTag).Set<CalculatorOptions>();
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status MotionAnalysisCalculator::Open(CalculatorContext* cc) {
options_ =
tool::RetrieveOptions(cc->Options<MotionAnalysisCalculatorOptions>(),
cc->InputSidePackets(), kOptionsTag);
video_input_ = cc->Inputs().HasTag("VIDEO");
selection_input_ = cc->Inputs().HasTag("SELECTION");
region_flow_feature_output_ = cc->Outputs().HasTag("FLOW");
camera_motion_output_ = cc->Outputs().HasTag("CAMERA");
saliency_output_ = cc->Outputs().HasTag("SALIENCY");
visualize_output_ = cc->Outputs().HasTag("VIZ");
dense_foreground_output_ = cc->Outputs().HasTag("DENSE_FG");
video_output_ = cc->Outputs().HasTag("VIDEO_OUT");
grayscale_output_ = cc->Outputs().HasTag("GRAY_VIDEO_OUT");
csv_file_input_ = cc->InputSidePackets().HasTag("CSV_FILE");
hybrid_meta_analysis_ = options_.meta_analysis() ==
MotionAnalysisCalculatorOptions::META_ANALYSIS_HYBRID;
if (video_output_) {
RET_CHECK(selection_input_) << "VIDEO_OUT requires SELECTION input";
}
if (selection_input_) {
switch (options_.selection_analysis()) {
case MotionAnalysisCalculatorOptions::NO_ANALYSIS_USE_SELECTION:
RET_CHECK(!visualize_output_)
<< "Visualization not supported for NO_ANALYSIS_USE_SELECTION";
RET_CHECK(!dense_foreground_output_)
<< "Dense foreground not supported for NO_ANALYSIS_USE_SELECTION";
RET_CHECK(!saliency_output_)
<< "Saliency output not supported for NO_ANALYSIS_USE_SELECTION";
break;
case MotionAnalysisCalculatorOptions::ANALYSIS_RECOMPUTE:
case MotionAnalysisCalculatorOptions::ANALYSIS_WITH_SEED:
RET_CHECK(video_input_) << "Need video input for feature tracking.";
break;
case MotionAnalysisCalculatorOptions::ANALYSIS_FROM_FEATURES:
// Nothing to add here.
break;
}
}
if (visualize_output_ || dense_foreground_output_ || video_output_) {
RET_CHECK(video_input_) << "Video input required.";
}
if (csv_file_input_) {
RET_CHECK(!selection_input_)
<< "Can not use selection input with csv input.";
if (!hybrid_meta_analysis_) {
RET_CHECK(!saliency_output_ && !visualize_output_ &&
!dense_foreground_output_ && !grayscale_output_)
<< "CSV file and meta input only supports flow and camera motion "
<< "output when using metadata only.";
}
}
if (csv_file_input_) {
// Read from file and parse.
const std::string filename =
cc->InputSidePackets().Tag("CSV_FILE").Get<std::string>();
std::string file_contents;
std::ifstream input_file(filename, std::ios::in);
input_file.seekg(0, std::ios::end);
const int file_length = input_file.tellg();
file_contents.resize(file_length);
input_file.seekg(0, std::ios::beg);
input_file.read(&file_contents[0], file_length);
input_file.close();
RET_CHECK(ParseModelCSV(file_contents, &meta_homographies_))
<< "Could not parse CSV file";
}
// Get video header from video or selection input if present.
const VideoHeader* video_header = nullptr;
if (video_input_ && !cc->Inputs().Tag("VIDEO").Header().IsEmpty()) {
video_header = &(cc->Inputs().Tag("VIDEO").Header().Get<VideoHeader>());
} else if (selection_input_ &&
!cc->Inputs().Tag("SELECTION").Header().IsEmpty()) {
video_header = &(cc->Inputs().Tag("SELECTION").Header().Get<VideoHeader>());
} else {
LOG(WARNING) << "No input video header found. Downstream calculators "
"expecting video headers are likely to fail.";
}
with_saliency_ = options_.analysis_options().compute_motion_saliency();
// Force computation of saliency if requested as output.
if (cc->Outputs().HasTag("SALIENCY")) {
with_saliency_ = true;
if (!options_.analysis_options().compute_motion_saliency()) {
LOG(WARNING) << "Enable saliency computation. Set "
<< "compute_motion_saliency to true to silence this "
<< "warning.";
options_.mutable_analysis_options()->set_compute_motion_saliency(true);
}
}
if (options_.bypass_mode()) {
cc->SetOffset(TimestampDiff(0));
}
if (cc->InputSidePackets().HasTag("DOWNSAMPLE")) {
options_.mutable_analysis_options()
->mutable_flow_options()
->set_downsample_factor(
cc->InputSidePackets().Tag("DOWNSAMPLE").Get<float>());
}
// If no video header is provided, just return and initialize on the first
// Process() call.
if (video_header == nullptr) {
return ::mediapipe::OkStatus();
}
////////////// EARLY RETURN; ONLY HEADER OUTPUT SHOULD GO HERE ///////////////
if (visualize_output_) {
cc->Outputs().Tag("VIZ").SetHeader(Adopt(new VideoHeader(*video_header)));
}
if (video_output_) {
cc->Outputs()
.Tag("VIDEO_OUT")
.SetHeader(Adopt(new VideoHeader(*video_header)));
}
if (cc->Outputs().HasTag("DENSE_FG")) {
std::unique_ptr<VideoHeader> foreground_header(
new VideoHeader(*video_header));
foreground_header->format = ImageFormat::GRAY8;
cc->Outputs().Tag("DENSE_FG").SetHeader(Adopt(foreground_header.release()));
}
if (cc->Outputs().HasTag("CAMERA")) {
cc->Outputs().Tag("CAMERA").SetHeader(
Adopt(new VideoHeader(*video_header)));
}
if (cc->Outputs().HasTag("SALIENCY")) {
cc->Outputs()
.Tag("SALIENCY")
.SetHeader(Adopt(new VideoHeader(*video_header)));
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status MotionAnalysisCalculator::Process(CalculatorContext* cc) {
if (options_.bypass_mode()) {
return ::mediapipe::OkStatus();
}
InputStream* video_stream =
video_input_ ? &(cc->Inputs().Tag("VIDEO")) : nullptr;
InputStream* selection_stream =
selection_input_ ? &(cc->Inputs().Tag("SELECTION")) : nullptr;
// Checked on Open.
CHECK(video_stream || selection_stream);
// Lazy init.
if (frame_width_ < 0 || frame_height_ < 0) {
MP_RETURN_IF_ERROR(InitOnProcess(video_stream, selection_stream));
}
const Timestamp timestamp = cc->InputTimestamp();
if ((csv_file_input_) && !hybrid_meta_analysis_) {
if (camera_motion_output_) {
RET_CHECK(!meta_motions_.empty()) << "Insufficient metadata.";
CameraMotion output_motion = meta_motions_.front();
meta_motions_.pop_front();
output_motion.set_timestamp_usec(timestamp.Value());
cc->Outputs().Tag("CAMERA").Add(new CameraMotion(output_motion),
timestamp);
}
if (region_flow_feature_output_) {
RET_CHECK(!meta_features_.empty()) << "Insufficient frames in CSV file";
RegionFlowFeatureList output_features = meta_features_.front();
meta_features_.pop_front();
output_features.set_timestamp_usec(timestamp.Value());
cc->Outputs().Tag("FLOW").Add(new RegionFlowFeatureList(output_features),
timestamp);
}
++frame_idx_;
return ::mediapipe::OkStatus();
}
if (motion_analysis_ == nullptr) {
// We do not need MotionAnalysis when using just metadata.
motion_analysis_.reset(new MotionAnalysis(options_.analysis_options(),
frame_width_, frame_height_));
}
std::unique_ptr<FrameSelectionResult> frame_selection_result;
// Always use frame if selection is not activated.
bool use_frame = !selection_input_;
if (selection_input_) {
CHECK(selection_stream);
// Fill in timestamps we process.
if (!selection_stream->Value().IsEmpty()) {
ASSIGN_OR_RETURN(
frame_selection_result,
selection_stream->Value().ConsumeOrCopy<FrameSelectionResult>());
use_frame = true;
// Make sure both features and camera motion are present.
RET_CHECK(frame_selection_result->has_camera_motion() &&
frame_selection_result->has_features())
<< "Frame selection input error at: " << timestamp
<< " both camera motion and features need to be "
"present in FrameSelectionResult. "
<< frame_selection_result->has_camera_motion() << " , "
<< frame_selection_result->has_features();
}
}
if (selection_input_ && use_frame &&
options_.selection_analysis() ==
MotionAnalysisCalculatorOptions::NO_ANALYSIS_USE_SELECTION) {
// Output concatenated results, nothing to compute here.
if (camera_motion_output_) {
cc->Outputs().Tag("CAMERA").Add(
frame_selection_result->release_camera_motion(), timestamp);
}
if (region_flow_feature_output_) {
cc->Outputs().Tag("FLOW").Add(frame_selection_result->release_features(),
timestamp);
}
if (video_output_) {
cc->Outputs().Tag("VIDEO_OUT").AddPacket(video_stream->Value());
}
return ::mediapipe::OkStatus();
}
if (use_frame) {
if (!selection_input_) {
const cv::Mat input_view =
formats::MatView(&video_stream->Get<ImageFrame>());
if (hybrid_meta_analysis_) {
// Seed with meta homography.
RET_CHECK(hybrid_meta_offset_ < meta_motions_.size())
<< "Not enough metadata received for hybrid meta analysis";
Homography initial_transform =
meta_motions_[hybrid_meta_offset_].homography();
std::function<void(RegionFlowFeatureList*)> subtract_helper = std::bind(
&MotionAnalysisCalculator::SubtractMetaMotion, this,
meta_motions_[hybrid_meta_offset_], std::placeholders::_1);
// Keep original features before modification around.
motion_analysis_->AddFrameGeneric(
input_view, timestamp.Value(), initial_transform, nullptr, nullptr,
&subtract_helper, &meta_features_[hybrid_meta_offset_]);
++hybrid_meta_offset_;
} else {
motion_analysis_->AddFrame(input_view, timestamp.Value());
}
} else {
selected_motions_.push_back(frame_selection_result->camera_motion());
switch (options_.selection_analysis()) {
case MotionAnalysisCalculatorOptions::NO_ANALYSIS_USE_SELECTION:
return ::mediapipe::UnknownErrorBuilder(MEDIAPIPE_LOC)
<< "Should not reach this point!";
case MotionAnalysisCalculatorOptions::ANALYSIS_FROM_FEATURES:
motion_analysis_->AddFeatures(frame_selection_result->features());
break;
case MotionAnalysisCalculatorOptions::ANALYSIS_RECOMPUTE: {
const cv::Mat input_view =
formats::MatView(&video_stream->Get<ImageFrame>());
motion_analysis_->AddFrame(input_view, timestamp.Value());
break;
}
case MotionAnalysisCalculatorOptions::ANALYSIS_WITH_SEED: {
Homography homography;
CameraMotionToHomography(frame_selection_result->camera_motion(),
&homography);
const cv::Mat input_view =
formats::MatView(&video_stream->Get<ImageFrame>());
motion_analysis_->AddFrameGeneric(input_view, timestamp.Value(),
homography, &homography);
break;
}
}
}
timestamp_buffer_.push_back(timestamp);
++frame_idx_;
VLOG_EVERY_N(0, 100) << "Analyzed frame " << frame_idx_;
// Buffer input frames only if visualization is requested.
if (visualize_output_ || video_output_) {
packet_buffer_.push_back(video_stream->Value());
}
// If requested, output grayscale thumbnails
if (grayscale_output_) {
cv::Mat grayscale_mat = motion_analysis_->GetGrayscaleFrameFromResults();
std::unique_ptr<ImageFrame> grayscale_image(new ImageFrame(
ImageFormat::GRAY8, grayscale_mat.cols, grayscale_mat.rows));
cv::Mat image_frame_mat = formats::MatView(grayscale_image.get());
grayscale_mat.copyTo(image_frame_mat);
cc->Outputs()
.Tag("GRAY_VIDEO_OUT")
.Add(grayscale_image.release(), timestamp);
}
// Output other results, if we have any yet.
OutputMotionAnalyzedFrames(false, cc);
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status MotionAnalysisCalculator::Close(CalculatorContext* cc) {
// Guard against empty videos.
if (motion_analysis_) {
OutputMotionAnalyzedFrames(true, cc);
}
if (csv_file_input_) {
if (!meta_motions_.empty()) {
LOG(ERROR) << "More motions than frames. Unexpected! Remainder: "
<< meta_motions_.size();
}
}
return ::mediapipe::OkStatus();
}
void MotionAnalysisCalculator::OutputMotionAnalyzedFrames(
bool flush, CalculatorContext* cc) {
std::vector<std::unique_ptr<RegionFlowFeatureList>> features;
std::vector<std::unique_ptr<CameraMotion>> camera_motions;
std::vector<std::unique_ptr<SalientPointFrame>> saliency;
const int buffer_size = timestamp_buffer_.size();
const int num_results = motion_analysis_->GetResults(
flush, &features, &camera_motions, with_saliency_ ? &saliency : nullptr);
CHECK_LE(num_results, buffer_size);
if (num_results == 0) {
return;
}
for (int k = 0; k < num_results; ++k) {
// Region flow features and camera motion for this frame.
auto& feature_list = features[k];
auto& camera_motion = camera_motions[k];
const Timestamp timestamp = timestamp_buffer_[k];
if (selection_input_ && options_.hybrid_selection_camera()) {
if (camera_motion->type() > selected_motions_.front().type()) {
// Composited type is more stable.
camera_motion->Swap(&selected_motions_.front());
}
selected_motions_.pop_front();
}
if (hybrid_meta_analysis_) {
AddMetaMotion(meta_motions_.front(), meta_features_.front(),
feature_list.get(), camera_motion.get());
meta_motions_.pop_front();
meta_features_.pop_front();
}
// Video frame for visualization.
std::unique_ptr<ImageFrame> visualization_frame;
cv::Mat visualization;
if (visualize_output_) {
// Initialize visualization frame with original frame.
visualization_frame.reset(new ImageFrame());
visualization_frame->CopyFrom(packet_buffer_[k].Get<ImageFrame>(), 16);
visualization = formats::MatView(visualization_frame.get());
motion_analysis_->RenderResults(
*feature_list, *camera_motion,
with_saliency_ ? saliency[k].get() : nullptr, &visualization);
cc->Outputs().Tag("VIZ").Add(visualization_frame.release(), timestamp);
}
// Output dense foreground mask.
if (dense_foreground_output_) {
std::unique_ptr<ImageFrame> foreground_frame(
new ImageFrame(ImageFormat::GRAY8, frame_width_, frame_height_));
cv::Mat foreground = formats::MatView(foreground_frame.get());
motion_analysis_->ComputeDenseForeground(*feature_list, *camera_motion,
&foreground);
cc->Outputs().Tag("DENSE_FG").Add(foreground_frame.release(), timestamp);
}
// Output flow features if requested.
if (region_flow_feature_output_) {
cc->Outputs().Tag("FLOW").Add(feature_list.release(), timestamp);
}
// Output camera motion.
if (camera_motion_output_) {
cc->Outputs().Tag("CAMERA").Add(camera_motion.release(), timestamp);
}
if (video_output_) {
cc->Outputs().Tag("VIDEO_OUT").AddPacket(packet_buffer_[k]);
}
// Output saliency.
if (saliency_output_) {
cc->Outputs().Tag("SALIENCY").Add(saliency[k].release(), timestamp);
}
}
if (hybrid_meta_analysis_) {
hybrid_meta_offset_ -= num_results;
CHECK_GE(hybrid_meta_offset_, 0);
}
timestamp_buffer_.erase(timestamp_buffer_.begin(),
timestamp_buffer_.begin() + num_results);
if (visualize_output_ || video_output_) {
packet_buffer_.erase(packet_buffer_.begin(),
packet_buffer_.begin() + num_results);
}
}
::mediapipe::Status MotionAnalysisCalculator::InitOnProcess(
InputStream* video_stream, InputStream* selection_stream) {
if (video_stream) {
frame_width_ = video_stream->Get<ImageFrame>().Width();
frame_height_ = video_stream->Get<ImageFrame>().Height();
// Ensure image options are set correctly.
auto* region_options =
options_.mutable_analysis_options()->mutable_flow_options();
// Use two possible formats to account for different channel orders.
RegionFlowComputationOptions::ImageFormat image_format;
RegionFlowComputationOptions::ImageFormat image_format2;
switch (video_stream->Get<ImageFrame>().Format()) {
case ImageFormat::GRAY8:
image_format = image_format2 =
RegionFlowComputationOptions::FORMAT_GRAYSCALE;
break;
case ImageFormat::SRGB:
image_format = RegionFlowComputationOptions::FORMAT_RGB;
image_format2 = RegionFlowComputationOptions::FORMAT_BGR;
break;
case ImageFormat::SRGBA:
image_format = RegionFlowComputationOptions::FORMAT_RGBA;
image_format2 = RegionFlowComputationOptions::FORMAT_BGRA;
break;
default:
RET_CHECK(false) << "Unsupported image format.";
}
if (region_options->image_format() != image_format &&
region_options->image_format() != image_format2) {
LOG(WARNING) << "Requested image format in RegionFlowComputation "
<< "does not match video stream format. Overriding.";
region_options->set_image_format(image_format);
}
// Account for downsampling mode INPUT_SIZE. In this case we are handed
// already downsampled frames but the resulting CameraMotion should
// be computed on higher resolution as specifed by the downsample scale.
if (region_options->downsample_mode() ==
RegionFlowComputationOptions::DOWNSAMPLE_TO_INPUT_SIZE) {
const float scale = region_options->downsample_factor();
frame_width_ = static_cast<int>(std::round(frame_width_ * scale));
frame_height_ = static_cast<int>(std::round(frame_height_ * scale));
}
} else if (selection_stream) {
const auto& camera_motion =
selection_stream->Get<FrameSelectionResult>().camera_motion();
frame_width_ = camera_motion.frame_width();
frame_height_ = camera_motion.frame_height();
} else {
LOG(FATAL) << "Either VIDEO or SELECTION stream need to be specified.";
}
// Filled by CSV file parsing.
if (!meta_homographies_.empty()) {
CHECK(csv_file_input_);
AppendCameraMotionsFromHomographies(meta_homographies_,
true, // append identity.
&meta_motions_, &meta_features_);
meta_homographies_.clear();
}
// Filter weights before using for hybrid mode.
if (hybrid_meta_analysis_) {
auto* motion_options =
options_.mutable_analysis_options()->mutable_motion_options();
motion_options->set_filter_initialized_irls_weights(true);
}
return ::mediapipe::OkStatus();
}
bool MotionAnalysisCalculator::ParseModelCSV(
const std::string& contents, std::deque<Homography>* homographies) {
std::vector<absl::string_view> values =
absl::StrSplit(contents, absl::ByAnyChar(",\n"));
// Trim off any empty lines.
while (values.back().empty()) {
values.pop_back();
}
// Convert to float.
std::vector<float> homog_values;
homog_values.reserve(values.size());
for (const auto& value : values) {
double value_64f;
if (!absl::SimpleAtod(value, &value_64f)) {
LOG(ERROR) << "Not a double, expected!";
return false;
}
homog_values.push_back(value_64f);
}
return HomographiesFromValues(homog_values, homographies);
}
bool MotionAnalysisCalculator::HomographiesFromValues(
const std::vector<float>& homog_values,
std::deque<Homography>* homographies) {
CHECK(homographies);
// Obvious constants are obvious :D
constexpr int kHomographyValues = 9;
if (homog_values.size() % kHomographyValues != 0) {
LOG(ERROR) << "Contents not a multiple of " << kHomographyValues;
return false;
}
for (int k = 0; k < homog_values.size(); k += kHomographyValues) {
std::vector<double> h_vals(kHomographyValues);
for (int l = 0; l < kHomographyValues; ++l) {
h_vals[l] = homog_values[k + l];
}
// Normalize last entry to 1.
if (h_vals[kHomographyValues - 1] == 0) {
LOG(ERROR) << "Degenerate homography, last entry is zero";
return false;
}
const double scale = 1.0f / h_vals[kHomographyValues - 1];
for (int l = 0; l < kHomographyValues; ++l) {
h_vals[l] *= scale;
}
Homography h = HomographyAdapter::FromDoublePointer(h_vals.data(), false);
homographies->push_back(h);
}
if (homographies->size() % options_.meta_models_per_frame() != 0) {
LOG(ERROR) << "Total homographies not a multiple of specified models "
<< "per frame.";
return false;
}
return true;
}
void MotionAnalysisCalculator::SubtractMetaMotion(
const CameraMotion& meta_motion, RegionFlowFeatureList* features) {
if (meta_motion.mixture_homography().model_size() > 0) {
CHECK(row_weights_ != nullptr);
RegionFlowFeatureListViaTransform(meta_motion.mixture_homography(),
features, -1.0f,
1.0f, // subtract transformed.
true, // replace feature loc.
row_weights_.get());
} else {
RegionFlowFeatureListViaTransform(meta_motion.homography(), features, -1.0f,
1.0f, // subtract transformed.
true); // replace feature loc.
}
// Clamp transformed features to domain and handle outliers.
const float domain_diam =
hypot(features->frame_width(), features->frame_height());
const float motion_mag = meta_motion.average_magnitude();
// Same irls fraction as used by MODEL_MIXTURE_HOMOGRAPHY scaling in
// MotionEstimation.
const float irls_fraction = options_.analysis_options()
.motion_options()
.irls_mixture_fraction_scale() *
options_.analysis_options()
.motion_options()
.irls_motion_magnitude_fraction();
float err_scale = std::max(1.0f, motion_mag * irls_fraction);
const float max_err =
options_.meta_outlier_domain_ratio() * domain_diam * err_scale;
const float max_err_sq = max_err * max_err;
for (auto& feature : *features->mutable_feature()) {
feature.set_x(
std::max(0.0f, std::min(features->frame_width() - 1.0f, feature.x())));
feature.set_y(
std::max(0.0f, std::min(features->frame_height() - 1.0f, feature.y())));
// Label anything with large residual motion an outlier.
if (FeatureFlow(feature).Norm2() > max_err_sq) {
feature.set_irls_weight(0.0f);
}
}
}
void MotionAnalysisCalculator::AddMetaMotion(
const CameraMotion& meta_motion, const RegionFlowFeatureList& meta_features,
RegionFlowFeatureList* features, CameraMotion* motion) {
// Restore old feature location.
CHECK_EQ(meta_features.feature_size(), features->feature_size());
for (int k = 0; k < meta_features.feature_size(); ++k) {
auto feature = features->mutable_feature(k);
const auto& meta_feature = meta_features.feature(k);
feature->set_x(meta_feature.x());
feature->set_y(meta_feature.y());
feature->set_dx(meta_feature.dx());
feature->set_dy(meta_feature.dy());
}
// Composite camera motion.
*motion = ComposeCameraMotion(*motion, meta_motion);
// Restore type from metadata, i.e. do not declare motions as invalid.
motion->set_type(meta_motion.type());
motion->set_match_frame(-1);
}
void MotionAnalysisCalculator::AppendCameraMotionsFromHomographies(
const std::deque<Homography>& homographies, bool append_identity,
std::deque<CameraMotion>* camera_motions,
std::deque<RegionFlowFeatureList>* features) {
CHECK(camera_motions);
CHECK(features);
CameraMotion identity;
identity.set_frame_width(frame_width_);
identity.set_frame_height(frame_height_);
*identity.mutable_translation() = TranslationModel();
*identity.mutable_linear_similarity() = LinearSimilarityModel();
*identity.mutable_homography() = Homography();
identity.set_type(CameraMotion::VALID);
identity.set_match_frame(0);
RegionFlowFeatureList empty_list;
empty_list.set_long_tracks(true);
empty_list.set_match_frame(-1);
empty_list.set_frame_width(frame_width_);
empty_list.set_frame_height(frame_height_);
if (append_identity) {
camera_motions->push_back(identity);
features->push_back(empty_list);
}
const int models_per_frame = options_.meta_models_per_frame();
CHECK_GT(models_per_frame, 0) << "At least one model per frame is needed";
CHECK_EQ(0, homographies.size() % models_per_frame);
const int num_frames = homographies.size() / models_per_frame;
// Heuristic sigma, similar to what we use for rolling shutter removal.
const float mixture_sigma = 1.0f / models_per_frame;
if (row_weights_ == nullptr) {
row_weights_.reset(new MixtureRowWeights(frame_height_,
frame_height_ / 10, // 10% margin
mixture_sigma * frame_height_,
1.0f, models_per_frame));
}
for (int f = 0; f < num_frames; ++f) {
MixtureHomography mix_homog;
const int model_start = f * models_per_frame;
for (int k = 0; k < models_per_frame; ++k) {
const Homography& homog = homographies[model_start + k];
*mix_homog.add_model() = ModelInvert(homog);
}
CameraMotion c = identity;
c.set_match_frame(-1);
if (mix_homog.model_size() > 1) {
*c.mutable_mixture_homography() = mix_homog;
c.set_mixture_row_sigma(mixture_sigma);
for (int k = 0; k < models_per_frame; ++k) {
c.add_mixture_inlier_coverage(1.0f);
}
*c.add_mixture_homography_spectrum() = mix_homog;
c.set_rolling_shutter_motion_index(0);
*c.mutable_homography() = ProjectViaFit<Homography>(
mix_homog, frame_width_, frame_height_, row_weights_.get());
} else {
// Guaranteed to exist because to check that models_per_frame > 0 above.
*c.mutable_homography() = mix_homog.model(0);
}
// Project remaining motions down.
*c.mutable_linear_similarity() = ProjectViaFit<LinearSimilarityModel>(
c.homography(), frame_width_, frame_height_);
*c.mutable_translation() = ProjectViaFit<TranslationModel>(
c.homography(), frame_width_, frame_height_);
c.set_average_magnitude(
std::hypot(c.translation().dx(), c.translation().dy()));
camera_motions->push_back(c);
features->push_back(empty_list);
}
}
} // namespace mediapipe
@@ -0,0 +1,111 @@
// 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.
syntax = "proto2";
package mediapipe;
import "mediapipe/framework/calculator.proto";
import "mediapipe/util/tracking/motion_analysis.proto";
// Next tag: 10
message MotionAnalysisCalculatorOptions {
extend CalculatorOptions {
optional MotionAnalysisCalculatorOptions ext = 270698255;
}
optional mediapipe.MotionAnalysisOptions analysis_options = 1;
// Determines how optional input SELECTION (if present) is used to compute
// the final camera motion.
enum SelectionAnalysis {
// Recompute camera motion for selected frame neighbors.
ANALYSIS_RECOMPUTE = 1;
// Use composited camera motion and region flow from SELECTION input. No
// tracking or re-computation is performed.
// Note that in this case only CAMERA, FLOW and VIDEO_OUT tags are
// supported as output.
NO_ANALYSIS_USE_SELECTION = 2;
// Recompute camera motion for selected frame neighbors using
// features supplied by SELECTION input. No feature tracking is performed.
ANALYSIS_FROM_FEATURES = 3;
// Recomputes camera motion for selected frame neighbors but seeds
// initial transform with camera motion from SELECTION input.
ANALYSIS_WITH_SEED = 4;
}
optional SelectionAnalysis selection_analysis = 4
[default = ANALYSIS_WITH_SEED];
// If activated when SELECTION input is activated, will replace the computed
// camera motion (for any of the ANALYSIS_* case above) with the one supplied
// by the frame selection, in case the frame selection one is more stable.
// For example, if recomputed camera motion is unstable but the one from
// the selection result is stable, will use the stable result instead.
optional bool hybrid_selection_camera = 5 [default = false];
// Determines how optional input META is used to compute the final camera
// motion.
enum MetaAnalysis {
// Uses metadata supplied motions as is.
META_ANALYSIS_USE_META = 1;
// Seeds visual tracking from metadata motions - estimates visual residual
// motion and combines with metadata.
META_ANALYSIS_HYBRID = 2;
}
optional MetaAnalysis meta_analysis = 8 [default = META_ANALYSIS_USE_META];
// Determines number of homography models per frame stored in the CSV file
// or the homography metadata in META.
// For values > 1, MixtureHomographies are created.
optional int32 meta_models_per_frame = 6 [default = 1];
// Used for META_ANALYSIS_HYBRID. Rejects features which flow deviates
// domain_ratio * image diagonal size from the ground truth metadata motion.
optional float meta_outlier_domain_ratio = 9 [default = 0.0015];
// If true, the MotionAnalysisCalculator will skip all processing and emit no
// packets on any output. This is useful for quickly creating different
// versions of a MediaPipe graph without changing its structure, assuming that
// downstream calculators can handle missing input packets.
// TODO: Remove this hack. See b/36485206 for more details.
optional bool bypass_mode = 7 [default = false];
}
// Taken from
// java/com/google/android/libraries/microvideo/proto/microvideo.proto to
// satisfy leakr requirements
// TODO: Remove and use above proto.
message HomographyData {
// For each frame, there are 12 homography matrices stored. Each matrix is
// 3x3 (9 elements). This field will contain 12 x 3 x 3 float values. The
// first row of the first homography matrix will be followed by the second row
// of the first homography matrix, followed by third row of first homography
// matrix, followed by the first row of the second homography matrix, etc.
repeated float motion_homography_data = 1 [packed = true];
// Vector containing histogram counts for individual patches in the frame.
repeated uint32 histogram_count_data = 2 [packed = true];
// The width of the frame at the time metadata was sampled.
optional int32 frame_width = 3;
// The height of the frame at the time metadata was sampled.
optional int32 frame_height = 4;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

@@ -0,0 +1,134 @@
input_stream: "image_cpu_frames"
input_stream: "start_pos"
input_stream: "ra_track"
num_threads: 4
node: {
calculator: "MotionAnalysisCalculator"
input_stream: "VIDEO:image_cpu_frames"
output_stream: "CAMERA:camera_motion"
output_stream: "FLOW:region_flow"
options: {
[mediapipe.MotionAnalysisCalculatorOptions.ext]: {
analysis_options: {
analysis_policy: ANALYSIS_POLICY_CAMERA_MOBILE
flow_options: {
# Maybe move down to 50
fast_estimation_min_block_size: 100
top_inlier_sets: 1
frac_inlier_error_threshold: 3e-3
downsample_mode: DOWNSAMPLE_NONE
verification_distance: 5.0
verify_long_feature_acceleration: true
verify_long_feature_trigger_ratio: 0.1
tracking_options: {
max_features: 500
adaptive_extraction_levels: 2
min_eig_val_settings: {
adaptive_lowest_quality_level: 2e-4
}
klt_tracker_implementation: KLT_OPENCV
}
}
motion_options: {
label_empty_frames_as_valid: false
}
}
}
}
}
node: {
calculator: "FlowPackagerCalculator"
input_stream: "FLOW:region_flow"
input_stream: "CAMERA:camera_motion"
output_stream: "TRACKING:tracking_data"
options: {
[mediapipe.FlowPackagerCalculatorOptions.ext]: {
flow_packager_options: {
binary_tracking_data_support: false
}
}
}
}
node: {
calculator: "BoxTrackerCalculator"
input_stream: "TRACKING:tracking_data"
input_stream: "START_POS:start_pos"
output_stream: "BOXES:boxes"
input_side_packet: "OPTIONS:calculator_options"
input_stream_handler: {
input_stream_handler: "SyncSetInputStreamHandler"
options: {
[mediapipe.SyncSetInputStreamHandlerOptions.ext]: {
sync_set: {
tag_index: "TRACKING"
}
sync_set: {
tag_index: "START_POS"
}
}
}
}
options: {
[mediapipe.BoxTrackerCalculatorOptions.ext]: {
tracker_options: {
track_step_options: {
track_object_and_camera: true
tracking_degrees: TRACKING_DEGREE_OBJECT_PERSPECTIVE
object_similarity_min_contd_inliers: 6
inlier_spring_force: 0.0
static_motion_temporal_ratio: 3e-2
}
}
visualize_tracking_data: false
streaming_track_data_cache_size: 100
}
}
}
node: {
calculator: "BoxTrackerCalculator"
input_stream: "TRACKING:tracking_data"
input_stream: "RA_TRACK:ra_track"
output_stream: "RA_BOXES:ra_boxes"
input_side_packet: "OPTIONS:calculator_options"
input_stream_handler: {
input_stream_handler: "SyncSetInputStreamHandler"
options: {
[mediapipe.SyncSetInputStreamHandlerOptions.ext]: {
sync_set: {
tag_index: "TRACKING"
}
sync_set: {
tag_index: "RA_TRACK"
}
}
}
}
options: {
[mediapipe.BoxTrackerCalculatorOptions.ext]: {
tracker_options: {
track_step_options: {
track_object_and_camera: true
tracking_degrees: TRACKING_DEGREE_OBJECT_PERSPECTIVE
object_similarity_min_contd_inliers: 6
inlier_spring_force: 0.0
static_motion_temporal_ratio: 3e-2
}
}
visualize_tracking_data: false
streaming_track_data_cache_size: 100
}
}
}
+122
View File
@@ -0,0 +1,122 @@
input_stream: "image_cpu_frames"
input_stream: "start_pos"
input_stream: "cancel_object_id"
input_stream: "ra_track"
input_stream: "restart_pos"
input_stream: "track_time"
num_threads: 4
node: {
calculator: "MotionAnalysisCalculator"
options: {
[mediapipe.MotionAnalysisCalculatorOptions.ext]: {
analysis_options: {
analysis_policy: ANALYSIS_POLICY_CAMERA_MOBILE
flow_options: {
# Maybe move down to 50
fast_estimation_min_block_size: 100
top_inlier_sets: 1
frac_inlier_error_threshold: 3e-3
# For mobile application, downsample before input into graph
# and use DOWNSAMPLE_TO_INPUT_SIZE and specify
# downsampling_factor option or DOWNSAMPLE input_side_packet
downsample_mode: DOWNSAMPLE_TO_INPUT_SIZE
verification_distance: 5.0
verify_long_feature_acceleration: true
verify_long_feature_trigger_ratio: 0.1
tracking_options: {
max_features: 500
corner_extraction_method: EXTRACTION_FAST
adaptive_extraction_levels: 2
min_eig_val_settings: {
adaptive_lowest_quality_level: 2e-4
}
klt_tracker_implementation: KLT_OPENCV
}
}
}
}
}
# Drops packets if calculator cannot keep up with the input rate.
input_stream_handler: {
input_stream_handler: "FixedSizeInputStreamHandler"
}
input_stream: "VIDEO:image_cpu_frames"
input_side_packet: "DOWNSAMPLE:analysis_downsample_factor"
input_side_packet: "OPTIONS:calculator_options"
output_stream: "CAMERA:camera_motion"
output_stream: "FLOW:region_flow"
}
node: {
calculator: "FlowPackagerCalculator"
input_stream: "FLOW:region_flow"
input_stream: "CAMERA:camera_motion"
output_stream: "TRACKING:tracking_data"
options: {
[mediapipe.FlowPackagerCalculatorOptions.ext]: {
flow_packager_options: {
binary_tracking_data_support: false
}
}
}
}
node: {
calculator: "BoxTrackerCalculator"
input_side_packet: "OPTIONS:calculator_options"
input_stream: "TRACKING:tracking_data"
input_stream: "TRACK_TIME:track_time"
input_stream: "START_POS:start_pos"
input_stream: "RESTART_POS:restart_pos"
input_stream: "CANCEL_OBJECT_ID:cancel_object_id"
input_stream: "RA_TRACK:ra_track"
output_stream: "BOXES:boxes"
output_stream: "RA_BOXES:ra_boxes"
input_stream_handler: {
input_stream_handler: "SyncSetInputStreamHandler"
options: {
[mediapipe.SyncSetInputStreamHandlerOptions.ext]: {
sync_set: {
tag_index: "TRACKING"
tag_index: "TRACK_TIME"
}
sync_set: {
tag_index: "START_POS"
}
sync_set: {
tag_index: "RESTART_POS"
}
sync_set: {
tag_index: "CANCEL_OBJECT_ID"
}
sync_set: {
tag_index: "RA_TRACK"
}
}
}
}
options: {
[mediapipe.BoxTrackerCalculatorOptions.ext]: {
tracker_options: {
track_step_options: {
track_object_and_camera: true
tracking_degrees: TRACKING_DEGREE_OBJECT_ROTATION_SCALE
inlier_spring_force: 0.0
static_motion_temporal_ratio: 3e-2
object_similarity_min_contd_inliers: 10
}
}
visualize_tracking_data: false
streaming_track_data_cache_size: 100
}
}
}
@@ -0,0 +1,319 @@
// 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 <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "absl/container/node_hash_map.h"
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/detection.pb.h"
#include "mediapipe/framework/formats/location_data.pb.h"
#include "mediapipe/framework/formats/rect.pb.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/util/tracking/box_tracker.h"
#include "mediapipe/util/tracking/tracked_detection.h"
#include "mediapipe/util/tracking/tracked_detection_manager.h"
#include "mediapipe/util/tracking/tracking.h"
namespace mediapipe {
namespace {
constexpr int kDetectionUpdateTimeOutMS = 5000;
constexpr char kDetectionsTag[] = "DETECTIONS";
constexpr char kDetectionBoxesTag[] = "DETECTION_BOXES";
constexpr char kDetectionListTag[] = "DETECTION_LIST";
constexpr char kTrackingBoxesTag[] = "TRACKING_BOXES";
constexpr char kCancelObjectIdTag[] = "CANCEL_OBJECT_ID";
// Move |src| to the back of |dst|.
void MoveIds(std::vector<int>* dst, std::vector<int> src) {
dst->insert(dst->end(), std::make_move_iterator(src.begin()),
std::make_move_iterator(src.end()));
}
int64 GetInputTimestampMs(::mediapipe::CalculatorContext* cc) {
return cc->InputTimestamp().Microseconds() / 1000; // 1 ms = 1000 us.
}
// Converts a Mediapipe Detection Proto to a TrackedDetection class.
std::unique_ptr<TrackedDetection> GetTrackedDetectionFromDetection(
const Detection& detection, int64 timestamp) {
std::unique_ptr<TrackedDetection> tracked_detection =
absl::make_unique<TrackedDetection>(detection.detection_id(), timestamp);
const float top = detection.location_data().relative_bounding_box().ymin();
const float bottom =
detection.location_data().relative_bounding_box().ymin() +
detection.location_data().relative_bounding_box().height();
const float left = detection.location_data().relative_bounding_box().xmin();
const float right = detection.location_data().relative_bounding_box().xmin() +
detection.location_data().relative_bounding_box().width();
NormalizedRect bounding_box;
bounding_box.set_x_center((left + right) / 2.f);
bounding_box.set_y_center((top + bottom) / 2.f);
bounding_box.set_height(bottom - top);
bounding_box.set_width(right - left);
tracked_detection->set_bounding_box(bounding_box);
for (int i = 0; i < detection.label_size(); ++i) {
tracked_detection->AddLabel(detection.label(i), detection.score(i));
}
return tracked_detection;
}
// Converts a TrackedDetection class to a Mediapipe Detection Proto.
Detection GetAxisAlignedDetectionFromTrackedDetection(
const TrackedDetection& tracked_detection) {
Detection detection;
LocationData* location_data = detection.mutable_location_data();
auto corners = tracked_detection.GetCorners();
float x_min = std::numeric_limits<float>::max();
float x_max = std::numeric_limits<float>::min();
float y_min = std::numeric_limits<float>::max();
float y_max = std::numeric_limits<float>::min();
for (int i = 0; i < 4; ++i) {
x_min = std::min(x_min, corners[i].x());
x_max = std::max(x_max, corners[i].x());
y_min = std::min(y_min, corners[i].y());
y_max = std::max(y_max, corners[i].y());
}
location_data->set_format(LocationData::RELATIVE_BOUNDING_BOX);
LocationData::RelativeBoundingBox* relative_bbox =
location_data->mutable_relative_bounding_box();
relative_bbox->set_xmin(x_min);
relative_bbox->set_ymin(y_min);
relative_bbox->set_width(x_max - x_min);
relative_bbox->set_height(y_max - y_min);
// Use previous id which is the id the object when it's first detected.
if (tracked_detection.previous_id() > 0) {
detection.set_detection_id(tracked_detection.previous_id());
} else {
detection.set_detection_id(tracked_detection.unique_id());
}
for (const auto& label_and_score : tracked_detection.label_to_score_map()) {
detection.add_label(label_and_score.first);
detection.add_score(label_and_score.second);
}
return detection;
}
} // namespace
// TrackedDetectionManagerCalculator accepts detections and tracking results at
// different frame rate for real time tracking of targets.
// Input:
// DETECTIONS: A vector<Detection> of newly detected targets.
// TRACKING_BOXES: A TimedBoxProtoList which contains a list of tracked boxes
// from previous detections.
//
// Output:
// CANCEL_OBJECT_ID: Ids of targets that are missing/lost such that it should
// be removed from tracking.
// DETECTIONS: List of detections that are being tracked.
// DETECTION_BOXES: List of bounding boxes of detections that are being
// tracked.
//
// Usage example:
// node {
// calculator: "TrackedDetectionManagerCalculator"
// input_stream: "DETECTIONS:detections"
// input_stream: "TRACKING_BOXES:boxes"
// output_stream: "CANCEL_OBJECT_ID:cancel_object_id"
// output_stream: "DETECTIONS:output_detections"
// }
class TrackedDetectionManagerCalculator : public CalculatorBase {
public:
static ::mediapipe::Status GetContract(CalculatorContract* cc);
::mediapipe::Status Process(CalculatorContext* cc) override;
private:
// Adds new list of detections to |waiting_for_update_detections_|.
void AddDetectionList(const DetectionList& detection_list,
CalculatorContext* cc);
void AddDetections(const std::vector<Detection>& detections,
CalculatorContext* cc);
// Manages existing and new detections.
TrackedDetectionManager tracked_detection_manager_;
// Set of detections that are not up to date yet. These detections will be
// added to the detection manager until they got updated from the box tracker.
absl::node_hash_map<int, std::unique_ptr<TrackedDetection>>
waiting_for_update_detections_;
};
REGISTER_CALCULATOR(TrackedDetectionManagerCalculator);
::mediapipe::Status TrackedDetectionManagerCalculator::GetContract(
CalculatorContract* cc) {
if (cc->Inputs().HasTag(kDetectionsTag)) {
cc->Inputs().Tag(kDetectionsTag).Set<std::vector<Detection>>();
}
if (cc->Inputs().HasTag(kDetectionListTag)) {
cc->Inputs().Tag(kDetectionListTag).Set<DetectionList>();
}
if (cc->Inputs().HasTag(kTrackingBoxesTag)) {
cc->Inputs().Tag(kTrackingBoxesTag).Set<TimedBoxProtoList>();
}
if (cc->Outputs().HasTag(kCancelObjectIdTag)) {
cc->Outputs().Tag(kCancelObjectIdTag).Set<int>();
}
if (cc->Outputs().HasTag(kDetectionsTag)) {
cc->Outputs().Tag(kDetectionsTag).Set<std::vector<Detection>>();
}
if (cc->Outputs().HasTag(kDetectionBoxesTag)) {
cc->Outputs().Tag(kDetectionBoxesTag).Set<std::vector<NormalizedRect>>();
}
return ::mediapipe::OkStatus();
}
::mediapipe::Status TrackedDetectionManagerCalculator::Process(
CalculatorContext* cc) {
if (cc->Inputs().HasTag("TRACKING_BOXES")) {
if (!cc->Inputs().Tag("TRACKING_BOXES").IsEmpty()) {
const TimedBoxProtoList& tracked_boxes =
cc->Inputs().Tag("TRACKING_BOXES").Get<TimedBoxProtoList>();
// Collect all detections that are removed.
auto removed_detection_ids = absl::make_unique<std::vector<int>>();
for (const TimedBoxProto& tracked_box : tracked_boxes.box()) {
NormalizedRect bounding_box;
bounding_box.set_x_center((tracked_box.left() + tracked_box.right()) /
2.f);
bounding_box.set_y_center((tracked_box.bottom() + tracked_box.top()) /
2.f);
bounding_box.set_height(tracked_box.bottom() - tracked_box.top());
bounding_box.set_width(tracked_box.right() - tracked_box.left());
bounding_box.set_rotation(tracked_box.rotation());
// First check if this box updates a detection that's waiting for
// update from the tracker.
auto waiting_for_update_detectoin_ptr =
waiting_for_update_detections_.find(tracked_box.id());
if (waiting_for_update_detectoin_ptr !=
waiting_for_update_detections_.end()) {
// Add the detection and remove duplicated detections.
auto removed_ids = tracked_detection_manager_.AddDetection(
std::move(waiting_for_update_detectoin_ptr->second));
MoveIds(removed_detection_ids.get(), std::move(removed_ids));
waiting_for_update_detections_.erase(
waiting_for_update_detectoin_ptr);
}
auto removed_ids = tracked_detection_manager_.UpdateDetectionLocation(
tracked_box.id(), bounding_box, tracked_box.time_msec());
MoveIds(removed_detection_ids.get(), std::move(removed_ids));
}
// TODO: Should be handled automatically in detection manager.
auto removed_ids = tracked_detection_manager_.RemoveObsoleteDetections(
GetInputTimestampMs(cc) - kDetectionUpdateTimeOutMS);
MoveIds(removed_detection_ids.get(), std::move(removed_ids));
// TODO: Should be handled automatically in detection manager.
removed_ids = tracked_detection_manager_.RemoveOutOfViewDetections();
MoveIds(removed_detection_ids.get(), std::move(removed_ids));
if (!removed_detection_ids->empty() &&
cc->Outputs().HasTag(kCancelObjectIdTag)) {
auto timestamp = cc->InputTimestamp();
for (int box_id : *removed_detection_ids) {
// The timestamp is incremented (by 1 us) because currently the box
// tracker calculator only accepts one cancel object ID for any given
// timestamp.
cc->Outputs()
.Tag(kCancelObjectIdTag)
.AddPacket(mediapipe::MakePacket<int>(box_id).At(timestamp++));
}
}
// Output detections and corresponding bounding boxes.
const auto& all_detections =
tracked_detection_manager_.GetAllTrackedDetections();
auto output_detections = absl::make_unique<std::vector<Detection>>();
auto output_boxes = absl::make_unique<std::vector<NormalizedRect>>();
for (const auto& detection_ptr : all_detections) {
const auto& detection = *detection_ptr.second;
// Only output detections that are synced.
if (detection.last_updated_timestamp() <
cc->InputTimestamp().Microseconds() / 1000) {
continue;
}
output_detections->emplace_back(
GetAxisAlignedDetectionFromTrackedDetection(detection));
output_boxes->emplace_back(detection.bounding_box());
}
if (cc->Outputs().HasTag(kDetectionsTag)) {
cc->Outputs()
.Tag(kDetectionsTag)
.Add(output_detections.release(), cc->InputTimestamp());
}
if (cc->Outputs().HasTag(kDetectionBoxesTag)) {
cc->Outputs()
.Tag(kDetectionBoxesTag)
.Add(output_boxes.release(), cc->InputTimestamp());
}
}
}
if (cc->Inputs().HasTag(kDetectionsTag) &&
!cc->Inputs().Tag(kDetectionsTag).IsEmpty()) {
const auto detections =
cc->Inputs().Tag(kDetectionsTag).Get<std::vector<Detection>>();
AddDetections(detections, cc);
}
if (cc->Inputs().HasTag(kDetectionListTag) &&
!cc->Inputs().Tag(kDetectionListTag).IsEmpty()) {
const auto detection_list =
cc->Inputs().Tag(kDetectionListTag).Get<DetectionList>();
AddDetectionList(detection_list, cc);
}
return ::mediapipe::OkStatus();
}
void TrackedDetectionManagerCalculator::AddDetectionList(
const DetectionList& detection_list, CalculatorContext* cc) {
for (const auto& detection : detection_list.detection()) {
// Convert from microseconds to milliseconds.
std::unique_ptr<TrackedDetection> new_detection =
GetTrackedDetectionFromDetection(
detection, cc->InputTimestamp().Microseconds() / 1000);
const int id = new_detection->unique_id();
waiting_for_update_detections_[id] = std::move(new_detection);
}
}
void TrackedDetectionManagerCalculator::AddDetections(
const std::vector<Detection>& detections, CalculatorContext* cc) {
for (const auto& detection : detections) {
// Convert from microseconds to milliseconds.
std::unique_ptr<TrackedDetection> new_detection =
GetTrackedDetectionFromDetection(
detection, cc->InputTimestamp().Microseconds() / 1000);
const int id = new_detection->unique_id();
waiting_for_update_detections_[id] = std::move(new_detection);
}
}
} // namespace mediapipe
@@ -0,0 +1,709 @@
// 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 <fstream>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "mediapipe/calculators/video/box_tracker_calculator.pb.h"
#include "mediapipe/framework/calculator.pb.h"
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/deps/file_path.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/packet.h"
#include "mediapipe/framework/port/advanced_proto_inc.h"
#include "mediapipe/framework/port/file_helpers.h"
#include "mediapipe/framework/port/gmock.h"
#include "mediapipe/framework/port/gtest.h"
#include "mediapipe/framework/port/opencv_highgui_inc.h"
#include "mediapipe/framework/port/proto_ns.h"
#include "mediapipe/framework/port/status.h"
#include "mediapipe/framework/port/status_matchers.h"
#include "mediapipe/util/tracking/box_tracker.pb.h"
#include "mediapipe/util/tracking/tracking.pb.h"
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
#endif // defined(__APPLE__)
namespace mediapipe {
namespace {
using ::testing::FloatNear;
using ::testing::Test;
std::string GetTestDir() {
#ifdef __APPLE__
char path[1024];
CFURLRef bundle_url = CFBundleCopyBundleURL(CFBundleGetMainBundle());
CFURLGetFileSystemRepresentation(
bundle_url, true, reinterpret_cast<UInt8*>(path), sizeof(path));
CFRelease(bundle_url);
return ::mediapipe::file::JoinPath(path, "testdata");
#elif defined(__ANDROID__)
char path[1024];
getcwd(path, sizeof(path));
return ::mediapipe::file::JoinPath(path,
"mediapipe/calculators/video/testdata");
#else
return ::mediapipe::file::JoinPath(
"./",
// This should match the path of the output files
// of the genrule() that generates test model files.
"mediapipe/calculators/video/testdata");
#endif // defined(__APPLE__)
}
bool LoadBinaryTestGraph(const std::string& graph_path,
CalculatorGraphConfig* config) {
std::ifstream ifs;
ifs.open(graph_path.c_str());
proto_ns::io::IstreamInputStream in_stream(&ifs);
bool success = config->ParseFromZeroCopyStream(&in_stream);
ifs.close();
if (!success) {
LOG(ERROR) << "could not parse test graph: " << graph_path;
}
return success;
}
class TrackingGraphTest : public Test {
protected:
TrackingGraphTest() {}
void SetUp() override {
test_dir_ = GetTestDir();
const auto graph_path = file::JoinPath(test_dir_, "tracker.binarypb");
ASSERT_TRUE(LoadBinaryTestGraph(graph_path, &config_));
original_image_ = cv::imread(file::JoinPath(test_dir_, "lenna.png"));
CreateInputFramesFromOriginalImage(kNumImages, kTranslationStep,
&input_frames_packets_);
const auto& first_input_img = input_frames_packets_[0].Get<ImageFrame>();
const int img_width = first_input_img.Width();
const int img_height = first_input_img.Height();
translation_step_x_ = kTranslationStep / static_cast<float>(img_width);
translation_step_y_ = kTranslationStep / static_cast<float>(img_height);
// Creat new configure and packet dump vector to store output.
mediapipe::CalculatorGraphConfig config_copy = config_;
mediapipe::tool::AddVectorSink("boxes", &config_copy, &output_packets_);
mediapipe::tool::AddVectorSink("ra_boxes", &config_copy,
&random_access_results_packets_);
// Initialize graph
MP_ASSERT_OK(graph_.Initialize(config_copy));
const auto parallel_graph_path =
file::JoinPath(test_dir_, "parallel_tracker.binarypb");
CalculatorGraphConfig parallel_config;
ASSERT_TRUE(LoadBinaryTestGraph(parallel_graph_path, &parallel_config));
mediapipe::tool::AddVectorSink("boxes", &parallel_config, &output_packets_);
mediapipe::tool::AddVectorSink("ra_boxes", &parallel_config,
&random_access_results_packets_);
MP_ASSERT_OK(parallel_graph_.Initialize(parallel_config));
}
void CreateInputFramesFromOriginalImage(
int num_images, int translation_step,
std::vector<Packet>* input_frames_packets);
void TearDown() override {
output_packets_.clear();
random_access_results_packets_.clear();
}
std::unique_ptr<TimedBoxProtoList> MakeBoxList(
const Timestamp& timestamp, const std::vector<bool>& is_quad_tracking,
const std::vector<bool>& is_pnp_tracking,
const std::vector<bool>& reacquisition) const;
void RunGraphWithSidePacketsAndInputs(
const std::map<std::string, mediapipe::Packet>& side_packets,
const mediapipe::Packet& start_pos_packet);
// Utility functions used to judge if a given quad or box is near to the
// groundtruth location at a given frame.
// Examine box.reacquisition() field equals to `reacquisition`.
// `frame` can be float number to account for inter-frame interpolation.
void ExpectBoxAtFrame(const TimedBoxProto& box, float frame,
bool reacquisition);
// Examine box.aspect_ratio() field equals to `aspect_ratio` if asepct_ratio
// is positive.
void ExpectQuadAtFrame(const TimedBoxProto& box, float frame,
float aspect_ratio, bool reacquisition);
// Utility function to judge if two quad are near to each other.
void ExpectQuadNear(const TimedBoxProto& box1, const TimedBoxProto& box2);
std::unique_ptr<TimedBoxProtoList> CreateRandomAccessTrackingBoxList(
const std::vector<Timestamp>& start_timestamps,
const std::vector<Timestamp>& end_timestamps) const;
CalculatorGraph graph_;
CalculatorGraph parallel_graph_;
CalculatorGraphConfig config_;
std::string test_dir_;
cv::Mat original_image_;
std::vector<Packet> input_frames_packets_;
std::vector<mediapipe::Packet> output_packets_;
std::vector<mediapipe::Packet> random_access_results_packets_;
float translation_step_x_; // normalized translation step in x direction
float translation_step_y_; // normalized translation step in y direction
static constexpr float kInitialBoxHalfWidthNormalized = 0.25f;
static constexpr float kInitialBoxHalfHeightNormalized = 0.25f;
static constexpr float kImageAspectRatio = 1.0f; // for lenna.png
static constexpr float kInitialBoxLeft =
0.5f - kInitialBoxHalfWidthNormalized;
static constexpr float kInitialBoxRight =
0.5f + kInitialBoxHalfWidthNormalized;
static constexpr float kInitialBoxTop =
0.5f - kInitialBoxHalfHeightNormalized;
static constexpr float kInitialBoxBottom =
0.5f + kInitialBoxHalfHeightNormalized;
static constexpr int kFrameIntervalUs = 30000;
static constexpr int kNumImages = 8;
// Each image is shifted to the right and bottom by kTranslationStep
// pixels compared with the previous image.
static constexpr int kTranslationStep = 10;
static constexpr float kEqualityTolerance = 3e-4f;
};
void TrackingGraphTest::ExpectBoxAtFrame(const TimedBoxProto& box, float frame,
bool reacquisition) {
EXPECT_EQ(box.reacquisition(), reacquisition);
EXPECT_TRUE(box.has_rotation());
EXPECT_THAT(box.rotation(), FloatNear(0, kEqualityTolerance));
EXPECT_THAT(box.left(),
FloatNear(kInitialBoxLeft - frame * translation_step_x_,
kEqualityTolerance));
EXPECT_THAT(box.top(), FloatNear(kInitialBoxTop - frame * translation_step_y_,
kEqualityTolerance));
EXPECT_THAT(box.bottom(),
FloatNear(kInitialBoxBottom - frame * translation_step_y_,
kEqualityTolerance));
EXPECT_THAT(box.right(),
FloatNear(kInitialBoxRight - frame * translation_step_x_,
kEqualityTolerance));
}
void TrackingGraphTest::ExpectQuadAtFrame(const TimedBoxProto& box, float frame,
float aspect_ratio,
bool reacquisition) {
EXPECT_TRUE(box.has_quad()) << "quad must exist!";
if (aspect_ratio > 0) {
EXPECT_TRUE(box.has_aspect_ratio());
EXPECT_NEAR(box.aspect_ratio(), aspect_ratio, kEqualityTolerance);
}
EXPECT_EQ(box.reacquisition(), reacquisition);
const auto& quad = box.quad();
EXPECT_EQ(8, quad.vertices_size())
<< "quad has only " << box.quad().vertices_size() << " vertices";
EXPECT_THAT(quad.vertices(0),
FloatNear(kInitialBoxLeft - frame * translation_step_x_,
kEqualityTolerance));
EXPECT_THAT(quad.vertices(1),
FloatNear(kInitialBoxTop - frame * translation_step_y_,
kEqualityTolerance));
EXPECT_THAT(quad.vertices(3),
FloatNear(kInitialBoxBottom - frame * translation_step_y_,
kEqualityTolerance));
EXPECT_THAT(quad.vertices(4),
FloatNear(kInitialBoxRight - frame * translation_step_x_,
kEqualityTolerance));
}
void TrackingGraphTest::ExpectQuadNear(const TimedBoxProto& box1,
const TimedBoxProto& box2) {
EXPECT_TRUE(box1.has_quad());
EXPECT_TRUE(box2.has_quad());
EXPECT_EQ(8, box1.quad().vertices_size())
<< "quad has only " << box1.quad().vertices_size() << " vertices";
EXPECT_EQ(8, box2.quad().vertices_size())
<< "quad has only " << box2.quad().vertices_size() << " vertices";
for (int j = 0; j < box1.quad().vertices_size(); ++j) {
EXPECT_NEAR(box1.quad().vertices(j), box2.quad().vertices(j),
kEqualityTolerance);
}
}
std::unique_ptr<TimedBoxProtoList> TrackingGraphTest::MakeBoxList(
const Timestamp& timestamp, const std::vector<bool>& is_quad_tracking,
const std::vector<bool>& is_pnp_tracking,
const std::vector<bool>& reacquisition) const {
auto box_list = absl::make_unique<TimedBoxProtoList>();
int box_id = 0;
for (int j = 0; j < is_quad_tracking.size(); ++j) {
TimedBoxProto* box = box_list->add_box();
if (is_quad_tracking[j]) {
box->mutable_quad()->add_vertices(kInitialBoxLeft);
box->mutable_quad()->add_vertices(kInitialBoxTop);
box->mutable_quad()->add_vertices(kInitialBoxLeft);
box->mutable_quad()->add_vertices(kInitialBoxBottom);
box->mutable_quad()->add_vertices(kInitialBoxRight);
box->mutable_quad()->add_vertices(kInitialBoxBottom);
box->mutable_quad()->add_vertices(kInitialBoxRight);
box->mutable_quad()->add_vertices(kInitialBoxTop);
if (is_pnp_tracking[j]) {
box->set_aspect_ratio(kImageAspectRatio);
}
} else {
box->set_left(kInitialBoxLeft);
box->set_right(kInitialBoxRight);
box->set_top(kInitialBoxTop);
box->set_bottom(kInitialBoxBottom);
}
box->set_id(box_id++);
box->set_time_msec(timestamp.Value() / 1000);
box->set_reacquisition(reacquisition[j]);
}
return box_list;
}
void TrackingGraphTest::CreateInputFramesFromOriginalImage(
int num_images, int translation_step,
std::vector<Packet>* input_frames_packets) {
const int crop_width = original_image_.cols - num_images * translation_step;
const int crop_height = original_image_.rows - num_images * translation_step;
for (int i = 0; i < num_images; ++i) {
cv::Rect roi(i * translation_step, i * translation_step, crop_width,
crop_height);
cv::Mat cropped_img = cv::Mat(original_image_, roi);
auto cropped_image_frame = absl::make_unique<ImageFrame>(
ImageFormat::SRGB, crop_width, crop_height, cropped_img.step[0],
cropped_img.data, ImageFrame::PixelDataDeleter::kNone);
Timestamp curr_timestamp = Timestamp(i * kFrameIntervalUs);
Packet image_packet =
Adopt(cropped_image_frame.release()).At(curr_timestamp);
input_frames_packets->push_back(image_packet);
}
}
void TrackingGraphTest::RunGraphWithSidePacketsAndInputs(
const std::map<std::string, mediapipe::Packet>& side_packets,
const mediapipe::Packet& start_pos_packet) {
// Start running the graph
MP_EXPECT_OK(graph_.StartRun(side_packets));
MP_EXPECT_OK(graph_.AddPacketToInputStream("start_pos", start_pos_packet));
for (auto frame_packet : input_frames_packets_) {
MP_EXPECT_OK(
graph_.AddPacketToInputStream("image_cpu_frames", frame_packet));
MP_EXPECT_OK(graph_.WaitUntilIdle());
}
MP_EXPECT_OK(graph_.CloseAllInputStreams());
MP_EXPECT_OK(graph_.WaitUntilDone());
}
std::unique_ptr<TimedBoxProtoList>
TrackingGraphTest::CreateRandomAccessTrackingBoxList(
const std::vector<Timestamp>& start_timestamps,
const std::vector<Timestamp>& end_timestamps) const {
CHECK_EQ(start_timestamps.size(), end_timestamps.size());
auto ra_boxes = absl::make_unique<TimedBoxProtoList>();
for (int i = 0; i < start_timestamps.size(); ++i) {
auto start_box_list =
MakeBoxList(start_timestamps[i], std::vector<bool>{true},
std::vector<bool>{true}, std::vector<bool>{false});
auto end_box_list =
MakeBoxList(end_timestamps[i], std::vector<bool>{true},
std::vector<bool>{true}, std::vector<bool>{false});
*(ra_boxes->add_box()) = (*start_box_list).box(0);
*(ra_boxes->add_box()) = (*end_box_list).box(0);
}
return ra_boxes;
}
TEST_F(TrackingGraphTest, BasicBoxTrackingSanityCheck) {
// Create input side packets.
std::map<std::string, mediapipe::Packet> side_packets;
side_packets.insert(std::make_pair("analysis_downsample_factor",
mediapipe::MakePacket<float>(1.0f)));
side_packets.insert(std::make_pair(
"calculator_options",
mediapipe::MakePacket<CalculatorOptions>(CalculatorOptions())));
// Run the graph with input side packets, start_pos, and input image frames.
Timestamp start_box_time = input_frames_packets_[0].Timestamp();
// is_quad_tracking is used to indicate whether to track quad for each
// individual box.
std::vector<bool> is_quad_tracking{false};
// is_pnp_tracking is used to indicate whether to use perspective transform to
// track quad.
std::vector<bool> is_pnp_tracking{false};
// is_reacquisition is used to indicate whether to enable reacquisition for
// the box.
std::vector<bool> is_reacquisition{false};
auto start_box_list = MakeBoxList(start_box_time, is_quad_tracking,
is_pnp_tracking, is_reacquisition);
Packet start_pos_packet = Adopt(start_box_list.release()).At(start_box_time);
RunGraphWithSidePacketsAndInputs(side_packets, start_pos_packet);
EXPECT_EQ(input_frames_packets_.size(), output_packets_.size());
for (int i = 0; i < output_packets_.size(); ++i) {
const TimedBoxProtoList& boxes =
output_packets_[i].Get<TimedBoxProtoList>();
EXPECT_EQ(is_quad_tracking.size(), boxes.box_size());
ExpectBoxAtFrame(boxes.box(0), i, false);
}
}
TEST_F(TrackingGraphTest, BasicQuadTrackingSanityCheck) {
// Create input side packets.
std::map<std::string, mediapipe::Packet> side_packets;
side_packets.insert(std::make_pair("analysis_downsample_factor",
mediapipe::MakePacket<float>(1.0f)));
CalculatorOptions calculator_options;
calculator_options.MutableExtension(BoxTrackerCalculatorOptions::ext)
->mutable_tracker_options()
->mutable_track_step_options()
->set_tracking_degrees(
TrackStepOptions::TRACKING_DEGREE_OBJECT_PERSPECTIVE);
side_packets.insert(std::make_pair(
"calculator_options",
mediapipe::MakePacket<CalculatorOptions>(calculator_options)));
Timestamp start_box_time = input_frames_packets_[0].Timestamp();
// Box id 0 use quad tracking with 8DoF homography transform.
// Box id 1 use quad tracking with 6DoF perspective transform.
// Box id 2 use box tracking with 4DoF similarity transform.
std::vector<bool> is_quad_tracking{true, true, false};
std::vector<bool> is_pnp_tracking{false, true, false};
std::vector<bool> is_reacquisition{true, false, true};
auto start_box_list = MakeBoxList(start_box_time, is_quad_tracking,
is_pnp_tracking, is_reacquisition);
Packet start_pos_packet = Adopt(start_box_list.release()).At(start_box_time);
RunGraphWithSidePacketsAndInputs(side_packets, start_pos_packet);
EXPECT_EQ(input_frames_packets_.size(), output_packets_.size());
for (int i = 0; i < output_packets_.size(); ++i) {
const TimedBoxProtoList& boxes =
output_packets_[i].Get<TimedBoxProtoList>();
EXPECT_EQ(is_quad_tracking.size(), boxes.box_size());
for (int j = 0; j < boxes.box_size(); ++j) {
const TimedBoxProto& box = boxes.box(j);
if (is_quad_tracking[box.id()]) {
ExpectQuadAtFrame(box, i,
is_pnp_tracking[box.id()] ? kImageAspectRatio : -1.0f,
is_reacquisition[box.id()]);
} else {
ExpectBoxAtFrame(box, i, is_reacquisition[box.id()]);
}
}
}
}
TEST_F(TrackingGraphTest, TestRandomAccessTrackingResults) {
// Create input side packets.
std::map<std::string, mediapipe::Packet> side_packets;
side_packets.insert(std::make_pair("analysis_downsample_factor",
mediapipe::MakePacket<float>(1.0f)));
CalculatorOptions calculator_options;
calculator_options.MutableExtension(BoxTrackerCalculatorOptions::ext)
->mutable_tracker_options()
->mutable_track_step_options()
->set_tracking_degrees(
TrackStepOptions::TRACKING_DEGREE_OBJECT_PERSPECTIVE);
side_packets.insert(std::make_pair(
"calculator_options",
mediapipe::MakePacket<CalculatorOptions>(calculator_options)));
ASSERT_GT(input_frames_packets_.size(), 2); // at least 3 frames
ASSERT_TRUE(input_frames_packets_[2].Timestamp() -
input_frames_packets_[1].Timestamp() >
TimestampDiff(1000));
constexpr int start_frame = 0;
Timestamp start_box_time = input_frames_packets_[start_frame].Timestamp();
auto start_box_list =
MakeBoxList(start_box_time, std::vector<bool>{true},
std::vector<bool>{true}, std::vector<bool>{false});
constexpr int end_frame = 2;
Timestamp end_box_time = input_frames_packets_[end_frame].Timestamp();
// Also test reverse random access tracking.
// This offset of 1ms is simulating the case where the start query timestamp
// to be not any existing frame timestamp. In reality, it's highly encouraged
// to have the start query timestamp be aligned with frame timestamp.
constexpr int reverse_start_frame = 1;
Timestamp reverse_start_box_time =
input_frames_packets_[reverse_start_frame].Timestamp() + 1000;
auto ra_boxes = CreateRandomAccessTrackingBoxList(
{start_box_time, reverse_start_box_time}, {end_box_time, start_box_time});
Packet ra_packet = Adopt(ra_boxes.release()).At(start_box_time);
Packet start_packet = Adopt(start_box_list.release()).At(start_box_time);
// Start running the ordinary graph, verify random access produce same result
// as normal tracking.
MP_EXPECT_OK(graph_.StartRun(side_packets));
MP_EXPECT_OK(graph_.AddPacketToInputStream("start_pos", start_packet));
for (auto frame_packet : input_frames_packets_) {
MP_EXPECT_OK(
graph_.AddPacketToInputStream("image_cpu_frames", frame_packet));
Packet track_time_packet = Adopt(new int(0)).At(frame_packet.Timestamp());
MP_EXPECT_OK(
graph_.AddPacketToInputStream("track_time", track_time_packet));
MP_EXPECT_OK(graph_.WaitUntilIdle());
}
MP_EXPECT_OK(graph_.AddPacketToInputStream("ra_track", ra_packet));
MP_EXPECT_OK(graph_.CloseAllInputStreams());
MP_EXPECT_OK(graph_.WaitUntilDone());
EXPECT_EQ(input_frames_packets_.size(), output_packets_.size());
const TimedBoxProtoList tracking_result =
output_packets_[end_frame].Get<TimedBoxProtoList>();
EXPECT_EQ(1, tracking_result.box_size());
// Should have 1 random access packet.
EXPECT_EQ(1, random_access_results_packets_.size());
const TimedBoxProtoList& ra_result =
random_access_results_packets_[0].Get<TimedBoxProtoList>();
// Two box tracking results. One for comparison with normal tracking. The
// other for reverse random access tracking.
EXPECT_EQ(2, ra_result.box_size());
// Check if randan access tracking has same result with normal tracking.
ExpectQuadNear(tracking_result.box(0), ra_result.box(0));
ExpectQuadAtFrame(ra_result.box(0), end_frame - start_frame,
kImageAspectRatio, false);
ExpectQuadAtFrame(ra_result.box(1), start_frame - reverse_start_frame - 1,
kImageAspectRatio, false);
// Clear output and ra result packet vector before test parallel graph.
TearDown();
// Start running the parallel graph, verify random access produce same result
// as normal tracking.
MP_EXPECT_OK(parallel_graph_.StartRun(side_packets));
MP_EXPECT_OK(
parallel_graph_.AddPacketToInputStream("start_pos", start_packet));
for (auto frame_packet : input_frames_packets_) {
MP_EXPECT_OK(parallel_graph_.AddPacketToInputStream("image_cpu_frames",
frame_packet));
MP_EXPECT_OK(parallel_graph_.WaitUntilIdle());
}
MP_EXPECT_OK(parallel_graph_.AddPacketToInputStream("ra_track", ra_packet));
MP_EXPECT_OK(parallel_graph_.CloseAllInputStreams());
MP_EXPECT_OK(parallel_graph_.WaitUntilDone());
EXPECT_EQ(input_frames_packets_.size(), output_packets_.size());
const TimedBoxProtoList parallel_tracking_result =
output_packets_[end_frame].Get<TimedBoxProtoList>();
EXPECT_EQ(1, parallel_tracking_result.box_size());
// should have only 1 random access
EXPECT_EQ(1, random_access_results_packets_.size());
const TimedBoxProtoList& parallel_ra_result =
random_access_results_packets_[0].Get<TimedBoxProtoList>();
EXPECT_EQ(2, parallel_ra_result.box_size());
// Check if randan access tracking has same result with normal tracking.
ExpectQuadNear(parallel_tracking_result.box(0), parallel_ra_result.box(0));
ExpectQuadAtFrame(parallel_ra_result.box(0), end_frame - start_frame,
kImageAspectRatio, false);
ExpectQuadAtFrame(parallel_ra_result.box(1),
start_frame - reverse_start_frame - 1, kImageAspectRatio,
false);
}
// Tests what happens when random access request timestamps are
// outside of cache.
TEST_F(TrackingGraphTest, TestRandomAccessTrackingTimestamps) {
// Create input side packets.
std::map<std::string, mediapipe::Packet> side_packets;
side_packets.insert(std::make_pair("analysis_downsample_factor",
mediapipe::MakePacket<float>(1.0f)));
CalculatorOptions calculator_options;
calculator_options.MutableExtension(BoxTrackerCalculatorOptions::ext)
->mutable_tracker_options()
->mutable_track_step_options()
->set_tracking_degrees(
TrackStepOptions::TRACKING_DEGREE_OBJECT_PERSPECTIVE);
// We intentionally don't cache all frames, to see what happens when
// random access tracking request time falls outside cache range.
calculator_options.MutableExtension(BoxTrackerCalculatorOptions::ext)
->set_streaming_track_data_cache_size(input_frames_packets_.size() - 1);
side_packets.insert(std::make_pair(
"calculator_options",
mediapipe::MakePacket<CalculatorOptions>(calculator_options)));
// Set up random access boxes
const int num_frames = input_frames_packets_.size();
const int64 usec_in_sec = 1000000;
std::vector<Timestamp> start_timestamps{
input_frames_packets_[0].Timestamp() - usec_in_sec, // forward
input_frames_packets_[0].Timestamp(), // forward
input_frames_packets_[1].Timestamp(), // forward
input_frames_packets_[num_frames - 1].Timestamp() + usec_in_sec, // fwd
input_frames_packets_[0].Timestamp(), // backward
input_frames_packets_[num_frames - 1].Timestamp(), // backward
input_frames_packets_[num_frames - 1].Timestamp(), // backward
input_frames_packets_[num_frames - 1].Timestamp() + usec_in_sec // back
};
std::vector<Timestamp> end_timestamps{
input_frames_packets_[num_frames - 1].Timestamp(),
input_frames_packets_[num_frames - 1].Timestamp(),
input_frames_packets_[num_frames - 1].Timestamp() + usec_in_sec,
input_frames_packets_[num_frames - 1].Timestamp() + 2 * usec_in_sec,
input_frames_packets_[0].Timestamp() - usec_in_sec,
input_frames_packets_[0].Timestamp(),
input_frames_packets_[0].Timestamp() - usec_in_sec,
input_frames_packets_[1].Timestamp()};
auto ra_boxes =
CreateRandomAccessTrackingBoxList(start_timestamps, end_timestamps);
Packet ra_packet =
Adopt(ra_boxes.release()).At(input_frames_packets_[0].Timestamp());
// Run the graph and check if the outside-cache request have no results.
// Start running the parallel graph, verify random access produce same result
// as normal tracking.
MP_EXPECT_OK(parallel_graph_.StartRun(side_packets));
for (auto frame_packet : input_frames_packets_) {
MP_EXPECT_OK(parallel_graph_.AddPacketToInputStream("image_cpu_frames",
frame_packet));
MP_EXPECT_OK(parallel_graph_.WaitUntilIdle());
}
MP_EXPECT_OK(parallel_graph_.AddPacketToInputStream("ra_track", ra_packet));
MP_EXPECT_OK(parallel_graph_.CloseAllInputStreams());
MP_EXPECT_OK(parallel_graph_.WaitUntilDone());
// should have 1 random access packet with 0 result boxes
EXPECT_EQ(1, random_access_results_packets_.size());
const auto& ra_returned_boxes =
random_access_results_packets_[0].Get<TimedBoxProtoList>();
const int num_returned_ra_boxes = ra_returned_boxes.box_size();
EXPECT_EQ(0, num_returned_ra_boxes);
}
TEST_F(TrackingGraphTest, TestTransitionFramesForReacquisition) {
// Create input side packets.
std::map<std::string, mediapipe::Packet> side_packets;
side_packets.insert(std::make_pair("analysis_downsample_factor",
mediapipe::MakePacket<float>(1.0f)));
CalculatorOptions calculator_options;
calculator_options.MutableExtension(BoxTrackerCalculatorOptions::ext)
->mutable_tracker_options()
->mutable_track_step_options()
->set_tracking_degrees(
TrackStepOptions::TRACKING_DEGREE_OBJECT_PERSPECTIVE);
constexpr int kTransitionFrames = 3;
calculator_options.MutableExtension(BoxTrackerCalculatorOptions::ext)
->set_start_pos_transition_frames(kTransitionFrames);
side_packets.insert(std::make_pair(
"calculator_options",
mediapipe::MakePacket<CalculatorOptions>(calculator_options)));
Timestamp start_box_time = input_frames_packets_[0].Timestamp();
// Box id 0 use quad tracking with 8DoF homography transform.
// Box id 1 use quad tracking with 6DoF perspective transform.
// Box id 2 use box tracking with 4DoF similarity transform.
std::vector<bool> is_quad_tracking{true, true, false};
std::vector<bool> is_pnp_tracking{false, true, false};
std::vector<bool> is_reacquisition{true, true, true};
auto start_box_list = MakeBoxList(start_box_time, is_quad_tracking,
is_pnp_tracking, is_reacquisition);
Packet start_pos_packet = Adopt(start_box_list.release()).At(start_box_time);
// Setting box pos restart from initial position (frame 0's position).
constexpr int kRestartFrame = 3;
Timestamp restart_box_time = input_frames_packets_[kRestartFrame].Timestamp();
auto restart_box_list = MakeBoxList(restart_box_time, is_quad_tracking,
is_pnp_tracking, is_reacquisition);
Packet restart_pos_packet =
Adopt(restart_box_list.release()).At(restart_box_time);
MP_EXPECT_OK(graph_.StartRun(side_packets));
MP_EXPECT_OK(graph_.AddPacketToInputStream("start_pos", start_pos_packet));
for (int j = 0; j < input_frames_packets_.size(); ++j) {
// Add TRACK_TIME stream queries in between 2 frames.
if (j > 0) {
Timestamp track_time = Timestamp((j - 0.5f) * kFrameIntervalUs);
LOG(INFO) << track_time.Value();
Packet track_time_packet = Adopt(new Timestamp).At(track_time);
MP_EXPECT_OK(
graph_.AddPacketToInputStream("track_time", track_time_packet));
}
MP_EXPECT_OK(graph_.AddPacketToInputStream("image_cpu_frames",
input_frames_packets_[j]));
Packet track_time_packet =
Adopt(new int(0)).At(input_frames_packets_[j].Timestamp());
MP_EXPECT_OK(
graph_.AddPacketToInputStream("track_time", track_time_packet));
MP_EXPECT_OK(graph_.WaitUntilIdle());
if (j == kRestartFrame) {
MP_EXPECT_OK(
graph_.AddPacketToInputStream("restart_pos", restart_pos_packet));
}
}
MP_EXPECT_OK(graph_.CloseAllInputStreams());
MP_EXPECT_OK(graph_.WaitUntilDone());
EXPECT_EQ(input_frames_packets_.size() * 2 - 1, output_packets_.size());
for (int i = 0; i < output_packets_.size(); ++i) {
const TimedBoxProtoList& boxes =
output_packets_[i].Get<TimedBoxProtoList>();
EXPECT_EQ(is_quad_tracking.size(), boxes.box_size());
float frame_id = i / 2.0f;
float expected_frame_id;
if (frame_id <= kRestartFrame) {
// before transition
expected_frame_id = frame_id;
} else {
float transition_frames = frame_id - kRestartFrame;
if (transition_frames <= kTransitionFrames) {
// transitioning.
expected_frame_id =
kRestartFrame -
transition_frames / kTransitionFrames * kRestartFrame +
transition_frames;
} else {
// after transition.
expected_frame_id = transition_frames;
}
}
for (int j = 0; j < boxes.box_size(); ++j) {
const TimedBoxProto& box = boxes.box(j);
if (is_quad_tracking[box.id()]) {
ExpectQuadAtFrame(box, expected_frame_id,
is_pnp_tracking[box.id()] ? kImageAspectRatio : -1.0f,
is_reacquisition[box.id()]);
} else {
ExpectBoxAtFrame(box, expected_frame_id, is_reacquisition[box.id()]);
}
}
}
}
// TODO: Add test for reacquisition.
} // namespace
} // namespace mediapipe