Files
filament/libs/image/src/ImageOps.cpp
2019-05-28 11:43:50 -07:00

426 lines
16 KiB
C++

/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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 <image/ImageOps.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <utils/Panic.h>
#include <algorithm>
#include <memory>
#include <ratio>
using namespace filament::math;
namespace image {
LinearImage horizontalStack(std::initializer_list<LinearImage> images) {
size_t count = images.end() - images.begin();
return horizontalStack(images.begin(), count);
}
LinearImage horizontalStack(const LinearImage* first, size_t count) {
ASSERT_PRECONDITION(count > 0, "Must supply one or more images for stacking.");
// Compute the final size and allocate memory.
uint32_t width = 0;
uint32_t height = 0;
uint32_t nchannels = 0;
for (size_t c = 0; c < count; ++c) {
const auto& img = first[c];
width += img.getWidth();
if (height == 0) {
height = img.getHeight();
} else {
ASSERT_PRECONDITION(height == img.getHeight(), "Inconsistent heights.");
}
if (nchannels == 0) {
nchannels = img.getChannels();
} else {
ASSERT_PRECONDITION(nchannels == img.getChannels(), "Inconsistent channels.");
}
}
LinearImage result(width, height, nchannels);
// Copy over each row of each source image.
float* dst = result.getPixelRef();
for (int32_t row = 0; row < height; ++row) {
for (size_t c = 0; c < count; ++c) {
const auto& img = first[c];
uint32_t swidth = img.getWidth();
float const* src = img.getPixelRef() + row * swidth * nchannels;
memcpy(dst, src, swidth * nchannels * sizeof(float));
dst += swidth * nchannels;
}
}
return result;
}
LinearImage verticalStack(std::initializer_list<LinearImage> images) {
size_t count = images.end() - images.begin();
return verticalStack(images.begin(), count);
}
// To stack images vertically, we transpose them individually, then horizontalStack them, then transpose the
// result. This is incredibly lazy, but since we use row-major ordering, copying columns would be
// really painful.
LinearImage verticalStack(const LinearImage* first, size_t count) {
ASSERT_PRECONDITION(count > 0, "Must supply one or more images for stacking.");
std::unique_ptr<LinearImage[]> flipped(new LinearImage[count]);
int i = 0;
for (size_t c = 0; c < count; ++c) {
flipped[i++] = transpose(first[c]);
}
auto result = horizontalStack(flipped.get(), count);
return transpose(result);
}
LinearImage horizontalFlip(const LinearImage& image) {
const uint32_t width = image.getWidth();
const uint32_t height = image.getHeight();
const uint32_t channels = image.getChannels();
LinearImage result(width, height, channels);
for (uint32_t row = 0; row < height; ++row) {
for (uint32_t col = 0; col < width; ++col) {
float* dst = result.getPixelRef(width - 1 - col, row);
float const* src = image.getPixelRef(col, row);
for (uint32_t c = 0; c < channels; ++c) {
dst[c] = src[c];
}
}
}
return result;
}
LinearImage verticalFlip(const LinearImage& image) {
const uint32_t width = image.getWidth();
const uint32_t height = image.getHeight();
const uint32_t channels = image.getChannels();
LinearImage result(width, height, channels);
for (uint32_t row = 0; row < height; ++row) {
float const* src = image.getPixelRef(0, row);
float* dst = result.getPixelRef(0, height - 1 - row);
memcpy(dst, src, width * channels * sizeof(float));
}
return result;
}
template<class VecT>
LinearImage applyScaleOffset(const LinearImage& image,
typename VecT::value_type scale, typename VecT::value_type offset) {
const uint32_t width = image.getWidth(), height = image.getHeight();
LinearImage result(width, height, image.getChannels());
auto src = (VecT const*) image.getPixelRef();
auto dst = (VecT*) result.getPixelRef();
for (uint32_t n = 0, end = width * height; n < end; ++n) {
dst[n] = scale * src[n] + VecT{offset};
}
return result;
}
LinearImage vectorsToColors(const LinearImage& image) {
ASSERT_PRECONDITION(image.getChannels() == 3 || image.getChannels() == 4,
"Must be a 3 or 4 channel image");
return image.getChannels() == 3
? applyScaleOffset<float3>(image, 0.5f, 0.5f)
: applyScaleOffset<float4>(image, 0.5f, 0.5f);
}
LinearImage colorsToVectors(const LinearImage& image) {
ASSERT_PRECONDITION(image.getChannels() == 3 || image.getChannels() == 4,
"Must be a 3 or 4 channel image");
return image.getChannels() == 3
? applyScaleOffset<float3>(image, 2.0f, -1.0f)
: applyScaleOffset<float4>(image, 2.0f, -1.0f);
}
LinearImage extractChannel(const LinearImage& source, uint32_t channel) {
const uint32_t width = source.getWidth(), height = source.getHeight();
const uint32_t nchan = source.getChannels();
ASSERT_PRECONDITION(channel < nchan, "Channel is out of range.");
LinearImage result(width, height, 1);
auto src = source.getPixelRef();
auto dst = result.getPixelRef();
for (uint32_t n = 0, npixels = width * height; n < npixels; ++n, ++dst, src += nchan) {
dst[0] = src[channel];
}
return result;
}
LinearImage combineChannels(std::initializer_list<LinearImage> images) {
size_t count = images.end() - images.begin();
return combineChannels(images.begin(), count);
}
LinearImage combineChannels(LinearImage const* img, size_t count) {
ASSERT_PRECONDITION(count > 0, "Must supply one or more image planes for combining.");
const uint32_t width = img[0].getWidth();
const uint32_t height = img[0].getHeight();
for (size_t c = 0; c < count; ++c) {
const LinearImage& plane = img[c];
ASSERT_PRECONDITION(plane.getWidth() == width, "Planes must all have same width.");
ASSERT_PRECONDITION(plane.getHeight() == height, "Planes must all have same height.");
ASSERT_PRECONDITION(plane.getChannels() == 1, "Planes must be single channel.");
}
LinearImage result(width, height, (uint32_t) count);
float* dst = result.getPixelRef();
uint32_t sindex = 0, dindex = 0;
while (dindex < width * height * count) {
for (size_t c = 0; c < count; ++c, ++dindex) {
const LinearImage& plane = img[c];
float const* src = plane.getPixelRef();
dst[dindex] = src[sindex];
}
++sindex;
}
return result;
}
// The transpose operation does not simply set a flag, it performs actual movement of data. This is
// very handy for separable filters because it (a) improves cache coherency in the second pass, and
// (b) allows the client to consume columns in the same way that it consumes rows. Our
// implementation does not support in-place transposition but it is simple and robust for non-square
// images.
LinearImage transpose(const LinearImage& image) {
const uint32_t width = image.getWidth();
const uint32_t height = image.getHeight();
const uint32_t channels = image.getChannels();
LinearImage result(height, width, channels);
float const* source = image.getPixelRef();
float* target = result.getPixelRef();
for (uint32_t n = 0; n < width * height; ++n) {
const uint32_t i = n / width;
const uint32_t j = n % width;
float const* src = source + channels * n;
float* dst = target + channels * (height * j + i);
for (uint32_t c = 0; c < channels; ++c) {
dst[c] = src[c];
}
}
return result;
}
LinearImage cropRegion(const LinearImage& image, uint32_t left, uint32_t top, uint32_t right,
uint32_t bottom) {
uint32_t width = right - left;
uint32_t height = bottom - top;
uint32_t channels = image.getChannels();
LinearImage result(width, height, channels);
float const* source = image.getPixelRef(left, top);
float* target = result.getPixelRef();
for (int32_t row = 0; row < height; ++row) {
memcpy(target, source, width * channels * sizeof(float));
target += width * channels;
source += image.getWidth() * channels;
}
return result;
}
int compare(const LinearImage& a, const LinearImage& b, float epsilon) {
auto w = a.getWidth();
auto h = a.getHeight();
auto c = a.getChannels();
if (b.getWidth() != w || b.getHeight() != h || b.getChannels() != c) {
return -1;
}
float const* adata = a.getPixelRef();
float const* bdata = b.getPixelRef();
return std::lexicographical_compare(adata, adata + w * h * c, bdata, bdata + w * h * c,
[epsilon](float x, float y) { return x < y - epsilon; });
}
void clearToValue(LinearImage& image, float value) {
const uint32_t nvals = image.getWidth() * image.getHeight() * image.getChannels();
float* data = image.getPixelRef();
for (uint32_t index = 0; index < nvals; ++index) {
data[index] = value;
}
}
// Please avoid using numeric_limits::infinity here, it has undesireable properties in the context
// of our EDT algorithm. We simply need a large number that represents the largest squared distance
// that ever needs to be considered.
static constexpr float INF = 4096.0f * 4096.f;
// void edt(f, d, z, v, i, n)
//
// Finds the lower envelope of a sequence of parabolas, as per Felzenszwalb and Huttenlocher.
// This operates on a single row of an image.
//
// f...source data (returns the Y of the parabola vertex at X)
// d...destination data (final distance values are written here)
// z...temporary used to store X coords of parabola intersections
// v...temporary used to store X coords of parabola vertices
// i...resulting X coords of parabola vertices
// n...number of pixels in "f" to process
//
static void edt(const float* f, float* d, float* z, float* v, float* i, size_t n) {
int k = 0;
v[0] = 0;
z[0] = -INF;
z[1] = +INF;
for (size_t iq = 1; iq < n; ++iq) {
const float fq = iq;
float fp, s;
int ip;
// If the new parabola is lower than the right-most parabola in
// the envelope, remove it from the envelope. To make this
// determination, find the X coordinate of the intersection (s)
// between the parabolas with vertices at (q,f[q]) and (p,f[p]).
fp = v[k];
ip = fp;
s = ((f[iq] + fq * fq) - (f[ip] + fp * fp)) / (2.0f * fq - 2.0f * fp);
while (s <= z[k]) {
k = k - 1;
fp = v[k];
ip = fp;
s = ((f[iq] + fq * fq) - (f[ip] + fp * fp)) / (2.0f * fq - 2.0f * fp);
}
// Add the new parabola to the envelope.
++k;
v[k] = fq;
z[k] = s;
z[k + 1] = +INF;
}
// Go back through the parabolas in the envelope and evaluate them
// in order to populate the distance values at each X coordinate.
k = 0;
for (size_t iq = 0; iq < n; ++iq) {
const float fq = iq;
while (z[k + 1] < fq) ++k;
const float dx = fq - v[k];
d[iq] = dx * dx + f[int(v[k])];
i[iq] = v[k];
}
}
static LinearImage computeHorizontalEdt(const LinearImage& src, LinearImage cx) {
const uint32_t width = src.getWidth();
const uint32_t height = src.getHeight();
LinearImage tmp0(width + 1, height + 1, 1);
LinearImage tmp1(width + 1, height + 1, 1);
LinearImage dst(width, height, 1);
// TODO: use utils::job::parallel_for.
for (uint32_t row = 0; row < height; ++row) {
const float* f = src.getPixelRef(0, row);
float* d = dst.getPixelRef(0, row);
float* z = tmp0.getPixelRef(0, row);
float* v = tmp1.getPixelRef(0, row);
float* i = cx.getPixelRef(0, row);
edt(f, d, z, v, i, width);
}
return dst;
}
// Implements the paper 'Distance Transforms of Sampled Functions' by Felzenszwalb and Huttenlocher
// but generalized to compute a coordinate field rather than a distance field. Coordinate fields are
// more broadly useful and transforming them into distance fields is extremely cheap.
LinearImage computeCoordField(const LinearImage& src, PresenceCallback presence, void* user) {
const uint32_t width = src.getWidth();
const uint32_t height = src.getHeight();
LinearImage f0(width, height, 1);
for (uint32_t row = 0; row < height; ++row) {
float* pf = f0.getPixelRef(0, row);
for (uint32_t col = 0; col < width; ++col) {
pf[col] = presence(src, col, row, user) ? 0.0f : INF;
}
}
LinearImage cx(width, height, 1);
LinearImage cy(height, width, 1);
f0 = computeHorizontalEdt(f0, cx);
f0 = transpose(f0);
f0 = computeHorizontalEdt(f0, cy);
f0 = transpose(f0);
// NOTE: this could be extended to compute a volumetric distance field by transposing
// X with Z at this point (rather than X with Y) and re-invoking computeHorizontalEdt.
LinearImage coords(width, height, 2);
for (uint32_t row = 0; row < height; ++row) {
for (uint32_t col = 0; col < width; ++col) {
float y = cy.getPixelRef(row, col)[0];
float x = cx.getPixelRef(col, y)[0];
float* dst = coords.getPixelRef(col, row);
dst[0] = x;
dst[1] = y;
}
}
return coords;
}
LinearImage edtFromCoordField(const LinearImage& coordField, bool sqrt) {
const uint32_t width = coordField.getWidth();
const uint32_t height = coordField.getHeight();
LinearImage result(width, height, 1);
for (int32_t row = 0; row < height; ++row) {
const float frow = row;
float* dst = result.getPixelRef(0, row);
for (uint32_t col = 0; col < width; ++col) {
const float fcol = col;
const float* coord = coordField.getPixelRef(col, row);
const float dx = coord[0] - fcol;
const float dy = coord[1] - frow;
float distance = dx * dx + dy * dy;
if (sqrt) {
distance = std::sqrt(distance);
}
dst[col] = distance;
}
}
return result;
}
// Dereferences the given coordinate field. Useful for creating Voronoi diagrams or dilated images.
LinearImage voronoiFromCoordField(const LinearImage& coordField, const LinearImage& src) {
const uint32_t width = src.getWidth();
const uint32_t height = src.getHeight();
const uint32_t channels = src.getChannels();
LinearImage result(width, height, channels);
for (int32_t row = 0; row < height; ++row) {
for (uint32_t col = 0; col < width; ++col) {
const float* coord = coordField.getPixelRef(col, row);
uint32_t srccol = coord[0];
uint32_t srcrow = coord[1];
float* presult = result.getPixelRef(col, row);
const float* psource = src.getPixelRef(srccol, srcrow);
for (uint32_t channel = 0; channel < channels; ++channel) {
presult[channel] = psource[channel];
}
}
}
return result;
}
void blitImage(LinearImage& target, const LinearImage& source) {
ASSERT_PRECONDITION(source.getWidth() == target.getWidth(), "Images must have same width.");
ASSERT_PRECONDITION(source.getHeight() == target.getHeight(), "Images must have same height.");
ASSERT_PRECONDITION(source.getChannels() == target.getChannels(),
"Images must have same number of channels.");
memcpy(target.getPixelRef(), source.getPixelRef(),
sizeof(float) * source.getWidth() * source.getHeight() * source.getChannels());
}
} // namespace image