Table of Contents

사진 선별 프로그램

그동안 수 많은 사진을 선별하기 위해 많은 시간을 사용해왔다. 나름 자동화를 하려고 무단히 노력했지만, 좀처럼 쉽지 않았다. 여기서 설명하는 프로그램 역시 한계점은 분명하다.
개인적으로 생각할 때, 최종 단계는 AI 가 내가 원하는 사진의 기준으로 골라내는 것이다. 그러려면, 좋은 사진의 기준을 정해야 한다. 어떤 사진이 좋은 것인가? 이걸 정량적으로 나타낼 수 있는가?
이점이 어렵다.

다시 본론으로 돌아와서, 여기서의 프로그램은 AI 를 이용하지 않지만, 사진을 선별하는 작업에서 차지하는 상당부분의 시간을 절약하게 해준다.
기존에는 수백의 사진 중에서 괜찮은 사진의 파일명을 종이 또는 텍스트파일에 저장(a)하고, 여기에 적은 파일들만 골라(b)내서 서버에 올리고©, 위키 페이지에 추가(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("<Left>", self.prev_image)
        self.root.bind("<Right>", self.next_image)
        self.root.bind("<Escape>", self.exit_program)
        self.root.bind("<Delete>", self.delete_image)
 
        self.label.bind("<Button-1>", 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]))

아래의 기능을 가진다.

실행권한을 추가한다.

#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