yamaken1343’s blog

技術ブログもどき

GrowiのAPIを叩きたい

研究じゃなくてこういうことだけ無限にやりたい

まえがき

研究室でGrowiというWikiを使ってます.
github.com 自動でSlackの特定のチャンネルの投稿内容をWikiアーカイブすることになったので, その調査をします.

一応ドキュメントは存在しますが, docs.growi.org 手が回っていないようなのでソースを見ます.

API捜索

おそらくここにあるものがすべてかと思われます.*1
https://github.com/weseek/growi/blob/master/src/server/routes/index.js API名からあたりをつけます.

今回はページの更新をしたいので
https://github.com/weseek/growi/blob/master/src/server/routes/page.js
を見れば良さそうです.

仕様調査

ページの更新がしたいので, 旧ページのデータをもってくる Getpage API とページの更新を行う UpdatePage API を調査します.

Getpage API

https://github.com/weseek/growi/blob/master/src/server/routes/page.js

  • method: GET
  • endpoint: /_api/pages.get
> @api {get} /pages.get Get page data
> @apiName GetPage
> @apiGroup Page
>
> @apiParam {String} page_id
> @apiParam {String} path
> @apiParam {String} revision_id

とりあえずpathを指定しておけば良さそう

認証済みのブラウザからなら

https://demo.growi.org/_api/pages.get?path=/

認証してないブラウザ等からはAPI KEYをパラメータに渡してやれば良さそうです.

https://demo.growi.org/_api/pages.get?path=/&access_token=[ユーザー設定から確認できるAPI KEY]

ただし, API KEYに"+"等のURLに使えない値が含まれる場合はエンコードが必須です.
pythonのrequestsは勝手にやってくれました.

認証済みのブラウザからならトークンなしでも素直に通るので, もっと良いやり方があるかもしれません.

ちなみに叩くと以下のようなJSONが返ってきます.

{
  "page": {
    "status": "published",
    "grant": 1,
    "grantedUsers": [],
    "liker": [],
    "seenUsers": [
      "592f8f117f616a0019f77e74",
      "592f853a7f616a0019f77e6e",
      "592f9dd47f616a0019f77e7c"
    ],
    "commentCount": 0,
    "createdAt": "2019-03-20T21:57:34.092Z",
    "updatedAt": "2019-05-09T07:42:46.324Z",
    "_id": "5c9403b8ef06c40058a243e8",
    "path": "/",
    "creator": {
      "isGravatarEnabled": false,
      "isEmailPublished": true,
      "lang": "ja",
      "status": 2,
      "admin": true,
      "_id": "592f8f117f616a0019f77e74",
      "email": "admin1@crowi-plus.org",
      "username": "admin1",
      "name": "Admin 1",
      "createdAt": "2017-06-01T03:50:41.524Z"
    },
    "lastUpdateUser": {
      "isGravatarEnabled": false,
      "isEmailPublished": true,
      "lang": "ja",
      "status": 2,
      "admin": false,
      "_id": "592f9dd47f616a0019f77e7c",
      "email": "guest1@crowi-plus.org",
      "username": "guest1",
      "name": "Guest 1",
      "createdAt": "2017-06-01T04:53:40.461Z",
      "image": null
    },
    "redirectTo": null,
    "grantedGroup": null,
    "__v": 58,
    "revision": {
      "format": "markdown",
      "_id": "5cd3d9f6ef06c40058a245f7",
      "createdAt": "2019-05-09T07:42:46.323Z",
      "path": "/",
      "body": "# Overview\n\n ..."
      "author": {
        "isGravatarEnabled": false,
        "isEmailPublished": true,
        "lang": "ja",
        "status": 2,
        "admin": false,
        "_id": "592f9dd47f616a0019f77e7c",
        "email": "guest1@crowi-plus.org",
        "username": "guest1",
        "name": "Guest 1",
        "createdAt": "2017-06-01T04:53:40.461Z",
        "image": null
      },
      "hasDiffToPrev": true,
      "__v": 0
    },
    "hasDraftOnHackmd": false,
    "pageIdOnHackmd": "lcFmaxSxRXel2uwnGIWgIQ",
    "revisionHackmdSynced": "5cd3d9f6ef06c40058a245f7",
    "id": "5c9403b8ef06c40058a243e8"
  },
  "ok": true
}

