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(); 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, bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth, int maxOutputHeight,
QImage* out) { QImage* out) {
if (!out || absolutePath.isEmpty() || maxOutputWidth < 32 || maxOutputHeight < 32) { if (!out || absolutePath.isEmpty() || maxOutputWidth < 32 || maxOutputHeight < 32) {
+5
View File
@@ -16,6 +16,11 @@ namespace image_file {
/// 统一大图解码管线:libvips(可用)按像素预算缩小后再解码为 QImage,否则 Qt。 /// 统一大图解码管线:libvips(可用)按像素预算缩小后再解码为 QImage,否则 Qt。
[[nodiscard]] QImage loadImageLimited(const QString& absolutePath, qint64 maxPixelBudget); [[nodiscard]] QImage loadImageLimited(const QString& absolutePath, qint64 maxPixelBudget);
/// 加载深度图并按背景图的逻辑像素尺寸对齐(与 probeImagePixelSize 一致)。
/// 解决「背景走 vips 元数据为全尺寸、深度仍按像素预算降采样」时世界坐标采样错位。
[[nodiscard]] QImage loadDepthMapAlignedToBackground(const QString& depthAbsolutePath,
const QString& backgroundAbsolutePath);
/// 仅解码源图中矩形区域(逻辑像素坐标);超过 maxOutput* 则缩放该区域。 /// 仅解码源图中矩形区域(逻辑像素坐标);超过 maxOutput* 则缩放该区域。
/// libvips 可用时走随机访问裁剪;否则整图降采样回退后再拷贝矩形。 /// libvips 可用时走随机访问裁剪;否则整图降采样回退后再拷贝矩形。
[[nodiscard]] bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth, [[nodiscard]] bool loadRegionToQImage(const QString& absolutePath, const QRect& rectLogical, int maxOutputWidth,
+41 -17
View File
@@ -34,6 +34,7 @@
#include <QSet> #include <QSet>
#include <QPainter> #include <QPainter>
#include <QPainterPath> #include <QPainterPath>
#include <QTransform>
#include <QPolygonF> #include <QPolygonF>
#include <QRect> #include <QRect>
@@ -2275,41 +2276,64 @@ bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const
} }
const QImage srcSnapshot = bg; const QImage srcSnapshot = bg;
QPainterPath holePath; QSize logicalSz;
holePath.addPolygon(QPolygonF(ent.cutoutPolygonWorld)); if (!image_file::probeImagePixelSize(bgAbs, &logicalSz) || !logicalSz.isValid() || logicalSz.width() < 1 ||
holePath.closeSubpath(); logicalSz.height() < 1) {
const QRect targetRect = holePath.boundingRect().toAlignedRect().intersected(QRect(QPoint(0, 0), bg.size()));
if (!targetRect.isValid() || targetRect.width() <= 0 || targetRect.height() <= 0) {
return false; return false;
} }
QRect srcRect(targetRect.topLeft() + sourceOffsetPx, targetRect.size()); QPainterPath holePathWorld;
if (srcRect.left() < 0) srcRect.moveLeft(0); holePathWorld.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
if (srcRect.top() < 0) srcRect.moveTop(0); holePathWorld.closeSubpath();
if (srcRect.right() >= bg.width()) srcRect.moveRight(bg.width() - 1); const QRect targetWorld =
if (srcRect.bottom() >= bg.height()) srcRect.moveBottom(bg.height() - 1); holePathWorld.boundingRect().toAlignedRect().intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
srcRect = srcRect.intersected(QRect(QPoint(0, 0), bg.size())); if (!targetWorld.isValid() || targetWorld.width() <= 0 || targetWorld.height() <= 0) {
if (srcRect.width() != targetRect.width() || srcRect.height() != targetRect.height()) {
return false; 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); QPainter p(&bg);
p.setRenderHint(QPainter::Antialiasing, true); p.setRenderHint(QPainter::Antialiasing, true);
p.setClipPath(holePath); p.setClipPath(holePathImg);
p.drawImage(targetRect.topLeft(), srcSnapshot, srcRect); p.drawImage(targetImg.topLeft(), srcSnapshot, srcImg);
p.end(); p.end();
} }
const QImage patch = bg.copy(targetRect); const QImage patch = bg.copy(targetImg);
const auto before = m_project.entities(); const auto before = m_project.entities();
QString overlayRel; QString overlayRel;
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetRect, if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetWorld,
&overlayRel)) { &overlayRel)) {
return false; return false;
} }
ents[hit].blackholeOverlayPath = overlayRel; ents[hit].blackholeOverlayPath = overlayRel;
ents[hit].blackholeOverlayRect = targetRect; ents[hit].blackholeOverlayRect = targetWorld;
ents[hit].blackholeVisible = hideBlackholeAfterFill ? false : ents[hit].blackholeVisible; ents[hit].blackholeVisible = hideBlackholeAfterFill ? false : ents[hit].blackholeVisible;
if (ents[hit].blackholeId.isEmpty()) { if (ents[hit].blackholeId.isEmpty()) {
ents[hit].blackholeId = QStringLiteral("blackhole-%1").arg(ents[hit].id); ents[hit].blackholeId = QStringLiteral("blackhole-%1").arg(ents[hit].id);
+10 -1
View File
@@ -25,6 +25,14 @@ QPushButton* makeAlgoButton(const QString& title, QWidget* parent) {
} // namespace } // namespace
QString BlackholeResolveDialog::defaultModelInpaintPrompt() {
return QStringLiteral(
"中国画局部,需去除人物并只补背景:请严格依据掩膜外已见的笔墨皴擦、设色层次、留白与纸绢肌理,"
"在掩膜内自然延展,补全为山石、坡岸、云水、苔点或空灵留白等,与外缘笔法、墨色与气韵连贯衔接;"
"禁止出现人物轮廓、肢体、衣褶、五官、手足、发丝、肤色、投影及去人后的模糊晕带、鬼影边缘与色块台阶,"
"整体须像原画从未画入人物一样自然,不留修补感。");
}
BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWidget* parent) BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWidget* parent)
: QDialog(parent), : QDialog(parent),
m_blackholeName(blackholeName) { m_blackholeName(blackholeName) {
@@ -134,7 +142,8 @@ void BlackholeResolveDialog::buildDetailPage() {
pLay->setSpacing(8); pLay->setSpacing(8);
m_promptEdit = new QPlainTextEdit(panel); m_promptEdit = new QPlainTextEdit(panel);
m_promptEdit->setPlaceholderText(QStringLiteral("提示词(可选)")); m_promptEdit->setPlainText(BlackholeResolveDialog::defaultModelInpaintPrompt());
m_promptEdit->setPlaceholderText(QStringLiteral("可按画面增删;若全部删空,请求时仍使用内置默认描述"));
m_promptEdit->setMinimumHeight(72); m_promptEdit->setMinimumHeight(72);
pLay->addWidget(m_promptEdit); pLay->addWidget(m_promptEdit);
@@ -19,6 +19,9 @@ public:
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; } Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
QString promptText() const; QString promptText() const;
/// 模型补全(LaMa 等)内置正向提示:中国画去人物、补背景纹理,无人物残留。
[[nodiscard]] static QString defaultModelInpaintPrompt();
private: private:
void buildSelectPage(); void buildSelectPage();
void buildDetailPage(); void buildDetailPage();
+12 -6
View File
@@ -20,7 +20,7 @@ static QLabel* makeImageLabel(QWidget* parent) {
static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) { static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) {
auto* sc = new QScrollArea(parent); auto* sc = new QScrollArea(parent);
sc->setWidget(child); sc->setWidget(child);
sc->setWidgetResizable(true); sc->setWidgetResizable(false);
sc->setBackgroundRole(QPalette::Dark); sc->setBackgroundRole(QPalette::Dark);
return sc; return sc;
} }
@@ -28,7 +28,7 @@ static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) {
InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent) InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent)
: QDialog(parent) { : QDialog(parent) {
setModal(true); setModal(true);
setMinimumSize(720, 480); setMinimumSize(880, 520);
setWindowTitle(title); setWindowTitle(title);
auto* root = new QVBoxLayout(this); auto* root = new QVBoxLayout(this);
@@ -39,6 +39,8 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent
m_afterLabel = makeImageLabel(this); m_afterLabel = makeImageLabel(this);
m_beforeScroll = wrapScroll(m_beforeLabel, this); m_beforeScroll = wrapScroll(m_beforeLabel, this);
m_afterScroll = wrapScroll(m_afterLabel, this); m_afterScroll = wrapScroll(m_afterLabel, this);
m_beforeScroll->setMinimumHeight(200);
m_afterScroll->setMinimumHeight(200);
auto* splitter = new QSplitter(Qt::Horizontal, this); auto* splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(m_beforeScroll); splitter->addWidget(m_beforeScroll);
@@ -57,12 +59,16 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent
void InpaintPreviewDialog::setImages(const QImage& before, const QImage& after) { void InpaintPreviewDialog::setImages(const QImage& before, const QImage& after) {
if (m_beforeLabel) { if (m_beforeLabel) {
m_beforeLabel->setPixmap(QPixmap::fromImage(before)); const QPixmap pb = QPixmap::fromImage(before);
m_beforeLabel->adjustSize(); m_beforeLabel->setPixmap(pb);
m_beforeLabel->setMinimumSize(pb.size());
m_beforeLabel->resize(pb.size());
} }
if (m_afterLabel) { if (m_afterLabel) {
m_afterLabel->setPixmap(QPixmap::fromImage(after)); const QPixmap pa = QPixmap::fromImage(after);
m_afterLabel->adjustSize(); m_afterLabel->setPixmap(pa);
m_afterLabel->setMinimumSize(pa.size());
m_afterLabel->resize(pa.size());
} }
} }
+6 -12
View File
@@ -697,8 +697,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities,
if (!m_depthAbsPath.isEmpty()) { if (!m_depthAbsPath.isEmpty()) {
if (m_depthDirty) { if (m_depthDirty) {
m_depthDirty = false; m_depthDirty = false;
QImage img = readImageTolerant(m_depthAbsPath); m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} }
} }
@@ -861,8 +860,7 @@ void EditorCanvas::setTools(const QVector<core::Project::Tool>& tools, const QVe
if (!m_depthAbsPath.isEmpty()) { if (!m_depthAbsPath.isEmpty()) {
if (m_depthDirty) { if (m_depthDirty) {
m_depthDirty = false; m_depthDirty = false;
QImage img = readImageTolerant(m_depthAbsPath); m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} }
} }
const qsizetype n = tools.size(); const qsizetype n = tools.size();
@@ -1915,8 +1913,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
if (wantDepth) { if (wantDepth) {
if (m_depthDirty) { if (m_depthDirty) {
m_depthDirty = false; m_depthDirty = false;
QImage img = readImageTolerant(m_depthAbsPath); m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} }
if (!m_depthImage8.isNull()) { if (!m_depthImage8.isNull()) {
const int overlayAlpha = m_backgroundVisible ? m_depthOverlayAlpha : 255; const int overlayAlpha = m_backgroundVisible ? m_depthOverlayAlpha : 255;
@@ -2457,8 +2454,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
if (!m_depthAbsPath.isEmpty()) { if (!m_depthAbsPath.isEmpty()) {
if (m_depthDirty) { if (m_depthDirty) {
m_depthDirty = false; m_depthDirty = false;
QImage img = readImageTolerant(m_depthAbsPath); m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} }
if (!m_depthImage8.isNull()) { if (!m_depthImage8.isNull()) {
const int xi = static_cast<int>(std::floor(wp0.x())); const int xi = static_cast<int>(std::floor(wp0.x()));
@@ -2892,8 +2888,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
if (!m_depthAbsPath.isEmpty()) { if (!m_depthAbsPath.isEmpty()) {
if (m_depthDirty) { if (m_depthDirty) {
m_depthDirty = false; m_depthDirty = false;
QImage img = readImageTolerant(m_depthAbsPath); m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} }
if (!m_depthImage8.isNull()) { if (!m_depthImage8.isNull()) {
const int xi = static_cast<int>(std::floor(wp.x())); const int xi = static_cast<int>(std::floor(wp.x()));
@@ -3053,8 +3048,7 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
if (!m_depthAbsPath.isEmpty() && m_depthDirty) { if (!m_depthAbsPath.isEmpty() && m_depthDirty) {
m_depthDirty = false; m_depthDirty = false;
QImage img = readImageTolerant(m_depthAbsPath); m_depthImage8 = core::image_file::loadDepthMapAlignedToBackground(m_depthAbsPath, m_bgAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} }
if (!m_depthImage8.isNull()) { if (!m_depthImage8.isNull()) {
const QPointF c = const QPointF c =
+118 -38
View File
@@ -75,6 +75,7 @@
#include <QFontMetrics> #include <QFontMetrics>
#include <QHeaderView> #include <QHeaderView>
#include <QImage> #include <QImage>
#include <QTransform>
#include <QEventLoop> #include <QEventLoop>
#include <QMessageBox> #include <QMessageBox>
#include <QPixmap> #include <QPixmap>
@@ -3401,7 +3402,26 @@ void MainWindow::rebuildCentralPages() {
polishCompactToolButton(m_previewBtnPlay, 36); polishCompactToolButton(m_previewBtnPlay, 36);
pbl->addWidget(m_previewBtnPlay); pbl->addWidget(m_previewBtnPlay);
m_previewPlaybackBar->setParent(canvasHost); 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<int>::of(&QComboBox::currentIndexChanged), this, [this](int index) {
if (index == 0) {
m_animationRequested = false;
m_hotspotRequested = false;
setPreviewRequested(false);
if (m_workspace.isOpen() && if (m_workspace.isOpen() &&
m_workspace.project().activeAnimationId() != QStringLiteral("none")) { m_workspace.project().activeAnimationId() != QStringLiteral("none")) {
m_workspace.project().setActiveAnimationId(QStringLiteral("none")); m_workspace.project().setActiveAnimationId(QStringLiteral("none"));
@@ -3854,15 +3874,12 @@ cod setPreviewRequested(false);
ent.polygonLocal.push_back(pt - ent.originWorld); ent.polygonLocal.push_back(pt - ent.originWorld);
} }
const QString bgAbs = m_workspace.backgroundAbsolutePath();
QImage depth8; QImage depth8;
if (m_workspace.hasDepth()) { if (m_workspace.hasDepth()) {
const QString dpath = m_workspace.depthAbsolutePath(); const QString dpath = m_workspace.depthAbsolutePath();
if (!dpath.isEmpty() && QFileInfo::exists(dpath)) { if (!dpath.isEmpty() && QFileInfo::exists(dpath) && !bgAbs.isEmpty()) {
const QImage dimg = depth8 = core::image_file::loadDepthMapAlignedToBackground(dpath, bgAbs);
core::image_file::loadImageLimited(dpath, core::image_decode::kWorkspaceMaxPixels);
if (!dimg.isNull()) {
depth8 = dimg.convertToFormat(QImage::Format_Grayscale8);
}
} }
} }
const QPointF c = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld); const QPointF c = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
@@ -3879,8 +3896,6 @@ cod setPreviewRequested(false);
const double ds01 = static_cast<double>(std::clamp(z, 0, 255)) / 255.0; const double ds01 = static_cast<double>(std::clamp(z, 0, 255)) / 255.0;
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0; ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
} }
const QString bgAbs = m_workspace.backgroundAbsolutePath();
QImage bg = QImage bg =
core::image_file::loadImageLimited(bgAbs, core::image_decode::kWorkspaceMaxPixels); core::image_file::loadImageLimited(bgAbs, core::image_decode::kWorkspaceMaxPixels);
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) { if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
@@ -3925,10 +3940,9 @@ cod setPreviewRequested(false);
int z = 0; int z = 0;
if (m_workspace.hasDepth()) { if (m_workspace.hasDepth()) {
const QString dpath = m_workspace.depthAbsolutePath(); const QString dpath = m_workspace.depthAbsolutePath();
QImage depth8 = const QString bgAbs = m_workspace.backgroundAbsolutePath();
core::image_file::loadImageLimited(dpath, core::image_decode::kWorkspaceMaxPixels); const QImage depth8 = core::image_file::loadDepthMapAlignedToBackground(dpath, bgAbs);
if (!depth8.isNull()) { if (!depth8.isNull()) {
depth8 = depth8.convertToFormat(QImage::Format_Grayscale8);
const QPointF c = entity_cutout::polygonCentroid(polyWorld); const QPointF c = entity_cutout::polygonCentroid(polyWorld);
const int xi = static_cast<int>(std::floor(c.x())); const int xi = static_cast<int>(std::floor(c.x()));
const int yi = static_cast<int>(std::floor(c.y())); const int yi = static_cast<int>(std::floor(c.y()));
@@ -4797,14 +4811,12 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("背景文件无效。")); QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("背景文件无效。"));
return; return;
} }
QImage bg = core::image_file::loadImageLimited(bgAbs, core::image_decode::kWorkspaceMaxPixels); QSize bgLogical;
if (bg.isNull()) { if (!core::image_file::probeImagePixelSize(bgAbs, &bgLogical) || !bgLogical.isValid() ||
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("读取背景失败。")); bgLogical.width() < 1 || bgLogical.height() < 1) {
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("无法读取背景尺寸。"));
return; return;
} }
if (bg.format() != QImage::Format_ARGB32_Premultiplied) {
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
QVector<QPointF> holePolyWorld; QVector<QPointF> holePolyWorld;
for (const auto& e : m_workspace.entities()) { 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 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))); const int marginY = std::max(kMinMargin, int(std::round((holeRect.height() * (kExpandLinear - 1.0)) / 2.0)));
QRect cropRect = holeRect.adjusted(-marginX, -marginY, marginX, marginY); 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) { if (!cropRect.isValid() || cropRect.width() <= 1 || cropRect.height() <= 1) {
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("裁剪区域无效。")); QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("裁剪区域无效。"));
return; return;
} }
const QImage cropRgb = bg.copy(cropRect).convertToFormat(QImage::Format_RGB888); const int maxDec = std::clamp(std::max(cropRect.width(), cropRect.height()), 512, 4096);
QImage mask(cropRect.size(), QImage::Format_Grayscale8); 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); mask.fill(0);
{ {
QPainter pm(&mask); QPainter pm(&mask);
pm.setRenderHint(QPainter::Antialiasing, true); 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.fillPath(holePath, QColor(255, 255, 255));
pm.end(); pm.end();
} }
@@ -5025,15 +5050,20 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
client->setBaseUrl(QUrl(base)); client->setBaseUrl(QUrl(base));
QString immediateErr; QString immediateErr;
const QString prompt = dlg.promptText(); QString prompt = dlg.promptText();
if (prompt.isEmpty()) {
prompt = BlackholeResolveDialog::defaultModelInpaintPrompt();
}
QNetworkReply* reply = client->inpaintAsync( QNetworkReply* reply = client->inpaintAsync(
cropPng, cropPng,
maskPng, maskPng,
// LaMa(物体移除 / 擦除) // LaMa(物体移除 / 擦除)
QStringLiteral("lama"), QStringLiteral("lama"),
prompt, 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 太低容易“保留人物残影”;适当提高更干净 // 去人物场景:strength 太低容易“保留人物残影”;适当提高更干净
0.72, 0.72,
1024, 1024,
@@ -5056,7 +5086,8 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
}); });
connect(reply, &QNetworkReply::finished, this, 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 int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
const QByteArray raw = reply->readAll(); const QByteArray raw = reply->readAll();
const auto netErr = reply->error(); 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); QImage before = cropForInpaint.convertToFormat(QImage::Format_RGB888);
{ {
const QImage m8 = mask.convertToFormat(QImage::Format_Grayscale8); QPainter pb(&before);
for (int y = 0; y < before.height(); ++y) { pb.setRenderHint(QPainter::Antialiasing, true);
const uchar* mr = m8.constScanLine(y); QTransform xf;
uchar* br = before.scanLine(y); xf.translate(-cropRect.left(), -cropRect.top());
for (int x = 0; x < before.width(); ++x) { xf.scale(before.width() / double(cropRect.width()),
if (mr[x] == 0) continue; before.height() / double(cropRect.height()));
const int idx = x * 3; pb.setWorldTransform(xf);
br[idx + 0] = 0; for (const auto& e : m_workspace.entities()) {
br[idx + 1] = 0; if (e.id == entityId) {
br[idx + 2] = 0; 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<qreal>(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<qreal>(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); InpaintPreviewDialog preview(QStringLiteral("预览"), this);
preview.setImages(before, after); preview.setImages(before, afterView);
if (preview.exec() != QDialog::Accepted) { if (preview.exec() != QDialog::Accepted) {
return; return;
} }
if (after.size() != cropRect.size()) {
after = after.scaled(cropRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
const bool ok2 = const bool ok2 =
m_workspace.resolveBlackholeByModelInpaint(entityId, after, cropRect, true); m_workspace.resolveBlackholeByModelInpaint(entityId, after, cropRect, true);
if (!ok2) { if (!ok2) {
+2 -1
View File
@@ -24,9 +24,10 @@ def depth(req: DepthRequest):
""" """
try: try:
pil = b64_to_pil_image(req.image_b64).convert("RGB") pil = b64_to_pil_image(req.image_b64).convert("RGB")
w, h = pil.size
predictor = get_depth_predictor() predictor = get_depth_predictor()
depth_arr = predictor(pil) # type: ignore[misc] 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") return Response(content=png_bytes, media_type="image/png")
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e raise HTTPException(status_code=500, detail=str(e)) from e
+10 -2
View File
@@ -6,8 +6,16 @@ import numpy as np
from PIL import Image from PIL import Image
def depth_to_png16_bytes(depth: np.ndarray) -> bytes: def depth_to_png16_bytes(depth: np.ndarray, target_hw: tuple[int, int] | None = None) -> bytes:
depth = np.asarray(depth, dtype=np.float32) 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()) dmin = float(depth.min())
dmax = float(depth.max()) dmax = float(depth.max())
if dmax > dmin: if dmax > dmin: