2019年版いろんなアイドルの平均顔を作ってみた(Python)

今更ながら、アイドルの平均顔を作ってみたくなったのでPythonで作ってみた。

先に結果を載せるとこんな感じになりました。(大量に作ったので、いい感じにできたやつを先に紹介します)
※各グループの公式サイトの画像を使用しているので、完成度にはばらつきがあります。

欅坂46

欅坂46の平均顔



乃木坂46

乃木坂46の平均顔



AKB48

AKB48の平均顔



日向坂や他の48グループ、ハロプロ、韓国アイドルなどいろんなグループの平均顔もつくったのでこの記事の一番下にまとめて載せておきました。

坂道シリーズの期生ごとの平均顔や、48のチームごとの平均顔も作っています。


目次

画像を集める

まずは、平均顔を作りたいアイドルグループの画像を探します。
平均顔を作るためにはある程度正面を向いた画像がよろしいので、公式サイトの宣材写真的なやつをとってくることにしました。

画像は自分で一個一個保存していってもいいですが、AKBとかは人数が多いのでスクレイピングしてしまったほうが楽だと思います。
私は、こんな感じで画像を一気に集めました。保存するファイル名とかは適当です。

import requests
from bs4 import BeautifulSoup

URL = 'https://www.akb48.co.jp/about/members/' #AKB48の公式サイト

images = []
soup = BeautifulSoup(requests.get(URL).content,'lxml')

for link in soup.find_all("img"):
    if link.get("src").endswith(".jpg"):
        images.append(link.get("src"))

for i, target in enumerate(images):
    try:
        resp = requests.get(target)
    except:
        continue

    with open('./akb_{}.jpg'.format(i), 'wb') as f:
        f.write(resp.content)


スクレイピングはこちらのサイトが参考になりました。

Pythonで画像スクレイピングをしよう - Qiita


画像の左右を反転する

取得した画像は、右を向いている人や左を向いている人、正面を向いている人などバラバラです。これでは平均顔がきれいに作れないので、向きを合わせるために画像を反転させます。
今回は全員が向って左向きになるように合わせました。正面の人はそのままにします。

import cv2

img_path = "./image_path/filename.jpg"
img = cv2.imread(img_path) #画像読み込み
img = cv2.flip(img, 1) #反転
cv2.imwrite(img_path, img) #上書き保存

画像を回転させ、顔がまっすぐになるようにする

すべての画像の顔の部分がしっかり重なった、きれいな平均顔を作るためには集めてきた画像を回転させて顔をまっすぐにしてやる必要があります。

画像を回転させて顔をまっすぐにする
画像を回転させて顔をまっすぐにする(乃木坂46齋藤飛鳥さん)

そのために、今回はdlibという機械学習ライブラリを使って顔器官(顔のパーツ)を検出し、左右の目の高さがそろうように画像を回転させようと思います。

参考
機械学習のライブラリ dlib - Qiita

dlibで顔を検出

まずは、dlibで顔検出を行います。
dlibのget_frontal_face_detector()により、顔の位置の矩形情報を得ることができます。

今回は、画像に写っている顔は1つだけという想定でやっていきます。

import cv2
import dlib

img_path = "./saito_asuka.jpg"

img = cv2.imread(img_path)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

detector = dlib.get_frontal_face_detector()
faces = detector(img)

for i, rect in enumerate(faces):
    cv2.rectangle(img, (rect.left(), rect.top()), (rect.right(), rect.bottom()), (255, 255, 0), thickness=2) 
    #得られた顔の位置に矩形を描画

矩形を描画した画像を表示してみるとこんな感じになっています。

顔検出した画像

このようにして、画像の顔の位置を得ることができました。

dlibで顔のパーツを検出

次に、dlibのshape_predictor()で顔器官の検出をします。
shape_predictor()は人の顔の画像を入力すると、目や口や鼻などの顔の重要なランドマークの位置を特定してくれます。

顔器官の検出を行うには、学習済みデータが必要です。
http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2

こちらを解凍して使いました。

from imutils import face_utils

predictor_path = "./shape_predictor_68_face_landmarks.dat"
predictor = dlib.shape_predictor(predictor_path)

faces = detector(img)
for i, rect in enumerate(faces):
    shape = predictor(img, rect)
    shape = face_utils.shape_to_np(shape)
    #顔のランドマークの位置に対応した68個の座標を得る

    for point in shape:
        cv2.circle(img, tuple(point), 1,  (255, 255, 0), -1)
        #得られたランドマークの座標を画像に描画

そうすると、こんな感じになります。

顔器官の検出

点の数は68個あり、それぞれの点の番号と顔のパーツが対応している。
例えば、向かって左目は36~41番目の点と対応しており、右目は42~47番目と対応している。

左右の目の位置を求める

次に、左右の目の位置を求めていきます。
先ほど得た68個を使います。

left_eye_center = np.zeros(2)
right_eye_center = np.zeros(2)
    
for left_eye in shape[36:42]: #向かって左目
    left_eye_center += left_eye
for right_eye in shape[42:48]: #右目
    right_eye_center += right_eye
        
