티스토리 뷰

https://sungchuni.tistory.com/12

캔버스를 통한 이미지 다운스케일링 작업 시 발생하는 앨리어싱(계단 현상) 문제에 대해 임상적으로 알아보겠습니다.

 

우선 깨끗한 body를 준비하고, 스크립트 링크도 연결합니다.

<!-- /index.html -->
<!DOCTYPE html>
<html>
  <head>
    <script src="./index.js" defer></script>
  </head>
  <body></body>
</html>

이미지 파일을 업로드할 파일 타입의 입력 상자와, 리사이즈 목표 픽셀을 입력할 숫자 타입의 입력 상자를 추가합니다.

// /index.js
const inputElements = {
  file: null,
  number: null
};

function createInputFile() {
  const input = document.createElement("input");
  Object.assign(input, {
    accept: "image/*",
    multiple: true,
    type: "file"
  });
  return document.body.appendChild(input);
}

function createInputNumber() {
  const input = document.createElement("input");
  Object.assign(input, {
    min: 0,
    type: "number",
    value: 400
  });
  return document.body.appendChild(input);
}

function main() {
  inputElements.file = createInputFile();
  inputElements.number = createInputNumber();
}

window.addEventListener("DOMContentLoaded", main, {once: true});

파일 입력 시의 핸들러 함수를 정의합니다. 입력된 파일이 이미지 형식이면 이미지 요소와 objectURL을 만들어둡니다. 나중에 CanvasRenderingContext2D#drawImage 메소드를 호출할 때에 사용될 것입니다.

// /index.js
// ...
const objectURLs = new Set();
// ...
function handleChange(event) {
  const {files} = event.target;
  for (const file of files) {
    if (!file.type.startsWith("image/")) continue;
    const image = new Image();
    const objectURL = window.URL.createObjectURL(file);
    objectURLs.add(objectURL);
    image.src = objectURL;
  }
}
// ...
function main() {
  // ...
  inputElements.file.addEventListener("change", handleChange);
}
// ...

 

이제 원본 이미지가 준비되었습니다. 캔버스 요소와 렌더링 컨텍스트를 반환하는 헬퍼 함수를 작성하고, 우선 기본적인 방식으로 drawImage 메소드를 사용해봅시다.

function createCanvas(image) {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  const ratio = image.height / image.width;
  const width = Number(inputElements.number.value) || image.width;
  const height = width * ratio;
  Object.assign(canvas, {width, height});
  document.body.appendChild(canvas);
  return {canvas, ctx};
}

function renderDefault(image) {
  const {canvas, ctx} = createCanvas(image);
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
}

두번째 방식은 CanvasRenderingContext2D#imageSmoothingQuality 속성을 사용하는 방식입니다. 이것은 파이어폭스와 IE에서 지원되지 않습니다. (물론 지금 작성하는 자바스크립트 프로그램은 IE에서 실행되지 않습니다.)

function renderSmoothing(image, quality) {
  const {canvas, ctx} = createCanvas(image);
  ctx.imageSmoothingEnabled = true;
  ctx.imageSmoothingQuality = quality;
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
}

마지막 방식은 원본 이미지의 절반씩 다운스케일링을 진행하는 방식입니다. 브라우저가 다운스케일링 시 2x2 샘플링을 사용하는 것으로 알려져 있어, 이와 같은 방식으로 안티앨리어싱을 적용할 수 있겠습니다. 반복 횟수만큼 최종 타겟 이미지의 픽셀 하나가 사용하게 되는 샘플 크기가 늘어납니다. (2번 다운스케일링을 진행한다면 4x4 샘플링, 3번이라면 8x8 샘플링 등등)

function renderSupersample(image) {
  const {canvas, ctx} = createCanvas(image);
  const offscreenCanvas = document.createElement("canvas");
  const offscreenCtx = offscreenCanvas.getContext("2d");
  Object.assign(offscreenCanvas, {width: image.width, height: image.height});
  offscreenCtx.drawImage(image, 0, 0);
  const total = Math.max(
    Math.floor(Math.log(image.width / canvas.width) / Math.log(2)),
    0
  );
  const iteration = {total, count: total};
  while (iteration.count-- > 0) {
    offscreenCtx.drawImage(
      offscreenCanvas,
      0,
      0,
      offscreenCanvas.width * 0.5,
      offscreenCanvas.height * 0.5
    );
  }
  ctx.drawImage(
    offscreenCanvas,
    0,
    0,
    offscreenCanvas.width * 0.5 ** iteration.total,
    offscreenCanvas.height * 0.5 ** iteration.total,
    0,
    0,
    canvas.width,
    canvas.height
  );
}

이제 handleChange 함수에 위 함수들을 추가합니다. 파일이 다시 입력되면 기존의 objectURL과 캔버스 요소를 삭제하여, 새로운 결과물을 확인할 수 있도록 합니다.

async function handleChange(event) {
  objectURLs.forEach((objectURL) => {
    window.URL.revokeObjectURL(objectURL);
    objectURLs.delete(objectURL);
  });
  document.querySelectorAll("canvas").forEach((canvas) => {
    document.body.removeChild(canvas);
  });
  const {files} = event.target;
  for (const file of files) {
    if (!file.type.startsWith("image/")) continue;
    const image = new Image();
    const objectURL = window.URL.createObjectURL(file);
    objectURLs.add(objectURL);
    image.src = objectURL;
    await image.decode();
    renderDefault(image);
    ["low", "medium", "high"].forEach(renderSmoothing.bind(null, image));
    renderSupersample(image);
  }
}

렌더링한 결과물을 조금 더 쉽게 비교할 수 있도록 drawText, strokeText 메소드를 호출하는 헬퍼 함수를 하나 만듭니다.

function drawText(canvas, ctx, text) {
  ctx.font = "bold 32px sans-serif";
  ctx.fillStyle = "white";
  ctx.strokeStyle = "black";
  ctx.textAlign = "center";
  ctx.fillText(text, canvas.width * 0.5, canvas.height * 0.5);
  ctx.strokeText(text, canvas.width * 0.5, canvas.height * 0.5);
}

function renderDefault(image) {
  // ...
  drawText(canvas, ctx, "default");
}

function renderSmoothing(image, quality) {
  // ...
  drawText(canvas, ctx, quality);
}

function renderSupersample(image) {
  // ...
  drawText(canvas, ctx, "super sample");
}

이것으로 됐습니다. 

 

완성된 소스 코드는 https://github.com/sungchuni/canvas-aliasing에서,

프로그램은 https://sungchuni.github.io/canvas-aliasing/에서 보실 수 있습니다.

 

각자 이미지 파일을 입력해봅니다. 이미지 내용에 따라 그 정도는 다르겠지만 대부분의 경우 기본 설정의 렌더링 시 앨리어싱 문제가 있다는 것을 직접 확인할 수 있습니다.

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크