/* * 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 #include #include #include #include #include #include using namespace filament::math; namespace image { LinearImage horizontalStack(std::initializer_list 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 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 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 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(image, 0.5f, 0.5f) : applyScaleOffset(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(image, 2.0f, -1.0f) : applyScaleOffset(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 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