imagecore/image/resizecrop.cpp (261 lines of code) (raw):
/*
* MIT License
*
* Copyright (c) 2017 Twitter
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "imagecore/imagecore.h"
#include "imagecore/image/colorspace.h"
#include "imagecore/image/rgba.h"
#include "imagecore/utils/mathutils.h"
#include "imagecore/utils/securemath.h"
#include "imagecore/image/resizecrop.h"
#include "imagecore/image/yuv.h"
#include "imagecore/image/internal/filters.h"
namespace imagecore {
ResizeCropOperation::ResizeCropOperation()
{
m_FilteredImage[0] = NULL;
m_FilteredImage[1] = NULL;
m_CropRegion = NULL;
m_OutputWidth = 0;
m_OutputHeight = 0;
m_OutputMod = 1;
m_TargetWidth = 0;
m_TargetHeight = 0;
m_ResizeMode = kResizeMode_ExactCrop;
m_CropGravity = kGravityHeuristic;
m_ResizeQuality = kResizeQuality_High;
m_OutputColorModel = kColorModel_RGBX;
m_AllowUpsample = true;
m_AllowDownsample = true;
m_BackgroundFillColor = RGBA(255, 255, 255, 0);
}
ResizeCropOperation::~ResizeCropOperation()
{
for( unsigned int i = 0; i < 2; i++ ) {
if( m_FilteredImage[i] != NULL ) {
delete m_FilteredImage[i];
m_FilteredImage[i] = NULL;
}
}
if( m_CropRegion ) {
delete m_CropRegion;
m_CropRegion = NULL;
}
}
int ResizeCropOperation::performResizeCrop(Image*& resizedImage)
{
resizedImage = NULL;
if( m_ImageReader == NULL || m_OutputWidth == 0 || m_OutputHeight == 0 ) {
return IMAGECORE_INVALID_IMAGE_SIZE;
}
int ret = IMAGECORE_SUCCESS;
if( (ret = readHeader()) != IMAGECORE_SUCCESS ) {
return ret;
}
if( (ret = load()) != IMAGECORE_SUCCESS ) {
return ret;
}
if( (ret = fillBackground()) != IMAGECORE_SUCCESS ) {
return ret;
}
if( (ret = resize()) != IMAGECORE_SUCCESS ) {
return ret;
}
if( (ret = rotateCrop()) != IMAGECORE_SUCCESS ) {
return ret;
}
resizedImage = m_FilteredImage[m_WhichImage];
return IMAGECORE_SUCCESS;
}
int ResizeCropOperation::performResizeCrop(ImageRGBA*& resizedImage)
{
Image* image = NULL;
if( performResizeCrop(image) == IMAGECORE_SUCCESS && image != NULL ) {
resizedImage = image->asRGBA();
if (resizedImage != NULL) {
return IMAGECORE_SUCCESS;
}
}
return IMAGECORE_UNKNOWN_ERROR;
}
static float calcScale(unsigned int orientedWidth, unsigned int orientedHeight, unsigned int desiredWidth, unsigned int desiredHeight, bool fit, bool allowUpsample, bool allowDownsample)
{
float widthScale = (float)desiredWidth / (float)orientedWidth;
float heightScale = (float)desiredHeight / (float)orientedHeight;
float scale = fit ? fminf(widthScale, heightScale) : fmaxf(widthScale, heightScale);
if( !allowUpsample ) {
scale = fminf(scale, 1.0f);
}
if( !allowDownsample ) {
scale = fmaxf(scale, 1.0f);
}
return scale;
}
static void calcOutputSize(unsigned int orientedWidth, unsigned int orientedHeight, unsigned int desiredWidth, unsigned int desiredHeight, unsigned int& targetWidth, unsigned int& targetHeight, unsigned int& outputWidth, unsigned int& outputHeight, EResizeMode resizeMode, bool allowUpsample, bool allowDownsample, ImageRegion* cropRegion, unsigned int outputMod)
{
// If we have a crop region, we're attempting to scale the specified region to the output size.
unsigned sourceWidth = cropRegion != NULL ? cropRegion->width() : orientedWidth;
unsigned sourceHeight = cropRegion != NULL ? cropRegion->height() : orientedHeight;
float scale = calcScale(sourceWidth, sourceHeight, desiredWidth, desiredHeight, resizeMode == kResizeMode_AspectFit, allowUpsample, allowDownsample);
if( cropRegion != NULL ) {
// Adjust the crop region, since the actual crop occurs after scaling.
cropRegion->left(scale * (float)cropRegion->left());
cropRegion->top(scale * (float)cropRegion->top());
cropRegion->width(scale * (float)cropRegion->width());
cropRegion->height(scale * (float)cropRegion->height());
}
targetWidth = roundf(scale * orientedWidth);
targetHeight = roundf(scale * orientedHeight);
if( resizeMode == kResizeMode_ExactCrop ) {
// The final cropped output should match the desired aspect ratio.
float croppedScale = calcScale(desiredWidth, desiredHeight, targetWidth, targetHeight, true, false, true);
outputWidth = roundf(croppedScale * desiredWidth);
outputHeight = roundf(croppedScale * desiredHeight);
} else if( resizeMode == kResizeMode_Stretch ) {
outputWidth = desiredWidth;
outputHeight = desiredHeight;
targetWidth = desiredWidth;
targetHeight = desiredHeight;
} else {
outputWidth = targetWidth;
outputHeight = targetHeight;
}
if( outputMod != 1 ) {
outputWidth -= outputWidth % outputMod;
outputHeight -= outputHeight % outputMod;
}
}
void ResizeCropOperation::estimateOutputSize(unsigned int imageWidth, unsigned int imageHeight, unsigned int& outputWidth, unsigned int& outputHeight)
{
calcOutputSize(imageWidth, imageHeight, m_OutputWidth, m_OutputHeight, m_TargetWidth, m_TargetHeight, outputWidth, outputHeight, m_ResizeMode, m_AllowUpsample, m_AllowDownsample, m_CropRegion, m_OutputMod);
}
int ResizeCropOperation::readHeader()
{
// Read the image header.
m_InputWidth = m_ImageReader->getWidth();
m_InputHeight = m_ImageReader->getHeight();
m_TargetWidth = 0;
m_TargetHeight = 0;
if( !Image::validateSize(m_InputWidth, m_InputHeight) ) {
fprintf(stderr, "error: bad image dimensions\n");
return IMAGECORE_INVALID_IMAGE_SIZE;
}
m_Orientation = m_ImageReader->getOrientation();
unsigned int orientedWidth = m_ImageReader->getOrientedWidth();
unsigned int orientedHeight = m_ImageReader->getOrientedHeight();
calcOutputSize(orientedWidth, orientedHeight, m_OutputWidth, m_OutputHeight, m_TargetWidth, m_TargetHeight, m_OutputWidth, m_OutputHeight, m_ResizeMode, m_AllowUpsample, m_AllowDownsample, m_CropRegion, m_OutputMod);
if( m_Orientation == kImageOrientation_Left || m_Orientation == kImageOrientation_Right ) {
// Flip it back, we work on the image in the file orientation.
swap(m_TargetWidth, m_TargetHeight);
}
// Allow conversion between RGBA <-> RGBX, otherwise respect the output color model specified.
if( Image::colorModelIsRGBA(m_OutputColorModel) && Image::colorModelIsRGBA(m_ImageReader->getNativeColorModel()) ) {
m_OutputColorModel = m_ImageReader->getNativeColorModel();
}
return IMAGECORE_SUCCESS;
}
int ResizeCropOperation::load()
{
unsigned int reducedWidth;
unsigned int reducedHeight;
m_ImageReader->computeReadDimensions(m_TargetWidth, m_TargetHeight, reducedWidth, reducedHeight);
// Prepare the work buffers.
unsigned int padAmount = max(ImageRGBA::getDownsampleFilterKernelSize(m_ResizeQuality), ImageRGBA::getUpsampleFilterKernelSize(m_ResizeQuality));
unsigned int bufferWidth = max(reducedWidth, m_TargetWidth);
unsigned int bufferHeight = max(reducedHeight, m_TargetHeight);
unsigned int alignment = 16;
unsigned int padSize = max(padAmount, Image::colorModelIsYUV(m_OutputColorModel) ? 16U : 4U);
// Add an extra (alignment) rows to height, because we might rotate the image later, and need to have enough room on that axis for the row alignment.
unsigned int extraRows = m_ImageReader->getOrientedHeight() != m_ImageReader->getHeight() ? alignment * 2U : 0;
m_FilteredImage[0] = Image::create(m_OutputColorModel, bufferWidth, bufferHeight + extraRows, padSize, alignment);
if( m_FilteredImage[0] == NULL ) {
return IMAGECORE_OUT_OF_MEMORY;
}
// Since the second work buffer will be used to hold the resampled output, it doesn't need to be as large as the first.
m_FilteredImage[1] = Image::create(m_OutputColorModel, max(m_TargetWidth, (bufferWidth + 1) / 2), max(m_TargetHeight, (bufferHeight + 1) / 2) + extraRows, padSize, alignment);
if( m_FilteredImage[1] == NULL ) {
return IMAGECORE_OUT_OF_MEMORY;
}
m_WhichImage = 0;
START_CLOCK(decompress);
m_FilteredImage[m_WhichImage]->setDimensions(reducedWidth, reducedHeight);
bool result = m_ImageReader->readImage(m_FilteredImage[m_WhichImage]);
if (!result) {
fprintf(stderr, "error: unable to read source image\n");
}
END_CLOCK(decompress);
return (result ? IMAGECORE_SUCCESS : IMAGECORE_READ_ERROR);
}
int ResizeCropOperation::fillBackground()
{
if( m_FilteredImage[m_WhichImage]->getColorModel() == kColorModel_RGBA && m_BackgroundFillColor.a == 255 ) {
ImageRGBA* image = (ImageRGBA*)m_FilteredImage[m_WhichImage];
const float3 fillColor = ColorSpace::byteToFloat(RGBA(m_BackgroundFillColor.r,
m_BackgroundFillColor.g,
m_BackgroundFillColor.b));
const float3 linearFillColor = ColorSpace::srgbToLinear(fillColor);
unsigned int framePitch;
unsigned int width = image->getWidth();
unsigned int height = image->getHeight();
uint8_t* buffer = image->lockRect(width, height, framePitch);
for( unsigned int y = 0; y < height; y++ ) {
for( unsigned int x = 0; x < width; x++ ) {
RGBA* rgba = (RGBA*)(&buffer[y * framePitch + x * 4]);
if( rgba->a == 0 ) {
*rgba = ColorSpace::floatToByte(fillColor);
} else if( rgba->a < 255 ) {
float a = rgba->a / 255.0f;
const float3 currentColor = ColorSpace::srgbToLinear(ColorSpace::byteToFloat(*rgba));
*rgba = ColorSpace::floatToByte(ColorSpace::linearToSrgb(float3(a) * currentColor + float3(1-a) * linearFillColor));
}
}
}
image->unlockRect();
}
return IMAGECORE_SUCCESS;
}
int ResizeCropOperation::resize()
{
Image* inImage = m_FilteredImage[m_WhichImage];
Image* outImage = m_FilteredImage[m_WhichImage ^ 1];
// Do a fast iterative 2x2 reduce, equivalent in quality to the 'free' DCT downsampling done by the JPEG decoder,
// until the image is in the right range for the filter step. This done for input formats other than JPEG.
while( inImage->getWidth() / 2 >= m_TargetWidth && inImage->getHeight() / 2 >= m_TargetHeight ) {
START_CLOCK(reduce);
inImage->reduceHalf(outImage);
END_CLOCK(reduce);
m_WhichImage ^= 1;
inImage = m_FilteredImage[m_WhichImage];
outImage = m_FilteredImage[m_WhichImage ^ 1];
}
// If the reduce didn't happen to get us to the right size, do a final high-quality filter step.
if( inImage->getWidth() != m_TargetWidth || inImage->getHeight() != m_TargetHeight ) {
START_CLOCK(filter);
outImage->setDimensions(m_TargetWidth, m_TargetHeight);
if( !inImage->resize(outImage, m_ResizeQuality) ) {
return IMAGECORE_OUT_OF_MEMORY;
}
END_CLOCK(filter);
m_WhichImage ^= 1;
}
return IMAGECORE_SUCCESS;
}
int ResizeCropOperation::rotateCrop()
{
// Apply the EXIF rotation.
if( m_Orientation == kImageOrientation_Down || m_Orientation == kImageOrientation_Left || m_Orientation == kImageOrientation_Right ) {
START_CLOCK(orient);
switch( m_Orientation ) {
case kImageOrientation_Down:
m_FilteredImage[m_WhichImage]->rotate(m_FilteredImage[m_WhichImage ^ 1], kImageOrientation_Up);
break;
case kImageOrientation_Left:
m_FilteredImage[m_WhichImage]->rotate(m_FilteredImage[m_WhichImage ^ 1], kImageOrientation_Right);
break;
case kImageOrientation_Right:
m_FilteredImage[m_WhichImage]->rotate(m_FilteredImage[m_WhichImage ^ 1], kImageOrientation_Left);
break;
}
m_WhichImage ^= 1;
END_CLOCK(orient);
}
if( m_CropRegion != NULL ) {
m_FilteredImage[m_WhichImage]->crop(*m_CropRegion);
}
if( m_ResizeMode == kResizeMode_ExactCrop ) {
ImageRegion* bound = ImageRegion::fromGravity(
m_FilteredImage[m_WhichImage]->getWidth(),
m_FilteredImage[m_WhichImage]->getHeight(),
m_OutputWidth,
m_OutputHeight,
m_CropGravity);
ASSERT(bound != NULL);
m_FilteredImage[m_WhichImage]->crop(*bound);
delete bound;
}
return IMAGECORE_SUCCESS;
}
}