2015年3月1日日曜日

javaで画像処理する方法

画像処理は計算に時間かかるから普通はCとかでするんだけど、javaであえて書いてみる!
対象は「人によって青&黒または白&金に見える」って話題になったこのドレスの画像で

画像の仕組み

画像とは大量の点からできている
画像の情報を見るとき、下のかけ算をよくみると思うけどコレがこの画像を表示するための点の数

上記画像は、横方向に288個、縦方向に437個。つまり、125856個の点で構成されているってこと

この点を「ピクセル」といい、色の情報を持ってる
一般的にはピクセルは4Byteで表されてるんだけど、振り分けはこんな感じ
ビット00000000000000000000000000000000
意味
透過率
赤
青
緑
透過率はまぁいいとして、 それぞれ赤、青、緑を色深度を0x00〜0xFF(16進表記)で表してる
色深度は数値が低いほど暗い。例として、下記に示す※透過はFFとしておく
色の情報表示される色表示される色
0xFFFF0000赤(純粋)   
0xFF000100青(めっちゃ暗い)   
0xFFFF00FFマゼンダ(赤と緑を混ぜた色)   
こんな感じで、すべての色を赤青緑でつくってることになる

実装

javaには最初から画像処理するためのクラスがあるので、ダウンロードとかいらない。インポートさえすれば使える 一例に、画像を少し暗くする
/** ImageTest.java  画像を少し暗くする処理 */
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.ImageIO;
 
public class ImageTest {
     
    final static String INPUT_IMAGE = "image.jpg";// 読み込み画像(このjavaファイルと同じフォルダに入れとく)
    final static String OUTPUT_IMAGE = "reslt.jpg";// 出力する画像(このjavaファイルと同じフォルダに出力される)
     
    public static void main(String[] args) throws IOException {
         
        // 画像読み込み
        File in_file = new File(INPUT_IMAGE);
        BufferedImage read = ImageIO.read(in_file);
         
        // 情報取得 w, hは縦、横のピクセル数。writeは画像の1ピクセルごとにアクセスできるようにしてる
        int w = read.getWidth(),h=read.getHeight();
        BufferedImage write =
        new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
         
        // 格ピクセルに処理していく
        for(int y=0; y < h; y++) {
            for(int x = 0; x < w; x++) {
                // 色抽出
                int color = read.getRGB(x, y); // 座標の色を取得
                int red   = (color & 0xff0000) >> 16; // 赤抽出
                int green = (color & 0x00ff00) >> 8;  // 緑抽出
                int blue  = (color & 0x0000ff) >> 0;  // 青抽出
                 
                // 色の情報変換
                red   = red / 2;   // 赤色の色深度を半分にする
                green = green / 2; // 緑色の色深度を半分にする
                blue  = blue / 2;  // 青色の色深度を半分にする
   
                // 出力用にフォーマット
                red  =(red << 16)  & 0xff0000;
                green=(green << 8) & 0x00ff00;
                blue =(blue << 0)  & 0x0000ff;
                color = 0xff000000 | red | green | blue;
                write.setRGB(x, y, color); // 情報変換した色をもとの座標に戻す
            }
        }
         
        // 画像の出力
        File out_file = new File(OUTPUT_IMAGE);
        ImageIO.write(write, "jpg", out_file);
    }
}
結果はこんな感じ
ポイントは色ついてるとこ(32~34行目)。それぞれの色を半分にすることで、暗くできる

応用

ポイント部分を変えることで、いろんなことができる
赤のみ抽出
// 色の情報変換
red   = red; // 赤はそのまま
green = 0;   // 緑は0にする
blue  = 0;   // 青は0にする
青のみ抽出
// 色の情報変換
red   = 0;   // 赤は0にする
green = 0;   // 緑は0にする
blue  = blue;// 青はそのまま
緑のみ抽出
// 色の情報変換
red   = 0;     // 赤は0にする
green = green; // 緑はそのまま
blue  = 0;     // 青は0にする
ビット反転
// 色の情報変換
red   = ~red;   // ビット反転
green = ~green; // ビット反転
blue  = ~blue;  // ビット反転


