コンテンツへスキップ

GeFroceのGPUアップグレード検討

せっかくなので、メモですが記事として残します。

現在、MSI GTX 960 2GD5T OCV2 (GeForce GTX960 2GB OC)  を利用しております。
2GBのメモリ容量となります。2万5千円で購入しました。

ディープラーニング(Deep Learning)深層学習、ニューラルネットワーク(ディープニューラルネットワーク、Deep Neural Network: DNN)による機械学習向けにGeForceのアップグレードを検討。

  • YOLO, DarknetなどDNN実施時のGPUメモリ不足(out of memory)回避、メモリ不足回避の学習回数増加による学習時間増加
  • 画像関係のDNNでは、GPUメモリ不足により、満足に学習自体実行が出来ないケースあり
  • 趣味の範囲なので、過剰な投資は出来ない(個人的には2−3万円以内)
  • PCで基本的にゲームなどは実施しない、モニタもHDMI接続1台
  • メモリ容量が増えるが目的

グラフィックカードのメーカーですが、NVIDIAかAMDとなり、それぞれNVIDIAからGeForce、AMDからRadeonがリリースされています。
各種ツールとの親和性を考えると、GeForce一択となりました。(自身でコンパイル時の修正などが出来るスキルが有るならば、魅力的なRadeonもありと思います)

NVIDIAのGeForce RTX 20XXシリーズ(2080 Ti、2080、2070、2060)やGeForce GTX 16XXシリーズ(1660 Ti、1660)が現行モデルとなります。

予算オーバーのRTX20シリーズですが、深層学習用のテンソルコア(Tensor Core)搭載となります。4×4行列の積和算を4つ並列に行う事が出来るようになり、Tensor Coreを使えば、CNNの畳み込み高速化、メモリ転送の効率化などにより、前世代の Pascal GPU と比べて、学習(トレーニング)速度が4 倍になるようです。
Volta Tensor コア GPU が AI パフォーマンスの新記録を達成

消費電力も、現在のパソコンケースの電源サイズや電気料金にも影響するので、一応比較軸に入れます。消費電力とTDP:Thermal Design Power(熱設計電力)は混同されて利用されています。TDPは、電源および冷却に関する指標を示す数字となります。

NVIDIAのGPUコアは「CUDA(クーダ)コア」と呼ばれます。一般的には「シェイダープロセッサ(Shader Processor)」、「ストリームプロセッサ(Stream Processor)」などと呼びます。

GPU導入候補比較表

価格帯を考え、メモリサイズ6GB導入で検討しました。
メモリサイズ2GB部分のみが、(現在利用している)GTX960の弱点で、非常にコストパフォーマンスが良い機種であると再確認も出来ました。

GPUGeForce GTX 960GeForce RTX 2060GeForce GTX1660TiGeForce GTX1660GeForce GTX1060
価格イメージ
2019/08
当時購入金額
25,000
42,00036,00028,00026,000
メモリサイズ2GB6GB6GB6GB6GB
メモリ規格GDDR5GDDR6GDDR6GDDDR5GDDR5
CUDA(SP数)10241920153614081280
消費電力120W160W120W130W120W
サイズ230 x 111 x 38175x126x43 mm178x126x41 mm178x126x41 mm175x115x38 mm

個人深層学習向けGPU比較結果

  1. 価格的にGTX1660(現行モデル)とGTX1060を最終的に検討。価格差も誤差の範囲であり、GTX1660が購入の最有力候補
  2. 私は、予算外なのですが、想定以上に価格がやすかったので、あえて明記。
    予算的に可能ならば、GeForce RTX 2060が絶対におすすめです。
GeForce RTX 2060 AERO ITX 6G OC MSI PCI Express 3.0 x16対応 グラフィックスボードMSI GeForce RTX 2060 AERO ITX 6G OC

2019年08月06日 めざましじゃんけん結果

個人の実験的な試行内容であり、めざましじゃんけんの結果を保証したり、全ての結果が記載を保証するものではありません。

2019年08月06日のめざましじゃんけんの結果をベストエフォートで公開します。

Goo(グー) Choki(チョキ) Pa(パー)

めざましテレビ|めざましじゃんけんーフジテレビ

