6
5

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 3 years have passed since last update.

Web上でパーティクルフィルタのデモをJavaScriptで簡単に実装してみる.

Posted at

目的

パーティクルフィルタを勉強したので、JavaScriptで動かしてみる.
といっても最新のJavaScriptに明るいわけではないので、実装が雑なのはお許しを...

めちゃくちゃ単純に実装するのが今回の目標.

参考

下記のサイトと動画を参考に実装しています.

できたもの

長方形の位置を予測するパーティクルフィルタ.
4つのランドマークからの距離を取得して、尤度を計算.
赤、緑、青、黄色のランドマークは動かないものとして、
長方形だけは動かせられる.

ezgif.com-gif-maker.gif

予測→更新(重み)→リサンプリング
を繰り返し.

尤度の計算は4つのランドマークからの距離から計算.
システムモデルは前回の位置に移動量とノイズを加えたのみ

やりたいことはできてそう.
思ったよりもパラメータの調整が必要だった...

実装

コードはやや汚いけど、このまま載せておく

HTML

particle_filter.html

<html>
  <head>
    <script src="https://unpkg.com/konva@8.2.3/konva.min.js"></script>
    <meta charset="utf-8" />
    <title>Simple ParticleFilter Demo</title>
    <style>
      body {
        margin: 0;
        padding: 0;
        overflow: hidden;
        background-color: #f0f0f0;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>
  </body>
</html>

JavaScript

particle_filter.js
      var particleNum = 100;
      var defaultRadius = 10;
      var animationInterval = 2000;
      var controlSigma = 200;
      let radiusScaler = 20.0;
      let observationNoise = 5;

      

      function writeMessage(message) {
        text.text(message);
      }
      var width = window.innerWidth;
      var height = window.innerHeight;

      var stage = new Konva.Stage({
        container: 'container',
        width: width,
        height: height,
      });

      var layer = new Konva.Layer();

      var text = new Konva.Text({
        x: 10,
        y: 10,
        fontFamily: 'Calibri',
        fontSize: 24,
        text: '',
        fill: 'black',
      });


      
      var circles =[];
      for(var i = 0; i < particleNum; i++){
	  circles[i] =
	      new Konva.Circle({
		  x:  stage.width() * Math.random(),
		  y:  stage.height() * Math.random(),
		  radius: defaultRadius,
		  fill: 'red',
		  stroke: 'rgb(250, 200, 200)',
		  strokeWidth: 2,
	      });
      }

      var lastx=0,lasty=0;
      var speedx=0,speedy=0;

      function calculate4Length(centerx, centery){
	  let lengths = [];
	  for(let i = 0; i < 4; i++){
	      let diffx = centerx - hexagons[i].x();
	      let diffy = centery - hexagons[i].y();
	      lengths[i] = Math.sqrt(diffx * diffx + diffy * diffy);
	  }

	  return lengths
      }
      
      function getObservation(){
	  let boxCX = box.x() + box.width()/2;
	  let boxCY = box.y() + box.height()/2;
	  let lengths = calculate4Length(boxCX, boxCY);

	  for(let i = 0; i < 4; i++){
	      lengths[i] += observationNoise * (Math.random()-0.5)
	      if(lengths[i] < 0){
		  lengths[i] = 0;
	      }
	  }
	  
	  return lengths;
      }

      let likelihoodCoeff = 0.03;
      function likelihood(cx, cy, observation){
	  let lengths = calculate4Length(cx, cy);
	  
	  let diffSum = 0;
	  for(var i = 0; i < 4; i++){
	      let diff = lengths[i] - observation[i];
	      diffSum += diff*diff;
	  }

	  diffSum = Math.sqrt(diffSum);

	  //Very Simple Likelihood Function
	  //Calculate each particle center to 4 landmarks,
	  //squared diff of observation and predicted lengths and finally get exponential value.
	  return Math.exp(-diffSum * likelihoodCoeff);
      }

      
      function predict(){
	  writeMessage('Predict');
	  
	  //Predict with System Model
	  for(var i = 0; i < particleNum; i++){
	      //Very Simple Model : just add latest speed and noise
	      circles[i].x( circles[i].x() + speedx + controlSigma * (Math.random()-0.5));
	      circles[i].y( circles[i].y() + speedy + controlSigma * (Math.random()-0.5));
	  }
	  
	  setTimeout(update, animationInterval);
      }

      var weights = [];
      function update(){
	  //Get Observation
	  let observation  = getObservation();
	  writeMessage('Update');

	  //Calculate Likelihood and Weights
	  weights = [];
	  for(var i = 0; i < particleNum; i++){
	      weights[i] = likelihood(circles[i].x(), circles[i].y(), observation);
	  }

	  let sumWeight = weights.reduce((sum, element) => sum + element, 0);

	  //Resize particles size with wieghts
	  for(var i = 0; i < particleNum; i++){
	      weights[i] = 1.0 * weights[i] / sumWeight;
	      circles[i].radius(Math.min(circles[i].radius() * radiusScaler * weights[i], 80));
	  }

	  setTimeout(resample, animationInterval);
      }

      function resample(){

	  //Resampling the particle position
	  let w = 0.0;
	  let windex = 0;

	  let backupCircleXYs = [];
	  for(var i = 0; i < particleNum; i++){
	      backupCircleXYs[i] = {x:circles[i].x(), y:circles[i].y()};
	  }
	  
	  for(var i = 0; i < particleNum; i++){
	      let unitsize = 1.0 * (i+1) / particleNum;
	      if( unitsize <= w ){
		  
	      }else{
		  //until w become bigger than unitsize
		  for(var j = windex+1; j < particleNum; j++){
		      w += weights[j];

		      if( unitsize <= w){
			  windex = j;
			  break;
		      }
		  }
	      }
	      
	      circles[i].x(backupCircleXYs[windex].x);
	      circles[i].y(backupCircleXYs[windex].y);
	  }
	  
	  //Restore particles size
	  writeMessage('Resample');
	  for(var i = 0; i < particleNum; i++){
	      circles[i].radius(defaultRadius);
	  }

	  setTimeout(predict, animationInterval);
      }
      setTimeout(predict, animationInterval);

      var lines = [];
      var hexagons = [];
      var colors = ["red", "green", "blue", "orange"];

      var box = new Konva.Rect({
        x: 20,
        y: 100,
        offset: [50, 25],
        width: 100,
        height: 50,
        fill: '#00D2FF',
        stroke: 'black',
        strokeWidth: 4,
        draggable: true,
      });

      box.on('dragmove', function () {
	  for(var j = 0; j < 4; j++){	  
	      lines[j].points([box.x()+box.width()/2, box.y()+box.height()/2, hexagons[j].x(), hexagons[j].y()]);
	  }

	  //Calculate Velocity
	  let currentx = box.x();
	  let currenty = box.y();
	  speedx = currentx - lastx;
	  speedy = currenty - lasty;

	  lastx = currentx;
	  lasty = currenty;
      });


      for(var j = 0; j < 4; j++){	  
	  hexagons[j] = new Konva.RegularPolygon({
	      x:  stage.width() * (j%2+1)/3,
	      y:  stage.height() * (Math.floor(j/2)+1)/3,
              sides: 6,
              radius: 50,
              fill: colors[j],
              stroke: "white",
              strokeWidth: 2,
	      shadowColor: 'black',
              shadowBlur: 0,
              shadowOffset: { x: 10, y: 10 },
              shadowOpacity: 0.5,
	  });


	  // dashed line
	  lines[j] = new Konva.Line({
              points: [box.x()+box.width()/2, box.y()+box.height()/2, hexagons[j].x(), hexagons[j].y()],
              stroke: 'green',
              strokeWidth: 2,
              lineJoin: 'round'
	  });

	  layer.add(hexagons[j]);
	  layer.add(lines[j]);
      }

     
      layer.add(text);
      layer.add(box);
      for(var i = 0; i < particleNum; i++){
	  layer.add(circles[i]);
      }      

      // add the layer to the stage
      stage.add(layer);

6
5
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
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?