UpdatePage API

  • method: POST
  • endpoint: /_api/pages.update
> @api {post} /pages.update Update page
> @apiName UpdatePage
> @apiGroup Page
>
> @apiParam {String} body
> @apiParam {String} page_id
> @apiParam {String} revision_id
> @apiParam {String} grant
>
> In the case of the page exists:
> - If revision_id is specified => update the page,
> - If revision_id is not specified => force update by the new contents.

全部さっきの Getpage API で取得できます.
revison_idを指定しないと新しい内容で強制的に上書きするって書いてますが, ないと通りませんでした.
grantは1が全体公開っぽい. ないとnullが指定されます. 青枠が表示される以外は全体公開と同様?

ページの更新に失敗するとokカラムにfalseの入ったjsonが返ってきます.
成功すると先程のgetで取得可能なjsonが返ってきます.

次回は実際に使ってみます.

*1:node.jsわからない

manで自作のマニュアルを読めるようにする

まえがき

ソフトウェアにはmanコマンドで見ることのできるマニュアルが付属していることがあります. 例: github.com sl.1 というのがそうです. せっかくなのでmanコマンドでマニュアルを読めるようにします.

環境: ubuntu18.04

注意: マニュアル自体は作らないので書式には触れません

手っ取り早い方法

末尾が1なら /usr/share/man/man1にファイルを配置すれば見れます.
ただ, 自作のコマンドを/usr/local/binに配置しないように普通ここに配置しないような気がします. するのかな?

好きな場所に配置する方法

/usr/share/man/と同様のディレクトリ配置をもつディレクトリを作成します. (必要なものだけで良いです)
日本語のmanが読みたい場合はjaディレクトリ以下に同様のものを作成します. 例:

~/man
├── man1
├── man2
├── man3
├── man4
├── man5
├── man6
├── man7
├── man8
└── ja
    ├── man1
    ├── man5
    └── man8

続いて, マニュアルを配置します. マニュアルファイル末尾が1ならman1, 5ならman5に配置してください.
先程のslの例だと日本語のマニュアルsl.1.jaが付属していますが, 末尾.jaを削除した上でjaディレクトリ以下のman1に配置してください.

$ man -M ~/man sl で読めたらokです.

最後に, 環境変数を通します. $MANPATHにディレクトリを指定してあげるのが良さそうです.
例: $ echo "MANPATH=$HOME/man" >> .bashrc

以上

Androidでhttp通信の挙動がおかしい

Androidのバージョンを上げたら詰まった https化の流れ, つらい

原因

英語版のサイトを見てください*1 developer.android.com

Note: The guidance in this section applies only to apps that target Android 8.1 (API level 27) or lower. Starting with Android 9 (API level 28), cleartext support is disabled by default.

だそうでした どうやらデフォルトではhttps通信を使用しなければならないように変わったようです

解決策

http通信を有効にするには以下のようにします developer.android.com

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    <application
        ...
        android:usesCleartextTraffic="true"
        ...
        >

これで良いはずです.

参考:

stackoverflow.com

症状

WebViewのURIをhttpで指定してたのだけれど, 異様に動画の読み込みが遅いので困っていた Couldn't open requested URIとかjava.io.FileNotFoundException: No content providerとか出てたが関係あるのかしら 今回の方策を適用してもまだ出るしな

*1:日本語版にはないのでやっぱり英語のドキュメント当たらないとだめですね

正規表現で日本語文中の半角カンマ/ピリオドを全角に置き換える