めざましじゃんけん結果
回次結果対戦相手
8月06日
(火曜日)
【4戦目】07時58分生じゃんけん!梶裕貴さん
【3戦目】07時35分指原莉乃さん
【2戦目】06時58分三浦春馬さん
【1戦目】05時58分まちかどじゃんけん

初期検討時のシステム構想

機械学習どころか、プログラム言語Pythonも全く初めて、Raspberry PiやWEBカメラなどインフラ関係も知識なし。

めざましじゃんけん結果

WEBからの情報などで、こんな感じにしたいと思い描いたのが以下のシステム構想。

一般的なGeForce(GeForce GTX 960)ビデオカード搭載のWindowsメインマシン(Intel Core i7-6700 CPU)で機械学習・ニューラルネットワークを用いた学習モデルの作成。作成した学習モデルをRaspberry Pi 3
Model B+ へ移動し、Raspberry Piで画像検出を実施。

Raspberry Piでの画像検出結果により、じゃんけん結果をDBへ蓄積。蓄積したDBデータより、めざましじゃんけんの結果をホームページと速報としてTwitterで公開。

当初画像認識と画像検出の違いも分かっておらず、書籍などで紹介されていた画像認識を駆使したシステムを構築しようとしておりました。画像認識を試した時点ではRaspberry Piで処理可能に感じました。
画像認識・・・対象がアップ(画像一面)になっている画像の判定を行う
画像検出・・・対象が写り込んでいる画像より、対象を画像検出する

第一は、技術習得が1番のモチベーションであった、予期しないカメラのズレなどを考えても、画像検出のほうがシステムの安定稼働を望めるので、画像検出実装へ方針転換しました。実際には、画像認識は1日で実装出来たのですが、うまく動作せず、画像認識対象部分の画像切り取りなど、決め打ちの方式しか思いつかず、そうそうに画像検出に方針転換しました。他にも転用出来る素晴らしい技術に出会えたと思います。(Darknet、YOLO、Open CV、Python、DNN、ニューラルネットワーク)

V1での妥協と実装方式

本当は、リアルタイムでの画像検出を実装する予定でした。時間指定で動作するシステムよりもテレビさえ動いていれば、じゃんけんのスタートを自動検知し、結果を漏れなく収集するシステムを構想しました。
しかし、Raspberry Piの処理速度・信頼性より断念しました。画像検出中のRaspberry Piの発熱量など。
よって、V1ではめざましじゃんけん結果の判定の画像検出は、メインのWindowsマシンで実行しております。
めざましじゃんけんのみに特化して考えると、OpenCVの画像処理を駆使すれば軽くて高速なフィルタ作成も可能かなと考えています。

V1機能一覧と実装

機能名実装
画像検出フレームワーク準備学習データ収集Raspberry PiとWEBカメラで、めざましじゃんけん実施時の画面キャプチャー取得
学習データ整理メインのWindowsマシンで実施。
LabelImgで学習したい内容のラベル登録
学習メインのWindowsマシンでDarknetを用いて実施。
システム起動TV起動Raspberry PiよりNature Remo経由で実施。
TVチャネル変更Raspberry PiよりテレビREGZAのWEB APIを用いて実施。
じゃんけん時の「青」「赤」「緑」ボタン操作もRaspberry PiよりWEB APIを用いて実施。
メインマシン起動メインマシン(Windows)の起動をRaspberry Piより実施
画像検出画像蓄積Raspberry PiのWEBカメラを用いて目覚ましテレビの画像をキャプチャー
画像移動Raspberry PiでキャプチャーしたデータをWindowsのメインマインに移動
画像検出Windowsマシンで画像検出を実施。
検出結果結果をDB登録Windowsマシンより画像検出結果をSynology NASのSQL DBへ登録
情報発信WEBコンテンツSQL DB情報よりWEBコンテンツの更新。Synologyで実施。
Twitter発信WEBコンテンツ更新と同じタイミングでTwitter発信を実施。処理はSynologyで実施。

2019年08月05日 めざましじゃんけん結果

個人の実験的な試行内容であり、めざましじゃんけんの結果を保証したり、全ての結果が記載を保証するものではありません。

2019年08月05日のめざましじゃんけんの結果をベストエフォートで公開します。

Goo(グー) Choki(チョキ) Pa(パー)

