LoginSignup
7
3

More than 3 years have passed since last update.

ラズパイZeroとWebSocketとサーバーでCNNカメラ的なものを作ってみる

Last updated at Posted at 2020-12-01

概要

QiitaのRaspberry Pi Advent Calendar 2020への参加記事です。
今更感はありますが、RaspberryPiZeroのカメラ画像をWebSocketでサーバーに転送し、オブジェクト・ディテクションを行います。

処理の流れ

処理は、だいたい以下のような流れで実行します。
サーバーはJavaのマイクロフレームワークの一つ、Sparkを使用し、ラズパイ側はPython3を使用します。
また、オブジェクト・ディテクションは、Yolov3をOpenCV4で使用します。
002.jpg

作成するクラス等

    今回は以下のクラス等を作成しました。
  • 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メソッドでオブジェクト・ディテクションを実行するクラスへ画像を引き渡します。

CameraHandler.java
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クラスを作成しました。

WebHandler.java
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に読み込んで解析を行っています。

Yolo.java
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で実行しています。

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インターフェイスを定義しました。

SolverThread.java

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で行っています。

Main.java
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);
          }
    }
}
SessionList.java
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を作成しました。

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が実装です。

index.html
<!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>
main.js
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
    });

};
main.css

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(カメラ)複数台を同時接続することができます。

ezgif-6-54168fe7130e.gif

最後に

もともとは、RaspberryPiZeroを野生動物などの野外調査に利用できないかと思い、実験したコードです。
なかなか、使用する機会もないので、アドベントカレンダーのネタとして消費しました。
そのうち、Raspberry Pi4+Coral USB Acceleratorを使い、カメラ側でのCNNも試してみたいと思います。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3