Project import generated by Copybara.
GitOrigin-RevId: d91373b4d4d10abef49cab410caa6aadf0875049
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
BIN
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, ¶llel_config));
|
||||
mediapipe::tool::AddVectorSink("boxes", ¶llel_config, &output_packets_);
|
||||
mediapipe::tool::AddVectorSink("ra_boxes", ¶llel_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
|
||||
Reference in New Issue
Block a user