開発記録

飛跡のカウント

1フレームごとに行われる処理

毎フレームごとに飛跡の候補となるピクセルの集まりが findContours により検索され、vector の vector  (vector<vector<Point>>) として保持されます。それらをフレーム同志で結びつけるために図1のような処理が行われています。

図1の中で old  は、前フレームの merged のコピーであり全く同じです。フレームの中で new が、findContours された結果になります。

図1. フレームごとの飛跡認識の流れ

次に、前フレームのコピーである old の中の vectorたちに対して、new の中の vectorたちと比較することで、同じ飛跡だと思われる vector がみつかるとその情報で更新、みつからなかったらそこで飛跡は消滅と判断、全く新しい vector については新規に飛跡が発生したという処理を行い merged にします。

飛跡認識までの流れ

図2に飛跡認識までの流れを示します。まず old の中に含まれず、new の中にのみふくまれるvector があると、それは飛跡候補となります。そして何フレームかを経て、old の中に含まれるが new の中にはない vector は、発生してからの時間をチェックした上で飛跡として認識されカウントが行われます。発生してからの時間があまりにも長いものはノイズの可能性があるので棄却されます。

図2. 飛跡候補誕生から、飛跡として認識されるまで

[Swift4.2] iOS-Charts を用いて各種プロットの表示

表示させるプロット

iOS-Charts を利用して、霧箱で認識された各トラックについての、「時間変化」、「長さの分布」、「方向の分布」等のプロットを表示させる (図1)。 iOS-Charts については、以下のサイトを参照しました。 How to Use iOS Charts API to Create Beautiful Charts in Swift: https://www.appcoda.com/ios-charts-api-tutorial/
図1. 画面を右にスクロールさせることで、カメラ画面の上にグラフを重ねる形で表示させる。

BarChartView

霧箱で認識されたトラックの長さの分布をプロットするのに、ヒストグラムを表示させるのに適していそうな BarChartView を利用することにします。まず、Main.StoryBoard 上に View を配置し、配置した View の class を BarChartView に変更します。下の例では、plotLength としてコードに紐付けをしています。この後に、下の例のように必要なコードを加えていきます。実際の表示を図2に示します。
class ViewController: UIViewController{

  var dataLength = [[Double]]() 
  @IBOutlet weak var plotLength: BarChartView!
 
  override func viewDidLoad(){
    super.viewDidLoad() 
    // 2次元配列 dataLength に、xとyの値をセットする処理を行う。
    // 具体的な中身は、ここでは省略。  
    setData( val: dataLength )

    // setData で値の入れられた2次元配列 dataLength を、バーグラフで表示するための処理。
    setChart( val: dataLength )
  }

  func setChart( val: [[Double]] ){
    var entry: [ BarChartDataEntry ] = []
    let start = val[0].first
    let end   = val[0].last
    let num   = val[0].count

    // 表示させるデータの値を BarChartDataEntry にセットする。
    for i in 0..<num {
      entry.append( BarChartDataEntry( x: Double( i ), y: val[1][i] ) ) 
    }
    let dataSet = BarChartDataSet( entries: entry, label: "Length ( x: cm, y: events )" )
    
    // バーの境界線の色を設定する。
    dataSet.barBorderColor = UIColor( red:1.0, green:0.0, blue:0.0, alpha:1.0 )
    // バーの色を設定する。
    dataSet.setColor( NSUIColor.red )
    // バーの上に表示される数値の少数点を表示させないようにする。
    dataSet.valueFormatter = BarChartValueFormatter()
   // BarChartData をセットする。
    plotLength.data = BarChartData( dataSet: dataSet )

    // x軸のラベルをデータに沿ったものにする。   
    plotLength.xAxis.valueFormatter = BarChartFormatter( start!, end:end!, num:num )

   // x軸のラベルを下に表示させる。
    plotLength.xAxis.labelPosition = .bottom

    // y軸の数値表示の最小値を 0 にする。
    plotLength.rightAxis.axisMinimum = 0.0
    plotLength.leftAxis.axisMinimum = 0.0

    // y軸の数値表示の少数点を消去する。
    plotLength.rightAxis.granularityEnabled = true
    plotLength.rightAxis.granularity = 1.0
    plotLength.leftAxis.granularityEnabled = true
    plotLength.leftAxis.granularity = 1.0

    // 枠をはっきりと表示させる。
    plotLength.drawBorderEnabled = true

    // オフセットの設定。
    plotLength.setExtraOffsets( left: 0, top:0, right:0, bottom:0 )

  } 
}

// バーの上に表示される数値の少数点を表示させないようにする。
public class BarChartValueFormatter: NSObject, IValueFormatter {
  public func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler? ) -> String {
    return String( Int( entry.y ) )
  }
}

// x軸に対応する数値をセットする。
public class BarChartFormatter: NSObject, IAxisValueFormatter {
  var xLabel = [ String ]()

  var start_val = 0.0
  var end_val = 0.0
  var bin_num = 0
  var bin_width = 0.0

  init(_ start: Double, end: Double, num: Int ){
    start_val = start
    end_val = end
    bin_num = num
    
    if bin_num > 0 {
      bin_width = ( end_val - start_val ) / Double( bin_num )
    }
    for i in 0..<bin_num {
      xLabel[i] = String( format: "%.1lf", start_val + Double( i ) * bin_width )
    }
  }

