語り得ぬことについては、沈黙するほかない

世界は事実の総体であり、ものの総体ではない

画像の分類作業を手助けしてくれるRailsアプリ(手動)を作ったよ

前回前々回の記事の続きです。

やったこと

3000枚超の画像をbingからスクレイピングして、顔の部分だけ切り取って保存し直すというところまできました。
あとは目視でその顔画像が誰の顔なのかを分類していく訳なのですが、これがめちゃくちゃ辛かったです。
まずは2つのフォルダを並べてドラッグ&ドロップでやっていこうと思いましたが、
顔の画像が小さすぎて誰か判別できないし、そもそもドラッグ&ドロップという作業が結構めんどくさい。
ということで、この画像の分類作業を(少しだけ)手助けしてくれるアプリをRuby on Railsで書きました。

環境構築

今回作ったアプリは、サーバ上に置いて公開するようなものではないのでローカルで環境構築しました。
Macの方は、こちらのQiitaの記事で本当に爆速で環境構築できます。
Windowsの方はMac買ってください。
Linuxの方は頑張ってください。

qiita.com

作ったもの

ブラウザに画像をでっかく表示させて、ボタンをクリックするだけで指定しておいたフォルダに入れてくれます。

f:id:hoshimure-47:20171004115948p:plain

主なコード

構成については、'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使って学習させよう。

github.com

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)

という感じで顔の部分だけトリミングした画像を取得することができました。

f:id:hoshimure-47:20170929172220p:plain

OpenCVのCascadeClassifierクラスのメソッド'detectMultiScale'を使用すると、画像から顔を検出した場合に返り値として四角形の左上x座標、左上y座標、幅、高さをリスト型で返します。
それらの値をfor文のイテレータとして受け取って、うまい具合に顔部分だけ抽出して画像として保存しています。
詳しくは、他の方のリンクをご覧ください。

qiita.com

問題点

やっていくうちにいくつか問題点が出てきたのであげておきます。誰か解決してください。

  1. 画像内に違う人がいる、または複数の人がいる
  2. そもそも顔の画像じゃない
  3. 横顔は検出できない、顔が傾いていてもできない
  4. 顔以外の場所が顔として検出されてしまう

とりあえず今から約3000枚の画像を人力で分類していこうと思います。

githubのコードです!git少しだけ慣れてきました!
github.com

Rubyでbingから画像をスクレイピングしたよ

唐突ですが、乃木坂46の3期生の顔を機械学習で学習させて未知の画像データから名前を推測するWebアプリケーションを作っています。Webから画像データを取得するスクレイピングの作業の際に、色々とハマったので備忘録的に記させていただきます。

作ろうとしているもの

こちらの記事の乃木坂46版です。人数が多いので3期生限定にしました。

memo.sugyan.com

進捗

顔認識させるために大量の画像データが必要となるので集めることになりました。
ということで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サイトが正しく動作するか検証するためのツールだそうです。JavaRubyPythonなどいろんな言語でのサポートがあり、とても便利なのでオススメです。

Scraperクラスのインスタンスを生成するときに引数として'prefix'と'query'を指定します。
'query'では検索したいワードを指定します。'prefix'で指定した文字列がフォルダ名、ファイル名となって出力されます。
f:id:hoshimure-47:20170926225443p:plain

スクレイピングするとき、相手のサーバに負荷をかけないために所々で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とかいう描画ライブラリを使えば数値計算とグラフ描画を同時にやってくれるのがとてもありがたいですね。

パラメータ \displaystyle (p, r, b) = (10, 28, 8/3)、初期値 \displaystyle (x_0, y_0, z_0) = (1, 1, 1)でRungeKutta法を使って解いたときの結果です。

f:id:hoshimure-47:20170801072843p:plain


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;  
}


今度は時刻 tによってパラメータを動的に変えたときの描画とか他のアトラクタも描いてみます。

attractorって日本語でなんて訳すのかな。

Webアプリ開発の本を買ったらソケットプログラミングさせられた話

情報系の大学院に行きたいなあと思っている今日この頃です。

タイトルは少々語弊がありますが、記事の内容はタイトルの通りです。ふと「Webアプリ作ろう!」と思って本を買いました。

この本ではWebアプリケーションの作り方を理解するために、Webサーバを作るとこから始めてくれます。”Webサーバを作る”といってもLinuxマシンにApacheをインストールしてという意味ではなく、ApacheのようなWebサーバのプログラムを作ることで、Webの動作原理から丁寧に解説してくれます。そのためにまず、TCP通信の基礎であるソケットプログラミングからはじめようといった流れになっています。