left_eye_center = left_eye_center / 6
right_eye_center = right_eye_center / 6
#6点の座標の平均をとり、それぞれの目の中心とする

cv2.circle(img, tuple(left_eye_center.astype(np.int)), 2, (255, 255, 0), -1)
cv2.circle(img, tuple(right_eye_center.astype(np.int)), 2, (255, 255, 0), -1)
#左右それぞれの目の中心に点を打つ

こうして得られた画像はこんな感じです。

目の中心座標

左右それぞれの目のほぼ中心を求めることができています。

画像を回転させ、左右の目の高さを合わせる

左右の目の座標がわかったので、顔がどれだけ傾いているかを計算することができます。

左目が原点の座標系

上図のように向かって左目の中心座標を原点とするXY座標系を考え、右目の中心点をP(x, y)とすると顔の傾き\theta

\theta = arctan\frac{y}{x}

で得られます。

そして、左目を中心にして画像を-\theta 回転させてやれば顔の傾きはゼロになります。

import math

row, col, ch = img.shape

tan = (left_eye_center[1] - right_eye_center[1]) / (right_eye_center[0] - left_eye_center[0])
deg = math.degrees(math.atan(tan))

M = cv2.getRotationMatrix2D(tuple(left_eye_center.astype(np.int)), -deg, 1)
#2次元回転を表すアフィン変換を求める
img = cv2.warpAffine(img, M, (col, row), borderValue=(255, 255, 255))
#borderValueは領域外の色を指定

参考
画像の幾何学変換 — opencv 2.2 documentation
OpenCV - 画像座標系における回転、拡大縮小について - Pynote

こうしてできた画像がこちら

回転を施した画像


画像を回転させたことにより、しっかりと顔の傾きをなくすことができています。

画像を重ねるときの基準点を計算する

画像と画像を重ねて平均顔を作るときには、1つ目の画像の顔の部分と2つ目の画像の顔の部分がしっかりと重なるようにしなければなりません。
今回は、画像と画像の目の位置を合わせて重ねていくことにします。
先ほど求めた左目の中心点と右目の中心点の線分の中心を顔の基準点とし、そこが重なるようにすることで目の位置を合わせます。

下の図の赤い点が基準点です。

顔の基準点

eye_center = (left_eye_center + right_eye_center) / 2 
#eye_centerが基準点
#この基準点の座標は回転させる前の座標


いま、顔の傾きをなくすために画像を回転させているので、回転後の基準点の座標を計算してやる必要があります。

座標の回転

上図のように、点(x, y)を角度 \alpha 回転させた点(X, Y)は加法定理より

\begin{align}
X &= r{\rm cos}(\theta + \alpha) \\
&= r({\rm cos}\theta{\rm cos}\alpha - {\rm sin}\theta{\rm sin}\alpha)\\
&= r{\rm cos}\theta{\rm cos}\alpha - r{\rm sin}\theta{\rm sin}\alpha\\
&= x{\rm cos}\alpha - y{\rm sin}\alpha
\end{align}

\begin{align}
Y &= r{\rm sin}(\theta + \alpha) \\
&= r({\rm sin}\theta{\rm cos}\alpha + {\rm cos}\theta{\rm sin}\alpha)\\
&= r{\rm sin}\theta{\rm cos}\alpha + r{\rm cos}\theta{\rm sin}\alpha\\
&= y{\rm cos}\alpha + x{\rm sin}\alpha
\end{align}

となります。これを用いて、回転後の基準点を計算します。

rad = math.radians(-deg) #これが α (弧度法) 

x_eye_center_rotated = left_eye_center[0] + (eye_center[0] - left_eye_center[0]) * math.cos(rad) - (left_eye_center[1] - eye_center[1]) * math.sin(rad)
#回転後の基準点のx座標
y_eye_center_rotated = left_eye_center[1] - (eye_center[0] - left_eye_center[0]) * math.sin(rad) - (left_eye_center[1] - eye_center[1]) * math.cos(rad)
#回転後の基準点のy座標

cv2.circle(img, (int(x_eye_center_rotated), int(y_eye_center_rotated)), 2, (255, 0, 0), -1)

こうしてできた画像がこちら。

回転後の基準点

きちんと回転後の基準点の計算ができていますね。

複数の画像を重ね合わせ平均顔をつくる

いよいよ、画像を重ねて平均顔を作っていきます。
最終的なコードはこのようになりました。

import numpy as np
import cv2
import dlib
from imutils import face_utils
import math
import glob
from copy import copy

folder_path = "./nogizaka46/" #乃木坂の画像が詰まってるフォルダ
img_path_list = glob.glob(folder_path + "*.jpg")

detector = dlib.get_frontal_face_detector()
predictor_path = "./shape_predictor_68_face_landmarks.dat"
predictor = dlib.shape_predictor(predictor_path)