めざましテレビ|めざましじゃんけんーフジテレビ

めざましじゃんけん結果
回次結果対戦相手
8月05日
(月曜日)
【4戦目】07時58分山里亮太さん
【3戦目】07時35分尾崎世界観さん
【2戦目】06時58分戸次重幸さん
【1戦目】05時58分まちかどじゃんけん

Raspberry Piからのメール送信

Raspberry Pi 3 Model B+の利用を開始すると、Raspberry Piの各種モジュールへ通知先のメールアドレスを設定することがあります。
パッケージのアップデートやログのローテーション処理時のエラーなどもメール通知することが可能です。

今回は、Raspberry Piへ「mailutils」を導入し、以前に構築したSynology SMTPを経由し、e-mailを送信できる環境の構築を行います。
SynologyでSMTP(メールサーバ)構築

「mailutils」「ssmtp」インストール

$ sudo apt-get install -y mailutils
$ sudo apt-get install ssmtp

ssmtp.confを編集。

$ vi /etc/ssmtp/ssmtp.conf
#
# Config file for sSMTP sendmail
#
# The person who gets all mail for userids < 1000
# Make this empty to disable rewriting.
root=postmaster

# The place where the mail goes. The actual machine name is required no
# MX records are consulted. Commonly mailhosts are named mail.domain.com
mailhub=@SynologyIP@

# Where will the mail seem to come from?
#rewriteDomain=

# The full hostname
hostname=raspberrypi.miki-ie.com

# Are users allowed to set their own From: address?
# YES - Allow the user to specify their own From: address
# NO - Use the system generated From: address
#FromLineOverride=YES

メール送信テスト

@raspberrypi:~ $ mail -s test aaaa@gmail.com -aFrom:miki@192.168.0.108
Cc:
Null message body; hope that's ok

送信は、「Ctrl + D」コントロール + Dで送信します。

参考にさせて頂いたサイト

