Hey-Edge / edge-impulse-sdk /dsp /image /processing.cpp
eoinedge's picture
Publish Edge Impulse model via ModelForge
25ade36 verified
Raw
History Blame Contribute Delete
18.8 kB
/*
* Copyright (c) 2024 EdgeImpulse Inc.
*
* Generated by Edge Impulse and licensed under the applicable Edge Impulse
* Terms of Service. Community and Professional Terms of Service
* (https://edgeimpulse.com/legal/terms-of-service) or Enterprise Terms of
* Service (https://edgeimpulse.com/legal/enterprise-terms-of-service),
* according to your product plan subscription (the “License”).
*
* This software, documentation and other associated files (collectively referred
* to as the “Software”) is a single SDK variation generated by the Edge Impulse
* platform and requires an active paid Edge Impulse subscription to use this
* Software for any purpose.
*
* You may NOT use this Software unless you have an active Edge Impulse subscription
* that meets the eligibility requirements for the applicable License, subject to
* your full and continued compliance with the terms and conditions of the License,
* including without limitation any usage restrictions under the applicable License.
*
* If you do not have an active Edge Impulse product plan subscription, or if use
* of this Software exceeds the usage limitations of your Edge Impulse product plan
* subscription, you are not permitted to use this Software and must immediately
* delete and erase all copies of this Software within your control or possession.
* Edge Impulse reserves all rights and remedies available to enforce its rights.
*
* Unless required by applicable law or agreed to in writing, the Software 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, disclaimers and limitations under the License.
*/
#include "edge-impulse-sdk/dsp/image/processing.hpp"
#include "edge-impulse-sdk/dsp/ei_utils.h"
#include "edge-impulse-sdk/dsp/returntypes.hpp"
#include "edge-impulse-sdk/porting/ei_classifier_porting.h"
#include "edge-impulse-sdk/porting/ei_logging.h"
#include "edge-impulse-sdk/classifier/ei_constants.h"
#include <string.h>
#include <stddef.h>
namespace ei {
namespace image {
namespace processing {
/**
* @brief Convert YUV to RGB
*
* @param rgb_out Output buffer (can be the same as yuv_in if big enough)
* @param yuv_in Input buffer
* @param in_size_B Size of input image in B
* @param opts Note, only BIG_ENDIAN_ORDER supported presently
*/
int yuv422_to_rgb888(
unsigned char *rgb_out,
unsigned const char *yuv_in,
unsigned int in_size_B,
YUV_OPTIONS opts)
{
// Clamp out of range values
#define EI_CLAMP(t) (((t) > 255) ? 255 : (((t) < 0) ? 0 : (t)))
// Color space conversion for RGB
#define EI_GET_R_FROM_YUV(y, u, v) ((298 * y + 409 * v + 128) >> 8)
#define EI_GET_G_FROM_YUV(y, u, v) ((298 * y - 100 * u - 208 * v + 128) >> 8)
#define EI_GET_B_FROM_YUV(y, u, v) ((298 * y + 516 * u + 128) >> 8)
unsigned int in_size_pixels = in_size_B / 4;
yuv_in += in_size_B - 1;
int rgb_end = TEST_BIT_MASK(opts, PAD_4B) ? 2 * in_size_B : (6 * in_size_B) / 4;
rgb_out += rgb_end - 1;
// Going backwards probably looks strange, but
// This allows us to do the algorithm in place!
// User needs to put the YUV image into a larger buffer than necessary
// But going backwards means we don't overwrite the YUV bytes
// until we don't need them anymore
for (unsigned int i = 0; i < in_size_pixels; ++i) {
int y2 = *yuv_in-- - 16;
int v = *yuv_in-- - 128;
int y0 = *yuv_in-- - 16;
int u0 = *yuv_in-- - 128;
if (TEST_BIT_MASK(opts, BIG_ENDIAN_ORDER)) {
*rgb_out-- = EI_CLAMP(EI_GET_B_FROM_YUV(y2, u0, v));
*rgb_out-- = EI_CLAMP(EI_GET_G_FROM_YUV(y2, u0, v));
*rgb_out-- = EI_CLAMP(EI_GET_R_FROM_YUV(y2, u0, v));
if (TEST_BIT_MASK(opts, PAD_4B)) {
*rgb_out-- = 0;
}
*rgb_out-- = EI_CLAMP(EI_GET_B_FROM_YUV(y0, u0, v));
*rgb_out-- = EI_CLAMP(EI_GET_G_FROM_YUV(y0, u0, v));
*rgb_out-- = EI_CLAMP(EI_GET_R_FROM_YUV(y0, u0, v));
if (TEST_BIT_MASK(opts, PAD_4B)) {
*rgb_out-- = 0;
}
}
else {
// not yet supported
return EIDSP_NOT_SUPPORTED;
}
}
return EIDSP_OK;
}
/**
* @brief Crops an image. Can be in-place. 4B alignment for best performance
* (Alignment is tested, will fall back to B by B movement)
*
* @param srcWidth X dimension in pixels
* @param srcHeight Y dimension in pixels
* @param srcImage Input buffer
* @param startX X coord of first pixel to keep
* @param startY Y coord of the first pixel to keep
* @param dstWidth Desired X dimension in pixels (should be smaller than srcWidth)
* @param dstHeight Desired Y dimension in pixels (should be smaller than srcHeight)
* @param dstImage Output buffer, can be the same as srcImage
* @param iBpp 8 or 16 for bits per pixel
*/
int cropImage(
const uint8_t *srcImage,
int srcWidth,
int srcHeight,
int startX,
int startY,
uint8_t *dstImage,
int dstWidth,
int dstHeight,
int iBpp)
{
uint32_t *s32, *d32;
int x, y;
if (startX < 0 || startX >= srcWidth || startY < 0 || startY >= srcHeight ||
(startX + dstWidth) > srcWidth || (startY + dstHeight) > srcHeight) {
return EIDSP_PARAMETER_INVALID; // invalid parameters
}
if (iBpp != 8 && iBpp != 16) {
return EIDSP_PARAMETER_INVALID;
}
if (iBpp == 8) {
const uint8_t *s;
uint8_t *d;
for (y = 0; y < dstHeight; y++) {
s = &srcImage[srcWidth * (y + startY) + startX];
d = &dstImage[(dstWidth * y)];
x = 0;
if ((intptr_t)s & 3 || (intptr_t)d & 3) { // either src or dst pointer is not aligned
for (; x < dstWidth; x++) {
*d++ = *s++; // have to do it byte-by-byte
}
}
else {
// move 4 bytes at a time if aligned or alignment not enforced
s32 = (uint32_t *)s;
d32 = (uint32_t *)d;
for (; x < dstWidth - 3; x += 4) {
*d32++ = *s32++;
}
// any remaining stragglers?
s = (uint8_t *)s32;
d = (uint8_t *)d32;
for (; x < dstWidth; x++) {
*d++ = *s++;
}
}
} // for y
} // 8-bpp
else {
uint16_t *s, *d;
for (y = 0; y < dstHeight; y++) {
s = (uint16_t *)&srcImage[2 * srcWidth * (y + startY) + startX * 2];
d = (uint16_t *)&dstImage[(dstWidth * y * 2)];
x = 0;
if ((intptr_t)s & 2 || (intptr_t)d & 2) { // either src or dst pointer is not aligned
for (; x < dstWidth; x++) {
*d++ = *s++; // have to do it 16-bits at a time
}
}
else {
// move 4 bytes at a time if aligned or alignment no enforced
s32 = (uint32_t *)s;
d32 = (uint32_t *)d;
for (; x < dstWidth - 1; x += 2) { // we can move 2 pixels at a time
*d32++ = *s32++;
}
// any remaining stragglers?
s = (uint16_t *)s32;
d = (uint16_t *)d32;
for (; x < dstWidth; x++) {
*d++ = *s++;
}
}
} // for y
} // 16-bpp case
return EIDSP_OK;
} /* cropImage() */
/**
* @copydoc cropImage(
int srcWidth,
int srcHeight,
const uint8_t *srcImage,
int startX,
int startY,
int dstWidth,
int dstHeight,
uint8_t *dstImage,
int iBpp)
*/
int crop_image_rgb888_packed(
const uint8_t *srcImage,
int srcWidth,
int srcHeight,
int startX,
int startY,
uint8_t *dstImage,
int dstWidth,
int dstHeight)
{
// use 8 bpp mode, but do everything *3 for RGB
return cropImage(
srcImage,
srcWidth * 3,
srcHeight,
startX * 3,
startY,
dstImage,
dstWidth * 3,
dstHeight,
8);
}
/**
* @brief Resize an image using interpolation
* Can be used to resize the image smaller or larger
* If resizing much smaller than 1/3 size, then a more rubust algorithm should average all of the pixels
* This algorithm uses bilinear interpolation - averages a 2x2 region to generate each new pixel
*
* @param srcWidth Input image width in pixels
* @param srcHeight Input image height in pixels
* @param srcImage Input buffer
* @param dstWidth Output image width in pixels
* @param dstHeight Output image height in pixels
* @param dstImage Output buffer, can be same as input buffer
* @param pixel_size_B Size of pixels in Bytes. 3 for RGB, 1 for mono
*/
int resize_image(
const uint8_t *srcImage,
int srcWidth,
int srcHeight,
uint8_t *dstImage,
int dstWidth,
int dstHeight,
int pixel_size_B)
{
// Copied from ei_camera.cpp in firmware-eta-compute
// Modified for RGB888
// This needs to be < 16 or it won't fit. Cortex-M4 only has SIMD for signed multiplies
constexpr int FRAC_BITS = 14;
constexpr int FRAC_VAL = (1 << FRAC_BITS);
constexpr int FRAC_MASK = (FRAC_VAL - 1);
uint32_t src_x_accum, src_y_accum; // accumulators and fractions for scaling the image
uint32_t x_frac, nx_frac, y_frac, ny_frac;
int x, y, ty;
if (srcHeight < 2) {
return EIDSP_PARAMETER_INVALID;
}
src_y_accum = 0;
const uint32_t src_x_frac = (srcWidth * FRAC_VAL) / dstWidth;
const uint32_t src_y_frac = (srcHeight * FRAC_VAL) / dstHeight;
//from here out, *3 b/c RGB
srcWidth *= pixel_size_B;
//srcHeight not used for indexing
//dstWidth still needed as is
//dstHeight shouldn't be scaled
const uint8_t *s;
uint8_t *d;
for (y = 0; y < dstHeight; y++) {
// do indexing computations
ty = src_y_accum >> FRAC_BITS; // src y
y_frac = src_y_accum & FRAC_MASK;
src_y_accum += src_y_frac;
ny_frac = FRAC_VAL - y_frac; // y fraction and 1.0 - y fraction
s = &srcImage[ty * srcWidth];
d = &dstImage[y * dstWidth * pixel_size_B]; //not scaled above
src_x_accum = 0;
for (x = 0; x < dstWidth; x++) {
uint32_t tx, p00, p01, p10, p11;
// do indexing computations
tx = (src_x_accum >> FRAC_BITS) * pixel_size_B;
x_frac = src_x_accum & FRAC_MASK;
nx_frac = FRAC_VAL - x_frac; // x fraction and 1.0 - x fraction
src_x_accum += src_x_frac;
//interpolate and write out
for (int color = 0; color < pixel_size_B;
color++) // do pixel_size_B times for pixel_size_B colors
{
p00 = s[tx];
p10 = s[tx + pixel_size_B];
p01 = s[tx + srcWidth];
p11 = s[tx + srcWidth + pixel_size_B];
p00 = ((p00 * nx_frac) + (p10 * x_frac) + FRAC_VAL / 2) >> FRAC_BITS; // top line
p01 = ((p01 * nx_frac) + (p11 * x_frac) + FRAC_VAL / 2) >> FRAC_BITS; // bottom line
p00 = ((p00 * ny_frac) + (p01 * y_frac) + FRAC_VAL / 2) >> FRAC_BITS; //top + bottom
*d++ = (uint8_t)p00; // store new pixel
//ready next loop
tx++;
}
} // for x
} // for y
return EIDSP_OK;
} // resizeImage()
/**
* @brief Calculate new dims that match the aspect ratio of destination
* This prevents a squashed look
* The smallest axis is held constant
*
* @param srcWidth Input width in pixels
* @param srcHeight Input height in pixels
* @param dstWidth Ultimate width in pixels
* @param dstHeight Ultimate height in pixels
* @param[out] cropWidth Width in pixels that matches the aspect ratio
* @param[out] cropHeight Height in pixels that matches the aspect ratio
*/
void calculate_crop_dims(
int srcWidth,
int srcHeight,
int dstWidth,
int dstHeight,
int &cropWidth,
int &cropHeight)
{
//first, trim the largest axis to match destination aspect ratio
//calculate by fixing the smaller axis
if (srcWidth > srcHeight) {
cropWidth = (uint32_t)(dstWidth * srcHeight) / dstHeight; //cast in case int is small
cropHeight = srcHeight;
}
else {
cropHeight = (uint32_t)(dstHeight * srcWidth) / dstWidth;
cropWidth = srcWidth;
}
}
int crop_and_interpolate_rgb888(
const uint8_t *srcImage,
int srcWidth,
int srcHeight,
uint8_t *dstImage,
int dstWidth,
int dstHeight)
{
int cropWidth, cropHeight;
// What are dimensions that maintain aspect ratio?
calculate_crop_dims(srcWidth, srcHeight, dstWidth, dstHeight, cropWidth, cropHeight);
// Now crop to that dimension
int res = crop_image_rgb888_packed(
srcImage,
srcWidth,
srcHeight,
(srcWidth - cropWidth) / 2,
(srcHeight - cropHeight) / 2,
dstImage,
cropWidth,
cropHeight);
if (res != EIDSP_OK) {
return res;
}
// Finally, interpolate down to desired dimensions, in place
return resize_image(dstImage, cropWidth, cropHeight, dstImage, dstWidth, dstHeight, 3);
}
int crop_and_interpolate_image(
const uint8_t *srcImage,
int srcWidth,
int srcHeight,
uint8_t *dstImage,
int dstWidth,
int dstHeight,
int pixel_size_B)
{
int cropWidth, cropHeight;
// What are dimensions that maintain aspect ratio?
calculate_crop_dims(srcWidth, srcHeight, dstWidth, dstHeight, cropWidth, cropHeight);
// Now crop to that dimension
int res = cropImage(
srcImage,
srcWidth * pixel_size_B,
srcHeight,
((srcWidth - cropWidth) / 2) * pixel_size_B,
(srcHeight - cropHeight) / 2,
dstImage,
cropWidth * pixel_size_B,
cropHeight,
8);
if (res != EIDSP_OK) {
return res;
}
// Finally, interpolate down to desired dimensions, in place
return resize_image(dstImage, cropWidth, cropHeight, dstImage, dstWidth, dstHeight, pixel_size_B);
}
int resize_image_using_mode(
const uint8_t *srcImage,
int srcWidth,
int srcHeight,
uint8_t *dstImage,
int dstWidth,
int dstHeight,
int pixel_size_B,
int mode)
{
if (srcWidth == dstWidth && srcHeight == dstHeight) {
EI_LOGI("Source and destination sizes are the same. No resizing needed.\n");
if (srcImage != dstImage) {
memcpy(dstImage, srcImage, srcWidth * srcHeight * pixel_size_B);
}
return -1; // Return an error code
}
if (srcImage == dstImage && mode == EI_CLASSIFIER_RESIZE_FIT_LONGEST) {
EI_LOGI("FIT LONGEST in place, make sure source is oversized to fit destination size\n");
}
if (mode == EI_CLASSIFIER_RESIZE_FIT_SHORTEST) {
int res = crop_and_interpolate_image(
srcImage,
srcWidth,
srcHeight,
dstImage,
dstWidth,
dstHeight,
pixel_size_B);
if (res != 0) {
EI_LOGE("Error in crop_and_interpolate_image: %d\n", res);
return res;
}
return 0;
}
if (mode == EI_CLASSIFIER_RESIZE_SQUASH) {
int res =
resize_image(srcImage, srcWidth, srcHeight, dstImage, dstWidth, dstHeight, pixel_size_B);
if (res != 0) {
EI_LOGE("Error in resize_image: %d\n", res);
return res;
}
return 0;
}
if (mode == EI_CLASSIFIER_RESIZE_FIT_LONGEST) {
// Calculate aspect ratios
float srcAspect = static_cast<float>(srcWidth) / srcHeight;
float dstAspect = static_cast<float>(dstWidth) / dstHeight;
// Calculate new dimensions
int resizeWidth, resizeHeight;
if (srcAspect > dstAspect) {
resizeWidth = dstWidth;
resizeHeight = static_cast<int>(dstWidth / srcAspect);
}
else {
resizeHeight = dstHeight;
resizeWidth = static_cast<int>(dstHeight * srcAspect);
}
// Calculate the start position for the resized image in the destination buffer
int startX = (dstWidth - resizeWidth) / 2;
int startY = (dstHeight - resizeHeight) / 2;
// First, resize in place. We can't resize into the middle as this may destroy source pixels needed later
int res = resize_image(
srcImage,
srcWidth,
srcHeight,
dstImage,
resizeWidth,
resizeHeight,
pixel_size_B);
if (res != 0) {
EI_LOGE("Error in resize_image: %d\n", res);
return res;
}
// Zero out top and bottom easily
if (resizeWidth == dstWidth) {
// Now move the image to the correct location
size_t dstStart = (startY * dstWidth + startX) * pixel_size_B;
memmove(
dstImage + dstStart,
dstImage,
resizeWidth * resizeHeight * pixel_size_B);
// Zero out the right points before and after the new image
memset(dstImage, 0, dstStart);
// Zero out the bottom part
// Can just use the start addr as size again b/c it's symmetrical
memset(
dstImage + dstStart + resizeWidth * resizeHeight * pixel_size_B,
0,
dstStart);
}
// Sides are more work
else {
size_t resizeIndex = (dstWidth * dstHeight - resizeWidth * resizeHeight) * pixel_size_B;
// Move the image to the end of the dstBuffer
memmove(dstImage + resizeIndex, dstImage, resizeWidth * resizeHeight * pixel_size_B);
// Now move one row at a time, zero padding sides along the way
for (int y = 0; y < resizeHeight; y++) {
memmove(
dstImage + ((startY + y) * dstWidth + startX) * pixel_size_B,
dstImage + resizeIndex,
resizeWidth * pixel_size_B);
resizeIndex += resizeWidth * pixel_size_B;
// Zero out the left and right sides
memset(dstImage + (startY + y) * dstWidth * pixel_size_B, 0, startX * pixel_size_B);
memset(dstImage + ((startY + y) * dstWidth + startX + resizeWidth) * pixel_size_B, 0, (dstWidth - startX - resizeWidth) * pixel_size_B);
}
}
return 0;
} // fit longest
// shouldn't get here
return -2;
}
} //namespaces
}
}