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
+10 -1
View File
@@ -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);
@@ -19,6 +19,9 @@ public:
Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; }
QString promptText() const;
/// 模型补全(LaMa 等)内置正向提示:中国画去人物、补背景纹理,无人物残留。
[[nodiscard]] static QString defaultModelInpaintPrompt();
private:
void buildSelectPage();
void buildDetailPage();
+12 -6
View File
@@ -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());
}
}
+6 -12
View File
@@ -697,8 +697,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& 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<core::Project::Tool>& 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<int>(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<int>(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 =
+118 -38
View File
@@ -75,6 +75,7 @@
#include <QFontMetrics>
#include <QHeaderView>
#include <QImage>
#include <QTransform>
#include <QEventLoop>
#include <QMessageBox>
#include <QPixmap>
@@ -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<int>::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<double>(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<int>(std::floor(c.x()));
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("背景文件无效。"));
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<QPointF> 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<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);
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) {