Tio Boot DocsTio Boot Docs
Home
  • java-db
  • api-table
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
Home
  • java-db
  • api-table
  • mysql
  • postgresql
  • oceanbase
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
  • 01_tio-boot 简介

    • tio-boot:新一代高性能 Java Web 开发框架
    • tio-boot 入门示例
    • Tio-Boot 配置 : 现代化的配置方案
    • tio-boot 整合 Logback
    • tio-boot 整合 hotswap-classloader 实现热加载
    • 自行编译 tio-boot
    • 最新版本
    • 开发规范
  • 02_部署

    • 使用 Maven Profile 实现分环境打包 tio-boot 项目
    • Maven 项目配置详解:依赖与 Profiles 配置
    • tio-boot 打包成 FatJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 使用Systemctl启动项目
    • 使用 Jenkins 部署 Tio-Boot 项目
    • 使用 Nginx 反向代理 Tio-Boot
    • 使用 Supervisor 管理 Java 应用
    • 已过时
    • 胖包与瘦包的打包与部署
  • 03_配置

    • 配置参数
    • 服务器监听器
    • 内置缓存系统 AbsCache
    • 使用 Redis 作为内部 Cache
    • 静态文件处理器
    • 基于域名的静态资源隔离
    • DecodeExceptionHandler
    • 开启虚拟线程(Virtual Thread)
    • 框架级错误通知
  • 04_原理

    • 生命周期
    • 请求处理流程
    • 重要的类
  • 05_json

    • Json
    • 接受 JSON 和响应 JSON
    • 响应实体类
  • 06_web

    • 概述
    • 接收请求参数
    • 接收日期参数
    • 接收数组参数
    • 返回字符串
    • 返回文本数据
    • 返回网页
    • 请求和响应字节
    • 文件上传
    • 文件下载
    • 返回视频文件并支持断点续传
    • http Session
    • Cookie
    • HttpRequest
    • HttpResponse
    • Resps
    • RespBodyVo
    • Controller拦截器
    • 请求拦截器
    • LoggingInterceptor
    • 全局异常处理器
    • 异步处理
    • 动态 返回 CSS 实现
    • 返回图片
    • 跨域
    • 添加 Controller
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • handler入门
    • 返回 multipart
    • 待定
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 常用工具类
    • HTTP Basic 认证
    • Http响应加密
    • 使用零拷贝发送大文件
    • 分片上传
    • 接口访问统计
    • 接口请求和响应数据记录
    • WebJars
    • JProtobuf
    • 测速
    • Gzip Bomb:使用压缩炸弹防御恶意爬虫
  • 07_validate

    • 数据紧校验规范
    • 参数校验
  • 08_websocket

    • 使用 tio-boot 搭建 WebSocket 服务
    • WebSocket 聊天室项目示例
  • 09_java-db

    • java‑db
    • 操作数据库入门示例
    • SQL 模板 (SqlTemplates)
    • 数据源配置与使用
    • ActiveRecord
    • Db 工具类
    • 批量操作
    • Model
    • Model生成器
    • 注解
    • 异常处理
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • 整合 Enjoy 模板最佳实践
    • 多数据源支持
    • 独立使用 ActiveRecord
    • 调用存储过程
    • java-db 整合 Guava 的 Striped 锁优化
    • 生成 SQL
    • 通过实体类操作数据库
    • java-db 读写分离
    • Spring Boot 整合 Java-DB
    • like 查询
    • 常用操作示例
    • Druid 监控集成指南
    • SQL 统计
  • 10_api-table

    • ApiTable 概述
    • 使用 ApiTable 连接 SQLite
    • 使用 ApiTable 连接 Mysql
    • 使用 ApiTable 连接 Postgres
    • 使用 ApiTable 连接 TDEngine
    • 使用 api-table 连接 oracle
    • 使用 api-table 连接 mysql and tdengine 多数据源
    • EasyExcel 导出
    • EasyExcel 导入
    • 预留
    • 预留
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
    • TQL(Table SQL)前端输入规范
  • 11_aop

    • JFinal-aop
    • Aop 工具类
    • 配置
    • 配置
    • 独立使用 JFinal Aop
    • @AImport
    • 自定义注解拦截器
    • 原理解析
  • 12_cache

    • Caffine
    • Jedis-redis
    • hutool RedisDS
    • Redisson
    • Caffeine and redis
    • CacheUtils 工具类
    • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
    • 使用 java-db 整合 ehcache
    • 使用 java-db 整合 redis
    • Java DB Redis 相关 Api
    • redis 使用示例
  • 13_认证和权限

    • FixedTokenInterceptor
    • TokenManager
    • 数据表
    • 匿名登录
    • 注册和登录
    • 个人中心
    • 重置密码
    • Google 登录
    • 短信登录
    • 移动端微信登录
    • 移动端重置密码
    • 微信登录
    • 移动端微信登录
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
  • 14_i18n

    • i18n
  • 15_enjoy

    • tio-boot 整合 Enjoy 模版引擎文档
    • Tio-Boot 整合 Java-DB 与 Enjoy 模板引擎示例
    • 引擎配置
    • 表达式
    • 指令
    • 注释
    • 原样输出
    • Shared Method 扩展
    • Shared Object 扩展
    • Extension Method 扩展
    • Spring boot 整合
    • 独立使用 Enjoy
    • tio-boot enjoy 自定义指令 localeDate
    • PromptEngine
    • Enjoy 入门示例-擎渲染大模型请求体
    • Tio Boot + Enjoy:分页与 SEO 实战指南
    • Tio Boot + Enjoy:分页与 SEO 实战指南
    • Tio Boot + Enjoy:分页与 SEO 实战指南
  • 16_定时任务

    • Quartz 定时任务集成指南
    • 分布式定时任务 xxl-jb
    • cron4j 使用指南
  • 17_tests

    • TioBootTest 类
  • 18_tio

    • TioBootServer
    • 使用 tio-core 在 tio-boot 中构建独立的 TCP 服务器
    • 内置 TCP 处理器
    • 独立启动 UDPServer
    • 使用内置 UDPServer
    • t-io 消息处理流程
    • tio-运行原理详解
    • TioConfig
    • ChannelContext
    • Tio 工具类
    • 业务数据绑定
    • 业务数据解绑
    • 发送数据
    • 关闭连接
    • Packet
    • 监控: 心跳
    • 监控: 客户端的流量数据
    • 监控: 单条 TCP 连接的流量数据
    • 监控: 端口的流量数据
    • 单条通道统计: ChannelStat
    • 所有通道统计: GroupStat
    • 资源共享
    • 成员排序
    • SSL
    • DecodeRunnable
    • 使用 AsynchronousSocketChannel 响应数据
    • 拉黑 IP
    • 深入解析 Tio 源码:构建高性能 Java 网络应用
  • 19_aio

    • ByteBuffer
    • AIO HTTP 服务器
    • 自定义和线程池和池化 ByteBuffer
    • AioHttpServer 应用示例 IP 属地查询
    • 手写 AIO Http 服务器
  • 20_netty

    • Netty TCP Server
    • Netty Web Socket Server
    • 使用 protoc 生成 Java 包文件
    • Netty WebSocket Server 二进制数据传输
    • Netty 组件详解
  • 21_netty-boot

    • Netty-Boot
    • 原理解析
    • 整合 Hot Reload
    • 整合 数据库
    • 整合 Redis
    • 整合 Elasticsearch
    • 整合 Dubbo
    • Listener
    • 文件上传
    • 拦截器
    • Spring Boot 整合 Netty-Boot
    • SSL 配置指南
    • ChannelInitializer
    • Reserve
  • 22_MQ

    • Mica-mqtt
    • EMQX
    • Disruptor
  • 23_tio-utils

    • tio-utils
    • HttpUtils
    • Notification
    • Email
    • JSON
    • File
    • Base64
    • 上传和下载
    • Http
    • Telegram
    • RsaUtils
    • EnvUtils 配置工具
    • 系统监控
    • 线程
    • 虚拟线程
    • 毫秒并发 ID (MCID) 生成方案
  • 24_tio-http-server

    • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
    • tio-boot 添加 HttpRequestHandler
    • 在 Android 上使用 tio-boot 运行 HTTP 服务
    • tio-http-server-native
    • handler 常用操作
  • 25_tio-websocket

    • WebSocket 服务器
    • WebSocket Client
    • TCP数据转发
  • 26_tio-im

    • 通讯协议文档
    • ChatPacket.proto 文档
    • java protobuf
    • 数据表设计
    • 创建工程
    • 登录
    • 历史消息
    • 发消息
  • 27_mybatis

    • Tio-Boot 整合 MyBatis
    • 使用配置类方式整合 MyBatis
    • 整合数据源
    • 使用 mybatis-plus 整合 tdengine
    • 整合 mybatis-plus
  • 28_mongodb

    • tio-boot 使用 mongo-java-driver 操作 mongodb
  • 29_elastic-search

    • Elasticsearch
    • JavaDB 整合 ElasticSearch
    • Elastic 工具类使用指南
    • Elastic-search 注意事项
    • ES 课程示例文档
  • 30_magic-script

    • tio-boot 与 magic-script 集成指南
  • 31_groovy

    • tio-boot 整合 Groovy
  • 32_firebase

    • 整合 google firebase
    • Firebase Storage
    • Firebase Authentication
    • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
    • 导出用户
    • 注册回调
    • 登录注册
  • 33_文件存储

    • 文件上传数据表
    • 本地存储
    • 存储文件到 亚马逊 S3
    • 存储文件到 腾讯 COS
    • 存储文件到 阿里云 OSS
  • 34_spider

    • jsoup
    • 爬取 z-lib.io 数据
    • 整合 WebMagic
    • WebMagic 示例:爬取学校课程数据
    • Playwright
    • Flexmark (Markdown 处理器)
    • tio-boot 整合 Playwright
    • 缓存网页数据
  • 36_integration_thirty_party

    • 整合 okhttp
    • 整合 GrpahQL
    • 集成 Mailjet
    • 整合 ip2region
    • 整合 GeoLite 离线库
    • 整合 Lark 机器人指南
    • 集成 Lark Mail 实现邮件发送
    • Thymeleaf
    • Swagger
    • Clerk 验证
  • 37_dubbo

    • 概述
    • dubbo 2.6.0
    • dubbo 2.6.0 调用过程
    • dubbo 3.2.0
  • 38_spring

    • Spring Boot Web 整合 Tio Boot
    • spring-boot-starter-webflux 整合 tio-boot
    • tio-boot 整合 spring-boot-starter
    • Tio Boot 整合 Spring Boot Starter db
    • Tio Boot 整合 Spring Boot Starter Data Redis 指南
  • 39_spring-cloud

    • tio-boot spring-cloud
  • 40_quarkus

    • Quarkus(无 HTTP)整合 tio-boot(有 HTTP)
    • tio-boot + Quarkus + Hibernate ORM Panache
  • 41_postgresql

    • PostgreSQL 安装
    • PostgreSQL 主键自增
    • PostgreSQL 日期类型
    • Postgresql 金融类型
    • PostgreSQL 数组类型
    • 索引
    • PostgreSQL 查询优化
    • 获取字段类型
    • PostgreSQL 全文检索
    • PostgreSQL 向量
    • PostgreSQL 优化向量查询
    • PostgreSQL 其他
  • 42_mysql

    • 使用 Docker 运行 MySQL
    • 常见问题
  • 43_oceanbase

    • 快速体验 OceanBase 社区版
    • 快速上手 OceanBase 数据库单机部署与管理
    • 诊断集群性能
    • 优化 SQL 性能指南
    • 待定
  • 49_jooq

    • 使用配置类方式整合 jOOQ
    • tio-boot + jOOQ 事务管理
    • 批量操作与性能优化
    • 代码生成(可选)与类型安全升级
    • JSONB、Upsert、窗口函数实战
    • 整合agroal
  • 50_media

    • JAVE 提取视频中的声音
    • Jave 提取视频中的图片
    • 待定
  • 51_asr

    • Whisper-JNI
  • 54_native-media

    • java-native-media
    • JNI 入门示例
    • mp3 拆分
    • mp4 转 mp3
    • 使用 libmp3lame 实现高质量 MP3 编码
    • Linux 编译
    • macOS 编译
    • 从 JAR 包中加载本地库文件
    • 支持的音频和视频格式
    • 任意格式转为 mp3
    • 通用格式转换
    • 通用格式拆分
    • 视频合并
    • VideoToHLS
    • split_video_to_hls 支持其他语言
    • 持久化 HLS 会话
    • 获取视频长度
    • 保存视频的最后一帧
  • 55_cv

    • 使用 Java 运行 YOLOv8 ONNX 模型进行目标检测
  • 58_telegram4j

    • 数据库设计
    • 基于 HTTP 协议开发 Telegram 翻译机器人
    • 基于 MTProto 协议开发 Telegram 翻译机器人
    • 过滤旧消息
    • 保存机器人消息
    • 定时推送
    • 增加命令菜单
    • 使用 telegram-Client
    • 使用自定义 StoreLayout
    • 延迟测试
    • Reactor 错误处理
    • Telegram4J 常见错误处理指南
  • 59_telegram-bots

    • TelegramBots 入门指南
    • 使用工具库 telegram-bot-base 开发翻译机器人
  • 60_LLM

    • 简介
    • 流式生成
    • 图片多模态输入
    • 协议自动转换 Google Gemini示例
    • 请求记录
    • 限流和错误处理
    • AI 问答
    • 增强检索(RAG)
    • 搜索+AI
    • 集成第三方 API
    • 后置处理
    • 推荐问题生成
    • 连接代码执行器
  • 61_ai_agent

    • 数据库设计
    • 示例问题管理
    • 会话管理
    • 历史记录
    • Perplexity API
    • 意图识别
    • 智能问答
    • 文件上传与解析文档
    • 翻译
    • 名人搜索功能实现
    • Ai studio gemini youbue 问答使用说明
    • 自建 YouTube 字幕问答系统
    • 自建 获取 youtube 字幕服务
    • 使用 OpenAI ASR 实现语音识别接口(Java 后端示例)
    • 定向搜索
    • 16
    • 17
    • 18
    • 在 tio-boot 应用中整合 ai-agent
    • 16
  • 63_knowlege_base

    • 数据库设计
    • 用户登录实现
    • 模型管理
    • 知识库管理
    • 文档拆分
    • 片段向量
    • 命中测试
    • 文档管理
    • 片段管理
    • 问题管理
    • 应用管理
    • 向量检索
    • 推理问答
    • 问答模块
    • 统计分析
    • 用户管理
    • api 管理
    • 存储文件到 S3
    • 文档解析优化
    • 片段汇总
    • 段落分块与检索
    • 多文档解析
    • 对话日志
    • 检索性能优化
    • Milvus
    • 文档解析方案和费用对比
    • 离线运行向量模型
  • 64_ai-search

    • ai-search 项目简介
    • ai-search 数据库文档
    • ai-search SearxNG 搜索引擎
    • ai-search Jina Reader API
    • ai-search Jina Search API
    • ai-search 搜索、重排与读取内容
    • ai-search PDF 文件处理
    • ai-search 推理问答
    • Google Custom Search JSON API
    • ai-search 意图识别
    • ai-search 问题重写
    • ai-search 系统 API 接口 WebSocket 版本
    • ai-search 搜索代码实现 WebSocket 版本
    • ai-search 生成建议问
    • ai-search 生成问题标题
    • ai-search 历史记录
    • Discover API
    • 翻译
    • Tavily Search API 文档
    • 对接 Tavily Search
    • 火山引擎 DeepSeek
    • 对接 火山引擎 DeepSeek
    • ai-search 搜索代码实现 SSE 版本
    • jar 包部署
    • Docker 部署
    • 爬取一个静态网站的所有数据
    • 网页数据预处理
    • 网页数据检索与问答流程整合
  • 65_ai-coding

    • Cline 提示词
    • Cline 提示词-中文版本
  • 66_java-uni-ai-server

    • 语音合成系统
    • Fish.audio TTS 接口说明文档与 Java 客户端封装
    • 整合 fishaudio 到 java-uni-ai-server 项目
    • 待定
  • 67_java-llm-proxy

    • 使用tio-boot搭建多模型LLM代理服务
  • 68_java-kit-server

    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • 执行 Python (Manim) 代码
    • 待定
    • 待定
    • 待定
    • 视频下载增加水印说明文档
  • 69_ai-brower

    • AI Browser:基于用户指令的浏览器自动化系统
    • 提示词
    • dom构建- buildDomTree.js
    • dom构建- 将网页可点击元素提取与可视化
    • 提取网内容
    • 启动浏览器
    • 操作浏览器指令
  • 70_tio-boot-admin

    • 入门指南
    • 初始化数据
    • token 存储
    • 与前端集成
    • 文件上传
    • 网络请求
    • 多图片管理
    • 单图片管理(只读模式)
    • 布尔值管理
    • 字段联动
    • Word 管理
    • PDF 管理
    • 文章管理
    • 富文本编辑器
  • 73_tio-mail-wing

    • tio-mail-wing简介
    • 任务1:实现POP3系统
    • 使用 getmail 验证 tio-mail-wing POP3 服务
    • 任务2:实现 SMTP 服务
    • 数据库初始化文档
    • 用户管理
    • 邮件管理
    • 任务3:实现 SMTP 服务 数据库版本
    • 任务4:实现 POP3 服务(数据库版本)
    • IMAP 协议
    • 拉取多封邮件
    • 任务5:实现 IMAP 服务(数据库版本)
    • IMAP实现讲解
    • IMAP 手动测试脚本
    • IMAP 认证机制
    • 主动推送
  • 74_mcp-server

    • 实现 MCP Server 开发指南
  • 76_manim

    • Teach me anything - 基于大语言的知识点讲解视频生成系统
    • Manim 开发环境搭建
    • 生成场景提示词
    • 生成代码
    • 完整脚本示例
    • TTS服务端
    • 废弃
    • 废弃
    • 废弃
    • 使用 SSE 流式传输生成进度的实现文档
    • 整合全流程完整文档
    • HLS 动态推流技术文档
    • manim 分场景生成代码
    • 分场景运行代码及流式播放支持
    • 分场景业务端完整实现流程
    • Maiim布局管理器
    • 仅仅生成场景代码
    • 使用 modal 运行 manim 代码
    • Python 使用 Modal GPU 加速渲染
    • Modal 平台 GPU 环境下运行 Manim
    • Modal Manim OpenGL 安装与使用
    • 优化 GPU 加速
    • 生成视频封面流程
    • Java 调用 manim 命令 执行代码 生成封面
    • Manim 图像生成服务客户端文档
    • manim render help
    • 显示 中文公式
    • ManimGL(manimgl)
    • Manim 实战入门:用代码创造数学动画
    • 欢迎
  • 80_性能测试

    • 压力测试 - tio-http-serer
    • 压力测试 - tio-boot
    • 压力测试 - tio-boot-native
    • 压力测试 - netty-boot
    • 性能测试对比
    • TechEmpower FrameworkBenchmarks
    • 压力测试 - tio-boot 12 C 32G
    • HTTP/1.1 Pipelining 性能测试报告
    • tio-boot vs Quarkus 性能对比测试报告
  • 81_tio-boot

    • 简介
    • Swagger 整合到 Tio-Boot 中的指南
    • 待定
    • 待定
    • 高性能网络编程中的 ByteBuffer 分配与回收策略
    • TioBootServerHandler 源码解析
  • 99_案例

    • 封装 IP 查询服务
    • tio-boot 案例 - 全局异常捕获与企业微信群通知
    • tio-boot 案例 - 文件上传和下载
    • tio-boot 案例 - 整合 ant design pro 增删改查
    • tio-boot 案例 - 流失响应
    • tio-boot 案例 - 增强检索
    • tio-boot 案例 - 整合 function call
    • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
    • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
    • tio-boot 案例 - 执行 shell 命令

