OpenCV 비전 실무20장 · 교육 콘텐츠 설계와 종합 실습18 / 21← 이전목차다음 →MetaContents

20장. 교육 콘텐츠 설계와 종합 실습

실습 5 — 수업 자료 묶음 만들기

실습 2의 연습 그림으로 수강생에게 나눠 줄 자료 묶음 out_ch20_class_kit/ 을 코드로 만듭니다. data, code, answers 폴더와 README.md, requirements.txt 를 쓰고, 파일 목록 표를 찍고, 확인 목록을 자동으로 점검한 뒤 ZIP 한 파일로 묶습니다. 실습 2를 먼저 실행해야 합니다.

1단계. 폴더를 만들고 그림 나눠 담기

Python
import os
import shutil
from pathlib import Path

kit = Path("out_ch20_class_kit")
# 다시 돌려도 지난번 파일이 남지 않게 통째로 지우고 새로 만든다. rmtree = remove tree
shutil.rmtree(kit, ignore_errors=True)
for sub in ["data", "code", "answers"]:
    (kit / sub).mkdir(parents=True)

src = Path("out_ch20_exercise")
if not src.exists():
    raise SystemExit("out_ch20_exercise 가 없습니다. 실습 2를 먼저 실행하세요")
# 문제 그림은 data/ 에, 정답 마스크와 정답표는 answers/ 에 나눠 담는다
for p in sorted(src.iterdir()):
    dest = "answers" if ("mask" in p.name or p.suffix == ".csv") else "data"
    shutil.copy(p, kit / dest / p.name)       # copy: 파일 하나를 복사한다
for sub in ["data", "answers"]:
    print(sub, sorted(x.name for x in (kit / sub).iterdir()))
출력
data ['easy.png', 'hard.png', 'medium.png']
answers ['answer.csv', 'easy_mask.png', 'hard_mask.png', 'medium_mask.png']

2단계. 시작 코드, README, requirements.txt 쓰기

Python
from importlib.metadata import version

# 수강생이 채울 시작 코드. 빈칸은 ____ 로 둔다
starter = '''import cv2

img = cv2.imread("../data/medium.png", cv2.IMREAD_GRAYSCALE)
if img is None:
    raise SystemExit("그림을 읽지 못했습니다. code 폴더에서 실행했는지 보세요")

# 1단계: 노이즈를 줄인다(6차시)
blur = ____
# 2단계: Otsu 로 이진화한다(8차시)
_, binary = ____
cv2.imwrite("my_answer.png", binary)
'''
(kit / "code" / "step1_threshold.py").write_text(starter, encoding="utf-8")

readme = """# 동전 세기 이진화 실습

## 학습목표
- 노이즈 낀 그림을 흐리게 한 뒤 Otsu 로 이진화해 정답 마스크와의 IoU 0.9 이상을 얻을 수 있다.

## 준비
- Python 3.12 이상, `python -m pip install -r requirements.txt`

## 실행 순서
1. `code` 폴더로 옮긴 뒤 `step1_threshold.py` 의 빈칸(____)을 채워 실행한다.
2. 만든 `my_answer.png` 를 강사에게 내면 채점 결과와 피드백을 받는다.

## 자료와 라이선스
- `data/` 의 그림은 OpenCV 로 그린 가상의 그림이다(씨앗 2001). 실제 사진이 아니다.
- OpenCV 는 Apache 2.0 라이선스를 따른다.
"""
(kit / "README.md").write_text(readme, encoding="utf-8")

pkgs = ["opencv-python", "numpy"]                    # 이 실습이 쓰는 패키지만 적는다
(kit / "requirements.txt").write_text("".join(f"{p}=={version(p)}\n" for p in pkgs), encoding="utf-8")
print((kit / "requirements.txt").read_text(encoding="utf-8"))
출력
opencv-python==5.0.0.93
numpy==2.5.4

3단계. 파일 목록 표 만들기