@raspberrypi:~ $ sudo apt-get install -y mailutils
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
rpi.gpio-common
これを削除するには 'sudo apt autoremove' を利用してください。
以下の追加パッケージがインストールされます:
exim4-base exim4-config exim4-daemon-light guile-2.2-libs libgc1c2
libgnutls-dane0 libgsasl7 libkyotocabinet16v5 liblzo2-2 libmailutils5
libntlm0 libunbound8 mailutils-common
提案パッケージ:
exim4-doc-html | exim4-doc-info eximon4 spf-tools-perl swaks dns-root-data
mailutils-mh mailutils-doc
以下のパッケージが新たにインストールされます:
exim4-base exim4-config exim4-daemon-light guile-2.2-libs libgc1c2
libgnutls-dane0 libgsasl7 libkyotocabinet16v5 liblzo2-2 libmailutils5
libntlm0 libunbound8 mailutils mailutils-common
アップグレード: 0 個、新規インストール: 14 個、削除: 0 個、保留: 0 個。
10.4 MB のアーカイブを取得する必要があります。
この操作後に追加で 53.4 MB のディスク容量が消費されます。
取得:1 http://raspbian.raspberrypi.org/raspbian buster/main armhf libgc1c2 armhf 1:7.6.4-0.4 [212 kB]
取得:2 http://raspbian.raspberrypi.org/raspbian buster/main armhf exim4-config all 4.92-8 [323 kB]
取得:3 http://raspbian.raspberrypi.org/raspbian buster/main armhf exim4-base armhf 4.92-8 [1,131 kB]
取得:4 http://raspbian.raspberrypi.org/raspbian buster/main armhf libunbound8 armhf 1.9.0-2 [403 kB]
取得:5 http://raspbian.raspberrypi.org/raspbian buster/main armhf libgnutls-dane0 armhf 3.6.7-4 [314 kB]
取得:6 http://raspbian.raspberrypi.org/raspbian buster/main armhf exim4-daemon-light armhf 4.92-8 [492 kB]
取得:7 http://raspbian.raspberrypi.org/raspbian buster/main armhf guile-2.2-libs armhf 2.2.4+1-2 [4,914 kB]
取得:8 http://raspbian.raspberrypi.org/raspbian buster/main armhf libntlm0 armhf 1.5-1 [21.3 kB]
取得:9 http://raspbian.raspberrypi.org/raspbian buster/main armhf libgsasl7 armhf 1.8.0-8+b1 [196 kB]
取得:10 http://raspbian.raspberrypi.org/raspbian buster/main armhf liblzo2-2 armhf 2.10-0.1 [48.4 kB]
取得:11 http://raspbian.raspberrypi.org/raspbian buster/main armhf libkyotocabinet16v5 armhf 1.2.76-4.2+rpi1 [280 kB]
取得:12 http://raspbian.raspberrypi.org/raspbian buster/main armhf mailutils-common all 1:3.5-3 [689 kB]
取得:13 http://raspbian.raspberrypi.org/raspbian buster/main armhf libmailutils5 armhf 1:3.5-3 [810 kB]
取得:14 http://raspbian.raspberrypi.org/raspbian buster/main armhf mailutils armhf 1:3.5-3 [566 kB]
10.4 MB を 36秒 で取得しました (289 kB/s)
パッケージを事前設定しています ...
以前に未選択のパッケージ libgc1c2:armhf を選択しています。
(データベースを読み込んでいます ... 現在 143081 個のファイルとディレクトリがインストールされています。)
.../00-libgc1c2_1%3a7.6.4-0.4_armhf.deb を展開する準備をしています ...
libgc1c2:armhf (1:7.6.4-0.4) を展開しています...
以前に未選択のパッケージ exim4-config を選択しています。
.../01-exim4-config_4.92-8_all.deb を展開する準備をしています ...
exim4-config (4.92-8) を展開しています...
以前に未選択のパッケージ exim4-base を選択しています。
.../02-exim4-base_4.92-8_armhf.deb を展開する準備をしています ...
exim4-base (4.92-8) を展開しています...
以前に未選択のパッケージ libunbound8:armhf を選択しています。
.../03-libunbound8_1.9.0-2_armhf.deb を展開する準備をしています ...
libunbound8:armhf (1.9.0-2) を展開しています...
以前に未選択のパッケージ libgnutls-dane0:armhf を選択しています。
.../04-libgnutls-dane0_3.6.7-4_armhf.deb を展開する準備をしています ...
libgnutls-dane0:armhf (3.6.7-4) を展開しています...
以前に未選択のパッケージ exim4-daemon-light を選択しています。
.../05-exim4-daemon-light_4.92-8_armhf.deb を展開する準備をしています ...
exim4-daemon-light (4.92-8) を展開しています...
以前に未選択のパッケージ guile-2.2-libs:armhf を選択しています。
.../06-guile-2.2-libs_2.2.4+1-2_armhf.deb を展開する準備をしています ...
guile-2.2-libs:armhf (2.2.4+1-2) を展開しています...
以前に未選択のパッケージ libntlm0:armhf を選択しています。
.../07-libntlm0_1.5-1_armhf.deb を展開する準備をしています ...
libntlm0:armhf (1.5-1) を展開しています...
以前に未選択のパッケージ libgsasl7 を選択しています。
.../08-libgsasl7_1.8.0-8+b1_armhf.deb を展開する準備をしています ...
libgsasl7 (1.8.0-8+b1) を展開しています...
以前に未選択のパッケージ liblzo2-2:armhf を選択しています。
.../09-liblzo2-2_2.10-0.1_armhf.deb を展開する準備をしています ...
liblzo2-2:armhf (2.10-0.1) を展開しています...
以前に未選択のパッケージ libkyotocabinet16v5:armhf を選択しています。
.../10-libkyotocabinet16v5_1.2.76-4.2+rpi1_armhf.deb を展開する準備をしています ...
libkyotocabinet16v5:armhf (1.2.76-4.2+rpi1) を展開しています...
以前に未選択のパッケージ mailutils-common を選択しています。
.../11-mailutils-common_1%3a3.5-3_all.deb を展開する準備をしています ...
mailutils-common (1:3.5-3) を展開しています...
以前に未選択のパッケージ libmailutils5:armhf を選択しています。
.../12-libmailutils5_1%3a3.5-3_armhf.deb を展開する準備をしています ...
libmailutils5:armhf (1:3.5-3) を展開しています...
以前に未選択のパッケージ mailutils を選択しています。
.../13-mailutils_1%3a3.5-3_armhf.deb を展開する準備をしています ...
mailutils (1:3.5-3) を展開しています...
libgc1c2:armhf (1:7.6.4-0.4) を設定しています ...
liblzo2-2:armhf (2.10-0.1) を設定しています ...
libunbound8:armhf (1.9.0-2) を設定しています ...
libntlm0:armhf (1.5-1) を設定しています ...
mailutils-common (1:3.5-3) を設定しています ...
exim4-config (4.92-8) を設定しています ...
Adding system-user for exim (v4)
guile-2.2-libs:armhf (2.2.4+1-2) を設定しています ...
libgnutls-dane0:armhf (3.6.7-4) を設定しています ...
exim4-base (4.92-8) を設定しています ...
exim: DB upgrade, deleting hints-db
libkyotocabinet16v5:armhf (1.2.76-4.2+rpi1) を設定しています ...
libgsasl7 (1.8.0-8+b1) を設定しています ...
exim4-daemon-light (4.92-8) を設定しています ...
Initializing GnuTLS DH parameter file
libmailutils5:armhf (1:3.5-3) を設定しています ...
mailutils (1:3.5-3) を設定しています ...
update-alternatives: /usr/bin/frm (frm) を提供するために自動モードで /usr/bin/frm.mailutils を使います
update-alternatives: /usr/bin/from (from) を提供するために自動モードで /usr/bin/from.mailutils を使います
update-alternatives: /usr/bin/messages (messages) を提供するために自動モードで /usr/bin/messages.mailutils を使います
update-alternatives: /usr/bin/movemail (movemail) を提供するために自動モードで /usr/bin/movemail.mailutils を使います
update-alternatives: /usr/bin/readmsg (readmsg) を提供するために自動モードで /usr/bin/readmsg.mailutils を使います
update-alternatives: /usr/bin/dotlock (dotlock) を提供するために自動モードで /usr/bin/dotlock.mailutils を使います
update-alternatives: /usr/bin/mailx (mailx) を提供するために自動モードで /usr/bin/mail.mailutils を使います
systemd (241-5+rpi1) のトリガを処理しています ...
man-db (2.8.5-2) のトリガを処理しています ...
libc-bin (2.28-10+rpi1) のトリガを処理しています ...

