照片分类

作者: Phil Hughes

我们都知道分类照片的正确方法是在拍摄后立即进行分类。我们也知道在硬盘驱动器故障之前进行磁盘备份是备份的正确方法。但是,我们并不总是以正确的方式做事。现在说说我的情况。在过去的七年中,我用我的数码相机拍摄了近 10,000 张照片。是的,同一台相机——这可能是一则佳能 A20 的广告,它被我和其他人虐待、摔落,并被数十个从未用过相机的孩子以及一些甚至从未用过抽水马桶的孩子使用过。无论如何,来自相机的照片分布在几张 CF 卡、CD、两台不同的计算机以及其他未知的地方。

有时,相同的照片被多次保存。当我更换 CF 卡时,照片编号顺序被重置。或者,换句话说,我有一个烂摊子要收拾。我经常被要求查找某张特定的照片,并花费大量时间来寻找它。这一次,我决定花时间编写一个程序来帮助解决这个问题。

是的,有很多程序可以对照片进行排序和生成缩略图,但是当您一开始就有大约 10,000 张图像时,某种预排序是有意义的。以下是我希望预排序执行的操作。

  1. 读取可能的照片文件列表。
  2. 构建一个数据库,其中包含创建日期、一些来源信息以及用于存储每张照片的组织化位置。
  3. 能够标记每张照片。在这种情况下,任何数量的字母都可以。
  4. 可选地添加描述。
  5. 允许我对明显不好的照片说“忽略它”。
  6. 允许我逐步添加到此集合中。

是的,这只是一个开始,但考虑到问题的严重性,这是有道理的。例如,来源信息将是照片来自哪台计算机或 CD。对于大多数照片,来自相机的 EXIF 信息将为我提供照片的实际拍摄日期和时间。但是,如果这不可用(例如,编辑过的照片),我将接受 Linux 文件系统时间戳。

我认为整理这些东西是一个四步过程。

  1. 查找所有照片——这是体力劳动和构建文件名列表的结合。一个find命令可以完成繁重的工作。例如
    find /home/tux/Pix -iname "*.jpg" >file.list
    
    可以完成大部分工作。可以在目录或目录树的基础上构建多个列表。
  2. fotosort(我在这里谈论的程序)和我的时间可以用来处理每个列表。它将允许我跳过一张照片或添加一些标记信息并保存副本。所有“已处理”的照片都将最终放在一个大树中,数据库指向它们。
  3. 丢弃重复项。这将是下一个编程项目。通过 MD5 摘要和数据库中的文件大小(以字节为单位),可以很容易地找到重复的文件。
  4. 创建照片库。

无论我选择手动执行最后一步——创建图库,还是使用许多现有程序之一,或者自己编写一些东西来完成它,我都已经在朝着正确的方向前进。我需要的所有信息都在数据库中,照片都放在一个地方。

代码

让我们看看我创建了什么。它远非一件艺术品,因为它经历了大多数程序都会经历的典型演变过程。但是,它可以工作。如果我要经常使用它,我会花一些时间来清理它并添加错误处理,但对我来说它几乎是一次性的。

类 Rec不仅仅是一个注释,它显示了我将需要哪些数据。当用于创建实例时,它会传递 source_info 字符串。这对于在 fotosort 的单次运行中创建的所有记录来说都是通用的。

主程序打开作为命令行参数传递的文件名列表,并打开数据库(如果数据库不存在,则创建数据库和文件树)。然后,它循环遍历这些文件名,使用 GraphicMagick 的 display 函数显示它们,并检查您是否要保存每个文件名。如果您说跳过,它将移动到下一个文件。

如果您选择保存文件,它将获取文件时间戳、字节数和 MD5 摘要,提示输入标志和描述,将信息插入数据库,并将图像文件复制到新树中。无论您是否选择保存,图像显示都会通过调用 kill 并使用启动时返回的 pid 来终止。所有细节都由函数处理。以下是重要函数的快速浏览。

