Unknownpgr

꽁꽁 얼어붙은 한강 위로 고양이가 걸어다닙니다.

2024-07-14 19:27:23 | Korean

심심해서 HTML로 한강 고양이를 만들어봤습니다.

원본 사진을 가져온 후 K-means clustering을 사용하여 픽셀을 클러스터링합니다. 이때 단순히 픽셀의 색상값만을 가지고 클러스터링을 하게 되면 픽셀이 과도하게 파편화가 되므로 픽셀의 위치를 약간 가중치로 주어 클러스터링합니다.

이후 각 클러스터의 마스크에 대해 contour를 구합니다. 이때 HTML에서 다각형을 표현하기 위해서는 CSS의 clip-path를 사용해야 하는데, 이때 css의 clip-path 속성은 경계를 포함하지 않으므로 렌더링하는 경우 클러스터 사이의 경계가 비는 현상이 발생합니다. 따라서 contour를 구하기 전에 1px dilation을 적용하여 contour를 확장합니다.

이후 이것을 json으로 export한 후 적절히 html로 변환합니다. html로 변환 시 각 클러스터를 하나의 div에 대응시키고, 각 div에 대해 clip-path를 적용합니다. 이 작업은 문자열 연산으로 수행할 수도 있지만 여러모로 번거롭기 때문에 React를 사용한 후 개발자 도구에서 Element를 복사해왔습니다.

아래는 Python 코드입니다.

import json

import cv2
import numpy as np
from sklearn.cluster import KMeans

N = 64

print("Clustering")
img = cv2.imread("input.png")
img = cv2.resize(img, (0, 0), fx=0.5, fy=0.5)

positions = np.indices(img.shape[:2]).transpose(1, 2, 0) / 64
pixel_position_img = np.concatenate([img, positions], axis=2)
pixels = pixel_position_img.reshape(-1, pixel_position_img.shape[2])
kmeans = KMeans(n_clusters=N).fit(pixels)
labels = kmeans.predict(pixels).reshape(img.shape[:2])

empty = np.zeros_like(img)
filtered_contours = []
kernel = np.ones((3, 3), np.uint8)
for i in range(N):
    print(f"Processing cluster {i}")
    mask = labels == i
    mean = np.mean(img[mask], axis=0)
    mask = mask.astype(np.uint8)
    mask = cv2.dilate(mask, kernel, iterations=1)
    contours = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
    for contour in contours:
        color = tuple(map(int, mean))
        cv2.drawContours(empty, [contour], -1, color, -1)
        contour = np.squeeze(contour, axis=1).tolist()
        filtered_contours.append((contour, color))


with open("output.json", "w") as f:
    json.dump(filtered_contours, f)

아래는 React 코드입니다.

import _contours from "./output.json";
const contours: Contour[] = _contours as Contour[];

type Vec2 = [number, number];
type Color = [number, number, number];
type Contour = [Vec2[], Color];

export function Chuu() {
  return (
    <div>
      <h1>꽁꽁 얼어붙은 한강 위로 고양이가 걸어다닙니다.</h1>
      <div
        style={{
          position: "relative",
        }}>
        {contours.map((contour, i) => {
          const contourPoints = contour[0];
          const contourColor = contour[1];

          let left = Infinity;
          let right = -Infinity;
          let top = Infinity;
          let bottom = -Infinity;

          for (const point of contourPoints) {
            left = Math.min(left, point[0]);
            right = Math.max(right, point[0]);
            top = Math.min(top, point[1]);
            bottom = Math.max(bottom, point[1]);
          }

          const width = right - left;
          const height = bottom - top;

          const clipPath = `polygon(${contourPoints
            .map(
              (point) =>
                `${((point[0] - left) * 100) / width}% ${
                  ((point[1] - top) * 100) / height
                }%`
            )
            .join(", ")})`;
          const colorString = `rgb(${contourColor[2]}, ${contourColor[1]}, ${contourColor[0]})`;
          return (
            <div
              key={i}
              style={{
                position: "absolute",
                left: `${left}px`,
                top: `${top}px`,
                clipPath,
                backgroundColor: colorString,
                width,
                height,
              }}
            />
          );
        })}
      </div>
    </div>
  );
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -