/* 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/components/processors/image_preprocessing_graph.h"

#include <array>
#include <complex>
#include <limits>
#include <utility>
#include <vector>

#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "mediapipe/calculators/image/image_clone_calculator.pb.h"
#include "mediapipe/calculators/tensor/image_to_tensor_calculator.pb.h"
#include "mediapipe/calculators/tensor/inference_calculator.pb.h"
#include "mediapipe/framework/api2/builder.h"
#include "mediapipe/framework/api2/port.h"
#include "mediapipe/framework/calculator_framework.h"
#include "mediapipe/framework/formats/image.h"
#include "mediapipe/framework/formats/rect.pb.h"
#include "mediapipe/framework/formats/tensor.h"
#include "mediapipe/gpu/gpu_origin.pb.h"
#include "mediapipe/tasks/cc/common.h"
#include "mediapipe/tasks/cc/components/processors/proto/image_preprocessing_graph_options.pb.h"
#include "mediapipe/tasks/cc/core/model_resources.h"
#include "mediapipe/tasks/cc/core/proto/acceleration.pb.h"
#include "mediapipe/tasks/cc/vision/utils/image_tensor_specs.h"
#include "tensorflow/lite/schema/schema_generated.h"

namespace mediapipe {
namespace tasks {
namespace components {
namespace processors {
namespace {

using ::mediapipe::NormalizedRect;
using ::mediapipe::Tensor;
using ::mediapipe::api2::Input;
using ::mediapipe::api2::Output;
using ::mediapipe::api2::builder::Graph;
using ::mediapipe::api2::builder::Source;
using ::mediapipe::tasks::core::ModelResources;
using ::mediapipe::tasks::vision::ImageTensorSpecs;

constexpr char kImageTag[] = "IMAGE";
constexpr char kNormRectTag[] = "NORM_RECT";
constexpr char kMatrixTag[] = "MATRIX";
constexpr char kTensorsTag[] = "TENSORS";
constexpr char kSizeTag[] = "SIZE";
constexpr char kImageSizeTag[] = "IMAGE_SIZE";
constexpr char kLetterboxPaddingTag[] = "LETTERBOX_PADDING";

// Struct holding the different output streams produced by the subgraph.
struct ImagePreprocessingOutputStreams {
  Source<std::vector<Tensor>> tensors;
  Source<std::array<float, 16>> matrix;
  Source<std::array<float, 4>> letterbox_padding;
  Source<std::pair<int, int>> image_size;
  Source<Image> image;
};

// Builds an ImageTensorSpecs for configuring the preprocessing calculators.
absl::StatusOr<ImageTensorSpecs> BuildImageTensorSpecs(
    const ModelResources& model_resources) {
  const tflite::Model& model = *model_resources.GetTfLiteModel();
  if (model.subgraphs()->size() != 1) {
    return CreateStatusWithPayload(
        absl::StatusCode::kInvalidArgument,
        "Image tflite models are assumed to have a single subgraph.",
        MediaPipeTasksStatus::kInvalidArgumentError);
  }
  const auto* primary_subgraph = (*model.subgraphs())[0];
  if (primary_subgraph->inputs()->size() != 1) {
    return CreateStatusWithPayload(
        absl::StatusCode::kInvalidArgument,
        "Image tflite models are assumed to have a single input.",
        MediaPipeTasksStatus::kInvalidArgumentError);
  }
  const auto* input_tensor =
      (*primary_subgraph->tensors())[(*primary_subgraph->inputs())[0]];
  ASSIGN_OR_RETURN(const auto* image_tensor_metadata,
                   vision::GetImageTensorMetadataIfAny(
                       *model_resources.GetMetadataExtractor(), 0));
  return vision::BuildInputImageTensorSpecs(*input_tensor,
                                            image_tensor_metadata);
}

// Fills in the ImageToTensorCalculatorOptions based on the ImageTensorSpecs.
absl::Status ConfigureImageToTensorCalculator(
    const ImageTensorSpecs& image_tensor_specs,
    mediapipe::ImageToTensorCalculatorOptions* options) {
  options->set_output_tensor_width(image_tensor_specs.image_width);
  options->set_output_tensor_height(image_tensor_specs.image_height);
  if (image_tensor_specs.tensor_type == tflite::TensorType_UINT8) {
    options->mutable_output_tensor_uint_range()->set_min(0);
    options->mutable_output_tensor_uint_range()->set_max(255);
  } else {
    const auto& normalization_options =
        image_tensor_specs.normalization_options;
    float mean = normalization_options->mean_values[0];
    float std = normalization_options->std_values[0];
    // TODO: Add support for per-channel normalization values.
    for (int i = 1; i < normalization_options->num_values; ++i) {
      if (normalization_options->mean_values[i] != mean ||
          normalization_options->std_values[i] != std) {
        return CreateStatusWithPayload(
            absl::StatusCode::kUnimplemented,
            "Per-channel image normalization is not available.");
      }
    }
    if (std::abs(std) < std::numeric_limits<float>::epsilon()) {
      return CreateStatusWithPayload(
          absl::StatusCode::kInternal,
          "NormalizationOptions.std_values can't be 0. Please check if the "
          "tensor metadata has been populated correctly.");
    }
    // Deduce min and max range from normalization options by applying the
    // normalization formula to the numerical limits of uint8, i.e:
    //   output = (input - mean) / std
    options->mutable_output_tensor_float_range()->set_min((0.0f - mean) / std);
    options->mutable_output_tensor_float_range()->set_max((255.0f - mean) /
                                                          std);
  }
  // TODO: need to support different GPU origin on differnt
  // platforms or applications.
  options->set_gpu_origin(mediapipe::GpuOrigin::TOP_LEFT);
  return absl::OkStatus();
}

}  // namespace

bool DetermineImagePreprocessingGpuBackend(
    const core::proto::Acceleration& acceleration) {
  return acceleration.has_gpu();
}

absl::Status ConfigureImagePreprocessingGraph(
    const ModelResources& model_resources, bool use_gpu,
    proto::ImagePreprocessingGraphOptions* options) {
  ASSIGN_OR_RETURN(auto image_tensor_specs,
                   BuildImageTensorSpecs(model_resources));
  MP_RETURN_IF_ERROR(ConfigureImageToTensorCalculator(
      image_tensor_specs, options->mutable_image_to_tensor_options()));
  // The GPU backend isn't able to process int data. If the input tensor is
  // quantized, forces the image preprocessing graph to use CPU backend.
  if (use_gpu && image_tensor_specs.tensor_type != tflite::TensorType_UINT8) {
    options->set_backend(proto::ImagePreprocessingGraphOptions::GPU_BACKEND);
  } else {
    options->set_backend(proto::ImagePreprocessingGraphOptions::CPU_BACKEND);
  }
  return absl::OkStatus();
}

Source<Image> AddDataConverter(Source<Image> image_in, Graph& graph,
                               bool output_on_gpu) {
  auto& image_converter = graph.AddNode("ImageCloneCalculator");
  image_converter.GetOptions<mediapipe::ImageCloneCalculatorOptions>()
      .set_output_on_gpu(output_on_gpu);
  image_in >> image_converter.In("");
  return image_converter[Output<Image>("")];
}

// An ImagePreprocessingGraph performs image preprocessing.
// - Accepts CPU input images and outputs CPU tensors.
//
// Inputs:
//   IMAGE - Image
//     The image to preprocess.
//   NORM_RECT - NormalizedRect @Optional
//     Describes region of image to extract.
//     @Optional: rect covering the whole image is used if not specified.
// Outputs:
//   TENSORS - std::vector<Tensor>
//     Vector containing a single Tensor populated with the converted and
//     preprocessed image.
//   MATRIX - std::array<float,16> @Optional
//     An std::array<float, 16> representing a 4x4 row-major-order matrix that
//     maps a point on the input image to a point on the output tensor, and
//     can be used to reverse the mapping by inverting the matrix.
//   LETTERBOX_PADDING - std::array<float, 4> @Optional
//     An std::array<float, 4> representing the letterbox padding from the 4
//     sides ([left, top, right, bottom]) of the output image, normalized to
//     [0.f, 1.f] by the output dimensions. The padding values are non-zero only
//     when the "keep_aspect_ratio" is true in ImagePreprocessingGraphOptions.
//   IMAGE_SIZE - std::pair<int,int> @Optional
//     The size of the original input image as a <width, height> pair.
//   IMAGE - Image @Optional
//     The image that has the pixel data stored on the target storage (CPU vs
//     GPU).
//
// The recommended way of using this subgraph is through the GraphBuilder API
// using the 'ConfigureImagePreprocessingGraph()' function. See header file for
// more details.
class ImagePreprocessingGraph : public Subgraph {
 public:
  absl::StatusOr<CalculatorGraphConfig> GetConfig(
      SubgraphContext* sc) override {
    Graph graph;
    auto output_streams = BuildImagePreprocessing(
        sc->Options<proto::ImagePreprocessingGraphOptions>(),
        graph[Input<Image>(kImageTag)],
        graph[Input<NormalizedRect>::Optional(kNormRectTag)], graph);
    output_streams.tensors >> graph[Output<std::vector<Tensor>>(kTensorsTag)];
    output_streams.matrix >> graph[Output<std::array<float, 16>>(kMatrixTag)];
    output_streams.letterbox_padding >>
        graph[Output<std::array<float, 4>>(kLetterboxPaddingTag)];
    output_streams.image_size >>
        graph[Output<std::pair<int, int>>(kImageSizeTag)];
    output_streams.image >> graph[Output<Image>(kImageTag)];
    return graph.GetConfig();
  }

 private:
  // Adds a mediapipe image preprocessing subgraph into the provided
  // builder::Graph instance. The image preprocessing subgraph takes images
  // (mediapipe::Image) and region of interest (mediapipe::NormalizedRect) as
  // inputs and returns 5 output streams:
  //   - the converted tensor (mediapipe::Tensor),
  //   - the transformation matrix (std::array<float, 16>),
  //   - the letterbox padding (std::array<float, 4>>),
  //   - the original image size (std::pair<int, int>),
  //   - the image that has pixel data stored on the target storage
  //     (mediapipe::Image).
  //
  // options: the mediapipe tasks ImagePreprocessingGraphOptions.
  // image_in: (mediapipe::Image) stream to preprocess.
  // graph: the mediapipe builder::Graph instance to be updated.
  ImagePreprocessingOutputStreams BuildImagePreprocessing(
      const proto::ImagePreprocessingGraphOptions& options,
      Source<Image> image_in, Source<NormalizedRect> norm_rect_in,
      Graph& graph) {
    // Convert image to tensor.
    auto& image_to_tensor = graph.AddNode("ImageToTensorCalculator");
    image_to_tensor.GetOptions<mediapipe::ImageToTensorCalculatorOptions>()
        .CopyFrom(options.image_to_tensor_options());
    switch (options.backend()) {
      case proto::ImagePreprocessingGraphOptions::CPU_BACKEND: {
        auto cpu_image =
            AddDataConverter(image_in, graph, /*output_on_gpu=*/false);
        cpu_image >> image_to_tensor.In(kImageTag);
        break;
      }
      case proto::ImagePreprocessingGraphOptions::GPU_BACKEND: {
        auto gpu_image =
            AddDataConverter(image_in, graph, /*output_on_gpu=*/true);
        gpu_image >> image_to_tensor.In(kImageTag);
        break;
      }
      default:
        image_in >> image_to_tensor.In(kImageTag);
    }
    norm_rect_in >> image_to_tensor.In(kNormRectTag);

    // Extract optional image properties.
    auto& image_size = graph.AddNode("ImagePropertiesCalculator");
    image_in >> image_size.In(kImageTag);

    // TODO: Replace PassThroughCalculator with a calculator that
    // converts the pixel data to be stored on the target storage (CPU vs GPU).
    auto& pass_through = graph.AddNode("PassThroughCalculator");
    image_in >> pass_through.In("");

    // Connect outputs.
    return {
        /* tensors= */ image_to_tensor[Output<std::vector<Tensor>>(
            kTensorsTag)],
        /* matrix= */
        image_to_tensor[Output<std::array<float, 16>>(kMatrixTag)],
        /* letterbox_padding= */
        image_to_tensor[Output<std::array<float, 4>>(kLetterboxPaddingTag)],
        /* image_size= */ image_size[Output<std::pair<int, int>>(kSizeTag)],
        /* image= */ pass_through[Output<Image>("")],
    };
  }
};
REGISTER_MEDIAPIPE_GRAPH(
    ::mediapipe::tasks::components::processors::ImagePreprocessingGraph);

}  // namespace processors
}  // namespace components
}  // namespace tasks
}  // namespace mediapipe