for img_num, img_path in enumerate(img_path_list):
    img = cv2.imread(img_path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    row, col, ch = img.shape

    faces = detector(img)
    for face_num, rect in enumerate(faces):
        shape = predictor(img, rect)
        shape = face_utils.shape_to_np(shape)

        left_eye_center = np.zeros(2)
        right_eye_center = np.zeros(2)

        for left_eye in shape[36:42]:
            left_eye_center += left_eye
        for right_eye in shape[42:48]:
            right_eye_center += right_eye

        left_eye_center = left_eye_center / 6
        right_eye_center = right_eye_center / 6

    #顔の大きさをそろえるときに使う
    #left_right_eye_dist = math.sqrt((left_eye_center[0] - right_eye_center[0]) ** 2 + (left_eye_center[1] - right_eye_center[1]) ** 2)

    tan = (left_eye_center[1] - right_eye_center[1]) / (right_eye_center[0] - left_eye_center[0])
    deg = math.degrees(math.atan(tan))
    rad = math.radians(-deg)
    eye_center = (left_eye_center + right_eye_center) / 2

    M = cv2.getRotationMatrix2D(tuple(left_eye_center.astype(np.int)), -deg, 1)
    img = cv2.warpAffine(img, M, (col, row), borderValue=(255, 255, 255))

    #回転中心の座標はint型になっているはずなので計算精度を上げる処理
    left_eye_center = left_eye_center.astype(np.int)  

    x_eye_center_rotated = left_eye_center[0] + (eye_center[0] - left_eye_center[0]) * math.cos(rad) - (left_eye_center[1] - eye_center[1]) * math.sin(rad)
    y_eye_center_rotated = left_eye_center[1] - (eye_center[0] - left_eye_center[0]) * math.sin(rad) - (left_eye_center[1] - eye_center[1]) * math.cos(rad)
    
    #最初の画像を基準として、その上に次々重ねていくイメージ
    if img_num == 0:
        row_original = row
        col_original = col
        x_eye_center_rotated_original = x_eye_center_rotated
        y_eye_center_rotated_original = y_eye_center_rotated
        img_original = copy(img)

        #顔の大きさをそろえるときに使う
        #left_right_eye_dist_original = left_right_eye_dist
        
    #2つ目以降の画像の処理
    else:
   #顔の大きさをそろえるときに使う
        #scale = left_right_eye_dist_original / left_right_eye_dist #originalとの左右の目の距離の比
        #img = cv2.resize(img, (int(col * scale), int(row * scale))) #originalに合わせて画像をresize
        #x_eye_center_rotated = x_eye_center_rotated * scale
        #y_eye_center_rotated = y_eye_center_rotated * scale #imgのresizeに合わせて目の中心座標も変更
        
        #1枚目の画像の基準点と2つ目以降の画像の基準点との距離
        x_eye_center_dist_from_original = x_eye_center_rotated_original - x_eye_center_rotated
        y_eye_center_dist_from_original = y_eye_center_rotated_original - y_eye_center_rotated
        
        #基準点の距離分平行移動させて基準点を合わせる
        M_shift = np.float32([[1, 0, x_eye_center_dist_from_original], [0, 1, y_eye_center_dist_from_original]])
        img = cv2.warpAffine(img, M_shift, (col_original, row_original), borderValue=(255, 255, 255))

        #すべての画像のweightが等しくなるように重ねて平均顔完成
        img_original = cv2.addWeighted(img_original, 1 - 1 / (img_num + 1), img, 1 / (img_num + 1), 0)

cv2.imwrite("./average_face/nogizaka46.jpg", cv2.cvtColor(img_original, cv2.COLOR_RGB2BGR))

所々出てくる”#顔の大きさをそろえるときに使う”という部分は、顔の大きさが違う画像が含まれるときなどに使用します。
1枚目の基準となる画像の目と目の距離と、2枚目以降の目と目の距離の比から画像を拡大縮小して顔の大きさをそろえます。

今回は公式サイトの画像を使用しているので、同グループの顔の大きさや画像の大きさがほぼ一定なので使用しませんでした。
ただし、別グループどうしの平均顔を作る際には使うといいと思います。


そんなこんなで完成した画像がこちらです。

乃木坂46の平均顔

乃木坂46の平均顔はこんな感じらしいです。しっかりと目の位置があっているのでやりたかったことはできました。


最後に

ここまで読んでいただきありがとうございました。

ここから先は、今回作成したいろんなアイドルの平均顔を紹介していきます。

私の趣味にだいぶ偏った内容になっています。
みたいグループがなかったら、上のコードで作ってみてください。


アイドル平均顔ギャラリー

TWICE

TWICEの平均顔



IZ*ONE

IZ*ONEの平均顔



坂道グループ

乃木坂46

1期生

乃木坂46 1期生の平均顔






2期生

乃木坂46 2期生の平均顔






3期生

乃木坂46 3期生の平均顔






4期生

乃木坂46 4期生の平均顔







乃木坂46の平均顔

乃木坂46の平均顔









欅坂46


1期生

欅坂46 1期生の平均顔







2期生

欅坂46 2期生の平均顔







欅坂46の平均顔

欅坂46の平均顔







日向坂46

1期生

日向坂46 1期生の平均顔






2期生

日向坂46 2期生の平均顔







日向坂46の平均顔

日向坂46の平均顔








坂道グループの平均顔

坂道グループの平均顔








48グループ

AKB48


チームA

AKB48チームAの平均顔







チームK

AKB48チームKの平均顔






チームB

AKB48チームBの平均顔






チーム4

AKB48チーム4の平均顔






チーム8


北海道・東北エリア

AKB48チーム8北海道・東北エリアの平均顔






関東エリア

AKB48チーム8関東エリアの平均顔






中部エリア

AKB48チーム8中部エリアの平均顔






関西エリア

AKB48チーム8関西エリアの平均顔







中国・四国エリア

AKB48チーム8中国・四国エリアの平均顔







九州・沖縄エリア

AKB48チーム8九州・沖縄エリアの平均顔







チーム8の平均顔

AKB48チーム8の平均顔







AKB48研究生

AKB48研究生の平均顔







AKB48の平均顔

AKB48の平均顔







SKE48


チームS

SKE48チームSの平均顔







チームK

SKE48チームKⅡの平均顔







チームE

SKE48チームEの平均顔








SKE48研究生

SKE48研究生の平均顔







SKE48の平均顔

SKE48の平均顔








NMB48


チームN

NMB48チームNの平均顔







チームM

NMB48チームMの平均顔






チームBⅡ

NMB48チームBⅡの平均顔







NMB48研究生

NMB48研究生の平均顔







NMB48の平均顔

NMB48の平均顔









HKT48


チームH

HKT48チームHの平均顔







チームK

HKT48チームKⅣの平均顔







チームTⅡ

HKT48チームTⅡの平均顔







HKT48研究生

HKT48研究生の平均顔







HKT48の平均顔

HKT48の平均顔







NGT48


1期生

NGT48 1期生の平均顔






NGT48研究生

NGT48研究生の平均顔







NGT48の平均顔

NGT48の平均顔








STU48


1期生

STU48 1期生の平均顔







STU48研究生

STU48研究生の平均顔







STU48の平均顔

STU48の平均顔








48グループの平均顔

48グループの平均顔










今回紹介したすべてのアイドルの平均顔

全体の平均顔

【めちゃ手軽】eBay輸出セラーのためのリサーチサイトを作成しました。【eBayリサーチツール】

この度、eBay輸出をしているセラーのためのリサーチサイトを作成したので紹介していきたいと思います。

早速ですが、以下がそのサイトになります。その名も"dululuttu"(ドゥルルットゥ)です。
dululuttu | ebay research tool for sellers


eBay輸出をする際、日本から出品されたものでよく売れるている商品を探すことは大切なことです。しかし、eBayサイトでAdvanced Searchなどを使って検索するのはとても面倒ですし、必ずしもよく売れる商品を見つけられるとは限りません。
特に初心者の方には、たくさんのカテゴリーの中から売れ筋の良いカテゴリーを探すことも難しいことだと思います。
そこで、dululuttuではAdvanced Searchよりも手軽に検索でき、よく売れているカテゴリーのランキングなども見れるようにしました。

また、他のリサーチサイトではサイトに登録しないと使えないものやAPIキーを取得しなければならないものなどがありますが、dululuttuではそのような作業は一切必要ないので今すぐに使い始めることができます。

dululuttuでできること

下の画像はdululuttuでできることを簡単に紹介したものです。

dululuttu_description


dululuttuでは、カテゴリーの売れ筋ランキングや、商品タイトルに含まれる単語ランキングを表示することができます。詳しい説明は、dululuttuの使い方に書いていきたいと思います。

dululuttuの使い方

1. dululuttuのサイトに行き、まずは検索オプションを指定しましょう。

search_option


一つ目の赤枠はeBayサイトの国を指定します。自分が出品しているeBayサイトを選択して下さい。
だいたいはアメリカのeBayサイトである"eBay United States"のままでいいと思います。

二つ目の赤枠の"Located in"ではどこの国から出品された商品を表示するか指定できます。
デフォルトでは"Japan"になっており、日本から出品されたもののみから検索するようになっています。

そして"Sold out in the past 〇〇 days"は、直近の何日間に売れた商品を表示するかを指定できます。
例えばこれを"7"に設定すると、7日前から今日までの間に売れた商品に検索結果が限定されます。


2. 検索オプションを指定すると、以下のような画面になると思います。

f:id:dululuttu:20190407014752p:plain


これで、eBayサイトとLocation、経過日数を指定することができました。
この後カテゴリーを指定すると、検索結果が表示されます。しかし、どのカテゴリーがよく売れているのかなんてわかりませんよね。
そんな時に、dululuttuではカテゴリーごとの商品が売れた数を取得しランキング化して表示することができるので、効果的に人気商品にたどり着くことができます!

f:id:dululuttu:20190407021007p:plain


上の画像で赤枠に囲われている部分は、カテゴリー情報を表示しています。
今、"Home"というカテゴリーにいて"Home"の子カテゴリーにあたるカテゴリー群がその下に箇条書きで表示されています。
この部分はeBayのサイトでも似たような表示になっています。

それでは、カテゴリーランキングを表示して一番商品が売れているカテゴリーを見つけてみましょう。
上の画像で青枠で囲まれている"View ranking"をクリックしてみましょう。しばらく待つと、子カテゴリー群の表示が変わると思います。

f:id:dululuttu:20190407021603p:plain


画像のように子カテゴリーの右側に数字が表示され、この数字が大きい順に並び変えられました。この数字はそのカテゴリに属する商品が、指定した検索オプション中で売れた数を表しています。
上の画像では、例えば"Collectibles"に属する商品で日本から出品されたものが、"eBay United States"で過去7日間に16443個売れたことを表しています。

この機能を使えば、人気の高いカテゴリーが一目瞭然ですね!


3. では、カテゴリーを選択してみましょう。カテゴリーランキングによると"Collectibles"が一番よく売れているようなのでこれをクリックしてみましょう。そうすると、下の画像のような表示になると思います。

f:id:dululuttu:20190407023837p:plain


サイトの右側に検索結果が表示されました。表示される商品はすべて実際に売れた商品のみとなっています。
表示された検索結果の画像かタイトルをクリックすると、eBayでのそのアイテムのページを表示することができます。しかし、その商品はもう売れてしまっているため、遷移先のページには類似商品が表示されている場合もあります。


また、検索結果の画像の下にある"Search by image"という部分をクリックするとGoogleで画像検索をすることができます。同じ商品がamazonなどで販売されていないか探して、仕入れ先をみつけましょう。


4. それでは、さらに細かくカテゴリーを選択してみましょう。もう一度"View ranking"をクリックして"Collectibles"の子カテゴリー群のランキングを表示しましょう。
この記事を書いているときは"Animation Art & Characters"というカテゴリーが一番上に来ました。このカテゴリーが一番売れているようなので、次はこのカテゴリーをクリックしてみました。すると下の画像のよう画面になりました。

f:id:dululuttu:20190407030533p:plain


様々なアニメ関係のアイテムが表示されました。アニメに詳しい人はこの中からいい感じの商品を見つけられるのかもしれませんが、多くの人は何が何だかわからないと思います。そこで、出現単語ランキングを表示して人気のアニメや売れている商品に共通する特徴を見つけましょう!


上の画像の赤枠で囲ってある"View"をクリックしてみましょう。しばらくたつと、下の画像のように単語頻度ランキングが表示されます。

f:id:dululuttu:20190407032103p:plain


このランキングは検索結果のタイトルに含まれる単語の中で、出現回数が多い順に表示しています。また、右の棒の長さで出現数を表現しています。
上の画像の例では、"japan"が一番多く出現しているということになります。これについては検索オプションでLocated in Japanを指定しているので多く出現するのは当たり前です。他にも"the"やなどのあまり意味のないものは無視してください。

もう少し下のほうまで見てみると、"Pokemon"や"bandai"、"persona"などがでてきますね。これらは人気商品を探すうえで意味のある単語です。
よく売れている商品はこれらの単語に関連のある商品であることが予想されます。

ここで、"persona"って何?と思う方もいらっしゃるのではないでしょうか。
このように知らない単語や気になる単語があった場合は、単語の左側のチェックボックスにチェックを入れて上の画像の赤枠で囲った"Add words"というボタンを押してみてください。すると、現在の検索結果の中でさらに"persona"というキーワードで検索することができます。

キーワード検索をすると、"persona"に関連がある商品が80個ほど表示され、その中にはフィギュア系の商品やキーホルダーがたくさんありました。どうやら、"persona"というのはゲームかアニメ関係の単語のようです。(一応ネットで調べたらペルソナというゲームがあるようです)

このように、ペルソナというゲームのフィギュアやキーホルダーが過去7日間に3、40個ほど売れているということがわかりました。
さらにGoogle画像検索をすると同様の商品が楽天amazonでも売られており、仕入れ先の確保もできそうです。
同じような商品を自分でも出品してみましょう!


このように、カテゴリーランキング単語頻度ランキングを使ってよく売れている商品を見つけることができました。


最後に

今後も様々な機能を追加していく予定です。「こんな機能があったら便利」とか「ここをもっとこうしてほしい」という要望がございましたら、この記事に気軽にコメントをお願いします。
また、たくさんの人にdululuttuを使っていただきたいのでよろしければSNSなどへのシェアをお願いいたします!

FFTでインターホンの音を検知する(Python)

インターホンが鳴ったかどうかをFFT高速フーリエ変換)で調べる。


