ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Sunlapse] #2 촬영하고 영상 제작하기
    프로그래밍/프로젝트 2020. 12. 26. 21:03

    https://youtu.be/fDGwROPqNtA

    샘플 영상

    제대로 촬영하는 결과를 얻는데 꽤나 오래 걸렸다. 일단 하루를 보내야 테스트 결과가 확인 가능한 이유도 있었고 내가 처음에 생각했던 부분이 원활히 되지 않았던 점도 있었다. 그래도 여러 시도 끝에 꽤나 만족할 만한 결과를 얻을 수 있었는데 위의 영상이 촬영 후 영상 제작에 성공한 결과다. 깃허브의 커밋 기록을 보면서 어떤 시도를 했는지 확인해본다.

     

    1. FFmpeg의 setpts 옵션 사용

     

    처음에 이 프로젝트를 떠올린 계기는 바로 이 옵션이었다. 간단하게 이야기하면 영상의 속도를 조절할 수 있는 옵션이다. 그래서 처음 생각은 영상을 해가 뜬 시간 동안 촬영하고 이 영상을 이후에 이 옵션을 이용해 후처리하려고 했다. 그러나 굉장히 당연한 문제가 있었는데 거의 12시간을 영상으로 촬영하다보니 영상 용량이 어마어마해졌다. 그리고 당연히 이 큰 영상을 타임랩스로 만드는 작업 역시 굉장히 큰 작업이었고 라즈베리 파이의 성능으로는 이게 가능한지 테스트조차 벅찬 수준이었기 때문에 새로운 방법을 찾아야 했다.

     

    2. raspistill의 timeout, timelapse 옵션 사용

     

    그렇게 탐색을 하다 이 글을 찾을 수 있었다. '라즈베리 파이로 타임랩스를 촬영하는 방법'이라는 글이었는데 여기서는 영상이 아니라 사진을 찍고 사진을 나중에 이어붙이는 방식으로 타임랩스를 만들었다. 이 방식을 보고 처음에 생각한 방법을 보니 결국 99%는 축소할 영상을 전부 찍는 짓이었다. 그래서 작성한 명령어는 다음과 같았다.

    raspistill 
        --width 1920
        --height 1080 
        --timeout ${영상 시간(ms)}
        --timelapse ${사진 간격} 
        --output image%09d.jpg

    해상도를 지정하고 몇 밀리초만큼 찍을지 timeout 옵션으로 지정하고 timelapse 옵션에 몇 초마다 찍을지 설정한다. 처음에는 하루를 5~10분은 만들어야하지 않을까해서 timelapse 옵션을 굉장히 짧게 주었다. 그렇게 하니 하루에 거의 4만 장이 넘게 찍혀서 용량 문제가 다시 나타났고 역시 인코딩 작업이 너무 무거워졌다. 그리고 억지로라도 영상을 만들어서 시청해보니 굉장히 지루했다. 그래서 1일 = 1분으로 정하고 timelapse 옵션을 어떻게 할지 식을 세워봤다.

     

    (분당 프레임) = (1분 영상에 필요한 총 프레임) / (촬영 시간) X 60(분)

    (프레임 당 간격) = 60(초) / (분당 프레임)

     

    나는 60fps의 영상을 만들 계획이었기 때문에 1분 영상에 필요한 총 프레임은 60 * 60 = 3600이었다. 그런데 이렇게 하니 계산식에 있는 두 개의 60으로 3600이 1로 나누어져버려서 결과적으로 촬영 시간 값을 그대로 초로 단위만 변경한 값이 되었다. 즉, 12시간 영상은 12초마다 사진을 촬영하는 식으로 만들면 됐다. 이렇게 하면 매일 항상 3600장의 사진이 규칙적으로 생성이 되고 라즈베리 파이에서도 소화할 수 있는 규모로 줄어들었다. 결국 최종적으로 사용하는 코드는 다음이 되었다.

    import cp from "child_process";
    import fs from 'fs';
    
    import schedule from "node-schedule";
    import moment from "moment";
    
    export default (sunTime) => {
      const { sunriseTime, duration } = sunTime;
      const height = 1080; // 1080p
      const width = height * 16 / 9;
    
      //raspistill --width 1920 --height 1080 --timeout 10000 --timelapse 1000 --output image%09d.jpg
      schedule.scheduleJob(sunriseTime, () => {
        console.log('Capture start');
        const today = moment().format('YYYY-MM-DD');
        fs.mkdirSync(today);
        console.log(`raspistill --width ${width} --height ${height} --timeout ${duration.asMilliseconds()} --timelapse ${duration.asHours().toFixed(3) * 1000} --awb off --awbgains 1.1,1.5, --output ${today}/image%06d.jpg`);
        cp.spawnSync('raspistill', [
          '--width', width,
          '--height', height,
          '--timeout', duration.asMilliseconds(),
          '--timelapse', duration.asHours().toFixed(3) * 1000 * 3, // 20초
          '--awb', 'off',
          '--awbgains', '1.0,1.4',
          '--output', `${today}/image%04d.jpg`
        ]);
        console.log('Capture end');
    
        console.log('Timelapse on');
        cp.spawnSync('ffmpeg', [
          '-framerate', 60,
          '-i', `${today}/image%04d.jpg`,
          '-c:v', 'h264_omx',
          '-b:v', '1.3M',
          '-pix_fmt', 'yuv420p',
          `/var/opt/sunlapse/timelapse-${today}.mp4`
        ]);
        console.log('Timelapse end');
    
        fs.rmdirSync(today, { recursive: true });
      });
    }
    

    패러미터로 받는 sunTime은 이전 글의 모듈에서 받아오는 객체이다. 객체에는 촬영을 시작해야 하는 시간과 그 날 해가 떠있는 시간의 길이가 담겨있다. 테스트 중 FFmpeg이 중단되는 경우가 몇 번 있어 720p로 화질을 낮췄다. 우선 촬영 날짜를 이름으로 디렉토리를 만들고 그 폴더에 촬영한 사진을 담는다. awb 옵션이 추가로 생겼는데 auto white balance의 줄임말으로 해당 기능을 사용하니 사진의 색감이 이상하게 나와 여러 옵션을 사용해보다가 직접 gain 값을 awbgains 옵션으로 지정해서 촬영했다. 위의 샘플 영상에서 확인 가능하듯이 해를 정면으로 볼 때 색감이 이상해지기는 하지만 전체적으로 가장 괜찮은 색감을 보여줬기 때문에 그대로 고정했다.

     

    FFmpeg은 이미지가 누락된 숫자 없이 파일명이 쭉 이어지면 저렇게 패턴만 입력해줘도 모든 이미지를 순서대로 입력한다. 라즈베리 파이의 부담을 줄일 수 있는 하드웨어 인코딩을 사용하기 위해 라즈베리 파이의 h264_omx 인코더로 테스트해봤지만 이미지를 영상으로 변환하는데 문제가 있는 듯해서 libx264 인코더를 사용했고 쭉 이어붙인 영상을 mp4로 저장했다. 그 후 사용한 이미지는 모두 삭제한다.

     

    child_process를 이용해 위의 명령어들을 실행시켰다. 이 프로그램이 다중 처리를 하지는 않기 때문에 코드의 가독성을 위해 spawn의 콜백이 아닌 spawnSync를 이용해 실행했다. 자바스크립트는 이전부터 많이 썼어도 노드는 최근에 많이 쓰기 시작했는데 async, await에 익숙해지기 시작하니까 콜백이 기반이 되는 노드가 확실히 불편한 부분이 있었다.

     

    이 모든 작업을 node-schedule 라이브러리를 이용해 예약하는 것으로 마무리를 했다. node-schedule은 기본적으로는 cron 문법을 사용해 작업을 예약하지만 객체를 사용해서 예약할 수도 있는데 반복이 아닌 한 번 작업이었기 때문에 객체로 명확하게 지정하는 것이 편리해 객체를 사용해 예약했다. 객체의 형태는 이전 글에서 확인할 수 있다. 작업 예약 방법은 node-schedule의 깃허브 페이지에서 확인 가능하다.

    import schedule from 'node-schedule';
    
    import getSunTime from './sunTime.js';
    import videoScheduler from "./videoRecorder.js";
    
    console.log('Start app');
    schedule.scheduleJob('0 0 4 * * *', async () => {
        const sunTime = await getSunTime();
        videoScheduler(sunTime);
    });

    마지막으로 매일 오전 4시 0분 0초에 해가 뜨는 시간을 구하고 영상을 촬영하는 작업을 실행하게 cron 문법을 사용해 예약했다. 그렇게 2개의 영상이 정상적으로 제작되는 것을 확인해볼 수 있었다.

     

    사실 처음에 떠오른 아이디어는 딱 여기까지였다. 하지만 프로그램을 만들다보니 필요성을 느낀 작업이 있었는데 이렇게 만들어진 영상을 편리하게 저장하거나 시청할 수 있는 방법이 필요해진 것이었다. 결국 어디서 실행이 되든 별도의 서버를 만들어야했다.

     

    수정1: 실제로 1분 영상을 만드니 굉장히 지루한 영상이 되었다. 이후에 테스트를 거쳐 20초로 조정했고 사진 수는 3분의 1로 줄어들었다.

    수정2: h264_omx가 사용 가능해지자 FFmpeg도 정상적으로 실행할 수 있었다. 다시 1080p로 해상도를 높였다.

    수정3: h264_omx를 테스트하면서 사용하다보니 다시 사용이 가능해졌다. 어떤 이유에서 그랬는지는 모르겠지만 일단 가능해졌고 -b:v 옵션으로 비트레이트를 높게 잡아 화질을 높였다.

Designed by Tistory.