全角スペースとか全角カンマとかプログラム中に混じると発狂してしまうので普段は直接入力できないようにしていますが, 日本語文中で使うとベースラインが崩れたりしていやらしいです.

しかしながら, あとで直接変換しようとすると, 英文が文中に混じっていたりすると非常に面倒です.

というわけで, 日本語文中の半角カンマ/ピリオドのみを全角に置き換えたいです.

後読みの正規表現を駆使して, 日本語の直後に存在する半角カンマ/ピリオドのみを変換します. 具体的には

s/(?<=[一-龠ぁ-んァ-ヶ]),/,/g

でいける(はず)です.

シェルスクリプトだと以下 sedだと-rでもダメそうだったのでperlです. sedgnu版とbsd版で差があったりするので常にperlで書いたほうが良いかもしれないですね.

#!/bin/sh

cat $1 | perl -pe 's/(?<=[一-龠ぁ-んァ-ヶ]),/,/g' | perl -pe 's/(?<=[一-龠ぁ-んァ-ヶ])\././g'

標準出力に出力されます. リダイレクトして上書きすると消えるので注意.

カッコも変換しておいたほうが良さそうですね.

#!/bin/sh

cat $1 | perl -pe 's/\((?=[一-龠ぁ-んァ-ヶ])/(/g' | perl -pe 's/(?<=[一-龠ぁ-んァ-ヶ])\)/)/g'

"("は先読みにしてます.

JavaScript で Google Cloud Speech-to-Text 音声認識

cloud.google.com

JavaScriptの練習がてらこれをブラウザ上で動作させます.
ページ内にサンプルがあると思いますが, あれの簡単なやつです.

もっとも, REST APIで公開されているのでそんなに難しくないです.
もうひとつgRPC形式のAPIが公開されてますが, 勉強中なので誰か教えてください.

準備

  • Google Cloud Platform にアカウントが必要なので取得
  • Cloud Speech APIを有効にしてAPIキーを発行

設計

  1. 音声取得
  2. base64エンコード
  3. JSON形式にしてAPIを叩く

実装

github.com

音声周りは素晴らしいライブラリがあるので利用します.
(最初は自分で書いてたけど挫折しました)

こちらの録音を吐き出す部分に上のエンコードAPI叩く部分を実装してあげれば良いです.

  • main.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>デモ用音声認識サンプル</title>
</head>
<body>
<button onclick="startRecording()">解析開始</button>
<button onclick="endRecording()">解析終了</button>
<hr>
<script src="api_key.js"></script>
<script src="./lib/recorder.js"></script>
<script src='main.js'></script>
</body>
</html>
  • main.js
let audio_context;
let recorder;

function arrayBufferToBase64(buffer) {
    let binary = '';
    let bytes = new Float32Array(buffer);
    let len = bytes.byteLength;
    for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
}

function startUserMedia(stream) {
    let input = audio_context.createMediaStreamSource(stream);

    recorder = new Recorder(input);
}


let startRecording = function () {
    audio_context.resume()
    recorder && recorder.record();
    console.log("record start")
};

let endRecording = function () {
    recorder && recorder.stop();
    console.log("record stop");

    // create WAV download link using audio data blob
    audioRecognize();

    recorder.clear();
};

function audioRecognize() {
    recorder && recorder.exportWAV(function (blob) {
        let reader = new FileReader();
        reader.onload = function () {
            let result = new Uint8Array(reader.result); // reader.result is ArrayBuffer
            let data = {
                "config": {
                    "encoding": "LINEAR16",
                    "sampleRateHertz": 44100, // 環境によってかわるっぽいので変えてください(おそらくエラーに正しい値が出てくると思います.
                    "languageCode": "ja-JP"
                },
                "audio": {
                    "content": arrayBufferToBase64(result)
                }
            };
            console.log("audio send...");
            fetch('https://speech.googleapis.com/v1/speech:recognize?key=' + apiKey, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json; charset=utf-8'
                },
                body: JSON.stringify(data)
            }).then(function (response) {
                return response.text();
            }).then(function (text) {
                let result_json = JSON.parse(text);
                //テキストデータ自体はresult_json.results[0].alternatives[0].transcriptに格納
                console.log("RESULT: " + text);
                console.log(result_json.results[0].alternatives[0].transcript);
                console.log(data)
            });
        };
        reader.readAsArrayBuffer(blob)

    });
}