@raspberrypi:~ $ sudo apt-get install ssmtp
パッケージリストを読み込んでいます... 完了
依存関係ツリーを作成しています
状態情報を読み取っています... 完了
以下のパッケージが自動でインストールされましたが、もう必要とされていません:
libgnutls-dane0 libunbound8 rpi.gpio-common
これを削除するには 'sudo apt autoremove' を利用してください。
以下の追加パッケージがインストールされます:
libgnutls-openssl27
以下のパッケージは「削除」されます:
exim4-base exim4-config exim4-daemon-light
以下のパッケージが新たにインストールされます:
libgnutls-openssl27 ssmtp
アップグレード: 0 個、新規インストール: 2 個、削除: 3 個、保留: 0 個。
368 kB のアーカイブを取得する必要があります。
この操作後に 3,377 kB のディスク容量が解放されます。
続行しますか? [Y/n] y
取得:1 http://raspbian.raspberrypi.org/raspbian buster/main armhf libgnutls-openssl27 armhf 3.6.7-4 [314 kB]
取得:2 http://raspbian.raspberrypi.org/raspbian buster/main armhf ssmtp armhf 2.64-8 [54.2 kB]
#
368 kB を 3秒 で取得しました (122 kB/s)
パッケージを事前設定しています ...
(データベースを読み込んでいます ... 現在 144153 個のファイルとディレクトリがインストールされています。)
exim4-daemon-light (4.92-8) を削除しています ...
exim4-base (4.92-8) を削除しています ...
exim4-config (4.92-8) を削除しています ...
以前に未選択のパッケージ libgnutls-openssl27:armhf を選択しています。
(データベースを読み込んでいます ... 現在 144019 個のファイルとディレクトリがインストールされています。)
.../libgnutls-openssl27_3.6.7-4_armhf.deb を展開する準備をしています ...
libgnutls-openssl27:armhf (3.6.7-4) を展開しています...
以前に未選択のパッケージ ssmtp を選択しています。
.../ssmtp_2.64-8_armhf.deb を展開する準備をしています ...
ssmtp (2.64-8) を展開しています...
libgnutls-openssl27:armhf (3.6.7-4) を設定しています ...
ssmtp (2.64-8) を設定しています ...
man-db (2.8.5-2) のトリガを処理しています ...
libc-bin (2.28-10+rpi1) のトリガを処理しています ...
miki@raspberrypi:~ $ sudo vi /etc/ssmtp/
revaliases ssmtp.conf
miki@raspberrypi:~ $ sudo vi /etc/ssmtp/
revaliases ssmtp.conf
miki@raspberrypi:~ $ sudo vi /etc/ssmtp/ssmtp.conf
miki@raspberrypi:~ $
miki@raspberrypi:~ $

