新增模型补全空洞
This commit is contained in:
@@ -87,6 +87,47 @@ QNetworkReply* ModelServerClient::segmentSamPromptAsync(
|
|||||||
return m_nam->post(req, body);
|
return m_nam->post(req, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QNetworkReply* ModelServerClient::inpaintAsync(
|
||||||
|
const QByteArray& cropRgbPngBytes,
|
||||||
|
const QByteArray& maskPngBytes,
|
||||||
|
const QString& prompt,
|
||||||
|
const QString& negativePrompt,
|
||||||
|
double strength,
|
||||||
|
int maxSide,
|
||||||
|
QString* outImmediateError
|
||||||
|
) {
|
||||||
|
if (outImmediateError) {
|
||||||
|
outImmediateError->clear();
|
||||||
|
}
|
||||||
|
if (!m_baseUrl.isValid() || m_baseUrl.isEmpty()) {
|
||||||
|
if (outImmediateError) *outImmediateError = QStringLiteral("后端地址无效。");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (cropRgbPngBytes.isEmpty()) {
|
||||||
|
if (outImmediateError) *outImmediateError = QStringLiteral("裁剪图像为空。");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (maskPngBytes.isEmpty()) {
|
||||||
|
if (outImmediateError) *outImmediateError = QStringLiteral("Mask 为空。");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUrl url = m_baseUrl.resolved(QUrl(QStringLiteral("/inpaint")));
|
||||||
|
QNetworkRequest req(url);
|
||||||
|
req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||||
|
|
||||||
|
QJsonObject payload;
|
||||||
|
payload.insert(QStringLiteral("image_b64"), QString::fromLatin1(cropRgbPngBytes.toBase64()));
|
||||||
|
payload.insert(QStringLiteral("mask_b64"), QString::fromLatin1(maskPngBytes.toBase64()));
|
||||||
|
payload.insert(QStringLiteral("prompt"), prompt);
|
||||||
|
payload.insert(QStringLiteral("negative_prompt"), negativePrompt);
|
||||||
|
payload.insert(QStringLiteral("strength"), strength);
|
||||||
|
payload.insert(QStringLiteral("max_side"), maxSide);
|
||||||
|
|
||||||
|
const QByteArray body = QJsonDocument(payload).toJson(QJsonDocument::Compact);
|
||||||
|
return m_nam->post(req, body);
|
||||||
|
}
|
||||||
|
|
||||||
bool ModelServerClient::computeDepthPng8(
|
bool ModelServerClient::computeDepthPng8(
|
||||||
const QByteArray& imageBytes,
|
const QByteArray& imageBytes,
|
||||||
QByteArray& outPngBytes,
|
QByteArray& outPngBytes,
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ public:
|
|||||||
const QJsonArray& boxXyxy,
|
const QJsonArray& boxXyxy,
|
||||||
QString* outImmediateError = nullptr);
|
QString* outImmediateError = nullptr);
|
||||||
|
|
||||||
|
// POST /inpaint,JSON 响应由调用方解析(success / output_image_b64 / error / output_path)。
|
||||||
|
QNetworkReply* inpaintAsync(
|
||||||
|
const QByteArray& cropRgbPngBytes,
|
||||||
|
const QByteArray& maskPngBytes,
|
||||||
|
const QString& prompt,
|
||||||
|
const QString& negativePrompt,
|
||||||
|
double strength,
|
||||||
|
int maxSide,
|
||||||
|
QString* outImmediateError = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkAccessManager* m_nam = nullptr;
|
QNetworkAccessManager* m_nam = nullptr;
|
||||||
QUrl m_baseUrl;
|
QUrl m_baseUrl;
|
||||||
|
|||||||
@@ -1869,6 +1869,69 @@ bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ProjectWorkspace::resolveBlackholeByModelInpaint(const QString& id, const QImage& patchedBackground,
|
||||||
|
bool hideBlackholeAfterFill) {
|
||||||
|
if (m_projectDir.isEmpty() || id.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const QString bgAbs = backgroundAbsolutePath();
|
||||||
|
if (bgAbs.isEmpty() || !QFileInfo::exists(bgAbs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (patchedBackground.isNull()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写回背景文件
|
||||||
|
{
|
||||||
|
QImage bg = patchedBackground;
|
||||||
|
if (bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
|
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
}
|
||||||
|
QImageWriter writer(bgAbs);
|
||||||
|
writer.setFormat("png");
|
||||||
|
writer.setCompression(1);
|
||||||
|
if (!writer.write(bg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新实体黑洞状态 + 记录历史
|
||||||
|
const auto before = m_project.entities();
|
||||||
|
auto ents = before;
|
||||||
|
int hit = -1;
|
||||||
|
for (int i = 0; i < ents.size(); ++i) {
|
||||||
|
if (ents[i].id == id) {
|
||||||
|
hit = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hit < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ents[hit].blackholeVisible = hideBlackholeAfterFill ? false : ents[hit].blackholeVisible;
|
||||||
|
if (ents[hit].blackholeId.isEmpty()) {
|
||||||
|
ents[hit].blackholeId = QStringLiteral("blackhole-%1").arg(ents[hit].id);
|
||||||
|
}
|
||||||
|
ents[hit].blackholeResolvedBy = QStringLiteral("model_inpaint");
|
||||||
|
|
||||||
|
m_project.setEntities(ents);
|
||||||
|
if (!saveSingleEntityPayload(ents[hit]) || !writeIndexJsonWithoutPayloadSync()) {
|
||||||
|
m_project.setEntities(before);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Operation op;
|
||||||
|
op.type = Operation::Type::SetEntities;
|
||||||
|
op.label = QStringLiteral("黑洞模型补全");
|
||||||
|
op.beforeEntities = before;
|
||||||
|
op.afterEntities = ents;
|
||||||
|
pushOperation(op);
|
||||||
|
m_redoStack.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ProjectWorkspace::setEntityVisibilityKey(const QString& id, int frame, bool visible) {
|
bool ProjectWorkspace::setEntityVisibilityKey(const QString& id, int frame, bool visible) {
|
||||||
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
|
if (m_projectDir.isEmpty() || id.isEmpty() || frame < 0) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ public:
|
|||||||
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
|
// 复制背景其他区域填充黑洞(sourceOffsetPx 以黑洞包围盒左上角为基准偏移)
|
||||||
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
bool resolveBlackholeByCopyBackground(const QString& id, const QPoint& sourceOffsetPx,
|
||||||
bool hideBlackholeAfterFill);
|
bool hideBlackholeAfterFill);
|
||||||
|
// 使用模型补全后的结果写回背景(patchedBackground 已包含补全贴合后的完整背景图像)
|
||||||
|
bool resolveBlackholeByModelInpaint(const QString& id, const QImage& patchedBackground,
|
||||||
|
bool hideBlackholeAfterFill);
|
||||||
bool setEntityVisibilityKey(const QString& id, int frame, bool visible);
|
bool setEntityVisibilityKey(const QString& id, int frame, bool visible);
|
||||||
bool removeEntityVisibilityKey(const QString& id, int frame);
|
bool removeEntityVisibilityKey(const QString& id, int frame);
|
||||||
bool setEntityDisplayName(const QString& id, const QString& displayName);
|
bool setEntityDisplayName(const QString& id, const QString& displayName);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ set(GUI_SOURCES
|
|||||||
${GUI_ROOT}/dialogs/CancelableTaskDialog.cpp
|
${GUI_ROOT}/dialogs/CancelableTaskDialog.cpp
|
||||||
${GUI_ROOT}/dialogs/EntityFinalizeDialog.cpp
|
${GUI_ROOT}/dialogs/EntityFinalizeDialog.cpp
|
||||||
${GUI_ROOT}/dialogs/BlackholeResolveDialog.cpp
|
${GUI_ROOT}/dialogs/BlackholeResolveDialog.cpp
|
||||||
|
${GUI_ROOT}/dialogs/InpaintPreviewDialog.cpp
|
||||||
${GUI_ROOT}/dialogs/EntityIntroPopup.cpp
|
${GUI_ROOT}/dialogs/EntityIntroPopup.cpp
|
||||||
${GUI_ROOT}/editor/EditorCanvas.cpp
|
${GUI_ROOT}/editor/EditorCanvas.cpp
|
||||||
${GUI_ROOT}/editor/EntityCutoutUtils.cpp
|
${GUI_ROOT}/editor/EntityCutoutUtils.cpp
|
||||||
@@ -37,6 +38,7 @@ set(GUI_HEADERS
|
|||||||
${GUI_ROOT}/dialogs/CancelableTaskDialog.h
|
${GUI_ROOT}/dialogs/CancelableTaskDialog.h
|
||||||
${GUI_ROOT}/dialogs/EntityFinalizeDialog.h
|
${GUI_ROOT}/dialogs/EntityFinalizeDialog.h
|
||||||
${GUI_ROOT}/dialogs/BlackholeResolveDialog.h
|
${GUI_ROOT}/dialogs/BlackholeResolveDialog.h
|
||||||
|
${GUI_ROOT}/dialogs/InpaintPreviewDialog.h
|
||||||
${GUI_ROOT}/dialogs/EntityIntroPopup.h
|
${GUI_ROOT}/dialogs/EntityIntroPopup.h
|
||||||
${GUI_ROOT}/editor/EditorCanvas.h
|
${GUI_ROOT}/editor/EditorCanvas.h
|
||||||
${GUI_ROOT}/editor/EntityCutoutUtils.h
|
${GUI_ROOT}/editor/EntityCutoutUtils.h
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <QDialogButtonBox>
|
#include <QDialogButtonBox>
|
||||||
#include <QFrame>
|
#include <QFrame>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPlainTextEdit>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStackedWidget>
|
#include <QStackedWidget>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -40,6 +41,10 @@ BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWi
|
|||||||
m_pages->setCurrentWidget(m_pageSelect);
|
m_pages->setCurrentWidget(m_pageSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString BlackholeResolveDialog::promptText() const {
|
||||||
|
return m_promptEdit ? m_promptEdit->toPlainText().trimmed() : QString();
|
||||||
|
}
|
||||||
|
|
||||||
void BlackholeResolveDialog::buildSelectPage() {
|
void BlackholeResolveDialog::buildSelectPage() {
|
||||||
m_pageSelect = new QWidget(this);
|
m_pageSelect = new QWidget(this);
|
||||||
auto* layout = new QVBoxLayout(m_pageSelect);
|
auto* layout = new QVBoxLayout(m_pageSelect);
|
||||||
@@ -61,14 +66,14 @@ void BlackholeResolveDialog::buildSelectPage() {
|
|||||||
QStringLiteral("使用原始背景"),
|
QStringLiteral("使用原始背景"),
|
||||||
QStringLiteral("撤销黑洞显示,恢复抠图前背景区域。"),
|
QStringLiteral("撤销黑洞显示,恢复抠图前背景区域。"),
|
||||||
m_pageSelect);
|
m_pageSelect);
|
||||||
|
auto* btnModel = makeAlgoButton(
|
||||||
|
QStringLiteral("模型补全(SDXL Inpaint)"),
|
||||||
|
QStringLiteral("输入提示词,自动补全缺失区域;可预览后再决定是否接受。"),
|
||||||
|
m_pageSelect);
|
||||||
layout->addWidget(btnCopy);
|
layout->addWidget(btnCopy);
|
||||||
layout->addWidget(btnOriginal);
|
layout->addWidget(btnOriginal);
|
||||||
|
layout->addWidget(btnModel);
|
||||||
|
|
||||||
auto* modelNote = new QLabel(
|
|
||||||
QStringLiteral("模型补全:已预留接口,本版本暂不实现。"),
|
|
||||||
m_pageSelect);
|
|
||||||
modelNote->setStyleSheet("color: palette(mid);");
|
|
||||||
layout->addWidget(modelNote);
|
|
||||||
layout->addStretch(1);
|
layout->addStretch(1);
|
||||||
|
|
||||||
auto* btns = new QDialogButtonBox(QDialogButtonBox::Cancel, m_pageSelect);
|
auto* btns = new QDialogButtonBox(QDialogButtonBox::Cancel, m_pageSelect);
|
||||||
@@ -81,6 +86,9 @@ void BlackholeResolveDialog::buildSelectPage() {
|
|||||||
connect(btnOriginal, &QPushButton::clicked, this, [this]() {
|
connect(btnOriginal, &QPushButton::clicked, this, [this]() {
|
||||||
enterAlgorithmPage(Algorithm::UseOriginalBackground);
|
enterAlgorithmPage(Algorithm::UseOriginalBackground);
|
||||||
});
|
});
|
||||||
|
connect(btnModel, &QPushButton::clicked, this, [this]() {
|
||||||
|
enterAlgorithmPage(Algorithm::ModelInpaint);
|
||||||
|
});
|
||||||
|
|
||||||
m_pages->addWidget(m_pageSelect);
|
m_pages->addWidget(m_pageSelect);
|
||||||
}
|
}
|
||||||
@@ -141,8 +149,29 @@ void BlackholeResolveDialog::buildDetailPage() {
|
|||||||
oLay->addStretch(1);
|
oLay->addStretch(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 详情页 C:模型补全(提示词)
|
||||||
|
m_modelDetail = new QWidget(m_algoDetails);
|
||||||
|
{
|
||||||
|
auto* mLay = new QVBoxLayout(m_modelDetail);
|
||||||
|
mLay->setSpacing(8);
|
||||||
|
|
||||||
|
auto* panel = new QFrame(m_modelDetail);
|
||||||
|
panel->setFrameShape(QFrame::StyledPanel);
|
||||||
|
auto* pLay = new QVBoxLayout(panel);
|
||||||
|
pLay->setSpacing(8);
|
||||||
|
|
||||||
|
m_promptEdit = new QPlainTextEdit(panel);
|
||||||
|
m_promptEdit->setPlainText(QStringLiteral("This is part of a Chinese painting; please complete the background for me, following the style of the other parts."));
|
||||||
|
m_promptEdit->setMinimumHeight(90);
|
||||||
|
pLay->addWidget(m_promptEdit);
|
||||||
|
|
||||||
|
mLay->addWidget(panel);
|
||||||
|
mLay->addStretch(1);
|
||||||
|
}
|
||||||
|
|
||||||
m_algoDetails->addWidget(m_copyDetail);
|
m_algoDetails->addWidget(m_copyDetail);
|
||||||
m_algoDetails->addWidget(m_originalDetail);
|
m_algoDetails->addWidget(m_originalDetail);
|
||||||
|
m_algoDetails->addWidget(m_modelDetail);
|
||||||
layout->addWidget(m_algoDetails, 1);
|
layout->addWidget(m_algoDetails, 1);
|
||||||
|
|
||||||
auto* btns = new QDialogButtonBox(m_pageDetail);
|
auto* btns = new QDialogButtonBox(m_pageDetail);
|
||||||
@@ -165,10 +194,14 @@ void BlackholeResolveDialog::enterAlgorithmPage(Algorithm algo) {
|
|||||||
m_detailTitle->setText(QStringLiteral("第 2 步:复制背景其他区域"));
|
m_detailTitle->setText(QStringLiteral("第 2 步:复制背景其他区域"));
|
||||||
m_detailHint->setText(QStringLiteral("准备进入画布拖动取样框模式。"));
|
m_detailHint->setText(QStringLiteral("准备进入画布拖动取样框模式。"));
|
||||||
m_algoDetails->setCurrentWidget(m_copyDetail);
|
m_algoDetails->setCurrentWidget(m_copyDetail);
|
||||||
} else {
|
} else if (algo == Algorithm::UseOriginalBackground) {
|
||||||
m_detailTitle->setText(QStringLiteral("第 2 步:使用原始背景"));
|
m_detailTitle->setText(QStringLiteral("第 2 步:使用原始背景"));
|
||||||
m_detailHint->setText(QStringLiteral("确认后将切换为原始背景显示。"));
|
m_detailHint->setText(QStringLiteral("确认后将切换为原始背景显示。"));
|
||||||
m_algoDetails->setCurrentWidget(m_originalDetail);
|
m_algoDetails->setCurrentWidget(m_originalDetail);
|
||||||
|
} else {
|
||||||
|
m_detailTitle->setText(QStringLiteral("第 2 步:模型补全(SDXL Inpaint)"));
|
||||||
|
m_detailHint->setText(QStringLiteral("输入提示词(可选),点击应用后将生成预览。"));
|
||||||
|
m_algoDetails->setCurrentWidget(m_modelDetail);
|
||||||
}
|
}
|
||||||
m_pages->setCurrentWidget(m_pageDetail);
|
m_pages->setCurrentWidget(m_pageDetail);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ public:
|
|||||||
enum class Algorithm {
|
enum class Algorithm {
|
||||||
CopyBackgroundRegion,
|
CopyBackgroundRegion,
|
||||||
UseOriginalBackground,
|
UseOriginalBackground,
|
||||||
|
ModelInpaint,
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit BlackholeResolveDialog(const QString& blackholeName, QWidget* parent = nullptr);
|
explicit BlackholeResolveDialog(const QString& blackholeName, QWidget* parent = nullptr);
|
||||||
|
|
||||||
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
|
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
|
||||||
|
QString promptText() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildSelectPage();
|
void buildSelectPage();
|
||||||
@@ -38,5 +40,8 @@ private:
|
|||||||
QWidget* m_copyDetail = nullptr;
|
QWidget* m_copyDetail = nullptr;
|
||||||
|
|
||||||
QWidget* m_originalDetail = nullptr;
|
QWidget* m_originalDetail = nullptr;
|
||||||
|
|
||||||
|
QWidget* m_modelDetail = nullptr;
|
||||||
|
class QPlainTextEdit* m_promptEdit = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
68
client/gui/dialogs/InpaintPreviewDialog.cpp
Normal file
68
client/gui/dialogs/InpaintPreviewDialog.cpp
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#include "dialogs/InpaintPreviewDialog.h"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QScrollArea>
|
||||||
|
#include <QSplitter>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
static QLabel* makeImageLabel(QWidget* parent) {
|
||||||
|
auto* lab = new QLabel(parent);
|
||||||
|
lab->setBackgroundRole(QPalette::Base);
|
||||||
|
lab->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||||
|
lab->setScaledContents(false);
|
||||||
|
lab->setAlignment(Qt::AlignCenter);
|
||||||
|
return lab;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) {
|
||||||
|
auto* sc = new QScrollArea(parent);
|
||||||
|
sc->setWidget(child);
|
||||||
|
sc->setWidgetResizable(true);
|
||||||
|
sc->setBackgroundRole(QPalette::Dark);
|
||||||
|
return sc;
|
||||||
|
}
|
||||||
|
|
||||||
|
InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent)
|
||||||
|
: QDialog(parent) {
|
||||||
|
setModal(true);
|
||||||
|
setMinimumSize(860, 520);
|
||||||
|
setWindowTitle(title);
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
root->setContentsMargins(8, 8, 8, 8);
|
||||||
|
root->setSpacing(8);
|
||||||
|
|
||||||
|
m_beforeLabel = makeImageLabel(this);
|
||||||
|
m_afterLabel = makeImageLabel(this);
|
||||||
|
m_beforeScroll = wrapScroll(m_beforeLabel, this);
|
||||||
|
m_afterScroll = wrapScroll(m_afterLabel, this);
|
||||||
|
|
||||||
|
auto* splitter = new QSplitter(Qt::Horizontal, this);
|
||||||
|
splitter->addWidget(m_beforeScroll);
|
||||||
|
splitter->addWidget(m_afterScroll);
|
||||||
|
splitter->setStretchFactor(0, 1);
|
||||||
|
splitter->setStretchFactor(1, 1);
|
||||||
|
root->addWidget(splitter, 1);
|
||||||
|
|
||||||
|
auto* btns = new QDialogButtonBox(this);
|
||||||
|
btns->addButton(QStringLiteral("取消"), QDialogButtonBox::RejectRole);
|
||||||
|
btns->addButton(QStringLiteral("接受并写回"), QDialogButtonBox::AcceptRole);
|
||||||
|
connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
root->addWidget(btns);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InpaintPreviewDialog::setImages(const QImage& before, const QImage& after) {
|
||||||
|
if (m_beforeLabel) {
|
||||||
|
m_beforeLabel->setPixmap(QPixmap::fromImage(before));
|
||||||
|
m_beforeLabel->adjustSize();
|
||||||
|
}
|
||||||
|
if (m_afterLabel) {
|
||||||
|
m_afterLabel->setPixmap(QPixmap::fromImage(after));
|
||||||
|
m_afterLabel->adjustSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
22
client/gui/dialogs/InpaintPreviewDialog.h
Normal file
22
client/gui/dialogs/InpaintPreviewDialog.h
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
class QLabel;
|
||||||
|
class QScrollArea;
|
||||||
|
|
||||||
|
class InpaintPreviewDialog final : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit InpaintPreviewDialog(const QString& title, QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setImages(const QImage& before, const QImage& after);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QLabel* m_beforeLabel = nullptr;
|
||||||
|
QLabel* m_afterLabel = nullptr;
|
||||||
|
QScrollArea* m_beforeScroll = nullptr;
|
||||||
|
QScrollArea* m_afterScroll = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "dialogs/CancelableTaskDialog.h"
|
#include "dialogs/CancelableTaskDialog.h"
|
||||||
#include "dialogs/EntityFinalizeDialog.h"
|
#include "dialogs/EntityFinalizeDialog.h"
|
||||||
#include "dialogs/BlackholeResolveDialog.h"
|
#include "dialogs/BlackholeResolveDialog.h"
|
||||||
|
#include "dialogs/InpaintPreviewDialog.h"
|
||||||
#include "editor/EditorCanvas.h"
|
#include "editor/EditorCanvas.h"
|
||||||
#include "editor/EntityCutoutUtils.h"
|
#include "editor/EntityCutoutUtils.h"
|
||||||
#include "dialogs/ImageCropDialog.h"
|
#include "dialogs/ImageCropDialog.h"
|
||||||
@@ -38,6 +39,9 @@
|
|||||||
#include <QFormLayout>
|
#include <QFormLayout>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QIODevice>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
#include <QMenuBar>
|
#include <QMenuBar>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
@@ -3566,11 +3570,329 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString
|
|||||||
}
|
}
|
||||||
statusBar()->showMessage(QStringLiteral("拖动画布中的青色取样框,松开鼠标即应用;Esc 取消"));
|
statusBar()->showMessage(QStringLiteral("拖动画布中的青色取样框,松开鼠标即应用;Esc 取消"));
|
||||||
return;
|
return;
|
||||||
} else {
|
} else if (dlg.selectedAlgorithm() == BlackholeResolveDialog::Algorithm::UseOriginalBackground) {
|
||||||
ok = m_workspace.resolveBlackholeByUseOriginalBackground(entityId);
|
ok = m_workspace.resolveBlackholeByUseOriginalBackground(entityId);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("应用“使用原始背景”失败。"));
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("应用“使用原始背景”失败。"));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 模型补全(SDXL Inpaint):裁剪黑洞区域 -> 生成 mask -> 请求后端补全 -> 预览 -> 接受后写回背景
|
||||||
|
const QString bgAbs = m_workspace.backgroundAbsolutePath();
|
||||||
|
if (bgAbs.isEmpty() || !QFileInfo::exists(bgAbs)) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("背景文件无效。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QImage bg(bgAbs);
|
||||||
|
if (bg.isNull()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("读取背景失败。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
|
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QPointF> holePolyWorld;
|
||||||
|
for (const auto& e : m_workspace.entities()) {
|
||||||
|
if (e.id == entityId) {
|
||||||
|
holePolyWorld = e.cutoutPolygonWorld;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (holePolyWorld.size() < 3) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("黑洞数据无效(多边形点数不足)。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPainterPath holePath;
|
||||||
|
holePath.addPolygon(QPolygonF(holePolyWorld));
|
||||||
|
holePath.closeSubpath();
|
||||||
|
|
||||||
|
// 给模型更多上下文,避免“只看见一小块洞”导致输出色块/噪声
|
||||||
|
constexpr int kMargin = 160;
|
||||||
|
const QRect holeRect = holePath.boundingRect().toAlignedRect();
|
||||||
|
QRect cropRect = holeRect.adjusted(-kMargin, -kMargin, kMargin, kMargin);
|
||||||
|
cropRect = cropRect.intersected(QRect(QPoint(0, 0), bg.size()));
|
||||||
|
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);
|
||||||
|
mask.fill(0);
|
||||||
|
{
|
||||||
|
QPainter pm(&mask);
|
||||||
|
pm.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
pm.translate(-cropRect.topLeft());
|
||||||
|
pm.fillPath(holePath, QColor(255, 255, 255));
|
||||||
|
pm.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关键:黑洞区域在原始背景里通常是“被抠走的前景(人)像素”,但我们目标是补背景。
|
||||||
|
// 为避免模型受前景像素干扰,这里先把 mask 内像素用周围背景的平均色“抹掉”,再发往后端 inpaint。
|
||||||
|
QImage cropForInpaint = cropRgb.convertToFormat(QImage::Format_RGB888);
|
||||||
|
{
|
||||||
|
const QImage m8 = mask.convertToFormat(QImage::Format_Grayscale8);
|
||||||
|
// 取一圈“mask 外的邻域”估计背景平均色
|
||||||
|
long long sr = 0, sg = 0, sb = 0;
|
||||||
|
int sc = 0;
|
||||||
|
for (int y = 0; y < cropForInpaint.height(); ++y) {
|
||||||
|
const uchar* mr = m8.constScanLine(y);
|
||||||
|
const uchar* pr = cropForInpaint.constScanLine(y);
|
||||||
|
for (int x = 0; x < cropForInpaint.width(); ++x) {
|
||||||
|
if (mr[x] != 0) continue;
|
||||||
|
bool nearHole = false;
|
||||||
|
for (int dy = -2; dy <= 2 && !nearHole; ++dy) {
|
||||||
|
const int yy = y + dy;
|
||||||
|
if (yy < 0 || yy >= m8.height()) continue;
|
||||||
|
const uchar* r2 = m8.constScanLine(yy);
|
||||||
|
for (int dx = -2; dx <= 2; ++dx) {
|
||||||
|
const int xx = x + dx;
|
||||||
|
if (xx < 0 || xx >= m8.width()) continue;
|
||||||
|
if (r2[xx] != 0) {
|
||||||
|
nearHole = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nearHole) continue;
|
||||||
|
const int idx = x * 3;
|
||||||
|
sr += pr[idx + 0];
|
||||||
|
sg += pr[idx + 1];
|
||||||
|
sb += pr[idx + 2];
|
||||||
|
sc += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int fillR = (sc > 0) ? int(sr / sc) : 128;
|
||||||
|
const int fillG = (sc > 0) ? int(sg / sc) : 128;
|
||||||
|
const int fillB = (sc > 0) ? int(sb / sc) : 128;
|
||||||
|
|
||||||
|
for (int y = 0; y < cropForInpaint.height(); ++y) {
|
||||||
|
const uchar* mr = m8.constScanLine(y);
|
||||||
|
uchar* pr = cropForInpaint.scanLine(y);
|
||||||
|
for (int x = 0; x < cropForInpaint.width(); ++x) {
|
||||||
|
if (mr[x] == 0) continue;
|
||||||
|
const int idx = x * 3;
|
||||||
|
pr[idx + 0] = uchar(std::clamp(fillR, 0, 255));
|
||||||
|
pr[idx + 1] = uchar(std::clamp(fillG, 0, 255));
|
||||||
|
pr[idx + 2] = uchar(std::clamp(fillB, 0, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray cropPng;
|
||||||
|
{
|
||||||
|
QBuffer buf(&cropPng);
|
||||||
|
if (!buf.open(QIODevice::WriteOnly) || !cropForInpaint.save(&buf, "PNG")) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("编码裁剪图失败。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QByteArray maskPng;
|
||||||
|
{
|
||||||
|
QBuffer buf(&maskPng);
|
||||||
|
if (!buf.open(QIODevice::WriteOnly) || !mask.save(&buf, "PNG")) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("编码 mask 失败。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString base;
|
||||||
|
const QByteArray env = qgetenv("MODEL_SERVER_URL");
|
||||||
|
base = env.isEmpty() ? QStringLiteral("http://127.0.0.1:8000") : QString::fromUtf8(env);
|
||||||
|
|
||||||
|
auto* client = new core::ModelServerClient(this);
|
||||||
|
client->setBaseUrl(QUrl(base));
|
||||||
|
|
||||||
|
QString immediateErr;
|
||||||
|
const QString prompt = dlg.promptText();
|
||||||
|
QNetworkReply* reply = client->inpaintAsync(
|
||||||
|
cropPng,
|
||||||
|
maskPng,
|
||||||
|
prompt,
|
||||||
|
QString(),
|
||||||
|
// 经验值:强度过高容易把整个裁剪块“重绘成色块”,先降低更稳
|
||||||
|
0.55,
|
||||||
|
1024,
|
||||||
|
&immediateErr);
|
||||||
|
if (!reply) {
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("黑洞修复"),
|
||||||
|
immediateErr.isEmpty() ? QStringLiteral("无法发起后端请求。") : immediateErr);
|
||||||
|
client->deleteLater();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* task = new CancelableTaskDialog(QStringLiteral("黑洞修复"),
|
||||||
|
QStringLiteral("正在请求后端模型补全,请稍候……"),
|
||||||
|
this);
|
||||||
|
task->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||||
|
connect(task, &CancelableTaskDialog::canceled, this, [reply, task]() {
|
||||||
|
if (reply) reply->abort();
|
||||||
|
if (task) task->reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(reply, &QNetworkReply::finished, this,
|
||||||
|
[this, reply, task, client, entityId, bg, cropRect, cropRgb, mask]() mutable {
|
||||||
|
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
const QByteArray raw = reply->readAll();
|
||||||
|
const auto netErr = reply->error();
|
||||||
|
const QString netErrStr = reply->errorString();
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
client->deleteLater();
|
||||||
|
if (task) task->close();
|
||||||
|
|
||||||
|
if (netErr != QNetworkReply::NoError) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"),
|
||||||
|
QStringLiteral("网络错误:%1").arg(netErrStr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (httpStatus != 200) {
|
||||||
|
QString detail;
|
||||||
|
const QJsonDocument jerr = QJsonDocument::fromJson(raw);
|
||||||
|
if (jerr.isObject()) {
|
||||||
|
detail = jerr.object().value(QStringLiteral("detail")).toString();
|
||||||
|
}
|
||||||
|
QMessageBox::warning(this,
|
||||||
|
QStringLiteral("黑洞修复"),
|
||||||
|
detail.isEmpty() ? QStringLiteral("后端返回 HTTP %1。").arg(httpStatus)
|
||||||
|
: QStringLiteral("后端错误(HTTP %1):%2").arg(httpStatus).arg(detail));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonDocument jd = QJsonDocument::fromJson(raw);
|
||||||
|
if (!jd.isObject()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("响应不是 JSON。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QJsonObject obj = jd.object();
|
||||||
|
if (!obj.value(QStringLiteral("success")).toBool()) {
|
||||||
|
const QString err = obj.value(QStringLiteral("error")).toString();
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"),
|
||||||
|
err.isEmpty() ? QStringLiteral("补全失败。") : err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString outB64 = obj.value(QStringLiteral("output_image_b64")).toString();
|
||||||
|
if (outB64.isEmpty()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("后端未返回图像数据。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QImage inpainted;
|
||||||
|
{
|
||||||
|
const QByteArray bytes = QByteArray::fromBase64(outB64.toLatin1());
|
||||||
|
inpainted.loadFromData(bytes, "PNG");
|
||||||
|
}
|
||||||
|
if (inpainted.isNull()) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("解析补全图失败。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (inpainted.size() != cropRgb.size()) {
|
||||||
|
inpainted = inpainted.scaled(cropRgb.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
if (inpainted.format() != QImage::Format_RGB888) {
|
||||||
|
inpainted = inpainted.convertToFormat(QImage::Format_RGB888);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合成:mask 羽化后 alpha blend,避免硬边与块状感
|
||||||
|
QImage after = cropRgb.convertToFormat(QImage::Format_RGB888);
|
||||||
|
QImage alpha = mask.convertToFormat(QImage::Format_Grayscale8);
|
||||||
|
{
|
||||||
|
// 轻微膨胀 + 模糊,让边缘更自然(开销小,且无需额外依赖)
|
||||||
|
QImage dil(alpha.size(), QImage::Format_Grayscale8);
|
||||||
|
dil.fill(0);
|
||||||
|
for (int y = 0; y < alpha.height(); ++y) {
|
||||||
|
for (int x = 0; x < alpha.width(); ++x) {
|
||||||
|
int best = 0;
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
const int yy = y + dy;
|
||||||
|
if (yy < 0 || yy >= alpha.height()) continue;
|
||||||
|
const uchar* row = alpha.constScanLine(yy);
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
const int xx = x + dx;
|
||||||
|
if (xx < 0 || xx >= alpha.width()) continue;
|
||||||
|
best = std::max(best, int(row[xx]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dil.scanLine(y)[x] = uchar(best);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alpha = dil;
|
||||||
|
// 简单 box blur 3x3
|
||||||
|
QImage blur(alpha.size(), QImage::Format_Grayscale8);
|
||||||
|
blur.fill(0);
|
||||||
|
for (int y = 0; y < alpha.height(); ++y) {
|
||||||
|
for (int x = 0; x < alpha.width(); ++x) {
|
||||||
|
int sum = 0;
|
||||||
|
int cnt = 0;
|
||||||
|
for (int dy = -1; dy <= 1; ++dy) {
|
||||||
|
const int yy = y + dy;
|
||||||
|
if (yy < 0 || yy >= alpha.height()) continue;
|
||||||
|
const uchar* row = alpha.constScanLine(yy);
|
||||||
|
for (int dx = -1; dx <= 1; ++dx) {
|
||||||
|
const int xx = x + dx;
|
||||||
|
if (xx < 0 || xx >= alpha.width()) continue;
|
||||||
|
sum += int(row[xx]);
|
||||||
|
cnt += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blur.scanLine(y)[x] = uchar(std::clamp(sum / std::max(1, cnt), 0, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alpha = blur;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = 0; y < after.height(); ++y) {
|
||||||
|
const uchar* arow = alpha.constScanLine(y);
|
||||||
|
uchar* orow = after.scanLine(y);
|
||||||
|
const uchar* irow = inpainted.constScanLine(y);
|
||||||
|
for (int x = 0; x < after.width(); ++x) {
|
||||||
|
const int a = int(arow[x]); // 0..255
|
||||||
|
if (a <= 0) continue;
|
||||||
|
const int idx = x * 3;
|
||||||
|
for (int c = 0; c < 3; ++c) {
|
||||||
|
const int o = int(orow[idx + c]);
|
||||||
|
const int n = int(irow[idx + c]);
|
||||||
|
const int v = (o * (255 - a) + n * a + 127) / 255;
|
||||||
|
orow[idx + c] = uchar(std::clamp(v, 0, 255));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InpaintPreviewDialog preview(QStringLiteral("模型补全预览"), this);
|
||||||
|
preview.setImages(cropRgb, after);
|
||||||
|
if (preview.exec() != QDialog::Accepted) {
|
||||||
|
statusBar()->showMessage(QStringLiteral("已取消模型补全"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage patchedBg = bg;
|
||||||
|
{
|
||||||
|
QPainter p(&patchedBg);
|
||||||
|
p.drawImage(cropRect.topLeft(), after);
|
||||||
|
p.end();
|
||||||
|
}
|
||||||
|
const bool ok2 = m_workspace.resolveBlackholeByModelInpaint(entityId, patchedBg, true);
|
||||||
|
if (!ok2) {
|
||||||
|
QMessageBox::warning(this, QStringLiteral("黑洞修复"), QStringLiteral("写回背景失败。"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusBar()->showMessage(QStringLiteral("黑洞已通过模型补全修复"));
|
||||||
|
refreshProjectTree();
|
||||||
|
updateUiEnabledState();
|
||||||
|
if (m_editorCanvas) {
|
||||||
|
m_editorCanvas->notifyBackgroundContentChanged();
|
||||||
|
}
|
||||||
|
refreshEditorPage();
|
||||||
|
if (m_previewRequested) {
|
||||||
|
refreshPreviewPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
task->show();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
|||||||
@@ -420,7 +420,12 @@ def inpaint(req: InpaintRequest) -> Dict[str, Any]:
|
|||||||
out_path = out_dir / f"{model_name}_inpaint.png"
|
out_path = out_dir / f"{model_name}_inpaint.png"
|
||||||
out.save(out_path)
|
out.save(out_path)
|
||||||
|
|
||||||
return {"success": True, "output_path": str(out_path)}
|
# 兼容 Qt 前端:直接返回结果图,避免前端再去读取服务器磁盘路径
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"output_path": str(out_path),
|
||||||
|
"output_image_b64": _pil_image_to_png_b64(out),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user