カメラの画像をリネームして圧縮するPython

  • URLをコピーしました!
目次

プログラムの内容

このプログラムは、ユーザーがドラッグアンドドロップで提供した画像と動画ファイルを処理するデスクトップアプリケーションです。主な機能は、ファイルの圧縮、リネーム、および指定された出力フォルダへの移動です。

機能概要

  • 画像圧縮: JPEGおよびPNGファイルをCaesiumCLTを使用して圧縮します。
  • リネーム処理: ファイルのメタデータ(Exif情報やファイルの最終変更日時)に基づいてリネームを行います。
  • ファイルの移動: 圧縮およびリネーム後のファイルをユーザーが指定した出力フォルダに移動します。
  • 設定の保存と読み込み: 出力フォルダ、CaesiumCLTのパス、圧縮設定などのユーザー設定を保存し、アプリケーション再起動時に読み込みます。

このプログラムは、画像編集や整理作業を効率化することを目的としています。

事前準備

CaesiumCLTの準備が必要です。

CaesiumCLTはCaesiumのコマンドラインから操作できるツールでgitに公開されている為、自身でビルドが必要です。

gitからダウンロードし、Rustをインストールしてgitに書かれている通りビルドしてください。

ソースコード

import sys
import os
import shutil
import subprocess
import pickle
import math
from datetime import datetime
from PIL import Image
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton, QMessageBox, QProgressBar,
                             QFileDialog, QSpinBox, QCheckBox, QLineEdit, QHBoxLayout, QFrame)
from PyQt5.QtCore import Qt, QCoreApplication
from concurrent.futures import ThreadPoolExecutor, as_completed

CONFIG_FILE = "app_config.pkl"