フーリエ変換とは、ある信号を様々な周波数の正弦波の和として表す考え方。

フーリエ変換FFTに関してはこちらの記事が参考になった。
離散フーリエ変換 - 人工知能に関する断創録


PyAudioでマイクから録音する

まずは、PyAudioを使ってパソコンにつないだUSBマイクから音を録音した。
以下のコードで、インターホンの「ピンポーン」という音を含んだ3秒間の音声を録音し、sound_data.wavというファイルに保存した。

import pyaudio
import numpy as np
import wave

CHUNK = 1024
RATE = 44100
RECORD_TIMES = 3    #3秒間録音
data = []

file_name = "./sound_data.wav"

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
                channels = 1,   #モノラル
                input_device_index = 0,
                rate = RATE,
                frames_per_buffer = CHUNK,
                input = True,
                output = False)

for i in range(int(RATE / CHUNK * RECORD_TIMES)):
    d = np.frombuffer(stream.read(CHUNK), dtype='int16')
    data.append(d)

wf = wave.open(file_name, 'w')
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(RATE)


stream.stop_stream()
stream.close()
p.terminate()

録音した音声の波の形を見てみる

matplotlibを使ってグラフに表示してみる。

import matplotlib.pyplot as plt

wf = wave.open(file, "rb")
data = np.frombuffer(wf.readframes(wf.getnframes()), dtype='int16')
wf.close()

