Back to creative
creative v1.0.0 6.5 min read 435 lines

image-batch-processor

이미지 배치 처리 — 리사이즈, 워터마크, 포맷 변환, 압축, 썸네일 생성, EXIF 제거 등 여러 이미지를 한번에 처리

Hermes Agent
MIT

이미지 배치 처리기

여러 이미지를 한번에 리사이즈, 변환, 워터마크, 압축 등 처리하는 스킬. Python Pillow와 ImageMagick을 활용합니다.

언제 사용할까

  • "이 사진들 전부 리사이즈해줘"
  • "워터마크 추가해줘"
  • "이미지들 WebP로 변환해줘"
  • "썸네일 만들어줘"
  • "EXIF 정보 지워줘"

전제 조건

  • Python 3.10+ 와 pip
  • 필요한 패키지: Pillow (자동 설치)
  • ImageMagick (선택, brew install imagemagick)

1. 리사이즈

단일 크기로 리사이즈

from PIL import Image
import os, glob

INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/output"
TARGET_SIZE = (800, 600) # (width, height)

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff'):
continue

img = Image.open(path)
# 비율 유지하면서 리사이즈
img.thumbnail(TARGET_SIZE, Image.Resampling.LANCZOS)

out_path = os.path.join(OUTPUT_DIR, os.path.basename(path))
img.save(out_path, quality=90)
print(f"✅ {os.path.basename(path)} → {img.size}")

print(f"완료! {len(glob.glob(os.path.join(OUTPUT_DIR, '*')))}개 처리됨")

비율 기준 리사이즈

# 최대 너비/높이 제한 (비율 유지)
MAX_WIDTH = 1200
MAX_HEIGHT = 1200

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
img = Image.open(path)
img.thumbnail((MAX_WIDTH, MAX_HEIGHT), Image.Resampling.LANCZOS)
# ...저장

배율 기준 리사이즈

SCALE = 0.5  # 50% 축소

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
img = Image.open(path)
new_size = (int(img.width SCALE), int(img.height SCALE))
img = img.resize(new_size, Image.Resampling.LANCZOS)
# ...저장

2. 포맷 변환

from PIL import Image
import os, glob

INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/output"
TARGET_FORMAT = "webp" # webp, png, jpeg, bmp
QUALITY = 85

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.gif'):
continue

img = Image.open(path)
# RGBA → RGB 변환 (JPEG 저장 시 필요)
if img.mode == 'RGBA' and TARGET_FORMAT == 'jpeg':
bg = Image.new('RGB', img.size, (255, 255, 255))
bg.paste(img, mask=img.split()[3])
img = bg

name = os.path.splitext(os.path.basename(path))[0]
out_path = os.path.join(OUTPUT_DIR, f"{name}.{TARGET_FORMAT}")

save_kwargs = {}
if TARGET_FORMAT == 'webp':
save_kwargs['quality'] = QUALITY
elif TARGET_FORMAT == 'jpeg':
save_kwargs['quality'] = QUALITY
save_kwargs['optimize'] = True
elif TARGET_FORMAT == 'png':
save_kwargs['optimize'] = True

img.save(out_path, **save_kwargs)
orig_size = os.path.getsize(path) / 1024
new_size = os.path.getsize(out_path) / 1024
print(f"✅ {os.path.basename(path)} ({orig_size:.0f}KB) → {name}.{TARGET_FORMAT} ({new_size:.0f}KB)")

3. 워터마크 추가

텍스트 워터마크

from PIL import Image, ImageDraw, ImageFont
import os, glob

INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/output"
WATERMARK_TEXT = "© 2024 Sample"
OPACITY = 128 # 0-255
POSITION = "bottom-right" # center, bottom-right, bottom-left, top-right, top-left

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp'):
continue

img = Image.open(path).convert('RGBA')
overlay = Image.new('RGBA', img.size, (255, 255, 255, 0))
draw = ImageDraw.Draw(overlay)

# 폰트 크기 = 이미지 크기의 3%
font_size = max(12, min(img.width, img.height) // 30)
try:
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size)
except:
font = ImageFont.load_default()

bbox = draw.textbbox((0, 0), WATERMARK_TEXT, font=font)
text_w, text_h = bbox[2] - bbox[0], bbox[3] - bbox[1]

