chromium/chromeos/ash/services/recording/octree_color_quantizer.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chromeos/ash/services/recording/octree_color_quantizer.h"

#include <algorithm>
#include <cstdint>
#include <memory>
#include <vector>

#include "base/check_op.h"
#include "base/notreached.h"
#include "chromeos/ash/services/recording/gif_encoding_types.h"
#include "chromeos/ash/services/recording/rgb_video_frame.h"

namespace recording {

namespace {

// The masks used to extract the bits the correspond to each level, where level
// is the index into this array.
// Level 0 corresponds to the most significant bit in an R, G, or B color
// component, whereas level 7 corresponds to the least significant bit.
constexpr uint8_t kLevelMasks[kNumBitsPerColorChannel] = {
    0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};

// Forms and returns an index by combining 3 bits, one from each color component
// R, G, and B respectively. The position of these bits are determined by the
// given `level`. See comment above.
ColorIndex GetColorIndexAtLevel(const RgbColor& color, int level) {
  ColorIndex index = 0;
  const auto mask = kLevelMasks[level];

  if (color.r & mask) {
    //         RGB.
    index |= 0b100;
  }
  if (color.g & mask) {
    //         RGB.
    index |= 0b010;
  }
  if (color.b & mask) {
    //         RGB.
    index |= 0b001;
  }

  return index;
}

// Invokes the given functor `f` on each pixel's `RgbColor` of the given
// `rgb_video_frame`.
template <class Functor>
void ForEachPixelColor(RgbVideoFrame& rgb_video_frame, Functor f) {
  auto* pixel = &rgb_video_frame.pixel_color(0, 0);
  const auto* const end = &pixel[rgb_video_frame.num_pixels()];
  for (; pixel < end; ++pixel) {
    f(*pixel);
  }
}

template <class Functor>
void ForEachPixelColor(const RgbVideoFrame& rgb_video_frame, Functor f) {
  ForEachPixelColor(const_cast<RgbVideoFrame&>(rgb_video_frame), f);
}

// Defines a color error per each color channel (R, G, and B), which is the
// difference between the original color of a pixel, and the quantized
// (predicted) color that we get from the Octree.
//
// We use this type instead of `RgbColor` (whose components are `uint8_t`s), as
// we need the components to be represented as `int`s, since the difference can
// be negative, and when scaled by the Floyd-Steinberg factors, the values can
// exceed the maximum of 255.
struct ErrorVector {
  inline bool IsZero() const { return r == 0 && g == 0 && b == 0; }