いやいや遠回りしすぎなんじゃと思う方もいらっしゃるでしょうが、実はそんなこともなくて、実際僕がこの本を買おうと思ったのもLINEのAPIを使って遊ぼうと思っていたらHTTPの処理あたりで詰まってしまってどうにもならなくなってしまったからです。今は素晴らしい時代で、簡単そうなフレームワークやツールを使えば単純なWebアプリケーションくらいなら誰でもすぐ作れますが、少しややこしいことをしようとすると上手くいかなくなって、根本を理解していないと自分一人では抜け出せないという状況に陥りがちです。

そうならないためにも、基礎からしっかりお勉強しておきたいところです。

今回の記事の流れはこんな感じになっています。

まず実際にTCPの規約に沿ったサーバとクライアントのプログラムを作って、それらのプログラムを使ってHTTPリクエストとHTTPレスポンスでは何が吐き出されているのかを確認していきたいと思います。

TCPサーバ/クライアントを作る

Webサーバ動作の理解のためにまず、ブラウザ(クライアント)とWebサーバをつなぐネットワークについて学びます。今回はインターネットで一般的に用いられているTCPプロトコルで通信を行うサーバのプログラムとクライアントのプログラムを作ります。

プロトコルとは、TCPとはという方にはまずこちらをご一読
【初心者向けに大体わかる】TCP/IPとは?

クライアントサーバシステムについてよく知らない人は検索してみてください。

この本は基本的にJavaを使いますが、僕は馴染みのあるC言語で書きました。一応、この本にもC言語のサンプルプログラムは載っていますが、解説やコメントがあまりなかったので記事に載せることにしました。

実際に行う動作の概念図がこちらです。
f:id:hoshimure-47:20170705015358p:plain
f:id:hoshimure-47:20170705015409p:plain

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ステップでいけるみたいですねー。

f:id:hoshimure-47:20170705015413p:plain

いやこれだけじゃ何のことかわからんぜって方のために、気持ち悪いくらいコメントついてる僕の勉強用ソースも末尾に載せておきます。

実行する前にtcp_server.cのあるフォルダにserver_send.txtという名前のファイル、tcp_client.cのあるフォルダにclient_sendという名前の送信用ファイルをそれぞれ置いておきます。ファイルがないとエラーがでます。

それから、tcp_server.cの実行中にtcp_client.cを実行すると
f:id:hoshimure-47:20170704023125p:plain
と、通信しているような感じが出てます。

実際にやってみると「TCPがコネクション型のプロトコルってそういう意味だったのか」とか、「今までネットワークって難しく思ってたけど案外単純」みたいな気持ちになりました。

とりあえずクライアントとサーバはこんな感じで表現します。意外とC言語でもサーバ実装とかできるんですね。そうですよね、元々はほとんどCで書かれてたんですから当然ですよね。

Webブラウザから、TCPサーバにアクセスしてHTTPリクエストを見る

私たちが実際に使っているWebブラウザは、Webサーバに対して一体どのようなリクエストを送っているのかというところを見てみます。

具体的には、先程作ったTCPサーバのプログラム実行中に普段使っているWebブラウザ(Google ChromeIEFirefoxなど)から

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レスポンスを見る

先にApacheをインストールしておきます。

先程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の具体的な手続きとソケットの概念
  • HTTPレスポンス、リクエストの内容

Webアプリ開発のための第一歩です。頑張ります。

ソースコード(コメント付き)

勉強用のコメント付きコードです。いないとは思いますがやりたい人がいたら参考までに。

// 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に関する知識が求められます。APIApplication Programming Interfaceの頭文字で、公式に提供されているWebサービスなどをプログラミングに利用することができます。例えば、アプリに地図を組み込みたいといった場合にはわざわざ自分で一から地図を作らなくとも、Google MapのAPIを利用することで大幅に開発期間を短縮し、なおかつクオリティの高いサービスを作ることができます。

次のリンクはハッカソンで用いられたプログラミング言語、およびAPIのランキング結果です。

jp.techcrunch.com

TwitterFacebookInstagramなど私たちに身近なSNSAPI、それからGooglePlayやSoundCloudAPIなんかもよく利用されています。また人気のあるプログラミング言語ランキング上位に位置するのもWebプログラミングが得意なJavaScriptPythonです。ハッカソンのような短い開発期間においては、既存のサービスにちょい足ししたり、組み合わせたりして新たな発想のものを作るといった考え方が好まれるようです。

なかなか学生では、プログラミングが本当に好きでもなければAPIを利用することは少ないかもしれません。ですが今はインターネットの時代なので、Webを活用できる能力が求められます。数値計算やソートアルゴリズムだけがプログラミングではありません。何よりWebは自身がよく利用しているので仕組みが分かると本当に面白いと思います。今までWebプログラミングやAPIに触れたことがないという学生の方はぜ使ってみてください。

僕もこの機会をきっかけにJavaScriptに出会うことができたので、それ関連の記事などもまた書けたらなと思います。

その3 UIデザインについて知る