使用 Java 运行 YOLOv8 ONNX 模型进行目标检测

  • 1. 概览与目标
  • 2. 环境与依赖
  • 3. 项目结构与关键类职责概览
  • 4. 逐文件详解
    • Detection(检测结果结构)
    • ImageUtil(图像与绘制工具)
    • Letterbox(letterbox 实现)
    • TensorUtils(张量工具类)
    • ODConfig(配置:标签、颜色等)
    • PreprocessResult(预处理结果封装)
    • 主程序:YoloObjectDetection
  • 5. 如何运行(步骤)
  • 6. 输出示例说明
  • 示例
  • 7. 性能与调优建议
  • 8. 常见问题与排查方法
  • 9. 总结

本文是一篇完整的入门文档。目标是让初学者能够理解项目结构、组件职责、关键实现细节,以及如何运行和调试。


1. 概览与目标

本示例展示如何使用 Java(OpenCV + ONNX Runtime)对 YOLOv8 ONNX 模型做单张或批量图片的目标检测。实现流程为:

  1. 读取图片并转换为 RGB(OpenCV 默认 BGR)
  2. 等比例缩放并 padding(letterbox),得到模型输入大小(例如 640×640)
  3. 将像素转成 CHW(channels-first)并归一化(0~1),封装为 OnnxTensor
  4. 使用 ONNX Runtime 进行推理
  5. 后处理(transpose、类别概率选取、xywh→xyxy、按类 NMS)
  6. 将检测框映射回原图并绘制结果

