##概要
QiitaのRaspberry Pi Advent Calendar 2020への参加記事です。
今更感はありますが、RaspberryPiZeroのカメラ画像をWebSocketでサーバーに転送し、オブジェクト・ディテクションを行います。
##処理の流れ
処理は、だいたい以下のような流れで実行します。
サーバーはJavaのマイクロフレームワークの一つ、Sparkを使用し、ラズパイ側はPython3を使用します。
また、オブジェクト・ディテクションは、Yolov3をOpenCV4で使用します。
##作成するクラス等
-
今回は以下のクラス等を作成しました。
- Main.java:SparkFrameWorkの組み込みサーバーを起動するメインクラス
- CameraHandler.java:サーバーにラズパイが接続した時の処理クラス
- WebHandler.java:サーバーにブラウザが接続した時の処理クラス
- SessionList.java:接続したセッションを管理するクラス
- Yolo.java:Yolov3によりオブジェクト・ディテクションを行うクラス
- YoloSolver.java:オブジェクトディテクションを実行するクラス
- SolverThread.java:オブジェクト・ディテクションのスレッドクラス
- index.html:ラズパイカメラ画像を表示するHTML
- main.js/main.css:index.htmlで使用するjs/css
- camera_server.py:ラズパイ側のカメラ画像をサーバーに転送するPythonコード
##サーバー側の処理/Webocket通信
ラズパイからの接続時、接続終了時、メッセージ受領時の処理のため、以下のCameraHandlerクラスを作成しました。
Camerahandlerクラスでは、カメラからメッセージ(カメラ画像等)を受け取るとYolo.getInstance().addSolverメソッドでオブジェクト・ディテクションを実行するクラスへ画像を引き渡します。
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import com.google.gson.Gson;
import yolo.Yolo;
@WebSocket
public class CameraHandler {
private static Gson gson=new Gson();
@OnWebSocketConnect
public void onConnect(Session session)throws IOException {
SessionList.getInstance().addCameraSession(session);
Map<String,Object> obj=new HashMap<>();
String key=Integer.toString(session.hashCode());
obj.put("id", key);
obj.put("act", "add");
obj.put("data", "");
SessionList.getInstance().broadcastWeb(gson.toJson(obj));
session.getRemote().sendString("O.K.");
}
@OnWebSocketClose
public void onClose(Session session, int statusCode, String reason)throws IOException {
SessionList.getInstance().removeCameraSession(session);
Map<String,Object> obj=new HashMap<>();
String key=Integer.toString(session.hashCode());
obj.put("id", key);
obj.put("act", "remove");
obj.put("data", "");
SessionList.getInstance().broadcastWeb(gson.toJson(obj));
}
@OnWebSocketMessage
public void onMessage(Session session, String message) throws IOException {
String key=Integer.toString(session.hashCode());
if(Main.doYolo()){
Yolo.getInstance().addSolver(key, message,session);
}else{
Map<String,Object> obj=new HashMap<>();
obj.put("id", key);
obj.put("act", "update");
obj.put("data", message);
SessionList.getInstance().broadcastWeb(gson.toJson(obj));
session.getRemote().sendString("O.K.");
}
}
@OnWebSocketMessage
public void onBinary(Session session, byte[] buffer, int offset, int length) throws IOException {}
public void createImage(String str)throws IOException{
try{
byte[] decodedBytes = Base64.getDecoder().decode(str);
BufferedImage img = ImageIO.read(new ByteArrayInputStream(decodedBytes));
ImageIO.write(img, "jpg", new File("test.jpg"));
}catch(Exception e){
e.printStackTrace();
}
}
}
ブラウザからの接続時、接続終了時の処理のため、以下のWebHandlerクラスを作成しました。
import java.io.IOException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
@WebSocket
public class WebHandler {
@OnWebSocketConnect
public void onConnect(Session session){
SessionList.getInstance().addWebSession(session);
}
@OnWebSocketClose
public void onClose(Session session, int statusCode, String reason){
SessionList.getInstance().removeWebSession(session);
}
@OnWebSocketMessage
public void onMessage(Session session, String message) throws IOException {
}
@OnWebSocketMessage
public void onBinary(Session session, byte[] buffer, int offset, int length) throws IOException {
// session.getRemote().sendBytes(ByteBuffer.wrap(buffer));
}
}
##サーバー側の処理/オブジェクト・ディテクション
オブジェクト・ディテクションを行うYoloクラスを作成しました。
Yolov3のモデルをOpenCV4.0のDNNに読み込んで解析を行っています。
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import javax.imageio.ImageIO;
import org.eclipse.jetty.websocket.api.Session;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.dnn.Dnn;
import org.opencv.dnn.Net;
import org.opencv.imgcodecs.Imgcodecs;
public class Yolo {
private static Yolo yolo=null;
private List<String> outputNames;
private Net net;
public static Yolo getInstance(){
if(yolo==null){
yolo=new Yolo();
return yolo;
}else{
return yolo;
}
}
public Yolo(){
nu.pattern.OpenCV.loadShared();
String modelWeights = "yolov3_320.weights";
String modelConfiguration = "yolov3_320.cfg";
net=Dnn.readNetFromDarknet(modelConfiguration, modelWeights);
net.setPreferableBackend(Dnn.DNN_BACKEND_CUDA);
net.setPreferableTarget(Dnn.DNN_TARGET_CUDA);
outputNames=createOutputNames(net);
SolverThread st=new SolverThread(net,0.6f);
st.start();
}
public void setListener(PostProcessingListener l){
SolverThread.setListsner(l);
}
public List<String> getOutputNames(){
return outputNames;
}
private List<String> createOutputNames(Net net) {
List<String> names = new ArrayList<>();
List<Integer> outLayers = net.getUnconnectedOutLayers().toList();
List<String> layersNames = net.getLayerNames();
outLayers.forEach((item) -> names.add(layersNames.get(item - 1)));
return names;
}
public void addSolver(String name,String base64){
try{
BufferedImage bi=createImage(base64);
YoloSolver sol=new YoloSolver(name,bi);
SolverThread.add(sol);
}catch(Exception e){
e.printStackTrace();
}
}
public void addSolver(String name,String base64,Session session){
try{
BufferedImage bi=createImage(base64);
YoloSolver sol=new YoloSolver(name,bi,session);
SolverThread.add(sol);
}catch(Exception e){
e.printStackTrace();
}
}
public void addSolver(String name,BufferedImage bi){
try{
YoloSolver sol=new YoloSolver(name,bi);
SolverThread.add(sol);
}catch(Exception e){
e.printStackTrace();
}
}
public void addSolver(String name,BufferedImage bi,Session session){
try{
YoloSolver sol=new YoloSolver(name,bi,session);
SolverThread.add(sol);
}catch(Exception e){
e.printStackTrace();
}
}
public void addSolver(String name,Mat mat){
try{
YoloSolver sol=new YoloSolver(name,mat);
SolverThread.add(sol);
}catch(Exception e){
e.printStackTrace();
}
}
public void addSolver(String name,Mat mat,Session session){
try{
YoloSolver sol=new YoloSolver(name,mat,session);
SolverThread.add(sol);
}catch(Exception e){
e.printStackTrace();
}
}
public static BufferedImage createImage(String str)throws IOException{
try{
byte[] decodedBytes = Base64.getDecoder().decode(str);
BufferedImage img = ImageIO.read(new ByteArrayInputStream(decodedBytes));
return img;
}catch(Exception e){
e.printStackTrace();
return null;
}
}
public static BufferedImage matToBi(Mat image){
MatOfByte bytemat = new MatOfByte();
Imgcodecs.imencode(".jpg", image, bytemat);
byte[] bytes = bytemat.toArray();
InputStream in = new ByteArrayInputStream(bytes);
BufferedImage img = null;
try {
img = ImageIO.read(in);
} catch (IOException e) {
e.printStackTrace();
}
return img;
}
public static Mat biToMat(BufferedImage image) {
image = convertTo3ByteBGRType(image);
byte[] data = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
Mat mat = new Mat(image.getHeight(), image.getWidth(), CvType.CV_8UC3);
mat.put(0, 0, data);
return mat;
}
private static BufferedImage convertTo3ByteBGRType(BufferedImage image) {
BufferedImage convertedImage = new BufferedImage(image.getWidth(), image.getHeight(),
BufferedImage.TYPE_3BYTE_BGR);
convertedImage.getGraphics().drawImage(image, 0, 0, null);
return convertedImage;
}
実際のオブジェクト・ディテクションは、以下のYoloSolver.javaで実行しています。
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Observable;
import org.eclipse.jetty.websocket.api.Session;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfFloat;
import org.opencv.core.MatOfInt;
import org.opencv.core.MatOfRect2d;
import org.opencv.core.Point;
import org.opencv.core.Rect2d;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.dnn.Dnn;
import org.opencv.dnn.Net;
import org.opencv.imgproc.Imgproc;
import org.opencv.utils.Converters;
public class YoloSolver extends Observable{
private BufferedImage bi;
private String name;
private long time;
private BufferedImage dst;
private List<Integer> clsIds;
private List<Float> confs;
private List<Rect2d> rects;
private Mat frame;
private Session session;
public YoloSolver(String name,BufferedImage bi){
time=System.currentTimeMillis();
this.name=name;
this.bi=bi;
this.frame=Yolo.biToMat(bi);
}
public YoloSolver(String name,BufferedImage bi,Session session){
this(name,bi);
this.session=session;
}
public YoloSolver(String name,Mat f){
time=System.currentTimeMillis();
this.name=name;
this.bi=Yolo.matToBi(f);
this.frame=f;
}
public YoloSolver(String name,Mat f,Session session){
this(name,f);
this.session=session;
}
public void solve(Net net,float confThreshold){
Size sz = new Size(288,288);
List<Mat> result = new ArrayList<>();
List<String> outBlobNames = Yolo.getInstance().getOutputNames();
Mat blob = Dnn.blobFromImage(frame, 0.00392, sz, new Scalar(0), true, false);
net.setInput(blob);
net.forward(result, outBlobNames);
clsIds = new ArrayList<>();
confs = new ArrayList<>();
rects = new ArrayList<>();
for (int i=0;i<result.size();++i){
Mat level = result.get(i);
for (int j=0;j<level.rows();++j){
Mat row = level.row(j);
Mat scores = row.colRange(5, level.cols());
Core.MinMaxLocResult mm = Core.minMaxLoc(scores);
float confidence = (float)mm.maxVal;
Point classIdPoint = mm.maxLoc;
if (confidence > confThreshold){
int centerX = (int)(row.get(0,0)[0] * frame.cols());
int centerY = (int)(row.get(0,1)[0] * frame.rows());
int width = (int)(row.get(0,2)[0] * frame.cols());
int height = (int)(row.get(0,3)[0] * frame.rows());
int left = centerX - width / 2;
int top = centerY - height / 2;
clsIds.add((int)classIdPoint.x);
confs.add((float)confidence);
rects.add(new Rect2d(left, top, width, height));
}
}
}
float nmsThresh = 0.5f;
if(confs.size()>0){
MatOfFloat confidences = new MatOfFloat(Converters.vector_float_to_Mat(confs));
Rect2d[] boxesArray = rects.toArray(new Rect2d[0]);
MatOfRect2d boxes = new MatOfRect2d(boxesArray);
MatOfInt indices = new MatOfInt();
Dnn.NMSBoxes(boxes, confidences, confThreshold, nmsThresh, indices);
int [] ind = indices.toArray();
for (int i = 0; i < ind.length; ++i){
int idx = ind[i];
Rect2d box = boxesArray[idx];
Imgproc.rectangle(frame, box.tl(), box.br(), new Scalar(0,255,255), 2);
}
dst=Yolo.matToBi(frame);
}
}
public BufferedImage getDstImage() {
return dst;
}
public List<Integer> getClsIds() {
return clsIds;
}
public List<Float> getConfs() {
return confs;
}
public List<Rect2d> getRects() {
return rects;
}
public BufferedImage getSrcImage(){
return bi;
}
public Date getDate(){
return new Date(time);
}
public String getName(){
return name;
}
public Session getSession() {
return session;
}
}
オブジェクト・ディテクションのためのスレッドとして、SolverThreadクラスを作成しました。
SolverTheradクラスは、キューに登録されたYoloSolverインスタンスを順次処理します。
また、解析終了後の処理のため、PostProcessingListenerインターフェイスを定義しました。
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.opencv.dnn.Net;
public class SolverThread implements Runnable{
private static List<YoloSolver> queue=Collections.synchronizedList(new LinkedList<YoloSolver>());
private Thread thread;
private Net net;
private static PostProcessingListener listener;
private float threshold=0.6f;
public SolverThread(Net net,float confThreshold){
this.net=net;
this.threshold=confThreshold;
}
public static void add(YoloSolver s){
synchronized (queue){
if(queue.contains(s)){
queue.notifyAll();
}else{
queue.add(s);
queue.notifyAll();
}
}
}
public static void remove(YoloSolver s){
synchronized (queue){
queue.remove(s);
queue.notifyAll();
}
}
public void start(){
thread=new Thread(this);
thread.start();
}
public void stop(){
thread=null;
}
public void run() {
while(thread!=null){
YoloSolver solver=null;
synchronized(queue){
while(queue.isEmpty()){
try{
queue.wait();
}catch (InterruptedException e){}
}
if(thread!=null){
solver=(YoloSolver)queue.get(0);
queue.remove(0);
}
}
try{
if(solver!=null){
try{
solver.solve(net,threshold);
if(listener!=null)listener.postProcessing(solver);
}catch(Exception e){
e.printStackTrace();
remove(solver);
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
public static void setListsner(PostProcessingListener l){
listener=l;
}
}
##サーバー側の処理/Mainクラス
サーバー全体の処理を行うMainクラスを作成しました。
また、通信セッションを管理するSessionListクラスを作成しました。
/cameraがラズパイカメラの接続アドレス、/webがブラウザの接続アドレスで、/session/cameraは接続中のカメラセッション一覧を取得するアドレスです。
/cameraに接続したラズパイカメラは逐次を画像をサーバーに送信し、CameraHanderクラスで画像をCNN解析を行うYoloクラスへ渡し、Yoloクラス内でYoloSolverクラスを生成し、SolverThreadのキューへ登録されます。
解析が終わったデータは、Mainクラス内で定義したPostProcessingListenerにより、全てのWebブラウザセッションに更新画像をブロードキャストすると共に、ラズパイカメラセッションに処理終了を通知します。
画像を含むデータのやりとりはJsonで行っています。
import static spark.Spark.get;
import static spark.Spark.init;
import static spark.Spark.port;
import static spark.Spark.staticFileLocation;
import static spark.Spark.webSocket;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Base64;
import javax.imageio.ImageIO;
import com.google.gson.Gson;
import yolo.PostProcessingListener;
import yolo.Yolo;
import yolo.YoloSolver;
public class Main {
private static SessionList list=SessionList.getInstance();
private static Gson gson=new Gson();
private static boolean doYolo=false;
public static void main(String[] args) {
setYolo(true);
staticFileLocation("/public");
webSocket("/camera", CameraHandler.class);
webSocket("/web", WebHandler.class);
port(8080);
init();
get("/session/camera", (request, response) -> {
try{
String[] ret=list.getCameraId();
response.type("application/json");
response.status(200);
return new Gson().toJson(ret);
}catch(Exception e){
e.printStackTrace();
response.status(400);
response.type("application/json");
return gson.toJson(new String[0]);
}
});
}
public static boolean doYolo(){
return doYolo;
}
public static void setYolo(boolean flg){
doYolo=flg;
if(doYolo){
PostProcessingListener li=new PostProcessingListener(){
@Override
public void postProcessing(YoloSolver sol) {
try{
Map<String,Object> obj=new HashMap<>();
obj.put("id", sol.getName());
obj.put("act", "update");
if(sol.getDstImage()==null){
String message=jpgToStr(sol.getSrcImage());
obj.put("data", message);
}else{
String message=jpgToStr(sol.getDstImage());
obj.put("data", message);
}
SessionList.getInstance().broadcastWeb(gson.toJson(obj));
}catch(Exception e){
e.printStackTrace();
}
try{
sol.getSession().getRemote().sendString("O.K.");
}catch(IOException e){
e.printStackTrace();
}
}
};
Yolo.getInstance().setListener(li);
}
}
private static String jpgToStr(BufferedImage img){
final ByteArrayOutputStream os = new ByteArrayOutputStream();
try{
ImageIO.write(img, "jpg", os);
return Base64.getEncoder().encodeToString(os.toByteArray());
} catch (final IOException ioe){
throw new UncheckedIOException(ioe);
}
}
}
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
public class SessionList {
private Map<String,Session> camera;
private Map<String,Session> web;
private static SessionList list=null;
private SessionList(){
camera=new HashMap<>();
web=new HashMap<>();
}
public static SessionList getInstance(){
if(list==null){
list=new SessionList();
return list;
}else{
return list;
}
}
public String[] getCameraId(){
String[] keys=camera.keySet().toArray(new String[camera.keySet().size()]);
Arrays.sort(keys);
return keys;
}
public void addCameraSession(Session s){
String key=Integer.toString(s.hashCode());
camera.put(key, s);
}
public void removeCameraSession(Session s){
String key=Integer.toString(s.hashCode());
camera.remove(key);
}
public void addWebSession(Session s){
String key=Integer.toString(s.hashCode());
web.put(key, s);
}
public void removeWebSession(Session s){
String key=Integer.toString(s.hashCode());
web.remove(key);
}
public void broadcastWeb(String mes)throws IOException{
for(Session s : web.values()){
s.getRemote().sendString(mes);
}
}
}
##ラスパイ側の処理
ラズパイZero側では、逐次カメラ画像をサーバーに転送するCameraServer.pyを作成しました。
import cv2
import websocket
import base64
url="ws://アドレス:ポート/camera"
WIDTH=640
HEIGHT=480
FPS=6
camera_id=0
cap = cv2.VideoCapture(camera_id)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
cap.set(cv2.CAP_PROP_FPS, FPS)
encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 85]
def on_message(ws, message):
if message=="O.K.":
try:
send_data(ws,cap)
except:
print('Error')
def on_error(ws, error):
print(error)
def on_close(ws):
print("### closed ###")
def on_open(ws):
print('### open ###')
ws = websocket.WebSocketApp(url,
on_open=on_open,
on_message = on_message,
on_error = on_error,
on_close = on_close)
def send_data(ws,cap):
ret, frame = cap.read()
res, enc = cv2.imencode('.jpg', frame, encode_param)
dst=base64.b64encode(enc).decode('utf-8')
ws.send(dst)
try:
ws.run_forever()
except KeyboardInterrupt:
ws.close()
cap.release()
##Web接続用HTML
最後にラズパイカメラ画像を表示するHtmlを作成しました。
以下のindex.htmlとmain.js、main.cssが実装です。
<!doctype html>
<html lang="ja" >
<head>
<meta charset="UTF-8" />
<title>Iot-Camera-Test</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-dragdata@0.1.0/dist/chartjs-plugin-dragData.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@1.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/js/chartjs-plugin-labels.min.js"></script>
<script src="js/main.js"></script>
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css" />
<link rel="stylesheet" type="text/css" href="css/main.css" />
</head>
<body>
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-white">テストサイトです。</p>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<input type="checkbox" class="openSidebarMenu" id="openSidebarMenu">
<label for="openSidebarMenu" class="sidebarIconToggle">
<div class="spinner diagonal part-1"></div>
<div class="spinner horizontal"></div>
<div class="spinner diagonal part-2"></div>
</label>
<div id="sidebarMenu">
<ul class="sidebarMenuInner" id="log">
<li><a href="#">サイドパネル</a></li>
<hr />
<li>接続状況 <span>Web/CAMERA</span></li>
</ul>
</div>
<div class="container d-flex justify-content-between">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
<strong>Bingo-IoT-Camera</strong>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main role="main">
<section class="jumbotron text-center">
<div class="container">
<h2 class="jumbotron-heading">IoTカメラ映像(仮)</h2>
<p class="lead text-muted">IoTカメラの映像を確認することができます。</p>
</div>
<a href="#" class="btn btn-primary my-2" onclick="addCardTest();">カード追加テスト</a>
<a href="#" class="btn btn-primary my-2" onclick="removeCardTest()">カード削除テスト</a>
</section>
<div class="container">
<div class="row justify-content-center">
<div class="col-auto mb-3 card-columns" id="card_deck">
</div>
</div>
<footer class="pt-4 my-md-5 pt-md-5 border-top">
<div class="row">
<div class="col-12 col-md">
<!-- <img class="mb-2" src="./img/ft_img_logo.png" alt="" height="40"> -->
<h5>Logo</h5>
<ul class="list-unstyled text-small">
<li><a class="text-muted" href="#">Cool stuff</a></li>
<li><a class="text-muted" href="#">Random feature</a></li>
</ul>
</div>
<div class="col-6 col-md">
<h5>Features</h5>
<ul class="list-unstyled text-small">
<li><a class="text-muted" href="#">Cool stuff</a></li>
<li><a class="text-muted" href="#">Random feature</a></li>
</ul>
</div>
<div class="col-6 col-md">
<h5>Resources</h5>
<ul class="list-unstyled text-small">
<li><a class="text-muted" href="#">Resource</a></li>
<li><a class="text-muted" href="#">Resource name</a></li>
</ul>
</div>
<div class="col-6 col-md">
<h5>About</h5>
<ul class="list-unstyled text-small">
<li><a class="text-muted" href="#">Team</a></li>
<li><a class="text-muted" href="#">Locations</a></li>
</ul>
</div>
</div>
</footer>
</div>
</main>
</body>
</html>
const socket=new WebSocket("ws://"+window.location.host+"/web");
socket.onopen = (e)=>{
$.get("/session/camera", function(data){
data.forEach(id => addCard(id));
});
};
socket.onmessage = (e)=>{
const obj=JSON.parse(e.data);
const id=obj.id;
const act=obj.act;
const data=obj.data;
if(act=="update"){
let canvas=document.getElementById(id+'_img');
let ctx=canvas.getContext('2d');
let img=new Image();
img.src='data:image/jepg;base64,'+data;
img.onload = function(){
ctx.drawImage(img, 0, 0,640,480,0,0,480,360);
}
}else if(act=="remove"){
removeCard(id);
const li=$("<li />");
li.text("camera("+id+") close");
li.attr("class","text-muted");
li.css("font-size","14");
li.css("line-height","18px");
$("#log").append(li);
}else if(act=="add"){
addCard(id);
const li=$("<li />");
li.text("camera("+id+") open");
li.attr("class","text-muted");
li.css("font-size","14");
li.css("line-height","18px");
$("#log").append(li);
}
};
const removeCard=(id_name)=>{
const div=$("#"+id_name);
div.remove();
};
let count=0;
const addCardTest=()=>{
addCard("Test"+String(count));
count++;
};
const removeCardTest=()=>{
if(count>0){
removeCard("Test"+String(count-1));
count--;
}
};
const addCard =(id_name) =>{
const container=$("<div />");
container.attr("class","card shadow-sm");
container.attr("id",id_name);
container.css("width","482px");
container.css("padding","0px");
const header=$("<div />");
header.attr("class","card-header");
container.append(header);
const title=$("<h4 />");
title.attr("class","my-0 font-weight-normal");
title.text(id_name);
header.append(title);
const body=$("<div />");
body.attr("class","card-body");
body.css("margin","0px");
body.css("padding","0px");
container.append(body);
canvas=$("<canvas />");
canvas.attr("id",id_name+"_img");
canvas.attr("width","480px");
canvas.attr("height","360px");
body.append(canvas);
const footer=$("<div />");
footer.attr("class","card-footer")
container.append(footer);
const bt=$("<a />");
bt.attr("class","btn btn-primary btn-sm" );
bt.attr("role","button");
bt.attr("onclick","addChart('"+id_name+"');");
bt.text("統計情報");
footer.append(bt);
$("#card_deck").append(container);
};
const addChart =(id_name) =>{
const container=$("<div />");
container.attr("class","card shadow-sm");
container.attr("id",id_name+"_chartcard");
container.css("width","482px");
container.css("padding","0px");
const header=$("<div />");
header.attr("class","card-header");
container.append(header);
const title=$("<h4 />");
title.attr("class","my-0 font-weight-normal");
title.text(" "+id_name);
header.append(title);
const body=$("<div />");
body.attr("class","card-body");
body.css("margin","0px");
body.css("padding","0px");
const close=$("<a />");
close.attr("class","btn btn-secondary btn-sm" );
close.attr("role","button");
close.attr("onclick","$('#"+id_name+"_chartcard').remove()");
close.text("×");
close.css("padding","0px")
close.css("width","24px");
close.css("height","24px");
close.css("font-size","14px");
title.prepend(close);
container.append(body);
canvas=$("<canvas />");
canvas.attr("id",id_name+"_chart");
canvas.attr("width","480px");
canvas.attr("height","360px");
body.append(canvas);
const footer=$("<div />");
footer.attr("class","card-footer")
container.append(footer);
const bt=$("<a />");
bt.attr("class","btn btn-primary btn-sm" );
bt.attr("role","button");
bt.attr("onclick","alert('Test');");
bt.text("日間変動");
footer.append(bt);
// $("#card_deck").append(container);
$("#"+id_name).after(container);
const ctx = document.getElementById(id_name+"_chart").getContext("2d");
const start=new Date(2020, 12, 1);
dates=[];
vals=[];
for(let i=0;i<31;i++){
dates.push(new Date(start.getTime()+i*60*60*24*1000));
let v=Math.round(Math.random()*50);
vals.push(v);
}
var data= {
labels: dates,
datasets: [
{
label: '検出回数',
data:vals,
backgroundColor: "rgba(219,39,91,0.5)"
}
]
};
var options={
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'day'
}
}]
}
};
var mChart = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
};
input[type=checkbox]:checked ~ .sidebarIconToggle > .horizontal {
transition: all 0.3s;
box-sizing: border-box;
opacity: 0;
}
input[type=checkbox]:checked ~ .sidebarIconToggle > .diagonal.part-1 {
transition: all 0.3s;
box-sizing: border-box;
transform: rotate(135deg);
margin-top: 8px;
}
input[type=checkbox]:checked ~ .sidebarIconToggle > .diagonal.part-2 {
transition: all 0.3s;
box-sizing: border-box;
transform: rotate(-135deg);
margin-top: -9px;
}
.card-columns {
column-count:2;
}
.spinner {
transition: all 0.3s;
box-sizing: border-box;
position: absolute;
height: 3px;
width: 100%;
background-color: #fff;
}
.horizontal {
transition: all 0.3s;
box-sizing: border-box;
position: relative;
float: left;
margin-top: 3px;
}
.diagonal.part-1 {
position: relative;
transition: all 0.3s;
box-sizing: border-box;
float: left;
}
.diagonal.part-2 {
transition: all 0.3s;
box-sizing: border-box;
position: relative;
float: left;
margin-top: 3px;
}
input[type="checkbox"]:checked ~ #sidebarMenu {
transform: translateX(0);
}
input[type=checkbox] {
transition: all 0.3s;
box-sizing: border-box;
display: none;
}
.sidebarIconToggle {
transition: all 0.3s;
box-sizing: border-box;
cursor: pointer;
position: absolute;
z-index: 99;
height: 100%;
width: 100%;
top: 22px;
left: 15px;
height: 22px;
width: 22px;
}
#sidebarMenu {
float: left;
height: 100%;
position: fixed;
left: 0;
top: 58px;
width: 250px;
z-index: 50;
transform: translateX(-250px);
transition: transform 250ms ease-in-out;
background: linear-gradient(180deg, #f0ffff 0%, #f0ffff 100%);
}
.sidebarMenuInner{
margin:0;
padding:0;
border-top: 1px solid rgba(255, 255, 255, 0.10);
}
.sidebarMenuInner li{
list-style: none;
color: #666;
text-transform: uppercase;
font-weight: bold;
padding: 20px;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.10);
}
.sidebarMenuInner li span{
display: block;
font-size: 14px;
color: rgba(180, 180, 180, 0.50);
}
.sidebarMenuInner li a{
color: #666;
text-transform: uppercase;
font-weight: bold;
cursor: pointer;
text-decoration: none;
}
##実装結果
ラズパイZeroの性能やシステムの仕様の関係もあり、かなり遅延が生じますが、一応、CNNカメラ的に機能します。
また、サーバー側に余裕があれば、ラズパイZero(カメラ)複数台を同時接続することができます。
##最後に
もともとは、RaspberryPiZeroを野生動物などの野外調査に利用できないかと思い、実験したコードです。
なかなか、使用する機会もないので、アドベントカレンダーのネタとして消費しました。
そのうち、Raspberry Pi4+Coral USB Acceleratorを使い、カメラ側でのCNNも試してみたいと思います。