赞
踩
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
在正式动手前,先把本次实战的步骤梳理清楚,后面按部就班执行即可;
为了减少环境和软件差异的影响,让程序的运行调试更简单,这里会把SpringBoot应用制作成docker镜像,然后在docker环境运行,所以,整个实战简单来说分为三步 :制做基础镜像、开发SpringBoot应用、把应用做成镜像,如下图:
上述流程中的第一步制做基础镜像,已经在《制作JavaCV应用依赖的基础Docker镜像(CentOS7+JDK8+OpenCV4)》一文中详细介绍,咱们直接使用镜像bolingcavalry/opencv4.5.3:0.0.1即可,接下来的内容将会聚焦SpringBoot应用的开发;
这个SpringBoot应用的功能很单一,如下图所示:
整个开发过程涉及到这些步骤:提交照片的网页、神经网络初始化、文件处理、图片检测、处理检测结果、在图片上标准识别结果、前端展示图片等,完整步骤已经整理如下图:
内容很丰富,收获也不会少,更何况前文已确保可以成功运行,那么,别犹豫啦,咱们开始吧!
名称 | 链接 | 备注 |
---|---|---|
项目主页 | https://github.com/zq2599/blog_demos | 该项目在GitHub上的主页 |
git仓库地址(https) | https://github.com/zq2599/blog_demos.git | 该项目源码的仓库地址,https协议 |
git仓库地址(ssh) | git@github.com:zq2599/blog_demos.git | 该项目源码的仓库地址,ssh协议 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.bolingcavalry</groupId> <version>1.0-SNAPSHOT</version> <artifactId>yolo-demo</artifactId> <packaging>jar</packaging> <properties> <java.version>1.8</java.version> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <maven-compiler-plugin.version>3.6.1</maven-compiler-plugin.version> <springboot.version>2.4.8</springboot.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${springboot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!--FreeMarker模板视图依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.5.6</version> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>opencv-platform-gpu</artifactId> <version>4.5.3-1.5.6</version> </dependency> </dependencies> <build> <plugins> <!-- 如果父工程不是springboot,就要用以下方式使用插件,才能生成正常的jar --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.bolingcavalry.yolodemo.YoloDemoApplication</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
### FreeMarker 配置 spring.freemarker.allow-request-override=false #Enable template caching.启用模板缓存。 spring.freemarker.cache=false spring.freemarker.check-template-location=true spring.freemarker.charset=UTF-8 spring.freemarker.content-type=text/html spring.freemarker.expose-request-attributes=false spring.freemarker.expose-session-attributes=false spring.freemarker.expose-spring-macro-helpers=false #设置面板后缀 spring.freemarker.suffix=.ftl # 设置单个文件最大内存 spring.servlet.multipart.max-file-size=100MB # 设置所有文件最大内存 spring.servlet.multipart.max-request-size=1000MB # 自定义文件上传路径 web.upload-path=/app/images # 模型路径 # yolo的配置文件所在位置 opencv.yolo-cfg-path=/app/model/yolov4.cfg # yolo的模型文件所在位置 opencv.yolo-weights-path=/app/model/yolov4.weights # yolo的分类文件所在位置 opencv.yolo-coconames-path=/app/model/coco.names # yolo模型推理时的图片宽度 opencv.yolo-width=608 # yolo模型推理时的图片高度 opencv.yolo-height=608
package com.bolingcavalry.yolodemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class YoloDemoApplication {
public static void main(String[] args) {
SpringApplication.run(YoloDemoApplication.class, args);
}
}
<!DOCTYPE html> <head> <meta charset="UTF-8" /> <title>图片上传Demo</title> </head> <body> <h1 >图片上传Demo</h1> <form action="fileUpload" method="post" enctype="multipart/form-data"> <p>选择检测文件: <input type="file" name="fileName"/></p> <p><input type="submit" value="提交"/></p> </form> <#--判断是否上传文件--> <#if msg??> <span>${msg}</span><br><br> <#else > <span>${msg!("文件未上传")}</span><br> </#if> <#--显示图片,一定要在img中的src发请求给controller,否则直接跳转是乱码--> <#if fileName??> <#--<img src="/show?fileName=${fileName}" style="width: 100px"/>--> <img src="/show?fileName=${fileName}"/> <#else> <#--<img src="/show" style="width: 200px"/>--> </#if> </body> </html>
private final ResourceLoader resourceLoader; @Autowired public YoloServiceController(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Value("${web.upload-path}") private String uploadPath; @Value("${opencv.yolo-cfg-path}") private String cfgPath; @Value("${opencv.yolo-weights-path}") private String weightsPath; @Value("${opencv.yolo-coconames-path}") private String namesPath; @Value("${opencv.yolo-width}") private int width; @Value("${opencv.yolo-height}") private int height; /** * 置信度门限(超过这个值才认为是可信的推理结果) */ private float confidenceThreshold = 0.5f; private float nmsThreshold = 0.4f; // 神经网络 private Net net; // 输出层 private StringVector outNames; // 分类名称 private List<String> names;
@PostConstruct private void init() throws Exception { // 初始化打印一下,确保编码正常,否则日志输出会是乱码 log.error("file.encoding is " + System.getProperty("file.encoding")); // 神经网络初始化 net = readNetFromDarknet(cfgPath, weightsPath); // 检查网络是否为空 if (net.empty()) { log.error("神经网络初始化失败"); throw new Exception("神经网络初始化失败"); } // 输出层 outNames = net.getUnconnectedOutLayersNames(); // 检查GPU if (getCudaEnabledDeviceCount() > 0) { net.setPreferableBackend(opencv_dnn.DNN_BACKEND_CUDA); net.setPreferableTarget(opencv_dnn.DNN_TARGET_CUDA); } // 分类名称 try { names = Files.readAllLines(Paths.get(namesPath)); } catch (IOException e) { log.error("获取分类名称失败,文件路径[{}]", namesPath, e); } }
/** * 上传文件到指定目录 * @param file 文件 * @param path 文件存放路径 * @param fileName 源文件名 * @return */ private static boolean upload(MultipartFile file, String path, String fileName){ //使用原文件名 String realPath = path + "/" + fileName; File dest = new File(realPath); //判断文件父目录是否存在 if(!dest.getParentFile().exists()){ dest.getParentFile().mkdir(); } try { //保存文件 file.transferTo(dest); return true; } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } }
@RequestMapping("fileUpload") public String upload(@RequestParam("fileName") MultipartFile file, Map<String, Object> map){ log.info("文件 [{}], 大小 [{}]", file.getOriginalFilename(), file.getSize()); // 文件名称 String originalFileName = file.getOriginalFilename(); if (!upload(file, uploadPath, originalFileName)){ map.put("msg", "上传失败!"); return "forward:/index"; } // 读取文件到Mat Mat src = imread(uploadPath + "/" + originalFileName); // 执行推理 MatVector outs = doPredict(src); // 处理原始的推理结果, // 对检测到的每个目标,找出置信度最高的类别作为改目标的类别, // 还要找出每个目标的位置,这些信息都保存在ObjectDetectionResult对象中 List<ObjectDetectionResult> results = postprocess(src, outs); // 释放资源 outs.releaseReference(); // 检测到的目标总数 int detectNum = results.size(); log.info("一共检测到{}个目标", detectNum); // 没检测到 if (detectNum<1) { // 显示图片 map.put("msg", "未检测到目标"); // 文件名 map.put("fileName", originalFileName); return "forward:/index"; } else { // 检测结果页面的提示信息 map.put("msg", "检测到" + results.size() + "个目标"); } // 计算出总耗时,并输出在图片的左上角 printTimeUsed(src); // 将每一个被识别的对象在图片框出来,并在框的左上角标注该对象的类别 markEveryDetectObject(src, results); // 将添加了标注的图片保持在磁盘上,并将图片信息写入map(给跳转页面使用) saveMarkedImage(map, src); return "forward:/index"; }
/** * 用神经网络执行推理 * @param src * @return */ private MatVector doPredict(Mat src) { // 将图片转为四维blog,并且对尺寸做调整 Mat inputBlob = blobFromImage(src, 1 / 255.0, new Size(width, height), new Scalar(0.0), true, false, CV_32F); // 神经网络输入 net.setInput(inputBlob); // 设置输出结果保存的容器 MatVector outs = new MatVector(outNames.size()); // 推理,结果保存在outs中 net.forward(outs, outNames); // 释放资源 inputBlob.release(); return outs; }
检测结果MatVector对象是个集合,里面有多个Mat对象,每个Mat对象是一个表格,里面有丰富的数据,具体的内容如下图:
看过上图后,相信您对如何处理原始的检测结果已经胸有成竹了,只要从MatVector中逐个取出Mat,把每个Mat当做表格,将表格每一行中概率最大的列找到,此列就是该物体的类别了(至于每一列到底是啥东西,为啥上面表格中第五列是人,第六列是自行车,最后一列是牙刷?这个稍后会讲到):
/** * 推理完成后的操作 * @param frame * @param outs * @return */ private List<ObjectDetectionResult> postprocess(Mat frame, MatVector outs) { final IntVector classIds = new IntVector(); final FloatVector confidences = new FloatVector(); final RectVector boxes = new RectVector(); // 处理神经网络的输出结果 for (int i = 0; i < outs.size(); ++i) { // extract the bounding boxes that have a high enough score // and assign their highest confidence class prediction. // 每个检测到的物体,都有对应的每种类型的置信度,取最高的那种 // 例如检车到猫的置信度百分之九十,狗的置信度百分之八十,那就认为是猫 Mat result = outs.get(i); FloatIndexer data = result.createIndexer(); // 将检测结果看做一个表格, // 每一行表示一个物体, // 前面四列表示这个物体的坐标,后面的每一列,表示这个物体在某个类别上的置信度, // 每行都是从第五列开始遍历,找到最大值以及对应的列号, for (int j = 0; j < result.rows(); j++) { // minMaxLoc implemented in java because it is 1D int maxIndex = -1; float maxScore = Float.MIN_VALUE; for (int k = 5; k < result.cols(); k++) { float score = data.get(j, k); if (score > maxScore) { maxScore = score; maxIndex = k - 5; } } // 如果最大值大于之前设定的置信度门限,就表示可以确定是这类物体了, // 然后就把这个物体相关的识别信息保存下来,要保存的信息有:类别、置信度、坐标 if (maxScore > confidenceThreshold) { int centerX = (int) (data.get(j, 0) * frame.cols()); int centerY = (int) (data.get(j, 1) * frame.rows()); int width = (int) (data.get(j, 2) * frame.cols()); int height = (int) (data.get(j, 3) * frame.rows()); int left = centerX - width / 2; int top = centerY - height / 2; // 保存类别 classIds.push_back(maxIndex); // 保存置信度 confidences.push_back(maxScore); // 保存坐标 boxes.push_back(new Rect(left, top, width, height)); } } // 资源释放 data.release(); result.release(); } // remove overlapping bounding boxes with NMS IntPointer indices = new IntPointer(confidences.size()); FloatPointer confidencesPointer = new FloatPointer(confidences.size()); confidencesPointer.put(confidences.get()); // 非极大值抑制 NMSBoxes(boxes, confidencesPointer, confidenceThreshold, nmsThreshold, indices, 1.f, 0); // 将检测结果放入BO对象中,便于业务处理 List<ObjectDetectionResult> detections = new ArrayList<>(); for (int i = 0; i < indices.limit(); ++i) { final int idx = indices.get(i); final Rect box = boxes.get(idx); final int clsId = classIds.get(idx); detections.add(new ObjectDetectionResult( clsId, names.get(clsId), confidences.get(idx), box.x(), box.y(), box.width(), box.height() )); // 释放资源 box.releaseReference(); } // 释放资源 indices.releaseReference(); confidencesPointer.releaseReference(); classIds.releaseReference(); confidences.releaseReference(); boxes.releaseReference(); return detections; }
@Data @AllArgsConstructor public class ObjectDetectionResult { // 类别索引 int classId; // 类别名称 String className; // 置信度 float confidence; // 物体在照片中的横坐标 int x; // 物体在照片中的纵坐标 int y; // 物体宽度 int width; // 物体高度 int height; }
/** * 计算出总耗时,并输出在图片的左上角 * @param src */ private void printTimeUsed(Mat src) { // 总次数 long totalNums = net.getPerfProfile(new DoublePointer()); // 频率 double freq = getTickFrequency()/1000; // 总次数除以频率就是总耗时 double t = totalNums / freq; // 将本次检测的总耗时打印在展示图像的左上角 putText(src, String.format("Inference time : %.2f ms", t), new Point(10, 20), FONT_HERSHEY_SIMPLEX, 0.6, new Scalar(255, 0, 0, 0), 1, LINE_AA, false); }
/** * 将每一个被识别的对象在图片框出来,并在框的左上角标注该对象的类别 * @param src * @param results */ private void markEveryDetectObject(Mat src, List<ObjectDetectionResult> results) { // 在图片上标出每个目标以及类别和置信度 for(ObjectDetectionResult result : results) { log.info("类别[{}],置信度[{}%]", result.getClassName(), result.getConfidence() * 100f); // annotate on image rectangle(src, new Point(result.getX(), result.getY()), new Point(result.getX() + result.getWidth(), result.getY() + result.getHeight()), Scalar.MAGENTA, 1, LINE_8, 0); // 写在目标左上角的内容:类别+置信度 String label = result.getClassName() + ":" + String.format("%.2f%%", result.getConfidence() * 100f); // 计算显示这些内容所需的高度 IntPointer baseLine = new IntPointer(); Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, baseLine); int top = Math.max(result.getY(), labelSize.height()); // 添加内容到图片上 putText(src, label, new Point(result.getX(), top-4), FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 255, 0, 0), 1, LINE_4, false); } }
# 基础镜像集成了openjdk8和opencv4.5.3 FROM bolingcavalry/opencv4.5.3:0.0.1 # 创建目录 RUN mkdir -p /app/images && mkdir -p /app/model # 指定镜像的内容的来源位置 ARG DEPENDENCY=target/dependency # 复制内容到镜像 COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib COPY ${DEPENDENCY}/META-INF /app/META-INF COPY ${DEPENDENCY}/BOOT-INF/classes /app ENV LANG C.UTF-8 ENV LANGUAGE zh_CN.UTF-8 ENV LC_ALL C.UTF-8 ENV TZ Asia/Shanghai # 指定启动命令(注意要执行编码,否则日志是乱码) ENTRYPOINT ["java","-Dfile.encoding=utf-8","-cp","app:app/lib/*","com.bolingcavalry.yolodemo.YoloDemoApplication"]
mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
docker build -t bolingcavalry/yolodemo:0.0.1 .
will@willMini yolo-demo % docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
bolingcavalry/yolodemo 0.0.1 d0ef6e734b53 About a minute ago 2.99GB
bolingcavalry/opencv4.5.3 0.0.1 d1518ffa4699 6 days ago 2.01GB
/home/will/temp/202110/19/
├── images
└── model
├── coco.names
├── yolov4.cfg
└── yolov4.weights
sudo docker run \
--rm \
--name yolodemo \
-p 8080:8080 \
-v /home/will/temp/202110/19/images:/app/images \
-v /home/will/temp/202110/19/model:/app/model \
bolingcavalry/yolodemo:0.0.1
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。