window.onload = function init() {
    try {
        // webkit shim
        window.AudioContext = window.AudioContext || window.webkitAudioContext;
        navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;
        window.URL = window.URL || window.webkitURL;

        audio_context = new AudioContext;
    } catch (e) {
        alert('No web audio support in this browser!');
    }

    navigator.getUserMedia({audio: true}, startUserMedia, function (e) {
    });
};

同階層にAPIキーを保持するファイルapi_key.jsとして作成してください

var apiKey = 'XXXXXXXXXXXXX';

成果物は以下 github.com

結果

f:id:yamaken1343:20181020193213p:plain

あとがき

JavaScriptわからん
結局ほとんどコード書いてないので勉強になったか微妙

注意点

APIキーが見え見えなので動作はローカルにとどめておいてください.

追記

2019/4/9 Google Chrome の自動再生ポリシーの変更に対応
動作しない場合コメントをください.

自作キーボードHelixビルドログ

Helixにやっと慣れてきたのでこれから作ってみたいという方の参考になるようビルドログを残しておきます.

僕が作成したものは5行版のUNDERGLOWタイプになります.

Helixについて

f:id:yamaken1343:20180605233215j:plain

遊舎工房さんのかっこいい格子配列の分割キーボードです. 以下から購入できます.

Helix キーボードキット | 遊舎工房

フルセットのキットになっていて, 細かいパーツを集めなくてよいので良いです. 他に必要なものはキースイッチ, キーキャップ, キーボードの左右を接続するためのオスーオス3極ミニプラグケーブル, A-microBのUSBケーブルと入手難度が比較的優しいです. キースイッチとキーキャップについては遊舎工房さんで購入できます.

必要な道具について

最低限はんだごてとはんだ, ピンセット, 精密プラスドライバがあれば作成できます. 僕はそれに加えてはんだ吸い取り線とテスターとマスキングテープを用意しました. 以下に詳しいので一読をおすすめします.
ハンダはダイオード用の細いものとキースイッチ用の太いもの両方用意するといいかなと思います.

キーボード自作、特に Helix キーボードキットの製作に最低必要な工具のメモ · GitHub

購入物品

以下のものを購入しました. 1ドル110円で計算してます.

名称 購入場所 価格
Helixキーボードキット 遊舎工房 10044
Cherry赤軸65個 AliExpress 3520
フルサイズキーボード用キーキャップ AliExpress 1089
TRSケーブル*1 Amazon 999
プリント基板用半田 Amazon 200
テスター Amazon 1191
合計 17043

ja.aliexpress.com

ja.aliexpress.com

Amazon.co.jp: ステレオミニプラグ オーディオケーブル、RAYWILL 高音質再生 オス/オス無指向性 標準3.5mm AUX接続 (0.5m): 家電・カメラ : https://www.amazon.co.jp/gp/product/B072KDTCYR/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1

Amazon | goot 両面プリント基板用はんだ SD-61 | DIY・工具・ガーデン : https://www.amazon.co.jp/gp/product/B0029LGAKW/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1

Amazon | OHM(オーム電機) デジタルマルチテスター 普及型 TDX-200 (04-1855) | テスター | 産業・研究開発用品 通販 : https://www.amazon.co.jp/gp/product/B005BE4XZS/ref=oh_aui_detailpage_o01_s00?ie=UTF8&psc=1

組み立て

