====== 사진 선별 프로그램 ====== 그동안 수 많은 사진을 선별하기 위해 많은 시간을 사용해왔다. 나름 자동화를 하려고 무단히 노력했지만, 좀처럼 쉽지 않았다. 여기서 설명하는 프로그램 역시 한계점은 분명하다. 개인적으로 생각할 때, 최종 단계는 AI 가 내가 원하는 사진의 기준으로 골라내는 것이다. 그러려면, 좋은 사진의 기준을 정해야 한다. 어떤 사진이 좋은 것인가? 이걸 정량적으로 나타낼 수 있는가? 이점이 어렵다. 다시 본론으로 돌아와서, 여기서의 프로그램은 AI 를 이용하지 않지만, 사진을 선별하는 작업에서 차지하는 상당부분의 시간을 절약하게 해준다. 기존에는 수백의 사진 중에서 괜찮은 사진의 파일명을 종이 또는 텍스트파일에 저장(a)하고, 여기에 적은 파일들만 골라(b)내서 서버에 올리고(c), 위키 페이지에 추가(d)했었다. 위 과정 중에서 a 단계를 간소화 시켜준다. 지금껏 일반적인 사진 뷰어 프로그램을 사용해서 작업을 했었다. 그렇다보니, 불편했다. 결국 chatGPT 를 통해 프로그램을 새로 하나 만들었다. ===== 개발 환경 만들기 ===== 파이썬으로 만들 것이기 때문에 실행을 위한 라이브러리 설치가 필요하다. #apt-get install python3-pil python3-tk python3-pil.imagetk ===== 프로그램 생성 ===== /usr/bin 아래에 select_image_viewer.py 파일을 만들고 아래를 입력한다. #!/usr/bin/env python3 import os import sys import re import subprocess import tkinter as tk from tkinter import messagebox from PIL import Image, ImageTk SUPPORTED = (".jpg", ".jpeg", ".png", ".gif", ".webp") def natural_key(s): return [int(text) if text.isdigit() else text.lower() for text in re.split('([0-9]+)', s)] def frac_to_float(v): if not v: return None try: if "/" in v: a, b = v.split("/") return float(a) / float(b) return float(v) except: return None def get_exif_value(path, key): try: cmd = ["identify", "-format", f"%[EXIF:{key}]", path] result = subprocess.check_output(cmd, stderr=subprocess.DEVNULL) return result.decode().strip() except: return "" def get_camera_info(path): aperture = "" shutter = "" focal = "" iso = "" fnum = get_exif_value(path, "FNumber") if fnum: f = frac_to_float(fnum) if f: aperture = f"f/{round(f,1)}" exp = get_exif_value(path, "ExposureTime") if exp: shutter = f"{exp} sec" fl = get_exif_value(path, "FocalLength") if fl: f = frac_to_float(fl) if f: focal = f"{round(f,1)} mm" iso_val = get_exif_value(path, "PhotographicSensitivity") if not iso_val: iso_val = get_exif_value(path, "ISOSpeedRatings") if iso_val: iso = f"ISO {iso_val}" return aperture, shutter, focal, iso class Viewer: def __init__(self, path): self.savefile = os.path.expanduser("~/work/c.txt") with open(self.savefile, "w"): pass self.dir = os.path.dirname(path) self.files = sorted( [f for f in os.listdir(self.dir) if f.lower().endswith(SUPPORTED)], key=natural_key ) self.index = self.files.index(os.path.basename(path)) self.root = tk.Tk() self.root.title("Image Viewer") self.root.geometry("+0+0") self.label = tk.Label(self.root) self.label.pack() self.info_label = tk.Label( self.root, text="", bg="black", fg="white", anchor="w", justify="left", font=("monospace", 10) ) self.info_label.place(x=5, rely=1.0, anchor="sw") self.root.bind("", self.prev_image) self.root.bind("", self.next_image) self.root.bind("", self.exit_program) self.root.bind("", self.delete_image) self.label.bind("", self.save_filename) self.show_image() self.root.mainloop() def show_image(self): if not self.files: print("No images left") self.root.destroy() return filename = self.files[self.index] path = os.path.join(self.dir, filename) try: img = Image.open(path) except Exception as e: print("image load error:", path, e) return width, height = img.size aperture, shutter, focal, iso = get_camera_info(path) size_kb = os.path.getsize(path) / 1024 screen_w = self.root.winfo_screenwidth() screen_h = self.root.winfo_screenheight() img.thumbnail((screen_w - 50, screen_h - 50)) self.photo = ImageTk.PhotoImage(img) self.label.config(image=self.photo) self.label.image = self.photo name, ext = os.path.splitext(filename) ext = ext.replace(".", "") info = f"{name},{ext}, {aperture}, {shutter}, {focal} {iso}, {width} x {height}, {size_kb:.1f} kB" self.info_label.config(text=info) self.root.title(filename) def next_image(self, event=None): if not self.files: return self.index = (self.index + 1) % len(self.files) self.show_image() def prev_image(self, event=None): if not self.files: return self.index = (self.index - 1) % len(self.files) self.show_image() def exit_program(self, event=None): self.root.destroy() def delete_image(self, event=None): if not self.files: return filename = self.files[self.index] path = os.path.join(self.dir, filename) answer = messagebox.askyesno( "Delete", f"Delete this file?\n\n{filename}" ) if not answer: return try: os.remove(path) print("deleted:", filename) except Exception as e: print("delete error:", e) return del self.files[self.index] if self.index >= len(self.files): self.index = 0 self.show_image() def save_filename(self, event=None): filename = self.files[self.index] name = os.path.splitext(filename)[0] savefile = os.path.expanduser("~/work/c.txt") if os.path.exists(savefile): with open(savefile) as f: if name in f.read().splitlines(): print("already saved:", name) return with open(savefile, "a") as f: f.write(name + "\n") print(f"saved: {name}") if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: viewer imagefile") sys.exit(1) Viewer(os.path.abspath(sys.argv[1])) 아래의 기능을 가진다. * 방향키로 사진을 옮겨다니며 볼 수 있다. * 해당 사진의 정보(날짜, 크기, ISO, 조리개, 렌즈거리, 사이즈)를 사진 왼쪽 하단에 출력한다. * 해당 사진을 클릭하면, 텍스트 파일(~/work/c.txt)에 파일명이 저장된다. * esc 키를 누르면 프로그램이 종료된다. 실행권한을 추가한다. #chmod +x simple_image_viewer.py 이제 파일매니저(PCManFM)에서 사진 파일을 더블클릭 했을 때, 위 프로그램이 실행되도록 해야 한다. 설정파일이 필요하다. ~/.local/share/applications/select_image_viewer.desktop 파일을 만든다. [Desktop Entry] Name=Select Image Viewer Exec=/usr/bin/select_image_viewer.py %f Type=Application Terminal=false MimeType=image/jpeg;image/png;image/gif;image/webp; Categories=Graphics; 실행권한을 추가한다. #chmod +x ~/.local/share/applications/select_image_viewer.desktop 파일매니저(PCManFM)를 실행하고, 사진 파일 선택 후, 마우스 오른쪽 버튼을 눌러 '다른 프로그램으로 열기' 를 선택한다. '설치한 프로그램 - 그래픽' 아래에 'Select Image Viewer' 가 보일 것이다. 이걸 선택하고, '선택한 프로그램을 이 파일에 대한 기본 동작으로 설정' 옵션을 체크한다. 이제 사진 파일을 더블클릭 해서 프로그램이 실행되는지 확인해보자. ===== 가로길이가 1500 픽셀 이상인 사진 삭제하기 ===== 아래의 명령어로 가능하다. #find . -type f -iname "*.jpg" | while read f do w=$(identify -format "%w" "$f") if [ "$w" -gt 1500 ] then rm "$f" fi done