/* Copyright 2022 The MediaPipe Authors. All Rights Reserved.

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/tasks/cc/vision/image_segmenter/image_segmenter.h"

#include <cstdint>
#include <memory>

#include "absl/flags/flag.h"
#include "mediapipe/framework/deps/file_path.h"
#include "mediapipe/framework/formats/image.h"
#include "mediapipe/framework/formats/image_frame.h"
#include "mediapipe/framework/formats/image_frame_opencv.h"
#include "mediapipe/framework/port/gmock.h"
#include "mediapipe/framework/port/gtest.h"
#include "mediapipe/framework/port/opencv_core_inc.h"
#include "mediapipe/framework/port/opencv_imgcodecs_inc.h"
#include "mediapipe/framework/port/status_matchers.h"
#include "mediapipe/tasks/cc/components/containers/rect.h"
#include "mediapipe/tasks/cc/core/proto/base_options.pb.h"
#include "mediapipe/tasks/cc/core/proto/external_file.pb.h"
#include "mediapipe/tasks/cc/vision/core/image_processing_options.h"
#include "mediapipe/tasks/cc/vision/image_segmenter/calculators/tensors_to_segmentation_calculator.pb.h"
#include "mediapipe/tasks/cc/vision/image_segmenter/proto/image_segmenter_graph_options.pb.h"
#include "mediapipe/tasks/cc/vision/utils/image_utils.h"
#include "tensorflow/lite/core/shims/cc/shims_test_util.h"
#include "tensorflow/lite/kernels/builtin_op_kernels.h"
#include "tensorflow/lite/mutable_op_resolver.h"

namespace mediapipe {
namespace tasks {
namespace vision {
namespace image_segmenter {
namespace {

using ::mediapipe::Image;
using ::mediapipe::file::JoinPath;
using ::mediapipe::tasks::components::containers::RectF;
using ::mediapipe::tasks::vision::core::ImageProcessingOptions;
using ::testing::HasSubstr;
using ::testing::Optional;

constexpr char kTestDataDirectory[] = "/mediapipe/tasks/testdata/vision/";
constexpr char kDeeplabV3WithMetadata[] = "deeplabv3.tflite";

constexpr char kSelfie128x128WithMetadata[] = "selfie_segm_128_128_3.tflite";

constexpr char kSelfie144x256WithMetadata[] = "selfie_segm_144_256_3.tflite";

constexpr float kGoldenMaskSimilarity = 0.98;

// Magnification factor used when creating the golden category masks to make
// them more human-friendly. Each pixel in the golden masks has its value
// multiplied by this factor, i.e. a value of 10 means class index 1, a value of
// 20 means class index 2, etc.
constexpr int kGoldenMaskMagnificationFactor = 10;

// Intentionally converting output into CV_8UC1 and then again into CV_32FC1
// as expected outputs are stored in CV_8UC1, so this conversion allows to do
// fair comparison.
cv::Mat PostProcessResultMask(const cv::Mat& mask) {
  cv::Mat mask_float;
  mask.convertTo(mask_float, CV_8UC1, 255);
  mask_float.convertTo(mask_float, CV_32FC1, 1 / 255.f);
  return mask_float;
}

Image GetSRGBImage(const std::string& image_path) {
  // TODO: fix test so RGB really is used and not BGR/BGRA.
  // mediapipe/app/aimatter/segmentation/segmenter_test_common.cc
  // golden masks are generated with BGR image. To align with the unittest of
  // aimatter segmenter, here reads image as BGR as well (opencv reads image as
  // BGR). Once the correctness of mediapipe tasks segmenter is verified, change
  // the golden masks to be generated by RGB image.
  cv::Mat image_mat = cv::imread(image_path);
  mediapipe::ImageFrame image_frame(
      mediapipe::ImageFormat::SRGB, image_mat.cols, image_mat.rows,
      image_mat.step, image_mat.data, [image_mat](uint8[]) {});
  Image image(std::make_shared<mediapipe::ImageFrame>(std::move(image_frame)));
  return image;
}

double CalculateSum(const cv::Mat& m) {
  double sum = 0.0;
  cv::Scalar s = cv::sum(m);
  for (int i = 0; i < m.channels(); ++i) {
    sum += s.val[i];
  }
  return sum;
}

double CalculateSoftIOU(const cv::Mat& m1, const cv::Mat& m2) {
  cv::Mat intersection;
  cv::multiply(m1, m2, intersection);
  double intersection_value = CalculateSum(intersection);
  double union_value =
      CalculateSum(m1.mul(m1)) + CalculateSum(m2.mul(m2)) - intersection_value;
  return union_value > 0.0 ? intersection_value / union_value : 0.0;
}

MATCHER_P2(SimilarToFloatMask, expected_mask, similarity_threshold, "") {
  cv::Mat actual_mask = PostProcessResultMask(arg);
  return arg.rows == expected_mask.rows && arg.cols == expected_mask.cols &&
         CalculateSoftIOU(arg, expected_mask) > similarity_threshold;
}

MATCHER_P3(SimilarToUint8Mask, expected_mask, similarity_threshold,
           magnification_factor, "") {
  if (arg.rows != expected_mask.rows || arg.cols != expected_mask.cols) {
    return false;
  }
  int consistent_pixels = 0;
  const int num_pixels = expected_mask.rows * expected_mask.cols;
  for (int i = 0; i < num_pixels; ++i) {
    consistent_pixels +=
        (arg.data[i] * magnification_factor == expected_mask.data[i]);
  }
  return static_cast<float>(consistent_pixels) / num_pixels >=
         similarity_threshold;
}

class DeepLabOpResolver : public ::tflite::MutableOpResolver {
 public:
  DeepLabOpResolver() {
    AddBuiltin(::tflite::BuiltinOperator_ADD,
               ::tflite::ops::builtin::Register_ADD());
    AddBuiltin(::tflite::BuiltinOperator_AVERAGE_POOL_2D,
               ::tflite::ops::builtin::Register_AVERAGE_POOL_2D());
    AddBuiltin(::tflite::BuiltinOperator_CONCATENATION,
               ::tflite::ops::builtin::Register_CONCATENATION());
    AddBuiltin(::tflite::BuiltinOperator_CONV_2D,
               ::tflite::ops::builtin::Register_CONV_2D());
    // DeepLab uses different versions of DEPTHWISE_CONV_2D.
    AddBuiltin(::tflite::BuiltinOperator_DEPTHWISE_CONV_2D,
               ::tflite::ops::builtin::Register_DEPTHWISE_CONV_2D(),
               /*min_version=*/1, /*max_version=*/2);
    AddBuiltin(::tflite::BuiltinOperator_RESIZE_BILINEAR,
               ::tflite::ops::builtin::Register_RESIZE_BILINEAR());
  }

  DeepLabOpResolver(const DeepLabOpResolver& r) = delete;
};