x = np.arange(data.shape[0]) / RATE
plt.plot(x, data)
plt.show()

するとこんなグラフがえられた。
f:id:dululuttu:20181009013334p:plain
横軸は時間、縦軸は振幅を表している。
約0.5秒くらいから「ピンポーン」が始まっている。
これを、FFTして各周波数成分ごとに分けて表示していく。

NumPyのfftパッケージを用いてFFTしていく。

fft_data = np.abs(np.fft.fft(data))    #FFTした信号の強度
freqList = np.fft.fftfreq(data.shape[0], d=1.0/RATE)    #周波数(グラフの横軸)の取得
plt.plot(freqList, fft_data)
plt.xlim(0, 5000)    #0~5000Hzまでとりあえず表示する
plt.show()

f:id:dululuttu:20181009014514p:plain
横軸は周波数(Hz)、縦軸は周波数成分の強度を表している。
グラフの2600Hzと2050Hz付近に大きなピークがみられる。おそらく、これらが「ピーン」と「ポーン」だと思われる。
確認のため、雑音も含めたインターホンの音のグラフも見てみる。
f:id:dululuttu:20181009015232p:plain
このグラフは、インターホンの音とテレビの雑音が含まれている音の波形である。
これを、FFTすると
f:id:dululuttu:20181009015353p:plain
こんな感じになった。やはり、2600Hzと2050Hz付近に大きなピークがみられる。インターホンの周波数はこの2つで間違いないと思う。

