This commit is contained in:
2026-05-14 17:30:20 +08:00
parent e43171521d
commit 5e6d8046e1
10 changed files with 224 additions and 77 deletions
+17
View File
@@ -38,6 +38,23 @@ QImage loadImageLimited(const QString& absolutePath, qint64 maxPixelBudget) {
return reader.read();
}
QImage loadDepthMapAlignedToBackground(const QString& depthAbsolutePath, const QString& backgroundAbsolutePath) {
if (depthAbsolutePath.isEmpty()) {
return {};
}
QImage g8 = loadImageLimited(depthAbsolutePath, core::image_decode::kWorkspaceMaxPixels);
if (g8.isNull()) {
return {};
}
g8 = g8.convertToFormat(QImage::Format_Grayscale8);
QSize logical;
if (!backgroundAbsolutePath.isEmpty() && probeImagePixelSize(backgroundAbsolutePath, &logical) &&
logical.isValid() && logical.width() > 0 && logical.height() > 0 && g8.size() != logical) {
g8 = g8.scaled(logical, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
return g8;
}
bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth, int maxOutputHeight,
QImage* out) {
if (!out || absolutePath.isEmpty() || maxOutputWidth < 32 || maxOutputHeight < 32) {
+5
View File
@@ -16,6 +16,11 @@ namespace image_file {
/// 统一大图解码管线:libvips(可用)按像素预算缩小后再解码为 QImage,否则 Qt。
[[nodiscard]] QImage loadImageLimited(const QString& absolutePath, qint64 maxPixelBudget);
/// 加载深度图并按背景图的逻辑像素尺寸对齐(与 probeImagePixelSize 一致)。
/// 解决「背景走 vips 元数据为全尺寸、深度仍按像素预算降采样」时世界坐标采样错位。
[[nodiscard]] QImage loadDepthMapAlignedToBackground(const QString& depthAbsolutePath,
const QString& backgroundAbsolutePath);
/// 仅解码源图中矩形区域(逻辑像素坐标);超过 maxOutput* 则缩放该区域。
/// libvips 可用时走随机访问裁剪;否则整图降采样回退后再拷贝矩形。
[[nodiscard]] bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth,
+41 -17
View File
@@ -34,6 +34,7 @@
#include <QSet>
#include <QPainter>
#include <QPainterPath>
#include <QTransform>
#include <QPolygonF>
#include <QRect>
@@ -2275,41 +2276,64 @@ bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const
}
const QImage srcSnapshot = bg;
QPainterPath holePath;
holePath.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
holePath.closeSubpath();
const QRect targetRect = holePath.boundingRect().toAlignedRect().intersected(QRect(QPoint(0, 0), bg.size()));
if (!targetRect.isValid() || targetRect.width() <= 0 || targetRect.height() <= 0) {
QSize logicalSz;
if (!image_file::probeImagePixelSize(bgAbs, &logicalSz) || !logicalSz.isValid() || logicalSz.width() < 1 ||
logicalSz.height() < 1) {
return false;
}
QRect srcRect(targetRect.topLeft() + sourceOffsetPx, targetRect.size());
if (srcRect.left() < 0) srcRect.moveLeft(0);
if (srcRect.top() < 0) srcRect.moveTop(0);
if (srcRect.right() >= bg.width()) srcRect.moveRight(bg.width() - 1);
if (srcRect.bottom() >= bg.height()) srcRect.moveBottom(bg.height() - 1);
srcRect = srcRect.intersected(QRect(QPoint(0, 0), bg.size()));
if (srcRect.width() != targetRect.width() || srcRect.height() != targetRect.height()) {
QPainterPath holePathWorld;
holePathWorld.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
holePathWorld.closeSubpath();
const QRect targetWorld =
holePathWorld.boundingRect().toAlignedRect().intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
if (!targetWorld.isValid() || targetWorld.width() <= 0 || targetWorld.height() <= 0) {
return false;
}
QRect srcWorld(targetWorld.topLeft() + sourceOffsetPx, targetWorld.size());
if (srcWorld.left() < 0) srcWorld.moveLeft(0);
if (srcWorld.top() < 0) srcWorld.moveTop(0);
if (srcWorld.right() >= logicalSz.width()) srcWorld.moveRight(logicalSz.width() - 1);
if (srcWorld.bottom() >= logicalSz.height()) srcWorld.moveBottom(logicalSz.height() - 1);
srcWorld = srcWorld.intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
if (srcWorld.width() != targetWorld.width() || srcWorld.height() != targetWorld.height()) {
return false;
}
const qreal sx = bg.width() / qreal(logicalSz.width());
const qreal sy = bg.height() / qreal(logicalSz.height());
QTransform worldToBg;
worldToBg.scale(sx, sy);
const QRect targetImg =
worldToBg.mapRect(QRectF(targetWorld)).toAlignedRect().intersected(QRect(0, 0, bg.width(), bg.height()));
const QRect srcImg =
worldToBg.mapRect(QRectF(srcWorld)).toAlignedRect().intersected(QRect(0, 0, bg.width(), bg.height()));
if (!targetImg.isValid() || !srcImg.isValid() || targetImg.width() != srcImg.width() ||
targetImg.height() != srcImg.height()) {
return false;
}
const QPainterPath holePathImg = worldToBg.map(holePathWorld);
{
QPainter p(&bg);
p.setRenderHint(QPainter::Antialiasing, true);
p.setClipPath(holePath);
p.drawImage(targetRect.topLeft(), srcSnapshot, srcRect);
p.setClipPath(holePathImg);
p.drawImage(targetImg.topLeft(), srcSnapshot, srcImg);
p.end();
}
const QImage patch = bg.copy(targetRect);
const QImage patch = bg.copy(targetImg);
const auto before = m_project.entities();
QString overlayRel;
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetRect,
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetWorld,
&overlayRel)) {
return false;
}
ents[hit].blackholeOverlayPath = overlayRel;
ents[hit].blackholeOverlayRect = targetRect;
ents[hit].blackholeOverlayRect = targetWorld;
ents[hit].blackholeVisible = hideBlackholeAfterFill ? false : ents[hit].blackholeVisible;
if (ents[hit].blackholeId.isEmpty()) {
ents[hit].blackholeId = QStringLiteral("blackhole-%1").arg(ents[hit].id);