Feat: 레이블 카테고리 수동 변경 기능 추가 - S11P21S002-236

This commit is contained in:
jhynsoo 2024-09-26 19:31:53 +09:00
parent 4a71a25a23
commit c58a6a5fd4
7 changed files with 125 additions and 36 deletions

View File

@ -11,34 +11,29 @@ import { Label } from '@/types';
import useLabelJson from '@/hooks/useLabelJson';
import { saveImageLabels } from '@/api/lablingApi';
import useProjectStore from '@/stores/useProjectStore';
import { LABEL_CATEGORY } from '@/constants';
export default function ImageCanvas() {
const project = useProjectStore((state) => state.project)!;
const { id: imageId, imagePath, dataPath } = useCanvasStore((state) => state.image)!;
const { data: labelData } = useLabelJson(dataPath, project);
const { labels, drawState, setDrawState, addLabel, setLabels, selectedLabelId, setSelectedLabelId, sidebarSize } =
useCanvasStore();
const { shapes } = labelData || [];
const selectedLabelId = useCanvasStore((state) => state.selectedLabelId);
const setSelectedLabelId = useCanvasStore((state) => state.setSelectedLabelId);
const sidebarSize = useCanvasStore((state) => state.sidebarSize);
const stageWidth = window.innerWidth * ((100 - sidebarSize) / 100) - 201;
const stageHeight = window.innerHeight - 64;
const stageRef = useRef<Konva.Stage>(null);
const dragLayerRef = useRef<Konva.Layer>(null);
const scale = useRef<number>(0);
const labels = useCanvasStore((state) => state.labels) ?? [];
const [image] = useImage(imagePath);
const drawState = useCanvasStore((state) => state.drawState);
const setDrawState = useCanvasStore((state) => state.setDrawState);
const addLabel = useCanvasStore((state) => state.addLabel);
const setLabels = useCanvasStore((state) => state.setLabels);
const [rectPoints, setRectPoints] = useState<[number, number][]>([]);
const [polygonPoints, setPolygonPoints] = useState<[number, number][]>([]);
useEffect(() => {
setLabels(
shapes.map<Label>(({ label, color, points, shape_type }, index) => ({
shapes.map<Label>(({ group_id, color, points, shape_type }, index) => ({
id: index,
name: label,
categoryId: group_id,
color,
type: shape_type === 'polygon' ? 'polygon' : 'rect',
coordinates: points,
@ -58,11 +53,13 @@ export default function ImageCanvas() {
const saveJson = () => {
const json = JSON.stringify({
...labelData,
shapes: labels.map(({ name, color, coordinates, type }) => ({
label: name,
shapes: labels.map(({ categoryId, color, coordinates, type }) => ({
label: LABEL_CATEGORY[categoryId],
color,
shape_type: type === 'polygon' ? 'polygon' : 'rectangle',
points: coordinates,
group_id: categoryId,
shape_type: type === 'polygon' ? 'polygon' : 'rectangle',
flags: {},
})),
imageWidth: image!.width,
imageHeight: image!.height,
@ -128,7 +125,7 @@ export default function ImageCanvas() {
const id = labels.length;
addLabel({
id: id,
name: 'label',
categoryId: 0,
type: 'polygon',
color: `#${color}`,
coordinates: polygonPoints.slice(0, -1),
@ -155,7 +152,7 @@ export default function ImageCanvas() {
const id = labels.length;
addLabel({
id: id,
name: 'label',
categoryId: 0,
type: 'rect',
color: `#${color}`,
coordinates: rectPoints,

View File

@ -1,14 +1,7 @@
import { useEffect, useState } from 'react';
import { Image, Layer, Stage, Line, Rect } from 'react-konva';
import useImage from 'use-image';
import { Label } from '@/types';
interface Shape {
label: string;
color: string;
points: [number, number][];
shape_type: 'polygon' | 'rectangle';
}
import { Label, Shape } from '@/types';
interface ImageWithLabelsProps {
imagePath: string;
@ -27,7 +20,7 @@ export default function ImageWithLabels({ imagePath, labelData }: ImageWithLabel
const json: { shapes: Shape[] } = await response.json();
const shapes = json.shapes.map((shape, index) => ({
id: index,
name: shape.label,
categoryId: shape.categoryId,
color: shape.color,
type: shape.shape_type === 'polygon' ? 'polygon' : 'rect',
coordinates: shape.points,

View File

@ -1,3 +1,4 @@
import { LABEL_CATEGORY } from '@/constants';
import useCanvasStore from '@/stores/useCanvasStore';
import { Label } from '@/types';
import { Trash2 } from 'lucide-react';
@ -5,7 +6,7 @@ import { MouseEventHandler } from 'react';
export default function LabelButton({
id,
name,
categoryId,
color,
selected,
setSelectedLabelId,
@ -23,10 +24,10 @@ export default function LabelButton({
return (
<div
className={`flex items-center gap-2.5 rounded-lg transition-colors ${selected ? 'bg-gray-200' : 'bg-gray-50 hover:bg-gray-100'}`}
className={`flex items-center rounded-lg transition-colors ${selected ? 'bg-gray-200' : 'bg-gray-50 hover:bg-gray-100'}`}
>
<button
className="flex grow items-center gap-2.5 p-2.5 text-left"
<div
className="flex grow cursor-pointer items-center gap-2.5 p-2.5 text-left"
onClick={handleClick}
>
<div
@ -35,8 +36,24 @@ export default function LabelButton({
backgroundColor: color,
}}
/>
<span className="body grow text-gray-900">{name}</span>
</button>
<select
className="body-small w-[97.2px] cursor-pointer rounded bg-transparent"
value={categoryId.toString()}
onChange={(event) => {
const newCategoryId = Number(event.target.value);
setLabels(labels.map((label) => (label.id === id ? { ...label, categoryId: newCategoryId } : label)));
}}
>
{LABEL_CATEGORY.map((category, index) => (
<option
value={index.toString()}
key={index}
>
{category}
</option>
))}
</select>
</div>
<button
className="p-2.5"
onClick={handleDelete}

82
frontend/src/constants.ts Normal file
View File

@ -0,0 +1,82 @@
export const LABEL_CATEGORY = [
'person',
'bicycle',
'car',
'motorcycle',
'airplane',
'bus',
'train',
'truck',
'boat',
'traffic light',
'fire hydrant',
'stop sign',
'parking meter',
'bench',
'bird',
'cat',
'dog',
'horse',
'sheep',
'cow',
'elephant',
'bear',
'zebra',
'giraffe',
'backpack',
'umbrella',
'handbag',
'tie',
'suitcase',
'frisbee',
'skis',
'snowboard',
'sports ball',
'kite',
'baseball bat',
'baseball glove',
'skateboard',
'surfboard',
'tennis racket',
'bottle',
'wine glass',
'cup',
'fork',
'knife',
'spoon',
'bowl',
'banana',
'apple',
'sandwich',
'orange',
'broccoli',
'carrot',
'hot dog',
'pizza',
'donut',
'cake',
'chair',
'couch',
'potted plant',
'bed',
'dining table',
'toilet',
'tv',
'laptop',
'mouse',
'remote',
'keyboard',
'cell phone',
'microwave',
'oven',
'toaster',
'sink',
'refrigerator',
'book',
'clock',
'vase',
'scissors',
'teddy bear',
'hair drier',
'toothbrush',
];

View File

@ -32,7 +32,7 @@ export type Workspace = {
// 레이블 관련 타입
export type Label = {
id: number;
name: string;
categoryId: number;
color: string;
type: 'polygon' | 'rect';
coordinates: Array<[number, number]>;
@ -248,7 +248,7 @@ export interface RefreshTokenResponse {
}
export interface Shape {
label: string;
categoryId: number;
color: string;
points: [number, number][];
group_id: number;

View File

@ -4,7 +4,7 @@ describe('createLabelJson', () => {
it('should create a label JSON for classification', () => {
const result = createLabelJson('classification');
expect(result).toEqual({
version: '0.1.0',
version: '0.0.0',
task_type: 'cls',
shapes: [],
split: 'none',
@ -17,7 +17,7 @@ describe('createLabelJson', () => {
it('should create a label JSON for detection', () => {
const result = createLabelJson('detection');
expect(result).toEqual({
version: '0.1.0',
version: '0.0.0',
task_type: 'det',
shapes: [],
split: 'none',
@ -30,7 +30,7 @@ describe('createLabelJson', () => {
it('should create a label JSON for segmentation', () => {
const result = createLabelJson('segmentation');
expect(result).toEqual({
version: '0.1.0',
version: '0.0.0',
task_type: 'seg',
shapes: [],
split: 'none',

View File

@ -2,7 +2,7 @@ import { LabelJson } from '@/types';
export default function createLabelJson(type: 'classification' | 'detection' | 'segmentation'): LabelJson {
return {
version: '0.1.0',
version: '0.0.0',
task_type: type === 'classification' ? 'cls' : type === 'detection' ? 'det' : 'seg',
shapes: [],
split: 'none',