style(code): format
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
# 辅助标注尺寸阈值过滤实施记录
|
||||
|
||||
## 背景
|
||||
在 `F1 -> 点选/框选 -> 请求 getAuxiliaryAnnotation -> 生成辅助轮廓 -> F1 确认生成对象` 的流程里,接口可能返回大量很小的区域或小镂空,影响最终对象质量与后续编辑效率。
|
||||
|
||||
## 目标
|
||||
- 在工具栏提供尺寸阈值输入框。
|
||||
- 提供按钮与快捷键,对小于阈值的区域/镂空进行过滤。
|
||||
- 该能力既能用于 `F1` 生成后的辅助预览,也能用于已确认生成并被选中的 `polygon` 对象。
|
||||
- 不影响正常标注流程。
|
||||
|
||||
## 链路分析
|
||||
1. `LabelNossr.tsx`
|
||||
- `F1` 进入 `support` 模式。
|
||||
- 再次 `F1` 时调用 `PaperContainer.saveSupportAnnotationData()` 完成确认。
|
||||
2. `usePaperStore.ts`
|
||||
- `supportTool` 收集点/框提示,调用 `PaperContainer.renderSupportAnnotation()`。
|
||||
3. `PaperContainer.tsx`
|
||||
- `renderSupportAnnotation()` 请求 `getAuxiliaryAnnotation()`。
|
||||
- 接口返回 `contours/hollows` 后,在画布上生成 `support` 预览。
|
||||
- `saveSupportAnnotationData()` 将 `support` 预览转成正式标注对象。
|
||||
|
||||
## 本次实现
|
||||
### 已完成
|
||||
- 在 `components/label/useTopToolsStore.ts` 增加 `auxiliarySizeThreshold` 状态。
|
||||
- 新增 `components/label/utils/geometry/areaFilter.ts`,用于按尺寸过滤 multipolygon 外轮廓与洞。
|
||||
- 在 `components/label/components/TopTools.tsx` 增加:
|
||||
- 尺寸阈值输入框
|
||||
- `过滤` 按钮
|
||||
- 在 `components/label/LabelNossr.tsx` 增加 `F2` 快捷键,触发尺寸过滤。
|
||||
- 在 `components/label/components/PaperContainer.tsx` 增加:
|
||||
- 辅助预览 geometry 重绘能力
|
||||
- 对 `support` 预览执行尺寸过滤
|
||||
- 对已选中 `polygon` 对象执行尺寸过滤
|
||||
- 若过滤后对象为空,则直接删除对象
|
||||
- 快捷键说明中补充 `F2` 文案。
|
||||
|
||||
### 当前行为
|
||||
- 若当前存在 `support` 预览,`F2` / `过滤` 优先作用于辅助预览。
|
||||
- 若当前没有 `support` 预览,则对当前选中的一个或多个 `polygon` 对象执行批量过滤。
|
||||
- 输入值含义为“尺寸阈值”,实际面积阈值为“尺寸阈值²”。
|
||||
- 小外轮廓会被删除;小镂空会被填平。
|
||||
- 阈值需大于 `0` 才会生效。
|
||||
|
||||
## 验证
|
||||
- 已执行:`pnpm exec tsc --noEmit`
|
||||
- 结果:通过
|
||||
|
||||
## 后续可选项
|
||||
- 支持一次过滤多个已选中 polygon 对象。
|
||||
- 在工具栏中增加更明确的阈值单位说明。
|
||||
- 如果需要,可再补一轮手动回归用例清单。
|
||||
@@ -59,12 +59,12 @@ import { useTopToolsStore } from "../useTopToolsStore"
|
||||
import { buildSubAttributeFormValues, checkCommentsIsSame } from "../util"
|
||||
import { safeClone } from "../utils/clone"
|
||||
import { labelTypeMap } from "../utils/constants"
|
||||
import { filterMultiPolygonByAreaThreshold } from "../utils/geometry/areaFilter"
|
||||
import {
|
||||
contoursToMultiPolygon,
|
||||
multiPolygonToStoredContours,
|
||||
paperShapeToMultiPolygon,
|
||||
} from "../utils/geometry/booleanAdapter"
|
||||
import { filterMultiPolygonByAreaThreshold } from "../utils/geometry/areaFilter"
|
||||
import { adjustPoints } from "../utils/paperjs"
|
||||
import AssistShapeComponent from "./AssistShapeComponent"
|
||||
import CrosshairComponent from "./CrosshairComponent"
|
||||
@@ -1177,11 +1177,9 @@ const PaperContainer = (
|
||||
|
||||
const getMinAreaBySizeThreshold = useCallback(
|
||||
(sizeThreshold: number) => {
|
||||
const normalizedSizeThreshold = Math.max(
|
||||
0,
|
||||
Number(sizeThreshold) || 0
|
||||
)
|
||||
const currentScale = usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||
const normalizedSizeThreshold = Math.max(0, Number(sizeThreshold) || 0)
|
||||
const currentScale =
|
||||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||
return (
|
||||
normalizedSizeThreshold *
|
||||
normalizedSizeThreshold *
|
||||
@@ -1274,14 +1272,17 @@ const PaperContainer = (
|
||||
return
|
||||
}
|
||||
|
||||
const { geometry, removedOuterCount: currentRemovedOuterCount, removedHoleCount: currentRemovedHoleCount } =
|
||||
filterMultiPolygonByAreaThreshold({
|
||||
geometry: contoursToMultiPolygon({
|
||||
outerContours: oldData[1],
|
||||
holeContours: oldData[3],
|
||||
}),
|
||||
minArea,
|
||||
})
|
||||
const {
|
||||
geometry,
|
||||
removedOuterCount: currentRemovedOuterCount,
|
||||
removedHoleCount: currentRemovedHoleCount,
|
||||
} = filterMultiPolygonByAreaThreshold({
|
||||
geometry: contoursToMultiPolygon({
|
||||
outerContours: oldData[1],
|
||||
holeContours: oldData[3],
|
||||
}),
|
||||
minArea,
|
||||
})
|
||||
|
||||
if (!currentRemovedOuterCount && !currentRemovedHoleCount) {
|
||||
return
|
||||
@@ -1291,12 +1292,10 @@ const PaperContainer = (
|
||||
removedHoleCount += currentRemovedHoleCount
|
||||
|
||||
if (!geometry.length) {
|
||||
nextLabel
|
||||
.get(activeImage)
|
||||
?.set(
|
||||
operationId,
|
||||
operationData.filter((item) => item[0] !== selectedId)
|
||||
)
|
||||
nextLabel.get(activeImage)?.set(
|
||||
operationId,
|
||||
operationData.filter((item) => item[0] !== selectedId)
|
||||
)
|
||||
removedObjectCount += 1
|
||||
removedPathIds.push(selectedId)
|
||||
removedParentGroupIds.push(oldData[2]?.parentGroupId)
|
||||
@@ -1344,7 +1343,9 @@ const PaperContainer = (
|
||||
labelData: nextLabel,
|
||||
})
|
||||
removedPathIds.forEach((pathId) => {
|
||||
useObjectStore.getState().updateSelectedPath(activeImage, "DELETE", pathId)
|
||||
useObjectStore
|
||||
.getState()
|
||||
.updateSelectedPath(activeImage, "DELETE", pathId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1375,7 +1376,9 @@ const PaperContainer = (
|
||||
}
|
||||
|
||||
function filterBySizeThreshold() {
|
||||
const sizeThreshold = Number(useTopToolsStore.getState().auxiliarySizeThreshold)
|
||||
const sizeThreshold = Number(
|
||||
useTopToolsStore.getState().auxiliarySizeThreshold
|
||||
)
|
||||
|
||||
if (!Number.isFinite(sizeThreshold) || sizeThreshold <= 0) {
|
||||
notifications.show({
|
||||
@@ -1407,8 +1410,7 @@ const PaperContainer = (
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
})
|
||||
const { strokeColor, fillColor, blankColor } =
|
||||
getSupportPreviewColors()
|
||||
// const { strokeColor, fillColor, blankColor } = getSupportPreviewColors()
|
||||
const currentScale =
|
||||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||||
|
||||
@@ -1503,7 +1505,8 @@ const PaperContainer = (
|
||||
.map((contour: any, index: number) => {
|
||||
if (hollows[index]) return null
|
||||
return normalizeContour(contour).map(
|
||||
([x, y]) => [x * currentScale, y * currentScale] as [number, number]
|
||||
([x, y]) =>
|
||||
[x * currentScale, y * currentScale] as [number, number]
|
||||
)
|
||||
})
|
||||
.filter(
|
||||
@@ -1513,7 +1516,8 @@ const PaperContainer = (
|
||||
.map((contour: any, index: number) => {
|
||||
if (!hollows[index]) return null
|
||||
return normalizeContour(contour).map(
|
||||
([x, y]) => [x * currentScale, y * currentScale] as [number, number]
|
||||
([x, y]) =>
|
||||
[x * currentScale, y * currentScale] as [number, number]
|
||||
)
|
||||
})
|
||||
.filter(
|
||||
@@ -1545,7 +1549,12 @@ const PaperContainer = (
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeImage, getSupportPreviewColors, projectDetail, renderSupportPreviewGeometry]
|
||||
[
|
||||
activeImage,
|
||||
// getSupportPreviewColors,
|
||||
projectDetail,
|
||||
renderSupportPreviewGeometry,
|
||||
]
|
||||
)
|
||||
|
||||
// const renderTrackingAnnotation = useCallback(async () => {
|
||||
|
||||
@@ -242,9 +242,8 @@ const TopTools = (
|
||||
setMagnetFlag,
|
||||
} = useTopToolsStore()
|
||||
const [nodeSizeInput, setNodeSizeInput] = useState<number | string>(nodeSize)
|
||||
const [auxiliarySizeThresholdInput, setAuxiliarySizeThresholdInput] = useState<
|
||||
number | string
|
||||
>(auxiliarySizeThreshold)
|
||||
const [auxiliarySizeThresholdInput, setAuxiliarySizeThresholdInput] =
|
||||
useState<number | string>(auxiliarySizeThreshold)
|
||||
|
||||
const { pathGroupMap } = useRightToolsStore()
|
||||
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
# 尺寸阈值预设档位方案 A 说明
|
||||
|
||||
## 背景
|
||||
当前项目中,辅助结果 / 选中 `polygon` 的过滤能力已经支持“尺寸阈值”输入:
|
||||
|
||||
- 用户输入一个尺寸值 `n`
|
||||
- 实际参与过滤的面积阈值为 `n²`
|
||||
- 在画布坐标中再结合当前图片缩放比例换算后执行过滤
|
||||
|
||||
这套方式已经可以满足精细调参,但在高频使用时,仍然存在一个体验问题:
|
||||
|
||||
- 用户每次都需要手动输入数字
|
||||
- 常用值虽然固定,但容易忘记
|
||||
- 即使理解了 `尺寸² = 面积阈值`,操作上仍然偏“参数化”而不是“工具化”
|
||||
|
||||
因此可以考虑引入“方案 A:尺寸阈值预设档位”。
|
||||
|
||||
## 方案 A 是什么
|
||||
在现有“尺寸阈值”输入框旁,增加一组常用快捷档位按钮,例如:
|
||||
|
||||
- `8`
|
||||
- `16`
|
||||
- `32`
|
||||
- `64`
|
||||
|
||||
点击任意一个档位时:
|
||||
|
||||
- 自动将尺寸阈值设置为该值
|
||||
- 不改变现有过滤逻辑
|
||||
- 用户仍然可以继续手动输入其他值
|
||||
|
||||
这不是替代手输,而是在手输之外增加“常用值一键设置”能力。
|
||||
|
||||
## 为什么考虑做
|
||||
### 1. 降低操作成本
|
||||
很多场景下,用户反复尝试的其实是几个相对稳定的阈值。
|
||||
|
||||
如果每次都手输:
|
||||
- 需要点击输入框
|
||||
- 输入数字
|
||||
- 再触发过滤
|
||||
|
||||
如果有预设档位:
|
||||
- 一次点击即可完成阈值设置
|
||||
- 再配合 `F2` 或直接点击过滤按钮,路径更短
|
||||
|
||||
### 2. 降低记忆负担
|
||||
当前用户需要自己记住:
|
||||
- `16² = 256`
|
||||
- `32² = 1024`
|
||||
- `64² = 4096`
|
||||
|
||||
有了固定档位后,用户不需要每次重新心算或回忆常用数值,久而久之会形成稳定操作习惯。
|
||||
|
||||
### 3. 更符合工具型操作习惯
|
||||
标注工具里高频操作通常更适合:
|
||||
- 固定档位
|
||||
- 一键切换
|
||||
- 快速试错
|
||||
|
||||
而不是每次都进入“参数输入”模式。
|
||||
|
||||
预设档位能让这个功能更像一个成熟工具,而不是一个单纯的数值配置项。
|
||||
|
||||
### 4. 与当前心智模型兼容
|
||||
当前已经将输入语义改成“尺寸阈值”,用户理解的是“边长感 / 尺寸感”。
|
||||
|
||||
在这个基础上,预设 `8 / 16 / 32 / 64` 是自然延伸:
|
||||
- 数字仍然是“尺寸”
|
||||
- 实际面积阈值仍然是 `尺寸²`
|
||||
- 不会引入新的概念
|
||||
|
||||
## 为什么建议优先采用数字档位
|
||||
相较于“极小 / 小 / 中 / 大”这类文字档位,数字档位更适合当前项目:
|
||||
|
||||
### 1. 语义更精确
|
||||
`32` 就是 `32`,不会有解释歧义。
|
||||
|
||||
而“中”在不同用户理解里可能对应:
|
||||
- 16
|
||||
- 24
|
||||
- 32
|
||||
- 48
|
||||
|
||||
### 2. 与现有输入框保持一致
|
||||
当前输入框本身就是数值输入,预设按钮如果也是数值,用户的认知路径最短。
|
||||
|
||||
### 3. 便于后续沟通与复现
|
||||
当团队成员交流时,可以直接说:
|
||||
- “试一下 16”
|
||||
- “32 过滤得更合适”
|
||||
- “这批图用 64 比较稳”
|
||||
|
||||
这比说“用中档”更容易复现与共享经验。
|
||||
|
||||
## 建议的预设档位
|
||||
建议初始只放 4 个:
|
||||
|
||||
- `8` → 实际面积阈值 `64`
|
||||
- `16` → 实际面积阈值 `256`
|
||||
- `32` → 实际面积阈值 `1024`
|
||||
- `64` → 实际面积阈值 `4096`
|
||||
|
||||
原因:
|
||||
- 数量少,不会挤占工具栏空间
|
||||
- 都是 2 的幂,容易记忆
|
||||
- 能覆盖从较小噪声到较大碎片区域的大多数场景
|
||||
|
||||
如果后续真实使用中发现跨度不够,再考虑补充:
|
||||
- `24`
|
||||
- `48`
|
||||
- `96`
|
||||
|
||||
但第一版不建议一开始就放太多。
|
||||
|
||||
## 如果要做,准备怎么做
|
||||
### UI 位置
|
||||
建议放在现有 `TopTools` 中“尺寸阈值”输入框旁边,保持同一区域:
|
||||
|
||||
- 输入框仍然保留
|
||||
- 预设档位按钮放在输入框右侧或下方
|
||||
- 当前选中的档位可以有高亮态
|
||||
|
||||
### 交互行为
|
||||
建议行为如下:
|
||||
|
||||
1. 点击某个预设档位
|
||||
- 直接把尺寸阈值设置为该值
|
||||
- 同步更新输入框显示
|
||||
- 不自动执行过滤
|
||||
|
||||
2. 用户随后:
|
||||
- 点击“过滤”按钮,或
|
||||
- 按 `F2`
|
||||
|
||||
这样可以和当前交互习惯保持一致,避免点击预设后立刻修改结果,减少误操作。
|
||||
|
||||
### 状态同步
|
||||
预设档位只是“设置输入值”的快捷方式,因此状态仍然复用现有尺寸阈值:
|
||||
|
||||
- 不需要新增一套独立过滤逻辑
|
||||
- 不需要新增第二套 store 字段
|
||||
- 只需要在 UI 层加上档位按钮和“当前档位是否命中”的判断
|
||||
|
||||
### 提示文案
|
||||
建议鼠标悬浮时展示:
|
||||
- 当前尺寸值
|
||||
- 对应面积阈值
|
||||
|
||||
例如:
|
||||
- `32(面积阈值 1024)`
|
||||
- `64(面积阈值 4096)`
|
||||
|
||||
## 做了之后的好处
|
||||
### 1. 高频场景效率更高
|
||||
对于常用阈值,用户几乎不再需要键盘输入。
|
||||
|
||||
### 2. 功能更容易被理解和使用
|
||||
一些用户即使理解了“尺寸阈值”的概念,也未必愿意频繁输入参数。
|
||||
|
||||
预设档位会明显降低上手门槛。
|
||||
|
||||
### 3. 更适合形成团队共识
|
||||
如果多人协作标注,预设档位更容易沉淀成经验值:
|
||||
- 某类图片常用 `16`
|
||||
- 某类图片常用 `32`
|
||||
- 某类图片常用 `64`
|
||||
|
||||
### 4. 不破坏现有能力
|
||||
因为手动输入仍然保留,所以:
|
||||
- 精细调参能力不丢
|
||||
- 只是新增一个更顺手的入口
|
||||
|
||||
## 风险与注意点
|
||||
### 1. 工具栏空间会更紧张
|
||||
如果按钮做得太多,会挤压现有工具布局。
|
||||
|
||||
因此第一版建议:
|
||||
- 只放少量档位
|
||||
- 使用较紧凑的按钮样式
|
||||
|
||||
### 2. 不宜默认自动过滤
|
||||
点击档位后如果立刻执行过滤,会让用户难以控制操作时机。
|
||||
|
||||
建议保留“设置值”和“执行过滤”两个动作分离。
|
||||
|
||||
### 3. 预设值不一定覆盖所有场景
|
||||
所以必须保留手动输入作为兜底方式。
|
||||
|
||||
## 当前结论
|
||||
方案 A 值得考虑,原因是:
|
||||
- 实现成本不高
|
||||
- 不会改动底层过滤逻辑
|
||||
- 对高频操作的收益明确
|
||||
- 与当前“尺寸阈值”心智模型完全兼容
|
||||
|
||||
但当前阶段不急于实现,先记录方案,后续根据你的使用体验再决定是否上线。
|
||||
Reference in New Issue
Block a user