사진 선별 프로그램
그동안 수 많은 사진을 선별하기 위해 많은 시간을 사용해왔다. 나름 자동화를 하려고 무단히 노력했지만, 좀처럼 쉽지 않았다. 여기서 설명하는 프로그램 역시 한계점은 분명하다.
개인적으로 생각할 때, 최종 단계는 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]))
아래의 기능을 가진다.
- 방향키로 사진을 옮겨다니며 볼 수 있다.
- 해당 사진의 정보(날짜, 크기, 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