歩いて行ける距離だと思ったので、歩いて行きました。
しんどかった。
みなさんはバスでの移動をオススメします。
バスで行く場合は地下鉄福住駅から直通のバスがあります。
クラーク像、けっこうかっこいいね。
放牧されていた羊。
近くに行くと思った以上に鼻息が荒いです。
フンフンいっているのが聞こえます。
レストランではジンギスカン食べ飲み放題ができます。
1人4100円。
比較的お手頃なお値段ではないでしょうか。
この記事のソースが余りにも冗長すぎるので、書き換えました。
こんな感じです。
CONTENT_TYPE = {'.html': 'text/html; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.js': 'text/javascript', '.json': 'application/json',
'.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif',
'.css':'text/css'}
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == '/Streaming':
enc = sys.getfilesystemencoding()
_, img = cap.read()
resized_img = cv2.resize(img, (480, 320))
_, encoded_img = cv2.imencode('.jpg', resized_img, [int(cv2.IMWRITE_JPEG_QUALITY), 30])
dst_base64 = base64.b64encode(encoded_img).decode('utf-8')
data = {
'image': 'data:image/jpg;base64,' + dst_base64
}
encoded = json.dumps(data).encode()
self.send_response(HTTPStatus.OK)
self.send_header("Content-type", "text/html; charset=%s" % enc)
self.send_header("Access-Control-Allow-Origin", "null")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
elif parsed.path.endswith('/'):
self.send_response(HTTPStatus.OK)
with open('index.html',mode='br') as f:
data = f.read()
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(bytes(data))
else:
self.send_response(HTTPStatus.OK)
filepath = '.' + parsed.path
with open(filepath, mode='br') as f:
data = f.read()
self.send_header("Content-type", CONTENT_TYPE[pathlib.Path(filepath).suffix])
self.end_headers()
self.wfile.write(bytes(data))
まず、URLが”/”の場合はindex.htmlを返します。
そして、URLが”/Streaming”の場合はカメラの映像をbase64で返します。
それ以外の場合(今回で言うと、jsファイル達)、URLで指定されたファイル名と同じファイルを返します。
ただ、その場合、content-typeを正しいものにしないとブラウザが正しく動いてくれないので、ファイル拡張子に対応する適切なcontent-typeを設定するようにしています。
それを対応づけているのがCONTENT_TYPEです。
こうすることで、今後jsファイル以外のものにも対応出来ます。
一般的なHTTPサーバに比べれば不十分ですが、今回目指しているのはそれではないので、こんなもんで十分でしょう。
ソースはgitHubに公開しました。
https://github.com/takishita2nd/RemoteCamera

自分は一歌推しです。
一応、今実装されている曲のEXまでの譜面を一通りプレイ終わりました。
今はほぼ毎日新曲が追加実装されているのですが。
ストーリーはなかなか精神えぐってきます。精神疾患者には少々辛い。
楽曲レベルは、ほぼガルパと同じ基準だと思って問題無いでしょう。
マスタークラスになると平気で難易度30以上が出てきますが、おそらくガルパの六兆年(レベル29)より難しいと思います。
クリアできる気がしません。
ライブのリトライ機能はなかなか神仕様ですね。
プレイ中にリトライすると、直接、曲の開始に戻ることができるだけでも嬉しいのですが、
ライブポイント(ブースト)はリザルト画面で消費されるので、リトライしてもリタイヤしてもポイントは減りません。
クリアできるまで何度でも挑戦できます。できるものなら。
ノーツのアイコンが横棒であるためか、他のリズムゲームより目が疲れる気がします。
スキンのカスタマイズとかできれば良いのですが。
老眼には少々きついかもしれません。
バーチャルライブもなかなか良いです。
満員で入れないときはブチ切れそうになりますが。