出力結果
赤抽出青抽出緑抽出ビット反転
ビット反転すると写真のネガみたいになる
不思議なことにビット反転すると白&金になるんだよねw
他にもモザイクをかけたり、色のヒストグラムとったりなどいろいろできるけど、今回はこれまでで

2015年2月19日木曜日

Androidの通信処理はAsyncTaskで処理すると楽

前回アニメ番組表APIの通信処理(http://tommyproguram.blogspot.jp/2015/02/apiandroid.html)で、Thread, Hadlerを使った処理を行ったが、UIスレッド、通信スレッドの行き来を意識するのが面倒だった。
なので、今回はそれを簡単に実装できるAsyncTaskクラスを使って通信処理を書き直してみる。

AsyncTaskとは

「エイシンクタスク」って読むらしい。 このクラスはexecute()メソッドを実行することで、下記の順でメソッドが呼ばれる。メソッドごとに実行スレッドが違うのが特徴

メソッド名意味使用例
onPreExecute非同期処理する前に何かする場合に使用ダイアログで通信中を表示
doInBackground非同期処理通信処理、DB処理、データ読み込み
onProgressUpdate非同期処理中にUI更新する時使用
publishProgress()でこの処理を実行
%とかバーで処理状態表示
onPostExecute非同期処理で得られた結果を元にUI更新ダイアログ消去
通信処理結果表示

使用上の注意

めっちゃ便利そうだけど、何でもかんでもAsyncTaskで平行処理するのはよくない
UIが絡むとそれだけコストがかかる。このクラスは必ずUIスレッドを経由するため、UIスレッドを絡めない処理は無難にThreadクラスで処理した方が良い
UI関係の起こりやすく、発見しにくかったバグを2つ紹介する
  • AsyncTaskを複数使用するときの処理
    • Android2.3以前
    • execute()メソッドを実行する度に並列処理されている
    • Android3.0以降
    • execute()メソッドを実行すると現在実行中のexecute()が終了してから処理が実行される
    この違いを意識しないと、通信中に表示するダイアログやヌルポに影響が出るので注意
  • 処理をキャンセルするときの処理
  • 通信中画面遷移したりすると、通信キャンセルせざるおえない場合がある
    キャンセル処理をAsyncTask #onCancelled()にオーバライドしておき、
    AsyncTask #cansel()を実行することで、上記メソッドが呼ばれキャンセルできる仕組みだが、 非同期処理中 ( doInBackground() ) にキャンセルすると下記の2パターンに分岐する
    • Android2.3以前
    • すぐAsyncTask #onCancelled()が呼ばれる
    • Android3.0以降
    • doInBackground()が終了してからAsyncTask #onCancelled()が呼ばれる
    まぁ結局どっちもdoInBackground()が終了するまで処理は実行されるんだけど、onCancelled()がきても非同期処理が続くことを考えた実装が必要になる。
    画面遷移のため非同期処理をキャンセルする場合は、onProgressUpdate()にも注意。doInBackground()は途中で終わらないので、画面遷移後の存在しないView更新でヌルポしたりして痛い目にあうw
    対処方はdoInBackground()処理にisCancelled()メソッドでこまめに今キャンセル中か確認すること!


実装方法

AsyncTask<Params、Progress、Result>で表される。ジェネリクスはそれぞれ下記の意味
型意味使用箇所
Params非同期通信の引数になる。execute()の引数で渡す →
doInBackground()の引数
Progress非同期処理中UIスレッド更新に渡す引数publishProgress()の引数で渡す →
onProgressUpdate()の引数
Result非同期処理結果のクラスdoInBackground()の戻り値 →
onPostExecute()の引数
今回の場合はこれなので
  • Params : リクエストするJSONのURL → String
  • Progress : 使用しない → Void
  • Result : レスポンスのJSONを文字列にしたもの → String
AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() {
    @Override
    protected String doInBackground(String... params) {
        // TODO !非同期処理はアブストラクトメソッドなので必須!
        return null;
    }
};
で、あとは非同期処理中に通信処理。通信処理後にListViewの更新かける処理記述
AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() {

    /**通信スレッド処理.
     * 引数のURLからリクエストし、レスポンスをJSONデータ文字列に変換
     * @param url リスエスト先
     * @return JSONデータの文字列
     */
    @Override
    protected String doInBackground(String... url) {
        return getAnimeInfo(url[0]);
    }

    /**UIスレッドの処理.
     * 引数のJSON文字列からタイトル配列を取得し、ListViewに表示
     * @param JSON文字列
     */
    @Override
    protected void onPostExecute(String json) {
        mListView.setAdapter(new ArrayAdapter<String>(
                MainActivity.this,
                android.R.layout.simple_expandable_list_item_1,
                getTitleForJSON(json)));
    }
};
最後に、Activityでexecute()を呼べば完成!※行削減の為コメ消去
public class MainActivity extends Activity{
    ListView mListView = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(mListView = new ListView(this));
        task.execute("http://animemap.net/api/table/osaka.json");
    }

    AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() {
        @Override
        protected String doInBackground(String... url) {
            return getAnimeInfo(url[0]);
        }
        @Override
        protected void onPostExecute(String json) {
            mListView.setAdapter(new ArrayAdapter<String>(MainActivity.this,
                    android.R.layout.simple_expandable_list_item_1,getTitleForJSON(json)));
        }
    };
    
    public static String getAnimeInfo(String url) {
        AndroidHttpClient client = AndroidHttpClient.newInstance(null);
        HttpGet get = new HttpGet(url);
        StringBuilder sb = new StringBuilder();
        try {
            HttpResponse response = client.execute(get);
            BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String line = null;
            while ((line = br.readLine()) != null) sb.append(line);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            client.close();
        }
        return sb.toString();
    }

    public static String[] getTitleForJSON(String jsonInfo) {
        final ArrayList<String> tmp = new ArrayList<String>();
        try {
            JSONArray json = new JSONObject(jsonInfo)
            .getJSONObject("response")
            .getJSONArray("item");
            for (int i = 0; i < json.length(); i++)
                tmp.add(((JSONObject) json.get(i)).getString("title"));
        } catch (JSONException e){
            e.printStackTrace();
        }
        return tmp.toArray(new String[0]);
    }
}
あ、マニフェストの書き忘れ注意!

