/* * This file is part of RawTherapee. * * Copyright (c) 2004-2010 Gabor Horvath * * RawTherapee is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * RawTherapee 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with RawTherapee. If not, see . */ #include "improcfun.h" #include "alpha.h" #include "procparams.h" #include "imagesource.h" #include namespace { // "ceil" rounding template constexpr T skips(T a, T b) { return a / b + static_cast(a % b); } } namespace rtengine { class SpotBox { public: enum class Type { SOURCE, TARGET, FINAL }; struct Rectangle { public: int x1; int y1; int x2; int y2; Rectangle() : x1(0), y1(0), x2(0), y2(0) {} Rectangle(const Rectangle &other) : x1(other.x1), y1(other.y1), x2(other.x2), y2(other.y2) {} Rectangle(int X1, int Y1, int X2, int Y2) : x1(X1), y1(Y1), x2(X2), y2(Y2) {} bool intersects(const Rectangle &other) const { return (other.x1 <= x2 && other.x2 >= x1) && (other.y1 <= y2 && other.y2 >= y1); } bool getIntersection(const Rectangle &other, std::unique_ptr &intersection) const { if (intersects(other)) { if (!intersection) { intersection.reset(new Rectangle()); } intersection->x1 = rtengine::max(x1, other.x1); intersection->x2 = rtengine::min(x2, other.x2); intersection->y1 = rtengine::max(y1, other.y1); intersection->y2 = rtengine::min(y2, other.y2); if (intersection->x1 > intersection->x2 || intersection->y1 > intersection->y2) { intersection.release(); return false; } return true; } if (intersection) { intersection.release(); } return false; } Rectangle& operator+=(const Coord &v) { x1 += v.x; y1 += v.y; x2 += v.x; y2 += v.y; return *this; } Rectangle& operator-=(const Coord &v) { x1 -= v.x; y1 -= v.y; x2 -= v.x; y2 -= v.y; return *this; } Rectangle& operator/=(const int &v) { if (v == 1) { return *this; } int w = x2 - x1 + 1; int h = y2 - y1 + 1; w = w / v + (w % v > 0); h = h / v + (h % v > 0); x1 /= v; y1 /= v; x2 = x1 + w - 1; y2 = y1 + h - 1; return *this; } }; private: Type type; Imagefloat* image; public: // top/left and bottom/right coordinates of the spot in image space (at some point divided by scale factor) Rectangle spotArea; // top/left and bottom/right coordinates of the spot in scaled image space (on borders, imgArea won't cover spotArea) Rectangle imgArea; // top/left and bottom/right coordinates of useful part of the image in scaled image space (rounding error workaround) Rectangle intersectionArea; float radius; float featherRadius; SpotBox (int tl_x, int tl_y, int br_x, int br_y, int radius, int feather_radius, Imagefloat* image, Type type) : type(type), image(image), spotArea(tl_x, tl_y, br_x, br_y), imgArea(spotArea), intersectionArea(), radius(radius), featherRadius(feather_radius) {} SpotBox (int tl_x, int tl_y, int radius, int feather_radius, Imagefloat* image, Type type) : type(type), image(image), spotArea(tl_x, tl_y, image ? tl_x + image->getWidth() - 1 : 0, image ? tl_y + image->getHeight() - 1 : 0), imgArea(spotArea), intersectionArea(), radius(radius), featherRadius(feather_radius) {} SpotBox (SpotEntry &spot, Type type) : type(type), image(nullptr), intersectionArea(), radius(spot.radius), featherRadius(int(spot.getFeatherRadius() + 0.5f)) // rounding to int before resizing { spotArea.x1 = int ((type == Type::SOURCE ? spot.sourcePos.x : spot.targetPos.x) - featherRadius); spotArea.x2 = int ((type == Type::SOURCE ? spot.sourcePos.x : spot.targetPos.x) + featherRadius); spotArea.y1 = int ((type == Type::SOURCE ? spot.sourcePos.y : spot.targetPos.y) - featherRadius); spotArea.y2 = int ((type == Type::SOURCE ? spot.sourcePos.y : spot.targetPos.y) + featherRadius); imgArea = spotArea; } ~SpotBox() { if (image && type != Type::FINAL) { delete image; } } SpotBox& operator /=(const int& v) { if (v == 1) { return *this; } spotArea /= v; imgArea /= v; radius /= float(v); featherRadius = getWidth() / 2.f; // intersectionArea doesn't need resize, because it's set after resizing return *this; } int getWidth() { return spotArea.x2 - spotArea.x1 + 1; } int getHeight() { return spotArea.y2 - spotArea.y1 + 1; } int getImageWidth() { return imgArea.x2 - imgArea.x1 + 1; } int getImageHeight() { return imgArea.y2 - imgArea.y1 + 1; } int getIntersectionWidth() { return intersectionArea.x2 - intersectionArea.x1 + 1; } int getIntersectionHeight() { return intersectionArea.y2 - intersectionArea.y1 + 1; } bool checkImageSize() { if (!image || getImageWidth() != image->getWidth() || getImageHeight() != image->getHeight()) { return false; } return true; } void tuneImageSize() { if (!image) { return; } if (getImageWidth() > image->getWidth()) { imgArea.x2 = imgArea.x1 + image->getWidth() - 1; } if (getImageHeight() > image->getHeight()) { imgArea.y2 = imgArea.y1 + image->getHeight() - 1; } } Imagefloat *getImage() { // TODO: this should send back a const value, but getImage don't want it to be const... return image; } void allocImage() { int newW = imgArea.x2 - imgArea.x1 + 1; int newH = imgArea.y2 - imgArea.y1 + 1; if (image && type != Type::FINAL && (image->getWidth() != newW || image->getHeight() != newH)) { delete image; image = nullptr; } if (image == nullptr) { image = new Imagefloat(newW, newH); } } bool spotIntersects(const SpotBox &other) const { return spotArea.intersects(other.spotArea); } bool getSpotIntersection(const SpotBox &other, std::unique_ptr &intersection) const { return spotArea.getIntersection(other.spotArea, intersection); } bool imageIntersects(const SpotBox &other, bool atDestLocation=false) const { if (atDestLocation) { Coord v(other.spotArea.x1 - spotArea.x1, other.spotArea.y1 - spotArea.y1); Rectangle imgArea2(imgArea.x1, imgArea.y1, imgArea.x2, imgArea.y2); imgArea2 += v; return imgArea2.intersects(other.imgArea); } return imgArea.intersects(other.imgArea); } bool mutuallyClipImageArea(SpotBox &other) { Coord v(other.spotArea.x1 - spotArea.x1, other.spotArea.y1 - spotArea.y1); Rectangle imgArea2 = imgArea; imgArea2 += v; std::unique_ptr intersection; if (!imgArea2.getIntersection(other.imgArea, intersection)) { return false; } other.intersectionArea = *intersection; Coord v2(-v.x, -v.y); *intersection -= v; intersectionArea = *intersection; return true; } bool setIntersectionWith(const SpotBox &other) { if (!spotIntersects(other)) { return false; } imgArea.x1 = rtengine::max(spotArea.x1, other.spotArea.x1); imgArea.x2 = rtengine::min(spotArea.x2, other.spotArea.x2); imgArea.y1 = rtengine::max(spotArea.y1, other.spotArea.y1); imgArea.y2 = rtengine::min(spotArea.y2, other.spotArea.y2); if (imgArea.x1 > imgArea.x2 || imgArea.y1 > imgArea.y2) { return false; } return true; } #define ALGO 1 #if ALGO==1 bool processIntersectionWith(SpotBox &destBox) { Imagefloat *dstImg = destBox.image; if (image == nullptr || dstImg == nullptr) { std::cerr << "One of the source or destination SpotBox image is missing !" << std::endl; return false; } int srcImgY = intersectionArea.y1 - imgArea.y1; int dstImgY = destBox.intersectionArea.y1 - destBox.imgArea.y1; for (int y = intersectionArea.y1; y <= intersectionArea.y2; ++y) { float dy = float(y - spotArea.y1) - featherRadius; int srcImgX = intersectionArea.x1 - imgArea.x1; int dstImgX = destBox.intersectionArea.x1 - destBox.imgArea.x1; for (int x = intersectionArea.x1; x <= intersectionArea.x2; ++x) { float dx = float(x - spotArea.x1) - featherRadius; float r = sqrt(dx * dx + dy * dy); if (r >= featherRadius) { ++srcImgX; ++dstImgX; continue; } if (r <= radius) { dstImg->r(dstImgY, dstImgX) = image->r(srcImgY, srcImgX); dstImg->g(dstImgY, dstImgX) = image->g(srcImgY, srcImgX); dstImg->b(dstImgY, dstImgX) = image->b(srcImgY, srcImgX); } else { float opacity = (featherRadius - r) / (featherRadius - radius); dstImg->r(dstImgY, dstImgX) = (image->r(srcImgY, srcImgX) - dstImg->r(dstImgY, dstImgX)) * opacity + dstImg->r(dstImgY,dstImgX); dstImg->g(dstImgY, dstImgX) = (image->g(srcImgY, srcImgX) - dstImg->g(dstImgY, dstImgX)) * opacity + dstImg->g(dstImgY,dstImgX); dstImg->b(dstImgY, dstImgX) = (image->b(srcImgY, srcImgX) - dstImg->b(dstImgY, dstImgX)) * opacity + dstImg->b(dstImgY,dstImgX); } ++srcImgX; ++dstImgX; } ++srcImgY; ++dstImgY; } return true; } #endif #if ALGO==2 bool processIntersectionWith(SpotBox &destBox) { /* The following disabled code has been taken from Gimp 2.8.10 and converted * for RawTherapee by Jean-Christophe FRISCH (aka Hombre) on 02.19.2014. * It has not been tested, at all, an can (does) contain bugs. Feel free to * replace the actual working code by this one, if results are better. */ /* ORIGINAL NOTES * * The method used here is similar to the lighting invariant correction * method but slightly different: we do not divide the RGB components, * but substract them I2 = I0 - I1, where I0 is the sample image to be * corrected, I1 is the reference pattern. Then we solve DeltaI=0 * (Laplace) with I2 Dirichlet conditions at the borders of the * mask. The solver is a unoptimized red/black checker Gauss-Siedel * with an over-relaxation factor of 1.8. It can benefit from a * multi-grid evaluation of an initial solution before the main * iteration loop. * * I reduced the convergence criteria to 0.1% (0.001) as we are * dealing here with RGB integer components, more is overkill. * * Jean-Yves Couleaud cjyves@free.fr */ /* Original Algorithm Design: * * T. Georgiev, "Photoshop Healing Brush: a Tool for Seamless Cloning * http://www.tgeorgiev.net/Photoshop_Healing.pdf */ // ----------------- Core function ----------------- int scaledPPX = pp.getX() / pp.skip; int scaledPPY = pp.getY() / pp.skip; int scaledPPW = pp.getWidth() / pp.skip + (pp.getWidth() % pp.getSkip() > 0); int scaledPPH = pp.getHeight() / pp.skip + (pp.getHeight() % pp.skip > 0); int sizeX = dst_XMax - dst_XMin + 1; int sizeY = dst_YMax - dst_YMin + 1; Imagefloat matrix (sizeX, sizeY); Imagefloat solution (sizeX, sizeY); // allocate the mask and draw it mask.setSize (sizeX, sizeY); { Cairo::RefPtr cr = Cairo::Context::create (mask.getSurface()); // clear the bitmap cr->set_source_rgba (0., 0., 0., 0.); cr->rectangle (0., 0., sizeX, sizeY); cr->set_line_width (0.); cr->fill(); // draw the mask cr->set_antialias (Cairo::ANTIALIAS_GRAY); cr->set_line_width (featherRadius); double gradientCenterX = double (sizeX) / 2.; double gradientCenterY = double (sizeY) / 2.; { Cairo::RefPtr radialGradient = Cairo::RadialGradient::create ( gradientCenterX, gradientCenterY, radius, gradientCenterX, gradientCenterY, featherRadius ); radialGradient->add_color_stop_rgb (0., 0., 0., 1.); radialGradient->add_color_stop_rgb (1., 0., 0., 0.); cr->set_source_rgba (0., 0., 0., 1.); cr->mask (radialGradient); cr->rectangle (0., 0., sizeX, sizeY); cr->fill(); } } // copy the src part to a temporary buffer to avoid possible self modified source Imagefloat *srcBuff = img->copySubRegion (srcX, srcY, sizeX, sizeY); // subtract pattern to image and store the result as a double in matrix for (int i = 0, i2 = dst_YMin; i2 < sizeY - 1; ++i, ++i2) { for (int j = 0, j2 = dst_XMin; i2 < sizeX - 1; ++j, ++j2) { matrix.r (i, j) = img->r (i2, j2) - srcBuff->r (i, j); matrix.g (i, j) = img->g (i2, j2) - srcBuff->g (i, j); matrix.b (i, j) = img->b (i2, j2) - srcBuff->b (i, j); } } // FIXME: is a faster implementation needed? #define EPSILON 0.001 #define MAX_ITER 500 // repeat until convergence or max iterations for (int n = 0; n < MAX_ITER; ++n) { // ---------------------------------------------------------------- /* Perform one iteration of the Laplace solver for matrix. Store the * result in solution and get the square of the cumulative error * of the solution. */ int i, j; double tmp, diff; double sqr_err_r = 0.0; double sqr_err_g = 0.0; double sqr_err_b = 0.0; const double w = 1.80 * 0.25; /* Over-relaxation = 1.8 */ // we use a red/black checker model of the discretization grid // do reds for (i = 0; i < matrix.getHeight(); ++i) { for (j = i % 2; j < matrix.getWidth(); j += 2) { if ((0 == mask (i, j)) || (i == 0) || (i == (matrix.getHeight() - 1)) || (j == 0) || (j == (matrix.getWidth() - 1))) { // do nothing at the boundary or outside mask solution.r (i, j) = matrix.r (i, j); solution.g (i, j) = matrix.g (i, j); solution.b (i, j) = matrix.b (i, j); } else { // Use Gauss Siedel to get the correction factor then over-relax it tmp = solution.r (i, j); solution.r (i, j) = (matrix.r (i, j) + w * ( matrix.r (i, j - 1) + // west matrix.r (i, j + 1) + // east matrix.r (i - 1, j) + // north matrix.r (i + 1, j) - 4.0 * matrix.r (i, j) // south ) ); diff = solution.r (i, j) - tmp; sqr_err_r += diff * diff; tmp = solution.g (i, j); solution.g (i, j) = (matrix.g (i, j) + w * ( matrix.g (i, j - 1) + // west matrix.g (i, j + 1) + // east matrix.g (i - 1, j) + // north matrix.g (i + 1, j) - 4.0 * matrix.g (i, j) // south ) ); diff = solution.g (i, j) - tmp; sqr_err_g += diff * diff; tmp = solution.b (i, j); solution.b (i, j) = (matrix.b (i, j) + w * ( matrix.b (i, j - 1) + // west matrix.b (i, j + 1) + // east matrix.b (i - 1, j) + // north matrix.b (i + 1, j) - 4.0 * matrix.b (i, j) // south ) ); diff = solution.b (i, j) - tmp; sqr_err_b += diff * diff; } } } /* Do blacks * * As we've done the reds earlier, we can use them right now to * accelerate the convergence. So we have "solution" in the solver * instead of "matrix" above */ for (i = 0; i < matrix.getHeight(); i++) { for (j = (i % 2) ? 0 : 1; j < matrix.getWidth(); j += 2) { if ((0 == mask (i, j)) || (i == 0) || (i == (matrix.getHeight() - 1)) || (j == 0) || (j == (matrix.getWidth() - 1))) { // do nothing at the boundary or outside mask solution.r (i, j) = matrix.r (i, j); solution.g (i, j) = matrix.g (i, j); solution.b (i, j) = matrix.b (i, j); } else { // Use Gauss Siedel to get the correction factor then over-relax it tmp = solution.r (i, j); solution.r (i, j) = (matrix.r (i, j) + w * ( matrix.r (i, j - 1) + // west matrix.r (i, j + 1) + // east matrix.r (i - 1, j) + // north matrix.r (i + 1, j) - 4.0 * matrix.r (i, j) // south ) ); diff = solution.r (i, j) - tmp; sqr_err_r += diff * diff; tmp = solution.g (i, j); solution.g (i, j) = (matrix.g (i, j) + w * ( matrix.g (i, j - 1) + // west matrix.g (i, j + 1) + // east matrix.g (i - 1, j) + // north matrix.g (i + 1, j) - 4.0 * matrix.g (i, j) // south ) ); diff = solution.g (i, j) - tmp; sqr_err_g += diff * diff; tmp = solution.b (i, j); solution.b (i, j) = (matrix.b (i, j) + w * ( matrix.b (i, j - 1) + // west matrix.b (i, j + 1) + // east matrix.b (i - 1, j) + // north matrix.b (i + 1, j) - 4.0 * matrix.b (i, j) // south ) ); diff = solution.b (i, j) - tmp; sqr_err_b += diff * diff; } } } // ---------------------------------------------------------------- // copy solution to matrix solution.copyData (&matrix); if (sqr_err_r < EPSILON && sqr_err_g < EPSILON && sqr_err_b < EPSILON) { break; } } // add solution to original image and store in tempPR for (int i = 0, i2 = dst_YMin; i2 < dst_YMax - 1; ++i, ++i2) { if (i2 < 0 || i2 >= img->getHeight()) { continue; } for (int j = 0, j2 = dst_XMin; j2 < dst_XMax - 1; ++j, ++j2) { if (j2 < 0 || j2 >= img->getWidth()) { continue; } float c2 = float (mask (i, j)) / 255.f; float c1 = 1.f - c2; resultPR->r(i,j) = (unsigned char) CLAMP0255 ( ROUND( double(first->r(i,j)) + double(secondPR->r(i,j)) ) ); img->r(i2,j2) = img->r(i2,j2)*c1 + (solution.r(i,j) + srcBuff->r(i,j))*c2; img->g(i2,j2) = img->g(i2,j2)*c1 + (solution.g(i,j) + srcBuff->g(i,j))*c2; img->b(i2,j2) = img->b(i2,j2)*c1 + (solution.b(i,j) + srcBuff->b(i,j))*c2; } } } #endif // Copy the intersecting part bool copyImgTo(SpotBox &destBox) { Imagefloat *destImg = destBox.image; if (image == nullptr || destImg == nullptr) { std::cerr << "One of the source or destination SpotBox image is missing !" << std::endl; return false; } std::unique_ptr intersection; if (!intersectionArea.getIntersection(destBox.intersectionArea, intersection)) { return false; } Imagefloat *srcImg = image; Imagefloat *dstImg = destBox.image; int srcImgY = intersection->y1 - imgArea.y1; int dstImgY = intersection->y1 - destBox.imgArea.y1; for (int y = intersection->y1; y <= intersection->y2; ++y) { int srcImgX = intersection->x1 - imgArea.x1; int dstImgX = intersection->x1 - destBox.imgArea.x1; for (int x = intersection->x1; x <= intersection->x2; ++x) { dstImg->r(dstImgY, dstImgX) = srcImg->r(srcImgY, srcImgX); dstImg->g(dstImgY, dstImgX) = srcImg->g(srcImgY, srcImgX); dstImg->b(dstImgY, dstImgX) = srcImg->b(srcImgY, srcImgX); ++srcImgX; ++dstImgX; } ++srcImgY; ++dstImgY; } return true; } }; void ImProcFunctions::removeSpots (Imagefloat* img, ImageSource* imgsrc, const std::vector &entries, const PreviewProps &pp, const ColorTemp &currWB, const ColorManagementParams *cmp, int tr) { //Get the clipped image areas (src & dst) from the source image std::vector< std::shared_ptr > srcSpotBoxs; std::vector< std::shared_ptr > dstSpotBoxs; int fullImgWidth = 0; int fullImgHeight = 0; imgsrc->getFullSize(fullImgWidth, fullImgHeight, tr); SpotBox fullImageBox(0, 0, fullImgWidth - 1, fullImgHeight - 1, 0, 0, nullptr, SpotBox::Type::FINAL); SpotBox cropBox(pp.getX(), pp.getY(), pp.getX() + pp.getWidth() - 1, pp.getY() + pp.getHeight() - 1, 0, 0, img, SpotBox::Type::FINAL); std::set visibleSpots; // list of dest spots intersecting the preview's crop int i = 0; for (auto entry : params->spot.entries) { std::shared_ptr srcSpotBox(new SpotBox(entry, SpotBox::Type::SOURCE)); std::shared_ptr dstSpotBox(new SpotBox(entry, SpotBox::Type::TARGET)); if ( !srcSpotBox->setIntersectionWith(fullImageBox) || !dstSpotBox->setIntersectionWith(fullImageBox) || !srcSpotBox->imageIntersects(*dstSpotBox, true)) { continue; ++i; } // If spot intersect the preview image, add it to the visible spots if (dstSpotBox->spotIntersects(cropBox)) { visibleSpots.insert(i); } ++i; // Source area PreviewProps spp(srcSpotBox->imgArea.x1, srcSpotBox->imgArea.y1, srcSpotBox->getImageWidth(), srcSpotBox->getImageHeight(), pp.getSkip()); int w = 0; int h = 0; imgsrc->getSize(spp, w, h); *srcSpotBox /= pp.getSkip(); srcSpotBox->allocImage(); Imagefloat *srcImage = srcSpotBox->getImage(); for (int y = 0; y < (int)srcImage->getHeight(); ++y) { for (int x = 0; x < (int)srcImage->getWidth(); ++x) { srcImage->r(y, x) = 60000.f; srcImage->g(y, x) = 500.f; srcImage->b(y, x) = 500.f; } } imgsrc->getImage(currWB, tr, srcSpotBox->getImage(), spp, params->toneCurve, params->raw); if (cmp) { imgsrc->convertColorSpace(srcImage, *cmp, currWB); } assert(srcSpotBox->checkImageSize()); // Destination area spp.set(dstSpotBox->imgArea.x1, dstSpotBox->imgArea.y1, dstSpotBox->getImageWidth(), dstSpotBox->getImageHeight(), pp.getSkip()); *dstSpotBox /= pp.getSkip(); dstSpotBox->allocImage(); Imagefloat *dstImage = dstSpotBox->getImage(); for (int y = 0; y < (int)dstImage->getHeight(); ++y) { for (int x = 0; x < (int)dstImage->getWidth(); ++x) { dstImage->r(y, x) = 500.f; dstImage->g(y, x) = 500.f; dstImage->b(y, x) = 60000.f; } } imgsrc->getImage(currWB, tr, dstSpotBox->getImage(), spp, params->toneCurve, params->raw); if (cmp) { imgsrc->convertColorSpace(dstImage, *cmp, currWB); } assert(dstSpotBox->checkImageSize()); // Update the intersectionArea between src and dest if (srcSpotBox->mutuallyClipImageArea(*dstSpotBox)) { srcSpotBoxs.push_back(srcSpotBox); dstSpotBoxs.push_back(dstSpotBox); } } // Construct list of upstream dependancies std::set requiredSpots = visibleSpots; // starting point, visible spots are necessarilly required spots for (auto i = requiredSpots.rbegin(); i != requiredSpots.rend(); i++) { int spotNbr = *i; requiredSpots.insert(spotNbr); if (spotNbr > 0) { for (int j = spotNbr - 1; j >= 0; --j) { if ((srcSpotBoxs.at(spotNbr))->imageIntersects(*dstSpotBoxs.at(j))) { requiredSpots.insert(spotNbr); } } } } // Process spots and copy them downstream for (auto i = requiredSpots.begin(); i != requiredSpots.end(); i++) { // Process srcSpotBoxs.at(*i)->processIntersectionWith(*dstSpotBoxs.at(*i)); // Propagate std::set positiveSpots; // For DEBUG purpose only ! auto j = i; ++j; while (j != requiredSpots.end()) { bool intersectionFound = false; int i_ = *i; int j_ = *j; intersectionFound |= dstSpotBoxs.at(i_)->copyImgTo(*srcSpotBoxs.at(j_)); intersectionFound |= dstSpotBoxs.at(i_)->copyImgTo(*dstSpotBoxs.at(j_)); if (intersectionFound) { positiveSpots.insert(j_); } ++j; } } // Copy the dest spot to the preview image cropBox /= pp.getSkip(); cropBox.tuneImageSize(); cropBox.intersectionArea = cropBox.imgArea; int f = 0; for (auto i : visibleSpots) { f += dstSpotBoxs.at(i)->copyImgTo(cropBox) ? 1 : 0; } } }