Day 19 - Webcam Fun

이번 과제는 웹캠을 브라우저에서 사용한다. 프로그래머스에서 온라인 코딩테스트를 볼 때 크롬에서 실시간 웹캠 사용이 되는 것이 신기했는데, 아마 이런 방식을 사용한게 아닐까 싶다.

웹캠의 사용이 클라이언트만 있어서 되는게 아니고, 서버 관련 모듈을 설치하고 환경을 세팅해야 한다. 하지만 npm 환경을 사용해서 내장된 package.json만으로 npm install을 통해 모든 환경을 한번에 세팅할 수 있었다.

비디오 가져오기

1
2
3
4
5
6
7
8
9
10
11
function getVideo() {
navigator.mediaDevices
.getUserMedia({ video: true, audio: false })
.then((localMediaStream) => {
video.srcObject = localMediaStream;
video.play();
})
.catch((err) => {
console.error(`Oh NO!!`, err);
});
}

캠의 비디오를 가져오는 방법은 navigator.mediaDevices.getUserMedia() 메소드를 사용하면 되므로 간편하다. 그 후 적절한 에러핸들링과 함께 미리 선언해놓은 video 돔 객체의 src 경로에 스트리밍되는 캠 데이터를 넣고 play() 시켜주면 된다.

원래는 video.src = window.URL.createObjectURL(localMediaStream)을 사용했었는데 현재의 브라우저에서는 video.srcObject=localMediaStream으로 사용한다고 한다. 관련 Deprecated 정보는 Mozilla에서 확인할 수 있다.

캔버스에 비디오 넣기

캔버스에 비디오를 넣는 이유는 R, G, B 픽셀 값을 조정하는 바(bar)로 비디오에 장난(?)을 치기 위해서인데 굳이 비디오에 효과를 안 줘도 되면 그냥 대충 보고 넘어갔다가 필요할 때 쓰면 될 것 같다. (내가 그렇다)

1
2
3
4
5
6
7
8
9
10
function paintToCanvas() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;

return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
}, 16);
}

위 코드가 데이터의 픽셀에 대한 접근없이, 순수하게 비디오를 캔버스에 그려넣는 과정이다. 캠마다 너비와 높이가 다르므로 전체화면에서 적절하게 조정하기 위해, 비디오의 너비와 높이를 캔버스에 넣어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function paintToCanvas() {
const width = video.videoWidth;
const height = video.videoHeight;
canvas.width = width;
canvas.height = height;

return setInterval(() => {
ctx.drawImage(video, 0, 0, width, height);
// take the pixels out
let pixels = ctx.getImageData(0, 0, width, height);
// mess with them
pixels = redEffect(pixels);

pixels = rgbSplit(pixels);
ctx.globalAlpha = 0.8;

pixels = greenScreen(pixels);
// put them back
ctx.putImageData(pixels, 0, 0);
}, 16);
}

위 코드가 캔버스의 픽셀을 rgb를 조정하는 함수를 작성하여 다시 그려내는 과정이다. 픽셀을 조정하는 함수는 밑에서 설명하겠다. 이제 이 함수를 video 객체에 이벤트로 걸어주면 된다.

1
video.addEventListener("canplay", paintToCanvas);

canplay 라는 이벤트 속성이 있는지 몰랐다, 꽤 유용하게 사용할 것 같다.

비디오 캡쳐하기

버튼을 누르면 비디오가 캡쳐되고, 미리보기 스냅샷이 화면 아래에 삽입된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function takePhoto() {
// 찰칵 소리 내기
snap.currentTime = 0;
snap.play();

// 캔버스의 데이터를 내보내기
const data = canvas.toDataURL("image/jpeg");
const link = document.createElement("a");
link.href = data;
link.setAttribute("download", "handsome");
link.innerHTML = `<img src="${data}" alt="Handsome Man" />`;
strip.insertBefore(link, strip.firstChild);
}

버튼을 누른 후 화면 밑을 보면

이렇게 스냅샷이 보여지고 클릭하면 다운로드된다. createElement를 써도 되고 innerHTML+=로 늘여나가도 상관없을 것 같다.

픽셀로 장난(?)치기

밑에서부터 나오는 코드들은 솔직히 제대로 보지는 않았다. 내가 이미지 처리에 관심도 없을 뿐더러 비디오 처리시에 필터 효과가 필요하다면 그때 검색해서 쓰면 되기 때문에 굳이 하나하나 파헤치지는 않았다.

그 전에, 한가지 괜찮다고 생각한 코드는

1
2
3
document.querySelectorAll(".rgb input").forEach((input) => {
levels[input.name] = input.value;
});

이 코드인데, 난 range bar에 이벤트가 걸려있지 않은데 어떻게 값을 실시간으로 바꿀까 생각하다가 코드를 보니 이렇게 비디오가 플레이되고 있을 때 실행되는 함수 내에서 이렇게 값을 받아오고 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function redEffect(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i + 0] = pixels.data[i + 0] + 200; // RED
pixels.data[i + 1] = pixels.data[i + 1] - 50; // GREEN
pixels.data[i + 2] = pixels.data[i + 2] * 0.5; // Blue
}
return pixels;
}

function rgbSplit(pixels) {
for (let i = 0; i < pixels.data.length; i += 4) {
pixels.data[i - 150] = pixels.data[i + 0]; // RED
pixels.data[i + 500] = pixels.data[i + 1]; // GREEN
pixels.data[i - 550] = pixels.data[i + 2]; // Blue
}
return pixels;
}

function greenScreen(pixels) {
const levels = {};

document.querySelectorAll(".rgb input").forEach((input) => {
levels[input.name] = input.value;
});

for (i = 0; i < pixels.data.length; i = i + 4) {
red = pixels.data[i + 0];
green = pixels.data[i + 1];
blue = pixels.data[i + 2];
alpha = pixels.data[i + 3];

if (
red >= levels.rmin &&
green >= levels.gmin &&
blue >= levels.bmin &&
red <= levels.rmax &&
green <= levels.gmax &&
blue <= levels.bmax
) {
// take it out!
pixels.data[i + 3] = 0;
}
}

return pixels;
}