JavaScript

Javascript Image Filter 만들기

La.place 2017. 4. 15. 20:19



 최근 어플리케이션 보면 셀카/셀피를 찍어 이미지 필터를 적용할 수 있는 앱을 쉽게 찾아볼 수 있다. Facebook, Instagram, B612 등 사진을 올릴 때 필터를 쉽게 적용해서 올릴 수 있다. 사실 이런 필터 기능들은 예전부터 포토샵을 이용하면 쉽게 적용할 수 있었다. 사실 필터란, 레스터 그래픽으로 이루어진 사진들의 픽셀(화소)값들을 특정 규칙에 따라 색을 바꿔주는 것이다.


 구글에 javascript Filter를 검색해보면 몇몇 Filter Library를 찾을 수 있다. Filter Library를 이용하면 쉽게 필터를 적용할 수 있지만 내 입맛에 맞게 바꾸는 것은 쉽지가 않다. 이번 포스트를 통해 이러한 이미지 필터를 직접 만들어보자. 플랫폼은 물론 웹 기반이 되겠다. 로컬에 저장된 이미지를 불러와 웹 브라우저에서 javascript를 이용해 만든 이미지 필터를 적용할 것이다. 


프로젝트 세팅

 사실 프로젝트에 필요한 세팅이란게 따로 없다. 그냥 DOM 컨트롤을 쉽게 해주는 jQuery정도만 쓰도록 하자. 앞에서 말했듯이 필터를 먹이기 위해선 픽셀값에 접근해야 한다. 웹 브라우저에서 유일하게 이미지 Pixel에 접근할 수 있는 요소가 존재한다. 바로 Canvas 요소이다. 이 Canvas를 사용해서 간단한(?) Image Processing 이론을 적용하면 누구나 쉽게 이미지 필터를 만들 수 있다. index.html 파일과 filter.js 파일을 만들어 다음과 같이 설정한다.


index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Image Filter</title>
</head>
<body>

</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="filter.js"></script>
</html>

filter.js

console.log('filter init');


웹브라우저를 열어보자. filter init log가 정상적으로 찍힌다면 준비가 필터를 만든 준비가 끝난 것이다.


Canvas 만들기

 앞에서 한번 언급했듯이 우리는 Canvas를 이용해서 이미지를 불러오고, 필터를 적용할 것이다. index.html 파일에 Canvas Element를 추가해주자. 크기는 적당히 500x500으로 만들도록 하겠다. 또 로컬에서 이미지를 불러오기 위해 Input Element도 하나 만들어 주도록 하자.


index.html - body에 추가

<body>
<canvas id="canvas" width="500" height="700"></canvas>
<input id="loadButton" type="file" accept="image/*">
</body>


index.js를 수정한 후 열어보면 캔버스가 투명해서 잘 안보인다. border를 넣어서 경계를 보이게 해주자.


index.js - style 추가

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Image Filter</title>
<style>
canvas {
border : black 1px solid;
border-radius: 5px;
}
</style>
</head>
<body>
<canvas id="canvas" width="600" height="600"></canvas>
<input id="loadButton" type="file" accept="image/*">
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="filter.js"></script>
</html>


완성된 Layout




브라우저를 살짝 줄이면 대충 이런식으로 보인다. 이제 파일 선택 버튼을 눌러서 캔버스에 이미지를 불러와 보도록 하자.


로컬 이미지 불러오기

이미지를 불러와서 저장할 캔버스 객체를 먼저 만들어 보자. 

filter.js

// canvas 객체 생성
var canvas = $('#canvas')[0];
var ctx = canvas.getContext('2d');

캔버스에서 이미지를 그리기 위해선 drawImage() method를 사용한다.

context.drawImage(img, x, y, width, height);

 만약 이미지를 그대로 넣으면 캔버스 보다 큰 이미지가 들어올 경우 짤리게 된다. 이미지 width나 height가 클 경우 캔버스에 맞춰서 그리도록 사전에 처리를 해주는 함수를 만들자.


function drawImageData(image) {
image.height *= canvas.offsetWidth / image.width;
image.width = canvas.offsetWidth;

if(image.height > canvas.offsetHeight){
image.width *= canvas.offsetHeight / image.height;
image.height = canvas.offsetHeight;
}

ctx.drawImage(image, 0, 0, image.width, image.height);
}


 자 이제 input 버튼에 이벤트를 걸어, 로컬파일을 불러왔을때 캔버스에 그리도록 하자. FileReader 객체를 이용해서 불러온 파일을 dataURL 포멧으로 바꿀 수 있다. 데이터 URL객체를 사용하는 이유는 drawImage에 전달할 이미지 객체를 만들고, width와 height를 가져오기 위해서이다. 

$('#loadButton').on('change', function (e) {
var file = e.target.files[0];
var fileReader = new FileReader();

fileReader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function () {
drawImageData(image);
}
};

fileReader.readAsDataURL(file);
});


filter.js - 이미지 불러오기

// canvas 객체 생성
var canvas = $('#canvas')[0];
var ctx = canvas.getContext('2d');

