前回はサリエンシーマップを使って画像に対する「目立ち度」を評価しましたが、ブログの読者はページをスクロールしたりするため動的に評価する方法も有効です。ここでは動的にブログのページを評価する方法の例を紹介します。
こんにちは。wat(@watlablog)です。
動画の処理を覚えたので、どんどん処理をしていきましょう!ここではブログの広告が目立つかどうかを動的に評価します!
サリエンシーマップの概要と動画に適用するメリット
サリエンシーマップの過去記事
サリエンシーとは、顕著性を意味する言葉であり、過去「Pythonでサリエンシーマップを作成!人の視線の行き場を可視化」で説明したように、画像の中で目立つ場所はどこか、を客観的に調べることができる画像処理手法です。
もし「サリエンシーとは何かよくわからない!」という方は是非上の記事を読んでみて下さい。サリエンシーについての内容の他に、Pythonでsaliency関数を使う時に必要なパッケージの記載もあります。
そして、「Pythonでブログの広告が目立つかどうか検証してみた」という記事では、サリエンシーマップをブログの広告に適用tいう1つの応用例について説明しました。
今回の記事はこれらの内容を踏まえた上でさらに一歩先へ行きます!
動画のサリエンシーを評価すればより現実の状況に近づく
過去記事は全て「ある画像に対するサリエンシー評価」を説明していますが、皆さんはブログのページを見る時に、全く動かないページを見ているのでしょうか?
スクロールしたり、ページ移動したり色々するよね?
そう、ブログの読者さんは僕も含めてページをスクロールしたり、リンクをクリックしたり、目次からジャンプしたり…とにかく人によってアクションは様々です。
目立つ場所があっても、スクロールしている最中にはその部分は移動し、また別の目立つ部分が登場する等、現実は動いています。
つまり実際に人が画面操作している最中の場面を動画にして、その動画に対してサリエンシーマップを適用すればより現実に即した評価ができるというワケです。
では早速動画に対するサリエンシー評価をする方法を紹介します!
動画に対するサリエンシー評価で広告が目立つか動的に検証する方法
準備するもの
スマホで撮影した動画
今回はiPhoneで撮影したmp4形式の動画をサンプルに使用します。
iPhoneでは、設定→コントロールセンター→コントロールをカスタマイズ→画面収録、という手順で画面収録機能をコントロールセンターに追加することができます。
この画面収録機能を使えば簡単にスマホ閲覧中の動画を保存することができます。
今回は以下の当ブログ閲覧時の動画に対してサリエンシー処理をしていきます。
まずは実行結果を確認!
コードを説明する前に、どんな処理結果になるかを確認しましょう。
以下の動画は先ほどのサンプル動画に対してサリエンシー評価を適用した結果です。画像をマップで表示しているわけではなく、動画の各場面に対して最も目立つ場所を1つの〇で示した結果にしてみました(これはお好みでしょうか?)。
結果、広告や画像、アイコンと、客観的な評価においてもこれらは目立つことがわかりました。
一部h2見出し等にも〇印が行っていたので、見出しにも目を惹く効果が期待できるということでしょうか。
あまり長い文章を詰め込み過ぎると、読者の視線が滞ってしまいそうですね。
読者の目を飽きさせないために、適宜見出しや画像、アイコンを挿入するのは有効な手段かも知れません。
Pythonコード
全コード
今回のコードはちょっと長いですが、それぞれの詳細を説明する前に、まずは全コードを載せます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
import cv2 import numpy as np # 画像の中でどこが最も明るいかを調べる関数 def brightness(img): h, w = img.shape[:2] # グレースケール画像のサイズ取得(カラーは3) x = int(w/20) # 領域の横幅 y = int(h/20) # 領域の高さ x_step = x # 領域の横方向へのずらし幅 y_step = y # 領域の縦方向へのずらし幅 x0 = 0 # 領域の初期値x成分 y0 = 0 # 領域の初期値y成分 j = 0 # 縦方向のループ指標を初期化 latest = 0 # 最新の平均輝度値 coordinate = [0, 0] # 最も明るい領域の座標値 # 縦方向の走査を行うループ while y + (j * y_step) < h: i = 0 # 横方向の走査が終わる度にiを初期化 ys = y0 + (j * y_step) # 高さ方向の始点位置を更新 yf = y + (j * y_step) # 高さ方向の終点位置を更新 # 横方向の走査をするループ while x + (i * x_step) < w: roi = img[ys:yf, x0 + (i * x_step):x + (i * x_step)] # 元画像から領域をroiで抽出 # ここからが領域に対する画像処理 # 領域毎に平均輝度を算出し、これまでの平均値と比べ大きかったらlatestを更新 ave = np.mean(roi).astype("uint8") if latest < ave: latest = ave coordinate = [i, j] else: pass img[ys:yf, x0 + (i * x_step):x + (i * x_step)] = np.full(roi.shape, ave) # ここまでが領域に対する画像処理 i = i + 1 # whileループの条件がFalse(横方向の端になる)まで、iを増分 j = j + 1 # whileループの条件がFalse(縦方向の端になる)まで、jを増分 # 最も明るい領域の中心座標を計算 px = int(coordinate[0] * x_step + (x / 2)) py = int(coordinate[1] * y_step + (y / 2)) return px, py, img # 動画を読み込み1フレームずつ画像処理をする関数 def m_slice(path, dir, step, extension): path_head = dir + '\out_' # 静止画のファイル名のヘッダー saliency = cv2.saliency.StaticSaliencyFineGrained_create() # サリエンシーディテクション用の設定 movie = cv2.VideoCapture(path) # 動画の読み込み Fs = int(movie.get(cv2.CAP_PROP_FRAME_COUNT)) # 動画の全フレーム数を計算 fps = movie.get(cv2.CAP_PROP_FPS) # 動画のFPS(フレームレート:フレーム毎秒)を取得 w = int(movie.get(cv2.CAP_PROP_FRAME_WIDTH) / 2) # 動画の横幅を取得(サイズを落とすために半分にしている) h = int(movie.get(cv2.CAP_PROP_FRAME_HEIGHT) / 2) # 動画の縦幅を取得(サイズを落とすために半分にしている) fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') # 動画保存時のfourcc設定(mp4用) video = cv2.VideoWriter('video.mp4', fourcc, int(fps / step), (w, h)) # 動画の仕様(ファイル名、fourcc, FPS, サイズ) ext_index = np.arange(0, Fs, step) # 静止画を抽出する間隔 for i in range(Fs): # フレームサイズ分のループを回す print(i) flag, frame = movie.read() # 動画から1フレーム読み込む check = i == ext_index # 現在のフレーム番号iが、抽出する指標番号と一致するかチェックする if flag == True: # フレームを取得できた時だけこの処理をする # もしi番目のフレームが静止画を抽出するものであれば、ファイル名を付けて保存する if True in check: # ファイル名は後でフォルダ内で名前でソートした時に連番になるようにする if i < 10: path_out = path_head + '0000' + str(i) + extension elif i < 100: path_out = path_head + '000' + str(i) + extension elif i < 1000: path_out = path_head + '00' + str(i) + extension elif i < 10000: path_out = path_head + '0' + str(i) + extension else: path_out = path_head + str(i) + extension # ここからサリエンシーディテクションと動画保存 bool, map = saliency.computeSaliency(frame) # frameに対してサリエンシーディテクション map = (map * 255).astype("uint8") # マップの輝度値を0-255にする px, py, img = brightness(map) # どこが最も明るいか、座標を調べる関数を実行 i_saliency = cv2.circle(frame, (px, py), 100, (255, 0, 0), 20) # 計算された座標にサークルを描画する i_saliency = cv2.resize(i_saliency, (w, h)) # 動画のサイズに画像をリサイズする video.write(i_saliency) # 動画を1フレームずつ保存する # ここまでが動画処理と保存 # i番目のフレームが静止画を抽出しないものであれば、何も処理をしない else: pass else: pass return # 関数実行:引数=(ファイル名のパス、保存先のフォルダパス、ステップ数、静止画拡張子) m_slice('sample.mp4', 'dir', 1, '.png') |
説明:画像のどの領域が最も明るいか調べる関数
まずは作成したdef brightness関数について説明します。
以下の図のようにこの関数では、サリエンシーディテクション後の画像に対して領域走査を行い、領域毎の平均輝度値を元画像にマッピングして輝度を粗くしています。粗くした画像の中で最も輝度の高い部分をlatestとして保持しておき、全ての領域走査終了後にlatestの輝度値を持つ領域の座標px, pyを返すというのが目的です。
輝度を粗くするのは、サリエンシーディテクション後そのままの画像だと、たった1ピクセルでも輝度値の高い部分があるとそこを検知してしまうため、感度を落とすためにこの処理をしています。
領域走査の詳細は「Python/OpenCVで画像内の領域を縦横に走査する方法」を参照して下さい。
説明:動画を読み込み1フレームずつ画像処理をする関数
次にm_slice関数を説明します。この関数が今回のメインですが、基本的な構造は「Python/OpenCVで動画から静止画を間引いて抽出する方法」で使用しているm_slice関数と同じです。
間引いて抽出した画像を静止画として保存していた部分を、画像処理を施してvideoという動画として再構築して行くところに違いがあります。
また、新たに動画を作成するため、fps(動画の再生速度)を取得しています。この関数はフレームを間引いて処理することができるように作っていますので、cv2.VideoWriterではfpsとstepの関係式を入れています。
動画はmp4で作っていますが、フォーマットはfourccで指定しています。fourccとは、four-character codeの略で、データフォーマットを識別する4byteの文字列の並びです。
画像処理はサリエンシーディテクションですが、サリエンシーマップを使って動画を作るのではなく、cv2.circleで元動画に円を描画するという処理にしています。
画像処理を施した画像からvideo.writeで保存していっています。
まとめ
このページではこれまで習得した画像処理や動画の取り扱い方法を使って、人がスマホでブログページをスクロールした動画ファイルの中でどこが目立つかを検出してみました。
コードの大部分は過去の記事で作成したものをほぼそのまま使っています。
以下に過去記事へのリンクを再掲しますので、是非見てみて下さい。
「Pythonでサリエンシーマップを作成!人の視線の行き場を可視化」では画像の中でどこが目立つかを計算するサリエンシーディテクションについての説明をしています。
「Pythonでブログの広告が目立つかどうか検証してみた」ではサリエンシーディテクションをブログのページに適用してみた結果とコードを載せています。但し、内容は単一の画像に対する処理のみです。
「Python/OpenCVで画像内の領域を縦横に走査する方法」では画像の中に小領域を設定して操作する方法を紹介しました。本ページで使っている平均値更新処理の部分です。
「Python/OpenCVで動画から静止画を間引いて抽出する方法」では動画ファイルの取り扱い方法を紹介しています。本ページのフレーム抽出処理の基礎です。
ついに動画に対して科学技術計算を適用できるようになりました!これは様々なことに応用できそうですね!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!
コメント