배경
일을 하다보면 참 예상치 못한 일을 해야하는 경우가 종종 발생한다.
이런 일들은 때론 내게 의도치않은 양질의 경험을 제공해주기 때문에 나는 개인적으로는 즐기는 편이다.
이번 케이스는 대략 12만장의 프레임이미지를 한번에 웹 페이지에 로드를 해야하는 경우다.
상세히는 서술하진 못하지만 우선 요구사항을 들어보니, 딱히 굳이 이미지를 꼭 로드해야하는 필요성은 없는 것으로 확인되었다.
일전에 HD급의 2400장~3000장의 이미지를 부분적으로 로드한 적이 있는데 사실 로드가 그리 오래걸리진 않았다한들, 아무리 생각해도 12만장은 무리로 판단했다.
이슈가 될 변수도 의외로 많았다. 네트워크 지연, 예상치 못한 브라우저에서의 보안 정책등..
회의를 통해 단순히 제공받은 프레임이미지를 품질을 낮추지 않고, 동일 해상도, 지정된 프레임으로만 웹에 출력해준다면 영상도 상관이 없다는 걸 확인되었고 바로 영상 제작에 들어갔다.
개발
프레임 이미지의 각 크기는 대략 20-30kb에 해상도도 낮지만 전체 이미지 수의 용량을 보면 대략 3기가가 넘는 꽤나 고용량의 영상이 출력될것이 예상됐다.
제공된 메터데이터 정보를 확인해보면 일정하게 1초에 3번의 프레임이미지가 표시되어있고, 이를 통해 3FPS 의 영상임을 확인할 수 있었다.
계산해보면 대략 30시간이 넘는 영상인 셈.
프레임이미지는 %d.jpg 로 되어있고, 오름차순의 정렬구조를 가졌지만 사이사이 빈 번호가 많이 보였다.
중간중간 프레임 드랍이 있었던 것으로 추측된다.
단순하게 프레임 이미지들을 모아 영상 제작에 들어갔다.
Open CV 를 사용한 코드
프로그래밍 언어는 파이썬, 그리고 open-cv를 이용하여 제작한 코드이다.
import os
import glob
import re
import cv2
def extract_number(fileName):
fileName = os.path.basename(fileName)
# 파일 이름에서 숫자를 추출
match = re.search(r'\d+', fileName)
if match:
return int(match.group())
return -1 # 그럴리 없지만 숫자가 없는 경우를 대비하여 -1을 반환
def create_video_from_images(input_folder, output_file, fps):
# 입력 폴더에서 이미지 파일을 가져옴
image_files = glob.glob(os.path.join(input_folder, '**/*.jpg'), recursive=True)
if len(image_files) > 0 :
# 파일 이름을 숫자를 기준으로 정렬
sorted_image_files = sorted(image_files, key=extract_number)
frame_array = []
size = None
for imageFile in sorted_image_files :
img = cv2.imread(imageFile)
height, width, layers = img.shape
if size is None :
size = (width, height)
frame_array.append(img)
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # for mp4
out = cv2.VideoWriter(output_file, fourcc, fps, size)
for i in range(len(frame_array)):
# 가만히 있으면 답답하니까 100개마다.. 어디 구간 프레임인지 표시
if i%100 == 0 :
print(i)
out.write(frame_array[i])
out.release()
else :
print('파일이 없음')
create_video_from_images("{프레임이미지가 담긴 폴더 경로}", "{최종결과파일경로}", "{FPS}")
# glob 에서 recursive 의 경우, 하위 폴더들의 파일까지 전부 가져와주는 옵션이다. 이떈 파일명에 **/* 넣어야 한다.
glob.glob(os.path.join(input_folder, '**/*.jpg'), recursive=True)
위 코드는 영상 제작에는 큰 시간이 들진 않았지만, 용량이 매우 컸다..
아무래도 각종 옵션을 타협해서 용량을 줄여야 했는데 영상에 대한 세부조정을 하는데는 아무래도 ffmpeg 만 못하다는걸 느꼈다.
추가로 인코딩을 할 경우 그에 따른 시간이 소요된다.
이런 불편한 부분을 인지하고 두번째 코드를 작성했다.
FFmpeg 를 사용한 코드
import os
import glob
import shutil
import subprocess
import re
import tempfile
import cv2
def create_video_from_images_ffmpeg(input_folder, output_file, fps):
# 입력 폴더에서 이미지 파일을 가져옴
image_files = glob.glob(os.path.join(input_folder, '**/*.jpg'), recursive=True) # recursive=True 하위폴더까지 다 긁으라고 시킴. 대신 *.jpg -> **/*.jpg 로 바꿔야함
if len(image_files) > 0 :
# 파일 이름을 숫자를 기준으로 정렬
sorted_image_files = sorted(image_files, key=extract_number)
# 임시 파일을 생성 , 이거 안하면 파라메터 너무 많아서 안된다는 에러가 뜨기에 추가
temp_file_list = tempfile.NamedTemporaryFile(delete=False)
for image_file in sorted_image_files:
temp_file_list.write(("file '%s'\n" % image_file).encode('utf-8'))
temp_file_list.close()
# FFmpeg 명령어를 생성
ffmpeg_cmd = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', temp_file_list.name, '-r', str(fps), output_file]
# FFmpeg 명령어를 실행하여 MP4 비디오를 만듦
subprocess.run(ffmpeg_cmd)
# 임시 파일을 삭제합니다.
os.unlink(temp_file_list.name)
else :
print('파일이 없음')
위 코드로 진행하니 매우 빠르게 영상이 구현됐다.
워낙 프레임이미지가 많다보니 12만장을 cmd 명령어로 넣으려니 파라메터 길이가 너무 길다는 에러를 출력하여, temp_file을 만들어 프레임이미지에 대한 정보를 기입하게 한 후 그걸 ffmpeg 가 읽는 식으로 수정하여 진행한 코드이다.
아쉬운 점은, 프레임 번호가 순차적이지 않은 이슈 때문인지 실제 영상에 꽤나 많은 프레임 SKIP 현상이 일어나 32시간이 아닌 1시간대의 영상으로 제작된다는걸 확인했다.
이런 이슈를 해결한 최종 코드는 아래와 같다.
#파일명 변경 및 위치 이동처리를 해주는 함수
def rename_and_save_images(image_files, output_folder):
if not os.path.exists(output_folder):
os.makedirs(output_folder)
for i, image_file in enumerate(image_files):
# 새로운 파일명 생성
new_filename = os.path.join(output_folder, f"{i}.jpg")
# 이미 존재하는 파일일 경우 이동하지 않고 넘어감
if os.path.exists(new_filename):
continue
# 이미지 파일 이동
shutil.move(image_file, new_filename)
def create_video_from_images2(input_folder, output_file, fps):
# 입력 폴더에서 이미지 파일을 가져옴
image_files = glob.glob(os.path.join(input_folder, '**/*.jpg'), recursive=True)
print(len(image_files) )
if len(image_files) > 0 :
# 파일 이름을 숫자를 기준으로 정렬
sorted_image_files = sorted(image_files, key=extract_number)
if not os.path.exists(input_folder + os.sep + "for_video"):
rename_and_save_images(sorted_image_files, input_folder + os.sep + "for_video")
# 임시 파일을 생성
with tempfile.NamedTemporaryFile(delete=True, mode='w', encoding='utf-8') as temp_file_list:
for image_file in sorted_image_files:
temp_file_list.write("file '%s'\n" % image_file)
# FFmpeg 명령어를 생성합니다.
#ffmpeg_cmd = ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', temp_file_list.name, '-r', str(fps), '-c:v', 'libx264', output_file]
ffmpeg_cmd = ['ffmpeg', '-y', '-r', str(fps), '-i', input_folder + os.sep + "for_video" + os.sep + '%d.jpg', '-c:v', 'libx265', '-crf', '28','-preset', 'ultrafast', '-an', output_file] #10분컷 설정
# FFmpeg 명령어를 실행하여 MP4 비디오를 만들자
subprocess.run(ffmpeg_cmd)
else :
print('파일이 없음')
코드에 나온 것과 같이,이미지들을 오름차순의 번호로 변경하고 for_video 라는 폴더가 없다면 생성 후 해당 폴더로 모아주는 역할을 하는 rename_and_save_images 라는 함수를 구현했다.
추가적으로 기존 ffmpeg 커멘드도 바뀐걸 볼 수 있다.
ffmpeg_cmd = ['ffmpeg', '-y', '-r', str(fps), '-i', input_folder + os.sep + "for_video" + os.sep + '%d.jpg', '-c:v', 'libx265', '-crf', '28','-preset', 'ultrafast', '-an', output_file]
-y : 이미 파일이 있는 경우 중복으로 저장되게 하기 위한 옵션
-r : 프레임 설정
-i : 경로/%d.jpg 를 줌으로써 포멧에 따른 이미지를 순차적으로 알아서 읽으라는 뜻.
-c:v : H.265 코덱을 사용하기 위한 옵션. 264보다 압축률이 우수하다는걸 확인 후 설정
-crf : 21~28 이 무난한 수준으로 확인되어 28로 설정, 품질 설정이다.
-preset : 느리게 할 수록 압축률이 좋은것으로 확인되지만 직접 돌려본 결과 fast 조차 매우 오랜시간이 걸리고 최종 산출물은 ultrafast 보다 용량이 줄순 있어도 다이나믹한 차이를 보여주지 않아 가장 빠른 설정으로 처리
위 옵션들은 다양하게 바꿔가며 지금 진행하는 일에서 가장 최적화된 옵션이다.
결과적으로 얼추 700mb ~ 1GB 정도 나오는 영상의 크기를 377mb 로 줄였고 뿐만아니라 영상화 하는 시간 또한 m1, 12만개 프레임이미지 해상도 320x320 기준으로는 총 10분정도로 크게 줄였다.
크로미움에서 지원하는 H.265 코덱으로 인코딩 된 덕에 스프링을 통해 OutputStream으로 내보낼때 크롬에서도 영상의 이동, 재생 등 이슈가 없이 잘 나오는걸 확인할 수 있었다.
'학습' 카테고리의 다른 글
이미지 기하학적 변환 (1) | 2024.02.16 |
---|---|
키즈노트 일괄 다운로드 방법 ( 사진, 알림노트 ) (1) | 2024.01.28 |
좌표 배열 최외곽 선 따기 ( feat. convex hull 알고리즘 ) (0) | 2023.09.05 |