2장. 이미지 입출력과 영상 시각화
실습 2 — JPG 품질별로 저장하고 크기와 차이 재기
상품 그림(ch02_product.png)을 PNG 와 여러 품질의 JPG 로 저장한 뒤 다시 읽어, 파일 크기와 원본과의 평균 절대 차이를 잽니다. 품질을 낮추면 파일은 얼마나 작아지고 그림은 얼마나 달라지는지 숫자로 확인합니다. 그림은 연습용 가상 상품입니다.
1단계. PNG 로 저장하고 다시 읽기
import os
import cv2
import numpy as np
src = cv2.imread("ch02_product.png")
print("원본 모양:", src.shape, "/ 메모리:", f"{src.nbytes:,}바이트")
# PNG 로 저장한다. 성공하면 True
ok = cv2.imwrite("out_ch02_png.png", src)
back = cv2.imread("out_ch02_png.png")
print("PNG 저장:", ok, f"/ 파일 {os.path.getsize('out_ch02_png.png'):,}바이트")
# np.array_equal: 모양과 모든 숫자가 같으면 True → 무손실인지 확인한다
print("PNG 를 다시 읽은 숫자가 원본과 같은가:", np.array_equal(src, back))원본 모양: (180, 240, 3) / 메모리: 129,600바이트
PNG 저장: True / 파일 10,787바이트
PNG 를 다시 읽은 숫자가 원본과 같은가: True2단계. 품질 다섯 단계로 JPG 저장하고 재기
import pandas as pd
# 품질 → 저장할 파일 이름. 이름을 그대로 적어 두면 어떤 파일이 생기는지 한눈에 보인다
names = {95: "out_ch02_q95.jpg", 80: "out_ch02_q80.jpg", 50: "out_ch02_q50.jpg",
20: "out_ch02_q20.jpg", 5: "out_ch02_q5.jpg"}
rows = []
for quality, name in names.items():
# cv2.imwrite(이름, 그림, [옵션, 값]): JPG 품질(0~100)을 정해 저장한다
cv2.imwrite(name, src, [cv2.IMWRITE_JPEG_QUALITY, quality])
back = cv2.imread(name)
# uint8 끼리 빼면 음수가 0~255 로 돌아 버리므로 int16 으로 바꿔 뺀다
diff = np.abs(src.astype(np.int16) - back.astype(np.int16))
rows.append({
"품질": quality,
"KB": round(os.path.getsize(name) / 1024, 1), # 파일 크기(KB)
"평균차이": round(float(diff.mean()), 3), # 평균 절대 차이
"가장큰차이": int(diff.max()), # 가장 많이 바뀐 칸
})
table = pd.DataFrame(rows)
print(table.to_string(index=False)) 품질 KB 평균차이 가장큰차이
95 10.3 1.780 48
80 5.8 2.719 79
50 4.0 3.678 100
20 2.8 5.727 125
5 1.8 11.449 1673단계. 표를 그래프로 그리기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
png_kb = round(os.path.getsize("out_ch02_png.png") / 1024, 1)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.4), layout="constrained")
# 왼쪽: 품질별 파일 크기 막대, PNG 크기는 가로 점선으로
ax1.bar(table["품질"].astype(str), table["KB"])
ax1.axhline(png_kb, linestyle="--", color="gray", label=f"PNG {png_kb}KB")
ax1.set_xlabel("JPG 품질")
ax1.set_ylabel("파일 크기 (KB)")
ax1.legend()
# 오른쪽: 품질별 평균 절대 차이 꺾은선. PNG 는 0
ax2.plot(table["품질"], table["평균차이"], marker="o")
ax2.set_xlabel("JPG 품질")
ax2.set_ylabel("원본과의 평균 절대 차이")
ax2.invert_xaxis() # 왼쪽이 높은 품질이 되게 가로축을 뒤집는다
plt.show()
4단계. PNG 압축 단계는 화질을 바꾸지 않는다
# 파일로 쓰지 않고 cv2.imencode 로 메모리 안에서 저장해 본다
for level in (0, 3, 9):
# cv2.IMWRITE_PNG_COMPRESSION: 0(압축 안 함)~9(가장 세게)
ok, buf = cv2.imencode(".png", src, [cv2.IMWRITE_PNG_COMPRESSION, level])
# cv2.imdecode: 바이트를 다시 그림으로 푼다
again = cv2.imdecode(buf, cv2.IMREAD_COLOR)
print(f"압축 {level}: {len(buf):>7,}바이트 원본과 같은가 {np.array_equal(src, again)}")압축 0: 130,043바이트 원본과 같은가 True
압축 3: 8,947바이트 원본과 같은가 True
압축 9: 7,887바이트 원본과 같은가 True결과를 읽어 봅니다. PNG 로 저장한 파일(10,787바이트)은 다시 읽은 숫자가 원본과 똑같았습니다. JPG 품질 95 는 10.3KB 로 PNG(10.5KB)와 거의 같은 크기였습니다. 이 그림처럼 넓은 면이 단색인 그림은 PNG 도 잘 줄어들기 때문입니다. 품질 80 에서는 5.8KB 로 품질 95 의 절반 남짓이 되고 평균 차이는 2.719 입니다. 품질을 더 낮추면 파일이 줄어드는 폭은 작아지는데 차이는 빠르게 커집니다. 품질 20 에서 5 로 내리면 크기는 2.8KB 에서 1.8KB 로 1KB 줄지만 평균 차이는 5.727 에서 11.449 로 두 배가 되고, 가장 많이 바뀐 칸은 167 이나 달라졌습니다. 4단계에서 PNG 압축 0 은 130,043바이트로 메모리 크기(129,600바이트)보다 조금 크고, 압축 9 는 7,887바이트까지 줄었지만 셋 다 원본과 숫자가 같았습니다. 1단계에서 옵션 없이 저장한 파일(10,787바이트)이 압축 3(8,947바이트)보다 큰 것은 OpenCV 의 기본값이 빠르게 저장하는 1 이기 때문입니다.
코드 설명
cv2.imwrite(name, src, [cv2.IMWRITE_JPEG_QUALITY, quality])는 셋째 자리의 옵션 목록으로 JPG 품질을 정해 저장합니다.src.astype(np.int16)은 숫자를 음수도 담는 16비트 정수로 바꿉니다. uint8 끼리 빼면 10 − 20 이 −10 이 아니라 246 이 되기 때문입니다.np.abs(...).mean()이 평균 절대 차이입니다. 칸마다 차이를 양수로 바꿔 모두 더한 뒤 숫자의 개수로 나눕니다.cv2.imencode와cv2.imdecode는 파일을 거치지 않고 메모리 안에서 저장과 읽기를 해 봅니다.len(buf)가 그 형식의 파일 크기입니다.