2015年2月8日日曜日

アニメ番組表 API使った通信Androidアプリを作ってみた

WebAPIをまとめてるサイト(http://www.find-job.net/startup/api-2013)みてたら面白そうなのがあったので使ってみた

    WebAPIとは

    Webサイトに外部のサイトの提供する機能や情報を組み込んだり、アプリケーションソフトからWeb上で公開されている機能や情報を利用できるサービス
んで、ついでにAndroidの通信処理でよく引っかかるところについてもまとめとく
  1. メインスレッド(UIスレッド)で通信処理ができない
  2. UIスレッドでないとViewの編集ができない
  3. マニフェストに定義しないと通信処理できない
  4. JSONの使い方

アニメマップAPIの使い方

  1. WebAPIの基本的な動き
  2. 通信は基本的にXMLかJSONの2通りのやり方がある。
    違いは取得するデータの形。試しに今週のアニメ情報で比較
    XML:http://animemap.net/api/table/osaka.xml
    JSON:http://animemap.net/api/table/osaka.json
    最近はJSON方式の方をよく見るので、JSONでつくってく。流れはこんな感じ

  3. アニメマップAPIへのリクエストとレスポンス
  4. アニメマップのAPIのサイト(http://animemap.net/pages/api/)を見ると地域ごとに取得できるみたい
    大阪の番組表が欲しいので、リクエスト(サーバへの要求)はコレ(http://animemap.net/api/table/osaka.json)
    実際にブラウザでこのURLに行くとJSON型がどんなデータが返ってくるのか確認できる
    Androidで書くとこんな感じでリクエストして、戻り値で JSON 型をStringにする。
    /**
     * アニメマップに大阪のテレビ情報をリクエスト.
     * 
     * @return JSON型のレスポンスをStringで返す
     */
    public static String getAnimeInfo() {
     // リクエスト
     AndroidHttpClient client = AndroidHttpClient.newInstance(null);
     HttpGet get = new HttpGet("http://animemap.net/api/table/osaka.json");
     StringBuilder sb = new StringBuilder();
     try {
      // リクエスト要求 ⇒ レスポンス
      HttpResponse response = client.execute(get);
      
      // レスポンスをStringにして返値にする
      BufferedReader br = new BufferedReader(new InputStreamReader(
        response.getEntity().getContent()));
      String line = null;
      while ((line = br.readLine()) != null) {
       sb.append(line);
      }
     } catch (IOException e) {
      e.printStackTrace();
     } finally {
      client.close();
     }
     return sb.toString();
    }
    
  5. JSONを解析
  6. JSONは{}で囲まれた領域に"変数名":"value"見たいな感じで定義される。
    ほんとの定義はこの中にもInt型とかいろいろあるけど省略
    レスポンスのJSONを改行や字下げとかで解析してみると、
    {
        "request":{
            "type":"json",
            "url":"地域ごとの一覧URL",
            "updated":"更新時間"
        },
        "response":{
            "item":[
                {
                    "title":"アニメのタイトル",
                    "url":"アニメの放送局一覧URL",
                    "time":"放送時間",
                    "station":"放送局",
                    "state":"よくわらん",
                    "next":"第X話",
                    "episode":"総話数",
                    "cable":"よくわからん",
                    "today":"よくわからん",
                    "week":"放送曜日"
                },
                {
                    "title":"アニメのタイトル",
                    ………省略………
                }
            ]
        }
    }
    
    このJSONは"request","response"の2つの連想配列を持ち、
    "response"にはitemって配列([]←この記号は配列)に1アニメずつ連想配列があることがわかる
    アニメ連想配列ごとに放送時間、放送局など色々な情報が含まれてるけど、とりあえずアニメのタイトル取得してリスト表示させようと思う
    さっきのJSONをStringにしたヤツからアニメタイトル一覧を取得する関数を作る
    /**
     * JSONからアニメのタイトルを抜き出す.
     * 
     * @param str_json
     * @return
     */
    public static String[] getTitleListForJSON(String jsonInfo) {
     final ArrayList<String> tmp = new ArrayList<String>();
     try {
      // String型をJSON型に変換
      JSONObject json = new JSONObject(jsonInfo);
      
      // "response"連想配列を取得
      JSONObject responce = json.getJSONObject("response");
      
      // "item"配列を配列型で取得
      final JSONArray animes = responce.getJSONArray("item");
      
      // "item"配列から1コずつ連想配列を取り出し、タイトルを出力用リストに入れてく
      for (int i = 0; i < animes.length(); i++) {
       JSONObject o = (JSONObject) animes.get(i);
       tmp.add(o.getString("title"));
      }
     } catch (JSONException e) {
      e.printStackTrace();
     }
     return tmp.toArray(new String[0]);
    }
    

Androidの実装

次にAndroidでの描画を実装。これがまた面倒。。 最初に述べたようにAndroidはUIスレッドでは通信できない。ので、別スレッドでデータの取得を行う。でも、取得したデータは通信のスレッドではViewに入れることができない。よって、元のUIスレッドでViewに表示させる処理がいる、、 なに言ってるのかわからなくなるので、一個ずつ処理していく
  1. ベース作り
  2. まず下準備。大本のマニフェストやレイアウトの設定
    アニメのタイトル一覧をListViewで表示させる
    • Manifest.xml
    • これ入れないと通信系動かないので注意!!
      <uses-sdk/>タグの後らへんに追記
       <uses-permission android:name="android.permission.INTERNET" />
      
    • MainActivity.java
    • public class MainActivity extends Activity {
       ListView mListView;
       @Override
       protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mListView = new ListView(this);
        mListView.setBackgroundColor(Color.BLACK);
        setContentView(mListView);
       }
      
       // 記載済みの為省略
       public static String getAnimeInfo() {……}
       public static String[] getTitleForJSON(String jsonInfo) {……}
      
      }
      
  3. 通信用スレッド立てる
  4. もはやAndroid以前にjavaの問題なんだけど、別スレッドを立てるにはThreadクラスのインスタンスがいる。 Thread#start()メソッドを実行することで、Runnable#run()メソッドが別スレッドで実行される。
    ActivityにRunnableインターフェースをくっつけて、runを実装
    public class MainActivity extends Activity implements Runnable {
     ListView mListView;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      mListView = new ListView(this);
      mListView.setBackgroundColor(Color.BLACK);
      setContentView(mListView);
      // スレッド起動!通信開始!!
      new Thread(this).start();
     }
    
     // 記載済みの為省略
     public static String getAnimeInfo() {……}
     public static String[] getTitleForJSON(String jsonInfo) {……}
    
     @Override
     public void run() {
      String jsonInfo = getAnimeInfo();
      final String[] titles = getTitleForJSON(jsonInfo);
      for(String str:titles)Log.d("TEST","title = "+str);
     }
    }
    
    ログにアニメタイトル一覧が出ればOK!

  5. Handler使ってUIスレッドで描画
  6. 現在通信スレッド上ににアニメタイトル一覧があるので、ここからViewにデータをセットしたい。そこで直面するのが「通信のスレッドではViewに入れることができない」問題。解決策はHandlerクラス
    HandlerとはHandlerのインスタンスを生成したスレッドで自信の処理を行う事ができる
    つまり、UIスレッド(Mainスレッド)でHandlerを生成しといて、別スレッドでこのHandlerを使えばUIスレッドで処理してることになる。run()をちょっと修正
    // UIスレッド上にHandler生成
    Handler mHandler =new Handler(); //Activityのメンバ変数
    @Override
    public void run() {
     String jsonInfo = getAnimeInfo();
     final String[] titles = getTitleForJSON(jsonInfo);
     for(String str:titles)Log.d("TEST","title = "+str);
     
     // Handlerはpostメソッドで引数のRunnable#run()メソッドを自信の生成スレッド上で実行させる
     mHandler.post(new Runnable() {
      @Override
      public void run() {
       // ここの処理がUIスレッドで実行される
       mListView.setAdapter(new ArrayAdapter<String>(MainActivity.this
         .getApplicationContext(),
         android.R.layout.simple_expandable_list_item_1, titles));
      }
     });
    }
    

完成!
最終的なコードはこんだけ。行数短くしたかったからコメント無、可読性落とした
public class MainActivity extends Activity implements Runnable{
 ListView mListView = null;
 Handler mHandler = new Handler();
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(mListView = new ListView(this));
  new Thread(this).start();
 }

 public static String getAnimeInfo() {
  AndroidHttpClient client = AndroidHttpClient.newInstance(null);
  HttpGet get = new HttpGet("http://animemap.net/api/table/osaka.json");
  StringBuilder sb = new StringBuilder();
  try {
   HttpResponse response = client.execute(get);
   BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
   String line = null;
   while ((line = br.readLine()) != null) sb.append(line);
  } catch (IOException e) {
   e.printStackTrace();
  } finally {
   client.close();
  }
  return sb.toString();
 }

 public static String[] getTitleForJSON(String jsonInfo) {
  final ArrayList<String> tmp = new ArrayList<String>();
  try {
   JSONArray json = new JSONObject(jsonInfo)
   .getJSONObject("response")
   .getJSONArray("item");
   for (int i = 0; i < json.length(); i++)
    tmp.add(((JSONObject) json.get(i)).getString("title"));
  } catch (JSONException e){
   e.printStackTrace();
  }
  return tmp.toArray(new String[0]);
 }

 @Override
 public void run() {
  final String[] titles = getTitleForJSON(getAnimeInfo());
  mHandler.post(new Runnable(){
   @Override
   public void run(){
    mListView.setAdapter(new ArrayAdapter<String>(MainActivity.this,android.R.layout.simple_expandable_list_item_1,titles));
   }
  });
 }
}
※マニフェストの追記は忘れない様に!