代码职责:Letterbox 与 ImageUtil 负责预处理和坐标变换;TensorUtils负责矩阵转置、argmax、NMS 等常见工具;YoloObjectDetection 是主逻辑(读图、推理、后处理、画框、显示/保存)。


2. 环境与依赖

建议的基本环境:

  • JDK 11 或更高(根据项目需要)
  • Maven(或其它构建工具)
  • 本地能加载 OpenCV 本地库(示例使用 nu.pattern.OpenCV.loadLocally())
  • ONNX Runtime 原生依赖(确保平台与运行时版本匹配)

在 pom.xml 中添加以下依赖(你在示例中已给出):

    <dependency>
      <groupId>org.openpnp</groupId>
      <artifactId>opencv</artifactId>
      <version>4.7.0-0</version>
    </dependency>

    <dependency>
      <groupId>com.microsoft.onnxruntime</groupId>
      <artifactId>onnxruntime</artifactId>
      <version>1.16.1</version>
    </dependency>
    
    <!--
    <dependency>
      <groupId>com.microsoft.onnxruntime</groupId>
      <artifactId>onnxruntime_gpu</artifactId>
      <version>1.16.1</version>
    </dependency>
    -->    

注意:

  • OpenCV 的本地库加载在不同平台上有细微差别,nu.pattern.OpenCV 可以简化开发环境(Windows/Mac/Linux 均有对应 binaries)。
  • ONNX Runtime 需要与目标平台的 native 库匹配;若需要 GPU 加速,还需使用对应 GPU 版本并配置 CUDA/cuDNN 环境。