基本は公式のビルドガイドを見れば良いです. 非常にわかりやすいのでしっかり読めば作れると思います.
github.com

以下の記事はビルドガイドにないTips的なものとなります.
全体として共通するポイントはただひとつ, 向きを間違えないことです.

組み立て前に

デスクを綺麗にしましょう. また, ゴミ箱が机上にあると良いです. パーツを入れるためのトレイがあるとなお良いです.
これはこの項で最も重要なポイントですが, 自作キーボード初めての方はこの時点でQMKをビルドするためのツールのインストールをはじめてください. かなり時間がかかります. 手順は以下に詳しいです.

helix/firmware_jp.md at master · MakotoKurauchi/helix · GitHub

表面実装用ダイオードの実装

  1. ランドの片側に予備ハンダをする
  2. ピンセットでダイオードをつまみ, 予備ハンダを溶かしながら近づけ, 片側をハンダ付けする
  3. もう片方をハンダ付けする

裏返せば見えるので綺麗に付けたいですね. 殆どのハンダは作ったあとも見えます.
綺麗につけるポイントは流しこむハンダの量を一定にすることです. これは以下のハンダ付けすべてに共通します.
僕は綺麗にはつけられませんでした.

ダイオードの実装チェック

テスター持ってる方はダイオードの向きが正しいかチェックしましょう. テスターをダイオードチェックモードにして, プローブをリード線付きダイオード用のランドに当ててください.

f:id:yamaken1343:20180605210812j:plain

ProMicroのもげ対策

ProMicroのUSB端子は脆いらしく, 補強必須です.
ただでさえファームウェアの書き込みの際に抜き差しするので, しっかり補強しましょう.
他のビルドログを見ると, 瞬間接着剤とかエポキシ接着剤で強化してる人が多いですが, 僕はグルーガンで行いました.

f:id:yamaken1343:20180604234829j:plain

もしももげたらtwitterにアップすれば人気者になれると思います.

ProMicroとリセットスイッチを実装できたらファームを書き込んで動作確認ができます. ピンセットでキースイッチ用のランドをショートさせてください. ダイオードの実装チェックをしていればいらない作業ですが, テスター持ってないという人はここでチェックすると良いかなと思います.

OLEDディスプレイの実装

リセットスイッチとかより先にピンソケットを先に実装するほうが楽なのでおすすめです.

まっすぐ付けたいので, マスキングテープで真っ直ぐにしてからはんだづけすると良いです.
写真だと一箇所ですが, この付け方だと微妙に浮くので根元側も仮止めしてください.

f:id:yamaken1343:20180605211850j:plain

ハンダゴテを当てる向きを気をつけてください. ディスプレイが溶けます. 幸い機能に影響はありませんでしたが.

f:id:yamaken1343:20180605212036j:plain

キースイッチの実装

裏表を間違えないようにプレートにキースイッチをはめていきます. 全てはめたら裏返してキースイッチの足が曲がっていないか確かめます.

f:id:yamaken1343:20180605195601j:plain

足の修正後ハンダ付けをしますが, ロープロファイルスイッチのランドが近く, ブリッヂしてしまうと面倒なことになります. まあ外側のランドに関しては片方ブリッジしても大丈夫なので気楽にやりましょう.
また, ハンダ付けの際基盤とキースイッチの間に隙間ができないようにしっかりと押し込みましょう

テスター持ってる方はキースイッチがショートしてないかテストしましょう. ショートした状態でつなぐと面倒です.

f:id:yamaken1343:20180605201225j:plain

ファーム書き込んでテスト

事前にrules.mkを書き換えておかないと光ったりディスプレイがつかないので不安にならないでください.

f:id:yamaken1343:20180605225502j:plain

最終組み立て

ネジを立てる -> 底面プレートにはめる -> スペーサを手で締める -> 裏返して本締め -> 全部終わったらPCB側を乗っけてネジ止め が楽かなと思います.

