OpenCV 비전 실무2장 · 이미지 입출력과 영상 시각화15 / 21← 이전목차다음 →MetaContents

2장. 이미지 입출력과 영상 시각화

실습 2 — JPG 품질별로 저장하고 크기와 차이 재기

상품 그림(ch02_product.png)을 PNG 와 여러 품질의 JPG 로 저장한 뒤 다시 읽어, 파일 크기와 원본과의 평균 절대 차이를 잽니다. 품질을 낮추면 파일은 얼마나 작아지고 그림은 얼마나 달라지는지 숫자로 확인합니다. 그림은 연습용 가상 상품입니다.

1단계. PNG 로 저장하고 다시 읽기

Python
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 를 다시 읽은 숫자가 원본과 같은가: True

2단계. 품질 다섯 단계로 JPG 저장하고 재기

Python
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    167

3단계. 표를 그래프로 그리기

Python
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()
JPG 품질별 파일 크기 막대그래프와 원본과의 평균 절대 차이 꺾은선그래프

4단계. PNG 압축 단계는 화질을 바꾸지 않는다

Python
# 파일로 쓰지 않고 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) 가 그 형식의 파일 크기입니다.
← 이전 절2장 · 실습 1 — 읽기 모드 세 가지 견주기다음 절 →2장 · 실습 3 — 좌표로 자리 찾고 잘라 내기