画像の分類作業を手助けしてくれるRailsアプリ(手動)を作ったよ
やったこと
3000枚超の画像をbingからスクレイピングして、顔の部分だけ切り取って保存し直すというところまできました。
あとは目視でその顔画像が誰の顔なのかを分類していく訳なのですが、これがめちゃくちゃ辛かったです。
まずは2つのフォルダを並べてドラッグ&ドロップでやっていこうと思いましたが、
顔の画像が小さすぎて誰か判別できないし、そもそもドラッグ&ドロップという作業が結構めんどくさい。
ということで、この画像の分類作業を(少しだけ)手助けしてくれるアプリをRuby on Railsで書きました。
環境構築
今回作ったアプリは、サーバ上に置いて公開するようなものではないのでローカルで環境構築しました。
Macの方は、こちらのQiitaの記事で本当に爆速で環境構築できます。
Windowsの方はMac買ってください。
Linuxの方は頑張ってください。
作ったもの
ブラウザに画像をでっかく表示させて、ボタンをクリックするだけで指定しておいたフォルダに入れてくれます。
主なコード
構成については、'home'という名前のコントローラに'show'と'update'という2つのアクションがあり、それらに処理を定義しています。
class HomeController < ApplicationController def show @keywords = { "ririan" => "伊藤理々杏", "rentan" => "岩本蓮加", "minamin" => "梅澤美波", "momochan" => "大園桃子", "kubochan" => "久保史緒里", "tamachan" => "阪口珠美", "denchan" => "佐藤楓", "renochan" => "中村麗乃", "hazukichan" => "向井葉月", "miichan" => "山下美月", "ayaty" => "吉田綾乃クリスティー", "yodachan" => "与田祐希" } @file = nil image_dirs = Dir.glob("#{Rails.root}/app/assets/images/face_images/*") until image_dirs.empty? do image_dir = image_dirs.find_all.first @file_cnt = Dir.entries(image_dir).size - 2 if @file_cnt == 0 image_dirs.delete(image_dir) Dir.rmdir(image_dir) else files = Dir.glob("#{image_dir}/*") @file = files.find_all.first break end end end def update prefix = params[:prefix] file = params[:file] extension = file.split(".")[-1] save_dir = "#{Rails.root}/app/assets/images/classified_face_images/#{prefix}_img" if prefix == "other" File.delete file flash[:notice] = "ファイルを消去しました" else if Dir.exist?(save_dir) else Dir.mkdir(save_dir) end cnt = Dir.glob(save_dir + "/*").count new_file = save_dir + "/#{prefix}-#{cnt+1}.#{extension}" FileUtils.cp file, new_file File.delete file flash[:notice] = "ファイルを分類しました" end redirect_to "/" end end
ちなみにルーティングはこんな感じです。
Rails.application.routes.draw do get '/' => "home#show" post '/' => "home#update" end
分類を終えて
手作業には変わりないのでめちゃくちゃ辛かったです。
あと顔の画像だけで誰であるかを判断するのって、人間にとっても意外と難しいんなと感じました。
人間は(たぶん)髪型、行動、仕草、振る舞いなどの様々なファクターで個人を特定しているので、そういうのも人工知能に組み込めたらヤバイなと思いました。
もしかしたらもうあるかも。
次はいよいよTensorFlow使って学習させよう。
PythonとOpenCVで写真から顔だけ切り取ってみたよ
前回の記事の続きです。
乃木坂46の3期生の顔認識Webアプリについて進捗ができました。
やったこと
bing画像検索から、一人につきおよそ300枚なので合計3000枚超の画像を集めてきました。
ここから人間が一つ一つ確認して、顔だけ正方形の形に切り取るのはこの上なく非効率なので、画像処理ライブラリのOpenCVを使っていきたいと思います。
OpenCVは、pipを使っている人なら簡単に使用できます。
$ pip install opencv-python
コードと解説
import os import cv2 def faceDetection(fname): print('input file: {}...'.format) prefix = fname.split('/')[-1].split('_')[0] num = fname.split('/')[-1].split('.')[0].split('_')[-1] extension = fname.split('.')[-1] save_dir = './face_images/{}_face'.format(prefix) if os.path.exists('{}'.format(save_dir)): pass else: os.mkdir('{}'.format(save_dir)) print('Succeed to make directory {}.'.format(save_dir)) face_cascade = cv2.CascadeClassifier('opencv/data/haarcascades/haarcascade_frontalface_alt.xml') if os.path.getsize(fname) == 0: pass else: img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, 1.3, 5) if len(faces) > 0: for i, (x, y, w, h) in enumerate(faces): face = img[y:y+h, x:x+w] save_path = '{}/{}_{}-{}.{}'.format(save_dir, prefix, num, i, extension) try: cv2.imwrite(save_path, face) except cv2.error as e: print('{} failed with OpenCV Error.'.format(save_path)) except: print('{} failed.'.format(save_path)) else: print('{} saved.'.format(save_path)) if __name__ == '__main__': prefixes = [ 'ririan', 'rentan', 'minamin', 'momochan', 'kubochan', 'tamachan', 'denchan', 'renochan', 'hazukichan', 'miichan', 'ayaty', 'yodachan' ] def find_all_files(dirname): for root, dirs, files in os.walk(dirname): for file in files: yield os.path.join(root, file) for file in find_all_files('./raw_images'): if os.path.getsize(file) == 0: pass else: faceDetection(file)
という感じで顔の部分だけトリミングした画像を取得することができました。
OpenCVのCascadeClassifierクラスのメソッド'detectMultiScale'を使用すると、画像から顔を検出した場合に返り値として四角形の左上x座標、左上y座標、幅、高さをリスト型で返します。
それらの値をfor文のイテレータとして受け取って、うまい具合に顔部分だけ抽出して画像として保存しています。
詳しくは、他の方のリンクをご覧ください。
問題点
やっていくうちにいくつか問題点が出てきたのであげておきます。誰か解決してください。
- 画像内に違う人がいる、または複数の人がいる
- そもそも顔の画像じゃない
- 横顔は検出できない、顔が傾いていてもできない
- 顔以外の場所が顔として検出されてしまう
とりあえず今から約3000枚の画像を人力で分類していこうと思います。
githubのコードです!git少しだけ慣れてきました!
github.com
Rubyでbingから画像をスクレイピングしたよ
唐突ですが、乃木坂46の3期生の顔を機械学習で学習させて未知の画像データから名前を推測するWebアプリケーションを作っています。Webから画像データを取得するスクレイピングの作業の際に、色々とハマったので備忘録的に記させていただきます。
進捗
顔認識させるために大量の画像データが必要となるので集めることになりました。
ということでbing画像検索から自動でローカルに画像を保存するスクリプトを書きました。
Googleを使わなかったのは、同じことをやろうとしていた人がGoogleよりbingの方がいいよって言っていたためです。
はじめPythonのBeautifulSoupとかScrapyとかのスクレイピング用ライブラリorフレームワークを使おうと思ったのですが、結構難しかったのでRubyで書くことにしました。似たようなスクリプトがネットにあったので参考にさせていただきました。
taremimi.hatenablog.jp
できたコードと解説
ほぼパクリになりました。
require 'uri' require 'open-uri' require 'nokogiri' require 'selenium-webdriver' class Scraper def initialize(prefix, query) @prefix = prefix @query = URI.escape(query.encode("utf-8")) @search_url = "https://www.bing.com/images/search?q=" + @query end def scrape_img driver = Selenium::WebDriver.for :chrome driver.navigate.to(@search_url) 10.times do driver.find_elements(:class, 'iusc').last.location_once_scrolled_into_view current_count = driver.find_elements(:class, 'iusc').length until current_count < driver.find_elements(:class, 'iusc').length sleep(3) end sleep(5) end elements = driver.find_elements(:class, "iusc") @array = [] elements.each do |element| @array << element.attribute("m").scan(/","murl\":"(.+)","turl":/) end @array = @array.flatten! @url_array = [] @array.each do |img| if /\.(jpg|png)$/ =~ img.to_s @url_array << URI.escape(img.to_s.force_encoding("utf-8")) end end @url_array.each_with_index do |url, i| begin if /\.(jpg)$/ =~ url filename = "#{@prefix}_#{i}.jpg" else filename = "#{@prefix}_#{i}.png" end p filename + " << " + url dirname = "#{@prefix}_img" FileUtils.mkdir_p(dirname) unless FileTest.exist?(dirname) filepath = dirname + "/" + filename open(filepath, "wb") do |f| open(url.encode("utf-8", invalid: :replace, undef: :replace)) do |data| sleep(2) f.write(data.read) end end p "できたよ" rescue p "無理でした" end end driver.quit end end if __FILE__ == $0 keywords = {"ririan" => "伊藤理々杏", "rentan" => "岩本蓮加", "minamin" => "梅澤美波", "momochan" => "大園桃子", "kubochan" => "久保史緒里", "tamachan" => "阪口珠美", "denchan" => "佐藤楓", "renochan" => "中村麗乃", "hazukichan" => "向井葉月", "miichan" => "山下美月", "ayaty" => "吉田綾乃クリスティー", "yodachan" => "与田祐希" } keywords.each do |prefix, query| p prefix p query scraper = Scraper.new(prefix, query) scraper.scrape_img end end
はじめ'Nokogiri'というスクレイピング用のgem(ライブラリ)を使用していたのですが、bing画像検索ではおそらくJavascriptによって動的にページを生成しているので'Nokogiri'では対処できなそうだ、ということで'Selenium'というgemを使うことにしました。この'Selenium'というgemは、プログラムからWebプラウザを操作し、Webサイトが正しく動作するか検証するためのツールだそうです。JavaやRubyやPythonなどいろんな言語でのサポートがあり、とても便利なのでオススメです。
Scraperクラスのインスタンスを生成するときに引数として'prefix'と'query'を指定します。
'query'では検索したいワードを指定します。'prefix'で指定した文字列がフォルダ名、ファイル名となって出力されます。
スクレイピングするとき、相手のサーバに負荷をかけないために所々でsleep関数を使っています。
sleepを使わないと短時間で無数のリクエストが相手サーバに送られてしまうので、マナーとしてsleepで1,2秒の間隔を置くらしいです。
そもそも公式のAPIを使っていない時点でマナーとしてどうなのっていう感じはある。
Selenium関連のエラーは、ドライバのバージョンによるエラーがほとんどだそうです。
僕が入れた最新バージョンのchromedriverは2.32でした。
Google Chrome使っている人は「selenium chromedriver」で検索すれば色々情報が出てきます。
qiita.com
zip形式のドライバを解凍して、$which rubyしたときに出てくるフォルダと同じ階層に入れればOKです。
僕の場合はrbenvを使用しているので、'~/.rbenv/shims'としました。
Githubにもあげてみました!pushできました!
github.com
Pythonでローレンツアトラクタをかいたよ
Pythonの練習のためにローレンツアトラクタを描きました。
ローレンツアトラクタ(Lorenz attractor)とは? >> ローレンツ方程式 - Wikipedia
カオスの教科書の一番最初に登場するやつです。めっちゃ単純な方程式なのにパラメータによってめちゃくちゃ解の挙動が変わるところが面白いです。
前にC++で描いたことがあるんですけど、PythonだとMatplotlibとかいう描画ライブラリを使えば数値計算とグラフ描画を同時にやってくれるのがとてもありがたいですね。
パラメータ、初期値でRungeKutta法を使って解いたときの結果です。
Pythonのコードです。まだCっぽい書き方になっちゃってます。
import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # const p = 10 r = 28 b = 8/3 dt = 0.01 t_0 = 0 t_1 = 50 X_0 = np.array([1, 1, 1]) # function def RungeKutta(t, X): k_1 = LorenzEquation(t, X) k_2 = LorenzEquation(t + dt/2, X + k_1*dt/2) k_3 = LorenzEquation(t + dt/2, X + k_2*dt/2) k_4 = LorenzEquation(t + dt, X + k_3*dt) X_next = X + dt/6*(k_1 + 2*k_2 + 2*k_3 + k_4) return X_next def LorenzEquation(t, X): x = X[0] y = X[1] z = X[2] return np.array([-p*x + p*y, -x*z + r*x - y, x*y - b*z]) # main process t = t_0 X = X_0 data = np.r_[X] while t < t_1: X = RungeKutta(t, X) t += dt data = np.c_[data, X] print(data) fig = plt.figure() ax = Axes3D(fig) ax.plot(data[0,:], data[1,:], data[2,:]) plt.show()
比較参考までにEigenライブラリを使ったC++のときのコードです。(Lorenzのつづりが間違ってる・・・)
#include <iostream> #include <fstream> #include <Eigen/Core> using namespace Eigen; Vector3f RungeKutta(float t, Vector3f X); Vector3f LorentzEquation(float t, Vector3f X); const float p = 10; const float r = 28; const float b = 8/3; const float dt = 0.01; const float t_0 = 0; const float t_1 = 10; const Vector3f X_0(1, 1, 1); int main() { float t = t_0; Vector3f X = X_0; std::ofstream ofs; ofs.open("lorentz_attractor.txt", std::ios::out); ofs << t << " " << X.transpose() << std::endl; do { X = RungeKutta(t, X); t += dt; ofs << t << " " << X.transpose() << std::endl; } while(t < t_1); return 0; } Vector3f RungeKutta(float t, Vector3f X) { Vector3f k_1 = LorentzEquation(t, X); Vector3f k_2 = LorentzEquation(t + dt/2, X + dt/2*k_1); Vector3f k_3 = LorentzEquation(t + dt/2, X + dt/2*k_2); Vector3f k_4 = LorentzEquation(t + dt, X + dt*k_3); Vector3f X_next = X + dt/6*(k_1 + 2*k_2 + 2*k_3 + k_4); return X_next; } Vector3f LorentzEquation(float t, Vector3f X) { float x = X[0]; float y = X[1]; float z = X[2]; Vector3f val(-p*x + p*y, -x*z + r*x - y, x*y - b*z); return val; }
今度は時刻によってパラメータを動的に変えたときの描画とか他のアトラクタも描いてみます。
attractorって日本語でなんて訳すのかな。
Webアプリ開発の本を買ったらソケットプログラミングさせられた話
情報系の大学院に行きたいなあと思っている今日この頃です。
タイトルは少々語弊がありますが、記事の内容はタイトルの通りです。ふと「Webアプリ作ろう!」と思って本を買いました。
Webサーバを作りながら学ぶ 基礎からのWebアプリケーション開発入門 (Software Design plus)
- 作者: 前橋和弥
- 出版社/メーカー: 技術評論社
- 発売日: 2016/06/07
- メディア: 大型本
- この商品を含むブログ (1件) を見る
この本ではWebアプリケーションの作り方を理解するために、Webサーバを作るとこから始めてくれます。”Webサーバを作る”といってもLinuxマシンにApacheをインストールしてという意味ではなく、ApacheのようなWebサーバのプログラムを作ることで、Webの動作原理から丁寧に解説してくれます。そのためにまず、TCP通信の基礎であるソケットプログラミングからはじめようといった流れになっています。
いやいや遠回りしすぎなんじゃと思う方もいらっしゃるでしょうが、実はそんなこともなくて、実際僕がこの本を買おうと思ったのもLINEのAPIを使って遊ぼうと思っていたらHTTPの処理あたりで詰まってしまってどうにもならなくなってしまったからです。今は素晴らしい時代で、簡単そうなフレームワークやツールを使えば単純なWebアプリケーションくらいなら誰でもすぐ作れますが、少しややこしいことをしようとすると上手くいかなくなって、根本を理解していないと自分一人では抜け出せないという状況に陥りがちです。
そうならないためにも、基礎からしっかりお勉強しておきたいところです。
今回の記事の流れはこんな感じになっています。
- TCPサーバ/クライアントを作る
- Webブラウザから、TCPサーバにアクセスしてHTTPリクエストを見る
- TCPクライアントからWebサーバにアクセスしてHTTPレスポンスを見る
- 分かったこと
- ソースコード(コメント付き)
まず実際にTCPの規約に沿ったサーバとクライアントのプログラムを作って、それらのプログラムを使ってHTTPリクエストとHTTPレスポンスでは何が吐き出されているのかを確認していきたいと思います。
TCPサーバ/クライアントを作る
Webサーバ動作の理解のためにまず、ブラウザ(クライアント)とWebサーバをつなぐネットワークについて学びます。今回はインターネットで一般的に用いられているTCPプロトコルで通信を行うサーバのプログラムとクライアントのプログラムを作ります。
プロトコルとは、TCPとはという方にはまずこちらをご一読
【初心者向けに大体わかる】TCP/IPとは?
クライアントサーバシステムについてよく知らない人は検索してみてください。
この本は基本的にJavaを使いますが、僕は馴染みのあるC言語で書きました。一応、この本にもC言語のサンプルプログラムは載っていますが、解説やコメントがあまりなかったので記事に載せることにしました。
実際に行う動作の概念図がこちらです。
TCPの通信方式は電話に例えられるように、お互いの接続を確認してから通信を行うコネクション型という通信方式です。
サーバとクライアントを介する仮想的な窓口としてソケットというものを使います。ソケットという言葉は、豆電球のカポってはめる部分を指すことが多いですが、イメージとしては「統一された規格の部品をはめ込むもの」という認識でいいと思います。詳しくはその他の文献を参照してください。
プログラムのコードはこんな感じです。まずはサーバの動きをする方です。
今回、ポート番号を8001で指定しています。他のサービスと被っていなければどんな番号でもいいんですけど、本に合わせて設定しました。
// tcp_server.c #define _POSIX_C_SOURCE 1 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <sys/uio.h> int main(int argc, char **argv) { int sock; struct sockaddr_in addr; int fd; FILE *socket_fp; FILE *file_out_fp; FILE *file_in_fp; int ch; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(8001); addr.sin_addr.s_addr = htonl(INADDR_ANY); if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0){ perror("socket_error"); return -1; } else { printf("ソケットを生成しました\n"); } if((ch = bind(sock, (struct sockaddr*)&addr, sizeof(addr))) < 0){ perror("bind_error"); return -1; } else { printf("サーバの準備ができました\n"); } if((ch = listen(sock, 5)) < 0){ perror("listen_error"); return -1; } else { printf("クライアントからの接続を待っています\n"); } if((fd = accept(sock, NULL, NULL)) < 0) { perror("accept_error"); return -1; } else { printf("クライアントを接続しました\n"); } socket_fp = fdopen(fd, "r+"); file_out_fp = fopen("server_recv.txt", "w"); while((ch = fgetc(socket_fp)) != 0) { fputc(ch, file_out_fp); } fclose(file_out_fp); printf("クライアントからのデータを受け取りました\n"); file_in_fp = fopen("server_send.txt", "r"); while((ch = fgetc(file_in_fp)) != EOF){ fputc(ch, socket_fp); } fclose(file_in_fp); fclose(socket_fp); printf("クライアントにデータを送信しました\n"); printf("通信が正常に終了しました\n"); return 0; }
クライアントから受け取るファイルでは、EOFという概念が使えないみたいなので、クライアントはファイルの末尾に0を送るようにしています。
次にクライアントの動きをする方です。
// tcp_client.c #define _POSIX_C_SOURCE 1 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <sys/uio.h> #include <netdb.h> int main(int argc, char **argv) { int sock; struct sockaddr_in addr; struct hostent *host; FILE *socket_fp; FILE *file_out_fp; FILE *file_in_fp; int ch; memset(&addr, 9, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(8001); host = gethostbyname("localhost"); memcpy(&addr.sin_addr, host->h_addr_list[0], sizeof(addr.sin_addr)); if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0){ perror("socket_error"); return -1; } else { printf("ソケットを生成しました\n"); } if(connect(sock, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0){ perror("connect_error"); return -1; } else { printf("サーバと接続されました\n"); } socket_fp = fdopen(sock, "r+"); file_in_fp = fopen("client_send.txt", "r"); while ((ch = fgetc(file_in_fp)) != EOF) { fputc(ch, socket_fp); } fclose(file_in_fp); fputc(0, socket_fp); printf("サーバにデータを送信しました\n"); file_out_fp = fopen("client_recv.txt", "w"); while ((ch = fgetc(socket_fp)) != EOF) { fputc(ch, file_out_fp); } fclose(file_out_fp); printf("サーバからのデータを受け取りました\n"); return 0; }
重要な手続きを抜き出すと下の図のような流れとなっております。クライアントの方は2ステップでいけるみたいですねー。
いやこれだけじゃ何のことかわからんぜって方のために、気持ち悪いくらいコメントついてる僕の勉強用ソースも末尾に載せておきます。
実行する前にtcp_server.cのあるフォルダにserver_send.txtという名前のファイル、tcp_client.cのあるフォルダにclient_sendという名前の送信用ファイルをそれぞれ置いておきます。ファイルがないとエラーがでます。
それから、tcp_server.cの実行中にtcp_client.cを実行すると
と、通信しているような感じが出てます。
実際にやってみると「TCPがコネクション型のプロトコルってそういう意味だったのか」とか、「今までネットワークって難しく思ってたけど案外単純」みたいな気持ちになりました。
とりあえずクライアントとサーバはこんな感じで表現します。意外とC言語でもサーバ実装とかできるんですね。そうですよね、元々はほとんどCで書かれてたんですから当然ですよね。
Webブラウザから、TCPサーバにアクセスしてHTTPリクエストを見る
私たちが実際に使っているWebブラウザは、Webサーバに対して一体どのようなリクエストを送っているのかというところを見てみます。
具体的には、先程作ったTCPサーバのプログラム実行中に普段使っているWebブラウザ(Google Chrome、IE、Firefoxなど)から
http://localhost:8001/index.html
にアクセスして、Webサーバに送られてきたserver_recv.txtの中身を見るという感じです。
送られてくるHTTPリクエストヘッダと呼ばれるファイルは、ファイルの最後が2回の改行が続くので、終端を示すためにtcp_server.cの該当部分を変更します。
HTTPでは改行コードがCR+LFという形式らしいので、少し力技っぽいですが10,13という値が2回続いたら終了するようにしました。
socket_fp = fdopen(fd, "r+"); file_out_fp = fopen("server_recv.txt", "w"); int ch2 = 0, ch3 = 0, ch4 = 0; while((ch = fgetc(socket_fp)) != EOF) { fputc(ch, file_out_fp); if(ch == 10 && ch2 == 13 && ch3 == 10 && ch4 == 13){ printf("HTTPレスポンスヘッダの終端です\n"); break; } ch4 = ch3; ch3 = ch2; ch2 = ch; } fclose(file_out_fp); printf("クライアントからのデータを受け取りました\n");
tcp_server.cを書き換えて、実行して、Webブラウザからhttp://localhost:8001/index.htmlに接続を試みると、server_recv.txtにHTTPリクエストの内容が表示されています。
GET /index.html HTTP/1.1 Host: localhost:8001 Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Linux armv7l) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.84 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Encoding: gzip, deflate, sdch, br Accept-Language: ja,en-US;q=0.8,en;q=0.6
Webブラウザがサーバに対してどういうリクエストを投げかけているかが分かりました。
逆にApacheなどのWebサーバプログラムはどういった処理をしているのか見てみます。
TCPクライアントからWebサーバにアクセスしてHTTPレスポンスを見る
先程server_recv.txtとして受け取ったファイルを、そのままclient_send.txtにコピーしてWebサーバ(Apache)に投げてみよう、ということをします。
まずserver_recv.txtでは8001となっていた箇所を、client_send.txtではHTTPデフォルトのポート番号の80に変更します。
で、クライアント側のプログラムもそれに対応する部分を変更します。また終了を表すことにしていた0もApache相手には必要ないのでコメントアウトしておきます。
addr.sin_port = htons(8001); → addr.sin_port = htons(80); fputc(0, socket_fp); → //fputc(0, socket_fp);
このプログラムを実行すると
HTTP/1.1 400 Bad Request Date: Mon, 03 Jul 2017 12:25:29 GMT Server: Apache/2.4.10 (Raspbian) Content-Length: 303 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>400 Bad Request</title> </head><body> <h1>Bad Request</h1> <p>Your browser sent a request that this server could not understand.<br /> </p> <hr> <address>Apache/2.4.10 (Raspbian) Server at 127.0.0.1 Port 80</address> </body></html>
WebサーバからのHTTPレスポンスが返されます。なんか400 Bad Requestでクライアントエラーになってますが、今回は表示させることが目的なのでスルーすることにします。
ソースコード(コメント付き)
勉強用のコメント付きコードです。いないとは思いますがやりたい人がいたら参考までに。
// tcp_server.c // まずPOSIXは、UNIXをはじめとする異なるOS実装に共通のAPIを定め、移植性の高いソフトウェア開発を簡易化することを目的としてIEEEが策定したAPI規格 // 内容は、カーネルへのC言語のインターフェイスであるシステムコールや、プロセス環境、ファイルとディレクトリ、システムデータベース、アーカイブフォーマットなど // つまりPOSIXはOSの規格 // If you define this macro to a value greater than or equal to 1, // then the functionality from the 1990 edition of the POSIX.1 standard (IEEE Standard 1003.1-1990) is made available. // つまり_POSIX_C_SOURCEを1以上と定義することでPOSIX.1に準拠したプログラムを作れる // このプログラムでは、glibcでfdopenを使うために定義されている(らしい) #define _POSIX_C_SOURCE 1 // 標準入出力ライブラリ #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <sys/uio.h> int main(int argc, char **argv) { int sock; // sockaddr_in構造体:<netinet/in.h>にある。 // 簡単にいうと、プロセスが持つソケットにプロセス間通信ができるように OS上のアドレスを割り当てるための手続き書類を作成するための構造体 // もっと簡単にいうと、Internet用のソケットのアドレスを指定したり、逆にソケットの アドレスを調べたりするときに使う // もともとsockaddr構造体という汎用的構造体があって、UNIX LOCALに特化したsockaddr_unとInternetに特化したsockaddr_inがある /* sockaddr_inの定義はこんな感じ struct sockaddr_in { u_char sin_len; // この構造体のサイズ、u_charはunsigned charの意味でその他も同様 u_char sin_family; // とりあえずAF_INET指定しておこう u_short sin_port; // マシンのポート番号 struct in_addr sin_addr; // マシンのIPアドレス(IPv4) char sin_zero[8]; // }; struct in_addr { u_int32_t s_addr; }; */ /* sin_familyはこんな感じで指定する ちなみにAF = Address Family PF = Protocol Familyの略らしい。違いは分からん。 1.AF_INET:ARPAインターネットプロトコル 2.AF_UNIX:UNIXファイルシステムドメイン 3.AF_ISO:ISO標準プロトコル 4.AF_NS:XeroxNetworkSystemsプロトコル 5.AF_IPX:NovellIPXプロトコル 6.AF_APPLETALK:AppletalkDDP 7.PF_INET:IPv4 AF_INETとほぼ同義 8.PF_INET6:IPv6 9.PF_IPX:IPX - Novell プロトコル 10.PF_NETLINK:カーネル・ユーザ・デバイス 11.PF_X25:ITU-T X.25 / ISO-8208 プロトコル 12.PF_AX25:アマチュア無線 AX.25 プロトコル 13.PF_ATMPVC:生の ATM PVC にアクセスする 14.PF_APPLETALK:アップルトーク 15.PF_PACKET:低レベルのパケットインターフェース */ struct sockaddr_in addr; // fd = file descriptor int fd; FILE *socket_fp; FILE *file_out_fp; FILE *file_in_fp; int ch; // void * memset( void *str , int chr , size_t len ):<string.h>にある // strの先頭からlenバイト分だけchrをセット // ここではaddrの長さだけ0で初期化している // 古いサイトや文献だとbzeroという関数で実装されているが、これは廃止される(された?)のであまり使わない方がいい(らしい) memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; // htonsとhtonlについて // htons = host to network short、htonl = host to network longの略 // 現在の多くのPCはリトルエンディアン方式で、ネットワークはインターネット黎明期の名残でビッグエンディアン方式が標準 // この問題を解決するための関数がhtonsとhtonlで、逆(ntohs、ntohl)もある addr.sin_port = htons(8001); addr.sin_addr.s_addr = htonl(INADDR_ANY); // htonl内に特定のアドレスを書くと、そのアドレスの要求だけを受け付ける。INADDR_ANYでどのアドレスからの要求でも受け付けるようになる // int socket(int domain, int type, int protocol); // 第1引数はプロトコルファミリと呼ばれるやつ。結局何なのかよくわかってない // 第2引数は通信方式を指定。SOCK_STREAMは順双方向のバイトストリーム、TCP/IPではこれを用いる // UDP/IPではSOCK_DGRAM、IPではSOCK_RAW。今回はTCP/IPなのでSOCK_STREAM // 第3引数は使用するプロトコルで、0を指定すると自動で設定してくれるっぽい // 成功すると新しいソケットのファイルディスクリプタを返し、失敗すると-1を返す // つまり新しいソケットを作ってくれるということらしい。できなければIPPROTO_TCPなど自分で指定する // sock = socket(AF_INET, SOCK_STREAM, 0); if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0){ perror("socket_error"); return -1; } else { printf("ソケットを生成しました\n"); } // int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // bind関数の定義的に第2引数はstruct sockaddrのポインタなので、キャストする // この関数はサーバ側で利用するIPアドレスとポート番号を利用する準備をしてる // "bind"は、"結び付ける、紐つける"といった意味があることからソケットとアドレスを紐付ける役割をしてると解釈した // ch = bind(sock, (struct sockaddr*)&addr, sizeof(addr)); if((ch = bind(sock, (struct sockaddr*)&addr, sizeof(addr))) < 0){ perror("bind_error"); return -1; } else { printf("サーバの準備ができました\n"); } // int listen(int sockfd, int backlog); // listen関数はsockfdで指定されるソケットを接続待ちソケットとして印づける // backlogは最大で何個のクライアントを待たせることができるか、という待ち行列の長さを表す。とりあえずお試しなので5くらいでいいでしょといった感じ // 成功した場合には0、失敗なら-1が返される if((ch = listen(sock, 5)) < 0){ perror("listen_error"); return -1; } else { printf("クライアントからの接続を待っています\n"); } if((fd = accept(sock, NULL, NULL)) < 0) { perror("accept_error"); return -1; } else { printf("クライアントを接続しました\n"); } socket_fp = fdopen(fd, "r+"); file_out_fp = fopen("server_recv.txt", "w"); while((ch = fgetc(socket_fp)) != 0) { fputc(ch, file_out_fp); } fclose(file_out_fp); printf("クライアントからのデータを受け取りました\n"); file_in_fp = fopen("server_send.txt", "r"); while((ch = fgetc(file_in_fp)) != EOF){ fputc(ch, socket_fp); } fclose(file_in_fp); fclose(socket_fp); printf("クライアントにデータを送信しました\n"); printf("通信が正常に終了しました\n"); return 0; }
// tcp_client.c #define _POSIX_C_SOURCE 1 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <string.h> #include <sys/uio.h> #include <netdb.h> int main(int argc, char **argv) { int sock; struct sockaddr_in addr; // hostentはマシンのIPアドレスなどの情報を調べる際に使う構造体でnetdb.hにある /* struct hostent { char *h_name; // ホストの正式名称 char **h_aliases; // 別名リスト(マシンの別名が存在すればここに入る) int h_addrtype; // ホストアドレスのタイプ (AF_INET6 など) int h_length; // アドレスの長さ char **h_addr_list; // NULL で終わるアドレスのリスト(普通0番目だけ使われる) }; */ struct hostent *host; FILE *socket_fp; FILE *file_out_fp; FILE *file_in_fp; int ch; memset(&addr, 9, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(8001); // gethostbyname(name)は、ホスト名nameに対応した構造体hostentを返す // nameには、ホスト名の他、IPv4アドレスIPv6アドレスも指定できる(らしい) host = gethostbyname("localhost"); // void *memcpy(void *buf1, const void *buf2, size_t n); // buf2の先頭からn文字分のアドレスをbuf1のアドレスにコピー // h_addr_list[0]に入ってるアドレスをsin_addrにコピーする // つまりマシンのアドレスをソケットに渡している memcpy(&addr.sin_addr, host->h_addr_list[0], sizeof(addr.sin_addr)); if((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0){ perror("socket_error"); return -1; } else { printf("ソケットを生成しました\n"); } // int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // ファイルディスクリプタsockfdが参照しているソケットをaddrで指定されたアドレスに接続する。 // addrlen 引き数は addr の大きさを示す。 if(connect(sock, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0){ perror("connect_error"); return -1; } else { printf("サーバと接続されました\n"); } socket_fp = fdopen(sock, "r+"); file_in_fp = fopen("client_send.txt", "r"); while ((ch = fgetc(file_in_fp)) != EOF) { fputc(ch, socket_fp); } fclose(file_in_fp); fputc(0, socket_fp); printf("サーバにデータを送信しました\n"); file_out_fp = fopen("client_recv.txt", "w"); while ((ch = fgetc(socket_fp)) != EOF) { fputc(ch, file_out_fp); } fclose(file_out_fp); printf("サーバからのデータを受け取りました\n"); return 0; }
AngelhackOsakaに参加して知ったこと
6/17、6/18に大阪のグランフロント大阪で行われた「AngelhackOsaka」というハッカソンイベントに参加しました。
Angelhackは世界中で行われているハッカソンで、日本では昨年(2016年)の東京に続いて今回が二度目の開催でした。
Angelhackの地方大会で優勝すると「HACKcelerator」と呼ばれるシリコンバレーで行われるスタートアップ支援プロジェクトに参加することができます。
今回、人生で初めてハッカソンに参加したので、気づいたこと、感じたこと、知ったことについて少し書きます。
その1 プログラム書けない人も結構いる
参加する前までは、眼鏡かけたゴリゴリのプラグラマやエンジニア、またはそれ志望の学生がガリガリコード書いてるみたいなイメージしかありませんでした。もちろんそういう方もたくさんおられましたが、プログラムなんて書いたことないという方が結構いて驚きました。目的も人により様々で、僕みたいにプログラミングをもっと勉強したいという人もいれば、本気で世界を変えたいと思っている起業家の卵みたいな人たち、単純ににいろんな人とつながりたい人など本当にいろんな方がいました。中でも、僕は某大手企業の研究職の方と同じチームに入ることになったのですが、普段の企業説明会などではあまり聞けないようなことまでお話が聞けて非常に参考になりました。
プログラムが書けないからハッカソンは自分には敷居が高いなと思っている方でも、必ず誰かがサポートしてくれるので、参加者の話を聞くだけといった気軽な感じでもっと参加してもらえればいいなと思います。
その2 APIプログラミングについて知る
ここからは少し技術に関して思ったことを書きます。
まずハッカソンではAPIに関する知識が求められます。APIはApplication Programming Interfaceの頭文字で、公式に提供されているWebサービスなどをプログラミングに利用することができます。例えば、アプリに地図を組み込みたいといった場合にはわざわざ自分で一から地図を作らなくとも、Google MapのAPIを利用することで大幅に開発期間を短縮し、なおかつクオリティの高いサービスを作ることができます。
次のリンクはハッカソンで用いられたプログラミング言語、およびAPIのランキング結果です。
TwitterやFacebook、Instagramなど私たちに身近なSNSのAPI、それからGooglePlayやSoundCloudのAPIなんかもよく利用されています。また人気のあるプログラミング言語ランキング上位に位置するのもWebプログラミングが得意なJavaScriptやPythonです。ハッカソンのような短い開発期間においては、既存のサービスにちょい足ししたり、組み合わせたりして新たな発想のものを作るといった考え方が好まれるようです。
なかなか学生では、プログラミングが本当に好きでもなければAPIを利用することは少ないかもしれません。ですが今はインターネットの時代なので、Webを活用できる能力が求められます。数値計算やソートアルゴリズムだけがプログラミングではありません。何よりWebは自身がよく利用しているので仕組みが分かると本当に面白いと思います。今までWebプログラミングやAPIに触れたことがないという学生の方はぜ使ってみてください。
僕もこの機会をきっかけにJavaScriptに出会うことができたので、それ関連の記事などもまた書けたらなと思います。
その3 UIデザインについて知る
UIはUser Interfaceの略で、どうすればサービスを実際に使用するユーザが操作しやすいかなどを考えてデザインされるものです。最近なんかもアプリ版Twitterのアイコンが四角から丸に変わっていて少し話題になりました。
情報系の学部なんかではアプリや製品のプロトタイプやサービスなんかを学校で作る機会が多少なりあるかと思いますが、UIについて教わったり学んだりする機会はまだあまりないのではないでしょうか。ここに学校で教わることと現実で求められることのギャップを感じました。実際、ハッカソンでも学生が創作したものとプロのデザインが加えられたものとでは、クオリティに大きな差があったように感じます。本職のデザイナーとまではいかなくとも、エンジニアでも必要最低限デザインの理解ができると、ワンランク上のエンジニアになれるのかなって思いました。
最後までお読みいただきましてありがとうございました。
どこかのハッカソン等で会う機会があればぜひお声かけください!
おまけ
最近アイドルの結婚宣言が世間を賑わせているので、アイドルに関する簡単なアルゴリズムの問題を一つご紹介します。よかったら考えてみてください。
川の一方の岸に、あなたとアイドル、そのアイドルのオタク、そのアイドルの彼氏がいます。今、川の反対側の岸に3人を送り届けなければならないという謎の状況が発生しました。川を渡るための船はあなたともう1人しか乗ることができません。ただし、アイドルが襲われる危険性があるのでオタクとアイドルを二人きりにすることはできません。また、彼氏と逃亡してしまう恐れがあるのでアイドルと彼氏を二人きりにすることもできません。無事に3人を対岸に送り届けることができるような方法は存在するでしょうか?
【PiCAST】ChromeCastっぽいものを作ってみた。
"ChromeCast"ってご存知ですか。
Chromecast - Google
https://www.google.com/intl/ja_jp/chromecast/
今の子はYouTube世代なので勿論知ってますよね。YouTubeの動画とかをTVで楽しめるやつです。ちなみに僕はでんぱ組.incのCMが好きでした。
僕も今年から晴れて大学生になったので、オシャレ大学生目指して買っちゃおうかな~と思ってGoogleで探してみたら、
え、高くね。YouTubeをTVで見るだけなのに5000円とかありえん。買うの諦めました。
作ろう。
自分でChromeCastっぽいものを作ろうという結論に達しました。必要は発明の母とはよく言ったもんです。
「RaspberryPI Chromecast」で探したら、どうやらおんなじ考えを持っていた人がいましたので、今回はその方のプログラムを使ってみることにします。
github.com
こちらのページのREADME.mdに書かれている手順に従ってやってみます。
結果から先に言うと、YouTubeのライブ配信動画はTVでキャストすることができました。
必要なもの
- Raspberry PI
- HDMIケーブル
- TV
- キャストしたい心
手順
1. GitHubからセットアップスクリプト(setup.sh)をダウンロードして実行
$ curl -OL http://raw.github.com/lanceseidman/PiCAST/master/setup.sh
$ sudo sh setup.sh
どえらい時間かかります。我が家の貧弱なWi-fi環境では4時間くらいかかりました。気長に待ちましょう。
2. PiCASTで必要なファイルをホームディレクトリにコピー
$ sudo cp -R /root/PiCAST $ sudo chown -R pi:pi /home/pi/PiCAST
3. 足りないモジュールを追加するのと環境変数のPATHを設定
$ sudo npm install -g express $ export NODE_PATH=/usr/local/lib/node_modules
"express"っていうのは、JavaScriptのサーバサイド実行環境である"Node.js"のフレームワークらしいです。
4. とりあえずPiCASTを実行してみる
$ cd /home/pi/PiCAST
$ ./picast_start.sh
picast_start.shがプロセスを開始してくれるスクリプトで、picast_stop.shがプロセスを終了してくれるスクリプトです。
5. スマホからアクセスしてみる。
GoogleChromeなどインターネットブラウザから”http://[自分のラズベリーパイのIPアドレス]:3000”にアクセスしてみる。今までの手順が正しく実行されていれば、「Welcome to PiCAST 3! in the URL, type what you want to do...」と表示されます。
とりあえずラズベリーパイをHDMI端子でテレビにつないでみます。ここで続けて”http://[自分のラズベリーパイのIPアドレス]:3000/yt-stream/[見たいYouTube動画のビデオID]”にアクセスしてみる。ビデオIDとはYouTubeの各動画のURLの末尾にある"v="より後ろの部分の英数字列です。ここで、動画はライブ動画を選ぶことに注意しましょう。正しく実行されると次のような画面になります。
6. テレビにキャストされる
さて、僕はこちらのNASAの心躍る壮大なライブ映像をキャストしてみることにしました。
今までの手順で一応動きます。こんな感じでテレビにキャストされます。
ちなみに"picast.js"の12行目あたりを
exec("livestreamer --player='mplayer -fs' https://www.youtube.com/watch?v=" + req.params.url + " best")
に変えるとフルスクリーンで実行されます。また最後の" best"を" worst"や" 480p"とすると画質を調整できます。
手順通りいったあなたはおめでとうございます。以下、この処理の内容と僕がハマった点について述べておきます。
処理の内容
PiCASTの処理についての簡単なイメージ図です。JavaScriptに明るい方は直接”picast.js”を読めばすぐに理解できるかと思いますが、スマホとラズパイの間で同期させてなんやかんやさせるみたいなカッコイイ処理ではなく、ただ単にブラウザに入力されたURLをサーバが読み込んで、サーバからラズパイに動画再生しろっていう命令をぶん投げてるだけです。
ハマったポイント
そもそもライブ配信動画にしか対応してない
CMのみたいに、でんぱ組.incの「でんでんぱっしょん」をキャストしようと試行錯誤していたわけですが、今回ラズパイ上で実行した"livestreamer"は、YouTubeのライブ配信動画しかサポートされていないので、普通の動画のビデオIDを指定するとエラー出されます。
"livestreamer"の代わりに"youtube-dl"とかいうプラグインを使えば普通の動画でも行けそうな気がします。でも著作権とか怖いので実装してません。誰かできた人いたら教えてください。
picast_start.sh実行しまくってプロセスがいっぱいできちゃう
// picastを実行 $ ./picast_start.sh // 生成されたプロセスのリストを表示 $ forever list // logが見れる。picast.jsのエラーとかもここにでてくる $ forever logs pycast.js
いっぱいプロセスが出てきたときは
$ ./picast_stop.sh
で消しておきましょう。
結論
なんとなくChromeCast”っぽい”のはできたかな。
でもやっぱりChromeCast欲しい。