インターホンが鳴ったかどうかを判断する

今回は、インターホンが鳴ったらその時の音を3秒間保存する機能を作った。
コードは以下の通り

import pyaudio
import numpy as np
import wave
from datetime import datetime

CHUNK = 1024
RATE = 44100
l = 10 ** 7 
sound_count = 0

data1 = []
data2 = []

freqList = np.fft.fftfreq(int(1.5 * RATE / CHUNK) * CHUNK * 2, d = 1.0 / RATE)

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
                channels = 1,
                input_device_index = 0,
                rate = RATE,
                frames_per_buffer = CHUNK,
                input = True,
                output = False)

try:
    while stream.is_active():
        for i in range(int(1.5 * RATE / CHUNK)):
            d = np.frombuffer(stream.read(CHUNK), dtype='int16')
            if sound_count == 0:
                data1.append(d)

            else:
                data1.append(d)
                data2.append(d)

        if sound_count >= 1:
            if sound_count % 2 == 1:
                data = np.asarray(data1).flatten()
                fft_data = np.fft.fft(data)
                data1 = []

            else:
                data = np.asarray(data2).flatten()
                fft_data = np.fft.fft(data)
                data2 = []

            fft_abs = np.abs(fft_data)

            data2050 = fft_abs[np.where((freqList < 2150) & (freqList > 2000))]    #2050Hz付近の周波数成分
            data2600 = fft_abs[np.where((freqList < 2700) & (freqList > 2500))]    #2600Hz付近の周波数成分

            if (data2050.max() > 0.5 * l) & (data2600.max() > 1 * l):    #2050Hz付近と2600Hz付近の強度が一定以上あったとき、インターホンが鳴ったと判断

                this_time = datetime.now().strftime("%Y-%m-%d %H-%M-%S ")
                file_name = this_time + ".wav"

                wf = wave.open(file_name, 'w')
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(RATE)
                wf.writeframes(data)
                wf.close()

                print("The bell is ringing! " + this_time)
                data1 = []
                data2 = []
                sound_count = 0

        sound_count += 1

except KeyboardInterrupt:
    stream.stop_stream()
    stream.close()
    p.terminate()

tryとかexceptはよくわからないので適当。
data1とdata2に分けている理由は3秒間の録音の切れ目で「ピーン」と「ポーン」が分かれてしまい、判定できないことを防ぐため。また、datetimeでインターホンが鳴った時の時間を取得している。

Pythonでモザイクアートを作った

Pythonでモザイク写真を作った。

モザイク写真とは、たくさんの写真を組み合わせて全体で一つの画像のようにしたもの。結婚式なんかでよく目にするやつ。モザイクアートとも呼ばれる。

今回はこの画像をモザイク写真にしていく。

f:id:dululuttu:20180906032435j:plain

こんなのができた

f:id:dululuttu:20180906022240j:plain

