header wave

Post

프로젝트) 이미지 라벨링 사이트 - 3

2022-10-06 AM 04/53
#javascript
#react
#study

4) Drawing Tool

HTML canvas에 Rectangle 라벨을 그리는 Drawing Tool을 만들었다.

본격적으로 라벨링 툴을 만들기 위해서, 3가지 이벤트 단계를 구분했다.

img.png

마우스 이벤트 3단계

1. onMouseDown

먼저 마우스를 클릭했을 때 좌표객체가 만들어져야 한다.

createElement란 함수를 통해서, 좌표객체를 생성했다. 내용물이 굉장히 많은데

id는 현재 좌표객체의 길이값을 id로 설정했다.

type은 "rectangle"로 라벨링의 모양을 결정한다. (추후 다른 툴들도 쉽게 적용하기 위해서다.)

labelName, labelId, labelColor,의 라벨링 정보를 담는다. (묶어서 하나의 객체 안에 라벨링 정보값을 담을 수도 있지만

key, value 값이 자꾸 헷갈려 일일히 추가했다.)

imageId는 현재 라벨링이 그려지고 있는 image의 id 값이다.

 const createElement = (
    id,
    x1,
    y1,
    x2,
    y2,
    type,
    labelName,
    labelId,
    labelColor,
    imageId
  ) => {
    return {
      id,
      x1,
      y1,
      x2,
      y2,
      type,
      labelName,
      labelId,
      labelColor,
      imageId,
    };
  };

2. onMouseMove

사각형을 그리려면 두 점이 필요하다. (X1, Y1), (X2, Y2)

onMouseMove에서는 두 번째 마우스 커서에 따라 움직이는 X2, Y2값을 업데이트해준다.

img.png

사각형을 그리는 방법

여기서 문제가 하나 발생했다.

상위 컨테이너의 속성은 relative이고, layer canvas의 속성의 absolute이기에 좌표값이 상위 컨테이너 기준으로 측정되어 제대로 된 마우스 커서의 좌표를 갱신받을 수 없었다.

그래서 canvas의 크기를 기준으로 좌표를 스케일링 해주는 함수를 만들었다.

DOM 요소의 프로토타입인 getBoundingClientRect()을 사용하면 뷰포트 기준으로 canvas의 위치를 알 수 있다.

img.png

https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect

이를 통해 캔버스의 실제 좌표로 스케일링 해주는 realXY란 함수를 만들었다.

  // function for real calculate real x,y position in canvas(layer)
  const realXY = (canvas, event) => {
    const rect = canvas.getBoundingClientRect();
    return {
      clientX:
        ((event.clientX - rect.left) / (rect.right - rect.left)) * canvas.width,
      clientY:
        ((event.clientY - rect.top) / (rect.bottom - rect.top)) * canvas.height,
    };
  };

3. onMouseUp

마우스를 놓게 되면, 마지막으로 지정된 x2,y2를 통해 라벨링 객체를 만들어 주면 된다.

라벨링 객체를 canvas에 그려주는 부분은 useEffect 내에서 실행했다.

        elements.forEach(({ x1, x2, y1, y2, labelColor, imageId }) => {
          if (currentImageId === imageId) {
            ctx.strokeStyle = labelColor;
            ctx.lineWidth = 3;
            ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
          }
        });

elements는 라벨링 객체가 들어있는 state 값인데, 여기서 여기서 현재 이미지 ID 같은 라벨링 속성을 찾아 그려주게 된다.

img.gif

Drawing

5) Moving & Resizing

라벨들을 이동시키거나, 움직이는 기능도 만들었다.

이를 위해서는 라벨요소에 마우스 커서가 근접했을 때, 마우스 커서의 위치를 먼저 찾아줘야 한다.

Inside 내에서는 Moving 이벤트가 발생하고, 라벨의 네 꼭지점에서는 Resizng Event가 발생할 것이다.

img.png

cursor position

  // offset for select
  const nearPoint = (x, y, x1, y1, name) => {
    return Math.abs(x - x1) < 5 && Math.abs(y - y1) < 5 ? name : null;
  };

이를 위한 함수이다.

nearPoint 함수는 현재 마우스 커서의 좌표와 라벨링의 꼭짓점 좌표가 입력된다.

현재 라벨링의 꼭짓점 좌표에서 대각선으로 5 이내의 거리만큼 떨어져있다면 현재 마우스 커서의 위치를 출력해준다.

img.png

positionWithinElement 함수는 위의 nearPoint 함수를 통해 라벨링 모서리 근처의 마우스 커서 위치를 구하거나, 라벨링의 x1,y1 좌표와 x2, y2좌표 이내에 커서가 있다면 "inside"를 반환해주는 함수이다.

  const positionWithinElement = (x, y, element) => {
    const { type, x1, y1, x2, y2 } = element;
    if (type === "rectangle") {
      const topLeft = nearPoint(x, y, x1, y1, "tl");
      const topRight = nearPoint(x, y, x2, y1, "tr");
      const bottomLeft = nearPoint(x, y, x1, y2, "bl");
      const bottomRight = nearPoint(x, y, x2, y2, "br");
      const inside = x >= x1 && x <= x2 && y >= y1 && y <= y2 ? "inside" : null;

      return topLeft || topRight || bottomLeft || bottomRight || inside;
    }
  };

현재 마우스 커서의 위치가 라벨의 꼭지점이라면 resizing이 발생한다.

하단의 코드를 통해 선택된 꼭지점을 기준으로 마우스 커서의 좌표에 따라 크기가 변한다.

  const resizedCoordinates = (clientX, clientY, position, coordinates) => {
    const { x1, y1, x2, y2 } = coordinates;
    switch (position) {
      case "tl":
        return { x1: clientX, y1: clientY, x2, y2 };
      case "tr":
        return { x1, y1: clientY, x2: clientX, y2 };
      case "bl":
        return { x1: clientX, y1, x2, y2: clientY };
      case "br":
        return { x1, y1, x2: clientX, y2: clientY };
      default:
        return null; //should not really get here...
    }
  };

현재의 커서가 inside라면 moving 이벤트가 발생한다.

새로 움직인 좌표에 따라, 라벨의 좌표를 업데이트 해주면 된다.

img.gif

img.gif

moving & resizing