// Copyright 2019 The MediaPipe Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "mediapipe/util/tracking/box_tracker.h" #include #include #include #include "absl/strings/str_cat.h" #include "absl/synchronization/mutex.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "mediapipe/framework/port/integral_types.h" #include "mediapipe/framework/port/logging.h" #include "mediapipe/util/tracking/measure_time.h" #include "mediapipe/util/tracking/tracking.pb.h" namespace mediapipe { // Time within which close checkpoints are removed. static constexpr int kSnapMs = 1000; static constexpr int kInitCheckpoint = -1; void MotionBoxStateQuadToVertices(const MotionBoxState::Quad& quad, std::vector* vertices) { CHECK_EQ(TimedBox::kNumQuadVertices * 2, quad.vertices_size()); CHECK(vertices != nullptr); vertices->clear(); for (int i = 0; i < TimedBox::kNumQuadVertices; ++i) { vertices->push_back( Vector2_f(quad.vertices(i * 2), quad.vertices(i * 2 + 1))); } } void VerticesToMotionBoxStateQuad(const std::vector& vertices, MotionBoxState::Quad* quad) { CHECK_EQ(TimedBox::kNumQuadVertices, vertices.size()); CHECK(quad != nullptr); for (const Vector2_f& vertex : vertices) { quad->add_vertices(vertex.x()); quad->add_vertices(vertex.y()); } } void MotionBoxStateFromTimedBox(const TimedBox& box, MotionBoxState* state) { CHECK(state); state->set_pos_x(box.left); state->set_pos_y(box.top); state->set_width(box.right - box.left); state->set_height(box.bottom - box.top); state->set_rotation(box.rotation); state->set_request_grouping(box.request_grouping); if (box.quad_vertices.size() == TimedBox::kNumQuadVertices) { VerticesToMotionBoxStateQuad(box.quad_vertices, state->mutable_quad()); if (box.aspect_ratio > 0.0f) { state->set_aspect_ratio(box.aspect_ratio); } // set pos_x and pos_y to be the top-left vertex x and y coordinates // set width and height to be max - min of x and y. float min_x = std::numeric_limits::max(); float max_x = std::numeric_limits::lowest(); float min_y = std::numeric_limits::max(); float max_y = std::numeric_limits::lowest(); for (const auto& vertex : box.quad_vertices) { min_x = std::min(min_x, vertex.x()); max_x = std::max(max_x, vertex.x()); min_y = std::min(min_y, vertex.y()); max_y = std::max(max_y, vertex.y()); } state->set_pos_x(min_x); state->set_pos_y(min_y); state->set_width(max_x - min_x); state->set_height(max_y - min_y); } } void TimedBoxFromMotionBoxState(const MotionBoxState& state, TimedBox* box) { CHECK(box); const float scale_dx = state.width() * (state.scale() - 1.0f) * 0.5f; const float scale_dy = state.height() * (state.scale() - 1.0f) * 0.5f; box->left = state.pos_x() - scale_dx; box->top = state.pos_y() - scale_dy; box->right = state.pos_x() + state.width() + scale_dx; box->bottom = state.pos_y() + state.height() + scale_dy; box->rotation = state.rotation(); box->confidence = state.tracking_confidence(); box->request_grouping = state.request_grouping(); if (state.has_quad()) { MotionBoxStateQuadToVertices(state.quad(), &(box->quad_vertices)); if (state.has_aspect_ratio()) { box->aspect_ratio = state.aspect_ratio(); } } } namespace { TimedBox BlendTimedBoxes(const TimedBox& lhs, const TimedBox& rhs, int64 time_msec) { CHECK_LT(lhs.time_msec, rhs.time_msec); const double alpha = (time_msec - lhs.time_msec) * 1.0 / (rhs.time_msec - lhs.time_msec); return TimedBox::Blend(lhs, rhs, alpha); } } // namespace. TimedBox TimedBox::Blend(const TimedBox& lhs, const TimedBox& rhs, double alpha, double beta) { // Due to large timestamps alpha beta should be in double. TimedBox result; result.top = alpha * lhs.top + beta * rhs.top; result.left = alpha * lhs.left + beta * rhs.left; result.bottom = alpha * lhs.bottom + beta * rhs.bottom; result.right = alpha * lhs.right + beta * rhs.right; result.rotation = alpha * lhs.rotation + beta * rhs.rotation; result.time_msec = std::round(alpha * lhs.time_msec + beta * rhs.time_msec); result.confidence = alpha * lhs.confidence + beta * rhs.confidence; if (lhs.quad_vertices.size() == kNumQuadVertices && rhs.quad_vertices.size() == kNumQuadVertices) { result.quad_vertices.clear(); for (int i = 0; i < lhs.quad_vertices.size(); ++i) { result.quad_vertices.push_back(alpha * lhs.quad_vertices[i] + beta * rhs.quad_vertices[i]); } // Since alpha and beta are not necessarily sum to 1, aspect ratio can not // be derived with alpha and beta. Here we are simply averaging the // aspect_ratio as the blended box aspect ratio. if (lhs.aspect_ratio > 0 && rhs.aspect_ratio > 0) { result.aspect_ratio = 0.5f * lhs.aspect_ratio + 0.5f * rhs.aspect_ratio; } } return result; } TimedBox TimedBox::Blend(const TimedBox& lhs, const TimedBox& rhs, double alpha) { return Blend(lhs, rhs, 1.0 - alpha, alpha); } std::array TimedBox::Corners(float width, float height) const { if (quad_vertices.size() == kNumQuadVertices) { std::array corners{{ Vector2_f(quad_vertices[0].x() * width, quad_vertices[0].y() * height), Vector2_f(quad_vertices[1].x() * width, quad_vertices[1].y() * height), Vector2_f(quad_vertices[2].x() * width, quad_vertices[2].y() * height), Vector2_f(quad_vertices[3].x() * width, quad_vertices[3].y() * height), }}; return corners; } else { // Rotate 4 corner w.r.t. center. const Vector2_f center(0.5f * (left + right) * width, 0.5f * (top + bottom) * height); const std::array corners{{ Vector2_f(left * width, top * height), Vector2_f(left * width, bottom * height), Vector2_f(right * width, bottom * height), Vector2_f(right * width, top * height), }}; const float cos_a = std::cos(rotation); const float sin_a = std::sin(rotation); std::array transformed_corners; for (int k = 0; k < 4; ++k) { // Scale and rotate w.r.t. center. const Vector2_f rad = corners[k] - center; const Vector2_f rot_rad(cos_a * rad.x() - sin_a * rad.y(), sin_a * rad.x() + cos_a * rad.y()); transformed_corners[k] = center + rot_rad; } return transformed_corners; } } TimedBox TimedBox::FromProto(const TimedBoxProto& proto) { TimedBox box; box.top = proto.top(); box.left = proto.left(); box.bottom = proto.bottom(); box.right = proto.right(); box.rotation = proto.rotation(); box.time_msec = proto.time_msec(); box.request_grouping = proto.request_grouping(); if (proto.has_quad() && proto.quad().vertices_size() == kNumQuadVertices * 2) { MotionBoxStateQuadToVertices(proto.quad(), &(box.quad_vertices)); if (proto.has_aspect_ratio()) { box.aspect_ratio = proto.aspect_ratio(); } } return box; } TimedBoxProto TimedBox::ToProto() const { TimedBoxProto proto; proto.set_top(top); proto.set_left(left); proto.set_bottom(bottom); proto.set_right(right); proto.set_rotation(rotation); proto.set_time_msec(time_msec); proto.set_confidence(confidence); proto.set_request_grouping(request_grouping); if (quad_vertices.size() == kNumQuadVertices) { VerticesToMotionBoxStateQuad(quad_vertices, proto.mutable_quad()); if (aspect_ratio > 0.0f) { proto.set_aspect_ratio(aspect_ratio); } } return proto; } BoxTracker::BoxTracker(const std::string& cache_dir, const BoxTrackerOptions& options) : options_(options), cache_dir_(cache_dir) { tracking_workers_.reset(new ThreadPool(options_.num_tracking_workers())); tracking_workers_->StartWorkers(); } BoxTracker::BoxTracker( const std::vector& tracking_data, bool copy_data, const BoxTrackerOptions& options) : BoxTracker("", options) { AddTrackingDataChunks(tracking_data, copy_data); } void BoxTracker::AddTrackingDataChunk(const TrackingDataChunk* chunk, bool copy_data) { CHECK_GT(chunk->item_size(), 0) << "Empty chunk."; int64 chunk_time_msec = chunk->item(0).timestamp_usec() / 1000; int chunk_idx = ChunkIdxFromTime(chunk_time_msec); CHECK_GE(chunk_idx, tracking_data_.size()) << "Chunk is out of order."; if (chunk_idx > tracking_data_.size()) { LOG(INFO) << "Resize tracking_data_ to " << chunk_idx; tracking_data_.resize(chunk_idx); } if (copy_data) { tracking_data_buffer_.emplace_back(new TrackingDataChunk(*chunk)); tracking_data_.push_back(tracking_data_buffer_.back().get()); } else { tracking_data_.emplace_back(chunk); } } void BoxTracker::AddTrackingDataChunks( const std::vector& tracking_data, bool copy_data) { for (const auto item : tracking_data) { AddTrackingDataChunk(item, copy_data); } } void BoxTracker::NewBoxTrack(const TimedBox& initial_pos, int id, int64 min_msec, int64 max_msec) { VLOG(1) << "New box track: " << id << " : " << initial_pos.ToString() << " from " << min_msec << " to " << max_msec; // Mark initialization with checkpoint -1. absl::MutexLock lock(&status_mutex_); if (canceling_) { LOG(WARNING) << "Box Tracker is in cancel state. Refusing request."; return; } ++track_status_[id][kInitCheckpoint].tracks_ongoing; auto operation = [this, initial_pos, id, min_msec, max_msec]() { this->NewBoxTrackAsync(initial_pos, id, min_msec, max_msec); }; tracking_workers_->Schedule(operation); } std::pair BoxTracker::TrackInterval(int id) { absl::MutexLock lock(&path_mutex_); const Path& path = paths_[id]; if (path.empty()) { return std::make_pair(-1, -1); } auto first_interval = path.begin()->second; auto last_interval = path.rbegin()->second; return std::make_pair(first_interval.front().time_msec, last_interval.back().time_msec); } void BoxTracker::NewBoxTrackAsync(const TimedBox& initial_pos, int id, int64 min_msec, int64 max_msec) { VLOG(1) << "Async track for id: " << id << " from " << min_msec << " to " << max_msec; // Determine start position and track forward and backward. int chunk_idx = ChunkIdxFromTime(initial_pos.time_msec); VLOG(1) << "Starting at chunk " << chunk_idx; AugmentedChunkPtr tracking_chunk(ReadChunk(id, kInitCheckpoint, chunk_idx)); if (!tracking_chunk.first) { absl::MutexLock lock(&status_mutex_); --track_status_[id][kInitCheckpoint].tracks_ongoing; LOG(ERROR) << "Could not read tracking chunk from file: " << chunk_idx << " for start position: " << initial_pos.ToString(); return; } // Grab ownership here, to avoid any memory leaks due to early return. std::unique_ptr chunk_owned; if (tracking_chunk.second) { chunk_owned.reset(tracking_chunk.first); } const int start_frame = ClosestFrameIndex(initial_pos.time_msec, *tracking_chunk.first); VLOG(1) << "Local start frame: " << start_frame; // Update starting position to coincide with a frame. TimedBox start_pos = initial_pos; start_pos.time_msec = tracking_chunk.first->item(start_frame).timestamp_usec() / 1000; VLOG(1) << "Request at " << initial_pos.time_msec << " revised to " << start_pos.time_msec; const int checkpoint = start_pos.time_msec; // TODO: // Compute min and max for tracking based on existing check points. if (!WaitToScheduleId(id)) { // Could not schedule, id already being canceled. return; } // If another checkpoint is close by, cancel that one. VLOG(1) << "Removing close checkpoints"; absl::MutexLock lock(&status_mutex_); RemoveCloseCheckpoints(id, checkpoint); VLOG(1) << "Cancel existing tracks"; CancelTracking(id, checkpoint); // Remove checkpoint results (to be replaced with current one). ClearCheckpoint(id, checkpoint); MotionBoxState start_state; MotionBoxStateFromTimedBox(start_pos, &start_state); VLOG(1) << "Adding initial result"; AddBoxResult(start_pos, id, checkpoint, start_state); // Perform forward and backward tracking and add to current PathSegment. // Track forward. track_status_[id][checkpoint].tracks_ongoing += 2; VLOG(1) << "Starting tracking workers ... "; AugmentedChunkPtr forward_chunk = tracking_chunk; AugmentedChunkPtr backward_chunk = tracking_chunk; if (tracking_chunk.second) { // We have ownership, need a copy here. forward_chunk = std::make_pair(new TrackingDataChunk(*chunk_owned), true); backward_chunk = std::make_pair(chunk_owned.release(), true); } auto forward_operation = [this, forward_chunk, start_state, start_frame, chunk_idx, id, checkpoint, min_msec, max_msec]() { this->TrackingImpl(TrackingImplArgs(forward_chunk, start_state, start_frame, chunk_idx, id, checkpoint, true, true, min_msec, max_msec)); }; tracking_workers_->Schedule(forward_operation); // Track backward. auto backward_operation = [this, backward_chunk, start_state, start_frame, chunk_idx, id, checkpoint, min_msec, max_msec]() { this->TrackingImpl(TrackingImplArgs(backward_chunk, start_state, start_frame, chunk_idx, id, checkpoint, false, true, min_msec, max_msec)); }; tracking_workers_->Schedule(backward_operation); DoneSchedulingId(id); // Tell a waiting request that we are done scheduling. status_condvar_.SignalAll(); VLOG(1) << "Scheduling done for " << id; } void BoxTracker::RemoveCloseCheckpoints(int id, int checkpoint) { if (track_status_[id].empty()) { return; } auto pos = track_status_[id].lower_bound(checkpoint); // Test current and previous location (if possible). int num_turns = 1; if (pos != track_status_[id].begin()) { --pos; ++num_turns; } for (int k = 0; k < num_turns; ++k, ++pos) { if (pos != track_status_[id].end()) { const int check_pos = pos->first; // Ignore marker init checkpoint from track_status_. if (check_pos > kInitCheckpoint && std::abs(check_pos - checkpoint) < kSnapMs) { CancelTracking(id, check_pos); ClearCheckpoint(id, check_pos); } } else { break; } } } bool BoxTracker::WaitToScheduleId(int id) { VLOG(1) << "Wait to schedule id: " << id; absl::MutexLock lock(&status_mutex_); while (new_box_track_[id]) { // Box tracking is currently ongoing for this id. if (track_status_[id][kInitCheckpoint].canceled) { // Canceled, remove myself from ongoing tracks. --track_status_[id][kInitCheckpoint].tracks_ongoing; status_condvar_.SignalAll(); return false; } // Only one request can be processing in the section till end of the // function NewBoxTrackAsync at a time. status_condvar_.Wait(&status_mutex_); } // We got canceled already, don't proceed. if (track_status_[id][kInitCheckpoint].canceled) { --track_status_[id][kInitCheckpoint].tracks_ongoing; status_condvar_.SignalAll(); return false; } // Signal we are about to schedule new tracking. new_box_track_[id] = true; VLOG(1) << "Ready to schedule id: " << id; return true; } void BoxTracker::DoneSchedulingId(int id) { new_box_track_[id] = false; --track_status_[id][kInitCheckpoint].tracks_ongoing; } void BoxTracker::CancelTracking(int id, int checkpoint) { // Wait for ongoing requests to terminate. while (track_status_[id][checkpoint].tracks_ongoing != 0) { // Cancel all ongoing requests. track_status_[id][checkpoint].canceled = true; status_condvar_.Wait(&status_mutex_); } track_status_[id][checkpoint].canceled = false; } bool BoxTracker::GetTimedPosition(int id, int64 time_msec, TimedBox* result, std::vector* states) { CHECK(result); MotionBoxState* lhs_box_state = nullptr; MotionBoxState* rhs_box_state = nullptr; if (states) { CHECK(options_.record_path_states()) << "Requesting corresponding tracking states requires option " << "record_path_states to be set"; states->resize(1); lhs_box_state = rhs_box_state = &states->at(0); } VLOG(1) << "Obtaining result at " << time_msec; absl::MutexLock lock(&path_mutex_); const Path& path = paths_[id]; if (path.size() < 1) { LOG(ERROR) << "Empty path!"; return false; } // Find corresponding checkpoint. auto check_pos = path.lower_bound(time_msec); if (check_pos == path.begin()) { VLOG(1) << "To left"; // We are to the left of the earliest checkpoint. return TimedBoxAtTime(check_pos->second, time_msec, result, lhs_box_state); } if (check_pos == path.end()) { VLOG(1) << "To right"; --check_pos; // We are to the right of the lastest checkpoint. return TimedBoxAtTime(check_pos->second, time_msec, result, rhs_box_state); } VLOG(1) << "Blending ..."; // We are inbetween checkpoints, get result for each, then blend. const PathSegment& rhs = check_pos->second; const int check_rhs = check_pos->first; --check_pos; const PathSegment& lhs = check_pos->second; const int check_lhs = check_pos->first; TimedBox lhs_box; TimedBox rhs_box; if (states) { states->resize(2); lhs_box_state = &states->at(0); rhs_box_state = &states->at(1); } if (!TimedBoxAtTime(lhs, time_msec, &lhs_box, lhs_box_state)) { return false; } if (!TimedBoxAtTime(rhs, time_msec, &rhs_box, rhs_box_state)) { return false; } VLOG(1) << "Blending: " << lhs_box.ToString() << " and " << rhs_box.ToString(); const double alpha = (time_msec - check_lhs) * 1.0 / (check_rhs - check_lhs); *result = TimedBox::Blend(lhs_box, rhs_box, alpha); return true; } bool BoxTracker::IsTrackingOngoingForId(int id) { absl::MutexLock lock(&status_mutex_); for (const auto& item : track_status_[id]) { if (item.second.tracks_ongoing > 0) { return true; } } return false; } bool BoxTracker::IsTrackingOngoing() { absl::MutexLock lock(&status_mutex_); return IsTrackingOngoingMutexHeld(); } bool BoxTracker::IsTrackingOngoingMutexHeld() { for (const auto& id : track_status_) { for (const auto& item : id.second) { if (item.second.tracks_ongoing > 0) { return true; } } } return false; } BoxTracker::AugmentedChunkPtr BoxTracker::ReadChunk(int id, int checkpoint, int chunk_idx) { VLOG(1) << __FUNCTION__ << " id=" << id << " chunk_idx=" << chunk_idx; if (cache_dir_.empty() && !tracking_data_.empty()) { if (chunk_idx < tracking_data_.size()) { return std::make_pair(tracking_data_[chunk_idx], false); } else { LOG(ERROR) << "chunk_idx >= tracking_data_.size()"; return std::make_pair(nullptr, false); } } else { std::unique_ptr chunk_data( ReadChunkFromCache(id, checkpoint, chunk_idx)); return std::make_pair(chunk_data.release(), true); } } std::unique_ptr BoxTracker::ReadChunkFromCache( int id, int checkpoint, int chunk_idx) { VLOG(1) << __FUNCTION__ << " id=" << id << " chunk_idx=" << chunk_idx; 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); } VLOG(1) << "Reading chunk from cache: " << chunk_file; std::unique_ptr chunk_data(new TrackingDataChunk()); struct stat tmp; if (stat(chunk_file.c_str(), &tmp)) { if (!WaitForChunkFile(id, checkpoint, chunk_file)) { return nullptr; } } VLOG(1) << "File exists, reading ..."; std::ifstream in(chunk_file, std::ios::in | std::ios::binary); if (!in) { LOG(ERROR) << "Could not read chunk file: " << chunk_file; return nullptr; } std::string data; in.seekg(0, std::ios::end); data.resize(in.tellg()); in.seekg(0, std::ios::beg); in.read(&data[0], data.size()); in.close(); chunk_data->ParseFromString(data); VLOG(1) << "Read success"; return chunk_data; } bool BoxTracker::WaitForChunkFile(int id, int checkpoint, const std::string& chunk_file) { VLOG(1) << "Chunk no exists, waiting for file: " << chunk_file; const int timeout_msec = options_.read_chunk_timeout_msec(); // Exponential backoff sleep till file exists. int wait_time_msec = 20; VLOG(1) << "In wait for chunk ...: " << chunk_file; // Maximum wait time. const int kMaxWaitPeriod = 5000; int total_wait_msec = 0; bool file_exists = false; while (!file_exists && total_wait_msec < timeout_msec) { // Check if we got canceled. { absl::MutexLock lock(&status_mutex_); if (track_status_[id][checkpoint].canceled) { return false; } } absl::SleepFor(absl::Milliseconds(wait_time_msec)); total_wait_msec += wait_time_msec; struct stat tmp; file_exists = stat(chunk_file.c_str(), &tmp) == 0; if (file_exists) { VLOG(1) << "Sucessfully waited on " << chunk_file << " for " << total_wait_msec; break; } if (wait_time_msec < kMaxWaitPeriod) { wait_time_msec *= 1.5; } } return file_exists; } int BoxTracker::ClosestFrameIndex(int64 msec, const TrackingDataChunk& chunk) const { CHECK_GT(chunk.item_size(), 0); typedef TrackingDataChunk::Item Item; Item item_to_find; item_to_find.set_timestamp_usec(msec * 1000); int pos = std::lower_bound(chunk.item().begin(), chunk.item().end(), item_to_find, [](const Item& lhs, const Item& rhs) -> bool { return lhs.timestamp_usec() < rhs.timestamp_usec(); }) - chunk.item().begin(); // Skip end. if (pos == chunk.item_size()) { return pos - 1; } else if (pos == 0) { // Nothing smaller exists. return 0; } // Determine closest timestamp. const int64 lhs_diff = msec - chunk.item(pos - 1).timestamp_usec() / 1000; const int64 rhs_diff = chunk.item(pos).timestamp_usec() / 1000 - msec; if (std::min(lhs_diff, rhs_diff) >= 67) { LOG(ERROR) << "No frame found within 67ms, probably using wrong chunk."; } if (lhs_diff < rhs_diff) { return pos - 1; } else { return pos; } } void BoxTracker::AddBoxResult(const TimedBox& box, int id, int checkpoint, const MotionBoxState& state) { absl::MutexLock lock(&path_mutex_); PathSegment& segment = paths_[id][checkpoint]; auto insert_pos = std::lower_bound(segment.begin(), segment.end(), box); const bool store_state = options_.record_path_states(); // Don't overwrite an existing box. if (insert_pos == segment.end() || insert_pos->time_msec != box.time_msec) { segment.insert(insert_pos, InternalTimedBox( box, store_state ? new MotionBoxState(state) : nullptr)); } } void BoxTracker::ClearCheckpoint(int id, int checkpoint) { absl::MutexLock lock(&path_mutex_); PathSegment& segment = paths_[id][checkpoint]; segment.clear(); } void BoxTracker::TrackingImpl(const TrackingImplArgs& a) { TrackStepOptions track_step_options = options_.track_step_options(); ChangeTrackingDegreesBasedOnStartPos(a.start_state, &track_step_options); MotionBox motion_box(track_step_options); const int chunk_data_size = a.chunk_data->item_size(); CHECK_GE(a.start_frame, 0); CHECK_LT(a.start_frame, chunk_data_size); VLOG(1) << " a.start_frame = " << a.start_frame << " @" << a.chunk_data->item(a.start_frame).timestamp_usec() << " with " << chunk_data_size << " items"; motion_box.ResetAtFrame(a.start_frame, a.start_state); auto cleanup_func = [&a, this]() -> void { if (a.first_call) { // Signal we are done processing in this direction. absl::MutexLock lock(&status_mutex_); --track_status_[a.id][a.checkpoint].tracks_ongoing; status_condvar_.SignalAll(); } }; if (a.forward) { // TrackingData at frame f, contains tracking information from // frame f to f - 1. Get information at frame f + 1 and invert: // Tracking from f to f + 1. for (int f = a.start_frame; f + 1 < chunk_data_size; ++f) { // Note: we use / 1000 instead of * 1000 to avoid overflow. if (a.chunk_data->item(f + 1).timestamp_usec() / 1000 > a.max_msec) { VLOG(2) << "Reached maximum tracking timestamp @" << a.max_msec; break; } VLOG(1) << "Track forward from " << f; MotionVectorFrame mvf; MotionVectorFrameFromTrackingData( a.chunk_data->item(f + 1).tracking_data(), &mvf); const int track_duration_ms = TrackingDataDurationMs(a.chunk_data->item(f + 1)); if (track_duration_ms > 0) { mvf.duration_ms = track_duration_ms; } // If this is the first frame in a chunk, there might be an unobserved // chunk boundary at the first frame. if (f == 0 && a.chunk_data->item(0).tracking_data().frame_flags() & TrackingData::FLAG_CHUNK_BOUNDARY) { mvf.is_chunk_boundary = true; } MotionVectorFrame mvf_inverted; InvertMotionVectorFrame(mvf, &mvf_inverted); const bool forward_tracking = true; if (!motion_box.TrackStep(f, mvf_inverted, forward_tracking)) { VLOG(1) << "Failed forward at frame: " << f; break; } else { // Test if current request is canceled. { absl::MutexLock lock(&status_mutex_); if (track_status_[a.id][a.checkpoint].canceled) { --track_status_[a.id][a.checkpoint].tracks_ongoing; status_condvar_.SignalAll(); return; } } TimedBox result; const MotionBoxState& result_state = motion_box.StateAtFrame(f + 1); TimedBoxFromMotionBoxState(result_state, &result); result.time_msec = a.chunk_data->item(f + 1).timestamp_usec() / 1000; AddBoxResult(result, a.id, a.checkpoint, result_state); } if (f + 2 == chunk_data_size && !a.chunk_data->last_chunk()) { // Last frame, successful track, continue; AugmentedChunkPtr next_chunk( ReadChunk(a.id, a.checkpoint, a.chunk_idx + 1)); if (next_chunk.first != nullptr) { TrackingImplArgs next_args(next_chunk, motion_box.StateAtFrame(f + 1), 0, a.chunk_idx + 1, a.id, a.checkpoint, a.forward, false, a.min_msec, a.max_msec); TrackingImpl(next_args); } else { cleanup_func(); LOG(ERROR) << "Can't read expected chunk file!"; } } } } else { // Backward tracking. // Don't attempt to track from the very first frame backwards. const int first_frame = a.chunk_data->first_chunk() ? 1 : 0; for (int f = a.start_frame; f >= first_frame; --f) { if (a.chunk_data->item(f).timestamp_usec() / 1000 < a.min_msec) { VLOG(2) << "Reached minimum tracking timestamp @" << a.min_msec; break; } VLOG(1) << "Track backward from " << f; MotionVectorFrame mvf; MotionVectorFrameFromTrackingData(a.chunk_data->item(f).tracking_data(), &mvf); const int64 track_duration_ms = TrackingDataDurationMs(a.chunk_data->item(f)); if (track_duration_ms > 0) { mvf.duration_ms = track_duration_ms; } const bool forward_tracking = false; if (!motion_box.TrackStep(f, mvf, forward_tracking)) { VLOG(1) << "Failed backward at frame: " << f; break; } else { // Test if current request is canceled. { absl::MutexLock lock(&status_mutex_); if (track_status_[a.id][a.checkpoint].canceled) { --track_status_[a.id][a.checkpoint].tracks_ongoing; status_condvar_.SignalAll(); return; } } TimedBox result; const MotionBoxState& result_state = motion_box.StateAtFrame(f - 1); TimedBoxFromMotionBoxState(result_state, &result); result.time_msec = a.chunk_data->item(f).prev_timestamp_usec() / 1000; AddBoxResult(result, a.id, a.checkpoint, result_state); } if (f == first_frame && !a.chunk_data->first_chunk()) { VLOG(1) << "Read next chunk: " << f << "==" << first_frame << " in " << a.chunk_idx; // First frame, successful track, continue. AugmentedChunkPtr prev_chunk( ReadChunk(a.id, a.checkpoint, a.chunk_idx - 1)); if (prev_chunk.first != nullptr) { const int last_frame = prev_chunk.first->item_size() - 1; TrackingImplArgs prev_args(prev_chunk, motion_box.StateAtFrame(f - 1), last_frame, a.chunk_idx - 1, a.id, a.checkpoint, a.forward, false, a.min_msec, a.max_msec); TrackingImpl(prev_args); } else { cleanup_func(); LOG(ERROR) << "Can't read expected chunk file! " << a.chunk_idx - 1 << " while tracking @" << a.chunk_data->item(f).timestamp_usec() / 1000 << " with cutoff " << a.min_msec; return; } } } } cleanup_func(); } bool TimedBoxAtTime(const PathSegment& segment, int64 time_msec, TimedBox* box, MotionBoxState* state) { CHECK(box); if (segment.empty()) { return false; } TimedBox to_find; to_find.time_msec = time_msec; auto pos = std::lower_bound(segment.begin(), segment.end(), to_find); if (pos != segment.end() && pos->time_msec == time_msec) { *box = *pos; if (state) { *state = *pos->state; } return true; } constexpr int kMaxDiff = 67; if (pos == segment.begin()) { if (pos->time_msec - time_msec < kMaxDiff) { *box = *pos; if (state && pos->state) { *state = *pos->state; } return true; } else { return false; } } if (pos == segment.end()) { if (time_msec - pos[-1].time_msec < kMaxDiff) { *box = pos[-1]; if (state && pos[-1].state) { *state = *pos[-1].state; } return true; } else { return false; } } // Interpolation necessary. *box = BlendTimedBoxes(pos[-1], pos[0], time_msec); if (state) { // Grab closest state. if (std::abs(pos[-1].time_msec - time_msec) < std::abs(pos[0].time_msec - time_msec)) { if (pos[-1].state) { *state = *pos[-1].state; } } else { if (pos[0].state) { *state = *pos[0].state; } } } return true; } void BoxTracker::ResumeTracking() { absl::MutexLock lock(&status_mutex_); canceling_ = false; } void BoxTracker::CancelAllOngoingTracks() { // Get a list of items to be canceled (id, checkpoint) absl::MutexLock lock(&status_mutex_); canceling_ = true; std::vector> to_be_canceled; for (auto& id : track_status_) { for (auto& checkpoint : id.second) { if (checkpoint.second.tracks_ongoing > 0) { checkpoint.second.canceled = true; to_be_canceled.push_back(std::make_pair(id.first, checkpoint.first)); } } } // Wait for ongoing requests to terminate. auto on_going_test = [&to_be_canceled, this]() -> bool { status_mutex_.AssertHeld(); for (const auto& item : to_be_canceled) { if (track_status_[item.first][item.second].tracks_ongoing > 0) { return true; } } return false; }; while (on_going_test()) { status_condvar_.Wait(&status_mutex_); } // Indicate we are done canceling. for (const auto& item : to_be_canceled) { track_status_[item.first][item.second].canceled = false; } } bool BoxTracker::WaitForAllOngoingTracks(int timeout_us) { MEASURE_TIME << "Tracking time ..."; absl::MutexLock lock(&status_mutex_); // Infinite wait for timeout <= 0. absl::Duration timeout = timeout_us > 0 ? absl::Microseconds(timeout_us) : absl::InfiniteDuration(); while (timeout > absl::ZeroDuration() && IsTrackingOngoingMutexHeld()) { absl::Time start_wait = absl::Now(); status_condvar_.WaitWithTimeout(&status_mutex_, timeout); absl::Duration elapsed = absl::Now() - start_wait; timeout -= elapsed; } return !IsTrackingOngoingMutexHeld(); } bool BoxTracker::GetTrackingData(int id, int64 request_time_msec, TrackingData* tracking_data, int* tracking_data_msec) { CHECK(tracking_data); int chunk_idx = ChunkIdxFromTime(request_time_msec); AugmentedChunkPtr tracking_chunk(ReadChunk(id, kInitCheckpoint, chunk_idx)); if (!tracking_chunk.first) { absl::MutexLock lock(&status_mutex_); --track_status_[id][kInitCheckpoint].tracks_ongoing; LOG(ERROR) << "Could not read tracking chunk from file."; return false; } std::unique_ptr owned_chunk; if (tracking_chunk.second) { owned_chunk.reset(tracking_chunk.first); } const int closest_frame = ClosestFrameIndex(request_time_msec, *tracking_chunk.first); *tracking_data = tracking_chunk.first->item(closest_frame).tracking_data(); if (tracking_data_msec) { *tracking_data_msec = tracking_chunk.first->item(closest_frame).timestamp_usec() / 1000; } return true; } } // namespace mediapipe