穴が9個に対してネジ及びスペーサは6個しかないですが仕様です. 上下の3つづつを使いましょう.*2

キーキャップ取り付け

キーキャップ取り付けて完成になります. フルサイズ用のキーキャップセットを購入した人はテンキーとかファンクションキーも総動員して頑張って埋めてください. *3

以上になります. 次回はファームウェア編をする予定

この記事はHelixで書かれました.*4

*1:みんなこれ買ってるの面白い

*2:PCBに穴が9つあるのは4行版との共通化でわかるんだけど5行版専用のケースに穴が9つあるのは不思議.

*3:僕はその状態でダサい事この上ないのでなんとかしたい

*4:自作キーボードの記事の最後のこれが好き. 作ったキーボード以外のキーボードで書かれてるとなおいい

pythonで疎な二次元データをバイリニア法で補完する

何かいい感じのライブラリがあったら教えて欲しい

データについて

例えば, 以下のようなデータが対象になります.

f:id:yamaken1343:20180628190305p:plain

気象庁のサイトから持ってきた気温のデータですが, 気温が示されていない部分を補完し, もっともらしい温度を出力します.

本来は緯度経度等から正しい位置を持って来るべきですが, とりあえず適当に配置します. 0は未定義の値とします.*1

[[20.9  0.   0.   0.   0.   0.   0.  21.8]
 [ 0.  21.7  0.  21.9  0.   0.  21.2  0. ]
 [ 0.  21.7  0.  21.9  0.   0.   0.  20.6]
 [ 0.  22.3  0.   0.   0.   0.   0.   0. ]
 [22.   0.  22.8  0.  20.7  0.   0.  21.2]
 [ 0.  22.6  0.   0.   0.   0.   0.   0. ]
 [ 0.   0.   0.   0.   0.   0.   0.   0. ]
 [20.2  0.  23.2  0.  23.2 22.6  0.  22. ]]

色で表すと以下のようになります. 暗い領域は未定義を示します. f:id:yamaken1343:20180628190145p:plain

結果

こうなります.

[[20.9 21.2 21.6 21.8 21.6 21.5 21.6 21.8]
 [21.3 21.7 21.8 21.9 21.7 21.4 21.2 21.4]
 [21.6 21.7 21.8 21.9 21.6 21.4 21.  20.6]
 [22.1 22.3 22.4 21.8 21.  21.2 20.9 20.9]
 [22.  22.3 22.8 21.8 20.7 20.9 21.  21.2]
 [21.9 22.6 22.8 22.2 21.5 21.7 21.6 21.5]
 [21.  22.2 23.  22.6 22.4 22.1 21.8 21.7]
 [20.2 21.7 23.2 23.2 23.2 22.6 22.3 22. ]]

f:id:yamaken1343:20180628190127p:plain

コード

import numpy as np


def search_near(src, x, y):
    """
    データのある近傍4点を検索し, 座標を返す
    :param src: 検索する配列
    :param x: 基準点のx座標
    :param y: 基準点のy座標
    :return: 検索した4点を2*4のnumpy.arrayで返す
    """

    def min_pear(a, b, x, y):
        """
        基準点とリスト内の2点間の距離が最も小さいペアを返す
        :param a: x座標のリスト
        :param b: y座標のリスト
        :param x: 基準点のx
        :param y: 基準点のy
        :return: リスト内のペア
        """
        # 近傍に点が4点以下でもあるだけの点を用いて補完を行う
        # if len(a) == 0:
        #     return x, y

        add = np.power((a - x), 2) + np.power((b - y), 2)
        return a[np.argmin(add)], b[np.argmin(add)]

    # 処理簡便化のためデータの有無を2値で持つ
    src = np.array(src != 0)

    # 処理対象の点にデータがあるとき, その点を4近傍として返す
    if src[x, y]:
        return np.array([[x, y], [x, y], [x, y], [x, y]])

    try:
        # スライスされるため, indexに入る値はsrcのインデックスと互換性がないことに注意する
        # 左上
        index_x, index_y = np.where(src[0:x, 0:y])
        min_x, min_y = min_pear(index_x, index_y, x, y)  # 基準点は右下
        upper_left = (min_x, min_y)
        # 右上
        index_x, index_y = np.where(src[x:, 0:y])
        min_x, min_y = min_pear(index_x, index_y, 0, y)
        upper_right = (min_x + x, min_y)
        # 左下
        index_x, index_y = np.where(src[0:x, y:])
        min_x, min_y = min_pear(index_x, index_y, x, 0)
        lower_left = (min_x, min_y + y)
        # 左下
        index_x, index_y = np.where(src[x:, y:])
        min_x, min_y = min_pear(index_x, index_y, 0, 0)
        lower_right = (min_x + x, min_y + y)

        near_4_points = (upper_left, upper_right, lower_left, lower_right)

        return np.array(near_4_points)

    # データが見つからなくても止まらないようにする
    except:
        return None