実際にじゃんけんバトルを実装していきます。
protected override void OnUpdated()
{
asd.Vector2DF pos = asd.Engine.Mouse.Position;
switch (_status)
{
case GameStatus.SelectDeffenceAction:
cycleProcessSelectDeffenceAction(pos);
break;
case GameStatus.SelectAttackAction:
cycleProcessSelectAttackAction(pos);
break;
case GameStatus.ShowActionResult:
cycleProcessShowActionResult(pos);
break;
}
if (asd.Engine.Mouse.LeftButton.ButtonState == asd.ButtonState.Push)
{
switch (_status)
{
case GameStatus.SelectDeffenceAction:
onClickMouseSelectDeffenceAction(pos);
break;
case GameStatus.SelectAttackAction:
onClickMouseSelectAttackAction(pos);
break;
case GameStatus.ShowActionResult:
onClickMouseShowActionResult(pos);
break;
}
}
}
private void cycleProcessSelectDeffenceAction(asd.Vector2DF pos)
{
if(_player == Player.Deffence)
{
_image_gu_deffence.Show();
_image_choki_deffence.Show();
_image_par_deffence.Show();
_image_gu_deffence.OnMouse(pos);
_image_choki_deffence.OnMouse(pos);
_image_par_deffence.OnMouse(pos);
}
else
{
var r = Singleton.Random;
switch(r.Next(0, 3))
{
case 0:
selectedDeffece = BattleIcon.Icon.Gu;
break;
case 1:
selectedDeffece = BattleIcon.Icon.Choki;
break;
case 2:
selectedDeffece = BattleIcon.Icon.Par;
break;
}
_status = GameStatus.SelectAttackAction;
}
}
private void cycleProcessSelectAttackAction(asd.Vector2DF pos)
{
if (_player == Player.Attack)
{
_image_gu_attack.Show();
_image_choki_attack.Show();
_image_par_attack.Show();
_image_gu_attack.OnMouse(pos);
_image_choki_attack.OnMouse(pos);
_image_par_attack.OnMouse(pos);
}
else
{
var r = Singleton.Random;
switch (r.Next(0, 3))
{
case 0:
selectedAttack = BattleIcon.Icon.Gu;
break;
case 1:
selectedAttack = BattleIcon.Icon.Choki;
break;
case 2:
selectedAttack = BattleIcon.Icon.Par;
break;
}
_status = GameStatus.ShowActionResult;
}
}
private void cycleProcessShowActionResult(asd.Vector2DF pos)
{
_attackResult.SetIcon(selectedAttack);
_attackResult.Show();
_deffenceResult.SetIcon(selectedDeffece);
_deffenceResult.Show();
}
private void onClickMouseSelectDeffenceAction(asd.Vector2DF pos)
{
if (_player == Player.Deffence)
{
if (_image_gu_deffence.IsOnMouse(pos))
{
selectedDeffece = BattleIcon.Icon.Gu;
}
else if (_image_choki_deffence.IsOnMouse(pos))
{
selectedDeffece = BattleIcon.Icon.Choki;
}
else if (_image_par_deffence.IsOnMouse(pos))
{
selectedDeffece = BattleIcon.Icon.Par;
}
_image_gu_deffence.Hide();
_image_choki_deffence.Hide();
_image_par_deffence.Hide();
_status = GameStatus.SelectAttackAction;
}
}
private void onClickMouseSelectAttackAction(asd.Vector2DF pos)
{
if (_player == Player.Attack)
{
if (_image_gu_attack.IsOnMouse(pos))
{
selectedAttack = BattleIcon.Icon.Gu;
}
else if (_image_choki_attack.IsOnMouse(pos))
{
selectedAttack = BattleIcon.Icon.Choki;
}
else if (_image_par_attack.IsOnMouse(pos))
{
selectedAttack = BattleIcon.Icon.Par;
}
_image_gu_attack.Hide();
_image_choki_attack.Hide();
_image_par_attack.Hide();
_status = GameStatus.ShowActionResult;
}
}
private void onClickMouseShowActionResult(asd.Vector2DF pos)
{
_attackResult.Hide();
_deffenceResult.Hide();
_status = GameStatus.SelectDeffenceAction;
}
シーンの状態は、
の順で遷移していきます。
防御側選択状態では、プレイヤーが防御側ならば、じゃんけんアイコンを選択します。プレイヤーが攻撃側ならば、ランダムで相手が何を選択したかを決定します。
攻撃側選択状態は、防御側選択の逆で、プレイヤーが攻撃側ならば、じゃんけんアイコンを選択、プレイヤーが防御側ならば、ランダムで相手が何を選択したかを決定します。
そして最後に攻撃側と防御側が選択したアイコンを表示し、じゃんけんの結果を表示します。
まぁ、ここまでは表示をどうするかの処理が中心になります。
次回はじゃんけんの結果を処理する実装を行います。
前回は、Webページをローカルで作成して、そこからブラウザに表示させていたのですが、
今回はWebページをラズパイに設置して、ブラウザからラズパイにアクセスすることによってプレビュー画面を表示させたいと思います。

