imagecore/image/tiledresize.cpp (190 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/image/rgba.h" #include "imagecore/image/tiledresize.h" #include "imagecore/utils/mathutils.h" namespace imagecore { TiledResizeOperation::TiledResizeOperation(ImageReader* imageReader, ImageWriter* imageWriter, unsigned int outputWidth, unsigned int outputHeight) { m_ImageReader = imageReader; m_ImageWriter = imageWriter; m_OutputWidth = outputWidth; m_OutputHeight = outputHeight; m_ResizeQuality = kResizeQuality_High; } TiledResizeOperation::~TiledResizeOperation() { } FilterKernelAdaptive* createFilterKernel(EResizeQuality quality, unsigned int inSize, unsigned int outSize) { return new FilterKernelAdaptive(ImageRGBA::getDownsampleFilterKernelType(quality), ImageRGBA::getDownsampleFilterKernelSize(quality), inSize, outSize); } ImageRGBA* resizeTile(ImageRGBA* source, ImageRGBA* dest1, ImageRGBA* dest2, int inSampleOffset, int outSampleOffset, FilterKernelAdaptive *kernelX, FilterKernelAdaptive *kernelY, bool isExact) { unsigned int destWidth = dest1->getWidth(); unsigned int destHeight = dest1->getHeight(); ImageRGBA* inImage = source; unsigned int whichOutImage = 0; ImageRGBA* outImages[2] = { dest1, dest2 }; while( inImage->getWidth() / 2 >= destWidth && inImage->getHeight() / 2 >= destHeight ) { START_CLOCK(reduce); inImage->reduceHalf(outImages[whichOutImage]); inSampleOffset /= 2; END_CLOCK(reduce); inImage = outImages[whichOutImage]; whichOutImage ^= 1; } if (!isExact) { START_CLOCK(filter); outImages[whichOutImage]->setDimensions(destWidth, destHeight); kernelY->setSampleOffset(inSampleOffset, outSampleOffset); if( !inImage->downsampleFilter(outImages[whichOutImage], kernelX, kernelY) ) { return NULL; } END_CLOCK(filter); whichOutImage ^= 1; } return outImages[whichOutImage ^ 1]; } int TiledResizeOperation::performResize() { unsigned int targetWidth = (unsigned int)m_OutputWidth; unsigned int targetHeight = (unsigned int)m_OutputHeight; // Only downsampling, haven't tested or thought about upsampling. if( targetWidth > m_ImageReader->getWidth() || targetHeight > m_ImageReader->getHeight() ) { targetWidth = m_ImageReader->getWidth(); targetHeight = m_ImageReader->getHeight(); } // Some readers (JPEG) can get us close to our desired size for free. unsigned int readWidth; unsigned int readHeight; m_ImageReader->computeReadDimensions(targetWidth, targetHeight, readWidth, readHeight); // If we're still more than a power of two away, reduce by powers of two until we're there. unsigned int reducedWidth = readWidth; unsigned int reducedHeight = readHeight; while( reducedWidth / 2 >= targetWidth && reducedHeight / 2 >= targetHeight ) { reducedWidth /= 2; reducedHeight /= 2; } // Avoid unnecessary filtering if we're close. if( abs((int)reducedWidth - (int)targetWidth) < 4 && abs((int)reducedHeight - (int)targetHeight) < 4 ) { targetWidth = reducedWidth; targetHeight = reducedHeight; } bool skipFiltering = false; if( targetWidth == reducedWidth && targetHeight == reducedHeight ) { skipFiltering = true; } // Compute tile sizes. unsigned int outMaxRows = 128; unsigned int inMaxRows = ((readHeight * outMaxRows) / targetHeight); while( inMaxRows * readWidth > 1024 * 1024 && outMaxRows >= 16 ) { // The input is huge, so try a smaller output size. outMaxRows /= 2; inMaxRows = ((readHeight * outMaxRows) / targetHeight); } unsigned int tileOverlap = 12; unsigned int imagePadding = 12; bool skipScale = targetWidth == readWidth && targetHeight == readHeight; if( skipScale || skipFiltering || readHeight <= inMaxRows ) { // If we're not scaling/filtering, or the entire image fits into a single tile, we don't need to // have any redundant overlap between tiles, which is usually there for the edges of the tile // to filter properly. tileOverlap = 0; } EImageColorModel colorModel = m_ImageReader->getNativeColorModel() == kColorModel_RGBA ? kColorModel_RGBA : kColorModel_RGBX; bool success = false; // If the image is just too big (> ~8192), don't even try. // It might work, but it's untested. if( outMaxRows >= 16 ) { FilterKernelAdaptive* filterKernelX = createFilterKernel(m_ResizeQuality, reducedWidth, targetWidth); FilterKernelAdaptive* filterKernelY = createFilterKernel(m_ResizeQuality, reducedHeight, targetHeight); if( filterKernelX != NULL && filterKernelY != NULL ) { if( m_ImageReader->beginRead(readWidth, readHeight, colorModel) ) { if( m_ImageWriter->beginWrite(targetWidth, targetHeight, colorModel) ) { unsigned int maxSourceHeight = inMaxRows + tileOverlap * 4; ImageRGBA* sourceImage = ImageRGBA::create(readWidth, maxSourceHeight, imagePadding, 16); ImageRGBA* destImage1 = skipScale ? NULL : ImageRGBA::create(readWidth, outMaxRows + tileOverlap * 2, imagePadding, 16); ImageRGBA* destImage2 = skipScale ? NULL : ImageRGBA::create(readWidth, outMaxRows + tileOverlap * 2, imagePadding, 16); unsigned int prevTileUnprocessed = 0; unsigned int prevTileOverlap = 0; if( sourceImage != NULL && ((destImage1 != NULL && destImage2 != NULL) || skipScale) ) { unsigned int currentInRow = 0; unsigned int currentOutRow = 0; unsigned int rowsInRemaining = readHeight; unsigned int rowsOutRemaining = targetHeight; bool failed = false; while( rowsOutRemaining > 0 ) { unsigned int desiredInRows = inMaxRows; if( currentOutRow == 0 ) { // For the first tile we need to load a bit of the next one, for proper filtering. desiredInRows += tileOverlap; } unsigned int effectiveInRows = min(rowsInRemaining + prevTileUnprocessed, inMaxRows); unsigned int effectiveOutRows = min(rowsOutRemaining, outMaxRows); unsigned int rowsToRead = min(rowsInRemaining, desiredInRows); if( rowsOutRemaining == effectiveOutRows ) { rowsToRead = rowsInRemaining; } if( rowsToRead > 0 ) { sourceImage->setDimensions(readWidth, rowsToRead); // Start loading the next tile into the image below the part of the previous tile we kept around. sourceImage->setOffset(0, prevTileUnprocessed + prevTileOverlap); START_CLOCK(read); unsigned int rowsRead = m_ImageReader->readRows(sourceImage, 0, rowsToRead); END_CLOCK(read); if( rowsRead != rowsToRead ) { failed = true; // Abort, stop processing tiles. break; } sourceImage->setOffset(0, 0); } unsigned int rowsProcessed = effectiveInRows; unsigned int rowsAvailable = prevTileUnprocessed + rowsToRead; unsigned int rowsLeftOver = rowsAvailable - rowsProcessed; int outPreOverlap = ((targetHeight * prevTileOverlap) / readHeight); int outPostOverlap = ((targetHeight * rowsLeftOver) / readHeight); sourceImage->setDimensions(readWidth, rowsProcessed + prevTileOverlap + rowsLeftOver); // Setting these sample offsets allows us to filter the tile exactly the same way it would be // if the entire image was being filtered. The filter kernel was constructed for the entire image. int inSampleOffset = currentInRow - prevTileOverlap; int outSampleOffset = currentOutRow - outPreOverlap; filterKernelY->setSampleOffset(inSampleOffset, outSampleOffset); ImageRGBA* image = NULL; if( skipScale ) { image = sourceImage; } else { // Perform the resize of the tile. destImage1->setDimensions(targetWidth, effectiveOutRows + outPreOverlap + outPostOverlap); destImage2->setDimensions(targetWidth, effectiveOutRows + outPreOverlap + outPostOverlap); image = resizeTile(sourceImage, destImage1, destImage2, inSampleOffset, outSampleOffset, filterKernelX, filterKernelY, skipFiltering); // For writing we skip past the top few rows, which are from the previous tile. image->setOffset(0, outPreOverlap); image->setDimensions(targetWidth, effectiveOutRows); } START_CLOCK(write); unsigned int rowsWritten = m_ImageWriter->writeRows(image, 0, image->getHeight()); END_CLOCK(write); if( rowsWritten != image->getHeight() ) { failed = true; // Abort, stop processing tiles. break; } image->setOffset(0, 0); if( rowsLeftOver > 0 ) { // Copy the bottom of this tile to the top of the image, so it serves as the filter edge padding for the next resize. sourceImage->setDimensions(readWidth, maxSourceHeight); sourceImage->copyRect(sourceImage, 0, prevTileOverlap + rowsProcessed - tileOverlap, 0, 0, readWidth, rowsLeftOver + tileOverlap); sourceImage->setDimensions(readWidth, rowsLeftOver + tileOverlap); prevTileUnprocessed = rowsLeftOver; prevTileOverlap = tileOverlap; } else { prevTileUnprocessed = 0; prevTileOverlap = 0; } currentInRow += rowsProcessed; currentOutRow += effectiveOutRows; rowsOutRemaining -= effectiveOutRows; rowsInRemaining -= rowsToRead; } if( !failed ) { START_CLOCK(finish); if( m_ImageReader->endRead() ) { if( m_ImageWriter->endWrite() ) { END_CLOCK(finish); success = true; } } } } delete destImage1; delete destImage2; delete sourceImage; } } } delete filterKernelX; delete filterKernelY; } return success ? IMAGECORE_SUCCESS : IMAGECORE_UNKNOWN_ERROR; } }