class DropArea(QFrame):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.load_config()
        self.compressed_files = []

    def initUI(self):
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        self.setAcceptDrops(True)

        self.layout = QVBoxLayout()
        self.label = QLabel("画像・動画ファイルをここにドロップしてください")

        self.outputFolderEdit = QLineEdit(self)
        self.outputFolderButton = QPushButton("出力フォルダを選択", self)
        self.outputFolderButton.clicked.connect(self.select_output_folder)

        self.threadCountSpinBox = QSpinBox(self)
        self.threadCountSpinBox.setMinimum(1)
        self.threadCountSpinBox.setMaximum(32)

        self.caesiumPathEdit = QLineEdit(self)
        self.caesiumPathButton = QPushButton("CaesiumCLTの場所を選択", self)
        self.caesiumPathButton.clicked.connect(self.select_caesium_path)

        self.compressCheckBox = QCheckBox("画像を圧縮する", self)
        self.progressBar = QProgressBar(self)
        self.progressBar.setMaximum(100)
        self.progressBar.setValue(0)

        self.layout.addWidget(self.label)
        self.layout.addLayout(self.create_horizontal_layout("出力フォルダ:", self.outputFolderEdit, self.outputFolderButton))
        self.layout.addLayout(self.create_horizontal_layout("スレッド数:", self.threadCountSpinBox))
        self.layout.addLayout(self.create_horizontal_layout("CaesiumCLTのパス:", self.caesiumPathEdit, self.caesiumPathButton))
        self.layout.addWidget(self.compressCheckBox)
        self.layout.addWidget(self.progressBar)
        self.setLayout(self.layout)

    def create_horizontal_layout(self, label_text, widget, button=None):
        layout = QHBoxLayout()
        label = QLabel(label_text)
        layout.addWidget(label)
        layout.addWidget(widget)
        if button:
            layout.addWidget(button)
        return layout

    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dropEvent(self, event):
        self.compressed_files.clear()
        files = [url.toLocalFile() for url in event.mimeData().urls()]
        self.process_files(files)

    def select_output_folder(self):
        folder = QFileDialog.getExistingDirectory(self, "出力フォルダを選択")
        if folder:
            self.outputFolderEdit.setText(folder)

    def select_caesium_path(self):
        path, _ = QFileDialog.getOpenFileName(self, "CaesiumCLTの場所を選択", "", "実行ファイル (*.exe)")
        if path:
            self.caesiumPathEdit.setText(path)

    def process_files(self, files):
        total_files = len(files)
        self.progressBar.setMaximum(total_files)
        self.progressBar.setValue(0)

        with ThreadPoolExecutor(max_workers=self.threadCountSpinBox.value()) as executor:
            futures = [executor.submit(self.process_image, file) for file in files]
            for future in as_completed(futures):
                result = future.result()
                if result:
                    self.compressed_files.append(result)
                self.progressBar.setValue(self.progressBar.value() + 1)
                QApplication.processEvents()

        self.rename_files()
        self.show_completion_message()
        self.progressBar.setValue(0)
        self.compressed_files.clear()

    def process_image(self, image_path):
        ext = os.path.splitext(image_path)[1].lower()
        if ext in [".jpg", ".jpeg", ".png"]:
            if self.compressCheckBox.isChecked():
                return self.compress_image(image_path)
            else:
                return self.copy_image(image_path)
        else:
            # 動画ファイルの場合、リネームしてからコピー
            timestamp = self.get_timestamp(image_path, ext)
            new_filename = timestamp.strftime("%Y%m%d-%H%M%S")
            new_filepath = self.generate_new_filepath(self.outputFolderEdit.text(), new_filename, ext)
            shutil.copy2(image_path, new_filepath)
            return new_filepath
    
    def compress_image(self, image_path):
        try:
            ext = os.path.splitext(image_path)[1].lower()
            if ext in [".jpg", ".jpeg", ".png"]:
                img = Image.open(image_path)
                width, height = img.size
                img.close()

                compress_arg = "--width" if width > height else "--height"
                compress_command = [self.caesiumPathEdit.text(), "-q", "90", compress_arg, "4898", "-e", "-o", self.outputFolderEdit.text(), image_path]
                subprocess.run(compress_command, check=True)

                return os.path.join(self.outputFolderEdit.text(), os.path.basename(image_path))
        except Exception as e:
            print(f"Error processing {image_path}: {e}")
            return None

    def copy_image(self, image_path):
        output_path = os.path.join(self.outputFolderEdit.text(), os.path.basename(image_path))
        shutil.copy2(image_path, output_path)
        return output_path

    def rename_files(self):
        renamed_files = []
        for image_path in self.compressed_files:
            ext = os.path.splitext(image_path)[1].lower()
            timestamp = self.get_timestamp(image_path, ext)
            new_filename = timestamp.strftime("%Y%m%d-%H%M%S")
            new_filepath = self.generate_new_filepath(self.outputFolderEdit.text(), new_filename, ext)

            compressed_file = os.path.join(self.outputFolderEdit.text(), os.path.basename(image_path))
            if os.path.exists(compressed_file):
                os.rename(compressed_file, new_filepath)
                renamed_files.append(new_filepath)
            else:
                renamed_files.append(image_path)
        self.compressed_files = renamed_files

    def generate_new_filepath(self, folder, base_filename, ext):
        counter = 0
        new_filepath = os.path.join(folder, f"{base_filename}-{counter}{ext}")
        while os.path.exists(new_filepath):
            counter += 1
            new_filepath = os.path.join(folder, f"{base_filename}-{counter}{ext}")
        return new_filepath

    def get_timestamp(self, file, ext):
        if ext in (".mov", ".mp4"):
            # 動画ファイルの場合、ファイルの最終変更日時を使用
            return datetime.fromtimestamp(os.path.getmtime(file))
        elif ext == ".png":
            # PNGファイルの場合、ファイルの最終変更日時を使用
            return datetime.fromtimestamp(os.path.getmtime(file))
        else:
            # JPEGファイルの場合、Exif情報を使用
            img = Image.open(file)
            exif_data = img._getexif()
            timestamp = exif_data[36867] if exif_data else None
            img.close()
            if timestamp:
                return datetime.strptime(timestamp, "%Y:%m:%d %H:%M:%S")
            else:
                # Exif情報がない場合はファイルの最終変更日時を使用
                return datetime.fromtimestamp(os.path.getmtime(file))


    def show_completion_message(self):
        total_size = self.calculate_total_size()
        readable_size = self.convert_size(total_size)
        QMessageBox.information(self, "完了", f"全てのファイルの処理が完了しました。\n処理後の合計サイズ: {readable_size}")

    def calculate_total_size(self):
        total_size = 0
        for file_path in self.compressed_files:
            if os.path.exists(file_path):
                total_size += os.path.getsize(file_path)
        return total_size

    def convert_size(self, size_bytes):
        if size_bytes == 0:
            return "0B"
        size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
        i = int(math.floor(math.log(size_bytes, 1024)))
        p = math.pow(1024, i)
        s = round(size_bytes / p, 2)
        return f"{s} {size_name[i]}"

    def load_config(self):
        if os.path.exists(CONFIG_FILE):
            with open(CONFIG_FILE, 'rb') as f:
                config = pickle.load(f)
                self.outputFolderEdit.setText(config.get('output_folder', ''))
                self.threadCountSpinBox.setValue(config.get('thread_count', 16))
                self.caesiumPathEdit.setText(config.get('caesium_path', ''))
                self.compressCheckBox.setChecked(config.get('compress', True))

    def save_config(self):
        config = {
            'output_folder': self.outputFolderEdit.text(),
            'thread_count': self.threadCountSpinBox.value(),
            'caesium_path': self.caesiumPathEdit.text(),
            'compress': self.compressCheckBox.isChecked()
        }
        with open(CONFIG_FILE, 'wb') as f:
            pickle.dump(config, f)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = DropArea()
    window.setWindowTitle("ファイルのドロップ")
    window.setGeometry(300, 300, 400, 200)
    window.show()
    sys.exit(app.exec_())

使い方

ソースコードを.pyで保存し、起動。起動するとGUIが立ち上がるので、CaesiumCLTのexeの場所とファイルの出力先、同時処理数を選択して画像ファイルをドラックアンドドロップしたら圧縮&リネームします。

リネームの基本形式

  1. 基本パターン: リネームされるファイル名は YYYYMMDD-HHMMSS 形式で生成されます。これはファイルが撮影または作成された年月日と時分秒を表します。
  2. 枝番の付与:
    • リネーム時に、ファイル名の末尾には常に枝番が付けられます。
    • 同名のファイルが存在しない場合でも、ファイル名は「-0」で終わります。
    • 既に同名のファイルが存在する場合、枝番が「-1」、「-2」… と増加していきます。
  3. ファイルタイプによる違い:
    • JPEGファイル: Exif情報から撮影日時を取得してリネーム。
    • PNGファイル: ファイルの最終変更日時を使用してリネーム。
    • 動画ファイル: ファイルの最終変更日時を使用してリネーム。

CaesiumCLTでの圧縮

CaesiumCLTでの圧縮は画像の最長辺を4898ピクセルに制限し、画像の品質を90%に設定しています。

雑記

完全に自分用のプログラムです。エラーハンドリングやいじわるなテストをしていないので不具合が出る可能性が高いです。

リネーム処理のPythonプログラムは昔から作っていたのですが、最終的に行う圧縮処理も同時にできたらなぁと思ってChatGPTに作らせました。

created by Rinker
¥2,970 (2024/06/15 10:50:26時点 Amazon調べ-詳細)
created by Rinker
¥2,475 (2024/06/15 10:50:26時点 Amazon調べ-詳細)
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

どうもZuxikkuです。
日本語だとズィックだとかジックって呼ばれています。
外国の方だとズクシーとか。ガジェットとか新しいもの大好き。

コメント

コメントする

CAPTCHA


目次