function drawImageData(image) {
image.height *= canvas.offsetWidth / image.width;
image.width = canvas.offsetWidth;

if(image.height > canvas.offsetHeight){
image.width *= canvas.offsetHeight / image.height;
image.height = canvas.offsetHeight;
}

ctx.drawImage(image, 0, 0, image.width, image.height);
}

// click input button
$('#loadButton').on('change', function (e) {
var file = e.target.files[0];
var fileReader = new FileReader();

fileReader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function () {
drawImageData(image);
}
};

fileReader.readAsDataURL(file);
});


Filter 적용하기

 이제 본격적으로 Filter를 만들고 적용해보자. Canvas Element에서 Pixel(화소)값을 가져오기 위해선 getImageData() method를 사용한다.

context.getImageData(x, y, width, height);

실제 파일이 어떻게 불러와지는지 확인해보자. ctx.drawImage 함수 밑에 console.log로 getImageData로 찍어보자.

function drawImageData(image) {
image.height *= canvas.offsetWidth / image.width;
image.width = canvas.offsetWidth;

if(image.height > canvas.offsetHeight){
image.width *= canvas.offsetHeight / image.height;
image.height = canvas.offsetHeight;
}

ctx.drawImage(image, 0, 0, image.width, image.height);
console.log(ctx.getImageData(0,0, canvas.width, canvas.height));
}


[그림] 로컬 파일을 불러온 후 console로 찍어본 결과


 console로 출력된 결과물을 보면 data key안에 Uint8ClampedArray로 이미지 정보가 들어있는 것을 볼 수 있다. 쉽게 말하면 배열로 이미지 정보가 들어있는 것이다. 여기서 한 Pixel의 정보는 4개의 단위로 쪼개진다. 예를들면 Uint8ClampedArray[0] ~ Uint8ClampedArray[3] 까지가 좌상단의 첫번째 픽셀에 대한 정보이다.

순서대로 RGBA값, 즉 빛의 3원색인 적,녹,청의 대한 값과 alpha(밝기)에 대한 정보라 할 수 있다. 각각의 값들은 0부터 255까지의 int값을 가질 수 있다.


 이제 픽셀값을 가져왔으니 실제로 값을 변경시켜서 필터 효과를 적용해보자. input element 아래다가 필터 적용 버튼을 만들자.


index.html

<body>
<canvas id="canvas" width="600" height="600"></canvas>
<input id="loadButton" type="file" accept="image/*">
<button id="filterButton">Filter</button>
</body>

 이제 가장 간단한 invert 필터부터 만들어 보도록 하자. invert란 말 그대로 화소의 색깔을 반전시켜 주는 필터이다. 쉽게 생각하면, RGB 값들을 반대로 뒤집어 주면 된다. filter.js 밑에 다음 함수를 추가시켜 주자. invertFilter 함수는 getImageData로 가져온 오브젝트를 넘겨받아 pixel 데이터에 일련의 프로세싱 과정을 거쳐 새로운 pixel 데이터를 리턴한다. 여기서는 최대값(255) - 기존값을 해서 색상이 반전되는 효과를 주도록 하자.


filter.js - invert filter

function invertFilter(pixels) {
var d = pixels.data;
for(var i=0; i<pixels.data.length; i+=4 ){
d[i] = 255 - d[i]; // R
d[i+1] = 255 - d[i+1]; // G
d[i+2] = 255 - d[i+2]; // B
d[i+3] = 255; // Alpha
}
return pixels;
}


이제 Filter 버튼을 눌렀을 때 이벤트를 걸어서, invertFilter를 적용해서 캔버스에 다시 그려보자. 캔버스에 image데이터를 그려주기 위해서는 putImageData() method를 사용한다.

context.putImageData(imageData, x, y);

$('#filterButton').on('click', function () {
// imageData를 가져온다.
var pixels = ctx.getImageData(0,0, canvas.width, canvas.height);

// image processing
var filteredData = invertFilter(pixels);

// Canvas에 다시 그린다.
ctx.putImageData(filteredData, 0 , 0);
});


filter.js - 완성된 버전

// canvas 객체 생성
var canvas = $('#canvas')[0];
var ctx = canvas.getContext('2d');

function drawImageData(image) {
image.height *= canvas.offsetWidth / image.width;
image.width = canvas.offsetWidth;

if(image.height > canvas.offsetHeight){
image.width *= canvas.offsetHeight / image.height;
image.height = canvas.offsetHeight;
}

ctx.drawImage(image, 0, 0, image.width, image.height);
console.log(ctx.getImageData(0,0, canvas.width, canvas.height));
}

// click input button
$('#loadButton').on('change', function (e) {
var file = e.target.files[0];
var fileReader = new FileReader();

fileReader.onload = function (e) {
var image = new Image();
image.src = e.target.result;
image.onload = function () {
drawImageData(image);
}
};

fileReader.readAsDataURL(file);
});

$('#filterButton').on('click', function () {
// imageData를 가져온다.
var pixels = ctx.getImageData(0,0, canvas.width, canvas.height);

// image processing
var filteredData = invertFilter(pixels);

// Canvas에 다시 그린다.
ctx.putImageData(filteredData, 0 , 0);
});

