Web Canvas Image 기초

HTML5 Canvas 에서 이미지 다루기

Canvas 에서 Image ?

WebGL에서 Texture 부분을 정리하다 보면, Nearest, Bilinear 등의 용어를 접하게 됩니다.
프로그램을 개발하는 관점에서는 추출하는 방식을 지정한 후, GPU 에서 수행한 결과를 확인하면 되는 것이니, 구체적인 알고리즘을 굳이 확인하지 않아도 문제가 되지는 않습니다.
하지만, Web 특히 Canvas 에서 제공하는 강력한 기능중 하나가, Image 를 Uint8Array 의 8bit 이미지 배열로 다룰 수 있도록 데이터를 제공하고 있는 것입니다.
이런 데이터를 기반으로 간단한 Image Processing 작업, 더 나아가 OpenCV 에서 제공하는 기능도 일부 활용할 수 있는 수준에 있습니다.
Canvas 에서 수행하는 작업은 WebGL 이 아닐 경우 CPU 연산을 통해 작업이 진행됩니다. 작업 과정이 명료해지니, 정리하기에는 더 좋을 수도 있을것 같습니다. 하나씩 확인해 보고자 합니다.

Image Loading async, await

Web 에서 Image는 데이터의 크기가 일반 text 보다 크고, 경우에 따라서는 수십메가 이상의 데이터를 로딩해야 할 수도 있습니다. 이런 데이터를 기반으로 작업을 해야하는 모듈은 이미지가 완전히 로딩된 후에 작업을 해야 문제가 발생하지 않습니다.
이런 이유로 이미지 로딩을 기다려 처리하는 로직을 만들어 사용하고 있습니다.
예전에 많이 사용하던 방식은 다음과 같습니다.

     var image = new Image();
     image.onload = function() {
         //  do something ...
     };
     image.src = url;

물론 이런 방식을 사용하는 것은 현재도 가능하고, 직관적이기 까지 합니다. 경우에 따라서 사용 할 수도 있습니다.
하지만 모듈이 복잡해지고, 비동기화 기법이 javascript 에 도입되면서 점차 아래와 같은 형식으로 변화하고 있습니다.


     export const loadImageFromURL = async (url) => {
         const img = new Image();
         return new Promise((resolve, reject) => {
             img.addEventListener("load", () => {
                 resolve(img);
             },false);
             img.addEventListener("error", () => {
                 reject(img);
             });
             img.src = url;
         });
     };

     export const makeCanvasImageData = async (url) => {
         const image = await loadImageFromURL(url);
         const canvas = document.createElement("canvas");
         const width = image.width;
         const height = image.height;
         canvas.width = width;
         canvas.height = height;
         const ctx = canvas.getContext("2d");
         ctx.drawImage(image,0,0);
         return ctx.getImageData(0,0,width,height);
     };

loadImageFromURL 함수는 async 라는 키워드와 함께 반환이 Promise 객체를 반환하고 있습니다. makeCanvasImageData 는 예시로 든 함수 입니다. 주요하게 볼 부분은 async 가 함수 에 정의 되어 있고, 이 때문에 await 를 사용할 수 있습니다. await 는 결과가 종료될 때 까지 대기 한다는 의미 입니다.

Uint8Array - getImageData, putImageData

Canvas 에서 이미지 데이터 처리하는 부분을 강조해 보이려다 보니 조금 억지스런 예시가 되었네요 …. 일반적으로는 이미지를 로딩하고 ImageData 만 가져오는데 putImageData 를 활용하기 위한 예제 입니다.

     <script type="module">
         import * as CanvasUtils from '../../js/CanvasUtils.js';

         const loadSampleData = async (url) => {
             let imageData = await CanvasUtils.makeCanvasImageData(url);
             const canvas = CanvasUtils.makeCanvasObject("testID", null, imageData.width, imageData.height );
             const ctx = canvas.getContext("2d");
             ctx.putImageData(imageData, 0, 0);
         }
         loadSampleData("/imgs/sea01.jpg");
     </script>    

