chromium/chromeos/ash/components/kiosk/vision/internal/detection_observer.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/kiosk/vision/internal/detection_observer.h"

#include <cstddef>
#include <cstdint>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/check_op.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/ranges/algorithm.h"
#include "base/time/time.h"
#include "chromeos/ash/components/kiosk/vision/internal/detection_processor.h"
#include "media/capture/video/chromeos/mojom/cros_camera_service.mojom-forward.h"

namespace ash::kiosk_vision {

namespace {

int64_t CurrentTimestampInMicroseconds() {
  return base::Time::Now().ToDeltaSinceWindowsEpoch().InMicroseconds();
}

bool IsSortedByTimestamp(
    const std::vector<cros::mojom::KioskVisionAppearancePtr>& appearances) {
  for (size_t i = 1; i < appearances.size(); i++) {
    if (appearances[i - 1]->timestamp_in_us > appearances[i]->timestamp_in_us) {
      return false;
    }
  }
  return true;
}

void ValidateDetection(const cros::mojom::KioskVisionDetection& detection) {
  bool face_or_body_are_present = base::ranges::all_of(
      detection.appearances, [](auto& a) { return a->face || a->body; });
  CHECK(face_or_body_are_present)
      << "Appearances must have either a face or body or both";
}

void ValidateTrack(const cros::mojom::KioskVisionTrack& track) {
  CHECK(track.appearances.size() > 0)
      << "A track's list of appearances must not be empty";

  bool is_same_person = base::ranges::all_of(
      track.appearances,
      [&](auto id) { return track.appearances[0]->person_id == id; },
      [](const auto& a) { return a->person_id; });
  CHECK(is_same_person) << "A track's appearances must all have the same id";

  bool face_or_body_are_present = base::ranges::all_of(
      track.appearances, [](auto& a) { return a->face || a->body; });
  CHECK(face_or_body_are_present)
      << "Appearances must have either a face or body or both";

  CHECK(track.start_timestamp_in_us <= track.end_timestamp_in_us)
      << "A track's start time must not be later than its end time";

  CHECK(IsSortedByTimestamp(track.appearances))
      << "A track's appearances must be sorted by time";
}

}  // namespace

BASE_FEATURE(kEmitKioskVisionFakes,
             "EmitKioskVisionFakes",
             base::FEATURE_DISABLED_BY_DEFAULT);

DetectionObserver::DetectionObserver(DetectionProcessors processors)
    : processors_(std::move(processors)) {
  CHECK_GT(processors_.size(), 0ul) << "No processors given";
  if (base::FeatureList::IsEnabled(kEmitKioskVisionFakes)) {
    constexpr base::TimeDelta kFakeEmissionPeriod = base::Seconds(5);
    fake_detection_timer_.Start(
        FROM_HERE, kFakeEmissionPeriod,
        base::BindRepeating(&DetectionObserver::EmitFakeDetection,
                            base::Unretained(this)));
  }
}

DetectionObserver::~DetectionObserver() = default;

void DetectionObserver::OnFrameProcessed(
    cros::mojom::KioskVisionDetectionPtr detection) {
  ValidateDetection(*detection);
  for (const auto& processor : processors_) {
    processor->OnFrameProcessed(*detection);
  }
}

void DetectionObserver::OnTrackCompleted(
    cros::mojom::KioskVisionTrackPtr track) {
  ValidateTrack(*track);
  for (const auto& processor : processors_) {
    processor->OnTrackCompleted(*track);
  }
}

void DetectionObserver::OnError(cros::mojom::KioskVisionError error) {
  for (const auto& processor : processors_) {
    processor->OnError(error);
  }
}

void DetectionObserver::EmitFakeDetection() {
  constexpr int kLargeCount = 3;
  constexpr int kSmallCount = 2;
  constexpr int kBoxesPerRow = 6;
  constexpr int kMaxOffset = 20;
  constexpr float kConfidence = 0.7;
  constexpr float kRollFaceAngle = 10;
  constexpr float kPanFaceAngle = 20;
  constexpr float kTiltFaceAngle = 30;
  constexpr float kLowAngle = 4;

  // Makes 2 or 3 fake appearances depending on `fake_detection_flag_`.
  auto fake_detection = cros::mojom::KioskVisionDetection::New();
  for (int i = 0; i < (fake_detection_flag_ ? kLargeCount : kSmallCount); i++) {
    int offset = fake_detection_offset_ + i;

    auto fake_appearance = cros::mojom::KioskVisionAppearance::New(
        /*timestamp_in_us=*/CurrentTimestampInMicroseconds(),
        /*person_id=*/i,
        /*face=*/
        cros::mojom::KioskVisionFaceDetection::New(
            /*confidence=*/kConfidence,
            /*roll=*/kRollFaceAngle,
            // Set low face angles for index 1. Useful to test the case when the
            // person faces the device.
            /*pan=*/i == 1 ? kLowAngle : kPanFaceAngle,
            /*tilt=*/i == 1 ? kLowAngle : kTiltFaceAngle,
            /*box=*/
            cros::mojom::KioskVisionBoundingBox::New(
                /*x=*/20 + 50 * (offset % kBoxesPerRow),
                /*y=*/20 + 50 * (offset / kBoxesPerRow),
                /*width=*/20,
                /*height=*/20)),
        /*body=*/
        cros::mojom::KioskVisionBodyDetection::New(
            /*confidence=*/kConfidence,
            /*box=*/cros::mojom::KioskVisionBoundingBox::New(
                /*x=*/10 + 50 * (offset % kBoxesPerRow),
                /*y=*/20 + 50 * (offset / kBoxesPerRow),
                /*width=*/40,
                /*height=*/40)));

    fake_detection->appearances.push_back(std::move(fake_appearance));
  }
  fake_detection_flag_ = !fake_detection_flag_;
  fake_detection_offset_ = (fake_detection_offset_ + 1) % kMaxOffset;

  OnFrameProcessed(std::move(fake_detection));
}

}  // namespace ash::kiosk_vision