3. 项目结构与关键类职责概览

(依据你给出的代码片段)

  • com.litongjava.yolo.domain.Detection:检测结果的数据结构(label、clsId、bbox、confidence)
  • com.litongjava.yolo.utils.ImageUtil:图像常用工具函数(resize with padding、通道变换、绘制预测等)
  • com.litongjava.yolo.utils.Letterbox:实现 letterbox(等比缩放并 pad),并记录 ratio、dw、dh,便于后续坐标还原
  • com.litongjava.yolo.config.ODConfig:颜色、标签等配置(示例中包含默认名称与随机色)
  • com.litongjava.yolo.domain.PreprocessResult:封装预处理输出(OnnxTensor、rows、cols、channels、ratio、dw、dh)
  • com.litongjava.yolo.demo.YoloObjectDetection:主程序,完成从读取模型、图片到推理、后处理、绘制、显示的完整流程
  • com.litongjava.yolo.utils.TensorUtils:实现 transposeMatrix, argmax, xywh2xyxy, nonMaxSuppression

4. 逐文件详解

下面按文件给出原始代码并在每段代码之后补充解释与关键注意点。


Detection(检测结果结构)

package com.litongjava.yolo.domain;

public class Detection {

  public String label;

  private Integer clsId;

  public float[] bbox;

  public float confidence;

  public Detection(String label, Integer clsId, float[] bbox, float confidence) {
    this.clsId = clsId;
    this.label = label;
    this.bbox = bbox;
    this.confidence = confidence;
  }

  public Detection() {

  }

  public Integer getClsId() {
    return clsId;
  }

  public void setClsId(Integer clsId) {
    this.clsId = clsId;
  }

  public String getLabel() {
    return label;
  }

  public void setLabel(String label) {
    this.label = label;
  }

  public float[] getBbox() {
    return bbox;
  }

  public void setBbox(float[] bbox) {
  }

  @Override
  public String toString() {
    return "  label=" + label + " \t clsId=" + clsId + " \t x0=" + bbox[0] + " \t y0=" + bbox[1] + " \t x1=" + bbox[2]
        + " \t y1=" + bbox[3] + " \t score=" + confidence;
  }
}

说明:

  • bbox 存储格式在示例里一般为 [xmin, ymin, xmax, ymax](在模型后处理时通过 xywh2xyxy 转换)。
  • toString() 提供打印信息便于调试。
  • 注意 setBbox 是空实现(原样保留)。如果需要设置 bbox,推荐实现为 this.bbox = bbox;(但此处保持你原始代码不变)。

ImageUtil(图像与绘制工具)

package com.litongjava.yolo.utils;

import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;

import com.litongjava.yolo.domain.Detection;

public class ImageUtil {

  public static Mat resizeWithPadding(Mat src, int width, int height) {

    Mat dst = new Mat();
    int oldW = src.width();
    int oldH = src.height();

    double r = Math.min((double) width / oldW, (double) height / oldH);

    int newUnpadW = (int) Math.round(oldW * r);
    int newUnpadH = (int) Math.round(oldH * r);

    int dw = (width - newUnpadW) / 2;
    int dh = (height - newUnpadH) / 2;

    int top = (int) Math.round(dh - 0.1);
    int bottom = (int) Math.round(dh + 0.1);
    int left = (int) Math.round(dw - 0.1);
    int right = (int) Math.round(dw + 0.1);

    Imgproc.resize(src, dst, new Size(newUnpadW, newUnpadH));
    Core.copyMakeBorder(dst, dst, top, bottom, left, right, Core.BORDER_CONSTANT);

    return dst;

  }

  public static void resizeWithPadding(Mat src, Mat dst, int width, int height) {

    int oldW = src.width();
    int oldH = src.height();

    double r = Math.min((double) width / oldW, (double) height / oldH);

    int newUnpadW = (int) Math.round(oldW * r);
    int newUnpadH = (int) Math.round(oldH * r);

    int dw = (width - newUnpadW) / 2;
    int dh = (height - newUnpadH) / 2;

    int top = (int) Math.round(dh - 0.1);
    int bottom = (int) Math.round(dh + 0.1);
    int left = (int) Math.round(dw - 0.1);
    int right = (int) Math.round(dw + 0.1);

    Imgproc.resize(src, dst, new Size(newUnpadW, newUnpadH));
    Core.copyMakeBorder(dst, dst, top, bottom, left, right, Core.BORDER_CONSTANT);

  }

  public static void whc2cwh(float[] src, float[] dst, int start) {
    int j = start;
    for (int ch = 0; ch < 3; ++ch) {
      for (int i = ch; i < src.length; i += 3) {
        dst[j] = src[i];
        j++;
      }
    }
  }

  public void xywh2xyxy(float[] bbox) {
    float x = bbox[0];
    float y = bbox[1];
    float w = bbox[2];
    float h = bbox[3];

    bbox[0] = x - w * 0.5f;
    bbox[1] = y - h * 0.5f;
    bbox[2] = x + w * 0.5f;
    bbox[3] = y + h * 0.5f;
  }

  public void scaleCoords(float[] bbox, float orgW, float orgH, float padW, float padH, float gain) {
    // xmin, ymin, xmax, ymax -> (xmin_org, ymin_org, xmax_org, ymax_org)
    bbox[0] = Math.max(0, Math.min(orgW - 1, (bbox[0] - padW) / gain));
    bbox[1] = Math.max(0, Math.min(orgH - 1, (bbox[1] - padH) / gain));
    bbox[2] = Math.max(0, Math.min(orgW - 1, (bbox[2] - padW) / gain));
    bbox[3] = Math.max(0, Math.min(orgH - 1, (bbox[3] - padH) / gain));
  }

  public static float[] whc2cwh(float[] src) {
    float[] chw = new float[src.length];
    int j = 0;
    for (int ch = 0; ch < 3; ++ch) {
      for (int i = ch; i < src.length; i += 3) {
        chw[j] = src[i];
        j++;
      }
    }
    return chw;
  }

  public static byte[] whc2cwh(byte[] src) {
    byte[] chw = new byte[src.length];
    int j = 0;
    for (int ch = 0; ch < 3; ++ch) {
      for (int i = ch; i < src.length; i += 3) {
        chw[j] = src[i];
        j++;
      }
    }
    return chw;
  }

  public static void drawPredictions(Mat img, List<Detection> detectionList) {
    // debugging image
    for (Detection detection : detectionList) {

      float[] bbox = detection.getBbox();
      Scalar color = new Scalar(249, 218, 60);
      Imgproc.rectangle(img, new Point(bbox[0], bbox[1]), new Point(bbox[2], bbox[3]), color, 2);
      Imgproc.putText(img, detection.getLabel(), new Point(bbox[0] - 1, bbox[1] - 5), Imgproc.FONT_HERSHEY_SIMPLEX, .5,
          color, 1);
    }
  }

}