// Filters
function invertFilter(pixels) {
var d = pixels.data;
for(var i=0; i<pixels.data.length; i+=4 ){
d[i] = 255 - d[i]; // R
d[i+1] = 255 - d[i+1]; // G
d[i+2] = 255 - d[i+2]; // B
d[i+3] = 255; // Alpha
}
return pixels;
}



[그림] invertFilter를 적용한 이미지

Image Filters

 이제 가장 간단한 invert Filter를 만들어 보았으니 이를 응용해서 다른 필터도 만들어보도록 하자. 눈치가 빠른 사람은 알아차렸겠지만 pixel을 처리하는 부분만 바꿔주면 무궁무진하게 필터를 만들어 낼 수가 있다. 아마도 다른 필터들은 이런 형태를 가질 것이다.
function somethingFilter(pixels) {
var d = pixels.data;

// image processing logic

return pixels;
}


Brightness Filter

 간단하게 밝기조절부터 시작하자. 밝기조절은 RGB값에다가 균등한 상수를 더해주면 전체적으로 밝게 보이도록 할 수 있다.

function brightnessFilter(pixels, value) {
var d = pixels.data;
for(var i =0; i< d.length; i+=4){
d[i] += value/3;
d[i+1] += value/3;
d[i+2] += value/3;
}
return pixels;
}

 필터를 추가해준후 filterButton Event에서 invertFilter(pixels); 부분을 brightnessFilter(pixels, value);로 바꿔주자. value 부분이 높을수록 더욱 밝게 바꿔 줄 수 있다. 좀더 응용하면 silde bar등을 이용해서 밝기조절이 가능하게도 만들 수 있다.

$('#filterButton').on('click', function () {
// imageData를 가져온다.
var pixels = ctx.getImageData(0,0, canvas.width, canvas.height);

// image processing
var filteredData = brightnessFilter(pixels, 100);

// Canvas에 다시 그린다.
ctx.putImageData(filteredData, 0 , 0);
})
;

[그림] Brightness Filter 적용


Grayscale Filter

 grayscale은 말그대로 회색 필터를 말한다. 회색 빛으로만 이루어진 사진들을 페이스북이나 인스타그램에서 많이 보았을 것이다. 사람의 눈은 RGB값이 모두 같을 경우 회색으로 느끼게 된다. 이를 응용해서 Grayscale Filter를 만들어 보자.
function grayscaleFilter(pixels) {
var d = pixels.data;
for(var i =0; i< d.length; i+=4){
var r = d[i];
var g = d[i+1];
var b = d[i+2];

var v = 0.2126*r + 0.7152*g + 0.0722*b; // 보정값
d[i] = d[i+1] = d[i+2] = v // RBG 색을 같게 맞추자
}
return pixels;
}

필터를 추가해준후 filterButton Event에서 invertFilter(pixels); 부분을 grayscaleFilter(pixels);로 바꿔주자.


$('#filterButton').on('click', function () {
// imageData를 가져온다.
var pixels = ctx.getImageData(0,0, canvas.width, canvas.height);

// image processing
var filteredData = grayscaleFilter(pixels);

// Canvas에 다시 그린다.
ctx.putImageData(filteredData, 0 , 0);
});


[그림] Grayscale Filter 적용


Sepia Filter

 이번엔 빛바랜 효과를 내는 필터를 만들어보자. 빛바랜 효과를 위해선 R값을 상대적으로 G값의 보정값을 높혀주고, G, B값은 G값의 0.5정도 보정값을 주도록 하자.

function sepiaFilter(pixels) {
var d = pixels.data;
for(var i =0; i< d.length; i+=4){
var r = d[i];
var g = d[i+1];
var b = d[i+2];

d[i] = r*0.3588 + g*0.7044 + b*0.1368;
d[i+1] = r*0.2990 + g*0.5870 + b*0.1140;
d[i+2] = r*0.2392 + g*0.4696 + b*0.0912;
}
return pixels;
}

[그림] Sepia Filter 적용

마치며

 
 이밖에도 R(빨간색)만 남겨두고 나머지 부분은 흑백처리하면 B612의 Apple Filter같은 것도 만들 수 있다. 사실 이미지 필터를 만드는데는 기술적인 요소보다 과학과 수학적인 이해가 필요하다. 그리고 앞에서 구현한 필터들은 단순 픽셀값 변화를 이용했지만, convolution(회선)이론을 이용하면 Bulr, sharp 효과, 윤곽선 검출중 다양한 방면으로 응용이 가능하다. 해당 부분에 대해서는 다음 포스팅에서 다뤄보도록 하겠다. 시간이 나면 나만의 필터를 만들고 이름도 붙여보도록 하자.


'JavaScript' 카테고리의 다른 글

Javascript Image Filter - convolution  (0) 2017.05.02
Javascript ES6 Proxy  (1) 2017.04.16
Webpack2 시작하기  (3) 2017.04.08
ECMA 2015를 공부해보자  (1) 2016.08.07
자바스크립트 배열  (0) 2015.11.01