Python
import pandas as pd

rows = []
# rglob("*"): 하위 폴더까지 모든 파일과 폴더를 찾는다(recursive glob)
for p in sorted(kit.rglob("*")):
    if p.is_file():
        rows.append({"파일": p.relative_to(kit).as_posix(), "KB": round(p.stat().st_size / 1024, 1)})
files = pd.DataFrame(rows)
print(files.to_string(index=False))
print("파일", len(files), "개, 모두", round(float(files["KB"].sum()), 1), "KB")
출력
                     파일   KB
     answers/answer.csv  0.3
  answers/easy_mask.png  1.2
  answers/hard_mask.png  1.4
answers/medium_mask.png  1.3
code/step1_threshold.py  0.3
          data/easy.png  1.4
          data/hard.png 69.5
        data/medium.png 61.8
              README.md  0.6
       requirements.txt  0.0
파일 10 개, 모두 137.8 KB

4단계. 확인 목록 점검하고 ZIP 으로 묶기

Python
checks = {
    "README 가 있다": (kit / "README.md").exists(),
    "requirements.txt 에 버전이 적혀 있다": "==" in (kit / "requirements.txt").read_text(encoding="utf-8"),
    "문제 그림 세 장이 data 에 있다": len(list((kit / "data").glob("*.png"))) == 3,
    "정답 마스크가 data 에 섞이지 않았다": not any("mask" in p.name for p in (kit / "data").iterdir()),
    "README 에 라이선스가 적혀 있다": "라이선스" in readme,
    "README 에 가상 그림이라고 밝혔다": "가상" in readme,
}
for name, ok in checks.items():
    print(f"[{'통과' if ok else '확인'}] {name}")

# make_archive(만들 이름, 형식, 묶을 폴더): 폴더를 ZIP 한 파일로 묶는다. 이름 뒤에 .zip 이 붙는다
zip_path = shutil.make_archive("out_ch20_class_kit", "zip", "out_ch20_class_kit")
print("묶은 파일:", os.path.basename(zip_path), os.path.getsize(zip_path) // 1024, "KB")
출력
[통과] README 가 있다
[통과] requirements.txt 에 버전이 적혀 있다
[통과] 문제 그림 세 장이 data 에 있다
[통과] 정답 마스크가 data 에 섞이지 않았다
[통과] README 에 라이선스가 적혀 있다
[통과] README 에 가상 그림이라고 밝혔다
묶은 파일: out_ch20_class_kit.zip 137 KB

코드 설명

  • shutil.rmtree(kit, ignore_errors=True) 는 폴더를 통째로 지웁니다. 다시 돌렸을 때 지난번의 파일이 섞여 남지 않게 합니다. ignore_errors=True 는 폴더가 없어도 오류를 내지 않습니다.
  • 정답 마스크와 정답표는 answers/ 에 따로 담습니다. 수강생에게 나눠 줄 때는 answers/ 를 빼고 묶을 수도 있습니다.
  • Path.write_text(글자, encoding="utf-8") 은 글자를 파일로 한 번에 씁니다. README 의 # 은 마크다운(Markdown, 기호로 꾸미는 글쓰기 방식)의 제목 표시입니다.
  • importlib.metadata.version(이름) 은 설치된 패키지의 버전을 돌려줍니다. 손으로 적지 않고 읽어 오므로 수업 PC 의 버전과 어긋나지 않습니다.
  • 묶음은 파일 10개, 모두 137.8KB 이고 ZIP 은 137KB 입니다. png 는 이미 줄여 저장한 형식이라 ZIP 으로 묶어도 거의 줄지 않습니다. 노이즈가 낀 medium.png(61.8KB)와 hard.png(69.5KB)가 대부분을 차지합니다.
← 이전 절20장 · 실습 4 — 전후 비교 격자와 한 장 요약 만들기다음 절 →20장 · 프로젝트 1 — 부품 검사 수업 데모 만들기