添加模型分割

This commit is contained in:
2026-04-08 14:37:01 +08:00
parent 088dd91e27
commit a79c31a056
17 changed files with 1327 additions and 183 deletions

View File

@@ -1,10 +1,12 @@
#include "editor/EditorCanvas.h"
#include "editor/EntityCutoutUtils.h"
#include "core/animation/AnimationSampling.h"
#include "core/depth/DepthService.h"
#include <algorithm>
#include <cmath>
#include <QBuffer>
#include <QDir>
#include <QFileInfo>
#include <QCursor>
@@ -14,9 +16,167 @@
#include <QPainterPath>
#include <QWheelEvent>
#include <QKeyEvent>
#include <QJsonArray>
#include <QIODevice>
#include <QPen>
#include <QPolygonF>
namespace {
constexpr int kSamCropMargin = 32;
constexpr int kMinStrokePointsSam = 4;
constexpr int kMinStrokePointsManual = 8;
constexpr int kMaxSamPointPrompts = 32;
QVector<QPointF> snapStrokeToEdges(const QVector<QPointF>& strokeWorld, const QImage& bgImage, int searchRadiusPx) {
if (strokeWorld.size() < 3 || bgImage.isNull()) {
return strokeWorld;
}
QImage gray = bgImage.convertToFormat(QImage::Format_Grayscale8);
const int w = gray.width();
const int h = gray.height();
auto at = [&](int x, int y) -> int {
x = std::clamp(x, 0, w - 1);
y = std::clamp(y, 0, h - 1);
return static_cast<int>(static_cast<const uchar*>(gray.constScanLine(y))[x]);
};
auto gradMag = [&](int x, int y) -> int {
// 简易 Sobel 近似(整数)
const int gx =
-at(x - 1, y - 1) + at(x + 1, y - 1) +
-2 * at(x - 1, y) + 2 * at(x + 1, y) +
-at(x - 1, y + 1) + at(x + 1, y + 1);
const int gy =
-at(x - 1, y - 1) - 2 * at(x, y - 1) - at(x + 1, y - 1) +
at(x - 1, y + 1) + 2 * at(x, y + 1) + at(x + 1, y + 1);
return std::abs(gx) + std::abs(gy);
};
QVector<QPointF> out;
out.reserve(strokeWorld.size());
const int r = std::max(1, searchRadiusPx);
for (const QPointF& p : strokeWorld) {
const int cx = static_cast<int>(std::round(p.x()));
const int cy = static_cast<int>(std::round(p.y()));
int bestX = cx;
int bestY = cy;
int bestG = -1;
for (int dy = -r; dy <= r; ++dy) {
for (int dx = -r; dx <= r; ++dx) {
const int x = cx + dx;
const int y = cy + dy;
if (x < 0 || y < 0 || x >= w || y >= h) continue;
const int g = gradMag(x, y);
if (g > bestG) {
bestG = g;
bestX = x;
bestY = y;
}
}
}
out.push_back(QPointF(bestX, bestY));
}
return out;
}
bool buildSamSegmentPayloadFromStroke(
const QVector<QPointF>& strokeWorld,
const QImage& bgImage,
QByteArray& outCropPng,
QByteArray& outOverlayPng,
QPointF& outCropTopLeftWorld,
QJsonArray& outPointCoords,
QJsonArray& outPointLabels,
QJsonArray& outBoxXyxy
) {
if (strokeWorld.size() < kMinStrokePointsSam || bgImage.isNull()) {
return false;
}
const QRectF polyBr = QPolygonF(strokeWorld).boundingRect();
if (polyBr.isEmpty()) {
return false;
}
const QRect cropRect = entity_cutout::clampRectToImage(
polyBr.adjusted(-kSamCropMargin, -kSamCropMargin, kSamCropMargin, kSamCropMargin).toAlignedRect(),
bgImage.size());
if (cropRect.isEmpty()) {
return false;
}
outCropTopLeftWorld = cropRect.topLeft();
const QImage cropRgb = bgImage.copy(cropRect).convertToFormat(QImage::Format_RGB888);
QBuffer bufCrop(&outCropPng);
if (!bufCrop.open(QIODevice::WriteOnly) || !cropRgb.save(&bufCrop, "PNG")) {
outCropPng.clear();
return false;
}
bufCrop.close();
const QPointF origin = cropRect.topLeft();
const int cw = cropRect.width();
const int ch = cropRect.height();
QImage overlay(cw, ch, QImage::Format_ARGB32_Premultiplied);
overlay.fill(Qt::transparent);
{
QPainter pop(&overlay);
pop.setRenderHint(QPainter::Antialiasing, true);
QPen pen(QColor(255, 60, 60, 240));
pen.setWidthF(4.0);
pen.setCapStyle(Qt::RoundCap);
pen.setJoinStyle(Qt::RoundJoin);
pop.setPen(pen);
QPolygonF local;
local.reserve(strokeWorld.size());
for (const QPointF& w : strokeWorld) {
local.append(w - origin);
}
pop.drawPolyline(local);
}
QBuffer bufOv(&outOverlayPng);
if (!bufOv.open(QIODevice::WriteOnly) || !overlay.save(&bufOv, "PNG")) {
outOverlayPng.clear();
return false;
}
bufOv.close();
const int n = static_cast<int>(strokeWorld.size());
const int step = std::max(1, (n + kMaxSamPointPrompts - 1) / kMaxSamPointPrompts);
for (int i = 0; i < n; i += step) {
const QPointF L = strokeWorld[i] - origin;
outPointCoords.append(QJsonArray{L.x(), L.y()});
outPointLabels.append(1);
}
if (n > 1) {
const QPointF Llast = strokeWorld.last() - origin;
if (!outPointCoords.isEmpty()) {
const QJsonArray prev = outPointCoords.last().toArray();
if (prev.size() >= 2) {
const double dx = Llast.x() - prev.at(0).toDouble();
const double dy = Llast.y() - prev.at(1).toDouble();
if (dx * dx + dy * dy > 4.0) {
outPointCoords.append(QJsonArray{Llast.x(), Llast.y()});
outPointLabels.append(1);
}
}
}
}
const QRectF tight = QPolygonF(strokeWorld).boundingRect();
auto clampD = [](double v, double lo, double hi) { return std::clamp(v, lo, hi); };
double x1 = clampD(tight.left() - origin.x(), 0.0, static_cast<double>(cw - 1));
double y1 = clampD(tight.top() - origin.y(), 0.0, static_cast<double>(ch - 1));
double x2 = clampD(tight.right() - origin.x(), 0.0, static_cast<double>(cw - 1));
double y2 = clampD(tight.bottom() - origin.y(), 0.0, static_cast<double>(ch - 1));
if (x2 <= x1) {
x2 = std::min(static_cast<double>(cw - 1), x1 + 1.0);
}
if (y2 <= y1) {
y2 = std::min(static_cast<double>(ch - 1), y1 + 1.0);
}
outBoxXyxy = QJsonArray{x1, y1, x2, y2};
return true;
}
void drawCheckerboard(QPainter& p, const QRect& r) {
// 轻量级棋盘格,让透明/纯色背景也有参照
const int cell = 16;
@@ -45,41 +205,6 @@ void drawGrid(QPainter& p, const QRect& r) {
}
}
QPainterPath pathFromWorldPolygon(const QVector<QPointF>& poly) {
QPainterPath path;
if (poly.size() < 3) {
return path;
}
path.addPolygon(QPolygonF(poly));
path.closeSubpath();
return path;
}
QPointF polygonCentroid(const QVector<QPointF>& poly) {
// 简单多边形质心(面积加权)。若面积接近 0则退化为包围盒中心。
if (poly.size() < 3) {
return {};
}
double a2 = 0.0; // 2*area
double cx6a = 0.0;
double cy6a = 0.0;
for (int i = 0; i < poly.size(); ++i) {
const QPointF p0 = poly[i];
const QPointF p1 = poly[(i + 1) % poly.size()];
const double cross = static_cast<double>(p0.x()) * static_cast<double>(p1.y()) -
static_cast<double>(p1.x()) * static_cast<double>(p0.y());
a2 += cross;
cx6a += (static_cast<double>(p0.x()) + static_cast<double>(p1.x())) * cross;
cy6a += (static_cast<double>(p0.y()) + static_cast<double>(p1.y())) * cross;
}
if (std::abs(a2) < 1e-6) {
const QRectF bb = pathFromWorldPolygon(poly).boundingRect();
return bb.center();
}
const double inv6a = 1.0 / (3.0 * a2); // 1/(6A) with a2=2A -> 1/(3*a2)
return QPointF(cx6a * inv6a, cy6a * inv6a);
}
QRectF transformedRectByScaleAndTranslate(const QRectF& r, const QPointF& center, double scaleRatio, const QPointF& delta) {
if (r.isNull()) {
return r.translated(delta);
@@ -99,53 +224,6 @@ QRectF transformedRectByScaleAndTranslate(const QRectF& r, const QPointF& center
return QRectF(QPointF(minX, minY), QPointF(maxX, maxY));
}
QRect clampRectToImage(const QRect& r, const QSize& size) {
QRect out = r.normalized();
if (out.isNull()) {
return {};
}
out.setLeft(std::max(0, out.left()));
out.setTop(std::max(0, out.top()));
out.setRight(std::min(size.width() - 1, out.right()));
out.setBottom(std::min(size.height() - 1, out.bottom()));
if (out.width() <= 0 || out.height() <= 0) {
return {};
}
return out;
}
int computeAverageDepthInPolygon(const QImage& depth8, const QVector<QPointF>& polyWorld) {
if (depth8.isNull() || depth8.format() != QImage::Format_Grayscale8 || polyWorld.size() < 3) {
return 0;
}
const QPainterPath path = pathFromWorldPolygon(polyWorld);
if (path.isEmpty()) {
return 0;
}
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), depth8.size());
if (bbox.isNull()) {
return 0;
}
quint64 sum = 0;
quint64 cnt = 0;
for (int y = bbox.top(); y <= bbox.bottom(); ++y) {
const uchar* row = depth8.constScanLine(y);
for (int x = bbox.left(); x <= bbox.right(); ++x) {
// 取像素中心点做包含测试
if (!path.contains(QPointF(x + 0.5, y + 0.5))) {
continue;
}
sum += static_cast<quint64>(row[x]);
++cnt;
}
}
if (cnt == 0) {
return 0;
}
return static_cast<int>(sum / cnt);
}
int sampleDepthAtPoint(const QImage& depth8, const QPointF& worldPos) {
if (depth8.isNull() || depth8.format() != QImage::Format_Grayscale8) {
return 0;
@@ -174,56 +252,6 @@ double distanceScaleFromDepth01(double depth01, double calibMult) {
return raw;
}
QImage extractEntityImage(const QImage& bg, const QVector<QPointF>& polyWorld, QPointF& outTopLeftWorld) {
if (bg.isNull() || polyWorld.size() < 3) {
outTopLeftWorld = {};
return {};
}
const QPainterPath path = pathFromWorldPolygon(polyWorld);
if (path.isEmpty()) {
outTopLeftWorld = {};
return {};
}
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), bg.size());
if (bbox.isNull()) {
outTopLeftWorld = {};
return {};
}
outTopLeftWorld = bbox.topLeft();
QImage out(bbox.size(), QImage::Format_ARGB32_Premultiplied);
out.fill(Qt::transparent);
QPainter p(&out);
p.setRenderHint(QPainter::Antialiasing, true);
// 把 path 平移到裁剪图坐标系
QTransform tr;
tr.translate(-bbox.left(), -bbox.top());
const QPainterPath localPath = tr.map(path);
p.setClipPath(localPath);
p.drawImage(QPoint(0, 0), bg, bbox);
p.end();
return out;
}
void applyBlackFillToBackground(QImage& bgCutout, const QVector<QPointF>& polyWorld) {
if (bgCutout.isNull() || polyWorld.size() < 3) {
return;
}
QPainterPath path = pathFromWorldPolygon(polyWorld);
if (path.isEmpty()) {
return;
}
QPainter p(&bgCutout);
p.setRenderHint(QPainter::Antialiasing, true);
p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 255));
p.drawPath(path);
p.end();
}
struct GizmoHit {
EditorCanvas::DragMode mode = EditorCanvas::DragMode::None;
};
@@ -333,7 +361,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
for (const auto& lp : e.polygonLocal) {
polyTmp.push_back(originWorld + lp);
}
const QPointF cTmp = polyTmp.isEmpty() ? originWorld : polygonCentroid(polyTmp);
const QPointF cTmp = polyTmp.isEmpty() ? originWorld : entity_cutout::polygonCentroid(polyTmp);
const int z = (!m_depthImage8.isNull()) ? sampleDepthAtPoint(m_depthImage8, cTmp) : e.depth;
v.depth = z;
@@ -354,7 +382,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
// 贴图按 origin 缩放
v.imageTopLeft = originWorld + (e.imageTopLeftWorld - e.originWorld) * scale;
v.pathWorld = pathFromWorldPolygon(v.polygonWorld);
v.pathWorld = entity_cutout::pathFromWorldPolygon(v.polygonWorld);
v.rect = v.pathWorld.boundingRect();
v.color = QColor(255, 120, 0, 70);
@@ -394,7 +422,7 @@ void EditorCanvas::setEntities(const QVector<core::Project::Entity>& entities, c
if (m_selectedEntity >= 0) {
const auto& ent = m_entities[m_selectedEntity];
const QPointF origin =
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
} else if (!prevSelectedId.isEmpty()) {
emit selectedEntityChanged(false, QString(), 0, QPointF());
@@ -437,7 +465,7 @@ QPointF EditorCanvas::selectedEntityCentroidWorld() const {
}
const auto& ent = m_entities[m_selectedEntity];
if (!ent.polygonWorld.isEmpty()) {
return polygonCentroid(ent.polygonWorld);
return entity_cutout::polygonCentroid(ent.polygonWorld);
}
return ent.rect.center();
}
@@ -500,7 +528,7 @@ void EditorCanvas::presentationComputeZoomTarget(int entityIndex, QPointF* outPa
const QSizeF sz(ent.image.width() * ent.visualScale, ent.image.height() * ent.visualScale);
bb = QRectF(ent.imageTopLeft, sz);
} else if (!ent.polygonWorld.isEmpty()) {
bb = pathFromWorldPolygon(ent.polygonWorld).boundingRect();
bb = entity_cutout::pathFromWorldPolygon(ent.polygonWorld).boundingRect();
} else {
bb = ent.rect;
}
@@ -575,7 +603,7 @@ void EditorCanvas::selectEntityById(const QString& id) {
m_selectedEntity = i;
const auto& ent = m_entities[i];
const QPointF origin =
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
update();
return;
@@ -635,6 +663,58 @@ void EditorCanvas::setTool(Tool tool) {
update();
}
void EditorCanvas::setEntityCreateSegmentMode(EntityCreateSegmentMode m) {
if (m_entityCreateSegmentMode == m) {
return;
}
m_entityCreateSegmentMode = m;
update();
}
void EditorCanvas::setPendingEntityPolygonWorld(const QVector<QPointF>& polyWorld) {
m_pendingPolyWorld = polyWorld;
m_pendingDragging = false;
m_pendingDragWhole = false;
m_pendingDragVertex = -1;
update();
}
void EditorCanvas::clearPendingEntityPolygon() {
m_pendingPolyWorld.clear();
m_pendingDragging = false;
m_pendingDragWhole = false;
m_pendingDragVertex = -1;
update();
}
bool EditorCanvas::isPointNearPendingVertex(const QPointF& worldPos, int* outIndex) const {
if (outIndex) *outIndex = -1;
if (m_pendingPolyWorld.size() < 3) return false;
const qreal rView = 10.0;
const qreal rWorld = rView / std::max<qreal>(m_scale, 0.001);
const qreal r2 = rWorld * rWorld;
int best = -1;
qreal bestD2 = r2;
for (int i = 0; i < m_pendingPolyWorld.size(); ++i) {
const QPointF d = m_pendingPolyWorld[i] - worldPos;
const qreal d2 = d.x() * d.x() + d.y() * d.y();
if (d2 <= bestD2) {
bestD2 = d2;
best = i;
}
}
if (best >= 0) {
if (outIndex) *outIndex = best;
return true;
}
return false;
}
bool EditorCanvas::pendingPolygonContains(const QPointF& worldPos) const {
if (m_pendingPolyWorld.size() < 3) return false;
return entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld).contains(worldPos);
}
void EditorCanvas::resetView() {
m_scale = 1.0;
m_pan = QPointF(0, 0);
@@ -768,7 +848,7 @@ int EditorCanvas::hitTestEntity(const QPointF& worldPos) const {
continue;
}
if (!ent.polygonWorld.isEmpty()) {
const QPainterPath path = pathFromWorldPolygon(ent.polygonWorld);
const QPainterPath path = entity_cutout::pathFromWorldPolygon(ent.polygonWorld);
if (path.contains(worldPos)) {
return static_cast<int>(idx);
}
@@ -829,7 +909,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
m_bgImageCutout = m_bgImage;
for (const auto& ent : m_entities) {
if (!ent.cutoutPolygonWorld.isEmpty() && !m_bgImageCutout.isNull()) {
applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
entity_cutout::applyBlackFillToBackground(m_bgImageCutout, ent.cutoutPolygonWorld);
}
}
}
@@ -1030,6 +1110,21 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
p.drawLine(m_strokeWorld.first(), m_strokeWorld.last());
}
// 待确认实体:多边形预览 + 顶点
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
const qreal lw = 2.5 / std::max<qreal>(m_scale, 0.001);
p.setPen(QPen(QColor(60, 180, 255, 230), lw));
p.setBrush(QColor(60, 180, 255, 45));
const QPainterPath path = entity_cutout::pathFromWorldPolygon(m_pendingPolyWorld);
p.drawPath(path);
const qreal vr = 5.0 / std::max<qreal>(m_scale, 0.001);
p.setPen(QPen(QColor(0, 0, 0, 120), lw));
p.setBrush(QColor(255, 255, 255, 220));
for (const QPointF& v : m_pendingPolyWorld) {
p.drawEllipse(v, vr, vr);
}
}
p.restore();
// 坐标轴/刻度:绘制在画布最外层,背景越界时贴边显示
@@ -1090,7 +1185,7 @@ void EditorCanvas::paintEvent(QPaintEvent* e) {
if (isDragPreview) {
originWorld = m_dragCentroidBase + m_dragDelta;
} else if (!ent.polygonWorld.isEmpty()) {
originWorld = polygonCentroid(ent.polygonWorld);
originWorld = entity_cutout::polygonCentroid(ent.polygonWorld);
}
const QPointF originView = worldToView(originWorld);
@@ -1155,7 +1250,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
if (hit >= 0) {
const auto& ent = m_entities[hit];
const QPointF cWorld =
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
const QPointF anchorView = worldToView(cWorld);
beginPresentationZoomTowardEntity(hit);
emit presentationEntityIntroRequested(ent.id, anchorView);
@@ -1178,6 +1273,33 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
}
if (m_tool == Tool::CreateEntity && e->button() == Qt::LeftButton) {
// 若已有待确认多边形:进入微调(顶点/整体拖拽),或点击空白直接确认
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
m_dragging = true;
m_lastMouseView = e->position();
const QPointF w = viewToWorld(e->position());
m_pendingLastMouseWorld = w;
int vi = -1;
if (isPointNearPendingVertex(w, &vi)) {
m_pendingDragging = true;
m_pendingDragVertex = vi;
m_pendingDragWhole = false;
e->accept();
return;
}
if (pendingPolygonContains(w)) {
m_pendingDragging = true;
m_pendingDragWhole = true;
m_pendingDragVertex = -1;
e->accept();
return;
}
// 点击空白:确认
emit requestFinalizePendingEntity(m_pendingPolyWorld);
e->accept();
return;
}
m_dragging = true;
m_drawingEntity = true;
m_draggingEntity = false;
@@ -1207,7 +1329,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
if (isDragPreview) {
originWorld = m_dragCentroidBase + m_dragDelta;
} else if (!ent.polygonWorld.isEmpty()) {
originWorld = polygonCentroid(ent.polygonWorld);
originWorld = entity_cutout::polygonCentroid(ent.polygonWorld);
}
const QPointF originView = worldToView(originWorld);
const GizmoHit gh = hitTestGizmo(e->position(), originView);
@@ -1220,7 +1342,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
// 为了统一复用 move 逻辑:初始化 rect 与基准点
const QRectF r = m_entities[m_selectedEntity].rect.isNull() && !m_entities[m_selectedEntity].polygonWorld.isEmpty()
? pathFromWorldPolygon(m_entities[m_selectedEntity].polygonWorld).boundingRect()
? entity_cutout::pathFromWorldPolygon(m_entities[m_selectedEntity].polygonWorld).boundingRect()
: m_entities[m_selectedEntity].rect;
m_entities[m_selectedEntity].rect = r;
m_entityDragOffsetOriginWorld = viewToWorld(e->position()) - m_entities[m_selectedEntity].animatedOriginWorld;
@@ -1236,7 +1358,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
m_dragPolyBase = m_entities[m_selectedEntity].polygonWorld;
m_dragPathBase = m_entities[m_selectedEntity].pathWorld;
m_dragCentroidBase =
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : polygonCentroid(m_dragPolyBase);
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
update();
return;
}
@@ -1249,7 +1371,7 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
m_dragMode = DragMode::Free;
emit entityDragActiveChanged(true);
const QRectF r = m_entities[hit].rect.isNull() && !m_entities[hit].polygonWorld.isEmpty()
? pathFromWorldPolygon(m_entities[hit].polygonWorld).boundingRect()
? entity_cutout::pathFromWorldPolygon(m_entities[hit].polygonWorld).boundingRect()
: m_entities[hit].rect;
m_entities[hit].rect = r;
m_entityDragOffsetOriginWorld = worldPos - m_entities[hit].animatedOriginWorld;
@@ -1265,8 +1387,8 @@ void EditorCanvas::mousePressEvent(QMouseEvent* e) {
m_dragPolyBase = m_entities[hit].polygonWorld;
m_dragPathBase = m_entities[hit].pathWorld;
m_dragCentroidBase =
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : polygonCentroid(m_dragPolyBase);
const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? polygonCentroid(m_entities[hit].polygonWorld)
m_dragPolyBase.isEmpty() ? m_dragRectBase.center() : entity_cutout::polygonCentroid(m_dragPolyBase);
const QPointF origin = !m_entities[hit].polygonWorld.isEmpty() ? entity_cutout::polygonCentroid(m_entities[hit].polygonWorld)
: m_entities[hit].rect.center();
emit selectedEntityChanged(true, m_entities[hit].id, m_entities[hit].depth, origin);
update();
@@ -1326,6 +1448,21 @@ void EditorCanvas::mouseMoveEvent(QMouseEvent* e) {
const QPointF deltaView = cur - m_lastMouseView;
m_lastMouseView = cur;
if (m_pendingDragging && !m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
const QPointF curWorld = viewToWorld(e->position());
const QPointF delta = curWorld - m_pendingLastMouseWorld;
m_pendingLastMouseWorld = curWorld;
if (m_pendingDragWhole) {
for (auto& pt : m_pendingPolyWorld) {
pt += delta;
}
} else if (m_pendingDragVertex >= 0 && m_pendingDragVertex < m_pendingPolyWorld.size()) {
m_pendingPolyWorld[m_pendingDragVertex] += delta;
}
update();
return;
}
if (m_tool == Tool::CreateEntity && m_drawingEntity) {
const QPointF w = viewToWorld(cur);
if (m_strokeWorld.isEmpty()) {
@@ -1442,40 +1579,27 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
m_drawingEntity = false;
updateCursor();
if (m_strokeWorld.size() >= 8) {
// 闭合:末点连回首点(绘制时 polygon 自动闭合)
core::Project::Entity ent;
// id 由上层workspace/UI统一分配避免因过滤/隐藏导致重复
ent.id.clear();
ent.cutoutPolygonWorld = m_strokeWorld;
ent.originWorld = 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);
if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Manual) {
if (m_strokeWorld.size() >= kMinStrokePointsManual) {
setPendingEntityPolygonWorld(m_strokeWorld);
}
// 确保深度加载(用于平均深度)
if (m_depthDirty) {
m_depthDirty = false;
QImage img(m_depthAbsPath);
m_depthImage8 = img.isNull() ? QImage() : img.convertToFormat(QImage::Format_Grayscale8);
} else if (m_entityCreateSegmentMode == EntityCreateSegmentMode::Snap) {
if (m_strokeWorld.size() >= kMinStrokePointsManual) {
ensurePixmapLoaded();
const QVector<QPointF> snapped = snapStrokeToEdges(m_strokeWorld, m_bgImage, 6);
setPendingEntityPolygonWorld(snapped);
}
const QPointF c = polygonCentroid(ent.cutoutPolygonWorld);
ent.depth = m_depthImage8.isNull() ? 0 : sampleDepthAtPoint(m_depthImage8, c);
{
const double ds01 = depthToScale01(ent.depth);
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
} else if (m_strokeWorld.size() >= kMinStrokePointsSam) {
ensurePixmapLoaded();
QByteArray cropPng;
QByteArray ovPng;
QPointF cropOrigin;
QJsonArray pts;
QJsonArray labs;
QJsonArray box;
if (buildSamSegmentPayloadFromStroke(m_strokeWorld, m_bgImage, cropPng, ovPng, cropOrigin, pts, labs, box)) {
emit requestSamSegment(cropPng, ovPng, cropOrigin, pts, labs, box);
}
// 抠实体:从原背景抠出,背景对应区域填黑
QImage cutout;
if (!m_bgImage.isNull()) {
QPointF topLeft;
cutout = extractEntityImage(m_bgImage, ent.cutoutPolygonWorld, topLeft);
ent.imageTopLeftWorld = topLeft;
}
emit requestAddEntity(ent, cutout);
}
m_strokeWorld.clear();
update();
@@ -1497,12 +1621,17 @@ void EditorCanvas::mouseReleaseEvent(QMouseEvent* e) {
}
if (!sentMove && !ent.id.isEmpty()) {
const QPointF origin =
ent.polygonWorld.isEmpty() ? ent.rect.center() : polygonCentroid(ent.polygonWorld);
ent.polygonWorld.isEmpty() ? ent.rect.center() : entity_cutout::polygonCentroid(ent.polygonWorld);
emit selectedEntityChanged(true, ent.id, ent.depth, origin);
}
}
m_dragging = false;
if (m_pendingDragging && e->button() == Qt::LeftButton) {
m_pendingDragging = false;
m_pendingDragWhole = false;
m_pendingDragVertex = -1;
}
if (m_draggingEntity) {
emit entityDragActiveChanged(false);
}
@@ -1546,6 +1675,18 @@ void EditorCanvas::keyPressEvent(QKeyEvent* e) {
e->accept();
return;
}
if (!m_presentationPreviewMode && m_pendingPolyWorld.size() >= 3) {
if (e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) {
emit requestFinalizePendingEntity(m_pendingPolyWorld);
e->accept();
return;
}
if (e->key() == Qt::Key_Escape) {
clearPendingEntityPolygon();
e->accept();
return;
}
}
QWidget::keyPressEvent(e);
}

View File

@@ -2,6 +2,8 @@
#include "core/domain/Project.h"
#include <QByteArray>
#include <QJsonArray>
#include <QPixmap>
#include <QPointF>
#include <QImage>
@@ -33,6 +35,17 @@ public:
void setTool(Tool tool);
Tool tool() const { return m_tool; }
/// 创建实体的分割方式(在「创」工具被选中后再次单击弹出面板选择)。
enum class EntityCreateSegmentMode { Manual, Snap, Sam };
Q_ENUM(EntityCreateSegmentMode)
void setEntityCreateSegmentMode(EntityCreateSegmentMode m);
EntityCreateSegmentMode entityCreateSegmentMode() const { return m_entityCreateSegmentMode; }
/// 进入/更新「待确认」实体多边形(允许用户在画布上微调)。
void setPendingEntityPolygonWorld(const QVector<QPointF>& polyWorld);
void clearPendingEntityPolygon();
bool hasPendingEntityPolygon() const { return m_pendingPolyWorld.size() >= 3; }
void resetView();
void zoomToFit();
@@ -81,6 +94,16 @@ signals:
void hoveredWorldPosDepthChanged(const QPointF& worldPos, int depthZ);
void selectedEntityChanged(bool hasSelection, const QString& id, int depth, const QPointF& originWorld);
void requestAddEntity(const core::Project::Entity& entity, const QImage& image);
/// 创建实体:将裁剪 RGB、标记叠加层与 SAM 提示发往模型服务,由主窗口收 JSON 轮廓后再 addEntity。
void requestSamSegment(
const QByteArray& cropRgbPng,
const QByteArray& overlayPng,
const QPointF& cropTopLeftWorld,
const QJsonArray& pointCoords,
const QJsonArray& pointLabels,
const QJsonArray& boxXyxy);
/// 待确认实体:用户按回车/点击空白处确认后触发(由主窗口弹窗命名并落盘)。
void requestFinalizePendingEntity(const QVector<QPointF>& polyWorld);
void requestMoveEntity(const QString& id, const QPointF& delta);
void entityDragActiveChanged(bool on);
void selectedEntityPreviewChanged(const QString& id, int depth, const QPointF& originWorld);
@@ -106,6 +129,8 @@ private:
QPointF viewToWorld(const QPointF& v) const;
QPointF worldToView(const QPointF& w) const;
QRectF worldRectOfBackground() const;
bool isPointNearPendingVertex(const QPointF& worldPos, int* outIndex) const;
bool pendingPolygonContains(const QPointF& worldPos) const;
void tickPresentationZoomAnimation();
void tickPresentationHoverAnimation();
@@ -159,6 +184,7 @@ private:
bool m_presentationPreviewMode = false;
Tool m_tool = Tool::Move;
EntityCreateSegmentMode m_entityCreateSegmentMode = EntityCreateSegmentMode::Manual;
qreal m_scale = 1.0;
QPointF m_pan; // world 原点对应的 view 坐标偏移view = world*scale + pan
@@ -193,6 +219,13 @@ private:
int m_currentFrame = 0;
// —— 创建实体:待确认多边形(可微调)——
QVector<QPointF> m_pendingPolyWorld;
bool m_pendingDragging = false;
bool m_pendingDragWhole = false;
int m_pendingDragVertex = -1;
QPointF m_pendingLastMouseWorld;
// —— 预览展示:实体悬停动效、点击聚焦缩放 ——
QTimer* m_presZoomTimer = nullptr;
QTimer* m_presHoverTimer = nullptr;

View File

@@ -0,0 +1,110 @@
#include "editor/EntityCutoutUtils.h"
#include <algorithm>
#include <cmath>
#include <QPainter>
#include <QPolygonF>
#include <QTransform>
namespace entity_cutout {
QPainterPath pathFromWorldPolygon(const QVector<QPointF>& poly) {
QPainterPath path;
if (poly.size() < 3) {
return path;
}
path.addPolygon(QPolygonF(poly));
path.closeSubpath();
return path;
}
QPointF polygonCentroid(const QVector<QPointF>& poly) {
if (poly.size() < 3) {
return {};
}
double a2 = 0.0;
double cx6a = 0.0;
double cy6a = 0.0;
for (int i = 0; i < poly.size(); ++i) {
const QPointF p0 = poly[i];
const QPointF p1 = poly[(i + 1) % poly.size()];
const double cross = static_cast<double>(p0.x()) * static_cast<double>(p1.y()) -
static_cast<double>(p1.x()) * static_cast<double>(p0.y());
a2 += cross;
cx6a += (static_cast<double>(p0.x()) + static_cast<double>(p1.x())) * cross;
cy6a += (static_cast<double>(p0.y()) + static_cast<double>(p1.y())) * cross;
}
if (std::abs(a2) < 1e-6) {
const QRectF bb = pathFromWorldPolygon(poly).boundingRect();
return bb.center();
}
const double inv6a = 1.0 / (3.0 * a2);
return QPointF(cx6a * inv6a, cy6a * inv6a);
}
QRect clampRectToImage(const QRect& r, const QSize& size) {
QRect out = r.normalized();
if (out.isNull()) {
return {};
}
out.setLeft(std::max(0, out.left()));
out.setTop(std::max(0, out.top()));
out.setRight(std::min(size.width() - 1, out.right()));
out.setBottom(std::min(size.height() - 1, out.bottom()));
if (out.width() <= 0 || out.height() <= 0) {
return {};
}
return out;
}
QImage extractEntityImage(const QImage& bg, const QVector<QPointF>& polyWorld, QPointF& outTopLeftWorld) {
if (bg.isNull() || polyWorld.size() < 3) {
outTopLeftWorld = {};
return {};
}
const QPainterPath path = pathFromWorldPolygon(polyWorld);
if (path.isEmpty()) {
outTopLeftWorld = {};
return {};
}
const QRect bbox = clampRectToImage(path.boundingRect().toAlignedRect(), bg.size());
if (bbox.isNull()) {
outTopLeftWorld = {};
return {};
}
outTopLeftWorld = bbox.topLeft();
QImage out(bbox.size(), QImage::Format_ARGB32_Premultiplied);
out.fill(Qt::transparent);
QPainter p(&out);
p.setRenderHint(QPainter::Antialiasing, true);
QTransform tr;
tr.translate(-bbox.left(), -bbox.top());
const QPainterPath localPath = tr.map(path);
p.setClipPath(localPath);
p.drawImage(QPoint(0, 0), bg, bbox);
p.end();
return out;
}
void applyBlackFillToBackground(QImage& bgCutout, const QVector<QPointF>& polyWorld) {
if (bgCutout.isNull() || polyWorld.size() < 3) {
return;
}
QPainterPath path = pathFromWorldPolygon(polyWorld);
if (path.isEmpty()) {
return;
}
QPainter p(&bgCutout);
p.setRenderHint(QPainter::Antialiasing, true);
p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 255));
p.drawPath(path);
p.end();
}
} // namespace entity_cutout

View File

@@ -0,0 +1,18 @@
#pragma once
#include <QImage>
#include <QPointF>
#include <QPainterPath>
#include <QRect>
#include <QSize>
#include <QVector>
namespace entity_cutout {
QPainterPath pathFromWorldPolygon(const QVector<QPointF>& poly);
QPointF polygonCentroid(const QVector<QPointF>& poly);
QRect clampRectToImage(const QRect& r, const QSize& size);
QImage extractEntityImage(const QImage& bg, const QVector<QPointF>& polyWorld, QPointF& outTopLeftWorld);
void applyBlackFillToBackground(QImage& bgCutout, const QVector<QPointF>& polyWorld);
} // namespace entity_cutout