2019年2月10日日曜日

PythonでExifをsqliteのdbにしてみた

写真のExifをsqlite3でデータベース化して、 指定した時刻近傍のファイルリストを作成してみた。

データベースを一度使ってみたかったからというか、 写真を閲覧して探しだす作業がすごくメモリを食って私の環境には厳しい。 なので、可能性を試そうかと考えた。

  • linux(debian)のpyenv環境で、3.7.2。
  • Exifはpyexifinfoを使う。exiftoolのラッパー。
  • sqliteはpython標準で。
  • dbの中身は、小さくすべきと思って、画像ファイル名(パス含む)、ユリウス年、緯度、経度。
  • ユリウス年は julianを使って処理。

スクリプト

header and oters

#!~/.pyenv/shims/python
"""Generate exif(Exchangeable Image File Format) database."""

import logging
import sqlite3
from pathlib import Path
from datetime import datetime
from contextlib import closing

import exifread
import julian
import logzero
import pyexifinfo as pex
from pytz import timezone

TABLE_EXIF = '''create table if not exists exifdata (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name VARCHAR(128), juda REAL, bla REAL, blo REAL,
                time_stamp DEFAULT CURRENT_TIMESTAMP
                )'''
SQL_E = '''insert into exifdata (name, juda, bla, blo) values (?,?,?,?)'''
TFORM = "{0:%Y-%m-%d %H:%M:%S %Z}"

文字列からdatetimeとjulian年を作成する。

私が保管している写真のExifは、フォーマットなど色々だったので、 いくつか対応するようにした。't_zone'は、文字列がUTCかどうかの指定。

def tstr2dtime(t_str, zo, _type):
    """Convert string to datetime."""
    if _type == 0:
        # 2018:06:22 11:20:42
        t_t = datetime.strptime(t_str, '%Y:%m:%d %H:%M:%S')
    elif _type == 1:
        # 2018-06-22 11:20:42
        t_t = datetime.strptime(t_str, '%Y-%m-%d %H:%M:%S')
    elif _type == 2:
        # 2019-01-04T07:06:23Z
        t_t = datetime.strptime(t_str, '%Y-%m-%dT%H:%M:%SZ')
    elif _type == 3:
        # 2019-01-04 07:06:23.000011
        t_t = datetime.strptime(t_str, '%Y-%m-%d %H:%M:%S.%f')
    elif _type == 4:
        # 2019:01:04 07:06:23.000011
        t_t = datetime.strptime(t_str, '%Y:%m:%d %H:%M:%S.%f')
    elif _type == 5:
        # 2005:01:30 09:59:48+09:00
        t_t = datetime.strptime(t_str, '%Y:%m:%d %H:%M:%S%z')

    if zo == 'UTC':
        st_utc = t_t
        st_loc = st_utc.astimezone(timezone('Asia/Tokyo'))
    else:
        st_loc = t_t
        st_utc = st_loc.astimezone(timezone('UTC'))

    ju_val = julian.to_jd(st_utc)
    mju_val = ju_val - 2400001

    return st_utc, st_loc, ju_val, mju_val

緯度経度をdegreeに変換

def conv2deg(lat_v):
    """Convert to degree."""
    a_deg, a_min, a_sec = int(lat_v[0]), int(lat_v[2][:-1]), float(lat_v[3][:-1])
    return a_deg + a_min / 60. + a_sec / 3600.

ファイルを指定して、Exifのリストを作成

デジカメによってExifの中身が異るので、 時刻に関連する項目はほぼ全てリスト化してみた。中身が無いのは'None'。

pyexifinfoは、jsonとかcsvで返してくれるので、素人にやさしい。 速度がどうかはわからなし。exiftoolのラッパー。