この画像を拡大すると小さい猫の画像の集合になっていることがわかる。

f:id:dululuttu:20180906022630j:plain


画像を大量に集める

まずは、モザイク写真を作るうえでその材料となる画像を大量に集める。今回はFlicker APIを使って画像を集めた。

FlickerAPIキーを取得し、それを使って猫の画像を集めていく。
コードは以下の通り

from flickrapi import FlickrAPI
import requests
import time

key = ""
secret = ""
wait_time = 1
text = 'cat'
savedir = './cats/'

flickr = FlickrAPI(key, secret, format='parsed-json')
result = flickr.photos.search(
    text = text,
    per_page = 500,
    media = 'photos',
    sort = 'relevance',
    safe_search = 1,
    extras = 'url_q, licence'
)

photos = result['photos']

for i, photo in enumerate(photos['photo']):
    try:
        url_q = photo['url_q']
    except:
        continue
    resp = requests.get(url_q)
    with open(savedir + url_q.split('/')[-1], 'wb') as f:
        f.write(resp.content)

    time.sleep(wait_time)


参考 ↓↓
qiita.com

これで猫の画像が500枚得られた。サイズはすべて150×150になってる。また、画像を取得する際サーバーへの負荷を考えて画像の取得毎に1秒待つことにしている。

モザイク写真の作り方

モザイク写真を作るときには、モザイク化したい画像を150×150の領域に分けていく。そして、分けられた領域の1つ1つと最も似ている画像を大量にある画像群から選んでいく。
似ている画像の選び方は、ピクセルごとに差をとり二乗したものを、150×150のすべてで計算し和をとる。その数値が一番小さかった画像が、分けられた領域と一番似ている画像ということにした。

コードは以下の通り
※最初は150×150の画像を使ってモザイク写真を作っていく予定だったが、計算量が多くて時間がかかってしまったため50×50にresizeしている。

import numpy as np
import cv2
import glob

files = glob.glob('./cats/*.jpg')
size = 50

#150×150の画像を50×50にresizeして行列で表す
small_img = []
for file in files:
    image = cv2.imread(file)
    image = cv2.resize(image, (size, size))
    image = image /255
    small_img.append(image)
        
small_img = np.asarray(small_img)

pic_filename = './cat_image.jpg' #モザイク写真にしたい画像のパス
big_img = cv2.imread(pic_filename)

#big_imgのサイズが50で割り切れるように指定する。このサイズを大きくするほど鮮明なモザイク写真ができる
height = (big_img.shape[0] - big_img.shape[0] % size) * 2
width = (big_img.shape[1] - big_img.shape[1] % size) * 2

big_img = big_img / 255
big_img = np.asarray(big_img)
big_img = cv2.resize(big_img, (width, height))

for i in range(int(height / size)):
    print('%d / %d' %(i + 1, int(height / size)))
    for j in range(int(width / size)):
        cut = big_img[size * i: size * (i + 1), size * j: size * (j + 1)]
        losses = []

        #small_imgの中で小さく分けた領域に一番似ているものを探す
        for s_img in small_img:
            loss = cut - s_img
            loss = loss * loss
            loss = np.sum(loss)            
            losses.append(loss)      
             
        losses = np.asarray(losses)
        big_img[size * i: size * (i + 1), size * j: size * (j + 1)] = small_img[np.argmin(losses)]

big_img = (big_img * 255).astype(np.int64)        
cv2.imwrite('cat_mosaic.jpg', big_img)

この記事の上のほうに貼ってあるモザイク写真は2200×2900にwidthとheightを指定している。
このサイズを大きくするほど、鮮明な画像になる。次の画像は4400×5800に指定した画像である。

f:id:dululuttu:20180906030903j:plain

完成したモザイク写真を見ると、配色が似ている部分には同じ画像が連続している。
これはちょっと嫌なので、もっともっと超大量に画像を準備して分ける領域を大きめにすれば鮮明さを保ったまま、同じ画像が連続することを防げるんじゃないかと思う。

SRGANでDVDの映像を高画質化したかった(tensorflow)

SRGANを使ってDVDの高画質化を目指す。

といっても、自分でやったのは学習用の画像を用意することとtensorflowが吐き出すエラーを眺めて変顔をしていたことくらいである。(僕はよくわからんエラーと直面すると顔が変になってしまう)

SRGANとは

SRGAN(Super-Resolution Usinga Generative Adversarial Network)とは、GAN:敵対的生成ネットワーク(Generative Adversarial Network)の構造を用いて低解像度画像から高解像度画像に生成する機械学習モデルである。
低解像度の画像に処理を行い高解像度にする技術を超解像というらしい。


以下がその論文
[1609.04802v5] Photo-Realistic Single Image Super-Resolution Using a Generative Adversarial Network

以下の記事ではGANについて詳しく書かれてる。SRGANについての記述もある。日本語。
elix-tech.github.io


今回は、SRGANにDVDのキャプチャを入力し超解像していく。超解像後の画像サイズは縦横がそれぞれ4倍になる。

モデルはGitHubの公開リポジトリのものを利用した。
github.com
こちらのファイルをすべてダウンロードした。