def bilinear(src):
    """
    疎なデータを持つ配列に対しバイリニア補完を行う
    :param src: 入力配列
    :return: 補完後の配列
    """
    it = np.nditer(src, flags=['multi_index'])
    dst = np.zeros(src.shape)
    # 配列の各要素をなめる
    while not it.finished:
        idx = it.multi_index  # 現在参照する要素のインデックス
        it.iternext()

        # near four points 近傍四点を取得
        n4p = search_near(src, idx[0], idx[1])
        # 4点のどれかにデータがない場合の処理
        if n4p is None:
            continue

        # 参照する要素と近傍4点の距離を取得
        near_4_points_dist = np.array([np.linalg.norm(n4p[0] - idx), np.linalg.norm(n4p[1] - idx),
                                       np.linalg.norm(n4p[2] - idx), np.linalg.norm(n4p[3] - idx)])

        # 近傍点と参照点が同じ場合と近傍点がないときnanが入るので, 変換する
        near_4_points_dist[np.isnan(near_4_points_dist)] = 0

        # 参照要素とデータの点が重なった時の処理
        if near_4_points_dist.sum() == 0:
            dst[idx] = src[idx]
            continue

        # 近傍点と参照点が同じ場合無視したいので次の処理でゼロにするために代入
        near_4_points_dist[near_4_points_dist == 0] = np.inf

        # 小さいほうが値が大きくなるように逆数を取る(距離なので近いほうが良いというふうにしたい)
        near_4_points_score = 1 / near_4_points_dist

        # 和が1になるように正規化
        near_4_points_score /= near_4_points_score.sum()

        # 結果を格納する配列に代入
        for i in range(len(n4p)):
            a, b = n4p[i]
            dst[idx] += near_4_points_score[i] * src[a, b]

    return dst

def main():
    np.set_printoptions(precision=1)
    in = np.array([[20.9, 0, 0, 0, 0, 0, 0, 21.8],
                  [0, 21.7, 0, 21.9, 0, 0, 21.2, 0],
                  [0, 21.7, 0, 21.9, 0, 0, 0, 20.6],
                  [0, 22.3, 0, 0, 0, 0, 0, 0],
                  [22.0, 0, 22.8, 0, 20.7, 0, 0, 21.2],
                  [0, 22.6, 0, 0, 0, 0, 0, 0],
                  [0, 0, 0, 0, 0, 0, 0, 0],
                  [20.2, 0, 23.2, 0, 23.2, 22.6, 0, 22.0]])
    print(in)
    out = bilinear(in)
    print(out)


if __name__ == '__main__':
    main()

解説

7/20変更 左上領域右上領域左下領域右下領域が重ならないように範囲を設定しています
min_pearのすぐ下のコメントを外すと4点未満でもあるだけの点を用いて補完を行うようになります.
ヒートマップを作成するコードは省いています.

*1:本来はNoneとかを使うべき