/** * @file llimagefilter.cpp * @brief Simple Image Filtering. See https://wiki.lindenlab.com/wiki/SL_Viewer_Image_Filters for complete documentation. * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2014, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "linden_common.h" #include "llimagefilter.h" #include "llmath.h" #include "v3color.h" #include "v4coloru.h" #include "m3math.h" #include "v3math.h" #include "llsdserialize.h" #include "llstring.h" //--------------------------------------------------------------------------- // LLImageFilter //--------------------------------------------------------------------------- LLImageFilter::LLImageFilter(const std::string& file_path) : mFilterData(LLSD::emptyArray()), mImage(NULL), mHistoRed(NULL), mHistoGreen(NULL), mHistoBlue(NULL), mHistoBrightness(NULL), mStencilBlendMode(STENCIL_BLEND_MODE_BLEND), mStencilShape(STENCIL_SHAPE_UNIFORM), mStencilGamma(1.0), mStencilMin(0.0), mStencilMax(1.0) { // Load filter description from file llifstream filter_xml(file_path.c_str()); if (filter_xml.is_open()) { // Load and parse the file LLPointer parser = new LLSDXMLParser(); parser->parse(filter_xml, mFilterData, LLSDSerialize::SIZE_UNLIMITED); filter_xml.close(); } } LLImageFilter::~LLImageFilter() { mImage = NULL; ll_aligned_free_16(mHistoRed); ll_aligned_free_16(mHistoGreen); ll_aligned_free_16(mHistoBlue); ll_aligned_free_16(mHistoBrightness); } /* *TODO * Rename stencil to mask * Improve perf: use LUT for alpha blending in uniform case * Add gradient coloring as a filter */ //============================================================================ // Apply the filter data to the image passed as parameter //============================================================================ void LLImageFilter::executeFilter(LLPointer raw_image) { mImage = raw_image; LLImageDataLock lock(mImage); //std::cout << "Filter : size = " << mFilterData.size() << std::endl; for (S32 i = 0; i < mFilterData.size(); ++i) { std::string filter_name = mFilterData[i][0].asString(); // Dump out the filter values (for debug) //std::cout << "Filter : name = " << mFilterData[i][0].asString() << ", params = "; //for (S32 j = 1; j < mFilterData[i].size(); ++j) //{ // std::cout << mFilterData[i][j].asString() << ", "; //} //std::cout << std::endl; if (filter_name == "stencil") { // Get the shape of the stencil, that is how the procedural alpha is computed geometrically std::string filter_shape = mFilterData[i][1].asString(); EStencilShape shape = STENCIL_SHAPE_UNIFORM; if (filter_shape == "uniform") { shape = STENCIL_SHAPE_UNIFORM; } else if (filter_shape == "gradient") { shape = STENCIL_SHAPE_GRADIENT; } else if (filter_shape == "vignette") { shape = STENCIL_SHAPE_VIGNETTE; } else if (filter_shape == "scanlines") { shape = STENCIL_SHAPE_SCAN_LINES; } // Get the blend mode of the stencil, that is how the effect is blended in the background through the stencil std::string filter_mode = mFilterData[i][2].asString(); EStencilBlendMode mode = STENCIL_BLEND_MODE_BLEND; if (filter_mode == "blend") { mode = STENCIL_BLEND_MODE_BLEND; } else if (filter_mode == "add") { mode = STENCIL_BLEND_MODE_ADD; } else if (filter_mode == "add_back") { mode = STENCIL_BLEND_MODE_ABACK; } else if (filter_mode == "fade") { mode = STENCIL_BLEND_MODE_FADE; } // Get the float params: mandatory min, max then the optional parameters (4 max) F32 min = (F32)(mFilterData[i][3].asReal()); F32 max = (F32)(mFilterData[i][4].asReal()); F32 params[4] = {0.0, 0.0, 0.0, 0.0}; for (S32 j = 5; (j < mFilterData[i].size()) && (j < 9); j++) { params[j-5] = (F32)(mFilterData[i][j].asReal()); } // Set the stencil setStencil(shape,mode,min,max,params); } else if (filter_name == "sepia") { filterSepia(); } else if (filter_name == "grayscale") { filterGrayScale(); } else if (filter_name == "saturate") { filterSaturate((float)(mFilterData[i][1].asReal())); } else if (filter_name == "rotate") { filterRotate((float)(mFilterData[i][1].asReal())); } else if (filter_name == "gamma") { LLColor3 color((float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal()),(float)(mFilterData[i][4].asReal())); filterGamma((float)(mFilterData[i][1].asReal()),color); } else if (filter_name == "colorize") { LLColor3 color((float)(mFilterData[i][1].asReal()),(float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal())); LLColor3 alpha((F32)(mFilterData[i][4].asReal()),(float)(mFilterData[i][5].asReal()),(float)(mFilterData[i][6].asReal())); filterColorize(color,alpha); } else if (filter_name == "contrast") { LLColor3 color((float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal()),(float)(mFilterData[i][4].asReal())); filterContrast((float)(mFilterData[i][1].asReal()),color); } else if (filter_name == "brighten") { LLColor3 color((float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal()),(float)(mFilterData[i][4].asReal())); filterBrightness((float)(mFilterData[i][1].asReal()),color); } else if (filter_name == "darken") { LLColor3 color((float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal()),(float)(mFilterData[i][4].asReal())); filterBrightness((float)(-mFilterData[i][1].asReal()),color); } else if (filter_name == "linearize") { LLColor3 color((float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal()),(float)(mFilterData[i][4].asReal())); filterLinearize((float)(mFilterData[i][1].asReal()),color); } else if (filter_name == "posterize") { LLColor3 color((float)(mFilterData[i][2].asReal()),(float)(mFilterData[i][3].asReal()),(float)(mFilterData[i][4].asReal())); filterEqualize((S32)(mFilterData[i][1].asReal()),color); } else if (filter_name == "screen") { std::string screen_name = mFilterData[i][1].asString(); EScreenMode mode = SCREEN_MODE_2DSINE; if (screen_name == "2Dsine") { mode = SCREEN_MODE_2DSINE; } else if (screen_name == "line") { mode = SCREEN_MODE_LINE; } filterScreen(mode,(F32)(mFilterData[i][2].asReal()),(F32)(mFilterData[i][3].asReal())); } else if (filter_name == "blur") { LLMatrix3 kernel; for (S32 i = 0; i < NUM_VALUES_IN_MAT3; i++) for (S32 j = 0; j < NUM_VALUES_IN_MAT3; j++) kernel.mMatrix[i][j] = 1.0; convolve(kernel,true,false); } else if (filter_name == "sharpen") { LLMatrix3 kernel; for (S32 k = 0; k < NUM_VALUES_IN_MAT3; k++) for (S32 j = 0; j < NUM_VALUES_IN_MAT3; j++) kernel.mMatrix[k][j] = -1.0; kernel.mMatrix[1][1] = 9.0; convolve(kernel,false,false); } else if (filter_name == "gradient") { LLMatrix3 kernel; for (S32 k = 0; k < NUM_VALUES_IN_MAT3; k++) for (S32 j = 0; j < NUM_VALUES_IN_MAT3; j++) kernel.mMatrix[k][j] = -1.0; kernel.mMatrix[1][1] = 8.0; convolve(kernel,false,true); } else if (filter_name == "convolve") { LLMatrix3 kernel; S32 index = 1; bool normalize = (mFilterData[i][index++].asReal() > 0.0); bool abs_value = (mFilterData[i][index++].asReal() > 0.0); for (S32 k = 0; k < NUM_VALUES_IN_MAT3; k++) for (S32 j = 0; j < NUM_VALUES_IN_MAT3; j++) kernel.mMatrix[k][j] = mFilterData[i][index++].asReal(); convolve(kernel,normalize,abs_value); } else if (filter_name == "colortransform") { LLMatrix3 transform; S32 index = 1; for (S32 k = 0; k < NUM_VALUES_IN_MAT3; k++) for (S32 j = 0; j < NUM_VALUES_IN_MAT3; j++) transform.mMatrix[k][j] = mFilterData[i][index++].asReal(); transform.transpose(); colorTransform(transform); } else { LL_WARNS() << "Filter unknown, cannot execute filter command : " << filter_name << LL_ENDL; } } } //============================================================================ // Filter Primitives //============================================================================ void LLImageFilter::blendStencil(F32 alpha, U8* pixel, U8 red, U8 green, U8 blue) { F32 inv_alpha = 1.0 - alpha; switch (mStencilBlendMode) { case STENCIL_BLEND_MODE_BLEND: // Classic blend of incoming color with the background image pixel[VRED] = inv_alpha * pixel[VRED] + alpha * red; pixel[VGREEN] = inv_alpha * pixel[VGREEN] + alpha * green; pixel[VBLUE] = inv_alpha * pixel[VBLUE] + alpha * blue; break; case STENCIL_BLEND_MODE_ADD: // Add incoming color to the background image pixel[VRED] = llclampb(pixel[VRED] + alpha * red); pixel[VGREEN] = llclampb(pixel[VGREEN] + alpha * green); pixel[VBLUE] = llclampb(pixel[VBLUE] + alpha * blue); break; case STENCIL_BLEND_MODE_ABACK: // Add back background image to the incoming color pixel[VRED] = llclampb(inv_alpha * pixel[VRED] + red); pixel[VGREEN] = llclampb(inv_alpha * pixel[VGREEN] + green); pixel[VBLUE] = llclampb(inv_alpha * pixel[VBLUE] + blue); break; case STENCIL_BLEND_MODE_FADE: // Fade incoming color to black pixel[VRED] = alpha * red; pixel[VGREEN] = alpha * green; pixel[VBLUE] = alpha * blue; break; } } void LLImageFilter::colorCorrect(const U8* lut_red, const U8* lut_green, const U8* lut_blue) { const S32 components = mImage->getComponents(); llassert( components >= 1 && components <= 4 ); S32 width = mImage->getWidth(); S32 height = mImage->getHeight(); U8* dst_data = mImage->getData(); for (S32 j = 0; j < height; j++) { for (S32 i = 0; i < width; i++) { // Blend LUT value blendStencil(getStencilAlpha(i,j), dst_data, lut_red[dst_data[VRED]], lut_green[dst_data[VGREEN]], lut_blue[dst_data[VBLUE]]); dst_data += components; } } } void LLImageFilter::colorTransform(const LLMatrix3 &transform) { const S32 components = mImage->getComponents(); llassert( components >= 1 && components <= 4 ); S32 width = mImage->getWidth(); S32 height = mImage->getHeight(); U8* dst_data = mImage->getData(); for (S32 j = 0; j < height; j++) { for (S32 i = 0; i < width; i++) { // Compute transform LLVector3 src((F32)(dst_data[VRED]),(F32)(dst_data[VGREEN]),(F32)(dst_data[VBLUE])); LLVector3 dst = src * transform; dst.clamp(0.0f,255.0f); // Blend result blendStencil(getStencilAlpha(i,j), dst_data, dst.mV[VRED], dst.mV[VGREEN], dst.mV[VBLUE]); dst_data += components; } } } void LLImageFilter::convolve(const LLMatrix3 &kernel, bool normalize, bool abs_value) { const S32 components = mImage->getComponents(); llassert( components >= 1 && components <= 4 ); // Compute normalization factors F32 kernel_min = 0.0; F32 kernel_max = 0.0; for (S32 i = 0; i < NUM_VALUES_IN_MAT3; i++) { for (S32 j = 0; j < NUM_VALUES_IN_MAT3; j++) { if (kernel.mMatrix[i][j] >= 0.0) kernel_max += kernel.mMatrix[i][j]; else kernel_min += kernel.mMatrix[i][j]; } } if (abs_value) { kernel_max = llabs(kernel_max); kernel_min = llabs(kernel_min); kernel_max = llmax(kernel_max,kernel_min); kernel_min = 0.0; } F32 kernel_range = kernel_max - kernel_min; // Allocate temporary buffers and initialize algorithm's data S32 width = mImage->getWidth(); S32 height = mImage->getHeight(); U8* dst_data = mImage->getData(); S32 buffer_size = width * components; llassert_always(buffer_size > 0); std::vector even_buffer(buffer_size); std::vector odd_buffer(buffer_size); U8* south_data = dst_data + buffer_size; U8* east_west_data; U8* north_data; // Line 0 : we set the line to 0 (debatable) memcpy( &even_buffer[0], dst_data, buffer_size ); /* Flawfinder: ignore */ for (S32 i = 0; i < width; i++) { blendStencil(getStencilAlpha(i,0), dst_data, 0, 0, 0); dst_data += components; } south_data += buffer_size; // All other lines for (S32 j = 1; j < (height-1); j++) { // We need to buffer 2 lines. We flip north and east-west (current) to avoid moving too much memory around if (j % 2) { memcpy( &odd_buffer[0], dst_data, buffer_size ); /* Flawfinder: ignore */ east_west_data = &odd_buffer[0]; north_data = &even_buffer[0]; } else { memcpy( &even_buffer[0], dst_data, buffer_size ); /* Flawfinder: ignore */ east_west_data = &even_buffer[0]; north_data = &odd_buffer[0]; } // First pixel : set to 0 blendStencil(getStencilAlpha(0,j), dst_data, 0, 0, 0); dst_data += components; // Set pointers to kernel U8* NW = north_data; U8* N = NW+components; U8* NE = N+components; U8* W = east_west_data; U8* C = W+components; U8* E = C+components; U8* SW = south_data; U8* S = SW+components; U8* SE = S+components; // All other pixels for (S32 i = 1; i < (width-1); i++) { // Compute convolution LLVector3 dst; dst.mV[VRED] = (kernel.mMatrix[0][0]*NW[VRED] + kernel.mMatrix[0][1]*N[VRED] + kernel.mMatrix[0][2]*NE[VRED] + kernel.mMatrix[1][0]*W[VRED] + kernel.mMatrix[1][1]*C[VRED] + kernel.mMatrix[1][2]*E[VRED] + kernel.mMatrix[2][0]*SW[VRED] + kernel.mMatrix[2][1]*S[VRED] + kernel.mMatrix[2][2]*SE[VRED]); dst.mV[VGREEN] = (kernel.mMatrix[0][0]*NW[VGREEN] + kernel.mMatrix[0][1]*N[VGREEN] + kernel.mMatrix[0][2]*NE[VGREEN] + kernel.mMatrix[1][0]*W[VGREEN] + kernel.mMatrix[1][1]*C[VGREEN] + kernel.mMatrix[1][2]*E[VGREEN] + kernel.mMatrix[2][0]*SW[VGREEN] + kernel.mMatrix[2][1]*S[VGREEN] + kernel.mMatrix[2][2]*SE[VGREEN]); dst.mV[VBLUE] = (kernel.mMatrix[0][0]*NW[VBLUE] + kernel.mMatrix[0][1]*N[VBLUE] + kernel.mMatrix[0][2]*NE[VBLUE] + kernel.mMatrix[1][0]*W[VBLUE] + kernel.mMatrix[1][1]*C[VBLUE] + kernel.mMatrix[1][2]*E[VBLUE] + kernel.mMatrix[2][0]*SW[VBLUE] + kernel.mMatrix[2][1]*S[VBLUE] + kernel.mMatrix[2][2]*SE[VBLUE]); if (abs_value) { dst.mV[VRED] = llabs(dst.mV[VRED]); dst.mV[VGREEN] = llabs(dst.mV[VGREEN]); dst.mV[VBLUE] = llabs(dst.mV[VBLUE]); } if (normalize) { dst.mV[VRED] = (dst.mV[VRED] - kernel_min)/kernel_range; dst.mV[VGREEN] = (dst.mV[VGREEN] - kernel_min)/kernel_range; dst.mV[VBLUE] = (dst.mV[VBLUE] - kernel_min)/kernel_range; } dst.clamp(0.0f,255.0f); // Blend result blendStencil(getStencilAlpha(i,j), dst_data, dst.mV[VRED], dst.mV[VGREEN], dst.mV[VBLUE]); // Next pixel dst_data += components; NW += components; N += components; NE += components; W += components; C += components; E += components; SW += components; S += components; SE += components; } // Last pixel : set to 0 blendStencil(getStencilAlpha(width-1,j), dst_data, 0, 0, 0); dst_data += components; south_data += buffer_size; } // Last line for (S32 i = 0; i < width; i++) { blendStencil(getStencilAlpha(i,0), dst_data, 0, 0, 0); dst_data += components; } } void LLImageFilter::filterScreen(EScreenMode mode, const F32 wave_length, const F32 angle) { const S32 components = mImage->getComponents(); llassert( components >= 1 && components <= 4 ); S32 width = mImage->getWidth(); S32 height = mImage->getHeight(); F32 wave_length_pixels = wave_length * (F32)(height) / 2.0; F32 sin = sinf(angle*DEG_TO_RAD); F32 cos = cosf(angle*DEG_TO_RAD); // Precompute the gamma table : gives us the gray level to use when cutting outside the screen (prevents strong aliasing on the screen) U8 gamma[256]; for (S32 i = 0; i < 256; i++) { F32 gamma_i = llclampf((float)(powf((float)(i)/255.0,1.0/4.0))); gamma[i] = (U8)(255.0 * gamma_i); } U8* dst_data = mImage->getData(); for (S32 j = 0; j < height; j++) { for (S32 i = 0; i < width; i++) { // Compute screen value F32 value = 0.0; F32 di = 0.0; F32 dj = 0.0; switch (mode) { case SCREEN_MODE_2DSINE: di = cos*i + sin*j; dj = -sin*i + cos*j; value = (sinf(2*F_PI*di/wave_length_pixels)*sinf(2*F_PI*dj/wave_length_pixels)+1.0)*255.0/2.0; break; case SCREEN_MODE_LINE: dj = sin*i - cos*j; value = (sinf(2*F_PI*dj/wave_length_pixels)+1.0)*255.0/2.0; break; } U8 dst_value = (dst_data[VRED] >= (U8)(value) ? gamma[dst_data[VRED] - (U8)(value)] : 0); // Blend result blendStencil(getStencilAlpha(i,j), dst_data, dst_value, dst_value, dst_value); dst_data += components; } } } //============================================================================ // Procedural Stencils //============================================================================ void LLImageFilter::setStencil(EStencilShape shape, EStencilBlendMode mode, F32 min, F32 max, F32* params) { mStencilShape = shape; mStencilBlendMode = mode; mStencilMin = llmin(llmax(min, -1.0f), 1.0f); mStencilMax = llmin(llmax(max, -1.0f), 1.0f); // Each shape will interpret the 4 params differenly. // We compute each systematically, though, clearly, values are meaningless when the shape doesn't correspond to the parameters mStencilCenterX = (S32)(mImage->getWidth() + params[0] * (F32)(mImage->getHeight()))/2; mStencilCenterY = (S32)(mImage->getHeight() + params[1] * (F32)(mImage->getHeight()))/2; mStencilWidth = (S32)(params[2] * (F32)(mImage->getHeight()))/2; mStencilGamma = (params[3] <= 0.0 ? 1.0 : params[3]); mStencilWavelength = (params[0] <= 0.0 ? 10.0 : params[0] * (F32)(mImage->getHeight()) / 2.0); mStencilSine = sinf(params[1]*DEG_TO_RAD); mStencilCosine = cosf(params[1]*DEG_TO_RAD); mStencilStartX = ((F32)(mImage->getWidth()) + params[0] * (F32)(mImage->getHeight()))/2.0; mStencilStartY = ((F32)(mImage->getHeight()) + params[1] * (F32)(mImage->getHeight()))/2.0; F32 end_x = ((F32)(mImage->getWidth()) + params[2] * (F32)(mImage->getHeight()))/2.0; F32 end_y = ((F32)(mImage->getHeight()) + params[3] * (F32)(mImage->getHeight()))/2.0; mStencilGradX = end_x - mStencilStartX; mStencilGradY = end_y - mStencilStartY; mStencilGradN = mStencilGradX*mStencilGradX + mStencilGradY*mStencilGradY; } F32 LLImageFilter::getStencilAlpha(S32 i, S32 j) { F32 alpha = 1.0; // That init actually takes care of the STENCIL_SHAPE_UNIFORM case... if (mStencilShape == STENCIL_SHAPE_VIGNETTE) { // alpha is a modified gaussian value, with a center and fading in a circular pattern toward the edges // The gamma parameter controls the intensity of the drop down from alpha 1.0 (center) to 0.0 F32 d_center_square = (i - mStencilCenterX)*(i - mStencilCenterX) + (j - mStencilCenterY)*(j - mStencilCenterY); alpha = powf(F_E, -(powf((d_center_square/(mStencilWidth*mStencilWidth)),mStencilGamma)/2.0f)); } else if (mStencilShape == STENCIL_SHAPE_SCAN_LINES) { // alpha varies according to a squared sine function. F32 d = mStencilSine*i - mStencilCosine*j; alpha = (sinf(2*F_PI*d/mStencilWavelength) > 0.0 ? 1.0 : 0.0); } else if (mStencilShape == STENCIL_SHAPE_GRADIENT) { alpha = (((F32)(i) - mStencilStartX)*mStencilGradX + ((F32)(j) - mStencilStartY)*mStencilGradY) / mStencilGradN; alpha = llclampf(alpha); } // We rescale alpha between min and max return (mStencilMin + alpha * (mStencilMax - mStencilMin)); } //============================================================================ // Histograms //============================================================================ U32* LLImageFilter::getBrightnessHistogram() { if (!mHistoBrightness) { computeHistograms(); } return mHistoBrightness; } void LLImageFilter::computeHistograms() { const S32 components = mImage->getComponents(); llassert( components >= 1 && components <= 4 ); // Allocate memory for the histograms if (!mHistoRed) { mHistoRed = (U32*) ll_aligned_malloc_16(256*sizeof(U32)); } if (!mHistoGreen) { mHistoGreen = (U32*) ll_aligned_malloc_16(256*sizeof(U32)); } if (!mHistoBlue) { mHistoBlue = (U32*) ll_aligned_malloc_16(256*sizeof(U32)); } if (!mHistoBrightness) { mHistoBrightness = (U32*) ll_aligned_malloc_16(256*sizeof(U32)); } // Initialize them for (S32 i = 0; i < 256; i++) { mHistoRed[i] = 0; mHistoGreen[i] = 0; mHistoBlue[i] = 0; mHistoBrightness[i] = 0; } // Compute them S32 pixels = mImage->getWidth() * mImage->getHeight(); U8* dst_data = mImage->getData(); for (S32 i = 0; i < pixels; i++) { mHistoRed[dst_data[VRED]]++; mHistoGreen[dst_data[VGREEN]]++; mHistoBlue[dst_data[VBLUE]]++; // Note: this is a very simple shorthand for brightness but it's OK for our use S32 brightness = ((S32)(dst_data[VRED]) + (S32)(dst_data[VGREEN]) + (S32)(dst_data[VBLUE])) / 3; mHistoBrightness[brightness]++; // next pixel... dst_data += components; } } //============================================================================ // Secondary Filters //============================================================================ void LLImageFilter::filterGrayScale() { LLMatrix3 gray_scale; LLVector3 luminosity(0.2125, 0.7154, 0.0721); gray_scale.setRows(luminosity, luminosity, luminosity); gray_scale.transpose(); colorTransform(gray_scale); } void LLImageFilter::filterSepia() { LLMatrix3 sepia; sepia.setRows(LLVector3(0.3588, 0.7044, 0.1368), LLVector3(0.2990, 0.5870, 0.1140), LLVector3(0.2392, 0.4696, 0.0912)); sepia.transpose(); colorTransform(sepia); } void LLImageFilter::filterSaturate(F32 saturation) { // Matrix to Lij LLMatrix3 r_a; LLMatrix3 r_b; // 45 degre rotation around z r_a.setRows(LLVector3( OO_SQRT2, OO_SQRT2, 0.0), LLVector3(-OO_SQRT2, OO_SQRT2, 0.0), LLVector3( 0.0, 0.0, 1.0)); // 54.73 degre rotation around y float oo_sqrt3 = 1.0f / F_SQRT3; float sin_54 = F_SQRT2 * oo_sqrt3; r_b.setRows(LLVector3(oo_sqrt3, 0.0, -sin_54), LLVector3(0.0, 1.0, 0.0), LLVector3(sin_54, 0.0, oo_sqrt3)); // Coordinate conversion LLMatrix3 Lij = r_b * r_a; LLMatrix3 Lij_inv = Lij; Lij_inv.transpose(); // Local saturation transform LLMatrix3 s; s.setRows(LLVector3(saturation, 0.0, 0.0), LLVector3(0.0, saturation, 0.0), LLVector3(0.0, 0.0, 1.0)); // Global saturation transform LLMatrix3 transfo = Lij_inv * s * Lij; colorTransform(transfo); } void LLImageFilter::filterRotate(F32 angle) { // Matrix to Lij LLMatrix3 r_a; LLMatrix3 r_b; // 45 degre rotation around z r_a.setRows(LLVector3( OO_SQRT2, OO_SQRT2, 0.0), LLVector3(-OO_SQRT2, OO_SQRT2, 0.0), LLVector3( 0.0, 0.0, 1.0)); // 54.73 degre rotation around y float oo_sqrt3 = 1.0f / F_SQRT3; float sin_54 = F_SQRT2 * oo_sqrt3; r_b.setRows(LLVector3(oo_sqrt3, 0.0, -sin_54), LLVector3(0.0, 1.0, 0.0), LLVector3(sin_54, 0.0, oo_sqrt3)); // Coordinate conversion LLMatrix3 Lij = r_b * r_a; LLMatrix3 Lij_inv = Lij; Lij_inv.transpose(); // Local color rotation transform LLMatrix3 r; angle *= DEG_TO_RAD; r.setRows(LLVector3( cosf(angle), sinf(angle), 0.0), LLVector3(-sinf(angle), cosf(angle), 0.0), LLVector3( 0.0, 0.0, 1.0)); // Global color rotation transform LLMatrix3 transfo = Lij_inv * r * Lij; colorTransform(transfo); } void LLImageFilter::filterGamma(F32 gamma, const LLColor3& alpha) { U8 gamma_red_lut[256]; U8 gamma_green_lut[256]; U8 gamma_blue_lut[256]; for (S32 i = 0; i < 256; i++) { F32 gamma_i = llclampf((float)(powf((float)(i)/255.0,1.0/gamma))); // Blend in with alpha values gamma_red_lut[i] = (U8)((1.0 - alpha.mV[0]) * (float)(i) + alpha.mV[0] * 255.0 * gamma_i); gamma_green_lut[i] = (U8)((1.0 - alpha.mV[1]) * (float)(i) + alpha.mV[1] * 255.0 * gamma_i); gamma_blue_lut[i] = (U8)((1.0 - alpha.mV[2]) * (float)(i) + alpha.mV[2] * 255.0 * gamma_i); } colorCorrect(gamma_red_lut,gamma_green_lut,gamma_blue_lut); } void LLImageFilter::filterLinearize(F32 tail, const LLColor3& alpha) { // Get the histogram U32* histo = getBrightnessHistogram(); // Compute cumulated histogram U32 cumulated_histo[256]; cumulated_histo[0] = histo[0]; for (S32 i = 1; i < 256; i++) { cumulated_histo[i] = cumulated_histo[i-1] + histo[i]; } // Compute min and max counts minus tail tail = llclampf(tail); U32 total = cumulated_histo[255]; U32 min_c = (U32)((F32)(total) * tail); U32 max_c = (U32)((F32)(total) * (1.0 - tail)); // Find min and max values S32 min_v = 0; while (cumulated_histo[min_v] < min_c) { min_v++; } S32 max_v = 255; while (cumulated_histo[max_v] > max_c) { max_v--; } // Compute linear lookup table U8 linear_red_lut[256]{}; U8 linear_green_lut[256]{}; U8 linear_blue_lut[256]{}; if (max_v == min_v) { // Degenerated binary split case for (S32 i = 0; i < 256; i++) { U8 value_i = (i < min_v ? 0 : 255); // Blend in with alpha values linear_red_lut[i] = (U8)((1.0 - alpha.mV[0]) * (float)(i) + alpha.mV[0] * value_i); linear_green_lut[i] = (U8)((1.0 - alpha.mV[1]) * (float)(i) + alpha.mV[1] * value_i); linear_blue_lut[i] = (U8)((1.0 - alpha.mV[2]) * (float)(i) + alpha.mV[2] * value_i); } } else { // Linearize between min and max F32 slope = 255.0 / (F32)(max_v - min_v); F32 translate = -min_v * slope; for (S32 i = 0; i < 256; i++) { U8 value_i = (U8)(llclampb((S32)(slope*i + translate))); // Blend in with alpha values linear_red_lut[i] = (U8)((1.0 - alpha.mV[0]) * (float)(i) + alpha.mV[0] * value_i); linear_green_lut[i] = (U8)((1.0 - alpha.mV[1]) * (float)(i) + alpha.mV[1] * value_i); linear_blue_lut[i] = (U8)((1.0 - alpha.mV[2]) * (float)(i) + alpha.mV[2] * value_i); } } // Apply lookup table colorCorrect(linear_red_lut,linear_green_lut,linear_blue_lut); } void LLImageFilter::filterEqualize(S32 nb_classes, const LLColor3& alpha) { // Regularize the parameter: must be between 2 and 255 nb_classes = llmax(nb_classes,2); nb_classes = llclampb(nb_classes); // Get the histogram U32* histo = getBrightnessHistogram(); // Compute cumulated histogram U32 cumulated_histo[256]; cumulated_histo[0] = histo[0]; for (S32 i = 1; i < 256; i++) { cumulated_histo[i] = cumulated_histo[i-1] + histo[i]; } // Compute deltas U32 total = cumulated_histo[255]; U32 delta_count = total / nb_classes; U32 current_count = delta_count; U32 delta_value = 256 / (nb_classes - 1); U32 current_value = 0; // Compute equalized lookup table U8 equalize_red_lut[256]{}; U8 equalize_green_lut[256]{}; U8 equalize_blue_lut[256]{}; for (S32 i = 0; i < 256; i++) { // Blend in current_value with alpha values equalize_red_lut[i] = (U8)((1.0 - alpha.mV[0]) * (float)(i) + alpha.mV[0] * current_value); equalize_green_lut[i] = (U8)((1.0 - alpha.mV[1]) * (float)(i) + alpha.mV[1] * current_value); equalize_blue_lut[i] = (U8)((1.0 - alpha.mV[2]) * (float)(i) + alpha.mV[2] * current_value); if (cumulated_histo[i] >= current_count) { current_count += delta_count; current_value += delta_value; current_value = llclampb(current_value); } } // Apply lookup table colorCorrect(equalize_red_lut,equalize_green_lut,equalize_blue_lut); } void LLImageFilter::filterColorize(const LLColor3& color, const LLColor3& alpha) { U8 red_lut[256]; U8 green_lut[256]; U8 blue_lut[256]; F32 red_composite = 255.0 * alpha.mV[0] * color.mV[0]; F32 green_composite = 255.0 * alpha.mV[1] * color.mV[1]; F32 blue_composite = 255.0 * alpha.mV[2] * color.mV[2]; for (S32 i = 0; i < 256; i++) { red_lut[i] = (U8)(llclampb((S32)((1.0 - alpha.mV[0]) * (F32)(i) + red_composite))); green_lut[i] = (U8)(llclampb((S32)((1.0 - alpha.mV[1]) * (F32)(i) + green_composite))); blue_lut[i] = (U8)(llclampb((S32)((1.0 - alpha.mV[2]) * (F32)(i) + blue_composite))); } colorCorrect(red_lut,green_lut,blue_lut); } void LLImageFilter::filterContrast(F32 slope, const LLColor3& alpha) { U8 contrast_red_lut[256]; U8 contrast_green_lut[256]; U8 contrast_blue_lut[256]; F32 translate = 128.0 * (1.0 - slope); for (S32 i = 0; i < 256; i++) { U8 value_i = (U8)(llclampb((S32)(slope*i + translate))); // Blend in with alpha values contrast_red_lut[i] = (U8)((1.0 - alpha.mV[0]) * (float)(i) + alpha.mV[0] * value_i); contrast_green_lut[i] = (U8)((1.0 - alpha.mV[1]) * (float)(i) + alpha.mV[1] * value_i); contrast_blue_lut[i] = (U8)((1.0 - alpha.mV[2]) * (float)(i) + alpha.mV[2] * value_i); } colorCorrect(contrast_red_lut,contrast_green_lut,contrast_blue_lut); } void LLImageFilter::filterBrightness(F32 add, const LLColor3& alpha) { U8 brightness_red_lut[256]; U8 brightness_green_lut[256]; U8 brightness_blue_lut[256]; S32 add_value = (S32)(add * 255.0); for (S32 i = 0; i < 256; i++) { U8 value_i = (U8)(llclampb(i + add_value)); // Blend in with alpha values brightness_red_lut[i] = (U8)((1.0 - alpha.mV[0]) * (float)(i) + alpha.mV[0] * value_i); brightness_green_lut[i] = (U8)((1.0 - alpha.mV[1]) * (float)(i) + alpha.mV[1] * value_i); brightness_blue_lut[i] = (U8)((1.0 - alpha.mV[2]) * (float)(i) + alpha.mV[2] * value_i); } colorCorrect(brightness_red_lut,brightness_green_lut,brightness_blue_lut); } //============================================================================