動作はURLのパスによって変えます。
パスが”/”ならば、Webページを、”/Streaming”ならカメラの画像を返すようにします。
まず、Pythonのコードはこうなりました。
import base64
import cv2
import json
import os
import sys
import time
import threading
from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer
from http import HTTPStatus
from urllib.parse import urlparse
PORT = 8000
cap = cv2.VideoCapture(0)
def __main__():
thread = threading.Thread(target=httpServe)
thread.start()
try:
while cap.isOpened():
time.sleep(1)
except KeyboardInterrupt:
return
def httpServe():
handler = StubHttpRequestHandler
httpd = HTTPServer(('',PORT),handler)
httpd.serve_forever()
class StubHttpRequestHandler(BaseHTTPRequestHandler):
server_version = "HTTP Stub/0.1"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path == '/Streaming':
enc = sys.getfilesystemencoding()
_, img = cap.read()
resized_img = cv2.resize(img, (480, 320))
_, encoded_img = cv2.imencode('.jpg', resized_img, [int(cv2.IMWRITE_JPEG_QUALITY), 30])
dst_base64 = base64.b64encode(encoded_img).decode('utf-8')
data = {
'image': 'data:image/jpg;base64,' + dst_base64
}
encoded = json.dumps(data).encode()
self.send_response(HTTPStatus.OK)
self.send_header("Content-type", "text/html; charset=%s" % enc)
self.send_header("Access-Control-Allow-Origin", "null")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
elif parsed.path == '/vue.min.js':
self.send_response(HTTPStatus.OK)
with open('vue.min.js',mode='br') as f:
data = f.read()
self.send_header("Content-type", "text/javascript")
self.end_headers()
self.wfile.write(bytes(data))
elif parsed.path == '/jquery-3.5.1.slim.min.js':
self.send_response(HTTPStatus.OK)
with open('jquery-3.5.1.slim.min.js',mode='br') as f:
data = f.read()
self.send_header("Content-type", "text/javascript")
self.end_headers()
self.wfile.write(bytes(data))
else:
self.send_response(HTTPStatus.OK)
with open('index.html',mode='br') as f:
data = f.read()
self.send_header("Content-type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(bytes(data))
__main__()
無駄にでかくなった気がする。
というのも、使用しているjsファイルもラズパイからGETしようとブラウザが動くので、jsファイルを送るように作成しないといけないのです。
めんどくさいから、他のライブラリを使用することも考えないといかんなぁ。
続いて、html側。
こちらはそんなに難しくはない。
URLを変えるだけなので。
<!DOCTYPE html>
<html>
<head>
<title>My first Vue app</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="vue.min.js"></script>
<script src="jquery-3.5.1.slim.min.js"></script>
</head>
<body>
<div id="app">
<image id="camera" src="" />
</div>
<script>
var app = new Vue({
el: '#app',
data: {
timer: null,
},
created: function() {
self = this;
this.timer = setInterval(function() {self.onLoad()}, 50)
},
methods: {
onLoad: function() {
axios.get('http://pi4.local:8000/Streaming').then(function(response){
$("#camera").attr('src', response.data.image);
}).catch(function(error){
});
}
}
})
</script>
</body>
</html>

これで動作しました。

今日はチェックポイントが集中している札幌中心部を歩きました。

これのためだけに北大に行ってきた。
久しぶり(2回目)だけど、ここは相変わらず広いな。
思った以上に観光客多いし。
ここから南下してJR札幌駅も回収。

旧道庁。
この右手奥に本当の道庁があるので、ここも回収。

道庁の鴨とコイ。
この近くに札幌時計台があります。

さらに大通まで行って地下鉄大通駅とさっぽろテレビ塔を回収。
さらに南下して、ニューリフレSAPPOROと市電狸小路駅、すすきの駅、地下鉄豊水すすきの駅も回収しました。
とりあえず中心部の回収は終わったので、残るは、
モエレ沼、羊ヶ丘展望台、サンピアザ水族館、滝野すずらん丘陵公園、旭山記念公園の5箇所。
んー、どこから言っても良いけど、片っ端から回るのが良いのかな。
これから考えます。
データベースをサクッと作成していきます。
$ php artisan make:migration create_eating_history_item
class CreateEatingHistoryItem extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('eating_history_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->double('protein');
$table->double('liqid');
$table->double('carbo');
$table->double('calorie');
$table->timestamps();
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
Schema::create('eating_history_item_user', function (Blueprint $table) {
$table->integer('user_id')
->foreign('user_id')
->references('id')->on('users')
->onDelete('cascade');
$table->integer('eating_history_item_id')
->foreign('eating_history_item_id')
->references('id')->on('eating_history_items')
->onDelete('cascade');
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
Schema::create('eating_template_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->double('protein');
$table->double('liqid');
$table->double('carbo');
$table->double('calorie');
$table->timestamps();
$table->engine = 'InnoDB';
$table->charset = 'utf8mb4';
$table->collation = 'utf8mb4_unicode_ci';
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('eating_template_items');
Schema::dropIfExists('eating_history_item_user');
Schema::dropIfExists('eating_history_items');
}
}
$ php artisan migrate

class EatingHistoryItem extends Model
{
protected $table = 'eating_history_items';
}
class EatingTemplateItem extends Model
{
protected $table = 'eating_template_items';
}
class User extends Authenticatable
{
public function EatingHistoryItems()
{
return $this->belongsToMany('App\Model\EatingHistoryItem');
}
}
データの検索は、テンプレートからだけでなく、ユーザーが入力したヒストリデータからも取得できることを想定して構築してみました。
あと、開発環境がUbuntu 18.04から20.04になったのですが、
PHPのバージョンが7.2から7.4に変わりました。
たぶん、このままだとLaravelからデータベース(mysql)にアクセスできない(could not find driver)ので、php-mysqlをインストールし直す必要があるようです。
$ sudo apt-get install php-mysql
こちらの中で、スマホで表示すると画像サイズが変わるような事を書きましたが。
分かった。
sprite->getContentSize()で画像のサイズを取得することができるのですが、
Windowsとスマホではこの値が変わる。
なので、
クリッピングする時はPixel値を直接入力するのでは無くて、
このsprite->getContentSize()で取得した値を元にクリップするサイズを指定しなければならない。
auto sprite = Sprite::create("pipo-charachip001b.png");
if (sprite == nullptr)
{
problemLoading("'pipo-charachip001b.png'");
}
else
{
// position the sprite on the center of the screen
sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));
// add the sprite as a child to this layer
this->addChild(sprite, 0);
}
sprite->setScale(3.0);
auto str = String();
str.appendWithFormat("width %f height %f", sprite->getContentSize().width, sprite->getContentSize().height);
label->setString(str.getCString());
double partswidth = sprite->getContentSize().width / 3.0;
double partsheight = sprite->getContentSize().height / 4.0;
Vector<SpriteFrame*> animFrames;
animFrames.reserve(12);
animFrames.pushBack(SpriteFrame::create("pipo-charachip001b.png", Rect(0,0, partswidth, partsheight)));
animFrames.pushBack(SpriteFrame::create("pipo-charachip001b.png", Rect(partswidth * 1,0, partswidth, partsheight)));
animFrames.pushBack(SpriteFrame::create("pipo-charachip001b.png", Rect(partswidth * 2,0, partswidth, partsheight)));
Animation* animation = Animation::createWithSpriteFrames(animFrames, 0.1f);
Animate* animate = Animate::create(animation);
sprite->runAction(RepeatForever::create(animate));
return true;
あとはプラットフォームに合わせて拡大すれば良い。

解決。

もう一つやりたいのはフリック処理。
使用するハンドラはonTouchBeganとonTouchEndedの二つ。
タッチ開始の座標(sx,sy)とタッチリリースの座標(ex,ey)から、フリックの方向を求めるのですが、
上下左右の4方向だけだったら値の大小だけで良いかもしれませんが、
今回はアナログスティックの様な360°フリックの方向を出したいので、
ここでは三角関数を使用します。
アークタンジェント(tanの逆関数)を使用すれば、y/xから求めることができます。
関数名はatan2()です。
class SampleScene : public cocos2d::Scene
{
private:
cocos2d::Label* label;
cocos2d::Vec2 startPos, endPos;
auto listener1 = EventListenerTouchOneByOne::create();
listener1->onTouchBegan = [this](Touch* touch, Event* event)->bool
{
startPos = touch->getLocation();
return true;
};
listener1->onTouchEnded = [this](Touch* touch, Event* event)->bool
{
endPos = touch->getLocation();
double angle = atan2(endPos.y - startPos.y,endPos.x - startPos.x) * 180.0 / M_PI;
auto str = String();
str.appendWithFormat("angle %f", angle);
label->setString(str.getCString());
return true;
};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, this);
atan2が返す値の単位はラジアンなので、これに×180/PIとすることで角度に変換できます。
よし、これで必要なことは一通りできたかな。