说明与要点:

  • resizeWithPadding 与 Letterbox 功能类似(都做等比缩放 + pad)。示例中同时存在两种实现,保持不冲突,但要确保你在预处理里一致使用某一种(主程序使用 Letterbox)。
  • whc2cwh 系列函数用于将像素从 WHC(OpenCV 顺序 y,x,channel)转换为 CHW(模型输入顺序)。
  • drawPredictions 使用静态颜色绘制检测框,适合调试。

Letterbox(letterbox 实现)

package com.litongjava.yolo.utils;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Size;
import org.opencv.imgproc.Imgproc;

public class Letterbox {

  private Size newShape;
  private final double[] color = new double[] { 114, 114, 114 };
  private final Boolean auto = false;
  private final Boolean scaleUp = true;
  private Integer stride = 32;

  private double ratio;
  private double dw;
  private double dh;

  public Letterbox(int w, int h) {
    this.newShape = new Size(w, h);
  }

  public Letterbox() {
    this.newShape = new Size(640, 640);
  }

  public double getRatio() {
    return ratio;
  }

  public double getDw() {
    return dw;
  }

  public Integer getWidth() {
    return (int) this.newShape.width;
  }

  public Integer getHeight() {
    return (int) this.newShape.height;
  }

  public double getDh() {
    return dh;
  }

  public void setNewShape(Size newShape) {
    this.newShape = newShape;
  }

  public void setStride(Integer stride) {
    this.stride = stride;
  }

  public Mat letterbox(Mat im) { // 调整图像大小和填充图像,使满足步长约束,并记录参数

    int[] shape = { im.rows(), im.cols() }; // 当前形状 [height, width]
    // Scale ratio (new / old)
    double r = Math.min(this.newShape.height / shape[0], this.newShape.width / shape[1]);
    if (!this.scaleUp) { // 仅缩小,不扩大(一且为了mAP)
      r = Math.min(r, 1.0);
    }
    // Compute padding
    Size newUnpad = new Size(Math.round(shape[1] * r), Math.round(shape[0] * r));
    double dw = this.newShape.width - newUnpad.width, dh = this.newShape.height - newUnpad.height; // wh 填充
    if (this.auto) { // 最小矩形
      dw = dw % this.stride;
      dh = dh % this.stride;
    }
    dw /= 2; // 填充的时候两边都填充一半,使图像居于中心
    dh /= 2;
    if (shape[1] != newUnpad.width || shape[0] != newUnpad.height) { // resize
      Imgproc.resize(im, im, newUnpad, 0, 0, Imgproc.INTER_LINEAR);
    }
    int top = (int) Math.round(dh - 0.1), bottom = (int) Math.round(dh + 0.1);
    int left = (int) Math.round(dw - 0.1), right = (int) Math.round(dw + 0.1);
    // 将图像填充为正方形
    Core.copyMakeBorder(im, im, top, bottom, left, right, Core.BORDER_CONSTANT, new org.opencv.core.Scalar(this.color));
    this.ratio = r;
    this.dh = dh;
    this.dw = dw;
    return im;
  }
}

说明与要点:

  • letterbox 返回处理后的 Mat 并记录 ratio、dw、dh,这些在后续把模型输出坐标映射回原图时必须用到。
  • auto, scaleUp, stride 为可选行为控制参数(与 YOLO 官方预处理选项一致)。
  • 注意:letterbox 会直接修改传入的 Mat im(就地 resize & padding),调用者要注意传入的是 clone 还是原图。

TensorUtils(张量工具类)

package com.litongjava.yolo.utils;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class TensorUtils {

  public static void scaleCoords(float[] bbox, float orgW, float orgH, float padW, float padH, float gain) {
    // xmin, ymin, xmax, ymax -> (xmin_org, ymin_org, xmax_org, ymax_org)
    bbox[0] = Math.max(0, Math.min(orgW - 1, (bbox[0] - padW) / gain));
    bbox[1] = Math.max(0, Math.min(orgH - 1, (bbox[1] - padH) / gain));
    bbox[2] = Math.max(0, Math.min(orgW - 1, (bbox[2] - padW) / gain));
    bbox[3] = Math.max(0, Math.min(orgH - 1, (bbox[3] - padH) / gain));
  }

  public static void xywh2xyxy(float[] bbox) {
    float x = bbox[0];
    float y = bbox[1];
    float w = bbox[2];
    float h = bbox[3];

    bbox[0] = x - w * 0.5f;
    bbox[1] = y - h * 0.5f;
    bbox[2] = x + w * 0.5f;
    bbox[3] = y + h * 0.5f;
  }

  public static float[][] transposeMatrix(float[][] m) {
    float[][] temp = new float[m[0].length][m.length];
    for (int i = 0; i < m.length; i++)
      for (int j = 0; j < m[0].length; j++)
        temp[j][i] = m[i][j];
    return temp;
  }

  public static List<float[]> nonMaxSuppression(List<float[]> bboxes, float iouThreshold) {

    List<float[]> bestBboxes = new ArrayList<>();

    bboxes.sort(Comparator.comparing(a -> a[4]));

    while (!bboxes.isEmpty()) {
      float[] bestBbox = bboxes.remove(bboxes.size() - 1);
      bestBboxes.add(bestBbox);
      bboxes = bboxes.stream().filter(a -> computeIOU(a, bestBbox) < iouThreshold).collect(Collectors.toList());
    }

    return bestBboxes;
  }

  public static float computeIOU(float[] box1, float[] box2) {

    float area1 = (box1[2] - box1[0]) * (box1[3] - box1[1]);
    float area2 = (box2[2] - box2[0]) * (box2[3] - box2[1]);

    float left = Math.max(box1[0], box2[0]);
    float top = Math.max(box1[1], box2[1]);
    float right = Math.min(box1[2], box2[2]);
    float bottom = Math.min(box1[3], box2[3]);

    float interArea = Math.max(right - left, 0) * Math.max(bottom - top, 0);
    float unionArea = area1 + area2 - interArea;
    return Math.max(interArea / unionArea, 1e-8f);

  }

  // 返回最大值的索引
  public static int argmax(float[] a) {
    float re = -Float.MAX_VALUE;
    int arg = -1;
    for (int i = 0; i < a.length; i++) {
      if (a[i] >= re) {
        re = a[i];
        arg = i;
      }
    }
    return arg;
  }
}

ODConfig(配置:标签、颜色等)

package com.litongjava.yolo.config;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

public final class ODConfig {

  public static final Integer lineThicknessRatio = 333;
  public static final Double fontSizeRatio = 1080.0;

  private static final List<String> default_names = new ArrayList<>(Arrays.asList("person", "bicycle", "car",
      "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign",
      "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe",
      "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite",
      "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup",
      "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog",
      "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop",
      "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book",
      "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"));

  private static final List<String> names = new ArrayList<>(Arrays.asList("no_helmet", "helmet"));

  private final Map<String, double[]> colors;

  public ODConfig() {
    this.colors = new HashMap<>();
    default_names.forEach(name -> {
      Random random = new Random();
      double[] color = { random.nextDouble() * 256, random.nextDouble() * 256, random.nextDouble() * 256 };
      colors.put(name, color);
    });
  }

  public String getName(int clsId) {
    return names.get(clsId);
  }

  public double[] getColor(int clsId) {
    return colors.get(getName(clsId));
  }

  public double[] getNameColor(String Name) {
    return colors.get(Name);
  }

  public double[] getOtherColor(int clsId) {
    return colors.get(default_names.get(clsId));
  }
}

