(TIL) Node.js : 이미지 합성 스크립트

김동우·2021년 11월 3일
0

nodejs

목록 보기
1/1

이미지 합성

Node에도 python의 openCV와 같이 이미지를 바이너리 코드로 변경해서 합성해주는 라이브러리가 있습니다.

정확하게는 배경이 되는 이미지에 특정 이미지를 쌓는 형태로, 덮어쓰는 것과 같이 동작하는 것 같습니다.

예시 )

from

to

mergeImages

대표적인 Image merge library인데, 저는 merge-images 라는 라이브러리를 활용해서 여러 이미지를 한 장에 합치는 스크립트를 작성했습니다.

install

  1. npm i merge-images
  2. npm i canvas

Node, npm(or yarn)이 있다면 두 라이브러리를 설치하면 됩니다.

참고링크

logic

로직은 생각보다 간단합니다.

  1. 사진파일이 들어있는 경로의 파일명을 가져와 저장한다.
  2. 파일 이름을 라이브러리가 원하는 구조로 변경한다.
  3. 2에서 정제한 Array를 정제해 output 파일명을 결정합니다.
  4. 합성재료로 2, 3을 사용해 합성한다.

의 flow로 진행됩니다.

그럼 코드를 볼까요?

const { writeFile } = require("fs");

const mergeImages = require("merge-images");
const { Canvas, Image } = require("canvas");

// 1. 파일명을 가져온 뒤, 배열에 저장한다.
const readFiles = () => {
  const fs = require("fs");
	
  // 5장의 사진을 합치기 위해 폴더(카테고리)로 나눈 뒤, 하나씩 다섯개의 폴더를 리스트화합니다.
  const IMG_FOLDER_LIST = [
    "dir1", // 파일에서 이미지에 접근할 상대경로
    "dir2",
    "dir3",
    "dir4",
    "dir5"
  ];

  // 각 5개 경로의 파일을 읽어옵니다. (2차원 Array)
  const fileReader = (dirArr) => {
    return dirArr.map((dir) => fs.readdirSync(dir).map((fileName) => dir + "/" + fileName));
  };

  // 불러온 파일명 Array를 반환
  return fileReader(IMG_FOLDER_LIST);
};


// 2. 라이브러리가 원하는 자료구조 형태로 변경합니다.
const fileNameGenerator = (prevFileNameArr) => {
  const fileNames = [];

  // 입력받은 2개의 Array에 대해 모든 경우의 수에 대해 합성합니다.
  // 최종적으로 5개 카테고리별로 각각 2장의 사진이라면 2^5 개의 element를 갖는 array가 나오게 됩니다.
  
  // input : [string[], string[]] (파일명 Array tuple)
  // => [[1,1], [2,2]]
  // output : [[1,2], [1,2], [1,2], [1,2]]
  
  // 만약 3개의 카테고리라면,
  // input: [[string[]], string[]]
  // => [[[1,2], [1,2]...], [3,3]]
  // output : [[1,2,3], [1,2,3], ...]
  for (let i = 0; i < prevFileNameArr[0].length; i++) {
    for (let j = 0; j < prevFileNameArr[1].length; j++) {
      if (typeof prevFileNameArr[0][i] === "string") {
        fileNames.push([prevFileNameArr[0][i], prevFileNameArr[1][j]]);
      } else {
        fileNames.push([...prevFileNameArr[0][i], prevFileNameArr[1][j]]);
      }
    }
  }

  return fileNames;
};

// 3, 4.
// 3은 내부함수로 구현했고,
// 4는 2, 3을 가지고 하나의 아웃풋(합성된 사진)을 만들어냅니다.
const mergeImage = (fileNamesArr) => {
  // 초기에는 2개의 카테고리, 2장의 이미지를 먼저 합성하기 위해 구조를 변경합니다.
  // input : [[1,1], [2,2]] 
  // ouput : [[1,2], [1,2], [1,2], [1,2]]
  let generatedArr = fileNameGenerator(fileNamesArr.slice(0, 2));

  // 이후 과정은 사진을 한장씩 추가하는 형태의 반복문입니다.
  // 3장이라면, [[1,2,3], [1,2,3], ...] 으로요
  for (let i = 2; i < fileNamesArr.length; i++) {
    let newArr = fileNameGenerator([generatedArr, fileNamesArr[i]]);
    generatedArr = newArr;
  }

  // 반복 이후 모든 파일명이 담긴 배열을 정제합니다.
  // 원하는 output naming을 위한 로직이니, 마음대로 custom할 수 있습니다.
  const outputNameArr = generatedArr.map((el) => {
    return el.map((el2) => {
      return el2.split("_")[4].split(".")[0];
    });
  });

  // 2의 과정을 마친 Array 내부의 원소들을 순회하며 라이브러리 메서드를 적용합니다.
  // Array elem (img files read, merge) => b64
  // b64 => png write
  // generatedArr를 기준으로 파일명을 분리했기 때문에 outputNameArr는 동일 조합에 대한 mapping이 이루어져 있습니다.
  generatedArr.forEach((imgs, idx) => {
    console.log(outputNameArr[idx]);
    mergeImages(imgs, {
      Canvas: Canvas,
      Image: Image,
    }).then((b64) => {
      const b64Data = b64.replace(/^data:image\/png;base64,/, "");
      writeFile(`./data/_OUTPUT/merged/${outputNameArr[idx].join("_")}.png`, b64Data, "base64", (err) => {
        console.log(err);
      });
    });
  });
};

코드는 생각보다 길지 않습니다.

정상적으로 동작하는 것도 잘 확인했고, 결과물도 나쁘지 않았습니다.

Refactoring

  1. 성능 개선을 위해 초기 코드에서 정규표현식을 최대한 제거하고, 문자열을 split으로 처리하기로 했습니다.

split.join.replace => split[] 로 변경

Js Array search 속도를 고려해 index 접근 방식으로 복잡도를 줄일 수 있을까 시도했으나, 근본적인 문제는 그게 아니었습니다.

  1. 라이브러리 메소드 자체가 느리다.

라이브러리를 사용하는 입장에서 이런 얘기를 하면 안되는데, 속도가 조금 늦은 감이 있습니다.

물론, 2장의 이미지를 합성할 때는 상당히 빠릅니다.

문제는 제가 5장의 이미지를 합성하기에 기하급수적으로 복잡도가 올라갑니다.

한번에 다수의 이미지를 합성하는게 문제인 것으로 판단되는데, 추후 generate 방식과 마찬가지로 2장씩 합성하는 loop로 해결해보는건 어떨지 고민하고 있습니다.

이상입니다.

0개의 댓글