diff --git a/app/project/detail/OwnTaskTableContainer.tsx b/app/project/detail/OwnTaskTableContainer.tsx
index 5b03352..32ece3c 100644
--- a/app/project/detail/OwnTaskTableContainer.tsx
+++ b/app/project/detail/OwnTaskTableContainer.tsx
@@ -20,6 +20,7 @@ import {
Button,
Collapse,
Group,
+ MultiSelect,
Select,
SimpleGrid,
Stack,
@@ -63,10 +64,17 @@ const EMPTY_TASK_SIZE: Task.SizeProps = Object.freeze({
comment_question_size: 0,
})
-function createInitialFilters() {
+interface FilterFormRecord {
+ search_id: string
+ search_status: string
+ current_uid: string[]
+}
+
+function createInitialFilters(): FilterFormRecord {
return {
search_id: "",
search_status: "0",
+ current_uid: [],
}
}
@@ -342,6 +350,20 @@ export default function OwnTaskTableContainer(props: {
{searchExpanded ? "收起筛选" : "展开筛选"}
+
+ }
+ onClick={handleReset}>
+ 重置
+
+ }
+ onClick={handleSearch}
+ loading={loading}>
+ 查询
+
+
+ ({
+ label: y,
+ value: x.toString(),
+ }))}
+ searchable
+ clearable
+ value={filters.current_uid}
+ onChange={(v) => setFilters((s) => ({ ...s, current_uid: v }))}
+ />
-
- }
- onClick={handleReset}>
- 重置
-
- }
- onClick={handleSearch}
- loading={loading}>
- 查询
-
-
+
+ }
+ onClick={handleReset}>
+ 重置
+
+ }
+ onClick={handleSearch}
+ loading={loading}>
+ 查询
+
+
-
- }
- onClick={handleReset}>
- 重置
-
- }
- onClick={handleSearch}
- loading={loading}>
- 查询
-
-
{
+ paperContainerRef.current?.filterBySizeThreshold?.()
+ }}
renderPolygons={
paperContainerRef.current
? paperContainerRef.current.renderPolygons
diff --git a/components/label/components/AssistShapeComponent.tsx b/components/label/components/AssistShapeComponent.tsx
index 0316000..544cc43 100644
--- a/components/label/components/AssistShapeComponent.tsx
+++ b/components/label/components/AssistShapeComponent.tsx
@@ -6,6 +6,8 @@ import React, {
useImperativeHandle,
useRef,
} from "react"
+import { useBottomToolsStore } from "../useBottomToolsStore"
+import { usePaperStore } from "../usePaperStore"
import { useTopToolsStore } from "../useTopToolsStore"
interface AssistShapeComponentProps {
@@ -18,6 +20,10 @@ const AssistShapeComponent = (
) => {
const { size } = props
const scale = useTopToolsStore((state) => state.scale)
+ const activeImage = useBottomToolsStore((state) => state.activeImage)
+ const rasterScale = usePaperStore(
+ (state) => state.rasterScale[activeImage] ?? 1
+ )
const canvasRef = useRef(null)
const lastPointerRef = useRef<{ x: number; y: number } | null>(null)
@@ -27,7 +33,7 @@ const AssistShapeComponent = (
const ctx = canvas?.getContext("2d")
if (!canvas || !ctx) return
- const radius = Math.max(1, size * scale)
+ const radius = Math.max(1, size * scale * rasterScale)
const side = radius * 2
const left = centerX - radius
const top = centerY - radius
@@ -41,7 +47,7 @@ const AssistShapeComponent = (
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2)
ctx.stroke()
},
- [scale, size]
+ [rasterScale, scale, size]
)
useEffect(() => {
diff --git a/components/label/components/PaperContainer.tsx b/components/label/components/PaperContainer.tsx
index 247c7b1..a3f4a9f 100644
--- a/components/label/components/PaperContainer.tsx
+++ b/components/label/components/PaperContainer.tsx
@@ -46,7 +46,11 @@ import { usePermissionStore } from "../store/auth"
import { useBottomToolsStore } from "../useBottomToolsStore"
import { useKeyboardStore } from "../useKeyBoardStore"
import { useOtherToolsStore } from "../useOtherToolsStore"
-import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
+import {
+ clearSupportAnnotationItem,
+ syncDeletedPathGroupState,
+ usePaperStore,
+} from "../usePaperStore"
import { usePaperSupportStore } from "../usePaperSupportStore"
import { renderGroupPath } from "../useRenderGroupPath"
import useRenderTag from "../useRenderTag"
@@ -55,6 +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 { adjustPoints } from "../utils/paperjs"
import AssistShapeComponent from "./AssistShapeComponent"
import CrosshairComponent from "./CrosshairComponent"
@@ -158,6 +168,7 @@ const SHORTCUT_GUIDE_SECTIONS = [
title: "专项工具",
items: [
["F1", "进入辅助标注 / 在辅助模式下确认"],
+ ["F2", "按尺寸阈值过滤小区域 / 小镂空"],
["I", "执行追踪"],
["P", "QA 文本场景检查错词"],
],
@@ -1040,6 +1051,343 @@ const PaperContainer = (
}
}, [clearPendingFrameSwitch, loadingData])
+ const getSupportPreviewColors = useCallback(() => {
+ const operationSchema =
+ projectDetail?.label_schema_list?.find(
+ (item) =>
+ item.category_id === +useTopToolsStore.getState().activeOperation
+ ) || null
+
+ return {
+ strokeColor: operationSchema
+ ? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
+ : "rgb(0,0,0)",
+ fillColor: operationSchema
+ ? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
+ : "rgba(0,0,0,0.3)",
+ blankColor: operationSchema
+ ? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
+ : "rgba(0,0,0,0.01)",
+ }
+ }, [projectDetail?.label_schema_list])
+
+ const clearSupportPreviewShapeItems = useCallback(() => {
+ usePaperStore
+ .getState()
+ .group?.getItems({
+ match: (item: paper.Item) =>
+ item.data?.id === "support" &&
+ ["polygon", "parent"].includes(item.data?.type || ""),
+ })
+ .forEach((item) => {
+ item.remove()
+ })
+ }, [])
+
+ const renderSupportPreviewGeometry = useCallback(
+ (geometry: ReturnType) => {
+ const paperGroup = usePaperStore.getState().group
+ if (!paperGroup) return
+
+ clearSupportPreviewShapeItems()
+
+ const { points, hollowPoints } = multiPolygonToStoredContours(geometry)
+ if (!points.length) return
+
+ const { strokeColor, fillColor, blankColor } = getSupportPreviewColors()
+ const outerPaths = points
+ .map((contour) => {
+ if (contour.length <= 3) return null
+ const path = new paper.Path({
+ segments: contour.slice(0, -1),
+ fillColor: blankColor,
+ strokeColor,
+ closed: true,
+ data: {
+ id: "support",
+ type: "polygon",
+ isHollowPolygon: false,
+ fillColor,
+ strokeColor,
+ blankColor,
+ },
+ parent: paperGroup,
+ strokeScaling: false,
+ })
+ path.fillColor = new paper.Color(fillColor)
+ return path
+ })
+ .filter((path): path is paper.Path => !!path)
+ const innerPaths = hollowPoints
+ .map((contour) => {
+ if (contour.length <= 3) return null
+ return new paper.Path({
+ segments: contour.slice(0, -1),
+ fillColor: blankColor,
+ strokeColor,
+ closed: true,
+ data: {
+ id: "support",
+ type: "polygon",
+ isHollowPolygon: true,
+ fillColor,
+ strokeColor,
+ blankColor,
+ },
+ parent: paperGroup,
+ strokeScaling: false,
+ })
+ })
+ .filter((path): path is paper.Path => !!path)
+
+ const compoundPath = new paper.CompoundPath({
+ children: [...outerPaths, ...innerPaths],
+ data: {
+ id: "support",
+ type: "parent",
+ },
+ strokeColor,
+ strokeWidth: 2,
+ fillColor,
+ strokeScaling: false,
+ })
+ compoundPath.parent = paperGroup
+ },
+ [clearSupportPreviewShapeItems, getSupportPreviewColors]
+ )
+
+ function rerenderActiveImageObjects() {
+ const group = usePaperStore.getState().group
+ if (!group) return
+
+ group
+ .getItems({})
+ .filter((item) => item.data.id)
+ .forEach((item) => {
+ item.remove()
+ })
+
+ const imageData = useLabelStore.getState().label.get(activeImage)
+ if (!imageData) return
+
+ for (const key of imageData.keys()) {
+ renderPolygons(key, imageData)
+ }
+ }
+
+ const getMinAreaBySizeThreshold = useCallback((sizeThreshold: number) => {
+ const normalizedSizeThreshold = Math.max(0, Number(sizeThreshold) || 0)
+ // Threshold input is already expressed in the visible canvas coordinate
+ // system, so the area threshold should be size^2 without reapplying
+ // rasterScale.
+ return normalizedSizeThreshold * normalizedSizeThreshold
+ }, [])
+
+ function filterSupportPreviewBySizeThreshold(sizeThreshold: number) {
+ const group = usePaperStore.getState().group
+ if (!group) return false
+
+ const compoundPath = group.getItems({
+ data: { id: "support", type: "parent" },
+ })?.[0] as paper.CompoundPath | undefined
+ if (!compoundPath) return false
+
+ const { geometry, removedOuterCount, removedHoleCount } =
+ filterMultiPolygonByAreaThreshold({
+ geometry: paperShapeToMultiPolygon(compoundPath),
+ minArea: getMinAreaBySizeThreshold(sizeThreshold),
+ })
+
+ if (!removedOuterCount && !removedHoleCount) {
+ notifications.show({
+ color: "yellow",
+ message: "辅助结果中未发现小于尺寸阈值的区域或镂空",
+ })
+ return true
+ }
+
+ renderSupportPreviewGeometry(geometry)
+ notifications.show({
+ color: "green",
+ message: `已按尺寸阈值过滤 ${removedOuterCount} 个小区域、${removedHoleCount} 个小镂空`,
+ })
+ return true
+ }
+
+ function filterSelectedPolygonBySizeThreshold(sizeThreshold: number) {
+ const selectedIds =
+ useObjectStore.getState().selectedPath[activeImage] || []
+
+ if (!selectedIds.length) {
+ notifications.show({
+ color: "yellow",
+ message: "请先选中一个或多个 polygon 对象",
+ })
+ return false
+ }
+
+ const minArea = getMinAreaBySizeThreshold(sizeThreshold)
+ const nextLabel = safeClone(useLabelStore.getState().label)
+ let removedOuterCount = 0
+ let removedHoleCount = 0
+ let updatedObjectCount = 0
+ let removedObjectCount = 0
+ let skippedNonPolygonCount = 0
+ let skippedMissingCount = 0
+ const removedPathIds: number[] = []
+ const removedParentGroupIds: Array = []
+
+ selectedIds.forEach((selectedId) => {
+ const selectedItems = usePaperStore.getState().getItemsById(selectedId)
+ const selectedShape =
+ selectedItems.find((item) => item instanceof paper.CompoundPath) ||
+ selectedItems.find((item) => item.data?.type === "polygon") ||
+ null
+
+ if (!selectedShape || selectedShape.data?.type !== "polygon") {
+ skippedNonPolygonCount += 1
+ return
+ }
+
+ const operationId = String(selectedShape.data?.operationId || "")
+ if (!operationId) {
+ skippedMissingCount += 1
+ return
+ }
+
+ const operationData = nextLabel.get(activeImage)?.get(operationId)
+ const pathIndex =
+ operationData?.findIndex((item) => item[0] === selectedId) ?? -1
+ const oldData = pathIndex >= 0 ? operationData?.[pathIndex] : null
+
+ if (!operationData || !oldData) {
+ skippedMissingCount += 1
+ return
+ }
+
+ const {
+ geometry,
+ removedOuterCount: currentRemovedOuterCount,
+ removedHoleCount: currentRemovedHoleCount,
+ } = filterMultiPolygonByAreaThreshold({
+ geometry: contoursToMultiPolygon({
+ outerContours: oldData[1],
+ holeContours: oldData[3],
+ }),
+ minArea,
+ })
+
+ if (!currentRemovedOuterCount && !currentRemovedHoleCount) {
+ return
+ }
+
+ removedOuterCount += currentRemovedOuterCount
+ removedHoleCount += currentRemovedHoleCount
+
+ if (!geometry.length) {
+ nextLabel.get(activeImage)?.set(
+ operationId,
+ operationData.filter((item) => item[0] !== selectedId)
+ )
+ removedObjectCount += 1
+ removedPathIds.push(selectedId)
+ removedParentGroupIds.push(oldData[2]?.parentGroupId)
+ return
+ }
+
+ const { points, hollowPoints } = multiPolygonToStoredContours(geometry)
+ const currentDetail = oldData[2]
+ const nextDetail = currentDetail
+ ? {
+ ...currentDetail,
+ first_modified_timestamp:
+ currentDetail.first_modified_timestamp || dayjs().unix(),
+ first_modified_uid:
+ currentDetail.first_modified_uid ||
+ usePermissionStore.getState().user_id,
+ last_modified_timestamp: dayjs().unix(),
+ last_modified_uid: usePermissionStore.getState().user_id,
+ }
+ : {
+ ...initialDetail,
+ create_timestamp: dayjs().unix(),
+ }
+
+ operationData[pathIndex] = [selectedId, points, nextDetail, hollowPoints]
+ updatedObjectCount += 1
+ })
+
+ if (!updatedObjectCount && !removedObjectCount) {
+ notifications.show({
+ color: skippedNonPolygonCount ? "yellow" : "yellow",
+ message:
+ skippedNonPolygonCount === selectedIds.length
+ ? "当前选中对象仅 polygon 支持尺寸过滤"
+ : "当前选中对象中未发现小于尺寸阈值的区域或镂空",
+ })
+ return true
+ }
+
+ if (removedPathIds.length) {
+ syncDeletedPathGroupState({
+ imageId: activeImage,
+ removedPathIds,
+ removedParentGroupIds,
+ labelData: nextLabel,
+ })
+ removedPathIds.forEach((pathId) => {
+ useObjectStore
+ .getState()
+ .updateSelectedPath(activeImage, "DELETE", pathId)
+ })
+ }
+
+ useLabelStore.getState().setLabel(nextLabel)
+ useLabelStore.getState().pushStateStack(nextLabel)
+ rerenderActiveImageObjects()
+
+ const summaryParts = [
+ `已按尺寸阈值过滤 ${updatedObjectCount + removedObjectCount} 个 polygon`,
+ `删除 ${removedOuterCount} 个小区域`,
+ `删除 ${removedHoleCount} 个小镂空`,
+ ]
+ if (removedObjectCount) {
+ summaryParts.push(`移除 ${removedObjectCount} 个空对象`)
+ }
+ if (skippedNonPolygonCount) {
+ summaryParts.push(`跳过 ${skippedNonPolygonCount} 个非 polygon 对象`)
+ }
+ if (skippedMissingCount) {
+ summaryParts.push(`忽略 ${skippedMissingCount} 个异常对象`)
+ }
+
+ notifications.show({
+ color: "green",
+ message: summaryParts.join(","),
+ })
+ return true
+ }
+
+ function filterBySizeThreshold() {
+ const sizeThreshold = Number(
+ useTopToolsStore.getState().auxiliarySizeThreshold
+ )
+
+ if (!Number.isFinite(sizeThreshold) || sizeThreshold <= 0) {
+ notifications.show({
+ color: "yellow",
+ message: "请先设置大于 0 的尺寸阈值",
+ })
+ return
+ }
+
+ if (filterSupportPreviewBySizeThreshold(sizeThreshold)) {
+ return
+ }
+
+ filterSelectedPolygonBySizeThreshold(sizeThreshold)
+ }
+
const renderSupportAnnotation = useCallback(
async (
points: Array<[number, number]>,
@@ -1055,20 +1403,7 @@ const PaperContainer = (
loading: true,
autoClose: false,
})
- const operationSchema =
- projectDetail?.label_schema_list?.find(
- (item) =>
- item.category_id === +useTopToolsStore.getState().activeOperation
- ) || null
- const strokeColor = operationSchema
- ? `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
- : "rgb(0,0,0)"
- const fillColor = operationSchema
- ? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
- : "rgba(0,0,0,0.3)"
- const blankColor = operationSchema
- ? `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
- : "rgba(0,0,0,0.01)"
+ // const { strokeColor, fillColor, blankColor } = getSupportPreviewColors()
const currentScale =
usePaperStore.getState().rasterScale[activeImage] ?? 1
@@ -1141,13 +1476,11 @@ const PaperContainer = (
if (!promptPoints.length && !normalizedBoxes.length) {
throw new Error("缺少有效提示,无法调用模型接口")
}
- // const picSize = usePaperStore.getState().rasterSize[activeImage]
try {
const data = await getAuxiliaryAnnotation({
project_id: projectId,
image_name: activeImage,
- // project_id: "image_test_bk",
prompt: {
...(promptPoints.length
? { foreground_points: promptPoints }
@@ -1160,50 +1493,31 @@ const PaperContainer = (
contour_approximation: 0,
})
const { contours = [], hollows = [] } = data || {}
- const outerPaths: any[] = []
- const innerPaths: any[] = []
- contours.forEach((contour: any, index: number) => {
- const normalizedContour = normalizeContour(contour)
- if (normalizedContour.length > 3) {
- const path = new paper.Path({
- segments: normalizedContour.map(([x, y]) => [
- x * currentScale,
- y * currentScale,
- ]),
- fillColor: blankColor,
- strokeColor: strokeColor,
- closed: true,
- data: {
- id: "support",
- type: "polygon",
- isHollowPolygon: false,
- fillColor,
- strokeColor,
- blankColor,
- },
- parent: usePaperStore.getState().group,
- strokeScaling: false,
+ const geometry = contoursToMultiPolygon({
+ outerContours: contours
+ .map((contour: any, index: number) => {
+ if (hollows[index]) return null
+ return normalizeContour(contour).map(
+ ([x, y]) =>
+ [x * currentScale, y * currentScale] as [number, number]
+ )
})
- if (hollows[index]) {
- path.data.isHollowPolygon = true
- innerPaths.push(path)
- } else {
- path.fillColor = new paper.Color(fillColor)
- outerPaths.push(path)
- }
- }
+ .filter(
+ (contour): contour is Array<[number, number]> => !!contour
+ ),
+ holeContours: contours
+ .map((contour: any, index: number) => {
+ if (!hollows[index]) return null
+ return normalizeContour(contour).map(
+ ([x, y]) =>
+ [x * currentScale, y * currentScale] as [number, number]
+ )
+ })
+ .filter(
+ (contour): contour is Array<[number, number]> => !!contour
+ ),
})
- const compoundPath = new paper.CompoundPath({
- children: [...outerPaths, ...innerPaths],
- data: {
- id: "support",
- type: "parent",
- },
- strokeColor: strokeColor,
- strokeWidth: 2,
- fillColor: fillColor,
- })
- compoundPath.parent = usePaperStore.getState().group!
+ renderSupportPreviewGeometry(geometry)
notifications.show({
id: "sam2",
message: "模型生成成功",
@@ -1228,7 +1542,12 @@ const PaperContainer = (
}
}
},
- [activeImage, projectDetail]
+ [
+ activeImage,
+ // getSupportPreviewColors,
+ projectDetail,
+ renderSupportPreviewGeometry,
+ ]
)
// const renderTrackingAnnotation = useCallback(async () => {
@@ -4153,6 +4472,7 @@ const PaperContainer = (
renderTrackingAnnotation,
renderSupportAnnotation,
saveSupportAnnotationData,
+ filterBySizeThreshold,
copyAnnotationDataToMultiFrame,
}))
diff --git a/components/label/components/RightObjectTools.tsx b/components/label/components/RightObjectTools.tsx
index cb1509e..5770106 100644
--- a/components/label/components/RightObjectTools.tsx
+++ b/components/label/components/RightObjectTools.tsx
@@ -8,8 +8,8 @@ import {
Collapse,
Flex,
Group,
+ HoverCard,
Paper,
- Popover,
ScrollArea,
Stack,
Text,
@@ -31,6 +31,7 @@ import {
} from "lucide-react"
import paper from "paper"
import { useCallback, useMemo, useState } from "react"
+import { Comment } from "../api/label/typing"
import { Project } from "../api/project/typing"
import { Task } from "../api/task/typing"
import { LabelState } from "../LabelNossr"
@@ -141,6 +142,65 @@ const RightObjectTools: React.FC = (props) => {
}
}, [])
+ const getCommentTextColor = useCallback((type: number) => {
+ switch (type) {
+ case 3:
+ return "yellow.9"
+ case 2:
+ return "red.7"
+ case 1:
+ return "green.7"
+ case 0:
+ return "gray.7"
+ default:
+ return "blue.7"
+ }
+ }, [])
+
+ const getUserLabel = useCallback(
+ (uid?: number) => {
+ if (uid === undefined || uid === null) return "-"
+ return (
+ userOpts.find((item) => item.value === uid)?.label || uid.toString()
+ )
+ },
+ [userOpts]
+ )
+
+ const getCommentCount = useCallback((commentItem: Comment) => {
+ const checkBoxComments = (commentItem.check_box_comments || []).filter(
+ (item) => item?.trim()
+ )
+ const textComments = (commentItem.text_comments || []).filter((item) =>
+ item?.trim()
+ )
+ return checkBoxComments.length + textComments.length
+ }, [])
+
+ const getCommentContent = useCallback((commentItem: Comment) => {
+ const mergedComments = [
+ ...(commentItem.check_box_comments || []),
+ ...(commentItem.text_comments || []),
+ ].filter((item) => item?.trim())
+ return mergedComments.length ? mergedComments.join(",") : "无批注"
+ }, [])
+
+ const getCommentDisplayType = useCallback(
+ (commentItem: Comment, lastModifiedTimestamp?: number) => {
+ const type = Math.max(0, Math.min(3, commentItem.comment_type ?? 0))
+ if (
+ type === 2 &&
+ lastModifiedTimestamp &&
+ lastModifiedTimestamp > commentItem.date &&
+ getCommentCount(commentItem) > 0
+ ) {
+ return 3
+ }
+ return type
+ },
+ [getCommentCount]
+ )
+
const { shift: pressShift } = useKeyEventStore()
const continuePolygonTarget = usePaperStore(
@@ -696,15 +756,10 @@ const RightObjectTools: React.FC = (props) => {
comment.length === 1
? comment[comment.length - 1]
: comment[comment.length - 2]
- let type = latest_comment.comment_type
- if (type === 2) {
- const { last_modified_timestamp } = child[2]
- if (
- last_modified_timestamp &&
- last_modified_timestamp > latest_comment.date
- )
- type = 3
- }
+ const type = getCommentDisplayType(
+ latest_comment,
+ child[2].last_modified_timestamp
+ )
visibleFlag = checkBoxsChecked[3 - type]
}
if (checkBoxsChecked.every((value) => !value))
@@ -954,12 +1009,7 @@ const RightObjectTools: React.FC = (props) => {
创建:
- {
- userOpts.find(
- (item) =>
- item.value === child[2]?.uid
- )?.label
- }
+ {getUserLabel(child[2]?.uid)}
{dayjs(
@@ -972,14 +1022,9 @@ const RightObjectTools: React.FC = (props) => {
首次修改:
- {
- userOpts.find(
- (item) =>
- item.value ===
- child[2]
- ?.first_modified_uid
- )?.label
- }
+ {getUserLabel(
+ child[2]?.first_modified_uid
+ )}
{dayjs(
@@ -994,14 +1039,9 @@ const RightObjectTools: React.FC = (props) => {
最新修改:
- {
- userOpts.find(
- (item) =>
- item.value ===
- child[2]
- ?.last_modified_uid
- )?.label
- }
+ {getUserLabel(
+ child[2]?.last_modified_uid
+ )}
{dayjs(
@@ -1027,33 +1067,28 @@ const RightObjectTools: React.FC = (props) => {
{comment ? (
- {comment.map((c: any) => {
- const count =
- c.check_box_comments.length +
- c.text_comments.length
- const { last_modified_timestamp } =
- child[2]
- let flag = false
- if (
- count > 0 &&
- last_modified_timestamp &&
- last_modified_timestamp > c.date
- )
- flag = true
-
- let badgeColor = "gray"
- if (c.comment_type === 1)
- badgeColor = "green"
- if (c.comment_type === 2)
- badgeColor = "red"
- if (flag) badgeColor = "yellow"
+ {comment.map((c: Comment) => {
+ const count = getCommentCount(c)
+ const commentType =
+ getCommentDisplayType(
+ c,
+ child[2]?.last_modified_timestamp
+ )
+ const badgeColor =
+ getCheckBoxColor(commentType)
+ const commentTextColor =
+ getCommentTextColor(commentType)
return (
-
-
+ withArrow
+ shadow="md"
+ openDelay={100}
+ closeDelay={100}
+ withinPortal>
+
= (props) => {
}}>
{count}
-
-
-
- {`P${child[2].image_id}:${
- child[2].uid
- }:${c.check_box_comments.join(
- ","
- )},${c.text_comments.join(
- ","
- )} ${
+
+
+
+ {`P${child[2].image_id}:${getUserLabel(
+ c.uid
+ )}:${getCommentContent(c)} ${
c.date
? dayjs(
c.date * 1000
@@ -1082,8 +1115,8 @@ const RightObjectTools: React.FC = (props) => {
: ""
}`}
-
-
+
+
)
})}
diff --git a/components/label/components/RightTaskTools.tsx b/components/label/components/RightTaskTools.tsx
index 1f1d57b..1a4d02b 100644
--- a/components/label/components/RightTaskTools.tsx
+++ b/components/label/components/RightTaskTools.tsx
@@ -25,6 +25,7 @@ import {
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useMemo, useState } from "react"
import { Project } from "../api/project/typing"
+import { Task } from "../api/task/typing"
import { getTaskList } from "../api/task"
import { useObjectStore } from "../store"
import type { Option } from "../store/auth"
@@ -38,11 +39,54 @@ interface RightTaskToolsComponentProps {
task_id: number
}
+interface FilterOption {
+ label: string
+ value: string
+}
+
interface FilterOptions {
- all_user: Option[]
- label_user: Option[]
- review_user1: Option[]
- review_user2: Option[]
+ all_user: FilterOption[]
+ label_user: FilterOption[]
+ review_user1: FilterOption[]
+ review_user2: FilterOption[]
+}
+
+interface FilterFormValues {
+ label_status: string[]
+ current_uid: string[]
+ label_user: string[]
+ review_user1: string[]
+ review_user2: string[]
+ rejected: string | null
+ id: string
+}
+
+type TaskListFilterParams = Partial<
+ Pick<
+ Task.ListRequest,
+ | "label_status"
+ | "current_uid"
+ | "label_user"
+ | "review_user1"
+ | "review_user2"
+ | "rejected"
+ | "id"
+ >
+>
+
+const normalizeFilterOption = (option: Option): FilterOption | null => {
+ if (option.value === null || option.value === undefined) return null
+
+ return {
+ label: option.label,
+ value: String(option.value),
+ }
+}
+
+const pushUniqueOption = (options: FilterOption[], option: FilterOption) => {
+ if (!options.some((item) => item.value === option.value)) {
+ options.push(option)
+ }
}
const RightTaskTools: React.FC = (props) => {
@@ -57,11 +101,12 @@ const RightTaskTools: React.FC = (props) => {
const { userOpts } = useAllUserStore()
- const [taskList, setTaskList] = useState([])
+ const [taskList, setTaskList] = useState([])
const [activePage, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
+ const [filterOpened, setFilterOpened] = useState(false)
- const filterForm = useForm({
+ const filterForm = useForm({
initialValues: {
label_status: [],
current_uid: [],
@@ -72,32 +117,25 @@ const RightTaskTools: React.FC = (props) => {
id: "",
},
})
- const [filterParams, setFilterParams] = useState({})
- const isParamsEmpty: boolean = JSON.stringify(filterParams) === "{}"
+ const [filterParams, setFilterParams] = useState({})
+ const isParamsEmpty = Object.keys(filterParams).length === 0
const getListData = useCallback(async () => {
let pg_num = 0,
pg_size = 1000
let total = 0
- let tasksCount = 1000000
- let finalData: any[] = []
+ let tasksCount = Number.POSITIVE_INFINITY
+ let finalData: Task.DataProps[] = []
while (total < tasksCount) {
pg_num += 1
- const params = filterParams
- ? {
- project_id: [projectId],
- get_data: true,
- page_number: pg_num,
- page_size: pg_size,
- ...filterParams,
- }
- : {
- project_id: [projectId],
- get_data: true,
- page_number: pg_num,
- page_size: pg_size,
- }
+ const params: Task.ListRequest = {
+ project_id: [projectId],
+ get_data: true,
+ page_number: pg_num,
+ page_size: pg_size,
+ ...filterParams,
+ }
const res = await getTaskList(params)
const { total_items, task_list } = res
finalData = [...finalData, ...task_list]
@@ -112,6 +150,18 @@ const RightTaskTools: React.FC = (props) => {
queueMicrotask(() => getListData())
}, [getListData])
+ const normalizedUserOpts = useMemo(() => {
+ return userOpts.reduce((result, item) => {
+ const normalizedOption = normalizeFilterOption(item)
+
+ if (normalizedOption) {
+ result.push(normalizedOption)
+ }
+
+ return result
+ }, [])
+ }, [userOpts])
+
const filterUserOpts = useMemo(() => {
const opts: FilterOptions = {
all_user: [],
@@ -121,29 +171,33 @@ const RightTaskTools: React.FC = (props) => {
}
if (!projectDetail) return opts
- const steps = projectDetail.label_process.step_detail
+ const normalizedUserMap = new Map(
+ normalizedUserOpts.map((item) => [item.value, item])
+ )
+ const steps = projectDetail.label_process.step_detail ?? []
steps.forEach(({ action, user_list }) => {
- let users: any[] = []
- user_list.forEach((v) => {
- userOpts.forEach((item) => {
- if (item.value === v) {
- users.push(item)
- const flag = opts.all_user.find((u) => u.value === v)
- if (!flag) opts.all_user.push(item)
- }
- })
+ const users: FilterOption[] = []
+
+ user_list.forEach((userId) => {
+ const matchedUser = normalizedUserMap.get(String(userId))
+
+ if (!matchedUser) return
+
+ pushUniqueOption(users, matchedUser)
+ pushUniqueOption(opts.all_user, matchedUser)
})
+
if (action === 1) {
- opts.label_user.push(...users)
+ users.forEach((user) => pushUniqueOption(opts.label_user, user))
} else if (action === 2) {
- opts.review_user1.push(...users)
+ users.forEach((user) => pushUniqueOption(opts.review_user1, user))
} else if (action === 3) {
- opts.review_user2.push(...users)
+ users.forEach((user) => pushUniqueOption(opts.review_user2, user))
}
})
return opts
- }, [projectDetail, userOpts])
+ }, [normalizedUserOpts, projectDetail])
const handleTurnPrevious = () => {
const index = taskList.findIndex((task) => task.id === taskId)
@@ -185,12 +239,14 @@ const RightTaskTools: React.FC = (props) => {
label: value,
value: key,
}))}
+ comboboxProps={{ withinPortal: false }}
{...filterForm.getInputProps("label_status")}
clearable
/>
= (props) => {
= (props) => {
= (props) => {
= (props) => {
{ label: "是", value: "1" },
{ label: "否", value: "0" },
]}
+ comboboxProps={{ withinPortal: false }}
{...filterForm.getInputProps("rejected")}
clearable
/>
@@ -237,23 +297,36 @@ const RightTaskTools: React.FC = (props) => {
filterForm.reset()
setFilterParams({})
setPage(1)
+ setFilterOpened(false)
}}>
重置
@@ -276,12 +349,18 @@ const RightTaskTools: React.FC = (props) => {
-
+
+ color={!isParamsEmpty ? "blue" : "gray"}
+ onClick={() => setFilterOpened((opened) => !opened)}>
diff --git a/components/label/components/TopTools.tsx b/components/label/components/TopTools.tsx
index 7d7770d..a3f4ae9 100644
--- a/components/label/components/TopTools.tsx
+++ b/components/label/components/TopTools.tsx
@@ -120,6 +120,7 @@ interface TopToolsComponentProps {
updateOldWorkLoad: (data: WorkLoad) => void
isLabelTask: boolean
onOperationChange: (operationId: string) => void
+ onFilterBySizeThreshold?: () => void
renderPolygons: (
operationId: string,
imageData: Map | undefined
@@ -164,6 +165,7 @@ const TopTools = (
updateOldWorkLoad,
isLabelTask,
onOperationChange,
+ onFilterBySizeThreshold,
renderPolygons,
} = props
const { backUrl, basePath } = useBackUrlStore()
@@ -182,6 +184,9 @@ const TopTools = (
const [operationPickerOpened, setOperationPickerOpened] = useState(false)
const [operationKeyword, setOperationKeyword] = useState("")
const labelData = useLabelStore((state) => state.label)
+ const labelDefaultComments = useLabelStore(
+ (state) => state.labelDefaultComments
+ )
const setLabel = useLabelStore((state) => state.setLabel)
const pushStateStack = useLabelStore((state) => state.pushStateStack)
const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
@@ -230,6 +235,8 @@ const TopTools = (
setAssistToolEnabled,
assistToolSize,
setAssistToolSize,
+ auxiliarySizeThreshold,
+ setAuxiliarySizeThreshold,
nodeSize,
setNodeSize,
needBackup,
@@ -238,6 +245,8 @@ const TopTools = (
setMagnetFlag,
} = useTopToolsStore()
const [nodeSizeInput, setNodeSizeInput] = useState(nodeSize)
+ const [auxiliarySizeThresholdInput, setAuxiliarySizeThresholdInput] =
+ useState(auxiliarySizeThreshold)
const { pathGroupMap } = useRightToolsStore()
const isAutoSave = useIntervalStore((state) => state.isAutoSave)
@@ -261,6 +270,10 @@ const TopTools = (
usePaperStore.getState().refreshNodeVisualSize()
}, [nodeSize])
+ useEffect(() => {
+ setAuxiliarySizeThresholdInput(auxiliarySizeThreshold)
+ }, [auxiliarySizeThreshold])
+
const toggleRightPanel = useCallback(
(visible: boolean, setter: (val: boolean) => void) => {
const nextVisible = !visible
@@ -1025,10 +1038,41 @@ const TopTools = (
})
return
}
- const finalData = adjustAllPoints(
+ const recoveredData = adjustAllPoints(
backupData,
usePaperStore.getState().rasterScale
)
+ const finalData = safeClone(recoveredData)
+ for (const [imageId, categoryMap] of finalData.entries()) {
+ const currentDetailById = new Map()
+ labelData.get(imageId)?.forEach((objects) => {
+ objects.forEach(([id, _points, detail]) => {
+ currentDetailById.set(id, detail)
+ })
+ })
+ for (const [operationId, objects] of categoryMap.entries()) {
+ categoryMap.set(
+ operationId,
+ objects.map(([id, points, detail, hollowPoints]) => {
+ const currentDetail = currentDetailById.get(id)
+ return [
+ id,
+ points,
+ {
+ ...detail,
+ comment: currentDetail?.comment
+ ? safeClone(currentDetail.comment)
+ : safeClone(labelDefaultComments),
+ last_modified_timestamp:
+ currentDetail?.last_modified_timestamp ??
+ detail.last_modified_timestamp,
+ },
+ hollowPoints,
+ ]
+ }) as any
+ )
+ }
+ }
for (const entry of finalData.entries()) {
// 赋值用过的id
for (const category of entry[1].values()) {
@@ -1089,6 +1133,8 @@ const TopTools = (
},
[
activeImage,
+ labelData,
+ labelDefaultComments,
projectDetail?.label_type,
pushStateStack,
renderPolygons,
@@ -2186,39 +2232,84 @@ const TopTools = (
-
-
-
- 节点
-
- {
- setNodeSizeInput(value)
- }}
- onFocus={() => {
- useKeyEventStore.getState().setFocusInput(true)
- }}
- onBlur={(event) => {
- useKeyEventStore.getState().setFocusInput(false)
- const rawValue = event.currentTarget.value.trim()
- const nextValue =
- rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
- setNodeSize(
- Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
- )
- }}
- />
-
-
+
+
+
+
+ 节点
+
+ {
+ setNodeSizeInput(value)
+ }}
+ onFocus={() => {
+ useKeyEventStore.getState().setFocusInput(true)
+ }}
+ onBlur={(event) => {
+ useKeyEventStore.getState().setFocusInput(false)
+ const rawValue = event.currentTarget.value.trim()
+ const nextValue =
+ rawValue === "" ? DEFAULT_NODE_SIZE : Number(rawValue)
+ setNodeSize(
+ Number.isFinite(nextValue) ? nextValue : DEFAULT_NODE_SIZE
+ )
+ }}
+ />
+
+
+
+
+
+ 尺寸
+
+ {
+ setAuxiliarySizeThresholdInput(value)
+ }}
+ onFocus={() => {
+ useKeyEventStore.getState().setFocusInput(true)
+ }}
+ onBlur={(event) => {
+ useKeyEventStore.getState().setFocusInput(false)
+ const rawValue = event.currentTarget.value.trim()
+ const nextValue = rawValue === "" ? 0 : Number(rawValue)
+ setAuxiliarySizeThreshold(
+ Number.isFinite(nextValue) ? nextValue : 0
+ )
+ }}
+ />
+
+
+
+
{renderEditIcon}