说明与要点:

  • 此配置类同时包含两个标签集合:default_names(80 类 COCO)与 names(示例里替换为 no_helmet, helmet)。请根据你的模型实际 label 配置选择使用哪一个集合或改造此类。
  • 构造函数为默认类别随机生成颜色(调试时很好用);生产环境下你可能想固定颜色来保持一致性。

PreprocessResult(预处理结果封装)

package com.litongjava.yolo.domain;

import ai.onnxruntime.OnnxTensor;

/**
 * 预处理结果封装
 */
public class PreprocessResult {
  public OnnxTensor tensor;
  public int rows;
  public int cols;
  public int channels;
  public double ratio;
  public double dw;
  public double dh;
}

说明:

  • 这个类把必要的预处理信息(包括 OnnxTensor 和 letterbox 参数)传递给推理与后处理模块,便于坐标还原。

主程序:YoloObjectDetection

下面是完整 YoloObjectDetection 类(——注意它包含 main、预处理、推理、后处理、绘制、以及辅助函数(加载 labels、汇集图片等)。

package com.litongjava.yolo.demo;

import java.io.File;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import com.litongjava.yolo.config.ODConfig;
import com.litongjava.yolo.domain.Detection;
import com.litongjava.yolo.domain.PreprocessResult;
import com.litongjava.yolo.utils.Letterbox;
import com.litongjava.yolo.utils.TensorUtils;

import ai.onnxruntime.OnnxTensor;
import ai.onnxruntime.OrtEnvironment;
import ai.onnxruntime.OrtException;
import ai.onnxruntime.OrtSession;

/**
 * YOLOv8 ONNX 推理示例(单张或目录图片) 重点:职责拆分、注释、日志(System.out.println)
 *
 * 依赖说明: 
 * - Letterbox:负责等比缩放 + padding,并提供 ratio、dw、dh、height、width 
 * - TensorUtils:包含 transposeMatrix, argmax, xywh2xyxy, nonMaxSuppression 等工具 
 * - ODConfig:颜色、画框配置等 - Detection:检测结果结构(label, clsId, bbox, score)
 */
public class YoloObjectDetection {

  static {
    // 加载 OpenCV 本地库(确保在运行环境中能找到)
    nu.pattern.OpenCV.loadLocally();
  }

  // 模型与阈值配置(可调整)
  private static final String MODEL_PATH = "model\\yolov8s.onnx";
  private static final String IMAGE_DIR = "images";
  private static final float CONF_THRESHOLD = 0.35f;
  private static final float NMS_THRESHOLD = 0.55f;

  public static void main(String[] args) {
    System.out.println("=== YOLOv8 ONNX Java 推理开始 ===");

    // 初始化 ONNX 环境与 session(try-with-resources 自动释放)
    try (OrtEnvironment env = OrtEnvironment.getEnvironment();
        OrtSession.SessionOptions sessionOptions = new OrtSession.SessionOptions();
        OrtSession session = env.createSession(MODEL_PATH, sessionOptions)) {

      System.out.println("Loaded model: " + MODEL_PATH);

      // 解析 labels
      String[] labels = loadLabels(session);
      System.out.println("Labels count = " + (labels == null ? 0 : labels.length));

      // 打印输入信息,方便判断静态/动态 shape
      session.getInputInfo().keySet().forEach(name -> {
        try {
          System.out.println("input name = " + name);
          System.out.println("input info = " + session.getInputInfo().get(name).getInfo().toString());
        } catch (OrtException e) {
          System.out.println("Failed to get input info: " + e.getMessage());
        }
      });

      // 找到图片列表
      Map<String, String> imageMap = gatherImages(IMAGE_DIR);
      System.out.println("Found images: " + imageMap.size());

      // OD 配置(颜色/线宽等)
      ODConfig odConfig = new ODConfig();

      // 循环处理图片
      for (Map.Entry<String, String> entry : imageMap.entrySet()) {
        String imageName = entry.getKey();
        String imagePath = entry.getValue();
        System.out.println("----- Processing: " + imageName + " (" + imagePath + ") -----");

        Mat orig = Imgcodecs.imread(imagePath);
        if (orig.empty()) {
          System.out.println("Failed to read image: " + imagePath);
          continue;
        }

        // 转为 RGB(OpenCV 默认 BGR)
        Mat image = orig.clone();
        Imgproc.cvtColor(image, image, Imgproc.COLOR_BGR2RGB);

        // 画框/文字参数:线宽随图像尺寸自适应
        int minDwDh = Math.min(orig.width(), orig.height());
        int thickness = Math.max(1, minDwDh / ODConfig.lineThicknessRatio);

        // 记录阶段时间
        long t0 = System.currentTimeMillis();

        // 1) 预处理(letterbox + CHW + normalize -> OnnxTensor)
        PreprocessResult pre = preprocess(env, image);
        System.out.println(String.format("Preprocess: rows=%d cols=%d channels=%d ratio=%.4f dw=%.2f dh=%.2f", pre.rows,
            pre.cols, pre.channels, pre.ratio, pre.dw, pre.dh));

        long t1 = System.currentTimeMillis();

        // 2) 推理
        float[][] rawOutput = runModel(session, pre.tensor);
        long t2 = System.currentTimeMillis();
        System.out.println("Inference time: " + (t2 - t1) + " ms");

        // 释放 tensor 资源
        try {
          pre.tensor.close();
        } catch (Exception ignored) {
        }

        // 3) 后处理(transpose->filter->xywh->NMS->map labels)
        List<Detection> detections = postprocess(rawOutput, labels, CONF_THRESHOLD, NMS_THRESHOLD);
        long t3 = System.currentTimeMillis();
        System.out.println("Postprocess time: " + (t3 - t2) + " ms");

        // 4) 将框从 model-space 映射回原图并画出
        drawDetections(orig, detections, pre.ratio, pre.dw, pre.dh, odConfig, thickness);

        long t4 = System.currentTimeMillis();
        System.out.printf("Total time: %d ms (pre:%d infer:%d post:%d draw:%d)%n", (t4 - t0), (t1 - t0), (t2 - t1),
            (t3 - t2), (t4 - t3));

        // 显示(本地测试用);服务器部署可改为保存图片
        HighGui.imshow("Detections", orig);
        HighGui.waitKey();
      }

      HighGui.destroyAllWindows();
      System.out.println("=== All done ===");

    } catch (OrtException e) {
      System.out.println("OrtException: " + e.getMessage());
      e.printStackTrace();
    } catch (Exception e) {
      System.out.println("Exception: " + e.getMessage());
      e.printStackTrace();
    }
  }

  /**
   * 从 session metadata 中解析 labels 字符串(例如 "{'person':0,'car':1,...}")
   */
  private static String[] loadLabels(OrtSession session) {
    try {
      String meta = session.getMetadata().getCustomMetadata().get("names");
      if (meta == null) {
        System.out.println("Model metadata 'names' not found.");
        return null;
      }
      // 去掉首尾大括号
      meta = meta.substring(1, meta.length() - 1);
      // 使用正则提取单引号内的 label
      Pattern p = Pattern.compile("'([^']*)'");
      Matcher m = p.matcher(meta);
      List<String> list = new ArrayList<>();
      while (m.find()) {
        list.add(m.group(1));
      }
      return list.toArray(new String[0]);
    } catch (Exception e) {
      System.out.println("Failed to load labels: " + e.getMessage());
      return null;
    }
  }

  /**
   * 预处理:letterbox -> CHW float /255 -> OnnxTensor 返回 tensor,以及 letterbox
   * 的元信息(ratio, dw, dh, rows, cols)
   */
  private static PreprocessResult preprocess(OrtEnvironment env, Mat image) throws OrtException {
    PreprocessResult r = new PreprocessResult();

    // Letterbox 等比缩放并 pad(你的 Letterbox 实现)
    Letterbox letterbox = new Letterbox();
    Mat letterboxed = letterbox.letterbox(image);

    r.ratio = letterbox.getRatio();
    r.dw = letterbox.getDw();
    r.dh = letterbox.getDh();
    r.rows = letterbox.getHeight();
    r.cols = letterbox.getWidth();
    r.channels = letterboxed.channels();

    // 将 Mat 的像素拷贝到 float[](CHW, 0~1)
    float[] pixels = new float[r.channels * r.rows * r.cols];
    // 注意:opencv Mat get(row, col) 的顺序是 (y,x)
    for (int i = 0; i < r.rows; i++) {
      for (int j = 0; j < r.cols; j++) {
        double[] px = letterboxed.get(i, j);
        for (int k = 0; k < r.channels; k++) {
          // CHW 排序:channel-major
          pixels[r.rows * r.cols * k + i * r.cols + j] = (float) px[k] / 255.0f;
        }
      }
    }

    long[] shape = { 1L, r.channels, r.rows, r.cols };
    r.tensor = OnnxTensor.createTensor(env, FloatBuffer.wrap(pixels), shape);

    return r;
  }

  /**
   * 执行推理并返回原始输出张量(未 transpose) 注意:返回值类型与原先代码保持一致:float[][] output =
   * ((float[][][]) output.get(0).getValue())[0];
   */
  private static float[][] runModel(OrtSession session, OnnxTensor tensor) throws OrtException {
    String inputName = session.getInputInfo().keySet().iterator().next();
    HashMap<String, OnnxTensor> inputs = new HashMap<>();
    inputs.put(inputName, tensor);

    long t0 = System.currentTimeMillis();
    try (OrtSession.Result outputs = session.run(inputs)) {
      long t1 = System.currentTimeMillis();
      // 输出一般是 [1, C, N] 或者 [1, N, C],根据模型输出取值方式保持与你原始代码一致
      Object out = outputs.get(0).getValue();
      // 期望 out 是 float[][][],取第一个 batch
      float[][] raw = ((float[][][]) out)[0];
      System.out.println("Raw output shape: " + raw.length + " x " + (raw.length > 0 ? raw[0].length : 0)
          + ", run cost: " + (t1 - t0) + " ms");
      return raw;
    }
  }

  /**
   * 后处理:transpose -> each row 为一个 box -> argmax/conf filter -> xywh->xyxy ->
   * by-class NMS -> map label
   */
  private static List<Detection> postprocess(float[][] rawOutput, String[] labels, float confThreshold,
      float nmsThreshold) {
    List<Detection> outDetections = new ArrayList<>();

    // 1) transpose => 每一行代表一个候选框
    float[][] outputData = TensorUtils.transposeMatrix(rawOutput);

    // 2) 按行解析,每行格式:[x, y, w, h, cls0, cls1, ...]
    Map<Integer, List<float[]>> class2Bbox = new HashMap<>();
    for (float[] row : outputData) {
      // 取类别概率段
      if (row.length <= 5)
        continue; // 防御性检查
      float[] probs = Arrays.copyOfRange(row, 4, row.length);
      int cls = TensorUtils.argmax(probs);
      float conf = probs[cls];
      if (conf < confThreshold)
        continue;

      // 把置信度写回到位置4,后面 NMS 会用它
      row[4] = conf;

      // xywh -> xyxy
      TensorUtils.xywh2xyxy(row);

      // 跳过无效框
      if (row[0] >= row[2] || row[1] >= row[3])
        continue;

      class2Bbox.computeIfAbsent(cls, k -> new ArrayList<>()).add(row);
    }

    // 3) per-class NMS & 封装 Detection
    for (Map.Entry<Integer, List<float[]>> e : class2Bbox.entrySet()) {
      int cls = e.getKey();
      List<float[]> bboxes = e.getValue();
      List<float[]> kept = TensorUtils.nonMaxSuppression(bboxes, nmsThreshold);
      for (float[] box : kept) {
        String label = labels != null && cls < labels.length ? labels[cls] : String.valueOf(cls);
        float score = box[4];
        float[] bboxXYXY = Arrays.copyOfRange(box, 0, 4);
        Detection d = new Detection(label, cls, bboxXYXY, score);
        outDetections.add(d);
      }
    }

    System.out.println("Detections after NMS: " + outDetections.size());
    return outDetections;
  }

  /**
   * 将检测框从 letterbox 空间映射回原图并绘制
   */
  private static void drawDetections(Mat orig, List<Detection> detections, double ratio, double dw, double dh,
      ODConfig odConfig, int thickness) {
    for (Detection det : detections) {
      float[] bbox = det.getBbox(); // model-space (在 letterbox 空间)
      // 还原为原图坐标
      Point tl = new Point((bbox[0] - dw) / ratio, (bbox[1] - dh) / ratio);
      Point br = new Point((bbox[2] - dw) / ratio, (bbox[3] - dh) / ratio);
      Scalar color = new Scalar(odConfig.getOtherColor(1));
      Imgproc.rectangle(orig, tl, br, color, thickness);
      Point textLoc = new Point(tl.x, tl.y - 3);
      Imgproc.putText(orig, det.getLabel(), textLoc, Imgproc.FONT_HERSHEY_SIMPLEX, 0.7, color, thickness);
      System.out.println(String.format("  %s clsId=%d x0=%.2f y0=%.2f x1=%.2f y1=%.2f score=%.4f", det.getLabel(),
          det.getClsId(), tl.x, tl.y, br.x, br.y, det.confidence));
    }
  }

  /**
   * 遍历目录,收集图片文件路径(递归)
   */
  private static Map<String, String> gatherImages(String imagePath) {
    Map<String, String> map = new TreeMap<>();
    File root = new File(imagePath);
    if (!root.exists()) {
      System.out.println("Image dir not found: " + imagePath);
      return map;
    }
    if (root.isFile()) {
      map.put(root.getName(), root.getAbsolutePath());
      return map;
    }
    File[] files = root.listFiles();
    if (files == null)
      return map;
    for (File f : files) {
      if (f.isDirectory()) {
        map.putAll(gatherImages(f.getPath()));
      } else {
        // 可在此加入对图片扩展名的过滤(png/jpg/jpeg)
        map.put(f.getName(), f.getAbsolutePath());
      }
    }
    return map;
  }
}

关键流程回顾:

  • loadLabels(session):从模型 metadata 的 custom metadata 中读取 names(常见于导出的 YOLO ONNX 模型),并用正则提取 label。
  • preprocess:调用 Letterbox,然后把像素按 CHW、归一化写入 float[],再用 OnnxTensor.createTensor 创建张量。
  • runModel:把 tensor 送入 session,取第一个输出并假设为 float[][][],取 [0] batch 作为 float[][] 返回。需要保证模型输出格式与这里的假设一致(若模型输出为 [1, N, C] 或 [1, C, N],你需要根据 TensorUtils 的实现来调整)。
  • postprocess:核心逻辑包括矩阵转置、类别选择、置信度过滤、坐标格式转换与 NMS,最终封装为 Detection 列表。
  • drawDetections:把检测框从 letterbox 空间映射回原图(使用 ratio, dw, dh),并绘制。

5. 如何运行(步骤)

  1. 确保在 MODEL_PATH 指定位置有你的 ONNX 模型(示例为 model\yolov8s.onnx)。模型应包含 names metadata(或你可以自行提供 labels 数组并修改 loadLabels)。

  2. 准备 images 目录并放入要检测的图片(或修改 IMAGE_DIR 为某个单张图片路径)。

  3. 在 pom.xml 中加入 OpenCV 与 ONNX Runtime 依赖(见前文)。

  4. 确保 native 库可加载:OpenCV 本地库与 ONNX Runtime 对应平台库正确安装(或使用 nu.pattern.OpenCV 的方法加载)。

  5. 编译并运行 YoloObjectDetection:

    • 在 IDE 中运行 main,或用 mvn exec:java(需配置)等方式。
  6. 程序会显示窗口(HighGui)并打印日志;在本地测试可以直接观察窗口显示结果,服务器上建议改为把带框图片保存到磁盘。


6. 输出示例说明

以下为你给出的运行输出示例(原样保留)——它展示了日志信息、模型输入信息、每张图片的处理时间与检测结果:

=== YOLOv8 ONNX Java 推理开始 ===
Loaded model: model\yolov8s.onnx
Labels count = 80
input name = images
input info = TensorInfo(javaType=FLOAT,onnxType=ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,shape=[1, 3, 640, 640])
Found images: 21
----- Processing: 10230731212230.png (E:\code\java\project-ppnt\manim-tutor\mc-qa-cv\images\10230731212230.png) -----
Preprocess: rows=640 cols=640 channels=3 ratio=0.3751 dw=0.00 dh=140.50
Raw output shape: 84 x 8400, run cost: 88 ms
Inference time: 96 ms
Detections after NMS: 11
Postprocess time: 16 ms
  person clsId=0 x0=681.35 y0=337.40 x1=896.04 y1=951.09 score=0.8843
  person clsId=0 x0=193.96 y0=296.92 x1=390.63 y1=949.13 score=0.8762
  person clsId=0 x0=509.08 y0=385.24 x1=686.32 y1=962.88 score=0.8738
  person clsId=0 x0=916.29 y0=363.28 x1=1121.89 y1=949.65 score=0.8687
  person clsId=0 x0=1272.78 y0=351.46 x1=1453.72 y1=949.50 score=0.8662
  person clsId=0 x0=321.19 y0=360.07 x1=509.52 y1=956.17 score=0.8521
  person clsId=0 x0=833.19 y0=327.07 x1=986.14 y1=955.20 score=0.7865
  person clsId=0 x0=1102.11 y0=322.76 x1=1281.91 y1=948.42 score=0.7675
  person clsId=0 x0=446.27 y0=310.94 x1=581.40 y1=947.31 score=0.6484
  person clsId=0 x0=629.68 y0=322.05 x1=767.26 y1=940.85 score=0.6145
  person clsId=0 x0=1038.93 y0=321.43 x1=1171.65 y1=944.10 score=0.3913
Total time: 256 ms (pre:137 infer:96 post:16 draw:7)
----- Processing: 20230731102545.png (E:\code\java\project-ppnt\manim-tutor\mc-qa-cv\images\20230731102545.png) -----
Preprocess: rows=640 cols=640 channels=3 ratio=0.5704 dw=0.00 dh=107.00
Raw output shape: 84 x 8400, run cost: 101 ms
Inference time: 103 ms
Detections after NMS: 7
Postprocess time: 12 ms
  person clsId=0 x0=360.66 y0=130.19 x1=654.48 y1=702.72 score=0.9203
  person clsId=0 x0=639.33 y0=298.43 x1=928.05 y1=740.09 score=0.8455
  person clsId=0 x0=271.92 y0=354.96 x1=584.70 y1=738.76 score=0.8363
  person clsId=0 x0=778.48 y0=207.74 x1=954.50 y1=551.59 score=0.8082
  person clsId=0 x0=612.80 y0=196.87 x1=795.94 y1=732.97 score=0.7952
  person clsId=0 x0=1030.43 y0=206.74 x1=1121.41 y1=400.02 score=0.6198
  person clsId=0 x0=874.44 y0=210.40 x1=1061.94 y1=461.17 score=0.5450
Total time: 244 ms (pre:125 infer:103 post:12 draw:4)

如何解读:

  • Raw output shape 显示模型输出张量维度(这里示例为 84 x 8400,需结合 TensorUtils.transposeMatrix 的实现来理解每行含义)。
  • 时间统计分为:预处理(pre)、推理(infer)、后处理(post)、绘制(draw),便于性能分析。
  • 最后一行列出检测到的每个目标的位置(x0, y0, x1, y1)与置信度。

示例 Alt text

7. 性能与调优建议

  • 模型尺寸:使用 yolov8s(小型)能在 CPU 上提供较好速度;若需要更高速度可考虑 yolov8n(nano)或在 GPU 上运行 ONNX Runtime GPU 版本。
  • 批量推理:当前实现为单张图片推理(batch=1),若对大量图片做批量推理,可以合并成 batch 增加吞吐量(注意内存与模型支持)。
  • 并发:可以用多线程并发读取/预处理图片,但 ONNX Runtime session 通常建议每线程创建独立 session 或者使用线程安全的推理池。
  • NMS 与阈值:CONF_THRESHOLD 与 NMS_THRESHOLD 会直接影响检测结果数量与重复框。可根据场景调整(例如置信度 0.25~0.6 的常见范围)。
  • IO 优化:大量图片时将图片读入内存、避免频繁 create/close tensor 会更快。注意资源释放(OnnxTensor.close())。
  • 硬件加速:在有 GPU 的环境下,使用 ONNX Runtime 的 GPU 构建能显著提升推理速度(需安装相应 runtime 与驱动)。
  • 预处理速度:letterbox 与像素转换是耗时步骤,若对速度敏感,尝试 JNI 或更高效的像素拷贝方式(直接访问 Mat 内存)。

8. 常见问题与排查方法

  • 模型加载失败:检查 MODEL_PATH 路径是否正确;检查 ONNX 模型与 ONNX Runtime 版本是否兼容。查看控制台输出的 OrtException 信息。
  • OpenCV 无法加载本地库:确保 native 库在 java.library.path 中,或使用 nu.pattern.OpenCV(如示例)来加载。
  • labels 为空:loadLabels 从 session.getMetadata().getCustomMetadata().get("names") 读取 metadata;如果模型没有包含 names,你需要手动提供 labels 文件或修改代码以从外部加载。
  • 输出维度与预期不符:打印 session.getInputInfo() 与 outputs.get(0).getInfo() 来确认模型输入/输出 shape;调整 preprocess 和 postprocess 以匹配模型输出格式。
  • 坐标映射错误(框位置不对):确认 letterbox 的 ratio, dw, dh 是否正确计算与应用;注意像素坐标与 float 的舍入问题;确保 drawDetections 中的映射公式与 letterbox 一致。
  • 内存泄漏:确保 OnnxTensor 在使用完成后 close(),并在适当位置释放不需要的 Mat 对象(尽管 JVM GC 会回收 Java 对象,但 native 资源要注意)。

9. 总结

这是一个完整且清晰的 Java 实现范例,用于运行 YOLOv8 ONNX 模型做目标检测。本文整理并解释了关键模块(预处理、模型推理、后处理、坐标还原与绘制),并附上完整代码(未删除或修改任何你给出的代码)。这些组件职责划分明确,便于扩展(例如替换模型、增加 GPU 支持、把显示改为保存图片或服务化部署等)。

Edit this page
Last Updated: 2/26/26, 3:06 PM
Contributors: litongjava