公式を使わなくたって、図形描いて点の数を数えたら面積になるんじゃない?っていうお話です。今回は円の面積を求めてみようと思います。
公式による円の面積の求め方
円の面積は、 円周率 * 半径**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 関係のライブラリを事前にインストールしておきます。
sudo apt-get install libcairo2-dev libjpeg-dev libgif-dev
npm install -g canvas
プログラムはこれ
以下がプログラムです。キャンパスに半径 1000 の白色の円を描いてキャンパス内の白色のピクセル数を数えています。
'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 のキャンバス内をピクセルを数えることになるので、速度的にどうかなって思っていたのですが、一瞬で終了しました。さすがです。
で、気になるその結果は…、以下のとおりです。
$ 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 にして計算してみました。…ちょこっと精度があがりました。
$ 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」の問題 3 は以下のような図形の黒色の面積を求めるというものです。その面積をピクセル数を数えて求めてみます。ホームページには計算の仕方や答えも書いてあるので比較しやすそうです。
プログラムはこれ
でもってプログラムは以下のとおりです。ホームページでは 10cmx10cm でしたが、プログラムでは、 1000x1000 のサイズで描画して集計して、それを最後に 10000 で割ることで求めています。
'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² と一緒になりました!おー!
$ 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
結局どう?
私個人的には使える!って思うのですが、実行する環境によって結果が変わっちゃいますし、「点の数を数えました」よりも「公式を使いました」の方が信頼されるかもしれないので努力は報われないかもしれないです。意味がないって言われちゃうかもしれないです。
でも、楽しかったので、ま、私的にはいいかなって、自己満足に浸ることができたんだし。ということでめでたしめでたし、です。