円の面積を公式を使わずして点を数えて求める

公式を使わなくたって、図形描いて点の数を数えたら面積になるんじゃない？っていうお話です。今回は円の面積を求めてみようと思います。

## 公式による円の面積の求め方
円の面積は、 円周率 * 半径\*\*2 の公式で求めることができます。 円周率 を 3.14 、半径を 100 とするなら、 3.14 * 100\*\*2 = 31400 で 31400 と求めることができます。

## 点の数を数えて求める？どうするの？
黒色キャンパス内に白色の円を描いて塗りつぶした後で、キャンパス内の全ピクセルから白色であるピクセルの数を数えます。

半径 1000 なら、 2000x2000 のキャンパスを用意して 4000000 、つまり 4 百万のピクセルをしらみつぶしにチェックして白色であるピクセルの数を数えます。

正確かどうかは、どんだけ正確な円を描くことができて、それに対して十分な解像度があるかといった環境にめちゃくちゃ依存してしまうという、なかなかスリリングな話となります。

## じゃあやってみよう、環境はこれ
実行環境は以下のとおりです。 JavaScript の実行環境としてウェブブラウザじゃなくて Node.js を利用したのはタイムアウトとかを気にしなくて良いし、ファイル出力とかも楽かなっと思ったからです、

- Lubuntu 16.04(64bit) 
- Memory 4G
- Node.js v10.16.0
- node-canvas 2.5.0
- libcairo2-dev 1.14.6-1

Node.js で canvas を使えるようにするために node-canvas をインストールします。　node-canvas　は cairo を使用しているので、 cairo 関係のライブラリを事前にインストールしておきます。

```console:node-canvasのインストール
sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
npm install -g canvas
```

### プログラムはこれ
以下がプログラムです。キャンパスに半径 1000 の白色の円を描いてキャンパス内の白色のピクセル数を数えています。

```javascript:円を描いてピクセル数を数える
'use strict'
/*
sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
npm install -g canvas
*/

let radius = 1000;
if (process.argv.length>2) radius = parseInt(process.argv[2]);
const { createCanvas, Canvas } = require('canvas');
let canvas = createCanvas(radius*2, radius*2);
let ctx = canvas.getContext('2d',{ pixelFormat: 'A8' });	//8bitグレースケール

//図形を描画
ctx.antialias = 'none';
ctx.fillStyle = 'rgb(255,255,255)';
ctx.beginPath();
ctx.arc(radius, radius, radius, 0, 2 * Math.PI);
ctx.fill();

//集計
let countArea = 0;
for(let x=0;x<radius*2;x+=1000) {
    for(let y=0;y<radius*2;y+=1000) {
        let image = ctx.getImageData(x,y,1000,1000).data;
        for(let i=0;i<image.length;i++) if (image[i]==255) countArea++;
    }
}

//比較のため公式を使って面積を計算
let formulaArea = Math.floor(Math.PI*radius**2);

//結果を表示
console.log(`radius       = ${radius}`);
console.log(`countArea    = ${countArea}`);
console.log(`formulaArea  = ${formulaArea}`);
console.log(`absolute_err = ${Math.abs(countArea-formulaArea)}`);
console.log(`relative_err = ${Math.abs((countArea-formulaArea)/formulaArea)}`);

//試しに算出した結果から逆算して円周率を計算してみる
let pi = countArea/radius**2;
console.log(`pi = ${pi}`);
```

### さぁ実行してみよう！
さぁて実行です！ 2000x2000 のキャンバス内をピクセルを数えることになるので、速度的にどうかなって思っていたのですが、一瞬で終了しました。さすがです。

で、気になるその結果は…、以下のとおりです。

```console:実行結果
$ time node circle.js 
radius       = 1000
countArea    = 3141577
formulaArea  = 3141592
absolute_err = 15
relative_err = 0.0000047746492860944385
pi = 3.141577

real	0m0.164s
user	0m0.140s
sys	0m0.024s
```

おー！なんということでしょう！公式で求めたのが 3141592 で、ピクセルの数を数えた結果が 3141577 ！想像以上の良い結果です！

さらに試しに逆算した円周率も四捨五入したらなんと 3.14 に！ node-canvas というか cairo がそこまで正確な円を描いているとは想像もしていなかったので、かなりの驚きです。

