From 5e6d8046e14dbcc2233400c78e1a73e403ee52dd Mon Sep 17 00:00:00 2001 From: DingVero Date: Thu, 14 May 2026 17:30:20 +0800 Subject: [PATCH] update --- client/core/image/ImageFileLoader.cpp | 17 ++ client/core/image/ImageFileLoader.h | 5 + client/core/workspace/ProjectWorkspace.cpp | 58 +++++-- client/gui/dialogs/BlackholeResolveDialog.cpp | 11 +- client/gui/dialogs/BlackholeResolveDialog.h | 3 + client/gui/dialogs/InpaintPreviewDialog.cpp | 18 +- client/gui/editor/EditorCanvas.cpp | 18 +- client/gui/main_window/MainWindow.cpp | 156 +++++++++++++----- python_server/app/routes/depth.py | 3 +- python_server/app/services/depth_io.py | 12 +- 10 files changed, 224 insertions(+), 77 deletions(-) diff --git a/client/core/image/ImageFileLoader.cpp b/client/core/image/ImageFileLoader.cpp index e611cfa..3c115b5 100644 --- a/client/core/image/ImageFileLoader.cpp +++ b/client/core/image/ImageFileLoader.cpp @@ -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) { diff --git a/client/core/image/ImageFileLoader.h b/client/core/image/ImageFileLoader.h index 1b51b81..06abe24 100644 --- a/client/core/image/ImageFileLoader.h +++ b/client/core/image/ImageFileLoader.h @@ -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, diff --git a/client/core/workspace/ProjectWorkspace.cpp b/client/core/workspace/ProjectWorkspace.cpp index e1aeb9c..0dc986a 100644 --- a/client/core/workspace/ProjectWorkspace.cpp +++ b/client/core/workspace/ProjectWorkspace.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -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); diff --git a/client/gui/dialogs/BlackholeResolveDialog.cpp b/client/gui/dialogs/BlackholeResolveDialog.cpp index 0daf3d7..1b44249 100644 --- a/client/gui/dialogs/BlackholeResolveDialog.cpp +++ b/client/gui/dialogs/BlackholeResolveDialog.cpp @@ -25,6 +25,14 @@ QPushButton* makeAlgoButton(const QString& title, QWidget* parent) { } // namespace +QString BlackholeResolveDialog::defaultModelInpaintPrompt() { + return QStringLiteral( + "中国画局部,需去除人物并只补背景:请严格依据掩膜外已见的笔墨皴擦、设色层次、留白与纸绢肌理," + "在掩膜内自然延展,补全为山石、坡岸、云水、苔点或空灵留白等,与外缘笔法、墨色与气韵连贯衔接;" + "禁止出现人物轮廓、肢体、衣褶、五官、手足、发丝、肤色、投影及去人后的模糊晕带、鬼影边缘与色块台阶," + "整体须像原画从未画入人物一样自然,不留修补感。"); +} + BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWidget* parent) : QDialog(parent), m_blackholeName(blackholeName) { @@ -134,7 +142,8 @@ void BlackholeResolveDialog::buildDetailPage() { pLay->setSpacing(8); m_promptEdit = new QPlainTextEdit(panel); - m_promptEdit->setPlaceholderText(QStringLiteral("提示词(可选)")); + m_promptEdit->setPlainText(BlackholeResolveDialog::defaultModelInpaintPrompt()); + m_promptEdit->setPlaceholderText(QStringLiteral("可按画面增删;若全部删空,请求时仍使用内置默认描述")); m_promptEdit->setMinimumHeight(72); pLay->addWidget(m_promptEdit); diff --git a/client/gui/dialogs/BlackholeResolveDialog.h b/client/gui/dialogs/BlackholeResolveDialog.h index 772c8d2..5b202af 100644 --- a/client/gui/dialogs/BlackholeResolveDialog.h +++ b/client/gui/dialogs/BlackholeResolveDialog.h @@ -19,6 +19,9 @@ public: Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; } QString promptText() const; + /// 模型补全(LaMa 等)内置正向提示:中国画去人物、补背景纹理,无人物残留。 + [[nodiscard]] static QString defaultModelInpaintPrompt(); + private: void buildSelectPage(); void buildDetailPage(); diff --git a/client/gui/dialogs/InpaintPreviewDialog.cpp b/client/gui/dialogs/InpaintPreviewDialog.cpp index bb8906d..2af2d3a 100644 --- a/client/gui/dialogs/InpaintPreviewDialog.cpp +++ b/client/gui/dialogs/InpaintPreviewDialog.cpp @@ -20,7 +20,7 @@ static QLabel* makeImageLabel(QWidget* parent) { static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) { auto* sc = new QScrollArea(parent); sc->setWidget(child); - sc->setWidgetResizable(true); + sc->setWidgetResizable(false); sc->setBackgroundRole(QPalette::Dark); return sc; } @@ -28,7 +28,7 @@ static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) { InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent) : QDialog(parent) { setModal(true); - setMinimumSize(720, 480); + setMinimumSize(880, 520); setWindowTitle(title); auto* root = new QVBoxLayout(this); @@ -39,6 +39,8 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent m_afterLabel = makeImageLabel(this); m_beforeScroll = wrapScroll(m_beforeLabel, this); m_afterScroll = wrapScroll(m_afterLabel, this); + m_beforeScroll->setMinimumHeight(200); + m_afterScroll->setMinimumHeight(200); auto* splitter = new QSplitter(Qt::Horizontal, this); splitter->addWidget(m_beforeScroll); @@ -57,12 +59,16 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent void InpaintPreviewDialog::setImages(const QImage& before, const QImage& after) { if (m_beforeLabel) { - m_beforeLabel->setPixmap(QPixmap::fromImage(before)); - m_beforeLabel->adjustSize(); + const QPixmap pb = QPixmap::fromImage(before); + m_beforeLabel->setPixmap(pb); + m_beforeLabel->setMinimumSize(pb.size()); + m_beforeLabel->resize(pb.size()); } if (m_afterLabel) { - m_afterLabel->setPixmap(QPixmap::fromImage(after)); - m_afterLabel->adjustSize(); + const QPixmap pa = QPixmap::fromImage(after); + m_afterLabel->setPixmap(pa); + m_afterLabel->setMinimumSize(pa.size()); + m_afterLabel->resize(pa.size()); } } diff --git a/client/gui/editor/EditorCanvas.cpp b/client/gui/editor/EditorCanvas.cpp index 85574d6..2f55fa5 100644 --- a/client/gui/editor/EditorCanvas.cpp +++ b/client/gui/editor/EditorCanvas.cpp @@ -697,8 +697,7 @@ void EditorCanvas::setEntities(const QVector& entities, if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; - QImage img = readImageTolerant(m_depthAbsPath); - m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8); + m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } } @@ -861,8 +860,7 @@ void EditorCanvas::setTools(const QVector& tools, const QVe if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; - QImage img = readImageTolerant(m_depthAbsPath); - m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8); + m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } } const qsizetype n = tools.size(); @@ -1915,8 +1913,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) { if (wantDepth) { if (m_depthDirty) { m_depthDirty = false; - QImage img = readImageTolerant(m_depthAbsPath); - m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8); + m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const int overlayAlpha = m_backgroundVisible ? m_depthOverlayAlpha : 255; @@ -2457,8 +2454,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) { if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; - QImage img = readImageTolerant(m_depthAbsPath); - m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8); + m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const int xi = static_cast(std::floor(wp0.x())); @@ -2892,8 +2888,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) { if (!m_depthAbsPath.isEmpty()) { if (m_depthDirty) { m_depthDirty = false; - QImage img = readImageTolerant(m_depthAbsPath); - m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8); + m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const int xi = static_cast(std::floor(wp.x())); @@ -3053,8 +3048,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) { if (!m_depthAbsPath.isEmpty() && m_depthDirty) { m_depthDirty = false; - QImage img = readImageTolerant(m_depthAbsPath); - m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8); + m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath); } if (!m_depthImage8.isNull()) { const QPointF c = diff --git a/client/gui/main_window/MainWindow.cpp b/client/gui/main_window/MainWindow.cpp index 0d50505..a2c7f34 100644 --- a/client/gui/main_window/MainWindow.cpp +++ b/client/gui/main_window/MainWindow.cpp @@ -75,6 +75,7 @@ #include #include #include +#include #include #include #include @@ -3401,7 +3402,26 @@ void MainWindow::rebuildCentralPages() { polishCompactToolButton(m_previewBtnPlay, 36); pbl->addWidget(m_previewBtnPlay); m_previewPlaybackBar->setParent(canvasHost); -cod setPreviewRequested(false); + + m_floatingModeDock = new QFrame(canvasHost); + m_floatingModeDock->setObjectName(QStringLiteral("FloatingModeDock")); + m_floatingModeDock->setFrameShape(QFrame::NoFrame); + m_floatingModeDock->setStyleSheet(QString::fromUtf8(kFloatingModeDockQss)); + auto* modeDockLayout = new QHBoxLayout(m_floatingModeDock); + modeDockLayout->setContentsMargins(4, 4, 4, 4); + modeDockLayout->setSpacing(6); + + m_modeSelector = new QComboBox(m_floatingModeDock); + m_modeSelector->addItem(QStringLiteral("编辑")); + m_modeSelector->addItem(QStringLiteral("动画")); + m_modeSelector->addItem(QStringLiteral("热点")); + m_modeSelector->addItem(QStringLiteral("预览")); + + connect(m_modeSelector, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { + if (index == 0) { + m_animationRequested = false; + m_hotspotRequested = false; + setPreviewRequested(false); if (m_workspace.isOpen() && m_workspace.project().activeAnimationId() != QStringLiteral("none")) { m_workspace.project().setActiveAnimationId(QStringLiteral("none")); @@ -3854,15 +3874,12 @@ cod setPreviewRequested(false); ent.polygonLocal.push_back(pt - ent.originWorld); } + const QString bgAbs = m_workspace.backgroundAbsolutePath(); QImage depth8; if (m_workspace.hasDepth()) { const QString dpath = m_workspace.depthAbsolutePath(); - if (!dpath.isEmpty() && QFileInfo::exists(dpath)) { - const QImage dimg = - core::image_file::loadImageLimited(dpath, core::image_decode::kWorkspaceMaxPixels); - if (!dimg.isNull()) { - depth8 = dimg.convertToFormat(QImage::Format_Grayscale8); - } + if (!dpath.isEmpty() && QFileInfo::exists(dpath) && !bgAbs.isEmpty()) { + depth8 = core::image_file::loadDepthMapAlignedToBackground(dpath, bgAbs); } } const QPointF c = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld); @@ -3879,8 +3896,6 @@ cod setPreviewRequested(false); const double ds01 = static_cast(std::clamp(z, 0, 255)) / 255.0; ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0; } - - const QString bgAbs = m_workspace.backgroundAbsolutePath(); QImage bg = core::image_file::loadImageLimited(bgAbs, core::image_decode::kWorkspaceMaxPixels); if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) { @@ -3925,10 +3940,9 @@ cod setPreviewRequested(false); int z = 0; if (m_workspace.hasDepth()) { const QString dpath = m_workspace.depthAbsolutePath(); - QImage depth8 = - core::image_file::loadImageLimited(dpath, core::image_decode::kWorkspaceMaxPixels); + const QString bgAbs = m_workspace.backgroundAbsolutePath(); + const QImage depth8 = core::image_file::loadDepthMapAlignedToBackground(dpath, bgAbs); if (!depth8.isNull()) { - depth8 = depth8.convertToFormat(QImage::Format_Grayscale8); const QPointF c = entity_cutout::polygonCentroid(polyWorld); const int xi = static_cast(std::floor(c.x())); const int yi = static_cast(std::floor(c.y())); @@ -4797,14 +4811,12 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("背景文件无效。")); return; } - QImage bg = core::image_file::loadImageLimited(bgAbs, core::image_decode::kWorkspaceMaxPixels); - if (bg.isNull()) { - QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("读取背景失败。")); + QSize bgLogical; + if (!core::image_file::probeImagePixelSize(bgAbs, &bgLogical) || !bgLogical.isValid() || + bgLogical.width() < 1 || bgLogical.height() < 1) { + QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("无法读取背景尺寸。")); return; } - if (bg.format() != QImage::Format_ARGB32_Premultiplied) { - bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied); - } QVector holePolyWorld; for (const auto& e : m_workspace.entities()) { @@ -4830,19 +4842,32 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString const int marginX = std::max(kMinMargin, int(std::round((holeRect.width() * (kExpandLinear - 1.0)) / 2.0))); const int marginY = std::max(kMinMargin, int(std::round((holeRect.height() * (kExpandLinear - 1.0)) / 2.0))); QRect cropRect = holeRect.adjusted(-marginX, -marginY, marginX, marginY); - cropRect = cropRect.intersected(QRect(QPoint(0, 0), bg.size())); + cropRect = cropRect.intersected(QRect(QPoint(0, 0), bgLogical)); if (!cropRect.isValid() || cropRect.width() <= 1 || cropRect.height() <= 1) { QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("裁剪区域无效。")); return; } - const QImage cropRgb = bg.copy(cropRect).convertToFormat(QImage::Format_RGB888); - QImage mask(cropRect.size(), QImage::Format_Grayscale8); + const int maxDec = std::clamp(std::max(cropRect.width(), cropRect.height()), 512, 4096); + QImage bg; + if (!core::image_file::loadRegionToQImage(bgAbs, cropRect, maxDec, maxDec, &bg) || bg.isNull()) { + QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("读取背景失败。")); + return; + } + if (bg.format() != QImage::Format_ARGB32_Premultiplied) { + bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied); + } + + const QImage cropRgb = bg.convertToFormat(QImage::Format_RGB888); + QImage mask(bg.size(), QImage::Format_Grayscale8); mask.fill(0); { QPainter pm(&mask); pm.setRenderHint(QPainter::Antialiasing, true); - pm.translate(-cropRect.topLeft()); + QTransform worldToMask; + worldToMask.translate(-cropRect.left(), -cropRect.top()); + worldToMask.scale(bg.width() / double(cropRect.width()), bg.height() / double(cropRect.height())); + pm.setWorldTransform(worldToMask); pm.fillPath(holePath, QColor(255, 255, 255)); pm.end(); } @@ -5025,15 +5050,20 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString client->setBaseUrl(QUrl(base)); QString immediateErr; - const QString prompt = dlg.promptText(); + QString prompt = dlg.promptText(); + if (prompt.isEmpty()) { + prompt = BlackholeResolveDialog::defaultModelInpaintPrompt(); + } QNetworkReply* reply = client->inpaintAsync( cropPng, maskPng, // LaMa(物体移除 / 擦除) QStringLiteral("lama"), prompt, - // 目标是“去掉人物/实体换成背景”,给一个默认 negative prompt 抑制人物结构与杂项 - QStringLiteral("person, human, face, body, hands, text, watermark, signature, logo"), + // 抑制人物与去人常见伪影(中英混合便于通用后端) + QStringLiteral( + "person, human, face, body, hands, feet, portrait, silhouette, skin, clothing folds, hair, " + "shadow, ghosting, blur halo, seam, banding, watermark, text, logo, signature, jpeg artifacts"), // 去人物场景:strength 太低容易“保留人物残影”;适当提高更干净 0.72, 1024, @@ -5056,7 +5086,8 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString }); connect(reply, &QNetworkReply::finished, this, - [this, reply, task, client, entityId, bg, cropRect, cropRgb, mask, cropForInpaint]() mutable { + [this, reply, task, client, entityId, bg, cropRect, cropRgb, mask, cropForInpaint, + holePath]() mutable { const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QByteArray raw = reply->readAll(); const auto netErr = reply->error(); @@ -5183,29 +5214,78 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString } } - // 预览左图:按你的需求,直接把洞区域挖空成纯黑(更直观) + const QRectF cropF(cropRect); + QImage before = cropForInpaint.convertToFormat(QImage::Format_RGB888); { - const QImage m8 = mask.convertToFormat(QImage::Format_Grayscale8); - for (int y = 0; y < before.height(); ++y) { - const uchar* mr = m8.constScanLine(y); - uchar* br = before.scanLine(y); - for (int x = 0; x < before.width(); ++x) { - if (mr[x] == 0) continue; - const int idx = x * 3; - br[idx + 0] = 0; - br[idx + 1] = 0; - br[idx + 2] = 0; + QPainter pb(&before); + pb.setRenderHint(QPainter::Antialiasing, true); + QTransform xf; + xf.translate(-cropRect.left(), -cropRect.top()); + xf.scale(before.width() / double(cropRect.width()), + before.height() / double(cropRect.height())); + pb.setWorldTransform(xf); + for (const auto& e : m_workspace.entities()) { + if (e.id == entityId) { + continue; } + if (!e.blackholeVisible || e.cutoutPolygonWorld.size() < 3) { + continue; + } + const QPainterPath op = entity_cutout::pathFromWorldPolygon(e.cutoutPolygonWorld); + if (!op.boundingRect().intersects(cropF)) { + continue; + } + pb.fillPath(op, QColor(0, 0, 0)); } + pb.fillPath(holePath, QColor(0, 0, 0)); + const qreal w = std::max(1.0, 2.0 / std::min(qAbs(xf.m11()), qAbs(xf.m22()))); + pb.setPen(QPen(QColor(255, 210, 72), w)); + pb.setBrush(Qt::NoBrush); + pb.drawPath(holePath); + pb.end(); + } + + // 修复后:叠画其他黑洞(与画布一致);当前洞仅描边,中间保留模型补全结果。 + QImage afterView = after.convertToFormat(QImage::Format_RGB888); + { + QPainter pa(&afterView); + pa.setRenderHint(QPainter::Antialiasing, true); + QTransform xf; + xf.translate(-cropRect.left(), -cropRect.top()); + xf.scale(afterView.width() / double(cropRect.width()), + afterView.height() / double(cropRect.height())); + pa.setWorldTransform(xf); + for (const auto& e : m_workspace.entities()) { + if (e.id == entityId) { + continue; + } + if (!e.blackholeVisible || e.cutoutPolygonWorld.size() < 3) { + continue; + } + const QPainterPath op = entity_cutout::pathFromWorldPolygon(e.cutoutPolygonWorld); + if (!op.boundingRect().intersects(cropF)) { + continue; + } + pa.fillPath(op, QColor(0, 0, 0)); + } + const qreal w = std::max(1.0, 2.0 / std::min(qAbs(xf.m11()), qAbs(xf.m22()))); + pa.setPen(QPen(QColor(255, 210, 72), w)); + pa.setBrush(Qt::NoBrush); + pa.drawPath(holePath); + pa.end(); } InpaintPreviewDialog preview(QStringLiteral("预览"), this); - preview.setImages(before, after); + preview.setImages(before, afterView); if (preview.exec() != QDialog::Accepted) { return; } + if (after.size() != cropRect.size()) { + after = after.scaled(cropRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + const bool ok2 = m_workspace.resolveBlackholeByModelInpaint(entityId, after, cropRect, true); if (!ok2) { diff --git a/python_server/app/routes/depth.py b/python_server/app/routes/depth.py index 7f288c9..a07e7db 100644 --- a/python_server/app/routes/depth.py +++ b/python_server/app/routes/depth.py @@ -24,9 +24,10 @@ def depth(req: DepthRequest): """ try: pil = b64_to_pil_image(req.image_b64).convert("RGB") + w, h = pil.size predictor = get_depth_predictor() depth_arr = predictor(pil) # type: ignore[misc] - png_bytes = depth_to_png16_bytes(np.asarray(depth_arr)) + png_bytes = depth_to_png16_bytes(np.asarray(depth_arr), target_hw=(h, w)) return Response(content=png_bytes, media_type="image/png") except Exception as e: raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/python_server/app/services/depth_io.py b/python_server/app/services/depth_io.py index fb5d8f6..138c557 100644 --- a/python_server/app/services/depth_io.py +++ b/python_server/app/services/depth_io.py @@ -6,8 +6,16 @@ import numpy as np from PIL import Image -def depth_to_png16_bytes(depth: np.ndarray) -> bytes: - depth = np.asarray(depth, dtype=np.float32) +def depth_to_png16_bytes(depth: np.ndarray, target_hw: tuple[int, int] | None = None) -> bytes: + depth = np.asarray(depth, dtype=np.float32).squeeze() + if depth.ndim != 2: + raise ValueError(f"depth 应为二维数组,实际 shape={depth.shape}") + if target_hw is not None: + th, tw = int(target_hw[0]), int(target_hw[1]) + if th > 0 and tw > 0 and (depth.shape[0] != th or depth.shape[1] != tw): + im = Image.fromarray(depth, mode="F") + im = im.resize((tw, th), Image.Resampling.BILINEAR) + depth = np.asarray(im, dtype=np.float32) dmin = float(depth.min()) dmax = float(depth.max()) if dmax > dmin: