SaliencyMapを使って視線誘導したい


# はじめに

## Saliencyとは
- サリエンシー（Saliency） ＝「顕著性」「目立ちやすさ」
- 画像や映像において、人間の注意を引きつけやすい特徴や領域を指す

## サリエンシーマップとは
- 画像や映像の中で、各領域がどの程度目立つかを視覚的に表現したもの
    - 人間の視覚的注意を予測するモデルとして使用される

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348593/c6778eb7-f946-0897-8124-88f9a4d7ea4c.png)

## サリエンシーマップの主な用途
 - マーケティング：広告の効果を予測・分析
 - UI設計：重要な要素が目立つようにデザインを調整
 - コンピュータビジョン：物体検出や画像分割のための前処理として使用

# 本題

## やりたいこと

- 意図した情報やメッセージを効果的に伝えるための分析を簡単に行いたい
- サリエンシーマップを使うことで、自然に目が引きつけられる領域を特定し、それに基づいて効果的な視線誘導ができているかを確認したい


# つくってみる

- 画像、動画、PDFをインプットに処理できるようにした
    - 処理結果として、サリエンシーマップを重ねた画像、動画がzipファイルでDLできるようにする
- tkinterを使ってGUIで動かせるようにしてみた


### 動作イメージ

![Videotogif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348593/e9007e44-94bd-4377-5bd1-23c270483057.gif)

![Videotogif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/348593/6926079a-6625-c358-f931-55970c5a9401.gif)



### 実行コード

```

# 必要なライブラリのインストール
pip install opencv-python-headless   # OpenCV
pip install numpy                    # NumPy
pip install pillow                   # Pillow
pip install pdf2image                # PDF to image変換
pip install imageio                  # ImageIO

brew install poppler

```


``` Saliency_Map_GUI.py
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import numpy as np
from PIL import Image, ImageTk
import os
import zipfile
from pdf2image import convert_from_path
import imageio
import tempfile
from datetime import datetime

class SaliencyApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Saliency Map Generator")
        
        # メインフレーム
        self.main_frame = ttk.Frame(root, padding="10")
        self.main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # プレビューキャンバス
        self.canvas = tk.Canvas(self.main_frame, width=600, height=400)
        self.canvas.grid(row=0, column=0, columnspan=3, pady=5)
        
        # ボタン
        ttk.Button(self.main_frame, text="画像を処理", command=self.process_image).grid(row=1, column=0, pady=5, padx=5)
        ttk.Button(self.main_frame, text="動画を処理", command=self.process_video).grid(row=1, column=1, pady=5, padx=5)
        ttk.Button(self.main_frame, text="PDFを処理", command=self.process_pdf).grid(row=1, column=2, pady=5, padx=5)
        
        # ステータスバー
        self.status_var = tk.StringVar()
        self.status_var.set("準備完了")
        ttk.Label(self.main_frame, textvariable=self.status_var).grid(row=2, column=0, columnspan=3, pady=5)
        
        # 一時ファイル用のディレクトリを作成
        self.temp_dir = tempfile.mkdtemp()

    def __del__(self):
        # 終了時に一時ディレクトリを削除
        try:
            import shutil
            shutil.rmtree(self.temp_dir)
        except:
            pass

    def __saliency(self, src):
        saliency = cv2.saliency.StaticSaliencySpectralResidual_create()
        (success, saliency_map) = saliency.computeSaliency(src)
        if not success:
            return (False, None)
        saliency_map = (saliency_map * 255).astype("uint8")
        heatmap = cv2.applyColorMap(saliency_map, cv2.COLORMAP_JET)
        weight = cv2.addWeighted(src, 0.7, heatmap, 0.5, 1.0)
        return (True, weight)

    def update_preview(self, cv_image):
        # OpenCV画像をPIL形式に変換
        image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB)
        image = Image.fromarray(image)
        
        # キャンバスのサイズに合わせてリサイズ
        canvas_width = self.canvas.winfo_width()
        canvas_height = self.canvas.winfo_height()
        image.thumbnail((canvas_width, canvas_height), Image.Resampling.LANCZOS)
        
        # PhotoImageに変換してキャンバスに表示
        photo = ImageTk.PhotoImage(image)
        self.canvas.create_image(canvas_width//2, canvas_height//2, image=photo, anchor="center")
        self.canvas.image = photo

    def create_zip(self, files, zip_name):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        zip_path = filedialog.asksaveasfilename(
            defaultextension=".zip",
            initialfile=f"{zip_name}_{timestamp}.zip",
            filetypes=[("ZIP files", "*.zip")]
        )
        if not zip_path:
            return None
            
        with zipfile.ZipFile(zip_path, 'w') as zipf:
            for file in files:
                if os.path.exists(file):
                    zipf.write(file, os.path.basename(file))
                    os.remove(file)  # 一時ファイルを削除
        
        return zip_path

    def process_image(self):
        file_paths = filedialog.askopenfilenames(
            filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.gif")]
        )
        if not file_paths:
            return
            
        processed_files = []
        for file_path in file_paths:
            self.status_var.set(f"画像を処理中... {os.path.basename(file_path)}")
            self.root.update()
            
            try:
                # 画像を読み込み
                image = cv2.imread(file_path)
                if image is None:
                    raise ValueError(f"画像の読み込みに失敗しました: {file_path}")
                
                # サリエンシーマップを適用
                success, result = self.__saliency(image)
                if not success:
                    raise ValueError(f"サリエンシーマップの生成に失敗しました: {file_path}")
                
                # プレビューを更新
                self.update_preview(result)
                
                # 一時ファイルとして保存
                temp_file = os.path.join(self.temp_dir, f"saliency_{os.path.basename(file_path)}")
                cv2.imwrite(temp_file, result)
                processed_files.append(temp_file)
                
            except Exception as e:
                messagebox.showwarning("警告", str(e))
        
        if processed_files:
            zip_path = self.create_zip(processed_files, "image_results")
            if zip_path:
                self.status_var.set(f"保存完了: {os.path.basename(zip_path)}")
            else:
                self.status_var.set("保存がキャンセルされました")
        else:
            self.status_var.set("処理可能な画像がありませんでした")
            
        self.root.update()

    def process_video(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("Video files", "*.mp4 *.avi *.mov")]
        )
        if not file_path:
            return
            
        self.status_var.set("動画を処理中...")
        self.root.update()
        
        try:
            # 動画を読み込み
            video = cv2.VideoCapture(file_path)
            if not video.isOpened():
                raise ValueError("動画の読み込みに失敗しました")
            
            # 一時ファイルとして保存
            temp_file = os.path.join(self.temp_dir, f"saliency_{os.path.basename(file_path)}")
            
            # 出力用のビデオライターを準備
            fourcc = cv2.VideoWriter_fourcc(*'mp4v')
            fps = video.get(cv2.CAP_PROP_FPS)
            width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
            height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
            writer = cv2.VideoWriter(temp_file, fourcc, fps, (width, height))
            
            frame_count = 0
            while True:
                ret, frame = video.read()
                if not ret:
                    break
                    
                # サリエンシーマップを適用
                success, result = self.__saliency(frame)
                if success:
                    writer.write(result)
                    if frame_count % 30 == 0:  # 30フレームごとにプレビューを更新
                        self.update_preview(result)
                        self.status_var.set(f"フレーム {frame_count} を処理中...")
                        self.root.update()
                frame_count += 1
            
            video.release()
            writer.release()
            
            # ZIPファイルとして保存
            zip_path = self.create_zip([temp_file], "video_results")
            if zip_path:
                self.status_var.set(f"保存完了: {os.path.basename(zip_path)}")
            else:
                self.status_var.set("保存がキャンセルされました")
            
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            self.status_var.set("エラーが発生しました")
            
        self.root.update()

    def process_pdf(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("PDF files", "*.pdf")]
        )
        if not file_path:
            return
            
        self.status_var.set("PDFを処理中...")
        self.root.update()
        
        try:
            # PDFを画像に変換
            pages = convert_from_path(file_path)
            processed_files = []
            
            for i, page in enumerate(pages):
                # PIL ImageをOpenCV形式に変換
                page_cv = cv2.cvtColor(np.array(page), cv2.COLOR_RGB2BGR)
                
                # サリエンシーマップを適用
                success, result = self.__saliency(page_cv)
                if success:
                    # プレビューを更新
                    self.update_preview(result)
                    self.status_var.set(f"ページ {i+1}/{len(pages)} を処理中...")
                    self.root.update()
                    
                    # 一時ファイルとして保存
                    temp_file = os.path.join(self.temp_dir, f"saliency_page_{i+1}.jpg")
                    cv2.imwrite(temp_file, result)
                    processed_files.append(temp_file)
            
            if processed_files:
                zip_path = self.create_zip(processed_files, "pdf_results")
                if zip_path:
                    self.status_var.set(f"保存完了: {os.path.basename(zip_path)}")
                else:
                    self.status_var.set("保存がキャンセルされました")
            else:
                self.status_var.set("処理可能なページがありませんでした")
            
        except Exception as e:
            messagebox.showerror("エラー", str(e))
            self.status_var.set("エラーが発生しました")
            
        self.root.update()

def main():
    root = tk.Tk()
    app = SaliencyApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()

```