update
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <QSet>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QTransform>
|
||||
#include <QPolygonF>
|
||||
#include <QRect>
|
||||
|
||||
@@ -2275,41 +2276,64 @@ bool ProjectWorkspace::resolveBlackholeByCopyBackground(const QString& id, const
|
||||
}
|
||||
const QImage srcSnapshot = bg;
|
||||
|
||||
QPainterPath holePath;
|
||||
holePath.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
|
||||
holePath.closeSubpath();
|
||||
const QRect targetRect = holePath.boundingRect().toAlignedRect().intersected(QRect(QPoint(0, 0), bg.size()));
|
||||
if (!targetRect.isValid() || targetRect.width() <= 0 || targetRect.height() <= 0) {
|
||||
QSize logicalSz;
|
||||
if (!image_file::probeImagePixelSize(bgAbs, &logicalSz) || !logicalSz.isValid() || logicalSz.width() < 1 ||
|
||||
logicalSz.height() < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QRect srcRect(targetRect.topLeft() + sourceOffsetPx, targetRect.size());
|
||||
if (srcRect.left() < 0) srcRect.moveLeft(0);
|
||||
if (srcRect.top() < 0) srcRect.moveTop(0);
|
||||
if (srcRect.right() >= bg.width()) srcRect.moveRight(bg.width() - 1);
|
||||
if (srcRect.bottom() >= bg.height()) srcRect.moveBottom(bg.height() - 1);
|
||||
srcRect = srcRect.intersected(QRect(QPoint(0, 0), bg.size()));
|
||||
if (srcRect.width() != targetRect.width() || srcRect.height() != targetRect.height()) {
|
||||
QPainterPath holePathWorld;
|
||||
holePathWorld.addPolygon(QPolygonF(ent.cutoutPolygonWorld));
|
||||
holePathWorld.closeSubpath();
|
||||
const QRect targetWorld =
|
||||
holePathWorld.boundingRect().toAlignedRect().intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
|
||||
if (!targetWorld.isValid() || targetWorld.width() <= 0 || targetWorld.height() <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QRect srcWorld(targetWorld.topLeft() + sourceOffsetPx, targetWorld.size());
|
||||
if (srcWorld.left() < 0) srcWorld.moveLeft(0);
|
||||
if (srcWorld.top() < 0) srcWorld.moveTop(0);
|
||||
if (srcWorld.right() >= logicalSz.width()) srcWorld.moveRight(logicalSz.width() - 1);
|
||||
if (srcWorld.bottom() >= logicalSz.height()) srcWorld.moveBottom(logicalSz.height() - 1);
|
||||
srcWorld = srcWorld.intersected(QRect(0, 0, logicalSz.width(), logicalSz.height()));
|
||||
if (srcWorld.width() != targetWorld.width() || srcWorld.height() != targetWorld.height()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const qreal sx = bg.width() / qreal(logicalSz.width());
|
||||
const qreal sy = bg.height() / qreal(logicalSz.height());
|
||||
QTransform worldToBg;
|
||||
worldToBg.scale(sx, sy);
|
||||
|
||||
const QRect targetImg =
|
||||
worldToBg.mapRect(QRectF(targetWorld)).toAlignedRect().intersected(QRect(0, 0, bg.width(), bg.height()));
|
||||
const QRect srcImg =
|
||||
worldToBg.mapRect(QRectF(srcWorld)).toAlignedRect().intersected(QRect(0, 0, bg.width(), bg.height()));
|
||||
if (!targetImg.isValid() || !srcImg.isValid() || targetImg.width() != srcImg.width() ||
|
||||
targetImg.height() != srcImg.height()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QPainterPath holePathImg = worldToBg.map(holePathWorld);
|
||||
|
||||
{
|
||||
QPainter p(&bg);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
p.setClipPath(holePath);
|
||||
p.drawImage(targetRect.topLeft(), srcSnapshot, srcRect);
|
||||
p.setClipPath(holePathImg);
|
||||
p.drawImage(targetImg.topLeft(), srcSnapshot, srcImg);
|
||||
p.end();
|
||||
}
|
||||
|
||||
const QImage patch = bg.copy(targetRect);
|
||||
const QImage patch = bg.copy(targetImg);
|
||||
const auto before = m_project.entities();
|
||||
QString overlayRel;
|
||||
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetRect,
|
||||
if (!writeBlackholeOverlayPng(m_projectDir, id, ents[hit].blackholeOverlayPath, patch, targetWorld,
|
||||
&overlayRel)) {
|
||||
return false;
|
||||
}
|
||||
ents[hit].blackholeOverlayPath = overlayRel;
|
||||
ents[hit].blackholeOverlayRect = targetRect;
|
||||
ents[hit].blackholeOverlayRect = targetWorld;
|
||||
ents[hit].blackholeVisible = hideBlackholeAfterFill ? false : ents[hit].blackholeVisible;
|
||||
if (ents[hit].blackholeId.isEmpty()) {
|
||||
ents[hit].blackholeId = QStringLiteral("blackhole-%1").arg(ents[hit].id);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user