tree_setup()创建 100 个名为 00 到 99 的子目录。由于我有 10,000 个文件要处理,我当然不想将它们全部放在一个目录中。它们将存储在由文件名最后两位数字选择的 100 个不同的目录中。例如,图片 z_000021、z_000121、z_099921 等都将存储在子目录 21 中。

store_open()检查数据目录是否可访问。如果可访问,它将打开数据库并返回 sqlite3 连接 ID。否则,在您允许的情况下,它将创建一个新的文件树(使用 tree_setup() 并初始化数据库。

store_add()向数据库添加记录。它返回最后一行 ID(自动递增 ID 字段),这也是文件名的数字部分。我们使用它将文件复制到数据树。

file_ts()嗯,很糟糕。干净的部分是使用 stat 获取字节数。糟糕的部分是从 EXIF 信息(如果存在)中获取图片创建时间。我找到了对 Python 中多个 EXIF 包的引用,但每个包似乎都有问题。我选择使用 Kubuntu 中包含的 exiv2 程序。我读取结果,直到找到“Image timestamp”行,并以笨拙的方式将其转换为真正的 Linux 式时间戳(自纪元以来的秒数)。这很痛苦,但这是以后数据比较的最佳选择。

如果没有 EXIF 信息或时间戳丢失,我将接受文件系统中最后的修改时间。stat 可以轻松提供此信息。

img_save()创建一个包含以下内容的文件名z_和一个六位数字。该数字是数据库记录 ID,并添加了前导零。然后,它使用与 tree_setup() 使用的目录名称相同的 mod 100 技巧计算实际目标路径。

img_hash()使用 hashlib 为文件创建 MD5 摘要。除了 hashlib 是新的并且取代了旧的摘要创建例程之外,没有其他神奇之处。

故事到此结束。正如我所说,该程序不断发展,并且有所体现。这实际上很好地说明了为什么程序应该编写两次。仍然存在一个严重的(好吧,令人恼火的)问题。当图像显示打开时,焦点会切换到它。因此,您需要单击鼠标才能返回控制台窗口以与主循环通信。可能有一种正确的方法来解决这个问题,但目前,只需将 KDE 控制模块中的焦点窃取预防级别(单击任务栏中的图标,选择“配置窗口行为”和“高级”)设置为高即可解决问题。不幸的是,这不是我想要的一般策略。我确信在程序控制下很容易修复——我只是还没有弄清楚如何修复。

现在,我想我需要实际花几天时间使用该程序。我确实需要一些照片用于 Geek Ranch 网站。

编者注:如果您复制并粘贴以下代码,则该代码将无法工作。请从此处获取代码。

# fotosort.py
# Takes a list of photo files and lets you play with them
# What it did goes in a database including user supplied flags and description
# Phil Hughes 25 Dec 2007@0643

import sys
import os
import time
import shutil
import hashlib
from pysqlite2 import dbapi2 as sqlite

dataloc ="/home/fyl/PIXTREE"    # where to build the tree

connection = 0                  # will be connection ID

class Rec():            # what we will put in the db
        def __init__(self, source_info):
                self.source_info = source_info  # where it came from
        # id integer primary key        # will be filename
        # flags text            # letters used for selection
        # md5 text              # MD5 hex digest
        # size integer          # file byte count
        # description text      # caption information
        # source_path text      # path we got it from
        # timestamp integer     # creation timestamp (from image of fs date)

def tree_setup():
        os.mkdir(dataloc, 0755)         # tree base
        for x in range(100):            # build 100 sub-directories
                os.mkdir("%s/%02i" % (dataloc, x), 0755)

def show_pix(path):     # runs display, returns display_pid so kill can work
        return os.spawnv(os.P_NOWAIT, "/usr/bin/gm",
                ["display", "-geometry", "240x240", path])

def store_open():       # opens, returns biggest ID or -1 on error
        # create data store if it doesn't exist
        if not os.access(dataloc, os.R_OK|os.W_OK|os.X_OK):
                print "can't open %s\n" % dataloc
                if raw_input("Create data structures (y/n): ") == 'y':
                        tree_setup()
                        # initialize the database
                        con = sqlite.connect(dataloc + "/pix.db")
                        cur = con.cursor()
                        cur.execute('''create table pix
                                (id integer primary key,
                                flags text,
                                md5 text,
                                size integer,
                                description text,
                                source_info text,
                                source_path text,
                                timestamp integer)
                                ''')
                else:           # the boss said forget it
                        exit(1)
        else:
                con = sqlite.connect(dataloc + "/pix.db")
        if con > 0:
                return con
        else:
                return -1

def store_close(con):
        con.close()

def store_add(data): # assigns next id, saves, returns id
        cur = connection.cursor()
        cur.execute('''
        insert into pix (flags, md5, size, description, source_info,
                source_path, timestamp) values (?, ?, ?, ?, ?, ?, ?)''',
                (data.flags, data.md5, data.size, data.description,
                data.source_info, data.source_path, data.timestamp)
        )
        connection.commit()
        return cur.lastrowid

def openfl(path):       # open a file list, returns file object
        return open(path, 'r')

def getfn(rec): # gets the next filename
        return readline(lfo)

def form_fill(rec):             # pass record to fill in
        rec.flags = raw_input("Flags: ")
        rec.description = raw_input("Desc.: ")

def file_ts(path):      # returns creation timestamp, file size in bytes
        size = os.stat(path).st_size
        # look for EXIF info but, if not found, uses filesystem timestamp
        exiv2fo = os.popen("/usr/bin/exiv2  %s" % path, 'r')
        for line in exiv2fo:
                if line[0:15] == "Image timestamp":
                        cl = line.index(':')
                        ts_str = line[cl+2:cl+21]
                        ts = time.mktime((int(line[cl+2:cl+6]),
                                int(line[cl+7:cl+9]), int(line[cl+10:cl+12]),
                                int(line[cl+13:cl+15]), int(line[cl+16:cl+18]),
                                int(line[cl+19:cl+21]), 0, 0, 0))
                        break
        else:                   # use filesystem timestamp
                ts = os.stat(path).st_mtime
        exiv2fo.close()
        return (long(ts), size)

def img_save(image_file, id):   # copy image file to store
        # store location is built from id and some other fun stuff

        fname = "z_%06d" % int(id)
        dest = dataloc + "/" + "%02d" % (int(id) % 100) + '/' + fname
        # print dest
        shutil.copyfile(image_file, dest)
        return dest

def img_hash(image_file):       # returns MD5 hash for a file
        fo = open(image_file, 'r')
        m = hashlib.md5()
        stuff = fo.read(8192)
        while len(stuff) > 0:
                m.update(stuff)
                stuff = fo.read(8192)
        fo.close()
        return (m.hexdigest())

###
### This is where the action starts ###

if len(sys.argv) != 2:
        print "usage %s names_file\n" % sys.argv[0]
        exit(1)

lfo = openfl(sys.argv[1])               # filename list file
connection = store_open()
if connection < 0:
        print "%s: unable to initialize database" % sys,agrv[0]
        exit(1)

# let's get the string to use for source info
rec = Rec(raw_input("Enter source info: "))

for f in lfo:
        f = f.strip()                           # toss possible newline
        display_pid = show_pix(f)
        disp = raw_input("s[ave]/d[iscard]/q[uit]: ")
        if disp != 'q' and disp != 'd':
                rec.timestamp, rec.size = file_ts(f)
                rec.source_path = f
                rec.md5 = img_hash(f)   # hash

                form_fill(rec)                          # get user input
                id = store_add(rec)                     # insert in db
                savedloc = img_save(f, id)      # copy the image

                print "Photo saved as %s\n" % savedloc
        os.system("kill %s" % display_pid)
        if disp == 'q':
                break

加载 Disqus 评论