File indexing completed on 2025-02-23 04:09:00
0001 /* 0002 * SPDX-FileCopyrightText: 2009 Dmitry Kazakov <dimula73@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 #include "kis_image_pyramid.h" 0007 0008 #include <QBitArray> 0009 #include <KoChannelInfo.h> 0010 #include <KoCompositeOp.h> 0011 #include <KoColorSpaceRegistry.h> 0012 #include <KoColorModelStandardIds.h> 0013 #include <KoColorSpaceMaths.h> 0014 0015 #include "kis_display_filter.h" 0016 #include "kis_painter.h" 0017 #include "kis_iterator_ng.h" 0018 #include "kis_datamanager.h" 0019 #include "kis_config_notifier.h" 0020 #include "kis_debug.h" 0021 #include "kis_config.h" 0022 #include "kis_image_config.h" 0023 0024 //#define DEBUG_PYRAMID 0025 0026 #include <config-ocio.h> 0027 0028 #define ORIGINAL_INDEX 0 0029 #define FIRST_NOT_ORIGINAL_INDEX 1 0030 #define SCALE_FROM_INDEX(idx) (1./qreal(1<<(idx))) 0031 0032 0033 /************* AUXILIARY FUNCTIONS **********************************/ 0034 0035 #include <KoConfig.h> 0036 #ifdef HAVE_OPENEXR 0037 #include <half.h> 0038 #endif 0039 0040 #define ceiledSize(sz) QSize(ceil((sz).width()), ceil((sz).height())) 0041 #define isOdd(x) ((x) & 0x01) 0042 0043 /** 0044 * Aligns @p value to the lowest integer not smaller than @p value and 0045 * that is a divident of alignment 0046 */ 0047 inline void alignByPow2Hi(qint32 &value, qint32 alignment) 0048 { 0049 qint32 mask = alignment - 1; 0050 value |= mask; 0051 value++; 0052 } 0053 0054 /** 0055 * Aligns @p value to the lowest integer not smaller than @p value and 0056 * that is, increased by one, a divident of alignment 0057 */ 0058 inline void alignByPow2ButOneHi(qint32 &value, qint32 alignment) 0059 { 0060 qint32 mask = alignment - 1; 0061 value |= mask; 0062 } 0063 0064 /** 0065 * Aligns @p value to the highest integer not exceeding @p value and 0066 * that is a divident of @p alignment 0067 */ 0068 inline void alignByPow2Lo(qint32 &value, qint32 alignment) 0069 { 0070 qint32 mask = alignment - 1; 0071 value &= ~mask; 0072 } 0073 0074 inline void alignRectBy2(qint32 &x, qint32 &y, qint32 &w, qint32 &h) 0075 { 0076 x -= isOdd(x); 0077 y -= isOdd(y); 0078 w += isOdd(x); 0079 w += isOdd(w); 0080 h += isOdd(y); 0081 h += isOdd(h); 0082 } 0083 0084 0085 /************* class KisImagePyramid ********************************/ 0086 0087 KisImagePyramid::KisImagePyramid(qint32 pyramidHeight) 0088 : m_monitorProfile(0) 0089 , m_monitorColorSpace(0) 0090 , m_pyramidHeight(pyramidHeight) 0091 { 0092 configChanged(); 0093 connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), this, SLOT(configChanged())); 0094 } 0095 0096 KisImagePyramid::~KisImagePyramid() 0097 { 0098 setImage(0); 0099 } 0100 0101 void KisImagePyramid::setMonitorProfile(const KoColorProfile* monitorProfile, 0102 KoColorConversionTransformation::Intent renderingIntent, 0103 KoColorConversionTransformation::ConversionFlags conversionFlags) 0104 { 0105 m_monitorProfile = monitorProfile; 0106 /** 0107 * If you change pixel size here, don't forget to change it 0108 * in optimized function downsamplePixels() 0109 */ 0110 m_monitorColorSpace = KoColorSpaceRegistry::instance()->rgb8(monitorProfile); 0111 m_renderingIntent = renderingIntent; 0112 m_conversionFlags = conversionFlags; 0113 0114 rebuildPyramid(); 0115 } 0116 0117 void KisImagePyramid::setChannelFlags(const QBitArray &channelFlags) 0118 { 0119 m_channelFlags = channelFlags; 0120 int selectedChannels = 0; 0121 const KoColorSpace *projectionCs = m_originalImage->projection()->colorSpace(); 0122 const QList<KoChannelInfo*> channelInfo = projectionCs->channels(); 0123 0124 if (channelInfo.size() != m_channelFlags.size()) { 0125 m_channelFlags = QBitArray(); 0126 } 0127 0128 for (int i = 0; i < m_channelFlags.size(); ++i) { 0129 if (m_channelFlags.testBit(i) && channelInfo[i]->channelType() == KoChannelInfo::COLOR) { 0130 selectedChannels++; 0131 m_selectedChannelIndex = i; 0132 } 0133 } 0134 m_allChannelsSelected = (selectedChannels == m_channelFlags.size()); 0135 m_onlyOneChannelSelected = (selectedChannels == 1); 0136 } 0137 0138 void KisImagePyramid::setDisplayFilter(QSharedPointer<KisDisplayFilter> displayFilter) 0139 { 0140 m_displayFilter = displayFilter; 0141 } 0142 0143 void KisImagePyramid::rebuildPyramid() 0144 { 0145 m_pyramid.clear(); 0146 for (qint32 i = 0; i < m_pyramidHeight; i++) { 0147 m_pyramid.append(new KisPaintDevice(m_monitorColorSpace)); 0148 } 0149 } 0150 0151 void KisImagePyramid::clearPyramid() 0152 { 0153 for (qint32 i = 0; i < m_pyramidHeight; i++) { 0154 m_pyramid[i]->clear(); 0155 } 0156 } 0157 0158 void KisImagePyramid::setImage(KisImageWSP newImage) 0159 { 0160 if (newImage) { 0161 m_originalImage = newImage; 0162 0163 clearPyramid(); 0164 setImageSize(m_originalImage->width(), m_originalImage->height()); 0165 0166 // Get the full image size 0167 QRect rc = m_originalImage->projection()->exactBounds(); 0168 0169 KisImageConfig config(true); 0170 0171 int patchWidth = config.updatePatchWidth(); 0172 int patchHeight = config.updatePatchHeight(); 0173 0174 if (rc.width() * rc.height() <= patchWidth * patchHeight) { 0175 retrieveImageData(rc); 0176 } 0177 else { 0178 qint32 firstCol = rc.x() / patchWidth; 0179 qint32 firstRow = rc.y() / patchHeight; 0180 0181 qint32 lastCol = (rc.x() + rc.width()) / patchWidth; 0182 qint32 lastRow = (rc.y() + rc.height()) / patchHeight; 0183 0184 for(qint32 i = firstRow; i <= lastRow; i++) { 0185 for(qint32 j = firstCol; j <= lastCol; j++) { 0186 QRect maxPatchRect(j * patchWidth, 0187 i * patchHeight, 0188 patchWidth, patchHeight); 0189 QRect patchRect = rc & maxPatchRect; 0190 retrieveImageData(patchRect); 0191 } 0192 } 0193 0194 } 0195 //TODO: check whether there is needed recalculateCache() 0196 } 0197 } 0198 0199 void KisImagePyramid::setImageSize(qint32 w, qint32 h) 0200 { 0201 Q_UNUSED(w); 0202 Q_UNUSED(h); 0203 /* nothing interesting */ 0204 } 0205 0206 void KisImagePyramid::updateCache(const QRect &dirtyImageRect) 0207 { 0208 retrieveImageData(dirtyImageRect); 0209 } 0210 0211 void KisImagePyramid::retrieveImageData(const QRect &rect) 0212 { 0213 // XXX: use QThreadStorage to cache the two patches (512x512) of pixels. Note 0214 // that when we do that, we need to reset that cache when the projection's 0215 // colorspace changes. 0216 const KoColorSpace *projectionCs = m_originalImage->projection()->colorSpace(); 0217 KisPaintDeviceSP originalProjection = m_originalImage->projection(); 0218 quint32 numPixels = rect.width() * rect.height(); 0219 0220 QScopedArrayPointer<quint8> originalBytes( 0221 new quint8[originalProjection->colorSpace()->pixelSize() * numPixels]); 0222 0223 originalProjection->readBytes(originalBytes.data(), rect); 0224 0225 if (m_displayFilter && 0226 m_useOcio && 0227 projectionCs->colorModelId() == RGBAColorModelID) { 0228 0229 #ifdef HAVE_OCIO 0230 const KoColorProfile *destinationProfile = 0231 m_displayFilter->useInternalColorManagement() ? 0232 m_monitorProfile : projectionCs->profile(); 0233 0234 const KoColorSpace *floatCs = 0235 KoColorSpaceRegistry::instance()->colorSpace( 0236 RGBAColorModelID.id(), 0237 Float32BitsColorDepthID.id(), 0238 destinationProfile); 0239 0240 const KoColorSpace *modifiedMonitorCs = 0241 KoColorSpaceRegistry::instance()->colorSpace( 0242 RGBAColorModelID.id(), 0243 Integer8BitsColorDepthID.id(), 0244 destinationProfile); 0245 0246 if (projectionCs->colorDepthId() == Float32BitsColorDepthID) { 0247 m_displayFilter->filter(originalBytes.data(), numPixels); 0248 } else { 0249 QScopedArrayPointer<quint8> dst(new quint8[floatCs->pixelSize() * numPixels]); 0250 projectionCs->convertPixelsTo(originalBytes.data(), dst.data(), floatCs, numPixels, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); 0251 m_displayFilter->filter(dst.data(), numPixels); 0252 originalBytes.swap(dst); 0253 } 0254 0255 { 0256 QScopedArrayPointer<quint8> dst(new quint8[modifiedMonitorCs->pixelSize() * numPixels]); 0257 floatCs->convertPixelsTo(originalBytes.data(), dst.data(), modifiedMonitorCs, numPixels, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); 0258 originalBytes.swap(dst); 0259 } 0260 #endif 0261 } 0262 else { 0263 if (m_channelFlags.size() != projectionCs->channelCount()) { 0264 setChannelFlags(QBitArray()); 0265 } 0266 if (!m_channelFlags.isEmpty() && !m_allChannelsSelected) { 0267 QScopedArrayPointer<quint8> dst(new quint8[projectionCs->pixelSize() * numPixels]); 0268 0269 KisConfig cfg(true); 0270 0271 if (m_onlyOneChannelSelected && !cfg.showSingleChannelAsColor()) { 0272 projectionCs->convertChannelToVisualRepresentation(originalBytes.data(), dst.data(), numPixels, m_selectedChannelIndex); 0273 } 0274 else { 0275 projectionCs->convertChannelToVisualRepresentation(originalBytes.data(), dst.data(), numPixels, m_channelFlags); 0276 } 0277 originalBytes.swap(dst); 0278 } 0279 0280 QScopedArrayPointer<quint8> dst(new quint8[m_monitorColorSpace->pixelSize() * numPixels]); 0281 projectionCs->convertPixelsTo(originalBytes.data(), dst.data(), m_monitorColorSpace, numPixels, m_renderingIntent, m_conversionFlags); 0282 originalBytes.swap(dst); 0283 } 0284 0285 m_pyramid[ORIGINAL_INDEX]->writeBytes(originalBytes.data(), rect); 0286 } 0287 0288 void KisImagePyramid::recalculateCache(KisPPUpdateInfoSP info) 0289 { 0290 KisPaintDevice *src; 0291 KisPaintDevice *dst; 0292 QRect currentSrcRect = info->dirtyImageRectVar; 0293 0294 for (int i = FIRST_NOT_ORIGINAL_INDEX; i < m_pyramidHeight; i++) { 0295 src = m_pyramid[i-1].data(); 0296 dst = m_pyramid[i].data(); 0297 if (!currentSrcRect.isEmpty()) { 0298 currentSrcRect = downsampleByFactor2(currentSrcRect, src, dst); 0299 } 0300 } 0301 0302 #ifdef DEBUG_PYRAMID 0303 QImage image = m_pyramid[ORIGINAL_INDEX]->convertToQImage(m_monitorProfile, m_renderingIntent, m_conversionFlags); 0304 image.save("./PYRAMID_BASE.png"); 0305 0306 image = m_pyramid[1]->convertToQImage(m_monitorProfile, m_renderingIntent, m_conversionFlags); 0307 image.save("./LEVEL1.png"); 0308 0309 image = m_pyramid[2]->convertToQImage(m_monitorProfile, m_renderingIntent, m_conversionFlags); 0310 image.save("./LEVEL2.png"); 0311 image = m_pyramid[3]->convertToQImage(m_monitorProfile, m_renderingIntent, m_conversionFlags); 0312 image.save("./LEVEL3.png"); 0313 #endif 0314 } 0315 0316 QRect KisImagePyramid::downsampleByFactor2(const QRect& srcRect, 0317 KisPaintDevice* src, 0318 KisPaintDevice* dst) 0319 { 0320 qint32 srcX, srcY, srcWidth, srcHeight; 0321 srcRect.getRect(&srcX, &srcY, &srcWidth, &srcHeight); 0322 alignRectBy2(srcX, srcY, srcWidth, srcHeight); 0323 0324 // Nothing to do 0325 if (srcWidth < 1) return QRect(); 0326 if (srcHeight < 1) return QRect(); 0327 0328 qint32 dstX = srcX / 2; 0329 qint32 dstY = srcY / 2; 0330 qint32 dstWidth = srcWidth / 2; 0331 qint32 dstHeight = srcHeight / 2; 0332 0333 KisHLineConstIteratorSP srcIt0 = src->createHLineConstIteratorNG(srcX, srcY, srcWidth); 0334 KisHLineConstIteratorSP srcIt1 = src->createHLineConstIteratorNG(srcX, srcY + 1, srcWidth); 0335 KisHLineIteratorSP dstIt = dst->createHLineIteratorNG(dstX, dstY, dstWidth); 0336 0337 int conseqPixels = 0; 0338 for (int row = 0; row < dstHeight; ++row) { 0339 do { 0340 int srcItConseq = srcIt0->nConseqPixels(); 0341 int dstItConseq = dstIt->nConseqPixels(); 0342 conseqPixels = qMin(srcItConseq, dstItConseq * 2); 0343 0344 Q_ASSERT(!isOdd(conseqPixels)); 0345 0346 downsamplePixels(srcIt0->oldRawData(), srcIt1->oldRawData(), 0347 dstIt->rawData(), conseqPixels); 0348 0349 0350 srcIt1->nextPixels(conseqPixels); 0351 dstIt->nextPixels(conseqPixels / 2); 0352 } while (srcIt0->nextPixels(conseqPixels)); 0353 srcIt0->nextRow(); 0354 srcIt0->nextRow(); 0355 srcIt1->nextRow(); 0356 srcIt1->nextRow(); 0357 dstIt->nextRow(); 0358 } 0359 return QRect(dstX, dstY, dstWidth, dstHeight); 0360 } 0361 0362 void KisImagePyramid::downsamplePixels(const quint8 *srcRow0, 0363 const quint8 *srcRow1, 0364 quint8 *dstRow, 0365 qint32 numSrcPixels) 0366 { 0367 /** 0368 * FIXME (mandatory): Use SSE and friends here. 0369 */ 0370 0371 qint16 b = 0; 0372 qint16 g = 0; 0373 qint16 r = 0; 0374 qint16 a = 0; 0375 0376 static const qint32 pixelSize = 4; // This is preview argb8 mode 0377 0378 for (qint32 i = 0; i < numSrcPixels / 2; i++) { 0379 b = srcRow0[0] + srcRow1[0] + srcRow0[4] + srcRow1[4]; 0380 g = srcRow0[1] + srcRow1[1] + srcRow0[5] + srcRow1[5]; 0381 r = srcRow0[2] + srcRow1[2] + srcRow0[6] + srcRow1[6]; 0382 a = srcRow0[3] + srcRow1[3] + srcRow0[7] + srcRow1[7]; 0383 0384 dstRow[0] = b / 4; 0385 dstRow[1] = g / 4; 0386 dstRow[2] = r / 4; 0387 dstRow[3] = a / 4; 0388 0389 dstRow += pixelSize; 0390 srcRow0 += 2 * pixelSize; 0391 srcRow1 += 2 * pixelSize; 0392 } 0393 } 0394 0395 int KisImagePyramid::findFirstGoodPlaneIndex(qreal scale, 0396 QSize originalSize) 0397 { 0398 qint32 nearest = 0; 0399 0400 for (qint32 i = 0; i < m_pyramidHeight; i++) { 0401 qreal planeScale = SCALE_FROM_INDEX(i); 0402 if (planeScale < scale) { 0403 if (originalSize*scale == originalSize*planeScale) 0404 nearest = i; 0405 break; 0406 } 0407 nearest = i; 0408 } 0409 0410 // FOR DEBUGGING 0411 //nearest = 0; 0412 //nearest = qMin(1, nearest); 0413 0414 dbgRender << "First good plane:" << nearest << "(sc:" << scale << ")"; 0415 return nearest; 0416 } 0417 0418 void KisImagePyramid::alignSourceRect(QRect& rect, qreal scale) 0419 { 0420 qint32 index = findFirstGoodPlaneIndex(scale, rect.size()); 0421 qint32 alignment = 1 << index; 0422 0423 dbgRender << "Before alignment:\t" << rect; 0424 0425 /** 0426 * Assume that KisImage pixels are always positive 0427 * It allows us to use binary op-s for aligning 0428 */ 0429 Q_ASSERT(rect.left() >= 0 && rect.top() >= 0); 0430 0431 qint32 x1, y1, x2, y2; 0432 rect.getCoords(&x1, &y1, &x2, &y2); 0433 0434 alignByPow2Lo(x1, alignment); 0435 alignByPow2Lo(y1, alignment); 0436 /** 0437 * Here is a workaround of Qt's QRect::right()/bottom() 0438 * "historical reasons". It should be one pixel smaller 0439 * than actual right/bottom position 0440 */ 0441 alignByPow2ButOneHi(x2, alignment); 0442 alignByPow2ButOneHi(y2, alignment); 0443 0444 rect.setCoords(x1, y1, x2, y2); 0445 0446 dbgRender << "After alignment:\t" << rect; 0447 } 0448 0449 KisImagePatch KisImagePyramid::getNearestPatch(KisPPUpdateInfoSP info) 0450 { 0451 qint32 index = findFirstGoodPlaneIndex(qMax(info->scaleX, info->scaleY), 0452 info->imageRect.size()); 0453 qreal planeScale = SCALE_FROM_INDEX(index); 0454 qint32 alignment = 1 << index; 0455 0456 alignByPow2Hi(info->borderWidth, alignment); 0457 0458 KisImagePatch patch(info->imageRect, info->borderWidth, 0459 planeScale, planeScale); 0460 0461 patch.setImage(convertToQImageFast(m_pyramid[index], 0462 patch.patchRect())); 0463 return patch; 0464 } 0465 0466 void KisImagePyramid::drawFromOriginalImage(QPainter& gc, KisPPUpdateInfoSP info) 0467 { 0468 KisImagePatch patch = getNearestPatch(info); 0469 patch.drawMe(gc, info->viewportRect, info->renderHints); 0470 } 0471 0472 QImage KisImagePyramid::convertToQImageFast(KisPaintDeviceSP paintDevice, 0473 const QRect& unscaledRect) 0474 { 0475 qint32 x, y, w, h; 0476 unscaledRect.getRect(&x, &y, &w, &h); 0477 0478 QImage image = QImage(w, h, QImage::Format_ARGB32); 0479 0480 paintDevice->dataManager()->readBytes(image.bits(), x, y, w, h); 0481 0482 return image; 0483 } 0484 0485 void KisImagePyramid::configChanged() 0486 { 0487 KisConfig cfg(true); 0488 m_useOcio = cfg.useOcio(); 0489 } 0490