## 半径を大きくすると正確になる？
もしかして半径をもっと大きくすればもう少し正確になるかと思い、半径を 10000 にして計算してみました。…ちょこっと精度があがりました。

```console:半径10000で計算
$ time node circle.js 10000
radius       = 10000
countArea    = 314158637
formulaArea  = 314159265
absolute_err = 628
relative_err = 0.0000019989860875183802
pi = 3.14158637

real	0m1.739s
user	0m1.424s
sys	0m0.336s
```

## もうちょっと調子に乗ってみる
思った以上に良い結果が出たので、調子にのって、もうちょっと複雑な図形でも試してみようと思います。

## ターゲットは？
「[面積の求め方（第3回）～葉っぱ型図形の面積 | 学びの場.com](https://www.manabinoba.com/math/6520.html)」の問題 3 は以下のような図形の黒色の面積を求めるというものです。その面積をピクセル数を数えて求めてみます。ホームページには計算の仕方や答えも書いてあるので比較しやすそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128670/17866eb9-dc3a-7394-9648-cf3e3ac58e30.png)

### プログラムはこれ
でもってプログラムは以下のとおりです。ホームページでは 10cmx10cm でしたが、プログラムでは、 1000x1000 のサイズで描画して集計して、それを最後に 10000 で割ることで求めています。

```javascript:問題3
'use strict'
/*
sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
npm install -g canvas
*/

//https://www.manabinoba.com/math/6520.html

let radius = 500;
if (process.argv.length>2) radius = parseInt(process.argv[2]);
const { createCanvas, Canvas } = require('canvas');
let canvas = createCanvas(radius*2, radius*2);
//let ctx = canvas.getContext('2d');
let ctx = canvas.getContext('2d',{ pixelFormat: 'A8' });	//8bitグレースケール

//図形の描画
ctx.antialias = 'none';
ctx.globalCompositeOperation = "xor";
ctx.fillStyle = "rgb(255,255,255)";
ctx.beginPath();
ctx.arc(0, radius, radius, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(radius, radius*2, radius, 0, Math.PI*2);
ctx.fill();
ctx.beginPath();
ctx.arc(0, radius*2, radius*2, 0, Math.PI*2);
ctx.fill();

//集計
let countArea = 0;
for(let x=0;x<radius*2;x+=1000) {
	for(let y=0;y<radius*2;y+=1000) {
		let image = ctx.getImageData(x,y,1000,1000).data;
		for(let i=0;i<image.length;i++) if (image[i]==255) countArea++;
	}
}
countArea/=(radius*2/10)**2;

//比較のため公式を使って面積を計算
let formulaArea = 10*10 * ((10*10*Math.PI/4 - 10*10/2) * 2 / 100) / 2;

//結果を表示
console.log(`radius       = ${radius}`);
console.log(`countArea    = ${countArea}`);
console.log(`formulaArea  = ${formulaArea}`);
console.log(`absolute_err = ${Math.abs(countArea-formulaArea)}`);
console.log(`relative_err = ${Math.abs((countArea-formulaArea)/formulaArea)}`);

//確認用
const fs = require('fs');
const out = fs.createWriteStream("fig3.png");
const stream = canvas.createPNGStream();
stream.pipe(out);
out.on('finish', () =>  console.log('The PNG file was created.'));
```

### さぁ実行してみよう！
では実行！…あいかわらずあっけないくらい一瞬で終わってしまいました。で、で、気になる結果はというと…、 じゃじゃん 28.5389 となりました！小数点以下第2位で四捨五入すれば 28.5 となり、ホームページでの答えである 28.5cm² と一緒になりました！おー！

```console:結果
$ time node fig3.js 
radius       = 500
countArea    = 28.5389
formulaArea  = 28.539816339744835
absolute_err = 0.0009163397448332944
relative_err = 0.00003210741561630831
The PNG file was created.

real	0m0.173s
user	0m0.152s
sys	0m0.020s

```

## 結局どう？
私個人的には使える！って思うのですが、実行する環境によって結果が変わっちゃいますし、「点の数を数えました」よりも「公式を使いました」の方が信頼されるかもしれないので努力は報われないかもしれないです。意味がないって言われちゃうかもしれないです。

でも、楽しかったので、ま、私的にはいいかなって、自己満足に浸ることができたんだし。ということでめでたしめでたし、です。