添加模型分割
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
#include "main_window/MainWindow.h"
|
||||
#include "dialogs/AboutWindow.h"
|
||||
#include "dialogs/CancelableTaskDialog.h"
|
||||
#include "dialogs/EntityFinalizeDialog.h"
|
||||
#include "editor/EditorCanvas.h"
|
||||
#include "editor/EntityCutoutUtils.h"
|
||||
#include "dialogs/ImageCropDialog.h"
|
||||
#include "core/domain/EntityIntro.h"
|
||||
#include "core/net/ModelServerClient.h"
|
||||
#include "widgets/ToolOptionPopup.h"
|
||||
#include "params/ParamControls.h"
|
||||
#include "props/BackgroundPropertySection.h"
|
||||
#include "props/EntityPropertySection.h"
|
||||
@@ -55,8 +58,13 @@
|
||||
#include <QStyle>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QSet>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QSet>
|
||||
|
||||
@@ -524,6 +532,24 @@ void MainWindow::createTimelineDock() {
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::syncCreateEntityToolButtonTooltip() {
|
||||
if (!m_btnCreateEntity || !m_editorCanvas) {
|
||||
return;
|
||||
}
|
||||
using Mode = EditorCanvas::EntityCreateSegmentMode;
|
||||
const Mode m = m_editorCanvas->entityCreateSegmentMode();
|
||||
if (m == Mode::Manual) {
|
||||
m_btnCreateEntity->setToolTip(QStringLiteral("创建实体:手动分割\n"
|
||||
"再次单击本按钮可选择:手动/吸附/模型"));
|
||||
} else if (m == Mode::Snap) {
|
||||
m_btnCreateEntity->setToolTip(QStringLiteral("创建实体:吸附分割(前端边缘吸附算法)\n"
|
||||
"再次单击本按钮可选择:手动/吸附/模型"));
|
||||
} else {
|
||||
m_btnCreateEntity->setToolTip(QStringLiteral("创建实体:模型分割(SAM)\n"
|
||||
"再次单击本按钮可选择:手动/吸附/模型"));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateStatusBarText() {
|
||||
// 坐标系:图片左上角为 (0,0),单位为像素(world 坐标与背景像素一致)
|
||||
if (m_hasSelectedEntity) {
|
||||
@@ -1734,6 +1760,30 @@ void MainWindow::rebuildCentralPages() {
|
||||
polishCompactToolButton(m_btnCreateEntity, 40);
|
||||
toolLayout->addWidget(m_btnCreateEntity, 0, Qt::AlignHCenter);
|
||||
group->addButton(m_btnCreateEntity, static_cast<int>(EditorCanvas::Tool::CreateEntity));
|
||||
if (!m_createEntityPopup) {
|
||||
m_createEntityPopup = new ToolOptionPopup(this);
|
||||
m_createEntityPopup->setOptions({
|
||||
{static_cast<int>(EditorCanvas::EntityCreateSegmentMode::Manual), QStringLiteral("手动分割")},
|
||||
{static_cast<int>(EditorCanvas::EntityCreateSegmentMode::Snap), QStringLiteral("吸附分割")},
|
||||
{static_cast<int>(EditorCanvas::EntityCreateSegmentMode::Sam), QStringLiteral("模型分割")},
|
||||
});
|
||||
connect(m_createEntityPopup, &ToolOptionPopup::optionChosen, this, [this](int id) {
|
||||
if (!m_editorCanvas) return;
|
||||
m_editorCanvas->setEntityCreateSegmentMode(static_cast<EditorCanvas::EntityCreateSegmentMode>(id));
|
||||
syncCreateEntityToolButtonTooltip();
|
||||
statusBar()->showMessage(QStringLiteral("已切换分割方式"));
|
||||
});
|
||||
}
|
||||
connect(m_btnCreateEntity, &QToolButton::clicked, this, [this]() {
|
||||
if (!m_editorCanvas || !m_btnCreateEntity) return;
|
||||
if (m_btnCreateEntity->isChecked() && m_editorCanvas->tool() == EditorCanvas::Tool::CreateEntity) {
|
||||
// 已选中时再次单击:弹出选择面板
|
||||
if (m_createEntityPopup) {
|
||||
m_createEntityPopup->popupNearToolButton(m_btnCreateEntity);
|
||||
}
|
||||
}
|
||||
});
|
||||
syncCreateEntityToolButtonTooltip();
|
||||
|
||||
m_btnToggleDepthOverlay = new QToolButton(m_floatingToolDock);
|
||||
m_btnToggleDepthOverlay->setCheckable(true);
|
||||
@@ -1845,6 +1895,288 @@ void MainWindow::rebuildCentralPages() {
|
||||
refreshProjectTree();
|
||||
updateUiEnabledState();
|
||||
});
|
||||
connect(m_editorCanvas, &EditorCanvas::requestSamSegment, this,
|
||||
[this](const QByteArray& cropRgbPng,
|
||||
const QByteArray& overlayPng,
|
||||
const QPointF& cropTopLeftWorld,
|
||||
const QJsonArray& pointCoords,
|
||||
const QJsonArray& pointLabels,
|
||||
const QJsonArray& boxXyxy) {
|
||||
if (!m_workspace.isOpen() || !m_workspace.hasBackground()) {
|
||||
QMessageBox::warning(this, QStringLiteral("实体"), QStringLiteral("请先打开项目并设置背景。"));
|
||||
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;
|
||||
QNetworkReply* reply = client->segmentSamPromptAsync(
|
||||
cropRgbPng, overlayPng, pointCoords, pointLabels, boxXyxy, &immediateErr);
|
||||
if (!reply) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("SAM 分割"),
|
||||
immediateErr.isEmpty() ? QStringLiteral("无法发起后端请求。") : immediateErr);
|
||||
client->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* dlg = new CancelableTaskDialog(QStringLiteral("SAM 分割"),
|
||||
QStringLiteral("正在请求后端进行分割,请稍候……"),
|
||||
this);
|
||||
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
|
||||
connect(dlg, &CancelableTaskDialog::canceled, this, [reply, dlg]() {
|
||||
if (reply) {
|
||||
reply->abort();
|
||||
}
|
||||
if (dlg) {
|
||||
dlg->reject();
|
||||
}
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, dlg, client, cropTopLeftWorld]() {
|
||||
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 (dlg) {
|
||||
dlg->close();
|
||||
}
|
||||
|
||||
if (netErr != QNetworkReply::NoError) {
|
||||
if (netErrStr.contains(QStringLiteral("canceled"), Qt::CaseInsensitive) ||
|
||||
netErr == QNetworkReply::OperationCanceledError) {
|
||||
statusBar()->showMessage(QStringLiteral("已取消分割"));
|
||||
return;
|
||||
}
|
||||
QMessageBox::warning(this, QStringLiteral("SAM 分割"),
|
||||
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("SAM 分割"),
|
||||
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("SAM 分割"), 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("SAM 分割"),
|
||||
err.isEmpty() ? QStringLiteral("分割失败。") : err);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray contour = obj.value(QStringLiteral("contour")).toArray();
|
||||
if (contour.size() < 3) {
|
||||
QMessageBox::warning(this, QStringLiteral("SAM 分割"), QStringLiteral("轮廓点数不足。"));
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<QPointF> polyWorld;
|
||||
polyWorld.reserve(contour.size());
|
||||
for (const QJsonValue& v : contour) {
|
||||
if (!v.isArray()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonArray p = v.toArray();
|
||||
if (p.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
const double x = p.at(0).toDouble();
|
||||
const double y = p.at(1).toDouble();
|
||||
polyWorld.append(cropTopLeftWorld + QPointF(x, y));
|
||||
}
|
||||
if (polyWorld.size() < 3) {
|
||||
QMessageBox::warning(this, QStringLiteral("SAM 分割"), QStringLiteral("无效轮廓数据。"));
|
||||
return;
|
||||
}
|
||||
|
||||
core::Project::Entity ent;
|
||||
ent.id.clear();
|
||||
ent.cutoutPolygonWorld = polyWorld;
|
||||
ent.originWorld = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
|
||||
ent.polygonLocal.clear();
|
||||
ent.polygonLocal.reserve(ent.cutoutPolygonWorld.size());
|
||||
for (const auto& pt : ent.cutoutPolygonWorld) {
|
||||
ent.polygonLocal.push_back(pt - ent.originWorld);
|
||||
}
|
||||
|
||||
QImage depth8;
|
||||
if (m_workspace.hasDepth()) {
|
||||
const QString dpath = m_workspace.depthAbsolutePath();
|
||||
if (!dpath.isEmpty() && QFileInfo::exists(dpath)) {
|
||||
const QImage dimg(dpath);
|
||||
if (!dimg.isNull()) {
|
||||
depth8 = dimg.convertToFormat(QImage::Format_Grayscale8);
|
||||
}
|
||||
}
|
||||
}
|
||||
const QPointF c = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
|
||||
int z = 0;
|
||||
if (!depth8.isNull()) {
|
||||
const int xi = static_cast<int>(std::floor(c.x()));
|
||||
const int yi = static_cast<int>(std::floor(c.y()));
|
||||
if (xi >= 0 && yi >= 0 && xi < depth8.width() && yi < depth8.height()) {
|
||||
z = static_cast<int>(static_cast<const uchar*>(depth8.constScanLine(yi))[xi]);
|
||||
}
|
||||
}
|
||||
ent.depth = z;
|
||||
{
|
||||
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(bgAbs);
|
||||
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
QImage cutout;
|
||||
if (!bg.isNull()) {
|
||||
QPointF topLeft;
|
||||
cutout = entity_cutout::extractEntityImage(bg, ent.cutoutPolygonWorld, topLeft);
|
||||
ent.imageTopLeftWorld = topLeft;
|
||||
}
|
||||
|
||||
QSet<QString> used;
|
||||
for (const auto& ex : m_workspace.entities()) {
|
||||
used.insert(ex.id);
|
||||
}
|
||||
int n = static_cast<int>(m_workspace.entities().size()) + 1;
|
||||
for (int guard = 0; guard < 100000; ++guard, ++n) {
|
||||
const QString cand = QStringLiteral("entity-%1").arg(n);
|
||||
if (!used.contains(cand)) {
|
||||
ent.id = cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 不直接落盘:进入待确认(可微调)
|
||||
m_editorCanvas->setPendingEntityPolygonWorld(polyWorld);
|
||||
statusBar()->showMessage(QStringLiteral("分割完成:可拖动顶点微调,回车/点击空白确认"));
|
||||
});
|
||||
|
||||
dlg->show();
|
||||
});
|
||||
|
||||
connect(m_editorCanvas, &EditorCanvas::requestFinalizePendingEntity, this, [this](const QVector<QPointF>& polyWorld) {
|
||||
if (!m_workspace.isOpen() || !m_workspace.hasBackground()) {
|
||||
return;
|
||||
}
|
||||
if (polyWorld.size() < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 自动深度
|
||||
int z = 0;
|
||||
if (m_workspace.hasDepth()) {
|
||||
const QString dpath = m_workspace.depthAbsolutePath();
|
||||
QImage depth8(dpath);
|
||||
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()));
|
||||
if (xi >= 0 && yi >= 0 && xi < depth8.width() && yi < depth8.height()) {
|
||||
z = static_cast<int>(static_cast<const uchar*>(depth8.constScanLine(yi))[xi]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成稳定且不重复的 id:entity-<n>,同时作为“默认名称”
|
||||
QString newId;
|
||||
{
|
||||
QSet<QString> used;
|
||||
for (const auto& ex : m_workspace.entities()) {
|
||||
used.insert(ex.id);
|
||||
}
|
||||
int n = static_cast<int>(m_workspace.entities().size()) + 1;
|
||||
for (int guard = 0; guard < 100000; ++guard, ++n) {
|
||||
const QString cand = QStringLiteral("entity-%1").arg(n);
|
||||
if (!used.contains(cand)) {
|
||||
newId = cand;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EntityFinalizeDialog dlg(this);
|
||||
dlg.setDefaultName(newId.isEmpty() ? QStringLiteral("entity-1") : newId);
|
||||
dlg.setUserScale(1.0);
|
||||
if (dlg.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
core::Project::Entity ent;
|
||||
ent.id = newId;
|
||||
ent.displayName = dlg.name();
|
||||
ent.cutoutPolygonWorld = polyWorld;
|
||||
ent.originWorld = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
|
||||
ent.polygonLocal.clear();
|
||||
ent.polygonLocal.reserve(ent.cutoutPolygonWorld.size());
|
||||
for (const auto& pt : ent.cutoutPolygonWorld) {
|
||||
ent.polygonLocal.push_back(pt - ent.originWorld);
|
||||
}
|
||||
ent.depth = std::clamp(z, 0, 255);
|
||||
ent.userScale = std::max(1e-6, dlg.userScale());
|
||||
{
|
||||
const double ds01 = static_cast<double>(ent.depth) / 255.0;
|
||||
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
|
||||
}
|
||||
// 若用户把名称清空,则 displayName 置空,UI 会回退显示 id(保持原习惯)
|
||||
if (ent.displayName == ent.id) {
|
||||
// 默认情况保留 displayName=id,便于树上直接显示 entity-x
|
||||
}
|
||||
if (ent.displayName.isEmpty()) {
|
||||
// 允许空:界面会用 id 展示
|
||||
}
|
||||
|
||||
QImage bg(m_workspace.backgroundAbsolutePath());
|
||||
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
QImage cutout;
|
||||
if (!bg.isNull()) {
|
||||
QPointF topLeft;
|
||||
cutout = entity_cutout::extractEntityImage(bg, ent.cutoutPolygonWorld, topLeft);
|
||||
ent.imageTopLeftWorld = topLeft;
|
||||
}
|
||||
|
||||
if (!m_workspace.addEntity(ent, cutout)) {
|
||||
QMessageBox::warning(this, QStringLiteral("实体"), QStringLiteral("保存实体失败。"));
|
||||
return;
|
||||
}
|
||||
if (m_editorCanvas) {
|
||||
m_editorCanvas->clearPendingEntityPolygon();
|
||||
}
|
||||
statusBar()->showMessage(QStringLiteral("实体已创建"));
|
||||
refreshEditorPage();
|
||||
refreshProjectTree();
|
||||
updateUiEnabledState();
|
||||
});
|
||||
connect(m_editorCanvas, &EditorCanvas::requestMoveEntity, this, [this](const QString& id, const QPointF& delta) {
|
||||
// 动画编辑:拖动即写入当前位置关键帧,避免被既有关键帧插值“拉回去”
|
||||
const bool autoKey = true;
|
||||
@@ -2128,7 +2460,7 @@ void MainWindow::refreshDopeSheet() {
|
||||
|
||||
void MainWindow::showBackgroundContextMenu(const QPoint& globalPos) {
|
||||
QMenu menu(this);
|
||||
QAction* actComputeDepth = menu.addAction(QStringLiteral("计算深度(后端)"));
|
||||
QAction* actComputeDepth = menu.addAction(QStringLiteral("计算深度"));
|
||||
actComputeDepth->setEnabled(m_workspace.isOpen() && m_workspace.hasBackground());
|
||||
|
||||
QAction* chosen = menu.exec(globalPos);
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
#include <QIcon>
|
||||
#include <QTimer>
|
||||
|
||||
#include <QToolButton>
|
||||
|
||||
class ToolOptionPopup;
|
||||
|
||||
class QAction;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
@@ -112,6 +116,7 @@ private:
|
||||
gui::BackgroundPropertySection* m_bgPropertySection = nullptr;
|
||||
gui::EntityPropertySection* m_entityPropertySection = nullptr;
|
||||
QToolButton* m_btnCreateEntity = nullptr;
|
||||
ToolOptionPopup* m_createEntityPopup = nullptr;
|
||||
QToolButton* m_btnToggleDepthOverlay = nullptr;
|
||||
|
||||
EditorCanvas* m_editorCanvas = nullptr;
|
||||
@@ -155,6 +160,7 @@ private:
|
||||
QString m_bgAbsCache;
|
||||
QString m_bgSizeTextCache;
|
||||
void updateStatusBarText();
|
||||
void syncCreateEntityToolButtonTooltip();
|
||||
void refreshPropertyPanel();
|
||||
void refreshEntityPropertyPanelFast();
|
||||
void syncProjectTreeFromCanvasSelection();
|
||||
|
||||
Reference in New Issue
Block a user