ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Python/애플 사진 .HEIC의 Exif 조작 - ExifTool
    코딩/Python 2024. 1. 19. 13:55
    728x90

    HEIC의 exif 수정법을 찾는 것이 무척이나 어려웠다.

     

    PIL, pyheif, piheif, heif 등등 기억도 다 안난다. 애플 사진 .heic의 exif 조작을 하기 위해 라이브러리들을 뒤졌지만 모두 실패했다.

    겨우 건질 수 있었던 것은 heifread를 통해 exif를 읽기만 할 수 있다는 것. 읽을 수 있다는 것만으로도 고마웠다.

     

    그러던 중 반가운 것을 발견.

    ExifTool이 heic의 exif의 읽기/쓰기를 지원한다는 것.

    테스트 결과 HEIC 포함해서 거의 모든 이미지와 동영상 처리가 가능했다.

    단지 라이브러리로 사용하는 것이 아닌 터미널에서 사용해야 한다는 것이 문제였다.

    그래서 파이썬에서 커맨드라인 명령을 사용하는 subprocess를 이용해 코드를 만들어야 했다.

    그렇다면 왜 .HEIC를 .JPEG로 변환하지 않고 꼭 HEIC를 건드려야 하나?

    파일 크기 차이 때문이다. 아래 예에서는 HEIC가 2.6M, JPEG는 3.7M로 차이가 난다. 사진의 복잡성에 따라 다르지만 2-3배 차이가 나는 것 같다.

    728x90

    사용하는 라이브러리

    세 가지의 라이브러리를 사용한다.

    subprocess를 이용해 커맨드라인 명령어를 처리하고, 경로명을 처리하기 위해 os, 그리고 pandas는 csv를 터치한다.

    import subprocess
    import os
    import pandas as pd

    사전 입력 데이터

    Tag들을 얻기 위해 많은 시행착오가 있었다.

    수 많은 태그들 중 어떤 것이 유효한 지 확인하기 위해 이렇게 해보고 저렇게 해보고 한 끝에 모두 정리할 수 있었다.

    사진들은 비교적 쉬웠는데, .MOV 파일의 GPS값을 세팅하는 게 힘들었다. 처음엔 GPSCoorddinates가 잘 작동하는 듯 했는데 테스트 과정에서 GPS값을 잘못 넣어 오류가 생기는 바람에 고생을 했다. 처음엔 분명 잘 작동했는데 두번째 테스트부터 문제가 생겨 원인을 파악하는데 고생을 했다. 알고보니 -180~180도를 입력해야 하는데 280을 입력하는 바람에 오류가 생긴 것이었다.

    DATA = {
        'path':'/your/directory',
        'csv_img':'images.csv',
        'csv_mv':'movies.csv',
        'csv_exif':'exif.csv',
        'tags_img':[
            '-DateTimeOriginal',
            '-GPSLatitude',
            '-GPSLongitude',
            '-GPSAltitude'
            ],
        'tags_mv':[
            '-FileModifyDate',
            '-GPSCoordinates',
            ],
        'extensions_img':[
            '-ext', 'jpg',
            '-ext', 'jpeg',
            '-ext', 'heic'
            ],
        'extensions_mv':[
            '-ext', 'mp4',
            '-ext', 'mov',
            ]
    }

    메인 프로그램

    main 함수는 어떤 작업을 수행할 것인지 물어보고, 선택에 따라 다양한 함수를 호출하는 역할을 한다. 함수에서 사용되는 변수들은 DATA라는 전역 변수에서 가져온다.

    작업 유형에 따라 read_exif, write_exif, read_all 의 서브 프로그램 중 하나를 실행.

    1. 변수 초기화:
      • path: 파일 경로
      • csv_img, csv_mv, csv_exif: CSV 파일 이름들
      • tags_img, tags_mv: 이미지와 동영상에 대한 태그 정보
      • extensions_img, extension_mv: 이미지와 동영상에 대한 확장자 정보
    2. 사용자 입력:
      • task 변수에 사용자로부터 입력을 받아 다른 함수를 호출.
    3. 작업 수행:
      • task 값이 'r'일 경우 read_exif 함수를 호출하여 Exif 정보를 읽고 CSV로 저장.
      • task 값이 'w'일 경우는 덮어쓰기 작업이므로 진행 여부를 확인하고, 'y'일 경우 write_exif 함수를 호출하여 Exif 정보를 덮어쓴다.
      • task 값이 'a'일 경우 read_all 함수를 호출하여 모든 Exif 정보를 읽고 CSV에 저장. read_exif는 태그 데이터에 지정된 exif 정보(날짜와 GPS)만 저장하는데 read_all은 모든 exif 정보를 저장한다.
    4. 입력 오류 처리:
      • 잘못된 입력이 들어온 경우 'Wrong input!' 메시지를 출력.
    def main():
        path = DATA['path']
        csv_img = DATA['csv_img']
        csv_mv = DATA['csv_mv']
        csv_exif = DATA['csv_exif']
        tags_img = DATA['tags_img']
        tags_mv = DATA['tags_mv']
        extensions_img = DATA['extensions_img']
        extension_mv = DATA['extensions_mv']
            
        task = input('Enter task to do (r)ead, over(w)rite, read (a)ll exifs: ')
        if task.lower() == 'r':
            read_exif(path, tags_img, tags_mv, extensions_img, extension_mv, csv_img, csv_mv)
        elif task.lower() == 'w':
            ans = input('Overwrite, confirm (y/n): ')
            if ans.lower() == 'y': # need confirm
                write_exif(path, csv_img, csv_mv)
        elif task.lower() == 'a':
            read_all(path, csv_exif, extensions_img, extension_mv)
        else:
            print('Wrong input!')

    서브 프로그램

    4개의 서브프로그램이 있다.

    세 개는 메인에서 호출하여 사용하고 하나는 서브 프로그램에서 호출하는 유틸리티이다.

    EXIF를 읽는 read_exif()

    Exif 정보를 읽어와 CSV 파일로 저장. ExifTool을 subprocess를 통해 호출하여 결과를 CSV 파일로 저장.

    1. 매개변수:
      • path: 대상 파일이 있는 디렉토리 경로
      • tags_img, tags_mv: 이미지와 동영상에 대한 태그 리스트
      • extensions_img, extensions_mv: 이미지와 동영상에 대한 확장자 리스트
      • csv_img, csv_mv: 이미지와 동영상에 대한 CSV 파일 이름
    2. ExifTool 명령 생성:
      • cmd_read: 기본적인 ExifTool 명령어로, CSV 형식으로 출력하고 탭으로 구분하는 명령.
      • cmd_img, cmd_mv: 이미지와 동영상에 대한 Exif 정보를 읽어오기 위한 ExifTool 명령어를 생성.
      • ExifTool의 명령어 세트: exiftool -csv -T -Tag -ext Extension /path
    3. CSV 파일 경로 설정:
      • fp_csv_img, fp_csv_mv: 결과를 저장할 CSV 파일의 경로를 설정.
    4. Exif 정보 읽기 및 CSV 파일 저장:
      • subprocess.run()을 사용하여 ExifTool 명령어를 실행하고 결과를 변수에 저장.
      • 결과를 각각의 CSV 파일로 저장.
    5. 예외 처리:
      • subprocess.CalledProcessError 예외를 처리하여 명령어 실행 중 에러가 발생한 경우 에러 메시지와 stderr 내용을 출력.
    def read_exif(path: str, tags_img:list[str], tags_mv:list[str], \
        extensions_img:list[str], extensions_mv:list[str], csv_img: str, csv_mv: str):
    	'''
        디렉토리 안 파일의 exif를 읽고 CSV로 저장한다.
        '''
        cmd_read = [
            'exiftool',
            '-csv',
            '-T', # 탭 구분으로 출력
            ]
        
        cmd_img = cmd_read + tags_img + extensions_img + [path]
        cmd_mv = cmd_read + tags_mv + extensions_mv + [path]
        
        fp_csv_img = os.path.join(path, csv_img)
        fp_csv_mv = os.path.join(path, csv_mv)
        try:
            # subprocess를 통해 명령어 실행
            result_img = subprocess.run(cmd_img, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
            result_mv = subprocess.run(cmd_mv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
    
            # 결과를 CSV 파일로 저장    
            with open(fp_csv_img, 'w', encoding='utf-8-sig') as f:
                f.write(result_img.stdout)
            
            with open(fp_csv_mv, 'w', encoding='utf-8-sig') as f:
                f.write(result_mv.stdout)    
        except subprocess.CalledProcessError as e:
            print(f"Error: {e}")
            print(e.stderr)  # 에러가 있다면 stderr 내용 출력

    사진의 생성일은 EXIF의 DateTimeOriginal 태그로 처리하면 되지만 POSIX(Mac OS)에서는 파일 생성일을 일반적인 방법으로는 수정할 수 없다. 그래서 FileModifyDate를 사용한다.

    굳이 생성일을 변경하고자 하면 다음 글 참조

    Mac에서 GoPro Max 또는 360 camera로 생성된 동영상 생성시간을 Python으로 변경/File birthtime modification/파일 생성일 변경

     

    Mac에서 GoPro Max 또는 360 camera로 생성된 동영상 생성시간을 Python으로 변경/File birthtime modification/파

    Python os.stat 파이선의 os.stat에서는 파일의 액세스, 내용수정시간은 변경할 수 있어도 생성일을 변경할 수 있는 방법이 없다. os.stat_result의 타임스탬프는 아래와 같다. st_atime: 초 단위의 가장 최근

    summertrees.tistory.com

    images.csv
    movies.csv

    CSV 파일에서 정보 수정

    이제 수집된 정보를 가지고 CSV를 수정한다. MP4는 GPS 태그가 없으므로 수정해도 소용없다.

    EXIF를 수정하는 write_exif()

    CSV 파일에 저장된 Exif 정보를 이용하여 이미지와 동영상의 Exif 데이터를 수정하고, 변경 사항을 원본 파일에 덮어쓴다.

    이 코드는 실제 파일을 변경시키므로 메인에서 이 코드를 실행할 때는 한 번 더 실행여부를 확인한다.

    1. 매개변수:
      • path: 대상 파일이 있는 디렉토리 경로
      • csv_img, csv_mv: 이미지와 동영상에 대한 CSV 파일 이름
    2. CSV 파일 경로 설정:
      • fp_csv_img, fp_csv_mv: 대상 CSV 파일의 경로.
    3. ExifTool 명령 생성:
      • cmd_img, cmd_mv: Exif 데이터를 수정하고자 하는 이미지와 동영상에 대한 ExifTool 명령어를 생성합니다. CSV 파일을 활용하며, -overwrite_original 옵션은 원본 파일을 덮어쓰기 위한 옵션.
      • ExifTool 명령 세트: exiftool -csv=extension -overwrite_original your/path
    4. Exif 데이터 수정 및 덮어쓰기:
      • subprocess.run()을 사용하여 ExifTool 명령어를 실행하고 결과를 무시. 즉, 표준 출력 및 표준 에러 출력을 무시하고 에러가 발생하면 예외가 발생.
    5. 예외 처리:
      • subprocess.CalledProcessError 예외를 처리하여 명령어 실행 중 에러가 발생한 경우 에러 메시지와 stderr 내용을 출력.
    def write_exif(path: str, csv_img: str, csv_mv: str):
        fp_csv_img = os.path.join(path, csv_img)
        fp_csv_mv = os.path.join(path, csv_mv)
        cmd_img = ['exiftool', '-csv=' + fp_csv_img, '-overwrite_original', path]
        cmd_mv = ['exiftool', '-csv=' + fp_csv_mv, '-overwrite_original', path]
        try:
            subprocess.run(cmd_img, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
            subprocess.run(cmd_mv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
        except subprocess.CalledProcessError as e:
            print(f"Error: {e}")
            print(e.stderr)  # 에러가 있다면 stderr 내용 출력

    모든 Exif 정보를 출력하는 read_all()

    모든 이미지와 동영상 파일의 Exif 정보를 읽어와서 CSV 파일로 저장하는 함수.

    1. 매개변수:
      • path: 대상 파일이 있는 디렉토리 경로
      • csv_exif: 모든 Exif 정보를 저장할 CSV 파일 이름
      • extensions_img, extensions_mv: 이미지와 동영상에 대한 확장자 리스트
    2. CSV 파일 경로 설정:
      • fp_csv_exif: 대상 CSV 파일의 경로.
      • fp_csv_tmp: 임시 CSV 파일의 경로.
    3. ExifTool 명령 생성:
      • cmd: -a, -G1 옵션을 사용하여 모든 태그를 가져오고, -csv, -T 옵션을 사용하여 CSV 형식으로 출력하며, 이미지와 동영상에 대한 확장자 리스트를 포함한 ExifTool 명령어를 생성.
      • ExifTool 명령 세트: exiftool -a -G1 -csv -T -ext extension your/path
    4. Exif 정보 읽기 및 CSV 파일 저장:
      • subprocess.run()을 사용하여 ExifTool 명령어를 실행하고 결과를 변수에 저장.
      • 결과를 임시 CSV 파일로 저장.
    5. CSV 파일 전치 (Transpose):
      • transpose_csv() 함수를 호출하여 임시 CSV 파일을 전치. 이 함수는 CSV 파일의 행과 열을 바꾸는 역할을 한다.
    6. 예외 처리:
      • subprocess.CalledProcessError 예외를 처리하여 명령어 실행 중 에러가 발생한 경우 에러 메시지와 stderr 내용을 출력합니다.
    def read_all(path: str, csv_exif: str, extensions_img: list[str], extensions_mv: list[str]):
        fp_csv_exif = os.path.join(path, csv_exif)
        fp_csv_tmp = os.path.join(path, 'tmp.csv')
        cmd = ['exiftool', '-a', '-G1', '-csv', '-T'] + extensions_img + extensions_mv + [path]
        try:
            result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
            # 결과를 CSV 파일로 저장
            with open(fp_csv_tmp, 'w', encoding='utf-8-sig') as f:
                f.write(result.stdout)
            transpose_csv(path, fp_csv_tmp, csv_exif)
        except subprocess.CalledProcessError as e:
            print(f"Error: {e}")
            print(e.stderr)  # 에러가 있다면 stderr 내용 출력

    CSV의 행/열을 바꾸는 유틸리티 transpose_csv()

    CSV 파일의 행과 열을 바꾸는 함수. Pandas 라이브러리를 사용하여 CSV 파일을 읽고 행과 열을 전치시키고, 그 결과를 새로운 CSV 파일로 저장하고 원본 CSV 파일을 삭제.

    1. 매개변수:
      • path: 대상 파일이 있는 디렉토리 경로
      • csv_source: 전치를 수행할 원본 CSV 파일 이름
      • csv_target: 전치된 결과를 저장할 새로운 CSV 파일 이름
    2. CSV 파일 경로 설정:
      • fp_source, fp_target: 원본과 결과 CSV 파일의 경로를 설정.
    3. Pandas를 사용한 CSV 파일 읽기와 전치:
      • pd.read_csv(fp_source): Pandas를 사용하여 원본 CSV 파일을 읽어 DataFrame으로 로드.
      • df.transpose(): DataFrame을 전치시켜서 새로운 DataFrame을 생성.
    4. 전치된 결과를 CSV 파일로 저장:
      • transposed_df.to_csv(fp_target, index=True): 전치된 DataFrame을 CSV 파일로 저장. index=True는 DataFrame의 인덱스를 함께 저장할 것인지 여부를 나타낸다.
    5. 원본 CSV 파일 삭제:
      • os.remove(fp_source): 전치된 결과를 저장한 후에 원본 CSV 파일을 삭제.
    def transpose_csv(path: str, csv_source: str, csv_target: str):
        fp_source = os.path.join(path, csv_source)
        fp_target = os.path.join(path, csv_target)
        
        df = pd.read_csv(fp_source)
        transposed_df = df.transpose()
    
        transposed_df.to_csv(fp_target, index=True)
        os.remove(fp_source)
    728x90

    댓글

Designed by Tistory.