blob: e3a1087d89b2601865876bc271ce539955f8e28c [file] [log] [blame]
// Copyright 2019 Google LLC
//
// 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
//
// https://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.
// -----------------------------------------------------------------------------
//
// utilities around planes and filters
//
// Author: Skal (pascal.massimino@gmail.com)
#ifndef WP2_UTILS_PLANE_H_
#define WP2_UTILS_PLANE_H_
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include "src/dsp/math.h"
#include "src/utils/csp.h"
#include "src/utils/utils.h"
#include "src/utils/vector.h"
#include "src/wp2/base.h"
#include "src/wp2/format_constants.h"
namespace WP2 {
//------------------------------------------------------------------------------
typedef int8_t tap_t;
// Contains a pair of 'Taps' used for up- or downsampling (e.g. chroma planes).
struct SamplingTaps {
struct Taps {
// Taps is assumed to contain only coefficients in [-31, 31] range
// and be normalized to a sum of '16' (4 bits of fractional precision).
// 'tap_coeffs' are centered at 'r'. They must be persistent.
Taps(int r, const tap_t tap_coeffs[]) : radius(r), taps(tap_coeffs) {}
tap_t operator[](int j) const { return taps[j]; }
bool operator==(const Taps& rhs) const;
bool operator!=(const Taps& rhs) const { return !operator==(rhs); }
bool IsValid() const;
public:
const int radius; // Half-size. Access to taps[-w ... w] should be valid.
const tap_t* const taps;
};
// Default filters.
static const SamplingTaps kDownSharp;
static const SamplingTaps kDownAvg;
static const SamplingTaps kUpSmooth;
static const SamplingTaps kUpNearest;
bool operator==(const SamplingTaps& rhs) const;
bool operator!=(const SamplingTaps& rhs) const { return !operator==(rhs); }
bool IsValid() const { return t1.IsValid() && t2.IsValid(); }
// Can be either full/half-pixel pair, or horizontal/vertical.
const Taps t1, t2;
};
//------------------------------------------------------------------------------
// everything unrelated to the template goes here
class PlaneBase {
public:
bool IsEmpty() const { return w_ == 0 || h_ == 0; }
bool IsView() const { return !is_private_; }
Rectangle AsRect() const { return {0, 0, w_, h_}; }
uint32_t w_ = 0, h_ = 0;
size_t step_ = 0;
protected:
WP2Status CheckRect(const Rectangle& rect) const;
Vector_u8 memory_;
bool is_private_ = true;
};
//------------------------------------------------------------------------------
// the type-dependent template
template <typename T>
class Plane : public PlaneBase {
public:
// Resizes the plane to zero and clears (private) memory. If this is a view,
// it becomes an empty view, but the external memory is not affected, unless
// it was a view of itself.
void Clear() {
w_ = h_ = step_ = 0;
is_private_ = true;
memory_.clear();
data_ = nullptr;
}
// Returns a lower bound of the number of bytes available in this buffer/view.
size_t Size() const { return (step_ * SafeSub(h_, 1u) + w_) * sizeof(T); }
// Resizes the memory allocated without preserving content. 'step' is the
// number of elements to go from one line to the next, it is computed if 0.
WP2Status Resize(uint32_t width, uint32_t height, size_t step = 0) {
if (width == 0 || height == 0) {
Clear();
return WP2_STATUS_OK;
}
if (step == 0) step = width;
WP2_CHECK_OK(step >= width, WP2_STATUS_INVALID_PARAMETER);
w_ = width;
h_ = height;
step_ = step;
is_private_ = true;
WP2_CHECK_ALLOC_OK(memory_.resize(h_ * step_ * sizeof(T)));
data_ = reinterpret_cast<T*>(memory_.data());
return WP2_STATUS_OK;
}
// Copies data.
WP2Status Copy(const Plane& src, bool resize_if_needed) {
if (w_ != src.w_ || h_ != src.h_) {
WP2_CHECK_OK(resize_if_needed, WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_STATUS(Resize(src.w_, src.h_));
}
const T* src_row = src.data_;
T* dst_row = data_;
for (uint32_t y = 0; y < h_; ++y) {
std::memcpy(dst_row, src_row, w_ * sizeof(T));
src_row += src.Step();
dst_row += Step();
}
return WP2_STATUS_OK;
}
// Fills with uniform value. 'rect' will be clipped to the plane's dimension.
void Fill(const Rectangle& rect, T value) {
const Rectangle r = rect.ClipWith({0, 0, w_, h_});
if (r.width == 0 || r.height == 0) return;
for (uint32_t y = r.y; y < r.y + r.height; ++y) {
T* const row = &At(r.x, y);
std::fill(row, row + r.width, value);
}
}
void Fill(T value) { Fill({0, 0, w_, h_}, value); }
// Same as Fill() but only draws the outline.
void DrawRect(const Rectangle& rect, T value) {
const Rectangle r = rect.ClipWith({0, 0, w_, h_});
if (r.width == 0 || r.height == 0) return;
const uint32_t x0 = r.x, x1 = r.x + r.width - 1;
const uint32_t y0 = r.y, y1 = r.y + r.height - 1;
for (uint32_t x = x0; x <= x1; ++x) At(x, y0) = At(x, y1) = value;
for (uint32_t y = y0 + 1; y < y1; ++y) At(x0, y) = At(x1, y) = value;
}
// Stretches the values to fill the padded right and bottom borders.
WP2Status FillPad(uint32_t non_padded_width, uint32_t non_padded_height) {
WP2_CHECK_OK((non_padded_width >= 1 && non_padded_width <= w_ &&
non_padded_height >= 1 && non_padded_height <= h_),
WP2_STATUS_BAD_DIMENSION);
if (non_padded_width < w_) {
for (uint32_t y = 0; y < h_; ++y) {
T* const row_end = &At(non_padded_width, y);
std::fill(row_end, row_end + w_ - non_padded_width, row_end[-1]);
}
}
for (uint32_t y = non_padded_height; y < h_; ++y) {
std::memcpy(Row(y), Row(non_padded_height - 1), w_ * sizeof(T));
}
return WP2_STATUS_OK;
}
// Extracts a view from 'src' which must out-live the view. No memory is freed
// if it is a view of itself, only the new bounds are applied.
// Caution: the view may modify the pixels of 'src' even if it is const.
WP2Status SetView(const Plane& src, const Rectangle& rect) {
WP2_CHECK_STATUS(src.CheckRect(rect));
if (&memory_ != &src.memory_) {
memory_.clear();
is_private_ = false;
}
data_ = const_cast<T*>(&src.At(rect.x, rect.y));
w_ = rect.width;
h_ = rect.height;
step_ = src.step_;
return WP2_STATUS_OK;
}
WP2Status SetView(const Plane& src) {
return SetView(src, {0, 0, src.w_, src.h_});
}
// Sets view on external data.
WP2Status SetView(T src[], uint32_t width, uint32_t height, size_t step) {
WP2_CHECK_OK(width >= 1 && height >= 1, WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK(step >= width, WP2_STATUS_INVALID_PARAMETER);
memory_.clear();
is_private_ = false;
data_ = src;
w_ = width;
h_ = height;
step_ = step;
return WP2_STATUS_OK;
}
// Convert V src[] plane into plane's type T
template <typename V>
void From(const V* src, size_t step, T offset = 0) {
for (uint32_t y = 0; y < h_; ++y) {
T* const dst = Row(y);
for (uint32_t x = 0; x < w_; ++x) dst[x] = (T)src[x] + offset;
src += step;
}
}
// Convert plane's type T into dst[] plane of type V
template <typename V>
void To(V* dst, size_t step, T offset = 0) const {
for (uint32_t y = 0; y < h_; ++y) {
const T* const src = Row(y);
for (uint32_t x = 0; x < w_; ++x) dst[x] = (V)(src[x] + offset);
dst += step;
}
}
inline size_t Step() const { return step_; }
// Array access.
T* Row(uint32_t y) {
assert(y < h_);
return data_ + y * Step(); // TODO(skal): safe non-overflow mult
}
const T* Row(uint32_t y) const {
assert(y < h_);
return data_ + y * Step(); // TODO(skal): safe non-overflow mult
}
// Pixel access with assertion.
T& At(uint32_t x, uint32_t y) {
assert(x < w_);
return Row(y)[x];
}
const T& At(uint32_t x, uint32_t y) const {
assert(x < w_);
return Row(y)[x];
}
// Pixel access clamped to image boundaries.
const T& AtClamped(uint32_t x, uint32_t y) const {
assert(w_ >= 1 && h_ >= 1);
return At(std::min(x, w_ - 1), std::min(y, h_ - 1));
}
const T& AtClamped(int x, int y) const {
return AtClamped((uint32_t)std::max(x, 0), (uint32_t)std::max(y, 0));
}
T& AtClamped(int x, int y) {
return At(std::min(std::max(x, 0), (int)w_ - 1),
std::min(std::max(y, 0), (int)h_ - 1));
}
// No bound checking, to access pixels adjacent to a subview.
const T& AtUnclamped(int x, int y) const {
return data_[y * (int)Step() + x];
}
// Returns true if this plane has the same dimensions as 'other'.
bool IsSameSize(const Plane& other) const {
return w_ == other.w_ && h_ == other.h_;
}
// Chroma down-sampling. 'filter' is centered.
WP2Status DownsampleFrom(
const Plane& src, const SamplingTaps& filter = SamplingTaps::kDownSharp) {
WP2_CHECK_OK(src.w_ >= 1 && src.h_ >= 1, WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK(filter.IsValid(), WP2_STATUS_INVALID_PARAMETER);
WP2_CHECK_STATUS(Resize(DivCeil(src.w_, 2u), DivCeil(src.h_, 2u)));
for (uint32_t y = 0; y < h_; ++y) {
T* const dst = Row(y);
for (uint32_t x = 0; x < w_; ++x) {
dst[x] = src.Sample(filter, (int)x * 2, (int)y * 2);
}
}
return WP2_STATUS_OK;
}
WP2Status Downsample(const SamplingTaps& filter = SamplingTaps::kDownSharp) {
if (w_ == 1 && h_ == 1) return WP2_STATUS_OK;
Plane<T> p;
WP2_CHECK_STATUS(p.DownsampleFrom(*this, filter));
using std::swap;
swap(*this, p);
return WP2_STATUS_OK;
}
// Up-sampling. 'filter.t1' is full-pixel, 'filter.t2' is half-pixel.
WP2Status UpsampleFrom(const Plane& src, uint32_t to_width,
uint32_t to_height,
const SamplingTaps& filter = SamplingTaps::kUpSmooth) {
WP2_CHECK_OK(
src.w_ == DivCeil(to_width, 2u) && src.h_ == DivCeil(to_height, 2u),
WP2_STATUS_BAD_DIMENSION);
WP2_CHECK_OK(filter.IsValid(), WP2_STATUS_INVALID_PARAMETER);
const uint32_t half_width = to_width / 2, half_height = to_height / 2;
WP2_CHECK_STATUS(Resize(to_width, to_height));
const uint32_t step = Step();
const SamplingTaps ff = {filter.t1, filter.t1}; // full/full
const SamplingTaps& fh = filter;
const SamplingTaps hf = {filter.t2, filter.t1};
const SamplingTaps hh = {filter.t2, filter.t2}; // half/half
for (uint32_t j = 0; j < half_height; ++j) {
for (uint32_t i = 0; i < half_width; ++i) {
data_[(2 * i + 0) + (2 * j + 0) * step] = src.Sample(ff, i, j);
data_[(2 * i + 1) + (2 * j + 0) * step] = src.Sample(hf, i, j);
data_[(2 * i + 0) + (2 * j + 1) * step] = src.Sample(fh, i, j);
data_[(2 * i + 1) + (2 * j + 1) * step] = src.Sample(hh, i, j);
}
if (src.w_ == half_width + 1) {
const uint32_t i = half_width;
data_[(2 * i + 0) + (2 * j + 0) * step] = src.Sample(ff, i, j);
data_[(2 * i + 0) + (2 * j + 1) * step] = src.Sample(fh, i, j);
}
}
if (src.h_ == half_height + 1) {
const uint32_t j = half_height;
for (uint32_t i = 0; i < half_width; ++i) {
data_[(2 * i + 0) + (2 * j + 0) * step] = src.Sample(ff, i, j);
data_[(2 * i + 1) + (2 * j + 0) * step] = src.Sample(hf, i, j);
}
if (src.w_ == half_width + 1) {
const uint32_t i = half_width;
data_[(2 * i + 0) + (2 * j + 0) * step] = src.Sample(ff, i, j);
}
}
return WP2_STATUS_OK;
}
WP2Status Upsample(uint32_t to_width, uint32_t to_height,
const SamplingTaps& filter = SamplingTaps::kUpSmooth) {
if (to_width == 1 && to_height == 1) {
WP2_CHECK_OK(w_ == 1 && h_ == 1, WP2_STATUS_BAD_DIMENSION);
return WP2_STATUS_OK;
}
Plane<T> p;
WP2_CHECK_STATUS(p.UpsampleFrom(*this, to_width, to_height, filter));
using std::swap;
swap(*this, p);
return WP2_STATUS_OK;
}
// Mostly for debugging. 'dst' is not resized. 'bit_depth' describes the data
// in this Plane that will be converted to 8-bit unsigned to fit in 'dst'.
WP2Status ToGray(ArgbBuffer* dst, BitDepth bit_depth) const;
// Process 'src's samples with arbitrary function.
WP2Status Process(const ArgbBuffer& src,
int32_t (*convert)(const void* sample) = ToGrayF) {
WP2_CHECK_STATUS(Resize(src.width(), src.height()));
for (uint32_t y = 0; y < h_; ++y) {
T* const dst = Row(y);
const uint8_t* psrc = src.GetRow8(y);
for (uint32_t x = 0; x < w_; ++x, psrc += WP2FormatBpp(src.format())) {
dst[x] = (T)convert((const void*)psrc);
}
}
return WP2_STATUS_OK;
}
// Computes sum of squared error. Implemented for 8u/16s.
WP2Status GetSSE(const Plane<T>& src, uint64_t* score) const;
// Computes SSIM. Implemented for 16s.
WP2Status GetSSIM(const Plane<T>& src, BitDepth bit_depth,
double* score) const;
protected:
// Returns the sample at 'x, y' filtered horizontally by 'filter.t1' and
// vertically by 'filter.t2'.
T Sample(const SamplingTaps& filter, int32_t x, int32_t y) const {
const bool clipped =
(x < filter.t1.radius || x + filter.t1.radius >= (int32_t)w_);
int32_t sum = 0;
for (int32_t j = -filter.t2.radius; j <= filter.t2.radius; ++j) {
const T* const data = &data_[Clamp(y + j, 0, (int32_t)h_ - 1) * step_];
uint32_t sub_sum = 0;
if (!clipped) {
for (int32_t i = -filter.t1.radius; i <= filter.t1.radius; ++i) {
sub_sum += data[x + i] * filter.t1[i];
}
} else {
for (int32_t i = -filter.t1.radius; i <= filter.t1.radius; ++i) {
const int pos = Clamp(x + i, 0, (int32_t)w_ - 1);
sub_sum += data[pos] * filter.t1[i];
}
}
sum += sub_sum * filter.t2[j];
}
return RightShiftRound(sum, 8);
}
// simple Luma converter for Process()
static int32_t ToGrayF(const void* src) {
const uint8_t* const src8 = (const uint8_t*)src;
const int32_t r = src8[1], g = src8[2], b = src8[3];
return ((77 * r + 149 * g + 29 * b + 128) >> 8);
}
public: // minimal iterator
class iterator {
public:
iterator(const T* const ptr, uint32_t width, size_t step)
: x_(0), y_(0), width_(width), step_(step), ptr_((T*)ptr) {}
iterator operator++() {
if (++x_ == width_) {
ptr_ += step_, x_ = 0, ++y_;
}
return *this;
}
bool operator!=(const iterator& i) const {
return (&ptr_[x_] != &i.ptr_[i.x_]);
}
T& operator*() { return ptr_[x_]; }
size_t x_, y_;
private:
const size_t width_, step_;
T* ptr_;
};
iterator begin() const { return {data_, w_, step_}; }
iterator end() const { return {data_ + h_ * step_, 0, 0}; }
protected:
T* data_ = nullptr;
public:
friend void swap(Plane& a, Plane& b) {
using std::swap;
swap(a.w_, b.w_);
swap(a.h_, b.h_);
swap(a.step_, b.step_);
swap(a.memory_, b.memory_);
swap(a.is_private_, b.is_private_);
swap(a.data_, b.data_);
}
};
typedef Plane<int16_t> Plane16;
typedef Plane<uint8_t> Plane8u;
typedef Plane<float> Planef;
//------------------------------------------------------------------------------
// Points to the luma, chroma and alpha components of an image. Can own the data
// or be a view, so assumptions on the constness of the pixels should not be
// made even for const references.
class YUVPlane {
public:
// Returns true if each plane is empty (has no pixel).
bool IsEmpty() const;
// Returns true if this buffer points to a non-owned memory.
bool IsView() const;
// Returns the dimension of the image. Asserts that the size of each plane is
// consistent with the others.
uint32_t GetWidth() const;
uint32_t GetHeight() const;
Rectangle AsRect() const { return {0, 0, Y.w_, Y.h_}; }
// Resizes the planes to zero and clears (private) memory. If there are views,
// they become empty views, but the external memory is not affected.
void Clear();
// Returns either Y, U, V or A depending on the channel number.
const Plane16& GetChannel(Channel channel) const;
Plane16& GetChannel(Channel channel);
bool HasAlpha() const { return (A.w_ != 0); }
// Resizes each plane to be 'width x height' rounded up to a 'pad' multiple.
// If 'as_yuv420' is true, U/V planes will have half dimension.
WP2Status Resize(uint32_t width, uint32_t height, uint32_t pad = 1,
bool has_alpha = false, bool as_yuv420 = false);
// Copies 'src'.
WP2Status Copy(const YUVPlane& src, bool resize_if_needed);
// Fills each plane with its 'color' component.
void Fill(const Rectangle& rect, const Ayuv38b& color);
void Fill(const Ayuv38b& color);
// Stretch colors to fill the borders outside the 'non_padded_width' and
// 'non_padded_height'.
WP2Status FillPad(uint32_t non_padded_width, uint32_t non_padded_height);
// Sets view on a sub-region of 'src' which must out-live the view.
// Caution: the view may then modify the pixels of 'src'.
WP2Status SetView(const YUVPlane& src, const Rectangle& rect);
WP2Status SetView(const YUVPlane& src);
// 'resize_if_needed' this YUVPlane to fit 'rgb' and copies its content,
// applying the 'csp_transform'. Dimensions are rounded up to 'pad' multiple.
// If 'import_alpha' is false, alpha should be fully opaque.
WP2Status Import(const ArgbBuffer& rgb, bool import_alpha,
const CSPTransform& csp_transform, bool resize_if_needed,
size_t pad = 1);
// Same as above but uses 'rgb_to_ccsp_matrix' and 'rgb_to_ccsp_shift' to
// define the custom color space and precision of the output samples.
WP2Status Import(const ArgbBuffer& rgb, bool import_alpha,
const CSPMtx& rgb_to_ccsp, bool resize_if_needed,
size_t pad = 1);
// Same as above but uses 'ccsp_to_rgb_matrix' and 'ccsp_to_rgb_shift' to
// define the custom color space and precision of the input samples.
// See CSPTranform.
WP2Status Import(const YUVPlane& ccsp, const CSPMtx& ccsp_to_rgb,
const CSPTransform& csp_transform, bool resize_if_needed,
size_t pad = 1);
// Applies 'matrix' on all samples then 'shift' right. Ignores alpha.
template <typename TMtx, typename TInternal = int32_t>
WP2Status Apply(const TMtx matrix[9], uint32_t shift) {
for (uint32_t y = 0; y < GetHeight(); ++y) {
for (uint32_t x = 0; x < GetWidth(); ++x) {
Multiply<int16_t, TMtx, int16_t, TInternal>(
Y.At(x, y), U.At(x, y), V.At(x, y), matrix, shift, &Y.At(x, y),
&U.At(x, y), &V.At(x, y));
}
}
return WP2_STATUS_OK;
}
// Converts samples to Argb using the 'transform' and 'resize_if_needed'.
// The 'upsample_if_needed' will convert 420->444 if not null and if needed.
// 'dst' should be in WP2_Argb_32 or WP2_ARGB_32 format.
WP2Status Export(const CSPTransform& transform, bool resize_if_needed,
ArgbBuffer* dst,
const SamplingTaps* upsample_if_needed = nullptr) const;
// Same as above but uses 'ccsp_to_rgb_matrix' and 'ccsp_to_rgb_shift' to
// define the color space and precision of the input samples. See CSPTranform.
// 'dst' should be in WP2_Argb_32 format.
WP2Status Export(const CSPMtx& ccsp_to_rgb, bool resize_if_needed,
ArgbBuffer* dst,
const SamplingTaps* upsample_if_needed = nullptr) const;
// Chroma up- and downsampling (4:2:0 <-> 4:4:4).
// Some memory can be saved with 'use_views_for_ya' is 'src' is persistent.
WP2Status DownsampleFrom(const YUVPlane& src,
const SamplingTaps& f = SamplingTaps::kDownSharp,
bool use_views_for_ya = false);
WP2Status Downsample(const SamplingTaps& f = SamplingTaps::kDownSharp);
WP2Status UpsampleFrom(const YUVPlane& src,
const SamplingTaps& f = SamplingTaps::kUpSmooth,
bool use_views_for_ya = false);
WP2Status Upsample(const SamplingTaps& f = SamplingTaps::kUpSmooth);
bool IsDownsampled() const;
// Composites this buffer on the given background buffer. Buffer size must
// match. Assumes that the samples were converted from premultiplied RGB to
// YUV using the given transform.
WP2Status CompositeOver(const YUVPlane& background,
const CSPTransform& transform);
// Composites the given buffer on top of this buffer. Buffer size must match.
// Assumes that the samples were converted from premultiplied RGB to YUV
// using the given transform.
WP2Status CompositeUnder(const YUVPlane& foreground,
const CSPTransform& transform);
// Computes the distortion between two images. 'bit_depth' is for YUV because
// alpha is always unsigned 8 bits. Not all 'metric_type' are implemented.
// Results are in dB, stored in 'result' in the A/Y/U/V/All order. Returns
// WP2_STATUS_OK or an error.
WP2Status GetDistortion(const YUVPlane& ref, BitDepth bit_depth,
MetricType metric_type, float result[5]) const;
// Returns true if the U and V planes contain only zeros.
bool IsMonochrome() const;
// TODO(maryla): the A plane could be a Plane8.
Plane16 Y, U, V, A; // NOLINT
};
void swap(YUVPlane& a, YUVPlane& b);
//------------------------------------------------------------------------------
} // namespace WP2
#endif // WP2_UTILS_PLANE_H_