2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

NanoPi-NEOでOpenCV CapしてOLED表示

Last updated at Posted at 2017-06-30

概要

NanoPi-NEOもしくはNanoPi-NEO2に繋いだUSBカメラから、
OpenCV経由でカメラキャプチャして、リサイズして、グレースケール化して、
リニア空間での誤差拡散法で2値化して、NanoHat OLED (= SSD1306) に I2C使って表示する。以上、それだけ。

それだけなんだけど、リニア空間での誤差拡散法適用が、地味に論文1本書けるかもしれない(笑

ソースコード

全体はこちら。
https://github.com/blue777/NanoPi-NEO

### メイン部分

いたって普通の、なんの取り留めも、なんの変哲も無い、
普通のOpenCVコード。

opencv_cap_oled.cpp
#include "common/i2c_oled_ssd1306.h"
#include "common/img_halftone.h"
#include "common/perf_log.h"

#include <opencv2/opencv.hpp>

int main()
{
    I2C_OLED_SSD1306  oled;

    cv::VideoCapture    cap(0);

    if(!cap.isOpened()) 
    {
        printf("No capture devices.\n");
        return -1;
    }
    
    cap.set( CV_CAP_PROP_FRAME_WIDTH, 640 );
    cap.set( CV_CAP_PROP_FRAME_HEIGHT, 360 );
    cap.set( CV_CAP_PROP_FPS, 30 );

    oled.Init();
    oled.DisplayOn();
    
    while( 1 )
    {
        cv::Mat src,dst;

        cap >> src;

        cv::resize( src, src, cv::Size(128,64), 0, 0, cv::INTER_LANCZOS4 );
        cv::cvtColor( src, dst, CV_BGR2GRAY); 

		{
//			PerfLog  iPerf("dither");

//			ImageHalftoning::ErrDiff_LinearFloydSteinberg( dst.data, dst.step, dst.cols, dst.rows );
			ImageHalftoning::ErrDiff_LinearStucki( dst.data, dst.step, dst.cols, dst.rows );
//			ImageHalftoning::ErrDiff_FloydSteinberg( dst.data, dst.step, dst.cols, dst.rows );
//			ImageHalftoning::ErrDiff_Burkes( dst.data, dst.step, dst.cols, dst.rows );
//			ImageHalftoning::ErrDiff_Stucki( dst.data, dst.step, dst.cols, dst.rows );
//			ImageHalftoning::ErrDiff_Atkinson( dst.data, dst.step, dst.cols, dst.rows );
//			ImageHalftoning::PatternDither_2x2( dst.data, dst.step, dst.cols, dst.rows );
		}

//		cv::imwrite("src.png",src);
//		cv::imwrite("dst.png",dst);

//		PerfLog  iPerf("disp");
		oled.WriteImage(dst.data, dst.step);
	}

    return  0;
}

誤差拡散法による2値化

とりあえず、4種類実装してみた。うち2種類はリニア空間対応である。
ソースは「img_halftone.h」である。
コンパイルを簡単にするため、ヘッダ実装である。

それぞれの誤差拡散先の係数。

FloydSteinberg

- - * 7/16 -
- 3/16 5/16 1/16 -

Burkes

- - * 4/16 2/16
1/16 2/16 4/16 2/16 1/16

Stucki

- - * 8/42 4/42
2/42 4/42 8/42 4/42 2/42
1/42 2/42 4/42 2/42 1/42

Atkinson

- - * 1/8 1/8
- 1/8 1/8 1/8 -
- - 1/8 - -

いずれのアルゴリズムも、基本的にガンマ値を考慮したものではないので、そのまま実装すると、画像が明るくでてしまう。

そのため、Gamma=2.2でリニア空間に戻してから誤差拡散法実施すると、ものすごく知覚的に一致し、なぜか綺麗に見えてくる。

左から順に、「グレースケール」「リニア空間で誤差拡散」「誤差拡散」です。

※右側画像が明るく見えるのが期待値で、画像がピクセル等倍表示になっていないと、期待値がらハズレます。拡大・縮小された状態でみると、話がめんどくさくなりますので、必ずピクセル等倍で見てください。

DitherEval.PNG

2値タイプのOLED表示を行うがこそ必要な技法である(笑
以下にコードの一部を載せる。(真ん中の画像生成に使用したもの)

img_halftone.hの一部
	void    ErrDiff_LinearFloydSteinberg( unsigned char * image, int stride, int width, int height )
	{
		int					shift	= 20;
		int					gamma[256];
		std::vector<int>	data(width*height);

		for( int i = 0; i < 256; i++ )
		{
			gamma[i]	= (int)(pow( i / 255.0, 2.2 ) *((1 << shift) - 1));
		}
		
		for( int y = 0; y < height; y++ )
		{
			unsigned char*	src_line	= image + stride * y;
			int *			dst_line	= &data.data()[ width * y ];
			int				x			= 0;

			for( ; (x+4) <= width; x += 4 )
			{
				dst_line[x+0]	= gamma[ src_line[x+0] ];
				dst_line[x+1]	= gamma[ src_line[x+1] ];
				dst_line[x+2]	= gamma[ src_line[x+2] ];
				dst_line[x+3]	= gamma[ src_line[x+3] ];
			}

			for( ; x < width; x++ )
			{
				dst_line[x+0]	= gamma[ src_line[x+0] ];
			}
		}


		for( int y = 0; y < height; y++ )
		{
			int*			line		= &data.data()[ width * y ];
			unsigned char*	dst_line	= image + stride * y;

			for( int x = 0; x < width; x++ )
			{
				int		c	= line[x];
				int		e	= (1 << (shift-1)) <= c ? c - ((1 << shift) - 1) :  c;
				
				dst_line[x]	= (1 << (shift-1)) <= c ? 255 : 0;

				//	-		*		7/16
				//	3/16	5/16	1/16
				if( (x+1) < width )	line[x+1]	+= e * 7 / 16;

				if( (y+1) < height )
				{
					int*	lineN	= line + width;

					if( 0 <= (x-1) )	lineN[x-1]	+= e * 3 / 16;
										lineN[x  ]	+= e * 5 / 16;
					if( (x+2) < width ) lineN[x+1]	+= e * 1 / 16;
				}
			}
		}
	}

NanoPi-NEO(2)でCloud9から実行する場合

OLED表示はI2Cを直叩きしてますので、特に外部依存なく使えるハズです。
そのため、OpenCV入れて、Cloud9上でコンパイル&実行だけで通る(ハズ)。

NanoPi-NEO2へのCloud9のインストール手順は、
Cloud9 を NanoPi NEO2 にインストールするで。

NanoPi-NEO(2)にOpenCV入れる

パッケージ入れればOK

apt-get install libcv-dev libopencv-dev

Cloud9のRunnerを編集する

そのままだと、OpenCVが使えないのでRunnerを修正しライブラリ参照を追加します

C++(OpenCV).run
// This file overrides the built-in C++ (simple) runner
// For more information see http://docs.c9.io:8080/#!/api/run-method-run
{
  "script": [
    "set -e",
    "if [ \"$debug\" == true ]; then ",
    "/usr/bin/g++ -ggdb3 -std=c++11 $file -o $file.o `pkg-config --cflags opencv` `pkg-config --libs opencv`",
    "chmod 755 \"$file.o\"",
    "node $HOME/.c9/bin/c9gdbshim.js \"$file.o\" $args",
    "else",
    "/usr/bin/g++ -std=c++11 $file -o $file.o -O3 `pkg-config --cflags opencv` `pkg-config --libs opencv`",
    "chmod 755 $file.o",
    "$file.o $args",
    "fi"
  ],
  "info": "Running (C++ OpenCV) $file",
  "debugger": "gdb",
  "$debugDefaultState": false,
  "env": {},
  "selector": "^.*\\.(cpp|cc)$"
}

あとは実行すればOK

### OLEDの表示速度について

I2Cの転送速度 100kHz or 400kHz に依存します。

テキトーに計算すると、
OLEDのサイズが128x64、1Pixel=1bit表現なので、
$ ImageDataSize = 128 * 64 / 8 = 1288 [byte] $

I2Cで送る場合は制御コマンド1byteがつくので、StartCode/EndCode無視して、
bit単位に変換すると

$ (1288+1)*8 = 10312 [bit] $

になるので、I2C駆動速度が100kHzの場合は、

$ 1000000 [bits/sec]/ 10312 [bits/frame] = 9.6974 [frame/sec] $

となる。
400kHzの場合は、4倍になるので40fps近くになることになる。

そして、困ったことに、
NEO2 の 4.x,y系カーネルは、100kHz駆動
NEO の 3.x.y系カーネルは、400kHz駆動
その他未調査である。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?