AndroidでBluetooth関係のAPIを使ってPAPERANGに接続してみる その２

# はじめに
前回から進捗があったので。

コードはGitHubで公開。
ライセンス周りがちょっと怪しいところがあるのでその辺はもう少し調べて解決したい。
GitHubはこちら⇒[paperangsample](https://github.com/project-dev/paperangsample)

今回は画像の印刷で苦労したのでその辺を・・・

![P_20200225_002555_vHDR_Auto.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/ca6e3d36-4eb0-1ee9-df05-dfd79fffb195.jpeg)


# 画像の印刷について -前置き-
PAPERANG P1で扱う画像は、横384ピクセルになります。
画像は2値化して、1ピクセルを1ビットに変換します。
なので、384/8=48byteで横を表現します。
一度に印刷できる高さは41ピクセルのようです。
なので、一度に印刷できるデータ量は48bytex41=1968byteとなります。
このサイズを超える画像は、分割して処理する必要があります。

ビットで表現することになるので、当然濃淡はつけられません。
そのため、誤差分散法などを用いて、画像の濃淡を表現する必要があります。
今回のプログラムでは以下の記事を参考にさせていただきました。
[【OpenCV】３つの擬似濃淡変換（ディザリング）の解説【Python】](https://itech-program.com/python/994)


#画像の印刷について
今回はグレースケールに変換後、誤差拡散法を用いて変換しています。
この辺、OpenCVを使うと楽にできそうなんですが、なんとなく使いたくなかったので、Canvasを使って、グレースけるにしています。
グレースケールにする際に、元画像のサイズも調整しています。

グレースケールの処理は以下の通りです。

```java
    public Bitmap convGrayscale(Bitmap img){
        int width = img.getWidth();
        int height = img.getHeight();
        if(width != 384) {
            // サイズ調整
            float par = 384f / (float) width;
            Log.i("PAPERANG", Float.toString(par));
            width = Math.round(width * par);
            height = Math.round(height * par);
            if (width != 384) {
                width = 384;
            }
        }
        Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(bmp);
        Paint paint = new Paint();

        ColorMatrix cm = new ColorMatrix();
        cm.setSaturation(0);
        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
        paint.setColorFilter(f);

        Rect distRect = new Rect(0, 0, width, height);
        Rect srcRect = new Rect(0, 0, img.getWidth(), img.getHeight());
        canvas.drawBitmap(img, srcRect, distRect, paint);
        canvas = null;
        return bmp;
    }
```

Canvasにフィルタを適用していますが、ColorMatrix.setSaturation()メソッドで引数に0を指定し、ColorMatrixFilterに設定することで、グレースケールのフィルタになるとのことで、そのように処理しています。
あとは、元画像をグレースケールのフィルタを適用したキャンバスに描画します。
これで、グレースケールの画像が完成します。

次に、誤差拡散法で2値化します。
メソッド名がダサいですが、処理は以下の通りです。

```java
    public Bitmap conv2Value(Bitmap img){
        Bitmap bmp = convGrayscale(img);
        int width = bmp.getWidth();
        int height = bmp.getHeight();
        int thresh = 128;
        int err = 0;

        int[] gray= new int[width * height];
        bmp.getPixels(gray, 0, width, 0, 0, width, height);

        for(int y = 0; y < height; y++){
            for(int x = 0; x < width; x++){
                int color = gray[y * width + x];
                int r = Color.red(color);
                //int g = Color.green(color);
                //int b = Color.blue(color);

                if(r + err < thresh){
                    err = r + err - 0;
                    gray[y * width + x] = Color.argb(255,0,0,0);
                }else{
                    err = r + err - 255;
                    gray[y * width + x] = Color.argb(255,255,255,255);
                }
            }
        }

        bmp.setPixels(gray, 0, width, 0, 0, width, height);
        return bmp;
    }
```

引数の画像のピクセルの配列を取得します。
取得したピクセルからR(赤)の値を取得します。
これは、グレースケールにしているので、RGBのどれでもいいので、Rにしています。
そして、誤差拡散法で2値化していきます。
(誤差拡散法についてはどこかでちゃんと理解せねば・・・)
閾値を超えるか超えないかで白か黒かを決めていきます。
参考サイトを見てもらいとわかりますが、今回の方法からさらに進んだ方法もあります。

できたデータをビットマップを作成します。
Bitmapオブジェクトにすることで、画面に表示して確認することも可能です。


印刷コマンドを発行する部分ですが、以下のようなソースになっています。

```java
    public void printImage(Bitmap img) throws IOException {
        int width = img.getWidth();
        int height = img.getHeight();
        int[] pixcels = new int[width * height];
        img.getPixels(pixcels, 0, width, 0, 0, width, height);
        byte[] bits = new byte[48 * height];
        for(int y = 0; y < height; y++){
            for(int x = 0; x < width; x++){
                int r = Color.red(pixcels[y * width + x]);
                if(r == 0x00) {
                    int idx = y * 48 + (x / 8);
                    switch(x % 8){
                        case 0:
                            bits[idx] = (byte)(bits[idx] | (byte)0x80);
                            break;
                        case 1:
                            bits[idx] = (byte)(bits[idx] | (byte)0x40);
                            break;
                        case 2:
                            bits[idx] = (byte)(bits[idx] | (byte)0x20);
                            break;
                        case 3:
                            bits[idx] = (byte)(bits[idx] | (byte)0x10);
                            break;
                        case 4:
                            bits[idx] = (byte)(bits[idx] | (byte)0x08);
                            break;
                        case 5:
                            bits[idx] = (byte)(bits[idx] | (byte)0x04);
                            break;
                        case 6:
                            bits[idx] = (byte)(bits[idx] | (byte)0x02);
                            break;
                        case 7:
                            bits[idx] = (byte)(bits[idx] | (byte)0x01);
                            break;
                    }
                }
            }
        }
        int loffset = 41;
        if(height <= 41){
            loffset = height;
        }
        try {
            byte[] buff = new byte[48];
            ByteBuffer buffs = ByteBuffer.allocate(48 * loffset);
            buffs.order(ByteOrder.LITTLE_ENDIAN);
            for (int i = 0; i < bits.length; i++) {
                if(i % (48 * loffset) == 0 && i > 0){
                    //ResultData ret = getStatus();
                    Log.i("PAPERANG","print");
                    printData(buffs.array());
                    buffs.clear();
                    buffs = ByteBuffer.allocate(48 * loffset);
                    buffs.order(ByteOrder.LITTLE_ENDIAN);
                }
                buffs.put(bits[i]);
            }
            printData(buffs.array());
        }catch(Exception e){
            if(e != null){
                if(e.getMessage() != null){
                    Log.e("PAPERANG", e.getMessage());
                }else{
                    Log.e("PAPERANG", e.toString());
                }
            }
        }
    }
```

最初の方で、画像のピクセル情報を取ってきて、ビットに変換しています。
実際に印刷して気づいた点

+ 1は白、0は黒で印刷しないといけません。
+ 1バイト目は8ビット、2バイト目は7ビット・・・というように変換が必要

これら2点に気を付けないと、プレビューで誤差拡散した画像はOK、印刷するとそれっぽいのが出てるんだけどなんか違うということになります。
ちなみに、switch文でやるとダサい感じになるけど、面倒なのでｗ

ビットに変換して、配列に入れたあと、最後のfor文で印刷を実施します。
ここでは、48byte x 41 で印刷するように処理しています。

# 最後に
おもちゃとしてはなかなか楽しめました。
今後はまだ未確認のコマンドを確認していきたいと思います。
変数名やメソッド名がダサい部分があるので、どこかで見直しておきたいところ。
C#版とか作りたいような気もしてるけど作らないかもｗ
(公開したコード読んでくれれば多分簡単にできるんじゃないかな？)
画像が出るようになったので、[オープンソースカンファレンス 2020 Hamanako](https://www.ospn.jp/osc2020-hamanako/)でなにか展示するかもです。