2120 lines
70 KiB
TypeScript
2120 lines
70 KiB
TypeScript
"use client"
|
||
|
||
import {
|
||
Box,
|
||
Button,
|
||
Card,
|
||
Checkbox,
|
||
Group,
|
||
Modal,
|
||
Radio,
|
||
ScrollArea,
|
||
Select,
|
||
Stack,
|
||
Text,
|
||
Textarea,
|
||
TextInput,
|
||
} from "@mantine/core"
|
||
import { useForm } from "@mantine/form"
|
||
import { notifications } from "@mantine/notifications"
|
||
import dayjs from "dayjs"
|
||
import msgpack from "msgpack-lite"
|
||
import paper from "paper"
|
||
import React, {
|
||
DispatchWithoutAction,
|
||
forwardRef,
|
||
useCallback,
|
||
useEffect,
|
||
useImperativeHandle,
|
||
useMemo,
|
||
useRef,
|
||
useState,
|
||
} from "react"
|
||
import {
|
||
getAuxiliaryAnnotation,
|
||
getServerImage,
|
||
getTrackingAuxiliaryAnnotation,
|
||
} from "../api/label"
|
||
import { Project } from "../api/project/typing"
|
||
import { LabelState } from "../LabelNossr"
|
||
import {
|
||
initialDetail,
|
||
useImagesStore,
|
||
useKeyEventStore,
|
||
useLabelStore,
|
||
useObjectStore,
|
||
} from "../store"
|
||
import { usePermissionStore } from "../store/auth"
|
||
import { useBottomToolsStore } from "../useBottomToolsStore"
|
||
import { useKeyboardStore } from "../useKeyBoardStore"
|
||
import { useOtherToolsStore } from "../useOtherToolsStore"
|
||
import { clearSupportAnnotationItem, usePaperStore } from "../usePaperStore"
|
||
import { renderGroupPath } from "../useRenderGroupPath"
|
||
import useRenderTag from "../useRenderTag"
|
||
import { useRightToolsStore } from "../useRightToolsStore"
|
||
import { useTopToolsStore } from "../useTopToolsStore"
|
||
import { checkCommentsIsSame } from "../util"
|
||
import { labelTypeMap } from "../utils/constants"
|
||
import { adjustPoints } from "../utils/paperjs"
|
||
import CrosshairComponent from "./CrosshairComponent"
|
||
import { renderOperationIcon } from "./RightObjectTools"
|
||
|
||
interface PaperComponentProps {
|
||
imgSrc: string
|
||
forceUpdate: DispatchWithoutAction
|
||
projectDetail?: Project.DetailResponse
|
||
labelState: LabelState
|
||
updateLoadingFlag: (flag: boolean) => void
|
||
}
|
||
|
||
const renderPath = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Path(
|
||
Object.assign({ parent: paperGroup, closed: true }, option)
|
||
)
|
||
}
|
||
|
||
const renderCompoundPath = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.CompoundPath(
|
||
Object.assign({ parent: paperGroup, closed: true }, option)
|
||
)
|
||
}
|
||
|
||
const renderRectangle = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Path.Rectangle(Object.assign({ parent: paperGroup }, option))
|
||
}
|
||
|
||
const renderCircle = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Path.Circle(Object.assign({ parent: paperGroup }, option))
|
||
}
|
||
|
||
const renderRaster = (paperGroup: paper.Group, option: any) => {
|
||
return new paper.Raster(Object.assign({ parent: paperGroup }, option))
|
||
}
|
||
|
||
const PaperContainer = (
|
||
props: PaperComponentProps,
|
||
ref: React.Ref<unknown> | undefined
|
||
) => {
|
||
const { forceUpdate, projectDetail, labelState, updateLoadingFlag } = props
|
||
const containerRef = useRef<any>(null)
|
||
const [imgSize, setImageSize] = useState<{
|
||
clientWidth: number
|
||
clientHeight: number
|
||
}>()
|
||
|
||
const [firstRender, setFirstRender] = useState(false)
|
||
const [confirmModalOpened, setConfirmModalOpened] = useState(false)
|
||
const [confirmModalCallback, setConfirmModalCallback] = useState<
|
||
((flag?: boolean) => void) | null
|
||
>(null)
|
||
|
||
// 监听画布容器宽高变化
|
||
useEffect(() => {
|
||
const container = containerRef.current
|
||
const resizeObserver = new ResizeObserver((entries) => {
|
||
for (let entry of entries) {
|
||
if (entry.target === container) {
|
||
setImageSize({
|
||
clientWidth: entry.contentRect.width,
|
||
clientHeight: entry.contentRect.height,
|
||
})
|
||
if (!firstRender) setFirstRender(true)
|
||
}
|
||
}
|
||
})
|
||
if (container) {
|
||
resizeObserver.observe(container)
|
||
}
|
||
|
||
return () => {
|
||
if (container) {
|
||
resizeObserver.unobserve(container)
|
||
}
|
||
}
|
||
}, [firstRender])
|
||
|
||
const canvasElem = useRef<HTMLCanvasElement>(null)
|
||
|
||
const {
|
||
init,
|
||
initViewTool,
|
||
initPanTool,
|
||
initPolygonTool,
|
||
initRectangleTool,
|
||
initBrushTool,
|
||
initPointTool,
|
||
initSupportTool,
|
||
addPolygon,
|
||
addBrush,
|
||
addPointLine,
|
||
addPoint,
|
||
addCompoundPath,
|
||
addRectangle,
|
||
setRasterPath,
|
||
setRasterSize,
|
||
setRasterScale,
|
||
loadingData,
|
||
} = usePaperStore()
|
||
|
||
const { activeImage } = useBottomToolsStore()
|
||
|
||
const {
|
||
label: storeLabel,
|
||
deleteOperation,
|
||
setOperation,
|
||
getLabelDetailDataByKeys,
|
||
} = useLabelStore()
|
||
|
||
const {
|
||
objectOperations,
|
||
imageFilter,
|
||
crosshairStatus,
|
||
showTags,
|
||
showTagsConfigs,
|
||
} = useTopToolsStore()
|
||
|
||
const {
|
||
pathStatus: paperPathStatus,
|
||
selectedPath,
|
||
updateSelectedPath,
|
||
} = useObjectStore()
|
||
|
||
const {
|
||
showContextMenu,
|
||
position,
|
||
operationId,
|
||
id,
|
||
setContextMenuId,
|
||
setContextMenuOperationId,
|
||
} = useOtherToolsStore()
|
||
const { renderTag } = useRenderTag()
|
||
|
||
const [setup, setSetup] = useState(false)
|
||
|
||
const paperScope = useRef<paper.PaperScope>(null)
|
||
useEffect(() => {
|
||
if (firstRender) {
|
||
paperScope.current = new paper.PaperScope()
|
||
if (canvasElem.current) {
|
||
paperScope.current.setup(canvasElem.current)
|
||
setSetup(true)
|
||
}
|
||
}
|
||
}, [firstRender])
|
||
|
||
useEffect(() => {
|
||
const pScope = paperScope.current
|
||
if (pScope) {
|
||
const view = pScope.view
|
||
view.bounds.height = imgSize!.clientHeight
|
||
view.bounds.width = imgSize!.clientWidth
|
||
// 调整可见图层大小
|
||
const layer = pScope.project.activeLayer
|
||
layer.view.viewSize = new paper.Size(
|
||
imgSize!.clientWidth,
|
||
imgSize!.clientHeight
|
||
)
|
||
view.center = new paper.Point(
|
||
view.bounds.width / 2,
|
||
view.bounds.height / 2
|
||
)
|
||
}
|
||
}, [imgSize])
|
||
|
||
const renderSupportAnnotation = useCallback(
|
||
async (
|
||
points: Array<[number, number]>,
|
||
tags: number[],
|
||
clear_previous_state = false
|
||
) => {
|
||
try {
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成中",
|
||
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 currentScale =
|
||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||
|
||
const draw = () => {
|
||
points.forEach((point, index) => {
|
||
const drawPoint = new paper.Path.Circle({
|
||
center: point,
|
||
radius: 2,
|
||
fillColor: tags[index] === 1 ? "red" : "blue",
|
||
strokeColor: tags[index] === 1 ? "red" : "blue",
|
||
data: {
|
||
id: "support",
|
||
type: "point",
|
||
},
|
||
})
|
||
drawPoint.parent = usePaperStore.getState().group!
|
||
const raster = usePaperStore
|
||
.getState()
|
||
.group!.getItems({ data: { name: "pic" } })[0]
|
||
if (!raster.contains(drawPoint.position)) {
|
||
throw new Error("点位超出图片范围")
|
||
}
|
||
})
|
||
}
|
||
console.log(points, tags)
|
||
// 绘制点位
|
||
clearSupportAnnotationItem()
|
||
draw()
|
||
|
||
const picSize = usePaperStore.getState().rasterSize[activeImage]
|
||
|
||
const params = {
|
||
project_id: projectDetail?.id || 0,
|
||
file_name: activeImage,
|
||
image_hw:
|
||
picSize && picSize.length ? [picSize[1], picSize[0]] : [0, 0],
|
||
points_and_box: points.map(([x, y]) => [
|
||
Math.round(x / currentScale),
|
||
Math.round(y / currentScale),
|
||
]),
|
||
tags,
|
||
clear_previous_state,
|
||
}
|
||
console.log("传参", params)
|
||
try {
|
||
const res = await getAuxiliaryAnnotation(
|
||
msgpack.encode(params) as any
|
||
)
|
||
const data = msgpack.decode(new Uint8Array(res))
|
||
console.log(data)
|
||
if (data.msg === "success") {
|
||
const { contours, hollows } = data
|
||
const outerPaths: any[] = []
|
||
const innerPaths: any[] = []
|
||
contours.forEach((contour: any, index: number) => {
|
||
if (contour.length > 3) {
|
||
const path = new paper.Path({
|
||
segments: contour.map(([x, y]: [number, number]) => [
|
||
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,
|
||
})
|
||
if (
|
||
Math.abs(path.area) < useTopToolsStore.getState().checkSize
|
||
) {
|
||
path.remove()
|
||
} else {
|
||
// path.scaling = usePaperStore.getState().group!.scaling;
|
||
if (hollows[index]) {
|
||
// 更新标志位
|
||
path.data.isHollowPolygon = true
|
||
innerPaths.push(path)
|
||
} else {
|
||
// 上色
|
||
path.fillColor = new paper.Color(fillColor)
|
||
outerPaths.push(path)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
const compoundPath = new paper.CompoundPath({
|
||
children: [...outerPaths, ...innerPaths],
|
||
data: {
|
||
id: "support",
|
||
type: "parent",
|
||
},
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
fillColor: fillColor,
|
||
})
|
||
compoundPath.parent = usePaperStore.getState().group!
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成成功",
|
||
color: "green",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
} catch (error: any) {
|
||
console.log(error)
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: error.message ?? "模型生成失败",
|
||
color: "red",
|
||
})
|
||
if (error.message === "点位超出图片范围") {
|
||
usePaperStore
|
||
.getState()
|
||
.supportTool?.emit("keydown", { key: "escape" })
|
||
}
|
||
}
|
||
},
|
||
[activeImage, projectDetail]
|
||
)
|
||
|
||
const renderTrackingAnnotation = useCallback(async () => {
|
||
updateLoadingFlag(true)
|
||
try {
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成中",
|
||
loading: true,
|
||
autoClose: false,
|
||
})
|
||
const ids =
|
||
useObjectStore.getState().selectedPath[
|
||
useBottomToolsStore.getState().activeImage
|
||
]
|
||
const picArr = useBottomToolsStore.getState().selectedItems
|
||
const [width, height] = usePaperStore.getState().rasterSize[
|
||
activeImage
|
||
] ?? [0, 0]
|
||
const currentScale =
|
||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||
if (ids.length === 1) {
|
||
const id = ids[0]
|
||
let categoryId: any = null
|
||
let data: any = null
|
||
const activeImageData = useLabelStore.getState().label.get(activeImage)!
|
||
for (let [category, objArr] of activeImageData.entries()) {
|
||
objArr.forEach((item) => {
|
||
if (item[0] === id) {
|
||
categoryId = category
|
||
data = item
|
||
}
|
||
})
|
||
}
|
||
console.log(data)
|
||
if (categoryId && data) {
|
||
const contours: [number, number][] = []
|
||
const tags: boolean[] = []
|
||
data[1].forEach((item: any) => {
|
||
contours.push(
|
||
item.map(([x, y]: any) => [
|
||
Math.round(x / currentScale),
|
||
Math.round(y / currentScale),
|
||
])
|
||
)
|
||
tags.push(false)
|
||
})
|
||
data[3].forEach((item: any) => {
|
||
contours.push(
|
||
item.map(([x, y]: any) => [
|
||
Math.round(x / currentScale),
|
||
Math.round(y / currentScale),
|
||
])
|
||
)
|
||
tags.push(true)
|
||
})
|
||
const file_names = [...new Set([activeImage, ...picArr])]
|
||
const params = {
|
||
project_id: projectDetail?.id || 0,
|
||
file_names,
|
||
image_hw: [height, width],
|
||
contours,
|
||
hollows: tags,
|
||
}
|
||
console.log("传参", params)
|
||
try {
|
||
const res = await getTrackingAuxiliaryAnnotation(
|
||
msgpack.encode(params)
|
||
)
|
||
const resData = msgpack.decode(new Uint8Array(res))
|
||
console.log(resData)
|
||
if (resData.msg === "success") {
|
||
const {
|
||
future_contours,
|
||
}: {
|
||
future_contours: {
|
||
contours: [number, number][][]
|
||
hollows: boolean[]
|
||
}[]
|
||
} = resData
|
||
const nowTaskData = structuredClone(
|
||
useLabelStore.getState().label
|
||
)
|
||
future_contours.forEach(({ contours, hollows }, index) => {
|
||
const fileName = file_names[index + 1]
|
||
const imgScale =
|
||
usePaperStore.getState().rasterScale[fileName] ?? 1
|
||
let newId =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
useRightToolsStore.getState().pushPathId(newId)
|
||
// 处理点位数据
|
||
let segmentation: Array<[number, number][]> = []
|
||
let hollow_segmentation: Array<[number, number][]> = []
|
||
contours.forEach((contour, index) => {
|
||
if (!hollows[index])
|
||
segmentation.push(
|
||
contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
||
)
|
||
else
|
||
hollow_segmentation.push(
|
||
contour.map(([x, y]: any) => [x * imgScale, y * imgScale])
|
||
)
|
||
})
|
||
// 详情数据
|
||
let detail = {
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: {},
|
||
image_id: activeImage,
|
||
category_id: categoryId,
|
||
}
|
||
const updateData: any = [
|
||
newId,
|
||
segmentation,
|
||
detail,
|
||
hollow_segmentation,
|
||
]
|
||
if (nowTaskData.has(fileName)) {
|
||
const categoryMap = nowTaskData.get(fileName)!
|
||
if (categoryMap.has(categoryId)) {
|
||
const existArr = categoryMap.get(categoryId)
|
||
existArr?.push(updateData)
|
||
} else {
|
||
categoryMap.set(categoryId, [updateData])
|
||
}
|
||
} else {
|
||
let m = new Map()
|
||
m.set(categoryId, [updateData])
|
||
nowTaskData.set(fileName, m)
|
||
}
|
||
})
|
||
console.log(nowTaskData)
|
||
useLabelStore.getState().setLabel(nowTaskData)
|
||
useLabelStore.getState().pushStateStack(nowTaskData)
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成成功",
|
||
color: "green",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
}
|
||
}
|
||
} else if (!ids.length) {
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "请先选中标注对象后再进行模型调用",
|
||
color: "red",
|
||
})
|
||
} else {
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "请勿选择多个标注对象进行模型调用",
|
||
color: "red",
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
notifications.show({
|
||
id: "sam2",
|
||
message: "模型生成失败",
|
||
color: "red",
|
||
})
|
||
} finally {
|
||
updateLoadingFlag(false)
|
||
}
|
||
}, [activeImage, projectDetail?.id, updateLoadingFlag])
|
||
|
||
// const getBase64 = (file: any): Promise<string> =>
|
||
// new Promise((resolve, reject) => {
|
||
// const reader = new FileReader();
|
||
// reader.readAsDataURL(file);
|
||
// reader.onload = () => resolve(reader.result as string);
|
||
// reader.onerror = (error) => reject(error);
|
||
// });
|
||
|
||
useEffect(() => {
|
||
if (setup) {
|
||
const newGroup = new paper.Group({
|
||
applyMatrix: false,
|
||
selected: true,
|
||
strokeScaling: false,
|
||
})
|
||
|
||
if (canvasElem.current && paper) {
|
||
init(canvasElem.current, paperScope.current!, newGroup)
|
||
canvasElem.current.oncontextmenu = (e) => {
|
||
e.preventDefault()
|
||
}
|
||
let viewTool = new paper.Tool()
|
||
initViewTool(viewTool)
|
||
let panTool = new paper.Tool()
|
||
initPanTool(panTool, renderRectangle, forceUpdate)
|
||
let polygonTool = new paper.Tool()
|
||
initPolygonTool(
|
||
polygonTool,
|
||
renderPath,
|
||
renderCompoundPath,
|
||
forceUpdate
|
||
)
|
||
let rectangleTool = new paper.Tool()
|
||
initRectangleTool(rectangleTool, renderRectangle, forceUpdate)
|
||
let brushTool = new paper.Tool()
|
||
initBrushTool(brushTool, renderPath, renderCompoundPath, forceUpdate)
|
||
let pointTool = new paper.Tool()
|
||
initPointTool(pointTool, renderCircle, renderPath, forceUpdate)
|
||
let supportTool = new paper.Tool()
|
||
initSupportTool(supportTool, renderSupportAnnotation)
|
||
}
|
||
return () => {
|
||
newGroup.remove()
|
||
}
|
||
}
|
||
}, [
|
||
forceUpdate,
|
||
init,
|
||
initBrushTool,
|
||
initPanTool,
|
||
initPointTool,
|
||
initPolygonTool,
|
||
initRectangleTool,
|
||
initSupportTool,
|
||
initViewTool,
|
||
renderSupportAnnotation,
|
||
setup,
|
||
])
|
||
|
||
const [paperSelectedPath] = useState<{
|
||
// key = imageId
|
||
[key: string]: number[]
|
||
}>(selectedPath)
|
||
|
||
const getOperationSchema = useCallback(
|
||
(operationId: string) => {
|
||
return (
|
||
projectDetail?.label_schema_list?.find(
|
||
(item) => item.category_id === +operationId
|
||
) || null
|
||
)
|
||
},
|
||
[projectDetail?.label_schema_list]
|
||
)
|
||
|
||
const { getItemById, getItemsById } = usePaperStore()
|
||
|
||
const renderPolygons = useCallback(
|
||
(
|
||
operationId: string,
|
||
imageData: Map<string, [number, any[], any, any[]][]> | undefined
|
||
) => {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
|
||
if (operationSchema && imageData) {
|
||
let strokeColor = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
|
||
let fillColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
|
||
let blankColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`
|
||
|
||
let paths = imageData.get(operationId)
|
||
|
||
switch (
|
||
operationSchema &&
|
||
labelTypeMap.get(operationSchema.label_type)
|
||
) {
|
||
case "多边形":
|
||
paths?.forEach((item) => {
|
||
let outerPaths =
|
||
item[1]?.map((child) => {
|
||
return addPolygon(
|
||
renderPath,
|
||
child.map((segment: any) => {
|
||
if (segment instanceof paper.Point) {
|
||
return segment
|
||
} else {
|
||
return new paper.Point(segment[0], segment[1])
|
||
}
|
||
}),
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible:
|
||
!paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
}) || []
|
||
let innerPaths =
|
||
item[3]?.map((child) => {
|
||
return addPolygon(
|
||
renderPath,
|
||
child.map((segment: any) => {
|
||
if (segment instanceof paper.Point) {
|
||
return segment
|
||
} else {
|
||
return new paper.Point(segment[0], segment[1])
|
||
}
|
||
}),
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
fillColor: fillColor,
|
||
visible:
|
||
!paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(
|
||
item[0]
|
||
),
|
||
isHollowPolygon: true,
|
||
}
|
||
)
|
||
}) || []
|
||
|
||
addCompoundPath(
|
||
renderCompoundPath,
|
||
[...outerPaths, ...innerPaths],
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "polygon",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
}
|
||
)
|
||
|
||
// renderCompoundPath(usePaperStore.getState().group!, { children });
|
||
// item[1].forEach((child) => {
|
||
// addPolygon(
|
||
// renderPath,
|
||
// child.map((segment: any) => {
|
||
// if (segment instanceof paper.Point) {
|
||
// return segment;
|
||
// } else {
|
||
// return new paper.Point(segment[0], segment[1]);
|
||
// }
|
||
// }),
|
||
// {
|
||
// strokeColor: strokeColor,
|
||
// strokeWidth: 2,
|
||
// // fillColor: fillColor,
|
||
// fillColor: blankColor,
|
||
// visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
// selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
// },
|
||
// {
|
||
// type: "polygon",
|
||
// id: item[0],
|
||
// imageId: activeImage,
|
||
// operationId: operationId,
|
||
// fillColor: fillColor,
|
||
// blankColor: blankColor,
|
||
// }
|
||
// );
|
||
// });
|
||
})
|
||
return
|
||
case "关键点":
|
||
paths?.forEach((item) => {
|
||
item[1]?.forEach((child) => {
|
||
// 保存前排好序
|
||
child.forEach((segment: any, segmentIndex: number) => {
|
||
let point =
|
||
segment instanceof paper.Segment
|
||
? new paper.Point(segment.point)
|
||
: new paper.Point(segment[0], segment[1])
|
||
addPoint(
|
||
renderCircle,
|
||
point,
|
||
{
|
||
fillColor: null,
|
||
strokeColor: strokeColor,
|
||
radius: 3,
|
||
},
|
||
{
|
||
imageId: useBottomToolsStore.getState().activeImage,
|
||
operationId: useTopToolsStore.getState().activeOperation,
|
||
text: segmentIndex + 1,
|
||
id: item[0],
|
||
attributes: item[2]?.circles?.[segmentIndex]?.attributes,
|
||
}
|
||
)
|
||
})
|
||
addPointLine(
|
||
renderPath,
|
||
child.map((segment: any) => {
|
||
if (segment instanceof paper.Segment) {
|
||
return new paper.Point(segment.point)
|
||
} else {
|
||
return new paper.Point(segment[0], segment[1])
|
||
}
|
||
}),
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
}
|
||
)
|
||
})
|
||
})
|
||
return
|
||
case "多线段":
|
||
paths?.forEach((item) => {
|
||
let outerPaths =
|
||
item[1]?.map((child) => {
|
||
return addBrush(
|
||
renderPath,
|
||
child.map((segment: any) => {
|
||
if (segment instanceof paper.Point) {
|
||
return segment
|
||
} else {
|
||
return new paper.Point(segment[0], segment[1])
|
||
}
|
||
}),
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible:
|
||
!paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "brush",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(
|
||
item[0]
|
||
),
|
||
}
|
||
)
|
||
}) || []
|
||
|
||
addCompoundPath(
|
||
renderCompoundPath,
|
||
outerPaths,
|
||
{
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "brush",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
}
|
||
)
|
||
})
|
||
return
|
||
case "2D框":
|
||
paths?.forEach((item) => {
|
||
item[1].forEach((child) => {
|
||
let flag = child[0] instanceof paper.Point
|
||
let p = flag
|
||
? child[1]
|
||
: new paper.Point(child[1][0], child[1][1])
|
||
let dp = flag
|
||
? child[3]
|
||
: new paper.Point(child[3][0], child[3][1])
|
||
|
||
addRectangle(
|
||
renderRectangle,
|
||
{
|
||
from: p,
|
||
to: dp,
|
||
strokeColor: strokeColor,
|
||
strokeWidth: 2,
|
||
// fillColor: fillColor,
|
||
fillColor: blankColor,
|
||
visible: !paperPathStatus[activeImage + item[0]]?.["HIDE"],
|
||
},
|
||
{
|
||
type: "rectangle",
|
||
id: item[0],
|
||
imageId: activeImage,
|
||
operationId: operationId,
|
||
fillColor: fillColor,
|
||
blankColor: blankColor,
|
||
selected: paperSelectedPath[activeImage]?.includes(item[0]),
|
||
}
|
||
)
|
||
})
|
||
})
|
||
return
|
||
default:
|
||
return
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
addBrush,
|
||
addCompoundPath,
|
||
addPoint,
|
||
addPointLine,
|
||
addPolygon,
|
||
addRectangle,
|
||
getOperationSchema,
|
||
paperPathStatus,
|
||
paperSelectedPath,
|
||
]
|
||
)
|
||
|
||
// 生成辅助标注结果数据并绘制
|
||
const saveSupportAnnotationData = useCallback(() => {
|
||
const group = usePaperStore.getState().group
|
||
if (!group) return
|
||
const compoundPath = group.getItems({
|
||
data: { id: "support", type: "parent" },
|
||
})?.[0]
|
||
if (compoundPath) {
|
||
console.log(compoundPath)
|
||
const children = compoundPath.children as paper.Path[]
|
||
const id =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
useRightToolsStore.getState().pushPathId(id)
|
||
// 处理点位数据
|
||
let segmentation: Array<[number, number][]> = []
|
||
let hollow_segmentation: Array<[number, number][]> = []
|
||
children.forEach((child) => {
|
||
if (child.data.isHollowPolygon) {
|
||
hollow_segmentation.push(
|
||
child.segments.map((seg) => [seg.point.x, seg.point.y])
|
||
)
|
||
} else {
|
||
segmentation.push(
|
||
child.segments.map((seg) => [seg.point.x, seg.point.y])
|
||
)
|
||
}
|
||
})
|
||
console.log(segmentation, hollow_segmentation)
|
||
// 处理详情数据
|
||
let category = useTopToolsStore
|
||
.getState()
|
||
.objectOperations.find(
|
||
(item) =>
|
||
item.category_id.toString() ===
|
||
useTopToolsStore.getState().activeOperation
|
||
)
|
||
let label_class = category?.label_class || ""
|
||
let subAttr = Object.assign(
|
||
{},
|
||
useTopToolsStore.getState().subAttributes[label_class]
|
||
)
|
||
let detail = {
|
||
...initialDetail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
comment: [...useLabelStore.getState().labelDefaultComments],
|
||
create_timestamp: dayjs().unix(),
|
||
sub_attributes: subAttr,
|
||
image_id: activeImage,
|
||
category_id: Number(useTopToolsStore.getState().activeOperation),
|
||
}
|
||
// 清空support对象
|
||
clearSupportAnnotationItem()
|
||
setOperation(activeImage, useTopToolsStore.getState().activeOperation, [
|
||
id,
|
||
segmentation,
|
||
detail,
|
||
hollow_segmentation,
|
||
])
|
||
console.log(usePaperStore.getState().group)
|
||
// console.log(first)
|
||
// 更新当前页绘制数据
|
||
usePaperStore
|
||
.getState()
|
||
.group!.getItems({})
|
||
.filter((item) => item.data.id)
|
||
.forEach((item) => {
|
||
item.remove()
|
||
})
|
||
const imgMap = useLabelStore.getState().label.get(activeImage)!
|
||
for (const key of imgMap.keys()) {
|
||
renderPolygons(key, imgMap)
|
||
}
|
||
// 模型数据生成完毕 调整绘制模式
|
||
usePaperStore.getState().setPaperMode("pan")
|
||
}
|
||
}, [activeImage, renderPolygons, setOperation])
|
||
|
||
const copyAnnotationDataToMultiFrame = useCallback(() => {
|
||
const picArr = useBottomToolsStore
|
||
.getState()
|
||
.selectedItems.filter((k) => k !== activeImage)
|
||
const copyArr = useKeyboardStore.getState().copyDataIds
|
||
if (picArr.length && copyArr.length) {
|
||
const data = structuredClone(useLabelStore.getState().label)
|
||
// 先判断是否有重复的id
|
||
let existedIds: number[] = []
|
||
picArr.forEach((imageId) => {
|
||
if (data.has(imageId) && data.get(imageId)?.size) {
|
||
for (let arr of data.get(imageId)!.values()) {
|
||
arr.forEach((i) => existedIds.push(i[0]))
|
||
}
|
||
}
|
||
})
|
||
|
||
const handleData = (flag = false) => {
|
||
try {
|
||
const activeData = data.get(activeImage) ?? new Map()
|
||
const activeScale =
|
||
usePaperStore.getState().rasterScale[activeImage] ?? 1
|
||
const needCopyMap = new Map()
|
||
for (let [key, value] of activeData.entries()) {
|
||
let arr = value.filter(([id]: any) => copyArr.includes(id))
|
||
console.log(arr)
|
||
if (arr.length)
|
||
needCopyMap.set(
|
||
key,
|
||
arr.map(([id, seg, detail, hollow]: any) => [
|
||
id,
|
||
seg.map((item: any) =>
|
||
item.map((p: any) => {
|
||
if (p instanceof paper.Point) {
|
||
return [p.x / activeScale, p.y / activeScale]
|
||
} else {
|
||
return [p[0] / activeScale, p[1] / activeScale]
|
||
}
|
||
})
|
||
),
|
||
{
|
||
...detail,
|
||
uid: usePermissionStore.getState().user_id,
|
||
create_timestamp: dayjs().unix(),
|
||
first_modified_timestamp: 0,
|
||
first_modified_uid: 0,
|
||
last_modified_timestamp: 0,
|
||
last_modified_uid: 0,
|
||
},
|
||
hollow.map((item: any) =>
|
||
item.map((p: any) => {
|
||
if (paper instanceof paper.Segment) {
|
||
return [
|
||
p.point.x / activeScale,
|
||
p.point.y / activeScale,
|
||
]
|
||
} else {
|
||
return [p[0] / activeScale, p[1] / activeScale]
|
||
}
|
||
})
|
||
),
|
||
])
|
||
)
|
||
}
|
||
|
||
picArr.forEach((imageId) => {
|
||
const picScale = usePaperStore.getState().rasterScale[imageId] ?? 1
|
||
|
||
if (data.has(imageId)) {
|
||
const categoryMap = data.get(imageId)
|
||
if (categoryMap?.size) {
|
||
let needCopyIds: number[] = []
|
||
let needCopyCategoryIds: string[] = []
|
||
for (let [key, needCopyItemsArr] of needCopyMap.entries()) {
|
||
// console.log(key);
|
||
needCopyItemsArr.forEach((item: any) => {
|
||
needCopyIds.push(item[0])
|
||
needCopyCategoryIds.push(key)
|
||
})
|
||
// let arr = needCopyItemsArr.map((item: any) => [
|
||
// item[0],
|
||
// item[1].map((path: any) =>
|
||
// path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
// ),
|
||
// item[2],
|
||
// item[3].map((path: any) =>
|
||
// path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
// ),
|
||
// ]);
|
||
// needCopyMap.set(key, arr);
|
||
}
|
||
let currentImageItemIds: number[] = []
|
||
let currentImageCategoryIds: string[] = []
|
||
for (let [key, items] of categoryMap.entries()) {
|
||
items.forEach((item) => {
|
||
currentImageItemIds.push(item[0])
|
||
currentImageCategoryIds.push(key)
|
||
})
|
||
}
|
||
let finalArr: number[] = []
|
||
let finalCategory: string[] = []
|
||
needCopyIds.forEach((id, index) => {
|
||
const findIndex = currentImageItemIds.findIndex(
|
||
(i) => i === id
|
||
)
|
||
if (findIndex !== -1) {
|
||
if (flag) {
|
||
console.log(findIndex)
|
||
// 确认覆盖
|
||
finalArr.push(id)
|
||
finalCategory.push(needCopyCategoryIds[index])
|
||
let categoryId = currentImageCategoryIds[findIndex]
|
||
let arr = categoryMap.get(categoryId)!
|
||
let findIndexInArr = arr.findIndex((v) => v[0] === id)
|
||
let oldData = arr.find((a) => a[0] === id)!
|
||
let arr2 = needCopyMap.get(needCopyCategoryIds[index])!
|
||
let newData = {
|
||
...arr2.find((b: any) => b[0] === id),
|
||
uid: oldData[2].uid,
|
||
create_timestamp: oldData[2].create_timestamp,
|
||
first_modified_timestamp:
|
||
oldData[2].first_modified_timestamp || dayjs().unix(),
|
||
first_modified_uid:
|
||
oldData[2].first_modified_uid ||
|
||
usePermissionStore.getState().user_id,
|
||
last_modified_timestamp: dayjs().unix(),
|
||
last_modified_uid:
|
||
usePermissionStore.getState().user_id,
|
||
}
|
||
let updateArr = arr2.map((b: any) => {
|
||
return b[0] === id ? [b[0], b[1], newData, b[3]] : b
|
||
})
|
||
needCopyMap.set(needCopyCategoryIds[index], updateArr)
|
||
categoryMap.set(
|
||
categoryId,
|
||
arr.filter((_v, i) => i !== findIndexInArr)
|
||
)
|
||
}
|
||
} else {
|
||
finalArr.push(id)
|
||
finalCategory.push(needCopyCategoryIds[index])
|
||
}
|
||
})
|
||
|
||
finalCategory.forEach((category, index) => {
|
||
const categoryArr = categoryMap.get(category) ?? []
|
||
const copyCategoryArr = needCopyMap.get(category)!
|
||
let copyId = finalArr[index]
|
||
|
||
let newData = copyCategoryArr.find(
|
||
(copyItem: any) => copyItem[0] === copyId
|
||
)
|
||
newData = [
|
||
newData[0],
|
||
newData[1].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
newData[2],
|
||
newData[3].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
]
|
||
categoryArr.push(newData)
|
||
categoryMap.set(category, categoryArr)
|
||
})
|
||
} else {
|
||
// categoryMap实际没有值 需要重新生成
|
||
let newCategoryMap = new Map()
|
||
for (let [key, needCopyItemsArr] of needCopyMap) {
|
||
let arr = needCopyItemsArr.map((item: any) => [
|
||
item[0],
|
||
item[1].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
item[2],
|
||
item[3].map((path: any) =>
|
||
path.map(([x, y]: any) => [x * picScale, y * picScale])
|
||
),
|
||
])
|
||
newCategoryMap.set(key, arr)
|
||
}
|
||
data.set(imageId, newCategoryMap)
|
||
}
|
||
} else {
|
||
data.set(imageId, needCopyMap)
|
||
}
|
||
})
|
||
notifications.show({ message: "复制成功", color: "green" })
|
||
useLabelStore.getState().setLabel(data)
|
||
useLabelStore.getState().pushStateStack(data)
|
||
useBottomToolsStore.getState().setSelectedItems("", false, false)
|
||
setConfirmModalOpened(false)
|
||
} catch (error) {
|
||
console.log(error)
|
||
notifications.show({ message: "复制失败", color: "red" })
|
||
}
|
||
}
|
||
|
||
let flag = existedIds.find((item) => copyArr.includes(item))
|
||
if (flag) {
|
||
setConfirmModalCallback(() => handleData)
|
||
setConfirmModalOpened(true)
|
||
return
|
||
} else {
|
||
handleData()
|
||
}
|
||
}
|
||
}, [activeImage])
|
||
|
||
const [rasterInited, setRasterInited] = useState<boolean>(false)
|
||
|
||
const renderAndScaleRaster = useCallback(
|
||
async (paperGroup: paper.Group) => {
|
||
// const raster = renderRaster(paperGroup, {
|
||
// source: `${process.env.NEXT_PUBLIC_URL}/api/v1/label_server/data/image?data_name=${activeImage}&project_path=${projectDetail?.rpath}`,
|
||
// });
|
||
|
||
if (activeImage && projectDetail?.id) {
|
||
console.log(usePaperStore.getState().paperScope)
|
||
// console.log(activeImage, projectDetail?.rpath);
|
||
notifications.show({
|
||
message: "图片加载中...",
|
||
loading: true,
|
||
autoClose: false,
|
||
id: "loadingRaster",
|
||
})
|
||
try {
|
||
let raster
|
||
let text
|
||
const images = useImagesStore.getState().images
|
||
const currentImageBase64Text = images.get(activeImage)
|
||
if (currentImageBase64Text) {
|
||
text = currentImageBase64Text
|
||
} else {
|
||
const response = await getServerImage({
|
||
data_names: [activeImage],
|
||
data_type: 0,
|
||
project_id: projectDetail?.id,
|
||
})
|
||
text = `data:image/jpeg;base64,${response}`
|
||
images.set(activeImage, text)
|
||
useImagesStore.getState().setImages(images)
|
||
}
|
||
if (!text) return
|
||
raster = renderRaster(paperGroup, {
|
||
source: text,
|
||
data: {
|
||
name: "pic",
|
||
},
|
||
})
|
||
if (!raster) return
|
||
|
||
raster.onLoad = function () {
|
||
// Scale the raster to fit the canvas while maintaining aspect ratio
|
||
let canvasWidth = paperScope.current?.view.bounds.width
|
||
let canvasHeight = paperScope.current?.view.bounds.height
|
||
|
||
let scaleX = canvasWidth! / raster.bounds.width
|
||
let scaleY = canvasHeight! / raster.bounds.height
|
||
setRasterSize(activeImage, [
|
||
raster.bounds.width,
|
||
raster.bounds.height,
|
||
])
|
||
|
||
let scale = Math.min(scaleX, scaleY) // Maintain aspect ratio
|
||
|
||
// 将raster以(0,0)为中心点进行缩放
|
||
raster.scale(scale, new paper.Point(0, 0))
|
||
const currentImgScale =
|
||
usePaperStore.getState().rasterScale[activeImage]
|
||
// 当前图片无缩放比例或缩放比例不等于上次记录比例时
|
||
if (!currentImgScale) {
|
||
setRasterScale(activeImage, scale)
|
||
const newData = adjustPoints(
|
||
activeImage,
|
||
useLabelStore.getState().label,
|
||
scale
|
||
)
|
||
// 处理hollowPoints
|
||
useLabelStore.getState().setLabel(newData)
|
||
let stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(activeImage, item, scale))
|
||
)
|
||
}
|
||
} else if (currentImgScale !== scale) {
|
||
// 先将相关数据恢复到原始比例
|
||
const restoreScale =
|
||
usePaperStore.getState().reciprocalRasterScale[activeImage]
|
||
const restoreData = adjustPoints(
|
||
activeImage,
|
||
useLabelStore.getState().label,
|
||
restoreScale
|
||
)
|
||
setRasterScale(activeImage, scale)
|
||
const newData = adjustPoints(activeImage, restoreData, scale)
|
||
useLabelStore.getState().setLabel(newData)
|
||
let stack = useLabelStore.getState().stateStack
|
||
if (stack.length) {
|
||
useLabelStore
|
||
.getState()
|
||
.setStateStack(
|
||
stack.map((item) => adjustPoints(activeImage, item, scale))
|
||
)
|
||
}
|
||
}
|
||
// 调整raster的左上角到(0,0)
|
||
raster.position = new paper.Point(
|
||
raster.bounds.width / 2,
|
||
raster.bounds.height / 2
|
||
)
|
||
// if (!useTopToolsStore.getState().saveCurrentScale)
|
||
usePaperStore.getState().group!.position = raster.position
|
||
setRasterPath(new paper.Path.Rectangle(raster.bounds))
|
||
// 将raster置底
|
||
raster.sendToBack()
|
||
setRasterInited(true)
|
||
console.log(usePaperStore.getState().group)
|
||
usePaperStore.getState().group?.bringToFront()
|
||
}
|
||
} catch (error) {
|
||
console.log(error)
|
||
} finally {
|
||
notifications.hide("loadingRaster")
|
||
}
|
||
}
|
||
},
|
||
[
|
||
activeImage,
|
||
|
||
projectDetail?.id,
|
||
setRasterPath,
|
||
setRasterScale,
|
||
setRasterSize,
|
||
]
|
||
)
|
||
|
||
const loadImageAndPolygons = useCallback(
|
||
async (
|
||
// imageData: Map<string, [number, any[], any][]> | undefined,
|
||
group: paper.Group
|
||
) => {
|
||
// 清除当前的 raster 和多边形
|
||
group.removeChildren()
|
||
// 添加新的 raster
|
||
setRasterInited(false)
|
||
renderAndScaleRaster(group)
|
||
// if (imageData) {
|
||
// objectOperations?.forEach((item) => {
|
||
// renderPolygons(item.category_id.toString(), imageData);
|
||
// });
|
||
// }
|
||
},
|
||
[renderAndScaleRaster]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (loadingData) return
|
||
const currentGroup = usePaperStore.getState().group
|
||
if (currentGroup && projectDetail) {
|
||
if (!useTopToolsStore.getState().saveCurrentScale) {
|
||
currentGroup.scaling = new paper.Point(1, 1)
|
||
useTopToolsStore.getState().setScale(1)
|
||
} else {
|
||
usePaperStore
|
||
.getState()
|
||
.setGroupScale(useTopToolsStore.getState().scale)
|
||
}
|
||
loadImageAndPolygons(
|
||
// storeLabel.get(activeImage),
|
||
currentGroup
|
||
)
|
||
}
|
||
}, [activeImage, loadImageAndPolygons, loadingData, projectDetail])
|
||
|
||
useEffect(() => {
|
||
if (rasterInited) {
|
||
if (storeLabel.get(activeImage)) {
|
||
objectOperations?.forEach((item) => {
|
||
renderPolygons(
|
||
item.category_id.toString(),
|
||
storeLabel.get(activeImage)
|
||
)
|
||
})
|
||
renderGroupPath()
|
||
renderTag()
|
||
}
|
||
setRasterInited(false)
|
||
}
|
||
}, [
|
||
activeImage,
|
||
objectOperations,
|
||
rasterInited,
|
||
renderPolygons,
|
||
renderTag,
|
||
storeLabel,
|
||
])
|
||
|
||
useEffect(() => {
|
||
renderGroupPath()
|
||
}, [storeLabel])
|
||
|
||
useEffect(() => {
|
||
renderTag()
|
||
}, [showTags, showTagsConfigs, storeLabel, renderTag])
|
||
|
||
const [ids, setIds] = useState<number[]>([])
|
||
const [existIds, setExistIds] = useState<number[]>([])
|
||
// select use existIds todo
|
||
false && console.log(existIds)
|
||
|
||
const [comments, setComments] = useState<string[]>([])
|
||
|
||
const form = useForm<any>({
|
||
initialValues: {
|
||
id: undefined,
|
||
operation: undefined,
|
||
sub_attributes: {},
|
||
check_box_comments: [],
|
||
text_comments: "",
|
||
},
|
||
})
|
||
const checkboxComments = form.values.check_box_comments
|
||
const textComment = form.values.text_comments
|
||
|
||
const filterOperations = useMemo(() => {
|
||
let currentLabelType = objectOperations.find(
|
||
(operation) => operation.category_id.toString() === operationId
|
||
)?.label_type
|
||
|
||
return objectOperations.filter(
|
||
(operation) => operation.label_type === currentLabelType
|
||
)
|
||
}, [objectOperations, operationId])
|
||
|
||
useEffect(() => {
|
||
if (projectDetail?.comment_rule) {
|
||
if (projectDetail?.comment_rule?.check_box_rules?.length) {
|
||
setComments(
|
||
projectDetail?.comment_rule?.check_box_rules.map((item) => item.text)
|
||
)
|
||
}
|
||
}
|
||
}, [projectDetail?.comment_rule])
|
||
|
||
const pathItem = useMemo(() => {
|
||
return storeLabel
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.find((item) => item[0] === id)
|
||
}, [activeImage, id, operationId, storeLabel])
|
||
|
||
// 打开弹窗时,初始化form数据
|
||
useEffect(() => {
|
||
if (showContextMenu) {
|
||
form.reset()
|
||
|
||
const labelData = useLabelStore.getState().label
|
||
const currentPath = labelData
|
||
.get(activeImage)
|
||
?.get(operationId)
|
||
?.find((item) => item[0] === id)
|
||
|
||
form.setFieldValue("id", id)
|
||
form.setFieldValue("operation", operationId)
|
||
if (currentPath?.[2].sub_attributes) {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
let obj: {
|
||
[key: string]: string | string[]
|
||
} = {}
|
||
Object.entries(currentPath?.[2].sub_attributes).map(([key, value]) => {
|
||
let select_mode = operationSchema?.sub_attributes_describe?.find(
|
||
(item) => item.chinese_name === key
|
||
)?.select_mode
|
||
switch (select_mode) {
|
||
case 2:
|
||
obj[key] = (value as string).split(",")
|
||
return
|
||
default:
|
||
obj[key] = value as string
|
||
return
|
||
}
|
||
})
|
||
form.setFieldValue("sub_attributes", obj)
|
||
}
|
||
if (currentPath?.[2].comment) {
|
||
const nowComment =
|
||
currentPath?.[2].comment[currentPath?.[2].comment.length - 1]
|
||
form.setFieldValue(
|
||
"check_box_comments",
|
||
nowComment && nowComment.check_box_comments.length
|
||
? nowComment.check_box_comments
|
||
: []
|
||
)
|
||
form.setFieldValue(
|
||
"text_comments",
|
||
nowComment && nowComment.text_comments.length
|
||
? nowComment.text_comments[0]
|
||
: ""
|
||
)
|
||
}
|
||
// 可选的ids
|
||
let images = Array.from(labelState.keys()).filter(
|
||
(key) => key !== activeImage
|
||
)
|
||
let ids: number[] = [id]
|
||
images.map((imageId) => {
|
||
// 可切换成其他图片同一类型的id
|
||
filterOperations.map((operation) => {
|
||
labelData
|
||
.get(imageId)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])?.length &&
|
||
ids.push(
|
||
...labelData
|
||
.get(imageId)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])!
|
||
)
|
||
})
|
||
})
|
||
let existIds: number[] = []
|
||
objectOperations.forEach((operation) => {
|
||
labelData
|
||
.get(activeImage)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])?.length &&
|
||
existIds.push(
|
||
...labelData
|
||
.get(activeImage)
|
||
?.get(operation.category_id.toString())
|
||
?.map((arr) => arr[0])!
|
||
)
|
||
})
|
||
setExistIds(Array.from(new Set(existIds)))
|
||
|
||
ids.push(
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
)
|
||
let selectIds = ids.filter((id) => !existIds.includes(id))
|
||
|
||
setIds(Array.from(new Set(selectIds)))
|
||
} else {
|
||
// console.log(form.getFieldValue("id"), id);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
activeImage,
|
||
filterOperations,
|
||
getOperationSchema,
|
||
id,
|
||
labelState,
|
||
objectOperations,
|
||
operationId,
|
||
showContextMenu,
|
||
])
|
||
|
||
// 监听当前标注对象批注值变化
|
||
useEffect(() => {
|
||
if (!showContextMenu) return
|
||
if (!id || !operationId || !activeImage) return
|
||
if (useTopToolsStore.getState().currentTimeKey === "label") return
|
||
const currentDetail = useLabelStore
|
||
.getState()
|
||
.getLabelDetailDataByKeys(activeImage, operationId, id)
|
||
if (!currentDetail) return
|
||
const { comment } = currentDetail
|
||
let updatedComment: any[] = []
|
||
// 当表单当前值跟标注对象detail中comment值相同时不更新detail中comment状态
|
||
if (
|
||
checkCommentsIsSame(
|
||
comment[comment.length - 1].check_box_comments,
|
||
comment[comment.length - 1].text_comments,
|
||
checkboxComments,
|
||
textComment ? [textComment] : []
|
||
)
|
||
)
|
||
return
|
||
// 无批注
|
||
if ((!checkboxComments || !checkboxComments.length) && !textComment) {
|
||
updatedComment = comment.map((item: Comment, index: number) =>
|
||
index < comment.length - 1
|
||
? item
|
||
: {
|
||
...item,
|
||
comment_type: 1,
|
||
date: dayjs().unix(),
|
||
check_box_comments: [],
|
||
text_comments: [],
|
||
}
|
||
)
|
||
} else {
|
||
let latestItem = comment[comment.length - 1]
|
||
latestItem = {
|
||
...latestItem,
|
||
comment_type: 2,
|
||
date: dayjs().unix(),
|
||
check_box_comments:
|
||
checkboxComments && checkboxComments.length ? checkboxComments : [],
|
||
text_comments: textComment ? [textComment] : [],
|
||
}
|
||
updatedComment = comment.map((item: Comment, index: number) =>
|
||
index < comment.length - 1 ? item : latestItem
|
||
)
|
||
}
|
||
useLabelStore
|
||
.getState()
|
||
.updateLabelDetailDataByKeys(activeImage, operationId, id, {
|
||
...currentDetail,
|
||
comment: updatedComment,
|
||
})
|
||
}, [
|
||
activeImage,
|
||
checkboxComments,
|
||
id,
|
||
operationId,
|
||
showContextMenu,
|
||
textComment,
|
||
])
|
||
|
||
const newDetail = useMemo(() => {
|
||
const currentDetail = getLabelDetailDataByKeys(activeImage, operationId, id)
|
||
const detail = 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(),
|
||
}
|
||
return detail
|
||
}, [activeImage, getLabelDetailDataByKeys, id, operationId])
|
||
|
||
const handleChangeContextMenuId = useCallback(
|
||
(newId: number) => {
|
||
// 设置选中id
|
||
updateSelectedPath(activeImage, "DELETE", id)
|
||
updateSelectedPath(activeImage, "ADD", newId)
|
||
// 修改多边形data.id storelabel中 id detail
|
||
if (getItemById(id)) getItemById(id)!.data.id = newId
|
||
setOperation(activeImage, operationId, [
|
||
newId,
|
||
pathItem?.[1] || [],
|
||
newDetail,
|
||
pathItem?.[3] || [],
|
||
])
|
||
deleteOperation(activeImage, operationId, id)
|
||
// 设置菜单id
|
||
setContextMenuId(newId)
|
||
useRightToolsStore.getState().pushPathId(newId)
|
||
},
|
||
[
|
||
activeImage,
|
||
deleteOperation,
|
||
getItemById,
|
||
id,
|
||
newDetail,
|
||
operationId,
|
||
pathItem,
|
||
setContextMenuId,
|
||
setOperation,
|
||
updateSelectedPath,
|
||
]
|
||
)
|
||
|
||
const handleChangeContextMenuOperation = useCallback(
|
||
(newOperation: string) => {
|
||
const operationSchema = getOperationSchema(newOperation)
|
||
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)"
|
||
if (getItemsById(id)) {
|
||
getItemsById(id).forEach((item) => {
|
||
const data = item.data
|
||
if (data.operationId) {
|
||
data.operationId = newOperation
|
||
if (data.type !== "text")
|
||
item.strokeColor = new paper.Color(strokeColor)
|
||
if (data.type !== "brush" && data.type !== "point")
|
||
item.fillColor = new paper.Color(fillColor)
|
||
item.data =
|
||
data.type !== "mask" && data.type !== "text"
|
||
? Object.assign(data || {}, {
|
||
fillColor,
|
||
blankColor,
|
||
strokeColor,
|
||
})
|
||
: data
|
||
// // 如果是组合类型 则遍历将children的data变更
|
||
// if (item instanceof paper.CompoundPath) {
|
||
// item.children.forEach((path) => {
|
||
// path.strokeColor = new paper.Color(strokeColor);
|
||
// path.fillColor = new paper.Color(fillColor);
|
||
// path.data = Object.assign(path.data || {}, {
|
||
// operationId: newOperation,
|
||
// fillColor,
|
||
// blankColor,
|
||
// strokeColor,
|
||
// });
|
||
// });
|
||
// }
|
||
}
|
||
})
|
||
}
|
||
// newOperation的subAttributes
|
||
let subAttr = Object.assign(
|
||
{},
|
||
useTopToolsStore.getState().subAttributes[
|
||
operationSchema?.label_class ?? ""
|
||
]
|
||
)
|
||
// 修改labeldata
|
||
setOperation(activeImage, newOperation, [
|
||
id,
|
||
pathItem?.[1] || [],
|
||
{ ...newDetail, sub_attributes: subAttr },
|
||
pathItem?.[3] || [],
|
||
])
|
||
deleteOperation(activeImage, operationId, id)
|
||
// 修改context的operationId
|
||
setContextMenuOperationId(newOperation)
|
||
},
|
||
[
|
||
activeImage,
|
||
deleteOperation,
|
||
getItemsById,
|
||
getOperationSchema,
|
||
id,
|
||
newDetail,
|
||
operationId,
|
||
pathItem,
|
||
setContextMenuOperationId,
|
||
setOperation,
|
||
]
|
||
)
|
||
|
||
const handleChangeContextMenuSubAttr = useCallback(
|
||
(key: string, value: string) => {
|
||
setOperation(activeImage, operationId, [
|
||
id,
|
||
pathItem?.[1] || [],
|
||
{
|
||
...newDetail,
|
||
...(pathItem?.[2] || {}),
|
||
sub_attributes: Object.assign(newDetail.sub_attributes || {}, {
|
||
[key]: value,
|
||
}),
|
||
},
|
||
pathItem?.[3] || [],
|
||
])
|
||
},
|
||
[activeImage, id, newDetail, operationId, pathItem, setOperation]
|
||
)
|
||
|
||
const contextMenu = useMemo(() => {
|
||
let operationSchema = getOperationSchema(operationId)
|
||
const currentType = useTopToolsStore.getState().currentTimeKey
|
||
|
||
return (
|
||
<Card
|
||
shadow="sm"
|
||
padding="xs"
|
||
radius="md"
|
||
withBorder
|
||
style={{
|
||
position: "absolute",
|
||
zIndex: 1000,
|
||
width: 224,
|
||
height: "fit-content",
|
||
display: !showContextMenu ? "none" : "block",
|
||
top: `${position.top}px`,
|
||
left: `${position.left}px`,
|
||
}}>
|
||
<Box p="xs">
|
||
<Stack gap="xs">
|
||
<Select
|
||
label="ID"
|
||
size="xs"
|
||
placeholder="请选择ID!"
|
||
data={ids.map((item) => item.toString())}
|
||
searchable
|
||
// creatable
|
||
// getCreateLabel={(query) => `+ Add ${query}`}
|
||
// onCreate={(query) => {
|
||
// const id = +query
|
||
// if (isNaN(id) || ids.includes(id) || existIds.includes(id))
|
||
// return null
|
||
// setIds([...ids, id])
|
||
// return query
|
||
// }}
|
||
required
|
||
{...form.getInputProps("id")}
|
||
onChange={(value) => {
|
||
form.setFieldValue("id", value)
|
||
if (value && +value !== id) {
|
||
handleChangeContextMenuId(+value)
|
||
}
|
||
}}
|
||
/>
|
||
<Select
|
||
label="类型"
|
||
size="xs"
|
||
placeholder="请选择类型!"
|
||
data={filterOperations.map((operation) => ({
|
||
label: operation.label_class,
|
||
value: operation.category_id.toString(),
|
||
}))}
|
||
leftSection={
|
||
form.values.operation &&
|
||
renderOperationIcon(
|
||
getOperationSchema(form.values.operation.toString())
|
||
)
|
||
}
|
||
required
|
||
{...form.getInputProps("operation")}
|
||
onChange={(value) => {
|
||
form.setFieldValue("operation", value)
|
||
if (value && value !== operationId) {
|
||
handleChangeContextMenuOperation(value)
|
||
}
|
||
}}
|
||
/>
|
||
<Box
|
||
p="xs"
|
||
style={{
|
||
borderRadius: "2px",
|
||
border: "1px solid var(--mantine-color-blue-light)",
|
||
}}>
|
||
<Text c="blue" fw={600} mb="xs">
|
||
批注
|
||
</Text>
|
||
<ScrollArea.Autosize mah={48} mb="xs">
|
||
<Checkbox.Group {...form.getInputProps("check_box_comments")}>
|
||
<Stack gap="xs">
|
||
{comments.map((item) => (
|
||
<Checkbox
|
||
key={item}
|
||
value={item}
|
||
label={item}
|
||
disabled={currentType === "label"}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</Checkbox.Group>
|
||
</ScrollArea.Autosize>
|
||
<Textarea
|
||
autosize
|
||
minRows={2}
|
||
maxRows={2}
|
||
disabled={currentType === "label"}
|
||
onFocus={() => useKeyEventStore.getState().setFocusInput(true)}
|
||
onBlur={() => useKeyEventStore.getState().setFocusInput(false)}
|
||
{...form.getInputProps("text_comments")}
|
||
/>
|
||
</Box>
|
||
<Box
|
||
px="xs"
|
||
pt="xs"
|
||
style={{
|
||
borderRadius: "2px",
|
||
border: "1px solid var(--mantine-color-blue-light)",
|
||
display: !operationSchema?.sub_attributes?.length
|
||
? "none"
|
||
: "block",
|
||
}}>
|
||
{operationSchema?.sub_attributes?.length ? (
|
||
<Text c="blue" fw={600} mx="xs">
|
||
子属性
|
||
</Text>
|
||
) : null}
|
||
<ScrollArea.Autosize mah={280}>
|
||
{operationSchema?.sub_attributes_describe.map((item) => {
|
||
if (item.select_mode === 0) {
|
||
return (
|
||
<TextInput
|
||
key={item.chinese_name}
|
||
label={item.chinese_name}
|
||
size="xs"
|
||
placeholder={`请输入${item.chinese_name}`}
|
||
// required={item.isrequired}
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}
|
||
onChange={(e) => {
|
||
form
|
||
.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)
|
||
.onChange(e)
|
||
handleChangeContextMenuSubAttr(
|
||
item.chinese_name,
|
||
e.target.value
|
||
)
|
||
}}
|
||
mb="xs"
|
||
/>
|
||
)
|
||
} else if (item.select_mode === 1) {
|
||
return (
|
||
<Radio.Group
|
||
key={item.chinese_name}
|
||
label={item.chinese_name}
|
||
size="xs"
|
||
// required={item.isrequired}
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}
|
||
onChange={(value) => {
|
||
form.setFieldValue(
|
||
`sub_attributes.${item.chinese_name}`,
|
||
value
|
||
)
|
||
handleChangeContextMenuSubAttr(
|
||
item.chinese_name,
|
||
value
|
||
)
|
||
}}
|
||
mb="xs">
|
||
<Stack gap="xs">
|
||
{item.optional_item.map((option) => (
|
||
<Radio key={option} value={option} label={option} />
|
||
))}
|
||
</Stack>
|
||
</Radio.Group>
|
||
)
|
||
} else if (item.select_mode === 2) {
|
||
return (
|
||
<Checkbox.Group
|
||
key={item.chinese_name}
|
||
label={item.chinese_name}
|
||
size="xs"
|
||
// required={item.isrequired}
|
||
{...form.getInputProps(
|
||
`sub_attributes.${item.chinese_name}`
|
||
)}
|
||
onChange={(value) => {
|
||
form.setFieldValue(
|
||
`sub_attributes.${item.chinese_name}`,
|
||
value
|
||
)
|
||
handleChangeContextMenuSubAttr(
|
||
item.chinese_name,
|
||
value.toString()
|
||
)
|
||
}}
|
||
mb="xs">
|
||
<Stack gap="xs">
|
||
{item.optional_item.map((option) => (
|
||
<Checkbox
|
||
key={option}
|
||
value={option}
|
||
label={option}
|
||
/>
|
||
))}
|
||
</Stack>
|
||
</Checkbox.Group>
|
||
)
|
||
}
|
||
return null
|
||
})}
|
||
</ScrollArea.Autosize>
|
||
</Box>
|
||
</Stack>
|
||
</Box>
|
||
</Card>
|
||
)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [
|
||
comments,
|
||
filterOperations,
|
||
getOperationSchema,
|
||
handleChangeContextMenuId,
|
||
handleChangeContextMenuOperation,
|
||
handleChangeContextMenuSubAttr,
|
||
id,
|
||
ids,
|
||
operationId,
|
||
position.left,
|
||
position.top,
|
||
showContextMenu,
|
||
])
|
||
|
||
const imageFilterCss = useMemo(() => {
|
||
return {
|
||
brightness: +(1 + imageFilter.brightness / 100).toFixed(2),
|
||
saturate: +(1 + imageFilter.saturate / 100).toFixed(2),
|
||
contrast: +(1 + imageFilter.contrast / 100).toFixed(2),
|
||
}
|
||
}, [imageFilter.brightness, imageFilter.contrast, imageFilter.saturate])
|
||
|
||
const handleCreatePathGroup = useCallback(() => {
|
||
let selectedPaths =
|
||
useObjectStore.getState().selectedPath[activeImage] || []
|
||
let groupId =
|
||
useRightToolsStore
|
||
.getState()
|
||
.pathGroupIds.reduce((a, b) => Math.max(a, b), 0) + 1
|
||
if (selectedPaths && selectedPaths.length > 1) {
|
||
// 修改group Map
|
||
useRightToolsStore
|
||
.getState()
|
||
.addPathGroupInMap(activeImage, groupId, selectedPaths)
|
||
// push groupId
|
||
useRightToolsStore.getState().pushPathGroupId(groupId)
|
||
}
|
||
}, [activeImage])
|
||
|
||
const crosshairComponentRef = useRef<any>(null)
|
||
|
||
const handleCrosshairMove = (event: { clientX: number; clientY: number }) => {
|
||
crosshairComponentRef.current?.updateCrosshair(event)
|
||
}
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
handleCreatePathGroup,
|
||
renderPolygons,
|
||
handleCrosshairMove,
|
||
renderTrackingAnnotation,
|
||
saveSupportAnnotationData,
|
||
copyAnnotationDataToMultiFrame,
|
||
}))
|
||
|
||
return (
|
||
<Box
|
||
w="100%"
|
||
h="100%"
|
||
pos="relative"
|
||
style={{
|
||
flex: 1,
|
||
filter: `brightness(${imageFilterCss.brightness}) saturate(${imageFilterCss.saturate}) contrast(${imageFilterCss.contrast})`,
|
||
}}
|
||
id="paper-container"
|
||
ref={containerRef}>
|
||
{crosshairStatus !== "hidden" && (
|
||
<CrosshairComponent
|
||
isCarved={crosshairStatus === "carve"}
|
||
ref={crosshairComponentRef}
|
||
/>
|
||
)}
|
||
{imgSize?.clientWidth && imgSize?.clientHeight && (
|
||
<canvas
|
||
id="paper-canvas"
|
||
width={imgSize?.clientWidth}
|
||
height={imgSize?.clientHeight}
|
||
ref={canvasElem}
|
||
style={{
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
zIndex: 40,
|
||
minWidth: "fit-content",
|
||
maxWidth: "fit-content",
|
||
}}
|
||
/>
|
||
)}
|
||
{contextMenu}
|
||
<Modal
|
||
opened={confirmModalOpened}
|
||
onClose={() => setConfirmModalOpened(false)}
|
||
title="提示"
|
||
centered>
|
||
<Text>其他帧已存在相同id的对象,是否覆盖?</Text>
|
||
<Group justify="flex-end" mt="md">
|
||
<Button
|
||
variant="default"
|
||
onClick={() => {
|
||
useBottomToolsStore.getState().setSelectedItems("", false, false)
|
||
setConfirmModalOpened(false)
|
||
}}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
if (confirmModalCallback) confirmModalCallback()
|
||
setConfirmModalOpened(false)
|
||
}}>
|
||
否
|
||
</Button>
|
||
<Button
|
||
onClick={() => {
|
||
if (confirmModalCallback) confirmModalCallback(true)
|
||
setConfirmModalOpened(false)
|
||
}}>
|
||
是
|
||
</Button>
|
||
</Group>
|
||
</Modal>
|
||
</Box>
|
||
)
|
||
}
|
||
|
||
export default forwardRef(PaperContainer)
|