발단은 한 줄의 트윗이었다. jpeg로 반복 저장을 600회 하면 원래 없던 붉은 점이 가득 끼며 화질이 열화되는 것을 넘어 박살이 난다는 얘기다… 이 트윗에 대한 답글로 온갖 이론이 난무하고, 결국 MS의 jpeg 인코더가 범인이라는 아무런 근거 없는 결론(?)이 났다.
근데, 근본적으로 생각해봤다. jpeg는 이미지를 저장할 때 코사인을 기반으로 하는 DCT 변환을 하고, 고주파 성분을 제거하는 방식으로 손실압축을 한다. 즉, jpeg에서 화질의 열화는 있는 성분이 사라지는 것이지, 없던 게 태어나는 게 아니다. 좀 쉽게 표현하면 날카로움이 사라지고, 흐릿해지며, 두리뭉실해지는 것이다.
반복 저장을 하면 화질이 열화되는 건 당연한데, 결코 저런 방식일 수가 없는 것이다. 이론은 이론일 뿐이고, 과연 얼마나 손상되는지를 눈으로 확인하기 위해 간단하게 하나 만들어봤다.
이 프로그램의 기능은 다음과 같다.
- 원본 jpeg 이미지와 대상 폴더를 지정하면 해당 이미지를 폴더에 계속 읽었다 썼다를 반복함
- mozjpeg로 반복 인코딩을 하며, 최적화 여부를 선택할 수 있음. 즉 최적화를 선택하면 mozjpeg으로 인코딩하고, 그렇지 않으면 libjpeg-turbo로 인코딩함[1]
- 반복 회수를 지정할 수 있으며, 지정된 횟수를 제외하고는 생성된 이미지는 모두 삭제함. 위 캡쳐의 설정은 총 1,000번 반복하며, 이 중 20/40/60/80…번째 이미지만 남기고 모두 삭제한다는 뜻임
- 저장 품질은 일정 범위를 지정해서 랜덤하게 적용할 수도 있고, 특정 값을 적용할 수도 있음
- 남긴 이미지에 대해서는 원본과 비교하여 PSNR를 계산하고 csv 파일로 저장함
작업할 대상 이미지는 따로 구하지 않고, 발단이 되었던 이미지를 잘라내서 사용하기로 했다.
그리고, 이 이미지를 저장 품질을 바꿔가며 500번씩 저장해봤다.
상식적이고, 당연한 결과지만 add noise 필터를 잔뜩 먹인 듯한 점들은 결코 나타나지 않는다. 오히려 빨간 선의 색이 보라색으로 변하며 다소 뭉개진 느낌이 든다. 재미있는 건 품질을 더 높게 지정했을 때 빨간 선이 더 많이 망가진다는 점. 이 과정에서 PSNR의 변화는 다음과 같다.
이 그래프들을 보면 몇 가지 사실을 알 수 있다.
- 저품질(Q=40)을 지정했을 때는 아예 처음부터 작정이라도 한 듯이 PSNR이 44를 유지함
- 고품질에서는 PSNR이 떨어지다가 100번 이상 저장하면 일정한 값으로 수렴함[2]
- 앞의 사진에서도 추측할 수 있듯이, Q=100일 때가 Q=90일 때보다 PNSR이 낮음
시험 결과 요약
- 600회까지 할 것도 없고, 50회 넘어가면 열화는 일정한 수준으로 수렴
- 노이즈가 잔뜩 끼는 현상은 결코 벌어지지 않음.
이에 대한 반론 트윗이 올라왔다.
저 트윗을 쓴 분의 얘기는 그러니까…
1. 윈도우의 jpeg encoder는 DCT 변환 테이블을 잘못 만들었다고 추정함
2. C언어는 math.h의 오류로 sin/cos 함수는 오차가 꽤 나올 수 있음
대략 이렇게 정리될 수 있고, 요약하면 MS의 프로그래머들은 등신이라 저런 오류가 실제로 발생한다는 얘기다. 정말 저런 결과를 보여주는지 확인하기 위해 앞의 포스팅과 같은 기능을 하는 프로그램을 C#으로 만들었다.
이 프로그램은 이전의 프로그램과 사실상 동일한 기능을 한다. 딱 한 가지만 빼고. 믿을 수 있고, 수많은 사람들이 검증한 mozjpeg 대신 믿을 수 없고, 버그가 충만한 MS의 JpegBitmapEncoder를 사용한다는 것. 이 프로그램으로 지난번과 같은 소스를 놓고, 동일한 테스트를 해봤다.
품질을 바꿔가며 500번씩 저장해보는 것. 결과는 아래와 같다.
참고로, mozjpeg으로 테스트한 결과는 아래와 같다.
어? 뭔가 이상하다. 둘의 테스트 결과가 달라야 하는데, 거의 동일[3]하다. 심지어 Q가 올라갈 수록 빨간 선이 왜곡되는 것도 똑같다. 그럴 리가 없다. 멍청하고 아둔한 MS의 프로그래머들이 만든 jpeg 인코더는 노이즈를 마구 유발해야 되는 것인데! 혹시나 모르니까 PSNR의 변화를 확인해보자. MS의 jpeg 인코더가 엉망이기를 바라면서.
다시 한 번 mozjpeg으로 돌렸을 때의 PSNR을 꺼내보자.
이런! 이게 뭔가! 일정 횟수 이상으로 저장할 때 일정한 값으로 수렴하는 패턴부터 동일하다. Q=40일 때는 아예 두 그래프가 일치하고, Q=90/100일 때는 심지어 C# 쪽이 PSNR이 더 크기[4]까지 하다!
이 시험으로 내릴 수 있는 결론은 아래와 같다.
- MS의 JpegBitmapEncoder는 mozjpeg와 거의 같은 코드[5]를 사용함
- MS의 프로그래머들이 저지른 DCT 테이블의 실수 같은 건 존재하지 않음
- C언어의 math.h 얘긴 언급할 가치가 없음. 그게 맞으면 트위터가 아니라 IEEE에 제보해서 수정 요구하고 논문 써야 됨
덧
- 다양한 이미지로 테스트를 했는데, 본 포스트의 내용에서 벗어나는 결과는 없었음
- 이 테스트와 별도로 MS의 jpeg 인코더가 용량 대비 화질이 떨어진다는 글은 종종 볼 수 있는 듯 함. (참조링크)
원문: TEUS.me