林業応援ハッカソン~森と林業の未来をつくろう~
伊丹から鹿児島空港まで、ANAプレミアムクラスで。
やっぱりプレミアムは快適ですね。
鹿児島についたら、時間がかなり余る予定だったので、とりあえず足湯
それでもまだ暇なので、バスで霧島の麓にあるきのこの里までとぼとぼおふろに入りに行きました。
おふろ上がってきたらバスもないので、最寄り駅(徒歩45分)まで歩いて散策。
珍しい電車と古い駅舎がありました。
その後は高速バスに乗って会場へ。会場に前泊できるのは、運営GJです。
ついたら、また温泉入ってだらだらと。
談話室にを覗いたら、他の参加者が林業について話をしていたので、ちょっと混ざって情報収集。。。が結局2時過ぎまで喋ってました。
寝て起きておふろ入ってあさごはんたべて、さていよいよハッカソンへ。
まずお決まりの挨拶と長い自己紹介があったあと、今回の目的の1つになるフィールドワークへ。
操作はできないけど実際に重機に乗せてもらったり、木を切り倒して、丸太にするまでの過程を見せてもらいました。
かなり強引だったり、でも計算して切り倒さないと効率が悪かったり、結構危なかったりなどなど、、想像とは違ってなかなか発見の多いフィールドワークでした。
で、会場に戻ってきたら、いわゆる林業とはっていうお話を鹿児島大学の先生からしてもらいます。
林業の事故率や死亡率、市場規模とその割合などなど、、データに基づいたお話は発表時のプレゼンの信ぴょう性と共通理解に繋がるし、面白いところでした。
セッションが終わって、ここから休憩時間。
だけど、会場後方にはすでに資料だったりデバイスだったり材木だったり現場の方がいたりで。また情報収集。
実は最初から、植林から出荷まで35年かかる木にITを使って監視したりするのは相性が悪いなぁと思っていて、狙いを重機に絞っていました。
簡単に言うと、たっぷりセンサーなIoT重機ですね。それについてお話してるとコマツさんがそういう重機を作っているけど、まだまだ未知の領域らしい。
話を聞いているうちに、ネタが決まったので、この方向性でいくことに。
ところで今回もってきたおもちゃはこちら。
ArduinoとかGainerとからずぱいとかの電子工作開発セット
Kinectとかも持って行こうかなとおもったけど、ほかのデバイスはTMCNから結構かしてもらえるみたいなので、センサーとマイコン類にしぼりました。
Meshとかも便利だけど、高いし汎用性がないし、iOSしか対応してないのはだるいので、持ってきてよかったです。
休憩が終わって、アイデアソン。
他のアイデアソンでもそうなのだけど、だいたいハッカソンの手前にやるアイデアソンって長いしぐだるし、ネタももう決めてしまったので、席からはずれて個人プレーに移りました。
TMCNからTHETAを借りれたので、仕様を把握したり、撮れた映像を3Dモデルにマッピングしたりいろいろ技術検証を済ませて、デモの道筋までは立てておくことに。
はたから見てることにしたのでよかったけど、流石にアイデア交換は2回で十分だし、やっても3回のところを4回もやる必要はないんじゃないかなぁ。。。
そんなにやりたいなら、さらに時間つかって、間にブラッシュアップの時間取ったほうが、よっぽどマシだったんじゃないかなぁ。
ハッカソンの運営って立場上、ちゃんと手順を踏まないと、、って思うのはわかるけど、ハッカソンの進行慣れしてなさそうだなぁと思いました。
その後はアイデアを発表しないとチームが作れないとのことなので、簡単に魅力の伝わらないように仕様だけ説明。
重機にセンサーつけて、危険回避しまーす、ぐらい。
他のチームがビジネスモデルとかだったので、こっちの現実路線なお話には興味ないだろうなぁと思ったけど、案の定身内だけの最少人数チームになりました(
ただ、現場の人の目がすごくキラキラしてたので、イケる確信は十分にありました((
さて!いよいよ開発タイム!!!
他チームがホワイトボードで打ち合わせしてるのをスルーして、スーパーに連れて行ってもらいました。
会場が僻地なのと、今回は夜通し作業ができるので、まずは食料調達です。運営さんありがとー!
みんなで買い物して帰ってきたら、懇親会まで残り1時間。1時間だけ作業しても仕方ないしなーとおもって、さっさとまた温泉へ。
さっぱりしたら浴衣に着替えて、懇親会へー。
進捗報告会があったのだけど、この時点でTHETAのデモはもうできてたので、市長さんに頂いた球磨焼酎を飲みながら、開発すすんでまーすとジャブを打っておきましたb
ごはんをたべて夜8時からいよいよ自分のハッカソンをはじめることに。
と思ったけど、ふらっと覗いた体育館で大学生チームがバスケしてて誘われたので、浴衣で焼酎3杯飲んだあとにバスケすることに((
へろへろでバスケやったんは初めてやで。。。。
なんやかんやあって、ようやく開発を始めます。
Gainer+焦電型人感センター+THETAを組み合わせることにしたのだけど、ここでトラブル!
Gainerがひさびさすぎて、64bitのMacに対応してない!ElCapitanになってMacを32bitで起動する方法がない!VMだと3D重い!BootCampだとドライバ入ってなくて右クリックできない!!
というゴミ環境で作業することに。。。。ハッカソンの時はやっぱりマシンを2台は持ってくるべきだなぁ。。。
とりあえず不便な環境でBootCampのWindowsでコード書いてました。
ここで更にトラブル((
Processingで開発しててサンプルを改変していったのだけど、3Dの上に書いた2Dが消えてなくなる!!
この時点でUnityに移行するかなやんだけど、いろいろ試行錯誤した結果、背景への描画なら反映されることがわかったので、対処。
なんとか今日中に、Gainter + 焦電型人感センサー + THETAのデモが出来上がりました。
ここまで苦労するとは思わなかったけど、予定通りの進捗なので、一旦寝て。
朝ごはんスルーして起きて、また他のチームとは違って、予定してた2日目のフィールドワークへ。
まだ重機残ってるかなーと現場に向かいました。
何がしたかったかっていうと、作ったデモを実際に重機の上に乗っけて検証したかったんです。
反射してMacの画面が見にくいけど、いい感じに動作している状況が撮影できました。
www.youtube.com
この時点で発表までのこり5時間。
プレゼン資料はチームメイトにまるっとお任せしてたので、資料にフィードバックだけしてプレゼンターの色でやってもらうことに。
時間に余裕があったので、Linked OpenDataを内容に組み込んだり、デモ以外に取得できるデータとか、データの利用例をブラウザに表示したり、いろいろ肉付けしました。
こんなかんじ。
で、ぷれぜんー。
重機をIoT化すること -> 中央管理が可能 -> 安全性の向上 -> データの公開 -> 安全性の周知 -> 林業の発展 = IoT重機で他チームのビジネスモデルを支える林業の基盤づくりを提案します!っていう流れ。
www.slideshare.net
結果はー、、、、、今回のテーマである林業応援賞をいただきました。
残念ながら最優秀賞と30万は僅差でのがしてしまったようです、、、うぅ。。。いけるとおもったのになー。
でも、現場の人がほんとにこれやりたい!思い描いてた林業のそのものだ!!このたった2日で実現可能性を示してくれた!!!って言ってもらえたのが何よりです。
これ、もうちょっとまじめに開発して、ほんとに中の人と一緒に実地検証やりたいなぁ。
帰りは優秀賞を獲っていった大学生チームとうなぎ。今までさんざんいろんなところでうなぎたべたけど、柳川のうなせいろよりも、あつた蓬莱軒のひつまぶしよりも、どこよりもうまかった。。。。
口にいれた瞬間、声がでるうなぎは初めてでした。。。
大学生チームといろいろおもしろい話をしたり聞いたりして、お開きに。
次の日は、人吉のフレンチコースを食べて、球磨焼酎の蒸留所で散々試飲して、空港で鶏飯たべて、帰りました。
食べて飲んで、おふろはいってばっかやな。。。。
人吉、電車で遊びに来るにはなーんもないけど、なにたべても美味しいし、人はいいし、おふろはきもちいいし、いいとこでした。
また遊びにいきたいですね。
=============================================
今回のコード。
安定の「工学ナビ」さんのサンプルを拝借しました。
GitHub - kougaku/THETA-S-LiveViewer-P5: RICOH THETA S live viewer for Processing
import processing.gainer.*; import java.io.*; Gainer gainer; boolean isLive = true; ThetaSphere tsphere; PImage img; PCapture cam; int camera_id = 0; int angleX = 0; int angleY = 0; int fileNum = 0; boolean rotateFlag = true; int isInDengerZone = 100; PImage im; String generateLocation(){ double latitude = 32.2292236 + (Math.random() - 0.5); double longitude = 130.7817153 + (Math.random() - 0.5); double altitude = 175.0 + (Math.random() - 0.5); double heading = 225.0 + (Math.random() - 0.5); return "{\"latitude\":"+latitude+",\"longitude\":"+longitude+",\"altitude\":"+altitude+",\"heading\":"+heading+"}"; } String generateMachineInfo(){ return "{\"vin\":\"AA123456789\",\"manufacture\":\"HITACHI\",\"model\":\"XX-01K\"}"; } String generate(boolean emergency){ return "{\"image\":\"https://morihack.minamo.io/store/0000.png\",\"date\":" + (System.currentTimeMillis() / 1000) + ",\"isEmergency\":" + (emergency ? "true": "false") + ",\"location\":" + generateLocation() + "," + "\"accelerometer\":{\"x\":-0.3,\"y\":1.38,\"z\":9.77},\"driver\":{\"name\":\"yamada\",\"blood\":\"a\",\"health\":\"good\"}," + "\"machine\":"+generateMachineInfo()+",\"metadata\":{\"license\":\"MIT\",\"author\":\"takeda\"}}"; } void updateJson(boolean emergency){ FileWriter file = null; BufferedWriter osw = null; try{ String json = generate(emergency); file = new FileWriter("current.json"); osw = new BufferedWriter(file); osw.write(json); osw.flush(); osw.close(); }catch(Exception e){ try{ if(osw!=null){ osw.close(); }else if(file!=null){ file.close(); } }catch(Exception e2){ // supress double exception } } } void setup() { size(1280, 1440, P3D); frameRate(20); // live capture or image file cam = new PCapture( camera_id, 1280, 720 ); // sphere setup int k_div = 20; // division int s_div = 40; // division int sphere_r = 600; // sphere radius int xc1 = 310, yc1 = 320; // circle position in the image int xc2 = 960, yc2 = 320; // circle position in the image int r = 283; // circle radius tsphere = new ThetaSphere(k_div, s_div, sphere_r, xc1, yc1, xc2, yc2, r); gainer = new Gainer(this); gainer.beginAnalogInput(); im = loadImage("aa.jpg"); } void draw() { background(0); println(gainer.analogInput[0]); if (gainer.analogInput[0] > 100) { drawTheta(); updateJson(false); } else { resetMatrix(); background(255, 222, 0); image(im,0,0); updateJson(true); //textSize(32); //textAlign(CENTER, CENTER); //text( "Emergency Stop", width/2, height/2); } } double humanDetection() { return gainer.analogInput[0]; } void drawTheta() { img = cam.getImage(); resetMatrix(); rotateX( radians(angleY) ); rotateY( radians(angleX) ); rotateZ(PI/2); noStroke(); img.save("current.png"); tsphere.draw(img, gainer.analogInput[0] > 100); if (rotateFlag) { angleX+=2; } updateKeyInput(); } void updateKeyInput(){ if (!keyPressed) return; switch(keyCode){ case DOWN: angleY-=2; break; case UP: angleY+=2; break; } }
import com.github.sarxos.webcam.Webcam; import java.awt.image.BufferedImage; import java.util.List; import java.awt.Dimension; class PCapture { Webcam webcam; int width = 0; int height = 0; public PCapture(int id, int w, int h) { super(); List<Webcam> webcams = Webcam.getWebcams(); webcam = webcams.get(id); Dimension size = new Dimension( w, h ); webcam.setCustomViewSizes(new Dimension[] { size } ); webcam.setViewSize(size); webcam.open(); width = webcam.getImage().getWidth(); height = webcam.getImage().getHeight(); } PImage getImage() { BufferedImage bImg = webcam.getImage(); PImage pImg = createImage(bImg.getWidth(), bImg.getHeight(), ARGB); for (int y = 0; y < pImg.height; y++) { for (int x = 0; x < pImg.width; x++) { pImg.pixels[y * pImg.width + x] = bImg.getRGB(x, y); } } return pImg; } }
class ThetaSphere { // k : latitude // s : longitude PVector[][] vertices3D; // 3D vertices of hemisphere PVector[][] t_vertices1; // 2D texture vertices of hemisphere (1 of 2) PVector[][] t_vertices2; // 2D texture vertices of hemisphere (2 of 2) // constructor(initialize) ThetaSphere(int k_div, int s_div, int sphere_r, int xc1, int yc1, int xc2, int yc2, int img_r) { vertices3D = calcHemisphereVertices( k_div, s_div, sphere_r ); t_vertices1 = calcHemisphereTextureVertices( k_div, s_div, xc1, yc1, img_r ); t_vertices2 = calcHemisphereTextureVertices( k_div, s_div, xc2, yc2, img_r ); } // draw textured sphere void draw(PImage t_img, boolean flg) { if (flg) { drawHemisphere( vertices3D, t_vertices1, t_img ); rotateY(PI); drawHemisphere( vertices3D, t_vertices2, t_img ); } else { isEmergency(); } } // draw textured hemisphere void drawHemisphere(PVector[][] vertices, PVector[][] t_vertices, PImage t_img) { int k_div = vertices.length-1; int s_div = vertices[0].length-1; for (int k=1; k<=k_div; k++) { for (int s=0; s<s_div; s++) { PVector p1 = vertices[k][s]; PVector p2 = vertices[k-1][s]; PVector p3 = vertices[k][s+1]; PVector p4 = vertices[k-1][s+1]; PVector t1 = t_vertices[k][s]; PVector t2 = t_vertices[k-1][s]; PVector t3 = t_vertices[k][s+1]; PVector t4 = t_vertices[k-1][s+1]; beginShape(TRIANGLE_STRIP); texture(t_img); vertex( p1.x, p1.y, p1.z, t1.x, t1.y ); vertex( p2.x, p2.y, p2.z, t2.x, t2.y ); vertex( p3.x, p3.y, p3.z, t3.x, t3.y ); vertex( p4.x, p4.y, p4.z, t4.x, t4.y ); endShape(); } } } // calculate 3D vertices of hemisphere PVector[][] calcHemisphereVertices(int k_div, int s_div, int r) { PVector[][] vertices = new PVector[k_div+1][s_div+1]; for (int k=0; k<=k_div; k++) { float theta_k = k * (PI/2)/k_div; float slice_r = r * sin(theta_k); for (int s=0; s<=s_div; s++) { float theta_s = s * (2*PI)/s_div; float x = slice_r * cos(theta_s); float y = slice_r * sin(theta_s); float z = r * cos(theta_k); vertices[k][s] = new PVector(x, y, -z); } } return vertices; } // calculate 2D texture vertices of hemisphere PVector[][] calcHemisphereTextureVertices(int k_div, int s_div, int xc, int yc, int r) { PVector[][] t_vertices = new PVector[k_div+1][s_div+1]; for (int k=0; k<=k_div; k++) { float theta_k = k * (PI/2)/k_div; float slice_r = r * sin(theta_k); for (int s=0; s<=s_div; s++) { float theta_s = s * (2*PI)/s_div; float x = slice_r * cos(theta_s) + xc; float y = slice_r * sin(theta_s) + yc; t_vertices[k][s] = new PVector(x, y); } } return t_vertices; } } void isEmergency() { if (gainer.analogInput[0] > 100) { fill(255, 222, 0); rect(0, 0, width, height); fill(0); textSize(32); textAlign(CENTER, CENTER); text( "Emergency Stop", width/2, height/2); } }