margin = max(10, min(img.width, img.height) // 30)

if POSITION == "bottom-right":
x, y = img.width - text_w - margin, img.height - text_h - margin
elif POSITION == "bottom-left":
x, y = margin, img.height - text_h - margin
elif POSITION == "top-right":
x, y = img.width - text_w - margin, margin
elif POSITION == "top-left":
x, y = margin, margin
else: # center
x, y = (img.width - text_w) // 2, (img.height - text_h) // 2

draw.text((x, y), WATERMARK_TEXT, font=font, fill=(255, 255, 255, OPACITY))

result = Image.alpha_composite(img, overlay)
result = result.convert('RGB')

out_path = os.path.join(OUTPUT_DIR, os.path.basename(path))
result.save(out_path, quality=95)
print(f"✅ 워터마크 추가: {os.path.basename(path)}")

이미지 워터마크 (로고)

WATERMARK_PATH = "/path/to/logo.png"
watermark = Image.open(WATERMARK_PATH).convert('RGBA')

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
img = Image.open(path).convert('RGBA')

# 워터마크 크기 = 이미지의 20%
wm_ratio = 0.2
new_wm_size = (int(img.width wm_ratio), int(img.height wm_ratio))
wm_resized = watermark.resize(new_wm_size, Image.Resampling.LANCZOS)

# 위치 계산 (오른쪽 하단)
x = img.width - wm_resized.width - 20
y = img.height - wm_resized.height - 20

# 투명도 설정
wm_resized.putalpha(180)

img.paste(wm_resized, (x, y), wm_resized)
result = img.convert('RGB')

out_path = os.path.join(OUTPUT_DIR, os.path.basename(path))
result.save(out_path, quality=95)

4. 이미지 압축

from PIL import Image
import os, glob

INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/output"
TARGET_QUALITY = 80 # 1-100
MAX_FILE_SIZE_KB = 200 # 최대 파일 크기 제한 (KB)

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
img = Image.open(path)
name = os.path.splitext(os.path.basename(path))[0]

# WebP로 변환하여 압축
out_path = os.path.join(OUTPUT_DIR, f"{name}.webp")

quality = TARGET_QUALITY
while quality >= 20:
img.save(out_path, 'webp', quality=quality, method=6)
size_kb = os.path.getsize(out_path) / 1024
if size_kb <= MAX_FILE_SIZE_KB or quality <= 20:
break
quality -= 10

orig_size = os.path.getsize(path) / 1024
new_size = os.path.getsize(out_path) / 1024
ratio = (1 - new_size / orig_size) * 100
print(f"✅ {os.path.basename(path)}: {orig_size:.0f}KB → {new_size:.0f}KB ({ratio:.0f}% 감소)")

5. 썸네일 생성

from PIL import Image
import os, glob

INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/thumbnails"
THUMB_SIZE = (300, 300) # 정사각형 썸네일
CROP = True # True: 정사각형 자르기, False: 비율 유지

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp'):
continue

img = Image.open(path)
name = os.path.splitext(os.path.basename(path))[0]

if CROP:
# 중앙 크롭으로 정사각형
w, h = img.size
min_side = min(w, h)
left = (w - min_side) // 2
top = (h - min_side) // 2
right = (w + min_side) // 2
bottom = (h + min_side) // 2
img = img.crop((left, top, right, bottom))

img.thumbnail(THUMB_SIZE, Image.Resampling.LANCZOS)

out_path = os.path.join(OUTPUT_DIR, f"thumb_{name}.jpg")
img.save(out_path, 'JPEG', quality=85)
print(f"✅ 썸네일: {os.path.basename(path)} → {img.size}")

6. EXIF 메타데이터 제거

from PIL import Image
import os, glob

INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/cleaned"

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp', '.tiff'):
continue

img = Image.open(path)

# EXIF 데이터 확인
exif = img.getexif() if hasattr(img, 'getexif') else {}
if exif:
print(f"📌 {os.path.basename(path)}: {len(exif)}개 EXIF 태그 제거됨")

# 데이터 없이 새로 저장 (EXIF 제거)
data = list(img.getdata())
img_no_exif = Image.new(img.mode, img.size)
img_no_exif.putdata(data)

out_path = os.path.join(OUTPUT_DIR, os.path.basename(path))
img_no_exif.save(out_path, quality=95)

print(f"완료! EXIF 제거된 이미지: {OUTPUT_DIR}")

7. 이미지 정보 요약

from PIL import Image
import os, glob

INPUT_DIR = "/path/to/images"

total_size = 0
results = []

for path in sorted(glob.glob(os.path.join(INPUT_DIR, "*"))):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp'):
continue

img = Image.open(path)
size_kb = os.path.getsize(path) / 1024
total_size += size_kb

results.append({
'name': os.path.basename(path),
'size': f"{size_kb:.0f}KB",
'dimensions': f"{img.width}×{img.height}",
'mode': img.mode,
'format': img.format or 'Unknown',
})

print(f"📊 이미지 요약 ({len(results)}개, 총 {total_size/1024:.1f}MB)")
for r in results:
print(f" {r['name']}: {r['dimensions']} {r['mode']} {r['format']} ({r['size']})")

8. 종합 배치 처리 (한번에 여러 작업)

from PIL import Image
import os, glob

설정


INPUT_DIR = "/path/to/images"
OUTPUT_DIR = "/path/to/output"
MAX_SIZE = 1200
FORMAT = "webp"
QUALITY = 85
ADD_WATERMARK = True
WATERMARK_TEXT = "© ICBM2"
STRIP_EXIF = True

os.makedirs(OUTPUT_DIR, exist_ok=True)

for path in glob.glob(os.path.join(INPUT_DIR, "*")):
ext = os.path.splitext(path)[1].lower()
if ext not in ('.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff'):
continue

img = Image.open(path)
name = os.path.splitext(os.path.basename(path))[0]

# 1. EXIF 기반 자동 회전
if hasattr(img, '_getexif') and img._getexif():
from PIL import ImageOps
img = ImageOps.exif_transpose(img)

# 2. 리사이즈
if img.width > MAX_SIZE or img.height > MAX_SIZE:
img.thumbnail((MAX_SIZE, MAX_SIZE), Image.Resampling.LANCZOS)

# 3. 워터마크
if ADD_WATERMARK:
# ... (위 워터마크 코드 참고)

# 4. EXIF 제거 후 저장
data = list(img.getdata())
clean = Image.new(img.mode, img.size)
clean.putdata(data)

out_path = os.path.join(OUTPUT_DIR, f"{name}.{FORMAT}")
clean.save(out_path, FORMAT, quality=QUALITY)

orig = os.path.getsize(path) / 1024
new = os.path.getsize(out_path) / 1024
print(f"✅ {os.path.basename(path)}: {orig:.0f}KB → {new:.0f}KB")

9. ImageMagick (터미널 명령어)

간단한 작업은 터미널이 더 빠를 수 있음:

# 리사이즈 (비율 유지)
mogrify -resize 800x800 -path /output/ /input/*.jpg

포맷 변환


mogrify -format webp -quality 85 /input/*.jpg

워터마크


composite -gravity southeast -dissolve 50% logo.png input.jpg output.jpg

EXIF 제거


mogrify -strip /input/*.jpg

썸네일 생성 (정사각형)


mogrify -thumbnail 300x300^ -gravity center -extent 300x300 -path /thumbs/ /input/*.jpg

이미지 스프라이트 (여러 이미지를 하나로)


montage -geometry +2+2 -tile 4x0 /input/*.jpg sprite.jpg

주의사항

  • WebP 변환 시 구형 브라우저 호환성 고려
  • 워터마크는 PNG(투명도 지원)로 저장 권장
  • 원본 파일은 백업 후 작업할 것
  • GIF 애니메이션은 Pillow로 처리 시 첫 프레임만 저장됨 (ffmpeg 필요시 별도 처리)

Related Skills / 관련 스킬

ace-step-music

Kaggle T4 GPU에서 ACE-Step 1.5 터보로 가사 없는 인스트루멘탈 음악 생성 — 30초~60초 곡, 프롬프트 기반

creative v1.0.0

architecture-diagram

Generate dark-themed SVG diagrams of software systems and cloud infrastructure as standalone HTML files with inline SVG graphics. Semantic component colors (cyan=frontend, emerald=backend, violet=database, amber=cloud/AWS, rose=security, orange=message bus), JetBrains Mono font, grid background. Best suited for software architecture, cloud/VPC topology, microservice maps, service-mesh diagrams, database + API layer diagrams, security groups, message buses — anything that fits a tech-infra deck with a dark aesthetic. If a more specialized diagramming skill exists for the subject (scientific, educational, hand-drawn, animated, etc.), prefer that — otherwise this skill can also serve as a general-purpose SVG diagram fallback. Based on Cocoon AI's architecture-diagram-generator (MIT).

ascii-art

pyfiglet(571폰트), cowsay, boxes, toilet 등으로 ASCII 아트 생성. API 키 불필요.

ascii-video

ASCII 아트 비디오 프로덕션 파이프라인 — 비디오/오디오/이미지를 컬러 ASCII 캐릭터 비디오(MP4, GIF)로 변환