/* * Copyright 2026 Forschungszentrum J?lich * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #include #include #include #include #include "../layer/CageMesh.h" #include "GeometryUtils.h" #include // --------------------- TriangleWarp Methods --------------------- namespace TriangleWarp { struct WarpResult { QImage image; // Das fertig transformierte Bild QPointF offset; // Die Verschiebung relativ zum Original-Nullpunkt }; // --- --- --- void transformQuad( int x, int y, int cols, int rows, const QImage& originalImage, QImage& warped, const CageMesh& cageMesh, const QRectF& dstBounds ) { // Bounds-Check: Existiert das Quad (x, y) bis (x+1, y+1)? if ( x < 0 || x + 1 >= cols || y < 0 || y + 1 >= rows ) return; int i00 = y * cols + x; int i10 = i00 + 1; int i01 = i00 + cols; int i11 = i01 + 1; float srcL = float(x) * originalImage.width() / (cols - 1); float srcR = float(x + 1) * originalImage.width() / (cols - 1); float srcT = float(y) * originalImage.height() / (rows - 1); float srcB = float(y + 1) * originalImage.height() / (rows - 1); QVector dstQuad{ cageMesh.point(i00) - dstBounds.topLeft(), cageMesh.point(i10) - dstBounds.topLeft(), cageMesh.point(i11) - dstBounds.topLeft(), cageMesh.point(i01) - dstBounds.topLeft() }; QRect br = QPolygonF(dstQuad).boundingRect().toAlignedRect(); br &= warped.rect(); for ( int py = br.top(); py <= br.bottom(); ++py ) { QRgb* scanline = reinterpret_cast(warped.scanLine(py)); for ( int px = br.left(); px <= br.right(); ++px ) { QPointF p(px, py); if ( !GeometryUtils::pointInQuad(p, dstQuad) ) continue; QPointF uv = GeometryUtils::getBilinearUV(p,dstQuad); float srcX = srcL + uv.x() * (srcR - srcL); float srcY = srcT + uv.y() * (srcB - srcT); if ( srcX >= 0 && srcX < originalImage.width() && srcY >= 0 && srcY < originalImage.height() ) { scanline[px] = originalImage.pixel(int(srcX), int(srcY)); } } } } WarpResult warp( QImage & currentImage, const QImage& originalImage, const CageMesh& cageMesh ) { qCDebug(logEditor) << "TriangleWarp:warp(): useQuads =" << EditorStyle::instance().useCageQuads(); { if ( cageMesh.pointCount() < 4 ) { return { QImage(), QPointF(0,0) }; } // compute bounding box QRectF dstBounds; for ( int i=0 ; i srcQuad{ QPointF(x * originalImage.width() / (cols - 1), y * originalImage.height() / (rows - 1)), QPointF((x + 1) * originalImage.width() / (cols - 1), y * originalImage.height() / (rows - 1)), QPointF((x + 1) * originalImage.width() / (cols - 1), (y + 1) * originalImage.height() / (rows - 1)), QPointF(x * originalImage.width() / (cols - 1), (y + 1) * originalImage.height() / (rows - 1)) }; QVector dstQuad{ cageMesh.point(i00) - dstBounds.topLeft(), cageMesh.point(i10) - dstBounds.topLeft(), cageMesh.point(i11) - dstBounds.topLeft(), cageMesh.point(i01) - dstBounds.topLeft() }; QRectF br = QPolygonF(dstQuad).boundingRect(); QPointF start(-1,-1); double firstY = -1.0; for ( int py = int(br.top()); py <= int(br.bottom()); ++py ) { start.setX(-1); // reset at start of row start.setY(firstY); // reset at start of row int saveY = 1; for ( int px = int(br.left()); px <= int(br.right()); ++px ) { QPointF p(px, py); if ( !GeometryUtils::pointInQuad(p, dstQuad) ) continue; if( updateQuad ) { numcalls++; QPointF srcP = GeometryUtils::barycentric(p, dstQuad, srcQuad, &count, start ); if( saveY ) { firstY = start.y(); // first Y of the row remembered for next row saveY = 0; } if ( originalImage.rect().contains(srcP.toPoint()) ) { QRgb c = originalImage.pixel(srcP.toPoint()); warped.setPixel(px, py, c); } } else { QPointF pp(px, py); pp += cageMesh.getOffset(1); QRgb c = currentImage.pixel(pp.toPoint() ); warped.setPixel(px, py, c ); } } } } } } else { for ( int y = 0; y + 1 < rows; ++y ) { for ( int x = 0; x + 1 < cols; ++x ) { int i00 = y * cols + x; int i10 = i00 + 1; int i01 = i00 + cols; int i11 = i01 + 1; float srcL = float(x) * originalImage.width() / (cols - 1); float srcR = float(x + 1) * originalImage.width() / (cols - 1); float srcT = float(y) * originalImage.height() / (rows - 1); float srcB = float(y + 1) * originalImage.height() / (rows - 1); QVector dstQuad{ cageMesh.point(i00) - dstBounds.topLeft(), cageMesh.point(i10) - dstBounds.topLeft(), cageMesh.point(i11) - dstBounds.topLeft(), cageMesh.point(i01) - dstBounds.topLeft() }; QRect br = QPolygonF(dstQuad).boundingRect().toAlignedRect(); br &= warped.rect(); for ( int py = br.top(); py <= br.bottom(); ++py ) { QRgb* scanline = reinterpret_cast(warped.scanLine(py)); for ( int px = br.left(); px <= br.right(); ++px ) { QPointF p(px, py); if ( !GeometryUtils::pointInQuad(p, dstQuad) ) continue; QPointF uv = GeometryUtils::getBilinearUV(p, dstQuad); float srcX = srcL + uv.x() * (srcR - srcL); float srcY = srcT + uv.y() * (srcB - srcT); if ( srcX >= 0 && srcX < originalImage.width() && srcY >= 0 && srcY < originalImage.height() ) { scanline[px] = originalImage.pixel(int(srcX), int(srcY)); } } } } } } } else { // --- TRIANGLE WARP --- int count = 0; for ( int y = 0; y + 1 < rows; ++y ) { for ( int x = 0; x + 1 < cols; ++x ) { int i00 = y*cols + x; int i10 = i00 + 1; int i01 = i00 + cols; int i11 = i01 + 1; QVector srcQuad{ QPointF(x * originalImage.width() / (cols-1), y * originalImage.height() / (rows-1)), QPointF((x+1) * originalImage.width() / (cols-1), y * originalImage.height() / (rows-1)), QPointF(x * originalImage.width() / (cols-1), (y+1) * originalImage.height() / (rows-1)), QPointF((x+1) * originalImage.width() / (cols-1), (y+1) * originalImage.height() / (rows-1)) }; QVector dstQuad{ cageMesh.point(i00) - dstBounds.topLeft(), cageMesh.point(i10) - dstBounds.topLeft(), cageMesh.point(i01) - dstBounds.topLeft(), cageMesh.point(i11) - dstBounds.topLeft() }; // two tris per quad QVector tri1{srcQuad[0], srcQuad[1], srcQuad[2]}; QVector tri1Dst{dstQuad[0], dstQuad[1], dstQuad[2]}; QVector tri2{srcQuad[1], srcQuad[2], srcQuad[3]}; QVector tri2Dst{dstQuad[1], dstQuad[2], dstQuad[3]}; // triangle 1 QRectF br1 = QPolygonF(tri1Dst).boundingRect(); for ( int py = int(br1.top()); py <= int(br1.bottom()); ++py ) { for ( int px = int(br1.left()); px <= int(br1.right()); ++px ) { QPointF p(px, py); QPointF start(0,0); if ( !GeometryUtils::pointInTriangle(p, tri1Dst) ) continue; QPointF srcP = GeometryUtils::barycentric(p, tri1Dst, tri1, &count, start ); if ( !originalImage.rect().contains(srcP.toPoint()) ) continue; QColor c = originalImage.pixelColor(int(srcP.x()), int(srcP.y())); warped.setPixelColor(px, py, c); } } // triangle 2 QRectF br2 = QPolygonF(tri2Dst).boundingRect(); for ( int py = int(br2.top()); py <= int(br2.bottom()); ++py ) { for ( int px = int(br2.left()); px <= int(br2.right()); ++px ) { QPointF p(px, py); QPointF start(0,0); if ( !GeometryUtils::pointInTriangle(p, tri2Dst) ) continue; QPointF srcP = GeometryUtils::barycentric(p, tri2Dst, tri2, &count, start ); if ( !originalImage.rect().contains(srcP.toPoint()) ) continue; QColor c = originalImage.pixelColor(int(srcP.x()), int(srcP.y())); warped.setPixelColor(px, py, c); } } } } } return { warped, QPointF(0,0) }; } } // --------------------- helper --------------------- void drawTriangle( QPainter *painter, const QImage &src, QPointF s1, QPointF s2, QPointF s3, QPointF t1, QPointF t2, QPointF t3 ) { QPolygonF sourcePoly; sourcePoly << s1 << s2 << s3; QPolygonF targetPoly; targetPoly << t1 << t2 << t3; // Berechne die affine Transformation von Source-Dreieck zu Target-Dreieck QTransform trans; if (QTransform::quadToQuad(sourcePoly, targetPoly, trans)) { painter->save(); painter->setTransform(trans, true); // Clipping auf das Zieldreieck, um ?berlappungen zu vermeiden QPainterPath path; path.addPolygon(sourcePoly); painter->setClipPath(path); painter->drawImage(0, 0, src); painter->restore(); } } // --- OLD CODE --- WarpResult warp2( const QImage& originalImage, const CageMesh& mesh ) { if ( mesh.pointCount() < 4 || !mesh.isActive() ) return { QImage(), QPointF(0,0) }; { // --- prepare --- QVector sourceGrid; QVector targetGrid = mesh.points(); int resX = mesh.rows(); int resY = mesh.cols(); for ( int y = 0; y < resX; ++y ) { for ( int x = 0; x < resY; ++x ) { sourceGrid << QPointF(x * originalImage.width() / (resY-1), y * originalImage.height() / (resX-1)); } } // --- process --- float minX = targetGrid[0].x(), maxX = targetGrid[0].x(); float minY = targetGrid[0].y(), maxY = targetGrid[0].y(); for (const QPointF &p : targetGrid) { if (p.x() < minX) minX = p.x(); if (p.x() > maxX) maxX = p.x(); if (p.y() < minY) minY = p.y(); if (p.y() > maxY) maxY = p.y(); } QSize newSize(qCeil(maxX - minX) + 2, qCeil(maxY - minY) + 2); QPointF offset(-minX + 1, -minY + 1); QImage result(newSize, QImage::Format_ARGB32_Premultiplied); result.fill(Qt::transparent); QPainter painter(&result); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.translate(offset); for (int y = 0; y < resY - 1; ++y) { for (int x = 0; x < resX - 1; ++x) { int idx0 = y * resX + x; int idx1 = y * resX + (x + 1); int idx2 = (y + 1) * resX + x; int idx3 = (y + 1) * resX + (x + 1); drawTriangle(&painter, originalImage, sourceGrid[idx0], sourceGrid[idx1], sourceGrid[idx2], targetGrid[idx0], targetGrid[idx1], targetGrid[idx2]); drawTriangle(&painter, originalImage, sourceGrid[idx1], sourceGrid[idx2], sourceGrid[idx3], targetGrid[idx1], targetGrid[idx2], targetGrid[idx3]); } } return { result, -offset }; } } }