def get_exifs(p_file):                                                                                     
    """Get EXIF of an image if exists."""                                                                  
    p_data = pex.get_json(p_file)[0]                                                                       
    # logzero.logger.info(p_data)  # show all meta data                                                    
    c_exver = p_data.get('EXIF:ExifVersion', 'None')                                                       
    c_model = p_data.get('EXIF:Model', 'None')                                                             
    c_time = [p_data.get('EXIF:DateTimeOriginal', 'None'),                                                 
              p_data.get('EXIF:CreateDate', 'None'),                                                       
              p_data.get('EXIF:GPSDateStamp', 'None'),                                                     
              p_data.get('EXIF:GPSTimeStamp', 'None'),                                                     
              p_data.get('EXIF:DateTime', 'None'),                                                         
              p_data.get('EXIF:DateTimeDigitized', 'None'),                                                
              p_data.get('File:FileModifyDate', 'None'),                                                   
              p_data.get('EXIF:TimeZoneOffset', 'None')]                                                   
    c_geos = [p_data.get('EXIF:GPSLatitude', 'None'),                                                      
              p_data.get('EXIF:GPSLongitude', 'None'),                                                     
              p_data.get('EXIF:GPSImgDirection', 'None'),                                                  
              p_data.get('EXIF:GPSAltitude', 'None')]                                                      
                                                                                                           
    if 'None' in c_time[2:4]:                                                                              
        # camera の時間はローカルタイムとして扱う。TimeZoneOffset が無いから。                             
        if c_time[0] != 'None':                                                                            
            st_utc, st_loc, ju_val, mju_val = tstr2dtime(c_time[0], 'Asia/Tokyo', 0)                       
        elif c_time[1] != 'None':                                                                          
            st_utc, st_loc, ju_val, mju_val = tstr2dtime(c_time[1], 'Asia/Tokyo', 0)                       
        elif c_time[4] != 'None':                                                                          
            st_utc, st_loc, ju_val, mju_val = tstr2dtime(c_time[4], 'Asia/Tokyo', 0)                       
        else:                                                                                              
            st_utc, st_loc, ju_val, mju_val = tstr2dtime(c_time[6], 'Asia/Tokyo', 5)
            # これしかないデータがあった。                       
    else:                                                                                                  
        # gps時があれば、これをUTCとして使う。                                                             
        st_utc, st_loc, ju_val, mju_val = tstr2dtime(c_time[2] + ' ' + c_time[3], 'UTC', 4)                
                                                                                                           
    if 'None' not in c_geos[0:2]:                                                                          
        # gps の座標情報があるときは、変換する。                                                           
        g_locate = [conv2deg(c_geos[0].split()), conv2deg(c_geos[1].split())]                              
    else:                                                                                                  
        g_locate = [None, None]                                                                            
                                                                                                           
    return [str(p_file), c_exver, c_model, st_loc, ju_val, mju_val, g_locate, c_time, st_utc]              

ユリウス年の区間を指定して抽出

def search_byjud(s_jud, e_jud, sqdb_file):                                             
    """Search picture file in ju_date."""                                              
    logzero.logger.info("- Results:")                                                  
    with closing(sqlite3.connect(sqdb_file)) as conn:                                  
        db_curs = conn.cursor()                                                        
        db_curs.execute("SELECT * FROM exifdata")                                      
        sel_sql = '''select * from exifdata where juda > ? and juda < ?'''             
        for i, row in enumerate(db_curs.execute(sel_sql, (s_jud, e_jud, ))):           
            mes_st = " {0:3d}, {1:s}".format(i, Path(row[1]).name)                     
            p_time = timezone('UTC').localize(julian.from_jd(row[2], fmt='jd'))        
            mes_st += ", " + TFORM.format(p_time.astimezone(timezone('Asia/Tokyo')))   
            mes_st += ", MJD {0:.9f}".format(row[2] - 2400001)                         
            mes_st += ", {0:9.6f},{1:10.6f}".format(row[3], row[4])                    
            logzero.logger.info(mes_st)                                                

メイン