めざましじゃんけん 画像検出システム

めざましじゃんけん結果

テレビ画像よりフジテレビ、めざましテレビ内で実施されるめざましじゃんけんの結果を蓄積します。めざましじゃんけんは、デジタル放送のコンテンツであり、B-CAS(ビーキャス)カードを用いた受信装置により、データ放送を画面表示させ、リアルタイムに参加する必要があります。番組録画しても、データ放送のコンテンツは録画することは出来ません。

  1. 環境準備(インストール)

  2. Darknet/YOLOで学習モデル準備

  3. システム実装と結果公開

  4. プログラム関連技術

本システム実装に関連した、プログラム関係のTips。

    • PHP
    • Python
  1. 【機械学習導入】ニューラルネットワークChainerフレームワーク

    • 導入方法
    • 機械学習
    • Out of Memoryへの対応
    • 学習モデルのRaspberry Piでの実行

2019年07月29日週 めざましじゃんけん結果

個人の実験的な試行内容であり、めざましじゃんけんの結果を保証したり、全ての結果が記載を保証するものではありません。

2019年07月29日週 のめざましじゃんけんの結果をベストエフォートで公開します。

Goo(グー) Choki(チョキ) Pa(パー)

めざましテレビ|めざましじゃんけんーフジテレビ

めざましじゃんけん結果
回次結果対戦相手
8月03日
(土曜日)
【2戦目】08時21分生じゃんけん!遠藤憲一さん
【1戦目】07時22分高見侑里キャスター
8月02日
(金曜日)
【4戦目】07時58分永島優美キャスター
【3戦目】07時35分横山ルリカ リポーター
【2戦目】06時58分指原莉乃さん
【1戦目】05時58分まちかどじゃんけん
8月01日
(木曜日)
【4戦目】07時58分小沢真珠さん
【3戦目】07時35分山本美月さん
【2戦目】06時58分佐藤新さん、影山拓也さん
【1戦目】05時58分まちかどじゃんけん
7月31日
(水曜日)
【4戦目】07時58分生じゃんけん!大貫勇輔さん
【3戦目】07時35分窪田正孝さん 山本舞香さん りんごちゃん
【2戦目】06時58分A.B.C-Z
【1戦目】05時58分まちかどじゃんけん
7月30日
(火曜日)
【4戦目】07時58分久慈暁子アナウンサー
【3戦目】07時35分長州力さん
【2戦目】06時58分三浦貴大さん
【1戦目】05時58分まちかどじゃんけん
7月29日
(月曜日)
【4戦目】07時58分内藤剛志さん
【3戦目】07時35分りんごちゃん
【2戦目】06時58分志田未来さん
【1戦目】05時58分まちかどじゃんけん

2019年08月03日 めざましじゃんけん結果

個人の実験的な試行内容であり、めざましじゃんけんの結果を保証したり、全ての結果が記載を保証するものではありません。

2019年08月03日のめざましじゃんけんの結果をベストエフォートで公開します。

Goo(グー) Choki(チョキ) Pa(パー)

めざましテレビ|めざましじゃんけんーフジテレビ

めざましじゃんけん結果
回次結果対戦相手
8月03日
(土曜日)
【2戦目】08時21分生じゃんけん!遠藤憲一さん
【1戦目】07時22分高見侑里キャスター

2019年08月02日 めざましじゃんけん結果

個人の実験的な試行内容であり、めざましじゃんけんの結果を保証したり、全ての結果が記載を保証するものではありません。

2019年08月02日のめざましじゃんけんの結果をベストエフォートで公開します。

Goo(グー) Choki(チョキ) Pa(パー)