class CreateFromOptionsTest : public tflite_shims::testing::Test {};

class DeepLabOpResolverMissingOps : public ::tflite::MutableOpResolver {
 public:
  DeepLabOpResolverMissingOps() {
    AddBuiltin(::tflite::BuiltinOperator_ADD,
               ::tflite::ops::builtin::Register_ADD());
  }

  DeepLabOpResolverMissingOps(const DeepLabOpResolverMissingOps& r) = delete;
};

TEST_F(CreateFromOptionsTest, SucceedsWithSelectiveOpResolver) {
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->base_options.op_resolver = absl::make_unique<DeepLabOpResolver>();
  MP_ASSERT_OK(ImageSegmenter::Create(std::move(options)));
}

TEST_F(CreateFromOptionsTest, FailsWithSelectiveOpResolverMissingOps) {
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->base_options.op_resolver =
      absl::make_unique<DeepLabOpResolverMissingOps>();
  auto segmenter_or = ImageSegmenter::Create(std::move(options));
  // TODO: Make MediaPipe InferenceCalculator report the detailed
  // interpreter errors (e.g., "Encountered unresolved custom op").
  EXPECT_EQ(segmenter_or.status().code(), absl::StatusCode::kInternal);
  EXPECT_THAT(
      segmenter_or.status().message(),
      testing::HasSubstr("interpreter_builder(&interpreter) == kTfLiteOk"));
}