결과이미지

이 이미지의 빨간색 영역과 파란색 영역을 바꿔 보겠습니다.

변경이미지

위의 예를 조금 변경한 소스 입니다.

     function changeRedBluePositions( orgData, newData ) {
         for ( let i = 0, iSize = orgData.data.length; i < iSize; i += 4 ) {
             //  r g b a
             newData.data[i+2] = orgData.data[i];     //  r
             newData.data[i+1] = orgData.data[i+1];   //  g
             newData.data[i] = orgData.data[i+2];     //  b
             newData.data[i+3] = orgData.data[i+3];   //  a
         }
     }

     const loadSampleData = async (url) => {
         let imageData = await CanvasUtils.makeCanvasImageData(url);
         const canvas = CanvasUtils.makeCanvasObject("testID", null, imageData.width, imageData.height );
         const ctx = canvas.getContext("2d");
         ctx.putImageData(imageData, 0, 0);

         const nImageData = ctx.createImageData(imageData.width, imageData.height);

         changeRedBluePositions(imageData, nImageData);
         ctx.putImageData(nImageData, 0, 0);
     }

예시를 위해 조금 억지스럽게 구성하였지만, 간단히 살펴보면 imageData 의 data 는 Uint8Array 입니다. 각 8bit 씩(255) red, green, blue, alpha (빨강, 녹색, 파란색, 투명도) 가 기록되어 있습니다.
빨간색과 파란색 위치만 변경해서 데이터를 구성합니다. 그 결과가 보이는 화면입니다.

Pixel Data 살펴보기

Canvas 는 어떤면에서는 일반 프로그래밍 언어의 Graphics 객체가 제공하는 낮은 Level 의 제어가 가능한 Web 자원입니다.
이미지(png)는 canvas 위에 무엇이 그려져 있던 toDataURL() 이라는 함수로, png 파일로 생성이 가능합니다.
이 파일이 console 에 출력해 보면 base64 encoding 된 문자열로 구성되어 있고, 이를 이미지 파일로 사용할 수도 있습니다.
Canvas 자체가 이미지를 구성할 수 있는 기본 틀을 제공하고 있는 것입니다.
createImageData, getImageData, putImageData 를 통해 직접 이미지 data 에 접근할 수 있는 기능을 제공하고 있고, 각 데이터를 통해 얻은 객체가 imageData 라고 한다면, imageData.data 라는 자료형은 UInt8Array 의 Typed Array 형식으로 접근할 수 있습니다.

UInt8Array 는 순서대로 r,g,b,a(red,green,blue,alpha) 의 1byte 씩 4byte를 기준으로 픽셀 단위 정보를 담고 있기 때문에 어느 특정 위치의 pixel 색상을 얻어 올 수 도 있는 것입니다.
마우스로 특정 canvas 위치의 색상을 얻고자 할 경우 canvas 에서의 위치만 정확히 전달 할 수 있다면, 해당 위치의 pixel 정보를 반환할 수 있습니다.
아래의 함수는 지정된 위치를 벗어 났을 때에 대한 보정이 들어가 있지만, 특정 위치의 색상을 반환하는 함수 입니다.


     export const getPixels = (x,y,paddingType,w,h,pixels) => {
         if ( !pixels )
             return undefined;

         if ( !w ) 
             w = pixels.width;
         if ( !h )
             h = pixels.height;
         
         const result = new Uint8Array(4);
         if ( paddingType == 1 ) {   // extend edge values
             if ( x < 0 )
                 x = 0;
             if ( x >= w )
                 x = w-1;
             if ( y < 0 )
                 y = 0;
             if ( y >= h ) 
                 y = h-1;
         } else if ( paddingType == 2 ) {    //  zero padding
             if ( x < 0 || x >= w || y < 0 || y >= h ) {
                 result[0] = result[1] = result[2] = 0;
                 result[3] = 255;
                 return result;
             }
         } else {
             if ( x < 0 )
                 x = 0;
             if ( x >= w )
                 x = w-1;
             if ( y < 0 )
                 y = 0;
             if ( y >= h ) 
                 y = h-1;
         }
         const idx = y*w*4+x*4;
         
         for ( let t = 0; t < 4; t++ ) {
             result[t] = pixels.data[idx+t];
         }
         return result;
     };