def main():                                                                                                  
    """Do main prcess."""                                                                                    
    # 画像ファイル                                                                                           
    p_path = Path('/home/hogehoge/Pictures/CAMERA')                                                          
    p_files = sorted(list(p_path.glob("NIKON/**/*.JPG")))                                                    
                                                                                                             
    # データベースファイル                                                                                   
    sqldb_file = 'pic_exif.sqlite'                                                                           
                                                                                                             
    with closing(sqlite3.connect(sqldb_file)) as conn:                                                       
        db_curs = conn.cursor()                                                                              
                                                                                                             
        # tableがあれば削除させたい時                                                                        
        # db_curs.execute("DROP TABLE IF EXISTS exifdata")                                                   
                                                                                                             
        db_curs.execute(MAKE_EXID_TABLE)                                                                     
        db_curs.execute("SELECT * FROM exifdata")                                                            
                                                                                                             
        for p_f in p_files:                                                                                  
            # 登録済か確認                                                                                   
            db_curs.execute("SELECT name FROM exifdata WHERE name = ?", (str(p_f),))                         
            ck_row = db_curs.fetchall()                                                                      
            db_curs.execute("SELECT * FROM exifdata")                                                        
            f_nam = "{0:s}:".format(str(p_f.relative_to(p_path)))                                            
                                                                                                             
            if ck_row:                                                                                       
                # 登録あればパス                                                                             
                mes_st = "Has " + f_nam                                                                      
            else:                                                                                            
                # なければ、Exifを読んで登録                                                                 
                p_table = get_exifs(p_f)                                                                     
                p_jdate = p_table[4]                                                                         
                p_lat = p_table[6][0] if p_table[6][0] else 0.0                                              
                p_lon = p_table[6][1] if p_table[6][1] else 0.0                                              
                db_curs.execute(SQL_IN, (str(p_f), p_jdate, p_lat, p_lon))                                   
                                                                                                             
                # メッセージ作成                                                                             
                mes_st = "New " + f_nam + TFORM.format(p_table[3])                                           
                mes_st += ", JD {0:.9f}".format(p_table[4])                                                  
                mes_st += ", PO({0:},{1:})".format(p_table[6][0], p_table[6][1])                             
                                                                                                             
            logzero.logger.info(mes_st)                                                                      
                                                                                                             
        # 終了したら、保存、圧縮。                                                                           
        conn.commit()                                                                                        
        db_curs.execute('VACUUM')                                                                            
                                                                                                             
    # ユリウス年を指定して検索してみる                                                                       
    search_byjud(2457970.53, 2457970.54, sqldb_file)                                                         
                                                                                                             
                                                                                                             
if __name__ == '__main__':                                                                                   
                                                                                                             
    LOG_FORMAT = '%(color)s[%(module)s:%(lineno)d]%(end_color)s %(message)s'                                 
    FORMATTER = logzero.LogFormatter(fmt=LOG_FORMAT)                                                         
    logzero.setup_default_logger(formatter=FORMATTER)                                                        
    logzero.loglevel(logging.INFO)                                                                           
    logzero.logfile("./_logs/log.log", maxBytes=3e5, backupCount=3)                                          
                                                                                                             
    main()                                                                                                   

出力例

$ python exifdb.py                                                                      
 Has zenpad_Z380M/20161219/P_20161219_103516_1_p.jpg:                                   
 Has zenpad_Z380M/20161219/P_20161219_103530_1_p.jpg:                                   
      ;;                                                                                
      ;;                                                                                
 - Results:                                                                             
    0, RIMG1141.JPG, 2017-08-05 09:47:31 JST, MJD 57969.533007292, 35.312625,136.015147 
    1, RIMG1142.JPG, 2017-08-05 09:47:31 JST, MJD 57969.533007292, 35.312625,136.015147 
                                                                                        

このスクリプトに、gpxのログから'gpxpy'の'get_time_bounds'を使って ユリウス年を求めて使うようにすると、以下のような出力が得られる。

- Read log [hoge.gpx]:                                                                                       
  - (2017-08-04 21:01:36 UTC) - (2017-08-05 08:50:11 UTC)                                                    
  - (2017-08-05 06:01:36 JST) - (2017-08-05 17:50:11 JST)                                                    
- Results:                                                                                                   
   0, CAMERA/WG-4T/140_0805/RIMG1139.JPG, 2017-08-05 06:14:17 JST, 2457970.384930555,  0.000000,  0.000000   
   1, CAMERA/WG-4T/140_0805/RIMG1140.JPG, 2017-08-05 09:30:39 JST, 2457970.521286458, 35.274528,136.010953   
   2, CAMERA/WG-4T/140_0805/RIMG1141.JPG, 2017-08-05 09:47:31 JST, 2457970.533007292, 35.312625,136.015147   
   3, CAMERA/WG-4T/140_0805/RIMG1142.JPG, 2017-08-05 09:47:31 JST, 2457970.533007292, 35.312625,136.015147   
       ;                                                                                                     
       ;                                                                                                     

使ってみた感想なんだが、 デジカメの時刻設定がデタラメな時期があったことを知った。だけだった。
次はトラックポイントとの紐付けだと思っていたのに、萎えた。

まあ初心者でも、sqliteが少しは使えたのかも、ということで良しとした。

0 件のコメント:

コメントを投稿

麻のボディタオル

2018年の秋(まだ、自転車を封印してない)、 近江上布伝統産業会館 で、興味からボディタオルを購入した。 お、よかった。: 自然派パン工房 ふるさとの道 ほぼ毎日風呂で使ってきて、ついに寿命がきたようだ。 お店の方に、「糸が痩せて破れてくる」まで使える、と...