UIはUser Interfaceの略で、どうすればサービスを実際に使用するユーザが操作しやすいかなどを考えてデザインされるものです。最近なんかもアプリ版Twitterのアイコンが四角から丸に変わっていて少し話題になりました。

ferret-plus.com

情報系の学部なんかではアプリや製品のプロトタイプやサービスなんかを学校で作る機会が多少なりあるかと思いますが、UIについて教わったり学んだりする機会はまだあまりないのではないでしょうか。ここに学校で教わることと現実で求められることのギャップを感じました。実際、ハッカソンでも学生が創作したものとプロのデザインが加えられたものとでは、クオリティに大きな差があったように感じます。本職のデザイナーとまではいかなくとも、エンジニアでも必要最低限デザインの理解ができると、ワンランク上のエンジニアになれるのかなって思いました。

最後までお読みいただきましてありがとうございました。
どこかのハッカソン等で会う機会があればぜひお声かけください!

おまけ

最近アイドルの結婚宣言が世間を賑わせているので、アイドルに関する簡単なアルゴリズムの問題を一つご紹介します。よかったら考えてみてください。

川の一方の岸に、あなたとアイドル、そのアイドルのオタク、そのアイドルの彼氏がいます。今、川の反対側の岸に3人を送り届けなければならないという謎の状況が発生しました。川を渡るための船はあなたともう1人しか乗ることができません。ただし、アイドルが襲われる危険性があるのでオタクとアイドルを二人きりにすることはできません。また、彼氏と逃亡してしまう恐れがあるのでアイドルと彼氏を二人きりにすることもできません。無事に3人を対岸に送り届けることができるような方法は存在するでしょうか?

【PiCAST】ChromeCastっぽいものを作ってみた。

"ChromeCast"ってご存知ですか。

Chromecast - Google
https://www.google.com/intl/ja_jp/chromecast/
 今の子はYouTube世代なので勿論知ってますよね。YouTubeの動画とかをTVで楽しめるやつです。ちなみに僕はでんぱ組.incのCMが好きでした。
 僕も今年から晴れて大学生になったので、オシャレ大学生目指して買っちゃおうかな~と思ってGoogleで探してみたら、
f:id:hoshimure-47:20170610024453p:plain
 え、高くね。YouTubeをTVで見るだけなのに5000円とかありえん。買うの諦めました。

作ろう。

 自分でChromeCastっぽいものを作ろうという結論に達しました。必要は発明の母とはよく言ったもんです。 
 「RaspberryPI Chromecast」で探したら、どうやらおんなじ考えを持っていた人がいましたので、今回はその方のプログラムを使ってみることにします。
github.com
 こちらのページのREADME.mdに書かれている手順に従ってやってみます。
 結果から先に言うと、YouTubeライブ配信動画は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...」と表示されます。
f:id:hoshimure-47:20170610035431j:plain

 とりあえずラズベリーパイHDMI端子でテレビにつないでみます。ここで続けて”http://[自分のラズベリーパイIPアドレス]:3000/yt-stream/[見たいYouTube動画のビデオID]”にアクセスしてみる。ビデオIDとはYouTubeの各動画のURLの末尾にある"v="より後ろの部分の英数字列です。ここで、動画はライブ動画を選ぶことに注意しましょう。正しく実行されると次のような画面になります。
f:id:hoshimure-47:20170610035434j:plain

6. テレビにキャストされる

 さて、僕はこちらのNASAの心躍る壮大なライブ映像をキャストしてみることにしました。
f:id:hoshimure-47:20170610024513p:plain

 今までの手順で一応動きます。こんな感じでテレビにキャストされます。
f:id:hoshimure-47:20170610024505j:plain


 ちなみに"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をサーバが読み込んで、サーバからラズパイに動画再生しろっていう命令をぶん投げてるだけです。
f:id:hoshimure-47:20170610025706j:plain

ハマったポイント

そもそもライブ配信動画にしか対応してない

 CMのみたいに、でんぱ組.incの「でんでんぱっしょん」をキャストしようと試行錯誤していたわけですが、今回ラズパイ上で実行した"livestreamer"は、YouTubeライブ配信動画しかサポートされていないので、普通の動画のビデオIDを指定するとエラー出されます。
f:id:hoshimure-47:20170610035437p:plain
 "livestreamer"の代わりに"youtube-dl"とかいうプラグインを使えば普通の動画でも行けそうな気がします。でも著作権とか怖いので実装してません。誰かできた人いたら教えてください。

picast_start.sh実行しまくってプロセスがいっぱいできちゃう
// picastを実行
$ ./picast_start.sh
// 生成されたプロセスのリストを表示
$ forever list
// logが見れる。picast.jsのエラーとかもここにでてくる
$ forever logs pycast.js

 
 いっぱいプロセスが出てきたときは

$ ./picast_stop.sh

 で消しておきましょう。


結論

なんとなくChromeCast”っぽい”のはできたかな。
でもやっぱりChromeCast欲しい。