향후 Image Filter 를 적용할 때 사용하는 kernel 에서 활용하게 될 함수 입니다. 필터 조작없이 간단하게 이미지 변형이 되는 것을 정리해 보겠습니다.

반전 이미지

각 Pixel 값을 255 에서 빼면 원래의 색에서 반대 색으로 반전이 이뤄지게 됩니다. 아래는 그 결과와 해당 함수 입니다.

결과


     export const inverseImageData = ( originalData, transData) => {
         if ( !originalData || !transData  ) {
             alert( "Data 확인이 필요합니다. ");
             return transData;
         }
         const len = originalData.data.length;
         if ( len != transData.data.length ) {
             alert ( "Data 길이가 일치하지 않습니다. ");
             return transData;
         }    
         for ( let i = 0; i < len; i += 4 ) {
             let j = 0
             for ( ; j < 3; j++ ) {
                 transData.data[i+j] = 255-originalData.data[i+j];
             }
             transData.data[i+j] = originalData.data[i+j]; // alpha(투명도) 값은 변화 없음
         }    
         return transData;
     };

흑백 이미지 변환

https://en.wikipedia.org/wiki/Grayscale 에서 흑백이미지로 만드는 방법을 설명하고 있습니다.
간단히 3개의 r,g,b 값을 평균내어 구성하는 방법과, 녹색 영역이 흑백을 결정하는데 중요하니 각 색상별로 가중치를 두어 흑백으로 전환한다는 설명을 하고 있습니다.
일반적으로 평균을 내는 방법 보다는 녹색 중심영역에 가중치를 부여하는 방식이 더 효율적이라는 이야기가 있으니 그 방식에 따라 변환해 보겠습니다.

변환공식은 r,g,b 의 가중치를 다음의 방식으로 설명하고 있습니다.

  1. 0.299 * r + 0.587 * g + 0.114 * b
  2. 0.2126 * r + 0.7152 * g + 0.0722 * b
  3. 0.2627 * r + 0.6780 * g + 0.0593 * b

보정 수치의 미세한 차이는 무시하고, 2번째 보정 계수를 활용하여 변환해 보았습니다.

GrayScale


     export const translateGrayScaleData = (originalData, transData) => {
         /*
         1. 0.299 * r +  0.587 * g +  0.114 * b 
         2. 0.2126 * r +  0.7152 * g +  0.0722 * b 
         3. 0.2627 * r +  0.6780 * g +  0.0593 * b 
         */
         if ( !originalData || !transData  ) {
             alert( "Data 확인이 필요합니다. ");
             return transData;
         }
         
         const len = originalData.data.length;
         if ( len != transData.data.length ) {
             alert ( "Data 길이가 일치하지 않습니다. ");
             return transData;
         }
         let coffArray = new Float32Array([0.2126, 0.7152, 0.0722]);    
         for ( let i = 0; i < len; i += 4 ) {
             let v = 0;
             let j = 0
             for ( ; j < 3; j++ ) {
                 v += (originalData.data[i+j]*coffArray[j]);
             }
             v = Math.round(v);
             v = v > 255 ? 255 : (v < 0 ? 0 : v);
             for ( j = 0 ; j < 3; j++ ) {
                 transData.data[i+j] = v;
             }
             transData.data[i+j] = originalData.data[i+j]; // alpha(투명도) 값은 변화 없음
         }    
         return transData;
     };

글이 길어져서 다음에 Filter 를 기반으로 이미지를 변환하는 방법을 정리해 보도록 하겠습니다. ^^

 Share!