学習用画像の準備

今回、高画質化したいDVDはライブ映像が収録されたものであるため、学習用画像の作成にもライブ映像を使うことにした。

そこで、Blu-rayのライブ映像をキャプチャしたものを学習用として用意した。
コードはこんなかんじ

import cv2

input_video = 'video.m2ts'
cap = cv2.VideoCapture(input_video)

frame_count = int(cap.get(7))
for num, i in enumerate(range(0, frame_count, 1000)):
    cap.set(1, i)
    _, frame = cap.read()
    cv2.imwrite('%d.png' %(num), frame)

参考
動画を扱う — OpenCV-Python Tutorials 1 documentation

あらかじめBlu-ray映像をvideo.m2tsとして保存してある。
ちなみに今回学習用に用意したBlu-rayディスクはこれ

何を隠そう、僕はNMB48のファンである。

これで1920×1080のライブ画像が1400枚くらい取得できた。
ここから適当にtrain用に1000枚、valid用に400枚選びそれぞれのフォルダに保存。

どうやら、これらの画像を1/4倍した画像も必要らしいので適当にresizeして保存しておいた。(たぶんなくても学習はできるっぽい)

というわけで以下のフォルダに学習用画像の準備ができた。

  • 1080_1920_train ... 1080×1920の画像1000枚
  • 1080_1920_valid ... 1080×1920の画像400枚
  • 270_480_train ... 270×480の画像1000枚
  • 270_480_valid ... 270×480の画像400枚

学習

モデルなどはGitHubのものをほぼそのまま使った。config.pyを少しいじって準備した画像を利用できるようにした。

config.TRAIN.hr_img_path = "1080_1920_train"
config.TRAIN.lr_img_path = "270_480_train"
config.VALID.hr_img_path = "1080_1920_valid"
config.VALID.lr_img_path = "270_480_valid"

これだけ。

python main.py

で学習開始したが、このままではOOMエラーがでてしまった。
batch_sizeはデフォルトで16だが、4まで減らしたらOOMがでなくなった。
僕のPCはgtx1060 6GBを使っているので、もっと高性能なGPUを積んでいるPCなら16のままいけるかも。

学習は全部終わるのに5日くらいかかった。

DVDの高画質化

ようやく今回の目的であるDVDの高画質化をする。

高画質化するDVDはこれ

NMB48 GRADUATION CONCERT ~KEI JONISHI/SHU YABUSHITA/REINA FUJIE~ [DVD]

NMB48 GRADUATION CONCERT ~KEI JONISHI/SHU YABUSHITA/REINA FUJIE~ [DVD]

dvd.VOBとしてあらかじめ保存しておく。

DVDのサイズは480×720なので、SRGANに通すと1920×2880まで大きくなるというわけである。これで、超高画質で楽しめるとわくわくしながら以下のコードを書いた

import time
import cv2
import numpy as np
import tensorflow as tf
import tensorlayer as tl
from model import SRGAN_g

checkpoint_dir = "checkpoint"

t_image = tf.placeholder('float32', [1, None, None, 3], name='input_image')
net_g = SRGAN_g(t_image, is_train=False, reuse=False)

sess = tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=False))
tl.layers.initialize_global_variables(sess)
tl.files.load_and_assign_npz(sess=sess, name=checkpoint_dir + '/g_srgan.npz', network=net_g)

input_video = 'dvd.VOB'
output_video = 'dvd_hr.avi'

cap = cv2.VideoCapture(input_video)

frame_count = int(cap.get(7))
fps = 30
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) * 4
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) * 4

fourcc = cv2.VideoWriter_fourcc(*'XVID')
vw = cv2.VideoWriter(output_video, fourcc, fps, (width, height))

while(True):
    ret, img = cap.read()
    if ret == True:
        img = (img / 127.5) - 1
        img = sess.run(net_g.outputs, {t_image: [img]})
        img = (img + 1) * 127.5
        img = img.astype(np.uint8)

        vw.write(img[0])

    else :    
        break
        
cap.release()
cv2.destroyAllWindows()

結果

たしかにサイズは大きくなっているはずだが、動画を再生しても高画質化されてるという実感は得られなかった。

原因として考えられること(他に原因がわかる方がいらしたらおしえてください!)

  • DVDを4倍にしたら1920×2880になるが、学習用の画像サイズが1080×1920なのでそれ以上大きいサイズに超解像する場合にはうまくいかないのではないか
  • 今回もちいたDVDの映像自体がすでに綺麗なので、高画質化した効果が実感しづらい
  • ディスプレイが小さいため効果が実感しづらい

ていうか、そもそも学習がうまくいっていない可能性があるので別の画像で試してみた。

f:id:dululuttu:20180829030948p:plain

左がもともと150×150の画像、右がSRGANにかけて600×600に超解像したもの。
SRGANめちゃめちゃすごいじゃん。車の光沢感がしっかりとでてる。

このように150×150などの小さい画像を超解像することはうまくいったので、学習用の画像のサイズを大きくしたらDVD映像も超解像できるかもしれないと思った。