  int r;
  int g;
  int b;
};

// Given the `original_color` of a pixel, and its `quantized_color`, returns the
// color error vector, which is the difference between the two.
ErrorVector GetErrorVector(const RgbColor& original_color,
                           const RgbColor& quantized_color) {
  return ErrorVector(original_color.r - quantized_color.r,
                     original_color.g - quantized_color.g,
                     original_color.b - quantized_color.b);
}

// Diffuses the given color `error_vector` over the given `color` by a factor
// equal to `factor / 16`. This means that each color component of
// `error_vector` will be multiplied by a `factor / 16` and added to the
// corresponding color component of `color`. The resulting `RgbColor` is
// returned.
RgbColor DiffuseErrorOnColor(const ErrorVector& error_vector,
                             const RgbColor& color,
                             int factor) {
  return RgbColor(std::clamp(error_vector.r * factor / 16 + color.r, 0, 255),
                  std::clamp(error_vector.g * factor / 16 + color.g, 0, 255),
                  std::clamp(error_vector.b * factor / 16 + color.b, 0, 255));
}

}  // namespace

// -----------------------------------------------------------------------------
// OctreeColorQuantizer:

OctreeColorQuantizer::OctreeColorQuantizer() = default;

OctreeColorQuantizer::OctreeColorQuantizer(
    const RgbVideoFrame& rgb_video_frame) {
  // Insert all the colors in the given `rgb_video_frame` into this tree. If a
  // color is referenced by multiple pixels, the same corresponding leaf node
  // will be reached, and its `red_`, `green_`, `blue_` and `ref_count_` will be
  // incremented.
  NodesPerLevel nodes_per_level;
  ForEachPixelColor(rgb_video_frame, [&](const RgbColor& color) {
    InsertColor(color, nodes_per_level);
  });

  // Reduce the leaf nodes (i.e. number of unique colors) to a maximum of
  // `kMaxNumberOfColorsInPalette` (256) colors.
  Reduce(nodes_per_level);
}

OctreeColorQuantizer::OctreeColorQuantizer(OctreeColorQuantizer&&) = default;

OctreeColorQuantizer& OctreeColorQuantizer::operator=(OctreeColorQuantizer&&) =
    default;

OctreeColorQuantizer::~OctreeColorQuantizer() = default;

void OctreeColorQuantizer::ExtractColorPalette(ColorTable& out_color_palette) {
  out_color_palette.clear();

  size_t color_palette_index = 0;
  Node* curr = leaf_nodes_head_;
  while (curr != nullptr) {
    out_color_palette.push_back(curr->GetColor());
    curr->palette_index_ = color_palette_index++;
    curr = curr->next_;
  }
}

void OctreeColorQuantizer::ExtractPixelColorIndices(
    RgbVideoFrame& rgb_video_frame,
    const ColorTable& color_palette,
    ColorIndices& out_pixel_color_indices) const {
  size_t pixel_index = 0;
  const int width = rgb_video_frame.width();
  const int height = rgb_video_frame.height();
  ForEachPixelColor(rgb_video_frame, [&](const RgbColor& color) {
    const auto color_index = FindColorIndex(color);
    out_pixel_color_indices[pixel_index] = color_index;

    const int row = pixel_index / width;
    const int column = pixel_index % width;

    ++pixel_index;

    // The below implements the "Floyd-Steinberg" dithering algorithm (see
    // https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering). It
    // works by diffusing (i.e. distributing) the color error of a pixel over
    // neighboring pixels by the following factors:
    //
    // ----------------+----------+---------------+----------+------------------
    //                 |          | current pixel |  7 / 16  |
    // ----------------+----------+---------------+----------+------------------
    //                 |  3 / 16  |     5 / 16    |  1 / 16  |
    // ----------------+----------+---------------+----------+------------------
    //
    // It actually modifies the colors of the pixels that haven't been processed
    // yet in the `rgb_video_frame` which will affect their quantized color when
    // they get processed in the upcoming iterations. This results in the
    // dithering of the quantized image.
    const ErrorVector error_vector =
        GetErrorVector(/*original_color=*/color,
                       /*quantized_color=*/color_palette[color_index]);
    if (error_vector.IsZero()) {
      return;
    }

    const auto next_column = column + 1;
    const auto next_row = row + 1;
    const bool is_next_row_valid = next_row < height;

    if (next_column < width) {
      // Same row, next column. Add error with a factor of `7 / 16`.
      auto& next_col_color = rgb_video_frame.pixel_color(row, next_column);
      next_col_color =
          DiffuseErrorOnColor(error_vector, next_col_color, /*factor=*/7);

      if (is_next_row_valid) {
        // Next row, next column. Add error with a factor of `1 / 16`.
        auto& next_row_col_color =
            rgb_video_frame.pixel_color(next_row, next_column);
        next_row_col_color =
            DiffuseErrorOnColor(error_vector, next_row_col_color, /*factor=*/1);
      }
    }

    if (is_next_row_valid) {
      // Next row, same column. Add error with a factor of `5 / 16`.
      auto& next_row_color = rgb_video_frame.pixel_color(next_row, column);
      next_row_color =
          DiffuseErrorOnColor(error_vector, next_row_color, /*factor=*/5);

      // Next row, previous column. Add error with a factor of `3 / 16`.
      const auto prev_column = column - 1;
      if (prev_column >= 0) {
        auto& next_row_prev_col_color =
            rgb_video_frame.pixel_color(next_row, prev_column);
        next_row_prev_col_color = DiffuseErrorOnColor(
            error_vector, next_row_prev_col_color, /*factor=*/3);
      }
    }
  });
}

// -----------------------------------------------------------------------------
// OctreeColorQuantizer::Node:

OctreeColorQuantizer::Node::Node() = default;

OctreeColorQuantizer::Node::Node(Node&&) = default;

OctreeColorQuantizer::Node& OctreeColorQuantizer::Node::operator=(Node&&) =
    default;

OctreeColorQuantizer::Node::~Node() = default;

RgbColor OctreeColorQuantizer::Node::GetColor() const {
  DCHECK_GT(ref_count_, 0u);

  return RgbColor(red_ / ref_count_, green_ / ref_count_, blue_ / ref_count_);
}

size_t OctreeColorQuantizer::Node::ChildrenRefCount() const {
  size_t count = 0;
  for (const auto& child : child_nodes_) {
    if (child) {
      count += child->ref_count_;
    }
  }
  return count;
}

// -----------------------------------------------------------------------------
// OctreeColorQuantizer:

void OctreeColorQuantizer::InsertColor(const RgbColor& color,
                                       NodesPerLevel& nodes_per_level) {
  InsertColorInternal(&root_, color, /*level=*/0, nodes_per_level);
}

void OctreeColorQuantizer::InsertColorInternal(Node* node,
                                               const RgbColor& color,
                                               int level,
                                               NodesPerLevel& nodes_per_level) {
  if (level >= kNumBitsPerColorChannel) {
    // This is a leaf node. Accumulate all the color components and increment
    // the color count.
    node->red_ += color.r;
    node->green_ += color.g;
    node->blue_ += color.b;

    // If this is the very first time this leaf node is referenced, add it to
    // the leaf nodes linked list.
    if (++(node->ref_count_) == 1u) {
      InsertLeafNode(node);
    }

    return;
  }

  const auto index = GetColorIndexAtLevel(color, level);
  DCHECK_LE(index, 7u);

  auto& child_node = node->child_nodes_[index];

  if (!child_node) {
    // This is the first time a node at this index is accessed. Create a node
    // and track it as one of the nodes at the current `level`.
    child_node = std::make_unique<Node>();
    nodes_per_level[level].push_back(child_node.get());
  }

  InsertColorInternal(child_node.get(), color, level + 1, nodes_per_level);
}

void OctreeColorQuantizer::Reduce(NodesPerLevel& nodes_per_level) {
  // The nodes at the last level (level 7 = `kNumBitsPerColorChannel - 1`) are
  // all leaf nodes (i.e. they have no children). There's not point in starting
  // the reduction there. So we start from the level above (level 6).
  for (int level = kNumBitsPerColorChannel - 2; level >= 0; --level) {
    auto& cur_level_nodes = nodes_per_level[level];

    // Sort the nodes in this level, such that the ones with the least
    // referenced child nodes come first, so that they can be reduced first.
    // This keeps the important colors (the ones that were referenced the most
    // by many pixels in the video frame) are less likely to be merged with
    // other nodes.
    std::sort(cur_level_nodes.begin(), cur_level_nodes.end(),
              [](const Node* const a, const Node* const b) {
                return a->ChildrenRefCount() < b->ChildrenRefCount();
              });

    for (Node* node : cur_level_nodes) {
      for (auto& child_node : node->child_nodes_) {
        if (!child_node) {
          continue;
        }

        // Whether this `node` was a leaf before we merge a leaf child node with
        // it.
        const bool node_was_leaf = node->is_leaf();

        if (child_node->is_leaf()) {
          node->red_ += child_node->red_;
          node->green_ += child_node->green_;
          node->blue_ += child_node->blue_;
          node->ref_count_ += child_node->ref_count_;

          // `child_node` was merged into `node`. We can now get rid of it.
          RemoveLeafNode(child_node.get());
          child_node.reset();

          // If this is the first time `node` becomes a leaf node, we need to
          // insert it into the linked list.
          if (!node_was_leaf) {
            InsertLeafNode(node);
          }
        }
      }

      // After reducing the children of the current `node`, let's see if we are
      // at or below tha maximum number of colors.
      if (leaf_nodes_count_ <= kMaxNumberOfColorsInPalette) {
        return;
      }
    }
  }
}

size_t OctreeColorQuantizer::FindColorIndex(const RgbColor& color) const {
  return FindColorIndexInternal(&root_, /*level=*/0, color);
}

size_t OctreeColorQuantizer::FindColorIndexInternal(
    const Node* node,
    int level,
    const RgbColor& color) const {
  if (node->is_leaf()) {
    return node->palette_index_;
  }

  // We found that the colors look better when we start searching backwards
  // first starting at `index`.
  const auto index = GetColorIndexAtLevel(color, level);
  for (int8_t i = index; i >= 0; --i) {
    if (const auto& child = node->child_nodes_[i]) {
      return FindColorIndexInternal(child.get(), level + 1, color);
    }
  }

  // Search forward starting at `index + 1`.
  for (uint8_t i = index + 1; i < kNumBitsPerColorChannel; ++i) {
    if (const auto& child = node->child_nodes_[i]) {
      return FindColorIndexInternal(child.get(), level + 1, color);
    }
  }

  NOTREACHED();
}

void OctreeColorQuantizer::InsertLeafNode(Node* node) {
  ++leaf_nodes_count_;

  // Always insert the node at the front. The order of leaf nodes doesn't
  // matter.
  if (leaf_nodes_head_) {
    leaf_nodes_head_->prev_ = node;
  }
  node->next_ = leaf_nodes_head_;
  node->prev_ = nullptr;
  leaf_nodes_head_ = node;
}

void OctreeColorQuantizer::RemoveLeafNode(Node* node) {
  --leaf_nodes_count_;

  if (node->prev_) {
    node->prev_->next_ = node->next_;
  }
  if (node->next_) {
    node->next_->prev_ = node->prev_;
  }

  if (node == leaf_nodes_head_) {
    leaf_nodes_head_ = node->next_;
  }

  node->prev_ = nullptr;
  node->next_ = nullptr;
}

}  // namespace recording