TEST_F(CreateFromOptionsTest, FailsWithMissingModel) {
  absl::StatusOr<std::unique_ptr<ImageSegmenter>> segmenter_or =
      ImageSegmenter::Create(std::make_unique<ImageSegmenterOptions>());

  EXPECT_EQ(segmenter_or.status().code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(
      segmenter_or.status().message(),
      HasSubstr("ExternalFile must specify at least one of 'file_content', "
                "'file_name', 'file_pointer_meta' or 'file_descriptor_meta'."));
  EXPECT_THAT(segmenter_or.status().GetPayload(kMediaPipeTasksPayload),
              Optional(absl::Cord(absl::StrCat(
                  MediaPipeTasksStatus::kRunnerInitializationError))));
}

class ImageModeTest : public tflite_shims::testing::Test {};

TEST_F(ImageModeTest, SucceedsWithCategoryMask) {
  MP_ASSERT_OK_AND_ASSIGN(
      Image image,
      DecodeImageFromFile(JoinPath("./", kTestDataDirectory,
                                   "segmentation_input_rotation0.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CATEGORY_MASK;

  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  MP_ASSERT_OK_AND_ASSIGN(auto category_masks, segmenter->Segment(image));
  EXPECT_EQ(category_masks.size(), 1);

  cv::Mat actual_mask = mediapipe::formats::MatView(
      category_masks[0].GetImageFrameSharedPtr().get());

  cv::Mat expected_mask = cv::imread(
      JoinPath("./", kTestDataDirectory, "segmentation_golden_rotation0.png"),
      cv::IMREAD_GRAYSCALE);
  EXPECT_THAT(actual_mask,
              SimilarToUint8Mask(expected_mask, kGoldenMaskSimilarity,
                                 kGoldenMaskMagnificationFactor));
}

TEST_F(ImageModeTest, SucceedsWithConfidenceMask) {
  MP_ASSERT_OK_AND_ASSIGN(
      Image image,
      DecodeImageFromFile(JoinPath("./", kTestDataDirectory, "cat.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CONFIDENCE_MASK;
  options->activation = ImageSegmenterOptions::Activation::SOFTMAX;

  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  MP_ASSERT_OK_AND_ASSIGN(auto confidence_masks, segmenter->Segment(image));
  EXPECT_EQ(confidence_masks.size(), 21);

  cv::Mat expected_mask = cv::imread(
      JoinPath("./", kTestDataDirectory, "cat_mask.jpg"), cv::IMREAD_GRAYSCALE);
  cv::Mat expected_mask_float;
  expected_mask.convertTo(expected_mask_float, CV_32FC1, 1 / 255.f);

  // Cat category index 8.
  cv::Mat cat_mask = mediapipe::formats::MatView(
      confidence_masks[8].GetImageFrameSharedPtr().get());
  EXPECT_THAT(cat_mask,
              SimilarToFloatMask(expected_mask_float, kGoldenMaskSimilarity));
}

TEST_F(ImageModeTest, SucceedsWithRotation) {
  MP_ASSERT_OK_AND_ASSIGN(
      Image image, DecodeImageFromFile(
                       JoinPath("./", kTestDataDirectory, "cat_rotated.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CONFIDENCE_MASK;
  options->activation = ImageSegmenterOptions::Activation::SOFTMAX;

  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  ImageProcessingOptions image_processing_options;
  image_processing_options.rotation_degrees = -90;
  MP_ASSERT_OK_AND_ASSIGN(auto confidence_masks, segmenter->Segment(image));
  EXPECT_EQ(confidence_masks.size(), 21);

  cv::Mat expected_mask =
      cv::imread(JoinPath("./", kTestDataDirectory, "cat_rotated_mask.jpg"),
                 cv::IMREAD_GRAYSCALE);
  cv::Mat expected_mask_float;
  expected_mask.convertTo(expected_mask_float, CV_32FC1, 1 / 255.f);

  // Cat category index 8.
  cv::Mat cat_mask = mediapipe::formats::MatView(
      confidence_masks[8].GetImageFrameSharedPtr().get());
  EXPECT_THAT(cat_mask,
              SimilarToFloatMask(expected_mask_float, kGoldenMaskSimilarity));
}

TEST_F(ImageModeTest, FailsWithRegionOfInterest) {
  MP_ASSERT_OK_AND_ASSIGN(
      Image image,
      DecodeImageFromFile(JoinPath("./", kTestDataDirectory, "cat.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CONFIDENCE_MASK;
  options->activation = ImageSegmenterOptions::Activation::SOFTMAX;

  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  RectF roi{/*left=*/0.1, /*top=*/0, /*right=*/0.9, /*bottom=*/1};
  ImageProcessingOptions image_processing_options{roi, /*rotation_degrees=*/0};

  auto results = segmenter->Segment(image, image_processing_options);
  EXPECT_EQ(results.status().code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(results.status().message(),
              HasSubstr("This task doesn't support region-of-interest"));
  EXPECT_THAT(
      results.status().GetPayload(kMediaPipeTasksPayload),
      Optional(absl::Cord(absl::StrCat(
          MediaPipeTasksStatus::kImageProcessingInvalidArgumentError))));
}

TEST_F(ImageModeTest, SucceedsSelfie128x128Segmentation) {
  Image image =
      GetSRGBImage(JoinPath("./", kTestDataDirectory, "mozart_square.jpg"));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kSelfie128x128WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CONFIDENCE_MASK;
  options->activation = ImageSegmenterOptions::Activation::SOFTMAX;

  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  MP_ASSERT_OK_AND_ASSIGN(auto confidence_masks, segmenter->Segment(image));
  EXPECT_EQ(confidence_masks.size(), 2);

  cv::Mat expected_mask =
      cv::imread(JoinPath("./", kTestDataDirectory,
                          "selfie_segm_128_128_3_expected_mask.jpg"),
                 cv::IMREAD_GRAYSCALE);
  cv::Mat expected_mask_float;
  expected_mask.convertTo(expected_mask_float, CV_32FC1, 1 / 255.f);

  // Selfie category index 1.
  cv::Mat selfie_mask = mediapipe::formats::MatView(
      confidence_masks[1].GetImageFrameSharedPtr().get());
  EXPECT_THAT(selfie_mask,
              SimilarToFloatMask(expected_mask_float, kGoldenMaskSimilarity));
}

TEST_F(ImageModeTest, SucceedsSelfie144x256Segmentations) {
  Image image =
      GetSRGBImage(JoinPath("./", kTestDataDirectory, "mozart_square.jpg"));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kSelfie144x256WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CONFIDENCE_MASK;
  options->activation = ImageSegmenterOptions::Activation::NONE;
  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  MP_ASSERT_OK_AND_ASSIGN(auto confidence_masks, segmenter->Segment(image));
  EXPECT_EQ(confidence_masks.size(), 1);

  cv::Mat expected_mask =
      cv::imread(JoinPath("./", kTestDataDirectory,
                          "selfie_segm_144_256_3_expected_mask.jpg"),
                 cv::IMREAD_GRAYSCALE);
  cv::Mat expected_mask_float;
  expected_mask.convertTo(expected_mask_float, CV_32FC1, 1 / 255.f);

  cv::Mat selfie_mask = mediapipe::formats::MatView(
      confidence_masks[0].GetImageFrameSharedPtr().get());
  EXPECT_THAT(selfie_mask,
              SimilarToFloatMask(expected_mask_float, kGoldenMaskSimilarity));
}

class VideoModeTest : public tflite_shims::testing::Test {};

TEST_F(VideoModeTest, FailsWithCallingWrongMethod) {
  MP_ASSERT_OK_AND_ASSIGN(
      Image image,
      DecodeImageFromFile(JoinPath("./", kTestDataDirectory,
                                   "segmentation_input_rotation0.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CATEGORY_MASK;
  options->running_mode = core::RunningMode::VIDEO;

  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  auto results = segmenter->Segment(image);
  EXPECT_EQ(results.status().code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(results.status().message(),
              HasSubstr("not initialized with the image mode"));
  EXPECT_THAT(results.status().GetPayload(kMediaPipeTasksPayload),
              Optional(absl::Cord(absl::StrCat(
                  MediaPipeTasksStatus::kRunnerApiCalledInWrongModeError))));

  results = segmenter->SegmentAsync(image, 0);
  EXPECT_EQ(results.status().code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(results.status().message(),
              HasSubstr("not initialized with the live stream mode"));
  EXPECT_THAT(results.status().GetPayload(kMediaPipeTasksPayload),
              Optional(absl::Cord(absl::StrCat(
                  MediaPipeTasksStatus::kRunnerApiCalledInWrongModeError))));
  MP_ASSERT_OK(segmenter->Close());
}

TEST_F(VideoModeTest, Succeeds) {
  constexpr int iterations = 100;
  MP_ASSERT_OK_AND_ASSIGN(
      Image image,
      DecodeImageFromFile(JoinPath("./", kTestDataDirectory,
                                   "segmentation_input_rotation0.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CATEGORY_MASK;
  options->running_mode = core::RunningMode::VIDEO;
  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  cv::Mat expected_mask = cv::imread(
      JoinPath("./", kTestDataDirectory, "segmentation_golden_rotation0.png"),
      cv::IMREAD_GRAYSCALE);
  for (int i = 0; i < iterations; ++i) {
    MP_ASSERT_OK_AND_ASSIGN(auto category_masks,
                            segmenter->SegmentForVideo(image, i));
    EXPECT_EQ(category_masks.size(), 1);
    cv::Mat actual_mask = mediapipe::formats::MatView(
        category_masks[0].GetImageFrameSharedPtr().get());
    EXPECT_THAT(actual_mask,
                SimilarToUint8Mask(expected_mask, kGoldenMaskSimilarity,
                                   kGoldenMaskMagnificationFactor));
  }
  MP_ASSERT_OK(segmenter->Close());
}

class LiveStreamModeTest : public tflite_shims::testing::Test {};

TEST_F(LiveStreamModeTest, FailsWithCallingWrongMethod) {
  MP_ASSERT_OK_AND_ASSIGN(Image image, DecodeImageFromFile(JoinPath(
                                           "./", kTestDataDirectory,
                                           "cats_and_dogs_no_resizing.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CATEGORY_MASK;
  options->running_mode = core::RunningMode::LIVE_STREAM;
  options->result_callback =
      [](absl::StatusOr<std::vector<Image>> segmented_masks, const Image& image,
         int64 timestamp_ms) {};
  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));

  auto results = segmenter->Segment(image);
  EXPECT_EQ(results.status().code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(results.status().message(),
              HasSubstr("not initialized with the image mode"));
  EXPECT_THAT(results.status().GetPayload(kMediaPipeTasksPayload),
              Optional(absl::Cord(absl::StrCat(
                  MediaPipeTasksStatus::kRunnerApiCalledInWrongModeError))));

  results = segmenter->SegmentForVideo(image, 0);
  EXPECT_EQ(results.status().code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(results.status().message(),
              HasSubstr("not initialized with the video mode"));
  EXPECT_THAT(results.status().GetPayload(kMediaPipeTasksPayload),
              Optional(absl::Cord(absl::StrCat(
                  MediaPipeTasksStatus::kRunnerApiCalledInWrongModeError))));
  MP_ASSERT_OK(segmenter->Close());
}

TEST_F(LiveStreamModeTest, FailsWithOutOfOrderInputTimestamps) {
  MP_ASSERT_OK_AND_ASSIGN(Image image, DecodeImageFromFile(JoinPath(
                                           "./", kTestDataDirectory,
                                           "cats_and_dogs_no_resizing.jpg")));
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CATEGORY_MASK;
  options->running_mode = core::RunningMode::LIVE_STREAM;
  options->result_callback =
      [](absl::StatusOr<std::vector<Image>> segmented_masks, const Image& image,
         int64 timestamp_ms) {};
  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  MP_ASSERT_OK(segmenter->SegmentAsync(image, 1));

  auto status = segmenter->SegmentAsync(image, 0);
  EXPECT_EQ(status.code(), absl::StatusCode::kInvalidArgument);
  EXPECT_THAT(status.message(),
              HasSubstr("timestamp must be monotonically increasing"));
  EXPECT_THAT(status.GetPayload(kMediaPipeTasksPayload),
              Optional(absl::Cord(absl::StrCat(
                  MediaPipeTasksStatus::kRunnerInvalidTimestampError))));
  MP_ASSERT_OK(segmenter->SegmentAsync(image, 2));
  MP_ASSERT_OK(segmenter->Close());
}

TEST_F(LiveStreamModeTest, Succeeds) {
  constexpr int iterations = 100;
  MP_ASSERT_OK_AND_ASSIGN(
      Image image,
      DecodeImageFromFile(JoinPath("./", kTestDataDirectory,
                                   "segmentation_input_rotation0.jpg")));
  std::vector<std::vector<Image>> segmented_masks_results;
  std::vector<std::pair<int, int>> image_sizes;
  std::vector<int64> timestamps;
  auto options = std::make_unique<ImageSegmenterOptions>();
  options->base_options.model_asset_path =
      JoinPath("./", kTestDataDirectory, kDeeplabV3WithMetadata);
  options->output_type = ImageSegmenterOptions::OutputType::CATEGORY_MASK;
  options->running_mode = core::RunningMode::LIVE_STREAM;
  options->result_callback =
      [&segmented_masks_results, &image_sizes, &timestamps](
          absl::StatusOr<std::vector<Image>> segmented_masks,
          const Image& image, int64 timestamp_ms) {
        MP_ASSERT_OK(segmented_masks.status());
        segmented_masks_results.push_back(std::move(segmented_masks).value());
        image_sizes.push_back({image.width(), image.height()});
        timestamps.push_back(timestamp_ms);
      };
  MP_ASSERT_OK_AND_ASSIGN(std::unique_ptr<ImageSegmenter> segmenter,
                          ImageSegmenter::Create(std::move(options)));
  for (int i = 0; i < iterations; ++i) {
    MP_ASSERT_OK(segmenter->SegmentAsync(image, i));
  }
  MP_ASSERT_OK(segmenter->Close());
  // Due to the flow limiter, the total of outputs will be smaller than the
  // number of iterations.
  ASSERT_LE(segmented_masks_results.size(), iterations);
  ASSERT_GT(segmented_masks_results.size(), 0);
  cv::Mat expected_mask = cv::imread(
      JoinPath("./", kTestDataDirectory, "segmentation_golden_rotation0.png"),
      cv::IMREAD_GRAYSCALE);
  for (const auto& segmented_masks : segmented_masks_results) {
    EXPECT_EQ(segmented_masks.size(), 1);
    cv::Mat actual_mask = mediapipe::formats::MatView(
        segmented_masks[0].GetImageFrameSharedPtr().get());
    EXPECT_THAT(actual_mask,
                SimilarToUint8Mask(expected_mask, kGoldenMaskSimilarity,
                                   kGoldenMaskMagnificationFactor));
  }
  for (const auto& image_size : image_sizes) {
    EXPECT_EQ(image_size.first, image.width());
    EXPECT_EQ(image_size.second, image.height());
  }
  int64 timestamp_ms = -1;
  for (const auto& timestamp : timestamps) {
    EXPECT_GT(timestamp, timestamp_ms);
    timestamp_ms = timestamp;
  }
}

// TODO: Add test for hair segmentation model.

}  // namespace
}  // namespace image_segmenter
}  // namespace vision
}  // namespace tasks
}  // namespace mediapipe