めざましテレビ|めざましじゃんけんーフジテレビ

めざましじゃんけん結果
回次結果対戦相手
8月02日
(金曜日)
【4戦目】07時58分永島優美キャスター
【3戦目】07時35分横山ルリカ リポーター
【2戦目】06時58分指原莉乃さん
【1戦目】05時58分まちかどじゃんけん

メールデータのマルチバイト文字処理

SendGridのInbound Email Parse向けPHP APIを公開しましたが、受信メールアドレスにより文字化けが発生しました。
SendGrid-Inbound Email Parse Webhookでメール受信をPUSH(第2回)
PHPでメールデータ取り扱い時には、サーバー側のphp.iniなどの文字コード設定など、よく悩まされます。
SendGridでは、POSTでcharsetsデータを送付してきますが、そのcharsetを用いてmb_convert_encoding関数を用いて、エンコーディング変換しても文字化けが発生しました。

文字コード変換の便利な関数を紹介しているサイトがありましたので、紹介しておきます。これらの関数を用いて、文字化けが解消するケースもありました。
基本的に、今回のSendGridnoInbound Email Parseは、IoT向けなので、必要な部分を絞ってメール情報を利用すれば運用回避もできるのですが、文字コードの正確なでコード処理が必要な場面もあり、以下関数を利用させて頂きました。

/**
 * 日本語文字列の文字コード判定(ASCII/JIS/eucJP-win/SJIS-win/UTF-8 のみ)
 */
function detect_encoding_ja( $str )
{
    $enc = @mb_detect_encoding( $str, 'ASCII,JIS,eucJP-win,SJIS-win,UTF-8' );

    switch ( $enc ) {
    case FALSE   :
    case 'ASCII' : 
    case 'JIS'   : 
    case 'UTF-8' : break;
    case 'eucJP-win' :
        // ここで eucJP-win を検出した場合、eucJP-win として判定
        if ( @mb_detect_encoding( $str, 'SJIS-win,UTF-8,eucJP-win' ) === 'eucJP-win' ) {
            break;
        }
        $_hint = "\xbf\xfd" . $str; // "\xbf\xfd" : EUC-JP "雀"

        // EUC-JP -> UTF-8 変換時にマッピングが変更される文字を削除( ≒ ≡ ∫ など)
        mb_regex_encoding( 'EUC-JP' );
        $_hint = mb_ereg_replace( "\xad(?:\xe2|\xf5|\xf6|\xf7|\xfa|\xfb|\xfc|\xf0|\xf1|\xf2)", '', $_hint );

        $_tmp  = mb_convert_encoding( $_hint, 'UTF-8', 'eucJP-win' );
        $_tmp2 = mb_convert_encoding( $_tmp,  'eucJP-win', 'UTF-8' );
        if ( $_tmp2 === $_hint ) {

            // 例外処理( EUC-JP 以外と認識する範囲 )
            if (
                // SJIS と重なる範囲(2バイト|3バイト|iモード絵文字|1バイト文字)
                ! preg_match( '/^(?:'
                    . '[\x8E\xE0-\xE9][\x80-\xFC]|\xEA[\x80-\xA4]|'
                    . '\x8F[\xB0-\xEF][\xE0-\xEF][\x40-\x7F]|'
                    . '\xF8[\x9F-\xFC]|\xF9[\x40-\x49\x50-\x52\x55-\x57\x5B-\x5E\x72-\x7E\x80-\xB0\xB1-\xFC]|'
                    . '[\x00-\x7E]'
                    . ')+$/', $str ) && 

                // UTF-8 と重なる範囲(全角英数字|漢字|1バイト文字)
                ! preg_match( '/^(?:'
                    . '\xEF\xBC[\xA1-\xBA]|[\x00-\x7E]|'
                    . '[\xE4-\xE9][\x8E-\x8F\xA1-\xBF][\x8F\xA0-\xEF]|'
                    . '[\x00-\x7E]'
                    . ')+$/', $str )
            ) {
                // 条件式の範囲に入らなかった場合は、eucJP-win として検出
                break;
            }
            // 例外処理2(一部の頻度の多そうな熟語は eucJP-win として判定)
            // (珈琲|琥珀|瑪瑙|癇癪|碼碯|耄碌|膀胱|蒟蒻|薔薇|蜻蛉)
            if ( mb_ereg( '^(?:'
                . '\xE0\xDD\xE0\xEA|\xE0\xE8\xE0\xE1|\xE0\xF5\xE0\xEF|\xE1\xF2\xE1\xFB|'
                . '\xE2\xFB\xE2\xF5|\xE6\xCE\xE2\xF1|\xE7\xAF\xE6\xF9|\xE8\xE7\xE8\xEA|'
                . '\xE9\xAC\xE9\xAF|\xE9\xF1\xE9\xD9|[\x00-\x7E]'
                . ')+$', $str )
            ) {
                break;
            }
        }

    default :
        // ここで SJIS-win と判断された場合は、文字コードは SJIS-win として判定
        $enc = @mb_detect_encoding( $str, 'UTF-8,SJIS-win' );
        if ( $enc === 'SJIS-win' ) {
            break;
        }
        // デフォルトとして SJIS-win を設定
        $enc   = 'SJIS-win';

        $_hint = "\xe9\x9b\x80" . $str; // "\xe9\x9b\x80" : UTF-8 "雀"

        // 変換時にマッピングが変更される文字を調整
        mb_regex_encoding( 'UTF-8' );
        $_hint = mb_ereg_replace( "\xe3\x80\x9c", "\xef\xbd\x9e", $_hint );
        $_hint = mb_ereg_replace( "\xe2\x88\x92", "\xe3\x83\xbc", $_hint );
        $_hint = mb_ereg_replace( "\xe2\x80\x96", "\xe2\x88\xa5", $_hint );

        $_tmp  = mb_convert_encoding( $_hint, 'SJIS-win', 'UTF-8' );
        $_tmp2 = mb_convert_encoding( $_tmp,  'UTF-8', 'SJIS-win' );

        if ( $_tmp2 === $_hint ) {
            $enc = 'UTF-8';
        }
        // UTF-8 と SJIS 2文字が重なる範囲への対処(SJIS を優先)
        if ( preg_match( '/^(?:[\xE4-\xE9][\x80-\xBF][\x80-\x9F][\x00-\x7F])+/', $str ) ) {
            $enc = 'SJIS-win';
        }
    }
    return $enc;
}