  public func stringForValue( value: Double, axis: AxisBase? ) -> String {
    return xLabel[ Int( value ) ]
  }

図2. plotLength の表示。ただし、上についている “Length Distribution” の文字は、別に UILabel を配置することで実現している。

Xcode プロジェクトのコピーによる iOSアプリの複製

プロジェクト名を変更したり、バックアップを作っておくためにプロジェクトの複製を作る必要が出た場合は以下のように対処する。

コピー元の名前を”Old”、コピー先の名前を”New”とする。

Finder で、ディレクトリのコピー、リネーム

Finder で、Old をコピーして名前をNewにリネームする。

図1. Finder で “Old” を コピーして新しくディレクトリを作成し、名前を “New” に変更。

Xcode で、PROJECTの名前を変更

図1で、New の下にある、Old.xcodeproj (もしくは、Old.xcworkspace) をクリックして Xcode を立ち上げ、図2左側の赤枠部分をクリックし、図2右側の赤枠部分の “Old” を “New” に変更する。

図2. 左側の赤枠で示されているPROJECT の下にある “Old” をクリックし、右側の赤枠で示されている Identity and Type の下の Name: “Old” の “Old” を “New” に変更。 

“New” に変更すると図3のようなダイアログが立ち上がるので、”Rename” を選択する。

図3. ダイアログが立ち上がってくるので、”Rename” をクリック。

次に、図4にあるように左端のディレクトリ”Old” を選択して、右端の “Old” を “New” に変更する。

図4. 左側赤枠で示されているOldディレクトリを選択して、右側赤枠で示されている名前を New に変更。

Xcode で、info.plist の変更

図5のように、”General” の中で、”Choose info.plist Files…” をクリックし、図6のように、”New” の下にある info.plist を選択して、”Choose” をクリックする。

図5. 上赤枠にある “General” を選択し、中央赤枠にある “Choose info.plist File…” をクリック。
図6. “New” の下にある info.plist を選択し、”Choose” をクリック。

Build Settings の変更

iOS アプリで OpenCV を使う場合は、C++ の OpenCV を Objective C++ でラップして使うことになるので、bridging heade を New にする。Build Settings を選択し、図7のように検索窓から “Swift Compiler” を検索し、Objective-C Bridging Header の欄に残っている Old を New に変更する。

図7. Swift Compiler – General の変更。

検索窓から、”Framework Search Paths” を検索し、Old を New に変更する。 ここが変更されていないと、OpenCV のライブラリをインポートしている部分 (#import <opencv2/opencv.hpp> ) で、’opencv2/opencv.hpp’ file not found エラーが出る。

図8. Framework Search Paths の  Old を New に変更。

Cocoapods の修正

ターミナルでプロジェクトのあるディレクトリに移動し、Podfile をエディターで編集して ‘Old’ の部分を ‘New’ に変更する。

target 'New' do
  pod'Charts','~>3.0'

  use_frameworks!

end

この後、インストールし直す。

$ pod install

OpenCV による飛跡の認識(1)

動画に対して、1フレームごとに以下のような処理を施します。

UIImage フォーマットの image を Mat フォーマットに変換した後、cvtColor で gray scale にします。

cv::Mat mat;
UIImageToMat( image, mat );
cv::Mat mat_gray = mat.clone();
cv::cvtColor( mat_gray, mat_gray, CV_BGR2GRAY );

次に、gray scale にした mat_gray を2値化します。

cv::Mat mat_bin = mat_gray.clone();
cv::cvtColor( mat_bin, mat_bin, threshold, 255, CV_THRESH_BINARY );
 

gray scale は、各ピクセルの値が0(黒)~255(白)の間の値をとっていますが、ある値を境界に黒(0)と白(255)の2値のみに変換します。ここでは、”threshold” でその境界値を与えています。

(左図) 霧箱画像を gray scale に変換したもの。(右図) 2値化に変換したもの。

上図は、x=650~960, y=360~480 あたりに横向きに走る白い直線状の飛跡と、大小様々な大きさのたくさんの白いドットが映っています。下図は、2値化の様子を詳しく見たものです。threshold は、バックグラウンドノイズ(白いドット)をできるだけ拾わないようにしながら、必要となる飛跡を拾うように選択します。

(左上) x=650~960, y=360~480 あたりにある、横向きの直線状の白い飛跡が、赤点線で囲われている。(左下) 左上の赤点線領域を y軸上に射影した1次元ヒストグラム。(右図) threshold = 100 で2値化した図。

上図からわかるように、飛跡を拾うように threshold を100まで下げると、多くのバックグラウンドノイズも合わせて拾ってしまっています。このバックグラウンドノイズが時間的に変化しないという特徴を持っていれば、各ピクセル値の時間平均を差し引くことで除くことが可能です。それに関しては後程説明します。

次に、2値化で白に選ばれた領域の境界点を探します。

cv::findContours( mat_bin, contours_raw, CV_RETR_LIST, CV_CHAIN_APPROX_NONE );

(左図) findContours で見つかった境界点を全て図示したもの。ここではわかりやすくするために各領域ごとに色を変えて表示しています。 (右図) 境界点のうち、点の数が80以上の領域のみを表示したもの。

上左図の例では、バックグラウンドノイズが多いため、findContours で見つかった境界点の数が膨大になってしまっています。境界点の数が80以上のもののみを図示したのが上右図で、欲しい飛跡が抽出されています。しかし、このような抽出方法では短い飛跡が拾えなくなってしまうため、いずれにせよ、バックグラウンドノイズを落とすことは重要になります。下図に、2本の飛跡をズームインしたものを載せます。

選択された飛跡のズームイン画像。緑と赤の点は、findContours で返された点の集合を表す。

最後に、直線によるフィッティングを行い、直線らしさの判定、フィットで得られた直線の長さ等を得ます。

for( int i = 0; i < contours_raw.size(); i ++ ){
  cv::Vec4f line;
  cv::fitLine( contours_raw[i], line, CV_DIST_L2, 0, 0.01, 0.01 );
}

下図に、フィットで得られた直線をシアン色で合わせて表示します。

フィットで得られた直線をシアン色で表示。