本文环境(yolov5-lite 1.4 版本、ncnn 20210525 版本、numpy 1.21.6)已经配置完成,发布在 docker hub。对 docker 了解的用户,可以直接拉取镜像,跳过所有环境配置步骤。
docker pull 233zss/yolov5-lite:v1.4
YOLOV5-lite
数据集
下载路径 https://universe.roboflow.com/object-detection/chicken-jmyni
如需自己采集和标注,请参考 https://blog.csdn.net/black_sneak/article/details/131374492
训练
在远程服务器,基于 docker 进行训练
docker 安装和配置请参考 Docker 添加用户组1,在服务器终端依次执行
docker pull pytorch/pytorch:1.11.0-cuda11.3-cudnn8-devel
docker run -itd --gpus all -v /media/data/zhangshanshan/yolov5-lite/:/code --name yolov5-lite pytorch/pytorch:1.11.0-cuda11.3-cudnn8-devel bash
docker exec -it yolov5-lite bash#此时进入后是root
apt update
apt install -y libgl1-mesa-glx libglib2.0-0 git wget cmake libopencv-dev protobuf-compiler libprotobuf-dev
wget https://github.com/ppogg/YOLOv5-Lite/releases/download/v1.4/YOLOv5-Lite-1.4.zip
unzip YOLOv5-Lite-1.4.zip
cd YOLOv5-Lite
pip install -r requirements.txt
接下来,需要根据,修改训练文件 train.py。例如当出现以下错误时,可以将 train.py 中的--workers 设置为 1,减小--batch-size 等
RuntimeError: DataLoader worker (pid 23323) is killed by signal: Bus error. It is possible that dataloader's workers are out of shared memory. Please try to raise your shared memory limit.
我的修改示例如下:
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--weights', type=str, default='weights/v5Lite-s.pt', help='initial weights path')
parser.add_argument('--cfg', type=str, default='models/v5Lite-s.yaml', help='model.yaml path')
parser.add_argument('--data', type=str, default='data/coco.yaml', help='data.yaml path')
parser.add_argument('--hyp', type=str, default='data/hyp.scratch.yaml', help='hyperparameters path')
parser.add_argument('--epochs', type=int, default=300)
parser.add_argument('--batch-size', type=int, default=128, help='total batch size for all GPUs')
parser.add_argument('--img-size', nargs='+', type=int, default=[416, 416], help='[train, test] image sizes')
parser.add_argument('--rect', action='store_true', help='rectangular training')
parser.add_argument('--resume', nargs='?', const=True, default=False, help='resume most recent training')
parser.add_argument('--nosave', action='store_true', help='only save final checkpoint')
parser.add_argument('--notest', action='store_true', help='only test final epoch')
parser.add_argument('--noautoanchor', action='store_true', help='disable autoanchor check')
parser.add_argument('--evolve', action='store_true', help='evolve hyperparameters')
parser.add_argument('--bucket', type=str, default='', help='gsutil bucket')
parser.add_argument('--cache-images', action='store_true', help='cache images for faster training')
parser.add_argument('--image-weights', action='store_true', help='use weighted image selection for training')
parser.add_argument('--device', default='1', help='cuda device, i.e. 0 or 0,1,2,3 or cpu')
parser.add_argument('--multi-scale', action='store_true', help='vary img-size +/- 50%%')
parser.add_argument('--single-cls', action='store_true', help='train multi-class data as single-class')
parser.add_argument('--adam', action='store_true', help='use torch.optim.Adam() optimizer')
parser.add_argument('--sync-bn', action='store_true', help='use SyncBatchNorm, only available in DDP mode')
parser.add_argument('--local_rank', type=int, default=-1, help='DDP parameter, do not modify')
parser.add_argument('--workers', type=int, default=1, help='maximum number of dataloader workers')
parser.add_argument('--project', default='runs/train', help='save to project/name')
parser.add_argument('--entity', default=None, help='W&B entity')
parser.add_argument('--name', default='exp', help='save to project/name')
parser.add_argument('--exist-ok', action='store_true', help='existing project/name ok, do not increment')
parser.add_argument('--quad', action='store_true', help='quad dataloader')
parser.add_argument('--linear-lr', action='store_true', help='linear LR')
parser.add_argument('--label-smoothing', type=float, default=0.0, help='Label smoothing epsilon')
parser.add_argument('--upload_dataset', action='store_true', help='Upload dataset as W&B artifact table')
parser.add_argument('--bbox_interval', type=int, default=-1, help='Set bounding-box image logging interval for W&B')
parser.add_argument('--save_period', type=int, default=-1, help='Log model after every "save_period" epoch')
parser.add_argument('--artifact_alias', type=str, default="latest", help='version of dataset artifact to be used')
opt = parser.parse_args()
将预训练 v5lite-e.pt 文件放入 models 文件夹,将数据集文件夹,存放 YOLOv5-Lite 同级文件夹,如下所示
.
|-- YOLOv5-Lite
| |-- LICENSE
| |-- README.md
| |-- __pycache__
| | `-- test.cpython-38.pyc
| |-- android_demo
| | `-- ncnn-android-v5lite
| |-- cpp_demo
| | |-- mnn
| | |-- ncnn
| | |-- ort
| | |-- tengine
| | `-- tensorrt
| |-- data
| | |-- argoverse_hd.yaml
| | |-- coco.yaml
| | |-- coco128.yaml
| | |-- hyp.finetune.yaml
| | |-- hyp.scratch.yaml
| | |-- person.yaml
| | `-- voc.yaml
| |-- detect.py
| |-- export.py
| |-- models
| | |-- __init__.py
| | |-- __pycache__
| | |-- common.py
| | |-- experimental.py
| | |-- hub
| | |-- v5Lite-c.yaml
| | |-- v5Lite-e.yaml
| | |-- v5Lite-g.yaml
| | |-- v5Lite-s.yaml
| | |-- v5lite-e.pt
| | `-- yolo.py
| |-- python_demo
| | |-- onnxruntime
| | |-- openvino
| | `-- tensorrt
| |-- requirements.txt
| |-- runs
| | `-- train
| |-- scripts
| | |-- Grad_Cam.py
| | |-- __init__.py
| | |-- autoanchor.py
| | |-- check.py
| | |-- coco2voc.py
| | |-- eval.py
| | |-- get_argoverse_hd.sh
| | |-- get_coco.sh
| | |-- get_voc.sh
| | |-- main.py
| | |-- make_Txt.py
| | |-- rep_convert.py
| | `-- voc_label.py
| |-- test.py
| |-- train.py
| `-- utils
| |-- __init__.py
| |-- __pycache__
| |-- activations.py
| |-- autoanchor.py
| |-- aws
| |-- datasets.py
| |-- general.py
| |-- google_app_engine
| |-- google_utils.py
| |-- loss.py
| |-- metrics.py
| |-- plots.py
| |-- torch_utils.py
| `-- wandb_logging
`-- dataset
|-- README.dataset.txt
|-- README.roboflow.txt
|-- data.yaml
|-- test
| |-- images
| `-- labels
|-- train
| |-- images
| |-- labels
| `-- labels.cache
`-- valid
|-- images
|-- labels
`-- labels.cache
36 directories, 52 files
开始训练(路径请根据实际情况修改,我为了测试,只训练了两轮)
cd YOLOv5-Lite
python train.py --data /code/dataset/data.yaml --cfg v5Lite-e.yaml --weights models/v5lite-e.pt --batch-size 64
训练结束会得到如下:
量化
onnx
在服务器终端执行(路径请根据实际情况修改)
pip install numpy==1.21.6
pip install onnx
pip install onnx-simplifier
cd YOLOv5-Lite
export PYTHONPATH="$PWD" && python export.py --weights /code/YOLOv5-Lite/runs/train/exp3/weights/best.pt --img 416 --batch 1
进一步简化模型
python -m onnxsim /code/YOLOv5-Lite/runs/train/exp3/weights/best.onnx /code/YOLOv5-Lite/runs/train/exp3/weights/e.onnx
onnx 模型已经可以直接使用,理论上速度会略逊于 ncnn。用官方的代码测试一下,先参考/code/YOLOv5-Lite/python_demo/onnxruntime/coco.names 写个自己的 data.names,如下:
小改 ort.py 文件(源代码有点问题),模型路径、测试图片路径、names 路径、是否窗口显示等请根据自己的情况更改
import cv2
import time
import numpy as np
import argparse
import onnxruntime as ort
class yolov5_lite():
def __init__(self, model_pb_path, label_path, confThreshold=0.1, nmsThreshold=0.1, objThreshold=0.2):
so = ort.SessionOptions()
so.log_severity_level = 3
self.net = ort.InferenceSession(model_pb_path, so)
self.classes = list(map(lambda x: x.strip(), open(label_path, 'r').readlines()))
self.num_classes = len(self.classes)
anchors = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
self.nl = len(anchors)
self.na = len(anchors[0]) // 2
self.no = self.num_classes + 5
self.grid = [np.zeros(1)] * self.nl
self.stride = np.array([8., 16., 32.])
self.anchor_grid = np.asarray(anchors, dtype=np.float32).reshape(self.nl, -1, 2)
self.confThreshold = confThreshold
self.nmsThreshold = nmsThreshold
self.objThreshold = objThreshold
self.input_shape = (self.net.get_inputs()[0].shape[2], self.net.get_inputs()[0].shape[3])
def resize_image(self, srcimg, keep_ratio=True):
top, left, newh, neww = 0, 0, self.input_shape[0], self.input_shape[1]
if keep_ratio and srcimg.shape[0] != srcimg.shape[1]:
hw_scale = srcimg.shape[0] / srcimg.shape[1]
if hw_scale > 1:
newh, neww = self.input_shape[0], int(self.input_shape[1] / hw_scale)
img = cv2.resize(srcimg, (neww, newh), interpolation=cv2.INTER_AREA)
left = int((self.input_shape[1] - neww) * 0.5)
img = cv2.copyMakeBorder(img, 0, 0, left, self.input_shape[1] - neww - left, cv2.BORDER_CONSTANT,
value=0) # add border
else:
newh, neww = int(self.input_shape[0] * hw_scale), self.input_shape[1]
img = cv2.resize(srcimg, (neww, newh), interpolation=cv2.INTER_AREA)
top = int((self.input_shape[0] - newh) * 0.5)
img = cv2.copyMakeBorder(img, top, self.input_shape[0] - newh - top, 0, 0, cv2.BORDER_CONSTANT, value=0)
else:
img = cv2.resize(srcimg, self.input_shape, interpolation=cv2.INTER_AREA)
return img, newh, neww, top, left
def _make_grid(self, nx=20, ny=20):
xv, yv = np.meshgrid(np.arange(ny), np.arange(nx))
return np.stack((xv, yv), 2).reshape((-1, 2)).astype(np.float32)
def postprocess(self, frame, outs, pad_hw):
newh, neww, padh, padw = pad_hw
frameHeight = frame.shape[0]
frameWidth = frame.shape[1]
ratioh, ratiow = frameHeight / newh, frameWidth / neww
# Scan through all the bounding boxes output from the network and keep only the
# ones with high confidence scores. Assign the box's class label as the class with the highest score.
classIds = []
confidences = []
box_index = []
boxes = []
for detection in outs:
scores = detection[5:]
classId = np.argmax(scores)
confidence = scores[classId]
if confidence > self.confThreshold and detection[4] > self.objThreshold:
center_x = int((detection[0] - padw) * ratiow)
center_y = int((detection[1] - padh) * ratioh)
width = int(detection[2] * ratiow)
height = int(detection[3] * ratioh)
left = int(center_x - width / 2)
top = int(center_y - height / 2)
classIds.append(classId)
confidences.append(float(confidence))
boxes.append([left, top, width, height])
# Perform non maximum suppression to eliminate redundant overlapping boxes with
# lower confidences.
print(boxes)
indices = cv2.dnn.NMSBoxes(boxes, confidences, self.confThreshold, self.nmsThreshold)
print(indices)
for i in indices:
box_index.append(i)
for i in box_index:
box = boxes[i]
left = box[0]
top = box[1]
width = box[2]
height = box[3]
print(classIds[i], confidences[i])
frame = self.drawPred(frame, classIds[i], confidences[i], left, top, left + width, top + height)
return frame
def drawPred(self, frame, classId, conf, left, top, right, bottom):
# Draw a bounding box.
cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), thickness=2)
label = '%.2f' % conf
label = '%s:%s' % (self.classes[classId], label)
# Display the label at the top of the bounding box
labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
top = max(top, labelSize[1])
# cv.rectangle(frame, (left, top - round(1.5 * labelSize[1])), (left + round(1.5 * labelSize[0]), top + baseLine), (255,255,255), cv.FILLED)
cv2.putText(frame, label, (left, top - 10), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 255, 0), thickness=1)
return frame
def detect(self, srcimg):
img, newh, neww, top, left = self.resize_image(srcimg)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32) / 255.0
blob = np.expand_dims(np.transpose(img, (2, 0, 1)), axis=0)
t1 = time.time()
outs = self.net.run(None, {self.net.get_inputs()[0].name: blob})[0].squeeze(axis=0)
cost_time = time.time() - t1
print(outs.shape)
row_ind = 0
for i in range(self.nl):
h, w = int(self.input_shape[0] / self.stride[i]), int(self.input_shape[1] / self.stride[i])
length = int(self.na * h * w)
if self.grid[i].shape[2:4] != (h, w):
self.grid[i] = self._make_grid(w, h)
outs[row_ind:row_ind + length, 0:2] = (outs[row_ind:row_ind + length, 0:2] * 2. - 0.5 + np.tile(
self.grid[i], (self.na, 1))) * int(self.stride[i])
outs[row_ind:row_ind + length, 2:4] = (outs[row_ind:row_ind + length, 2:4] * 2) ** 2 * np.repeat(
self.anchor_grid[i], h * w, axis=0)
row_ind += length
srcimg = self.postprocess(srcimg, outs, (newh, neww, top, left))
infer_time = 'Inference Time: ' + str(int(cost_time * 1000)) + 'ms'
cv2.putText(srcimg, infer_time, (5, 20), cv2.FONT_HERSHEY_TRIPLEX, 0.5, (0, 0, 0), thickness=1)
return srcimg
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--imgpath', type=str, default='/code/dataset/test/images/1605236197942_jpg.rf.6bbdb01c9cd93bb481528cb9c58cb308.jpg', help="image path")
parser.add_argument('--modelpath', type=str, default='/code/YOLOv5-Lite/runs/train/exp3/weights/e.onnx', help="onnx filepath")
parser.add_argument('--classfile', type=str, default='/code/YOLOv5-Lite/python_demo/onnxruntime/chicken.names', help="classname filepath")
parser.add_argument('--confThreshold', default=0.1, type=float, help='class confidence')
parser.add_argument('--nmsThreshold', default=0.1, type=float, help='nms iou thresh')
args = parser.parse_args()
srcimg = cv2.imread(args.imgpath)
net = yolov5_lite(args.modelpath, args.classfile, confThreshold=args.confThreshold, nmsThreshold=args.nmsThreshold)
srcimg = net.detect(srcimg.copy())
cv2.imwrite("/code/YOLOv5-Lite/python_demo/onnxruntime/result.jpg", srcimg)
# winName = 'Deep learning object detection in onnxruntime'
# cv2.namedWindow(winName, cv2.WINDOW_NORMAL)
# cv2.imshow(winName, srcimg)
# cv2.waitKey(0)
# # cv2.imwrite('save.jpg', srcimg )
# cv2.destroyAllWindows()
然后执行
cd YOLOv5-Lite/python_demo/onnxruntime/
python3 ort.py
测试结果,如果没有检测到任何东西,可以调低阈值:
摄像头检测代码,改下 ort.py 的主函数就可以:
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--modelpath', type=str, default='/code/YOLOv5-Lite/runs/train/exp3/weights/e.onnx', help="onnx filepath")
parser.add_argument('--classfile', type=str, default='/code/YOLOv5-Lite/python_demo/onnxruntime/chicken.names', help="classname filepath")
parser.add_argument('--confThreshold', default=0.1, type=float, help='class confidence')
parser.add_argument('--nmsThreshold', default=0.1, type=float, help='nms iou thresh')
args = parser.parse_args()
cap = cv2.VideoCapture(0) # 设置为 0 表示使用默认摄像头
net = yolov5_lite(args.modelpath, args.classfile, confThreshold=args.confThreshold, nmsThreshold=args.nmsThreshold)
while cap.isOpened():
ret, frame = cap.read()
if not ret:
break
frame = net.detect(frame)
cv2.imshow('YOLOv5 Lite Object Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
ncnn
两种思路,一是将 onnx 转为 ncnn,还有一种是用 pnnx 转化(腾讯推荐,据称优化效果更好)
onnx 转
使用 ncnn 的 20210525 版本,在服务器终端执行
wget https://github.com/Tencent/ncnn/archive/refs/tags/20210525.zip
unzip ncnn-20210525.zip
cd ncnn-20210525
mkdir build
cd build
cmake ..
make -j8
make install
./tools/onnx/onnx2ncnn /code/YOLOv5-Lite/runs/train/exp3/weights/e.onnx /code/YOLOv5-Lite/runs/train/exp3/weights/e.param /code/YOLOv5-Lite/runs/train/exp3/weights/e.bin
# 模型优化为fp16
./tools/ncnnoptimize /code/YOLOv5-Lite/runs/train/exp3/weights/e.param /code/YOLOv5-Lite/runs/train/exp3/weights/e.bin /code/YOLOv5-Lite/runs/train/exp3/weights/e2.param /code/YOLOv5-Lite/runs/train/exp3/weights/e2.bin 65536
打开 e2.param,将 Permute 上方的 Reshape 修改为 0 = -1,此步是为了能够动态输入:
ncnn 提供了一份 ncnn-20210525/examples/yolov5.cpp 代码,将/code/YOLOv5-Lite/models/v5Lite-e.yaml 中的 anchors 对应填写到 ncnn-20210525/examples/yolov5.cpp
(因为 yolov5-lite 本就源于 yolov5,在模型的调用推理上是相同的,所以可以直接使用 ncnn 官方的代码,还能省去编写 CMkaeLists.txt 的麻烦)
将 e2.param 中的 permute 对应填写到 ncnn-20210525/examples/yolov5.cpp 中
对于 ncnn-20210525/examples/yolov5.cpp 中的 parm 和 bin 文件的路径、阈值等信息(我在下方代码中加了注释),请自行更改。
由于我使用的是服务器,无法显示视窗,所以注释了显示代码,用户可以根据需求更改
这是我修改完的文件
// Tencent is pleased to support the open source community by making ncnn available.
//
// Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
//
// Licensed under the BSD 3-Clause License (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at
//
// https://opensource.org/licenses/BSD-3-Clause
//
// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.
#include "layer.h"
#include "net.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <stdio.h>
#include <vector>
class YoloV5Focus : public ncnn::Layer
{
public:
YoloV5Focus()
{
one_blob_only = true;
}
virtual int forward(const ncnn::Mat& bottom_blob, ncnn::Mat& top_blob, const ncnn::Option& opt) const
{
int w = bottom_blob.w;
int h = bottom_blob.h;
int channels = bottom_blob.c;
int outw = w / 2;
int outh = h / 2;
int outc = channels * 4;
top_blob.create(outw, outh, outc, 4u, 1, opt.blob_allocator);
if (top_blob.empty())
return -100;
#pragma omp parallel for num_threads(opt.num_threads)
for (int p = 0; p < outc; p++)
{
const float* ptr = bottom_blob.channel(p % channels).row((p / channels) % 2) + ((p / channels) / 2);
float* outptr = top_blob.channel(p);
for (int i = 0; i < outh; i++)
{
for (int j = 0; j < outw; j++)
{
*outptr = *ptr;
outptr += 1;
ptr += 2;
}
ptr += w;
}
}
return 0;
}
};
DEFINE_LAYER_CREATOR(YoloV5Focus)
struct Object
{
cv::Rect_<float> rect;
int label;
float prob;
};
static inline float intersection_area(const Object& a, const Object& b)
{
cv::Rect_<float> inter = a.rect & b.rect;
return inter.area();
}
static void qsort_descent_inplace(std::vector<Object>& faceobjects, int left, int right)
{
int i = left;
int j = right;
float p = faceobjects[(left + right) / 2].prob;
while (i <= j)
{
while (faceobjects[i].prob > p)
i++;
while (faceobjects[j].prob < p)
j--;
if (i <= j)
{
// swap
std::swap(faceobjects[i], faceobjects[j]);
i++;
j--;
}
}
#pragma omp parallel sections
{
#pragma omp section
{
if (left < j) qsort_descent_inplace(faceobjects, left, j);
}
#pragma omp section
{
if (i < right) qsort_descent_inplace(faceobjects, i, right);
}
}
}
static void qsort_descent_inplace(std::vector<Object>& faceobjects)
{
if (faceobjects.empty())
return;
qsort_descent_inplace(faceobjects, 0, faceobjects.size() - 1);
}
static void nms_sorted_bboxes(const std::vector<Object>& faceobjects, std::vector<int>& picked, float nms_threshold)
{
picked.clear();
const int n = faceobjects.size();
std::vector<float> areas(n);
for (int i = 0; i < n; i++)
{
areas[i] = faceobjects[i].rect.area();
}
for (int i = 0; i < n; i++)
{
const Object& a = faceobjects[i];
int keep = 1;
for (int j = 0; j < (int)picked.size(); j++)
{
const Object& b = faceobjects[picked[j]];
// intersection over union
float inter_area = intersection_area(a, b);
float union_area = areas[i] + areas[picked[j]] - inter_area;
// float IoU = inter_area / union_area
if (inter_area / union_area > nms_threshold)
keep = 0;
}
if (keep)
picked.push_back(i);
}
}
static inline float sigmoid(float x)
{
return static_cast<float>(1.f / (1.f + exp(-x)));
}
static void generate_proposals(const ncnn::Mat& anchors, int stride, const ncnn::Mat& in_pad, const ncnn::Mat& feat_blob, float prob_threshold, std::vector<Object>& objects)
{
const int num_grid = feat_blob.h;
int num_grid_x;
int num_grid_y;
if (in_pad.w > in_pad.h)
{
num_grid_x = in_pad.w / stride;
num_grid_y = num_grid / num_grid_x;
}
else
{
num_grid_y = in_pad.h / stride;
num_grid_x = num_grid / num_grid_y;
}
const int num_class = feat_blob.w - 5;
const int num_anchors = anchors.w / 2;
for (int q = 0; q < num_anchors; q++)
{
const float anchor_w = anchors[q * 2];
const float anchor_h = anchors[q * 2 + 1];
const ncnn::Mat feat = feat_blob.channel(q);
for (int i = 0; i < num_grid_y; i++)
{
for (int j = 0; j < num_grid_x; j++)
{
const float* featptr = feat.row(i * num_grid_x + j);
// find class index with max class score
int class_index = 0;
float class_score = -FLT_MAX;
for (int k = 0; k < num_class; k++)
{
float score = featptr[5 + k];
if (score > class_score)
{
class_index = k;
class_score = score;
}
}
float box_score = featptr[4];
float confidence = sigmoid(box_score) * sigmoid(class_score);
if (confidence >= prob_threshold)
{
// yolov5/models/yolo.py Detect forward
// y = x[i].sigmoid()
// y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i].to(x[i].device)) * self.stride[i] # xy
// y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh
float dx = sigmoid(featptr[0]);
float dy = sigmoid(featptr[1]);
float dw = sigmoid(featptr[2]);
float dh = sigmoid(featptr[3]);
float pb_cx = (dx * 2.f - 0.5f + j) * stride;
float pb_cy = (dy * 2.f - 0.5f + i) * stride;
float pb_w = pow(dw * 2.f, 2) * anchor_w;
float pb_h = pow(dh * 2.f, 2) * anchor_h;
float x0 = pb_cx - pb_w * 0.5f;
float y0 = pb_cy - pb_h * 0.5f;
float x1 = pb_cx + pb_w * 0.5f;
float y1 = pb_cy + pb_h * 0.5f;
Object obj;
obj.rect.x = x0;
obj.rect.y = y0;
obj.rect.width = x1 - x0;
obj.rect.height = y1 - y0;
obj.label = class_index;
obj.prob = confidence;
objects.push_back(obj);
}
}
}
}
}
static int detect_yolov5(const cv::Mat& bgr, std::vector<Object>& objects)
{
ncnn::Net yolov5;
yolov5.opt.use_vulkan_compute = true;
// yolov5.opt.use_bf16_storage = true;
yolov5.register_custom_layer("YoloV5Focus", YoloV5Focus_layer_creator);
// original pretrained model from https://github.com/ultralytics/yolov5
// the ncnn model https://github.com/nihui/ncnn-assets/tree/master/models
yolov5.load_param("/code/YOLOv5-Lite/runs/train/exp5/weights/e2.param");//param路径
yolov5.load_model("/code/YOLOv5-Lite/runs/train/exp5/weights/e2.bin");//bin路径
const int target_size = 320;
const float prob_threshold = 0.5f;//阈值等
const float nms_threshold = 0.5f;
int img_w = bgr.cols;
int img_h = bgr.rows;
// letterbox pad to multiple of 32
int w = img_w;
int h = img_h;
float scale = 1.f;
if (w > h)
{
scale = (float)target_size / w;
w = target_size;
h = h * scale;
}
else
{
scale = (float)target_size / h;
h = target_size;
w = w * scale;
}
ncnn::Mat in = ncnn::Mat::from_pixels_resize(bgr.data, ncnn::Mat::PIXEL_BGR2RGB, img_w, img_h, w, h);
// pad to target_size rectangle
// yolov5/utils/datasets.py letterbox
int wpad = (w + 31) / 32 * 32 - w;
int hpad = (h + 31) / 32 * 32 - h;
ncnn::Mat in_pad;
ncnn::copy_make_border(in, in_pad, hpad / 2, hpad - hpad / 2, wpad / 2, wpad - wpad / 2, ncnn::BORDER_CONSTANT, 114.f);
const float norm_vals[3] = {1 / 255.f, 1 / 255.f, 1 / 255.f};
in_pad.substract_mean_normalize(0, norm_vals);
ncnn::Extractor ex = yolov5.create_extractor();
ex.input("images", in_pad);
std::vector<Object> proposals;
// anchor setting from yolov5/models/yolov5s.yaml
// stride 8
{
ncnn::Mat out;
ex.extract("onnx::Sigmoid_577", out);
ncnn::Mat anchors(6);
anchors[0] = 10.f;
anchors[1] = 13.f;
anchors[2] = 16.f;
anchors[3] = 30.f;
anchors[4] = 33.f;
anchors[5] = 23.f;
std::vector<Object> objects8;
generate_proposals(anchors, 8, in_pad, out, prob_threshold, objects8);
proposals.insert(proposals.end(), objects8.begin(), objects8.end());
}
// stride 16
{
ncnn::Mat out;
ex.extract("onnx::Sigmoid_599", out);
ncnn::Mat anchors(6);
anchors[0] = 30.f;
anchors[1] = 61.f;
anchors[2] = 62.f;
anchors[3] = 45.f;
anchors[4] = 59.f;
anchors[5] = 119.f;
std::vector<Object> objects16;
generate_proposals(anchors, 16, in_pad, out, prob_threshold, objects16);
proposals.insert(proposals.end(), objects16.begin(), objects16.end());
}
// stride 32
{
ncnn::Mat out;
ex.extract("onnx::Sigmoid_621", out);
ncnn::Mat anchors(6);
anchors[0] = 116.f;
anchors[1] = 90.f;
anchors[2] = 156.f;
anchors[3] = 198.f;
anchors[4] = 373.f;
anchors[5] = 326.f;
std::vector<Object> objects32;
generate_proposals(anchors, 32, in_pad, out, prob_threshold, objects32);
proposals.insert(proposals.end(), objects32.begin(), objects32.end());
}
// sort all proposals by score from highest to lowest
qsort_descent_inplace(proposals);
// apply nms with nms_threshold
std::vector<int> picked;
nms_sorted_bboxes(proposals, picked, nms_threshold);
int count = picked.size();
objects.resize(count);
for (int i = 0; i < count; i++)
{
objects[i] = proposals[picked[i]];
// adjust offset to original unpadded
float x0 = (objects[i].rect.x - (wpad / 2)) / scale;
float y0 = (objects[i].rect.y - (hpad / 2)) / scale;
float x1 = (objects[i].rect.x + objects[i].rect.width - (wpad / 2)) / scale;
float y1 = (objects[i].rect.y + objects[i].rect.height - (hpad / 2)) / scale;
// clip
x0 = std::max(std::min(x0, (float)(img_w - 1)), 0.f);
y0 = std::max(std::min(y0, (float)(img_h - 1)), 0.f);
x1 = std::max(std::min(x1, (float)(img_w - 1)), 0.f);
y1 = std::max(std::min(y1, (float)(img_h - 1)), 0.f);
objects[i].rect.x = x0;
objects[i].rect.y = y0;
objects[i].rect.width = x1 - x0;
objects[i].rect.height = y1 - y0;
}
return 0;
}
static void draw_objects(const cv::Mat& bgr, const std::vector<Object>& objects)
{
static const char* class_names[] = {
"chicken"
};
cv::Mat image = bgr.clone();
for (size_t i = 0; i < objects.size(); i++)
{
const Object& obj = objects[i];
fprintf(stderr, "%d = %.5f at %.2f %.2f %.2f x %.2f\n", obj.label, obj.prob,
obj.rect.x, obj.rect.y, obj.rect.width, obj.rect.height);
cv::rectangle(image, obj.rect, cv::Scalar(255, 0, 0));
char text[256];
sprintf(text, "%s %.1f%%", class_names[obj.label], obj.prob * 100);
int baseLine = 0;
cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
int x = obj.rect.x;
int y = obj.rect.y - label_size.height - baseLine;
if (y < 0)
y = 0;
if (x + label_size.width > image.cols)
x = image.cols - label_size.width;
cv::rectangle(image, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)),
cv::Scalar(255, 255, 255), -1);
cv::putText(image, text, cv::Point(x, y + label_size.height),
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
}
// cv::imshow("image", image);
cv::imwrite("/code/YOLOv5-Lite/cpp_demo/ncnn/result_ncnn.jpg", image);
// cv::waitKey(0);
}
int main(int argc, char** argv)
{
if (argc != 2)
{
fprintf(stderr, "Usage: %s [imagepath]\n", argv[0]);
return -1;
}
const char* imagepath = argv[1];
cv::Mat m = cv::imread(imagepath, 1);
if (m.empty())
{
fprintf(stderr, "cv::imread %s failed\n", imagepath);
return -1;
}
std::vector<Object> objects;
detect_yolov5(m, objects);
draw_objects(m, objects);
return 0;
}
最后重新编译 ncnn,在终端执行
cd ncnn-20210525/build
rm -rf *
cmake ..
make -j20
make install
cd examples
./yolov5 /code/dataset/test/images/1596210416143_jpg.rf.f0b02ec559c973cac67a0ca3de8c8bfc.jpg
树莓派部署
树莓派基础
-
参考微雪的教程 烧录树莓派镜像到 SD 卡,同时开启 SSH 和 WiFi(教程中包括相关基础知识,没接触过树莓派需要先过一遍,了解基本常识),
注意:记得按照教程,开启 SSH 并配置 wifi,后续有用
-
连接树莓派(以下方案任选一个即可,上一步已经配置了 wifi,树莓派启动后会自动连接 wifi)
-
最简单的方案:给树莓派外接键盘、鼠标、显示器;把树莓派当成一台电脑使用
-
获取树莓派 ip 地址
由于我们烧录镜像时,已经设置过连接的 wifi,可以直接登录路由器管理界面查看树莓派的 IP 地址
也可以用 ip 扫描软件 Advanced IP Scanner 来获取(电脑需要和树莓派连接相同的 wifi)
-
用 VScode 的 remote ssh
VSCODE 远程开发树莓派_vscode ssh orangepi-CSDN 博客
连接后,直接将代码文件拖进 VScode 中树莓派的文件夹即可
-
mobaxterm 连接树莓派
-
ssh
linux 指令熟的话,直接 ssh 就行
-
VNC(本文由于需要用到视觉,建议用 VNC)
-
-
然后参考教程更新源(依据教程,查看树莓派版本,再进一步选择相应的源)
yolov5-lite 部署
在终端执行
cd
#python版本必须替换为你的实际版本,请到/usr/lib/查看
sudo mv /usr/lib/python3.11/EXTERNALLY-MANAGED /usr/lib/python3.11/EXTERNALLY-MANAGED.bk
sudo apt-get install -y python3-opencv --fix-missing
pip install onnxruntime
执行代码:
import cv2
import time
import numpy as np
import argparse
import onnxruntime as ort
class yolov5_lite():
def __init__(self, model_pb_path, label_path, confThreshold=0.2, nmsThreshold=0.2, objThreshold=0.5):
so = ort.SessionOptions()
so.log_severity_level = 3
self.net = ort.InferenceSession(model_pb_path, so)
self.classes = list(map(lambda x: x.strip(), open(label_path, 'r').readlines()))
self.num_classes = len(self.classes)
anchors = [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]]
self.nl = len(anchors)
self.na = len(anchors[0]) // 2
self.no = self.num_classes + 5
self.grid = [np.zeros(1)] * self.nl
self.stride = np.array([8., 16., 32.])
self.anchor_grid = np.asarray(anchors, dtype=np.float32).reshape(self.nl, -1, 2)
self.confThreshold = confThreshold
self.nmsThreshold = nmsThreshold
self.objThreshold = objThreshold
self.input_shape = (self.net.get_inputs()[0].shape[2], self.net.get_inputs()[0].shape[3])
def resize_image(self, srcimg, keep_ratio=True):
top, left, newh, neww = 0, 0, self.input_shape[0], self.input_shape[1]
if keep_ratio and srcimg.shape[0] != srcimg.shape[1]:
hw_scale = srcimg.shape[0] / srcimg.shape[1]
if hw_scale > 1:
newh, neww = self.input_shape[0], int(self.input_shape[1] / hw_scale)
img = cv2.resize(srcimg, (neww, newh), interpolation=cv2.INTER_AREA)
left = int((self.input_shape[1] - neww) * 0.5)
img = cv2.copyMakeBorder(img, 0, 0, left, self.input_shape[1] - neww - left, cv2.BORDER_CONSTANT,
value=0) # add border
else:
newh, neww = int(self.input_shape[0] * hw_scale), self.input_shape[1]
img = cv2.resize(srcimg, (neww, newh), interpolation=cv2.INTER_AREA)
top = int((self.input_shape[0] - newh) * 0.5)
img = cv2.copyMakeBorder(img, top, self.input_shape[0] - newh - top, 0, 0, cv2.BORDER_CONSTANT, value=0)
else:
img = cv2.resize(srcimg, self.input_shape, interpolation=cv2.INTER_AREA)
return img, newh, neww, top, left
def _make_grid(self, nx=20, ny=20):
xv, yv = np.meshgrid(np.arange(ny), np.arange(nx))
return np.stack((xv, yv), 2).reshape((-1, 2)).astype(np.float32)
def postprocess(self, frame, outs, pad_hw):
newh, neww, padh, padw = pad_hw
frameHeight = frame.shape[0]
frameWidth = frame.shape[1]
ratioh, ratiow = frameHeight / newh, frameWidth / neww
# Scan through all the bounding boxes output from the network and keep only the
# ones with high confidence scores. Assign the box's class label as the class with the highest score.
classIds = []
confidences = []
box_index = []
boxes = []
for detection in outs:
scores = detection[5:]
classId = np.argmax(scores)
confidence = scores[classId]
if confidence > self.confThreshold and detection[4] > self.objThreshold:
center_x = int((detection[0] - padw) * ratiow)
center_y = int((detection[1] - padh) * ratioh)
width = int(detection[2] * ratiow)
height = int(detection[3] * ratioh)
left = int(center_x - width / 2)
top = int(center_y - height / 2)
classIds.append(classId)
confidences.append(float(confidence))
boxes.append([left, top, width, height])
# Perform non maximum suppression to eliminate redundant overlapping boxes with
# lower confidences.
# print(boxes)
indices = cv2.dnn.NMSBoxes(boxes, confidences, self.confThreshold, self.nmsThreshold)
# print(indices)
for i in indices:
box_index.append(i)
for i in box_index:
box = boxes[i]
left = box[0]
top = box[1]
width = box[2]
height = box[3]
# print(classIds[i], confidences[i])
frame = self.drawPred(frame, classIds[i], confidences[i], left, top, left + width, top + height)
return frame
def drawPred(self, frame, classId, conf, left, top, right, bottom):
# Draw a bounding box.
cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), thickness=2)
label = '%.2f' % conf
label = '%s:%s' % (self.classes[classId], label)
# Display the label at the top of the bounding box
labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.5, 1)
top = max(top, labelSize[1])
# cv.rectangle(frame, (left, top - round(1.5 * labelSize[1])), (left + round(1.5 * labelSize[0]), top + baseLine), (255,255,255), cv.FILLED)
cv2.putText(frame, label, (left, top - 10), cv2.FONT_HERSHEY_TRIPLEX, 4, (0, 255, 0), thickness=2)
return frame
def detect(self, srcimg):
img, newh, neww, top, left = self.resize_image(srcimg)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = img.astype(np.float32) / 255.0
blob = np.expand_dims(np.transpose(img, (2, 0, 1)), axis=0)
t1 = time.time()
outs = self.net.run(None, {self.net.get_inputs()[0].name: blob})[0].squeeze(axis=0)
cost_time = time.time() - t1
# print(outs.shape)
row_ind = 0
for i in range(self.nl):
h, w = int(self.input_shape[0] / self.stride[i]), int(self.input_shape[1] / self.stride[i])
length = int(self.na * h * w)
if self.grid[i].shape[2:4] != (h, w):
self.grid[i] = self._make_grid(w, h)
outs[row_ind:row_ind + length, 0:2] = (outs[row_ind:row_ind + length, 0:2] * 2. - 0.5 + np.tile(
self.grid[i], (self.na, 1))) * int(self.stride[i])
outs[row_ind:row_ind + length, 2:4] = (outs[row_ind:row_ind + length, 2:4] * 2) ** 2 * np.repeat(
self.anchor_grid[i], h * w, axis=0)
row_ind += length
srcimg = self.postprocess(srcimg, outs, (newh, neww, top, left))
infer_time = 'Inference Time: ' + str(int(cost_time * 1000)) + 'ms'
cv2.putText(srcimg, infer_time, (50, 50), cv2.FONT_HERSHEY_TRIPLEX, 2, (0, 0, 0), thickness=1)
return srcimg
# if __name__ == '__main__':
# parser = argparse.ArgumentParser()
# parser.add_argument('--imgpath', type=str, default='/home/zs/Desktop/智能鸡舍/onnxruntime/R-C.jpg', help="image path")
# parser.add_argument('--modelpath', type=str, default='/home/zs/Desktop/智能鸡舍/weights/e.onnx', help="onnx filepath")
# parser.add_argument('--classfile', type=str, default='/home/zs/Desktop/智能鸡舍/onnxruntime/chicken.names', help="classname filepath")
# parser.add_argument('--confThreshold', default=0.2, type=float, help='class confidence')
# parser.add_argument('--nmsThreshold', default=0.2, type=float, help='nms iou thresh')
# args = parser.parse_args()
# srcimg = cv2.imread(args.imgpath)
# net = yolov5_lite(args.modelpath, args.classfile, confThreshold=args.confThreshold, nmsThreshold=args.nmsThreshold)
# srcimg = net.detect(srcimg.copy())
# cv2.imwrite("/home/zs/Desktop/智能鸡舍/onnxruntime/result.jpg", srcimg)
# winName = 'Deep learning object detection in onnxruntime'
# cv2.namedWindow(winName, cv2.WINDOW_NORMAL)
# cv2.imshow(winName, srcimg)
# cv2.waitKey(0)
# # cv2.imwrite('save.jpg', srcimg )
# cv2.destroyAllWindows()
传感器数据读取
dht11
dht11 | 树莓派 |
---|---|
GND | GND |
5V | 5V |
S | GPIO7 |
在终端执行
cd
pip install pyserial
git clone https://gitee.com/outlaw-maniac-zhang-san-1/adafruit_-python_-dht_-with_-pi4.git#私有库,已关闭
cd adafruit_-python_-dht_-with_-pi4/
sudo python3 setup.py install
python3
import Adafruit_DHT
humidity, temperature = Adafruit_DHT.read_retry(11, 4)
humidity,temperature
TVOC-CO2 气体传感器
TVOC-CO2 气体传感器 | 树莓派 |
---|---|
GND | GND |
5V | 5V |
A | TX |
B | RX |
在终端输入:sudo raspi-config 打开界面设置
Interfacing Options→serial→ 否 → 是
在终端输入:ls -al /dev/查看设备
代码如下(难道是模块有问题,数据一直是 0):
import serial
def parse_uart_data(data):
if len(data) < 9:
print("Invalid data length")
return None, None, None
# 解析模块地址
module_address = (data[0] << 8) + data[1]
# 计算校验和
checksum = sum(data[:-1]) & 0xFF
# 检查校验和
if checksum != data[8]:
print("Checksum mismatch")
return None, None, None
# 计算TVOC、CH2O和CO2值
TVOC = (data[2] << 8 + data[3]) * 0.001
CH2O = (data[4] << 8 + data[5]) * 0.001
CO2 = (data[6] << 8 + data[7])
return TVOC, CH2O, CO2
def read_uart_data(port='/dev/ttyS0', baudrate=9600, bytesize=8, parity='N', stopbits=1, timeout=1):
try:
ser = serial.Serial(port, baudrate=baudrate, bytesize=bytesize, parity=parity, stopbits=stopbits, timeout=timeout)
while True:
# 读取数据
data = ser.read(9)
print(data)
# 解析数据
TVOC, CH2O, CO2 = parse_uart_data(data)
# 打印解析结果
if TVOC is not None:
print("TVOC:", TVOC)
if CH2O is not None:
print("CH2O:", CH2O)
if CO2 is not None:
print("CO2:", CO2)
except KeyboardInterrupt:
ser.close()
if __name__ == "__main__":
read_uart_data()
烟雾传感器(MQ2)
接线
MQ2 烟雾传感器 | pcf8591 | 树莓派 |
---|---|---|
GND | GND | GND |
VCC | VCC | 5V |
A0 | AIN0 | |
SCL | SCL1 | |
SDA | SDA1 |
登陆上树莓派后,输入命令:sudo raspi-config 后回车;选择 Interfacing Options 后回车,选择 I2C 回车, 选择 YES 回车,最后就设置成功啦!如图所示:
如果想要查看你的传感器有没有成功连接树莓派,输入命令 i2cdetect -y 1(如果不行在命令前面加个 sudo);(图中的 48 是 pcf8591 的地址)如图:
代码如下
import time
import math
import smbus
class PCF8591:
def __init__(self):
self.CAL_PPM =20 # 校准环境中PPM值
self.RL = 5 # RL阻值
self.bus = smbus.SMBus(1) # 自动选择可用的I2C总线
self.address = self.get_address()
def get_address(self):
# 遍历0x48到0x4F之间的地址,尝试读取设备的响应
for addr in range(0x48, 0x50):
try:
self.bus.read_byte(addr)
print(addr)
return addr
except IOError:
pass
raise RuntimeError("未找到PCF8591模块")
def read(self, chn):
if chn == 0:
self.bus.write_byte(self.address, 0x40)
elif chn == 1:
self.bus.write_byte(self.address, 0x41)
elif chn == 2:
self.bus.write_byte(self.address, 0x42)
elif chn == 3:
self.bus.write_byte(self.address, 0x43)
tmp = self.bus.read_byte(self.address)
Vrl = 5 * tmp / 255 #5V ad 为8位
RS = (5 - Vrl) / Vrl * self.RL
R0 = RS / pow(self.CAL_PPM / 613.9, 1 / -2.074)
ppm = 613.9 * pow(RS / R0, -2.074)
return tmp,ppm
def write(self, val):
temp = val*(255 - 125) / 255 + 125
temp = int(temp)
self.bus.write_byte_data(self.address, 0x40, temp)
if __name__ == "__main__":
pcf8591 = PCF8591()
while True:
tmp,ppm = pcf8591.read(0)
print(tmp,ppm)
pcf8591.write(tmp)
time.sleep(0.5)
结果
执行结果
数据上报阿里云
创建产品和设备
参考阿里官方教程,使用个人用户的免费示例试用即可
创建产品和对应设备并获取设备证书_物联网应用开发-阿里云帮助中心 (aliyun.com)
定义产品物模型
参考阿里官方教程,定义温度(Temperature
)、湿度(Humidity
)、TVOC、CO2、甲醛(Formaldehyde)、鸡群(Chicken)物模型,注意请把英文名称设置为物模型的标识符,这将在代码中用到
物联网应用开发如何为产品定义物模型_物联网应用开发-阿里云帮助中心 (aliyun.com)
举例(标识符最重要):
定义完成:
安装依赖程序
传感器的数据传递功能需要安装依赖程序开启。
- 在命令窗口执行以下命令,完成程序安装。
sudo apt-get update
sudo apt-get install -y build-essential python-dev-is-python3 git
cd
git clone https://gitee.com/outlaw-maniac-zhang-san-1/paho-mqtt-1.6.1.git#私有库,已关闭
cd paho-mqtt-1.6.1
sudo python3 setup.py install
程序解读(run.py)
以下是阿里云官方提供的数据上传代码
-
导入库
#!/usr/bin/python3 import aliLink,mqttd,rpi import time,json import Adafruit_DHT
-
三元素(iot 后台获取)
ProductKey = '***' DeviceName = 'raspberrypi4-******' DeviceSecret = "assef***"
-
topic (iot 后台获取)
POST = '/sys/***/raspberrypi4-***/thing/event/property/post' # 上报消息到云 POST_REPLY = '/sys/***/raspberrypi4-***/thing/event/property/post_reply' SET = '/sys/***/raspberrypi4-***/thing/service/property/set' # 订阅云端指令
-
消息回调(云端下发消息的回调函数)
def on_message(client, userdata, msg): # print(msg.payload) Msg = json.loads(msg.payload) switch = Msg['params']['PowerLed'] rpi.powerLed(switch) print(msg.payload) # 开关值
-
连接回调(与阿里云建立链接后的回调函数)
def on_connect(client, userdata, flags, rc): pass
-
链接信息
Server, ClientId, userNmae, Password = aliLink.linkiot(DeviceName, ProductKey, DeviceSecret)
-
mqtt 链接
mqtt = mqttd.MQTT(Server, ClientId, userNmae, Password) mqtt.subscribe(SET) # 订阅服务器下发消息topic mqtt.begin(on_message, on_connect)
-
信息获取上报,每 10 秒钟上报一次系统参数
if __name__ == "__main__": # 信息获取上报,每10秒钟上报一次系统参数 while True: time.sleep(1) # 构建与云端模型一致的消息结构 updateMsn = { 'MQ2':20, 'HC_SR05':1, 'buzzer':0, 'face_recognition': 1 } JsonUpdataMsn = aliLink.Alink(updateMsn) print(JsonUpdataMsn) mqtt.push(POST,JsonUpdataMsn) # 定时向阿里云IOT推送我们构建好的Alink协议数据
运行程序
在命令行窗口执行以下命令。
python3 run.py
数据结果如图所示。
-
在 IoT 平台查询上报的数据。
前往设备详情页,单击物模型数据 > 运行状态,查看新增的机房温度和湿度数据。
接入后的效果图如下所示。
数据备份
log 函数
def log_data(data=None, error=None):
"""
记录数据到日志文件
"""
# 确保日志目录存在
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# 构建日志文件路径
log_file = os.path.join(log_dir, f"{datetime.now().strftime('%Y-%m-%d')}.log")
# 打开日志文件并追加数据
with open(log_file, 'a') as f:
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if error:
log_entry = f"{timestamp}: {error}\n"
else:
log_entry = f"{timestamp}: {data}\n"
f.write(log_entry)
结果
云平台暨整合代码运行流程(甲方定制,付费内容)
配置好的系统,用户名为:zs 密码为:zs
默认连接 wifi,名为:G 密码为:goodlife
开机后,请先参考阿里云部分教程,完成基础信息填写
然后在终端执行
cd /home/zs/Desktop/智能鸡舍/flask
python3 app.py
打开浏览器,输入 http://树莓派 IP 地址:5000 即可看到:
Docker 添加用户组
安装 docker
直接在 bash
# Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Add the repository to Apt sources: echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo docker run hello-world
执行完如果没问题的话,执行如下指令试试
docker ps -a
没提示没有“docker”这个命令就成功了
配置用户组
# 添加docker用户组,一般已存在,不需要执行 sudo groupadd docker # 将登陆用户加入到docker用户组中 sudo gpasswd -a $USER docker # 更新用户组 newgrp docker # 测试docker命令是否可以使用sudo正常使用 docker version
↩
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于