function decode_mimeheader($str) {

	$enc_r = array(  // 変換するエンコーディング文字列を設定
		'iso-2022-jp' => 'iso-2022-jp-ms' ,
		'shift_jis'   => 'sjis-win'       ,
		'euc-jp'      => 'eucjp-win'      ,
		'utf-8'       => 'utf-8'
	);

	$str = str_replace('?==?' , "?=\n=?" , $str);  // 未改行文字対策

	$decode_str = '';
	foreach (preg_split('/\s/', $str) as $split_str) {  // SPACEで分割
		if (strlen($split_str) === 0) {
			$decode_str .= ($decode_str != '' ? ' ' : '');
			continue;
		}
		
		if (preg_match('/(.*?)=\?([^\?]+)\?([^\?]+)\?([^\?]+)\?=(.*?)$/', $split_str, $matches)) {  // =?[Charset]?[Text]?=形式の場合
			$enc_str  = strtolower($matches[2]);
			$from_enc = isset($enc_r[$enc_str]) ? $enc_r[$enc_str] : $enc_str;
			if ($from_enc != '') {
				$match_decode = null;
				switch (strtoupper($matches[3])) {
					case 'B':
						$match_decode = base64_decode($matches[4]);
						break;
					case 'Q':
						$match_decode = quoted_printable_decode($matches[4]);
						break;
				}
				if (!is_null($match_decode)) { // 変換に成功した場合のみ変換後テキストを追加し次の行へ
					$decode_str .= ($decode_str != '' ? ' ' : '') . mb_convert_encoding( $matches[1].$match_decode.$matches[5], mb_internal_encoding(), $from_enc );
					continue;
				}
			}
		}
		// 変換できなかった場合、又は =?[Charset]?[Text]?= 形式でなかった場合は、テキストのキャラクタセットを自動検出して変換する
		$decode_str .= ($decode_str != '' ? ' ' : '') . mb_convert_encoding( $split_str , mb_internal_encoding() , detect_encoding_ja( $split_str ) );
	}

	return $decode_str;
}