Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
I
instrument_reader_cpp
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
唐永康
instrument_reader_cpp
Commits
95f55843
Commit
95f55843
authored
Jun 05, 2026
by
唐永康
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
添加给浩天的注释,后续对接
parent
d7c16b1f
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
448 additions
and
27 deletions
+448
-27
CMakeLists.txt
CMakeLists.txt
+19
-0
airspeed_lut_cli.cpp
apps/airspeed_lut_cli.cpp
+12
-1
clion_demo.cpp
apps/clion_demo.cpp
+11
-1
instrument_reader_cli.cpp
apps/instrument_reader_cli.cpp
+18
-1
non_linear_gauge_demo.cpp
apps/non_linear_gauge_demo.cpp
+22
-1
airspeed_reader.hpp
include/instrument_reader/airspeed_reader.hpp
+60
-1
angle_math.hpp
include/instrument_reader/angle_math.hpp
+6
-1
dispatcher.hpp
include/instrument_reader/dispatcher.hpp
+29
-1
instrument_classifier.hpp
include/instrument_reader/instrument_classifier.hpp
+23
-1
lut_mapper.hpp
include/instrument_reader/lut_mapper.hpp
+25
-1
non_linear_gauge_reader.hpp
include/instrument_reader/non_linear_gauge_reader.hpp
+21
-8
types.hpp
include/instrument_reader/types.hpp
+72
-2
build_msvc.ps1
scripts/build_msvc.ps1
+14
-1
build_ubuntu22.sh
scripts/build_ubuntu22.sh
+6
-0
open_clion.ps1
scripts/open_clion.ps1
+8
-1
open_clion.sh
scripts/open_clion.sh
+3
-0
run_demo.ps1
scripts/run_demo.ps1
+9
-1
run_demo_ubuntu22.sh
scripts/run_demo_ubuntu22.sh
+7
-0
setup_ubuntu22.sh
scripts/setup_ubuntu22.sh
+2
-0
airspeed_reader.cpp
src/airspeed_reader.cpp
+0
-0
dispatcher.cpp
src/dispatcher.cpp
+0
-0
instrument_classifier.cpp
src/instrument_classifier.cpp
+26
-1
lut_mapper.cpp
src/lut_mapper.cpp
+31
-1
non_linear_gauge_reader.cpp
src/non_linear_gauge_reader.cpp
+17
-2
types.cpp
src/types.cpp
+7
-1
No files found.
CMakeLists.txt
View file @
95f55843
cmake_minimum_required
(
VERSION 3.20
)
cmake_minimum_required
(
VERSION 3.20
)
# 使用传统计算机视觉读取仪表和表盘的主 C++ 项目。
project
(
instrument_reader_cpp VERSION 0.1.0 LANGUAGES CXX
)
project
(
instrument_reader_cpp VERSION 0.1.0 LANGUAGES CXX
)
# C++17 已支持 filesystem,同时可保持代码兼容 Ubuntu 22.04。
set
(
CMAKE_CXX_STANDARD 17
)
set
(
CMAKE_CXX_STANDARD 17
)
set
(
CMAKE_CXX_STANDARD_REQUIRED ON
)
set
(
CMAKE_CXX_STANDARD_REQUIRED ON
)
set
(
CMAKE_CXX_EXTENSIONS OFF
)
set
(
CMAKE_CXX_EXTENSIONS OFF
)
# OpenCV 是唯一的大型依赖,用于图像读写和基础视觉运算。
find_package
(
OpenCV REQUIRED
)
find_package
(
OpenCV REQUIRED
)
# 演示目标使用的可选样例根目录,可通过 CMake、环境变量或仓库布局设置。
set
(
INSTRUMENT_READER_SAMPLE_ROOT
""
CACHE PATH
"Sample data root containing original_crops and generated_scales"
)
set
(
INSTRUMENT_READER_SAMPLE_ROOT
""
CACHE PATH
"Sample data root containing original_crops and generated_scales"
)
if
(
NOT INSTRUMENT_READER_SAMPLE_ROOT
)
if
(
NOT INSTRUMENT_READER_SAMPLE_ROOT
)
if
(
DEFINED ENV{INSTRUMENT_READER_SAMPLE_ROOT}
)
if
(
DEFINED ENV{INSTRUMENT_READER_SAMPLE_ROOT}
)
...
@@ -17,6 +21,7 @@ if(NOT INSTRUMENT_READER_SAMPLE_ROOT)
...
@@ -17,6 +21,7 @@ if(NOT INSTRUMENT_READER_SAMPLE_ROOT)
endif
()
endif
()
endif
()
endif
()
# CLI 工具和演示程序共用的核心库。
add_library
(
instrument_reader_core
add_library
(
instrument_reader_core
src/airspeed_reader.cpp
src/airspeed_reader.cpp
src/dispatcher.cpp
src/dispatcher.cpp
...
@@ -26,35 +31,41 @@ add_library(instrument_reader_core
...
@@ -26,35 +31,41 @@ add_library(instrument_reader_core
src/types.cpp
src/types.cpp
)
)
# 导出公共头文件,便于未来集成到 ROS 2 节点。
target_include_directories
(
instrument_reader_core
target_include_directories
(
instrument_reader_core
PUBLIC
PUBLIC
${
CMAKE_CURRENT_SOURCE_DIR
}
/include
${
CMAKE_CURRENT_SOURCE_DIR
}
/include
${
OpenCV_INCLUDE_DIRS
}
${
OpenCV_INCLUDE_DIRS
}
)
)
# 公共头文件暴露了 cv::Mat/cv::Point 类型,因此以传递方式链接 OpenCV。
target_link_libraries
(
instrument_reader_core
target_link_libraries
(
instrument_reader_core
PUBLIC
PUBLIC
${
OpenCV_LIBS
}
${
OpenCV_LIBS
}
)
)
# 启用严格警告,并为边缘设备吞吐量优化 Release 构建。
if
(
MSVC
)
if
(
MSVC
)
target_compile_options
(
instrument_reader_core PRIVATE /EHsc /W4 $<$<CONFIG:Release>:/O2>
)
target_compile_options
(
instrument_reader_core PRIVATE /EHsc /W4 $<$<CONFIG:Release>:/O2>
)
else
()
else
()
target_compile_options
(
instrument_reader_core PRIVATE -Wall -Wextra -Wpedantic $<$<CONFIG:Release>:-O3>
)
target_compile_options
(
instrument_reader_core PRIVATE -Wall -Wextra -Wpedantic $<$<CONFIG:Release>:-O3>
)
endif
()
endif
()
# 批量图像处理的主入口程序。
add_executable
(
instrument_reader_cli apps/instrument_reader_cli.cpp
)
add_executable
(
instrument_reader_cli apps/instrument_reader_cli.cpp
)
target_link_libraries
(
instrument_reader_cli PRIVATE instrument_reader_core
)
target_link_libraries
(
instrument_reader_cli PRIVATE instrument_reader_core
)
add_executable
(
airspeed_lut_cli apps/airspeed_lut_cli.cpp
)
add_executable
(
airspeed_lut_cli apps/airspeed_lut_cli.cpp
)
target_link_libraries
(
airspeed_lut_cli PRIVATE instrument_reader_core
)
target_link_libraries
(
airspeed_lut_cli PRIVATE instrument_reader_core
)
# 通用非线性分段插值的独立演示程序。
add_executable
(
non_linear_gauge_demo apps/non_linear_gauge_demo.cpp
)
add_executable
(
non_linear_gauge_demo apps/non_linear_gauge_demo.cpp
)
target_link_libraries
(
non_linear_gauge_demo PRIVATE instrument_reader_core
)
target_link_libraries
(
non_linear_gauge_demo PRIVATE instrument_reader_core
)
add_executable
(
clion_demo apps/clion_demo.cpp
)
add_executable
(
clion_demo apps/clion_demo.cpp
)
target_link_libraries
(
clion_demo PRIVATE instrument_reader_core
)
target_link_libraries
(
clion_demo PRIVATE instrument_reader_core
)
# Windows 下从 CLion/Visual Studio 启动调试时,需要在 PATH 中加入 OpenCV DLL。
if
(
WIN32 AND OpenCV_DIR
)
if
(
WIN32 AND OpenCV_DIR
)
get_filename_component
(
INSTRUMENT_READER_OPENCV_BIN
"
${
OpenCV_DIR
}
/../bin"
ABSOLUTE
)
get_filename_component
(
INSTRUMENT_READER_OPENCV_BIN
"
${
OpenCV_DIR
}
/../bin"
ABSOLUTE
)
set
(
INSTRUMENT_READER_RUN_ENV
"PATH=
${
INSTRUMENT_READER_OPENCV_BIN
}
;$ENV{PATH}"
)
set
(
INSTRUMENT_READER_RUN_ENV
"PATH=
${
INSTRUMENT_READER_OPENCV_BIN
}
;$ENV{PATH}"
)
...
@@ -67,11 +78,14 @@ else()
...
@@ -67,11 +78,14 @@ else()
set
(
INSTRUMENT_READER_RUN_ENV
"PATH=$ENV{PATH}"
)
set
(
INSTRUMENT_READER_RUN_ENV
"PATH=$ENV{PATH}"
)
endif
()
endif
()
# 运行目标在此根目录下创建 output/outputN 目录。
set
(
INSTRUMENT_READER_OUTPUT_ROOT
"
${
CMAKE_CURRENT_SOURCE_DIR
}
/output"
CACHE PATH
"Output root for CLion run targets"
)
set
(
INSTRUMENT_READER_OUTPUT_ROOT
"
${
CMAKE_CURRENT_SOURCE_DIR
}
/output"
CACHE PATH
"Output root for CLion run targets"
)
if
(
INSTRUMENT_READER_SAMPLE_ROOT AND EXISTS
"
${
INSTRUMENT_READER_SAMPLE_ROOT
}
/original_crops"
)
if
(
INSTRUMENT_READER_SAMPLE_ROOT AND EXISTS
"
${
INSTRUMENT_READER_SAMPLE_ROOT
}
/original_crops"
)
# 空速表底图用于在指针检测时去除静态表盘标记。
set
(
INSTRUMENT_READER_AIRSPEED_BASE
"
${
INSTRUMENT_READER_SAMPLE_ROOT
}
/generated_scales/01_airspeed_indicator/_restored_base_no_pointer.png"
CACHE FILEPATH
"Airspeed no-pointer base/template image"
)
set
(
INSTRUMENT_READER_AIRSPEED_BASE
"
${
INSTRUMENT_READER_SAMPLE_ROOT
}
/generated_scales/01_airspeed_indicator/_restored_base_no_pointer.png"
CACHE FILEPATH
"Airspeed no-pointer base/template image"
)
# 完整演示会写出叠加图和 dispatch_results.json。
add_custom_target
(
run_demo
add_custom_target
(
run_demo
COMMAND
${
CMAKE_COMMAND
}
-E make_directory
"
${
INSTRUMENT_READER_OUTPUT_ROOT
}
"
COMMAND
${
CMAKE_COMMAND
}
-E make_directory
"
${
INSTRUMENT_READER_OUTPUT_ROOT
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
...
@@ -86,6 +100,7 @@ if(INSTRUMENT_READER_SAMPLE_ROOT AND EXISTS "${INSTRUMENT_READER_SAMPLE_ROOT}/or
...
@@ -86,6 +100,7 @@ if(INSTRUMENT_READER_SAMPLE_ROOT AND EXISTS "${INSTRUMENT_READER_SAMPLE_ROOT}/or
VERBATIM
VERBATIM
)
)
# 无叠加图目标用于将算法耗时与 PNG 写出耗时分离。
add_custom_target
(
run_demo_no_overlay
add_custom_target
(
run_demo_no_overlay
COMMAND
${
CMAKE_COMMAND
}
-E make_directory
"
${
INSTRUMENT_READER_OUTPUT_ROOT
}
"
COMMAND
${
CMAKE_COMMAND
}
-E make_directory
"
${
INSTRUMENT_READER_OUTPUT_ROOT
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
...
@@ -102,6 +117,7 @@ if(INSTRUMENT_READER_SAMPLE_ROOT AND EXISTS "${INSTRUMENT_READER_SAMPLE_ROOT}/or
...
@@ -102,6 +117,7 @@ if(INSTRUMENT_READER_SAMPLE_ROOT AND EXISTS "${INSTRUMENT_READER_SAMPLE_ROOT}/or
)
)
endif
()
endif
()
# 用于验证角度到物理值转换的 LUT 冒烟测试目标。
add_custom_target
(
run_lut_example
add_custom_target
(
run_lut_example
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
$<TARGET_FILE:airspeed_lut_cli>
$<TARGET_FILE:airspeed_lut_cli>
...
@@ -111,6 +127,7 @@ add_custom_target(run_lut_example
...
@@ -111,6 +127,7 @@ add_custom_target(run_lut_example
VERBATIM
VERBATIM
)
)
# 小型合成非线性插值可视化的演示目标。
add_custom_target
(
run_non_linear_gauge_demo
add_custom_target
(
run_non_linear_gauge_demo
COMMAND
${
CMAKE_COMMAND
}
-E make_directory
"
${
INSTRUMENT_READER_OUTPUT_ROOT
}
"
COMMAND
${
CMAKE_COMMAND
}
-E make_directory
"
${
INSTRUMENT_READER_OUTPUT_ROOT
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
COMMAND
${
CMAKE_COMMAND
}
-E env
"
${
INSTRUMENT_READER_RUN_ENV
}
"
...
@@ -120,10 +137,12 @@ add_custom_target(run_non_linear_gauge_demo
...
@@ -120,10 +137,12 @@ add_custom_target(run_non_linear_gauge_demo
VERBATIM
VERBATIM
)
)
# 安装库和可执行工具,供外部项目使用。
install
(
TARGETS instrument_reader_core instrument_reader_cli airspeed_lut_cli non_linear_gauge_demo
install
(
TARGETS instrument_reader_core instrument_reader_cli airspeed_lut_cli non_linear_gauge_demo
RUNTIME DESTINATION bin
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
ARCHIVE DESTINATION lib
)
)
# 将公共头文件安装到标准 include 目录树。
install
(
DIRECTORY include/ DESTINATION include
)
install
(
DIRECTORY include/ DESTINATION include
)
apps/airspeed_lut_cli.cpp
View file @
95f55843
#include "instrument_reader/lut_mapper.hpp"
#
include
"instrument_reader/lut_mapper.hpp"
#include <filesystem>
#include <filesystem>
#include <iomanip>
#include <iomanip>
...
@@ -10,6 +10,7 @@ using namespace instrument_reader;
...
@@ -10,6 +10,7 @@ using namespace instrument_reader;
namespace
{
namespace
{
// 使用说明同时覆盖交互式标定和单次映射模式。
void
printUsage
()
{
void
printUsage
()
{
std
::
cout
std
::
cout
<<
"usage:
\n
"
<<
"usage:
\n
"
...
@@ -17,6 +18,7 @@ void printUsage() {
...
@@ -17,6 +18,7 @@ void printUsage() {
<<
" airspeed_lut_cli --map <lut.json> --theta <deg> [--boundary reject|clamp]
\n
"
;
<<
" airspeed_lut_cli --map <lut.json> --theta <deg> [--boundary reject|clamp]
\n
"
;
}
}
// 无参数模式是默认 LUT 的轻量冒烟测试。
int
runDefaultExample
()
{
int
runDefaultExample
()
{
LutMapper
mapper
;
LutMapper
mapper
;
std
::
string
error
;
std
::
string
error
;
...
@@ -27,6 +29,8 @@ int runDefaultExample() {
...
@@ -27,6 +29,8 @@ int runDefaultExample() {
}
}
MapResult
r
=
mapper
.
map
(
4.5
);
MapResult
r
=
mapper
.
map
(
4.5
);
// 输出类似 JSON 的结果,便于脚本解析。
std
::
cout
<<
"{
\n
"
;
std
::
cout
<<
"{
\n
"
;
std
::
cout
<<
"
\"
valid
\"
: "
<<
(
r
.
valid
?
"true"
:
"false"
)
<<
",
\n
"
;
std
::
cout
<<
"
\"
valid
\"
: "
<<
(
r
.
valid
?
"true"
:
"false"
)
<<
",
\n
"
;
if
(
r
.
valid
)
std
::
cout
<<
"
\"
value
\"
: "
<<
std
::
fixed
<<
std
::
setprecision
(
6
)
<<
r
.
value
<<
",
\n
"
;
if
(
r
.
valid
)
std
::
cout
<<
"
\"
value
\"
: "
<<
std
::
fixed
<<
std
::
setprecision
(
6
)
<<
r
.
value
<<
",
\n
"
;
...
@@ -52,6 +56,7 @@ int main(int argc, char** argv) {
...
@@ -52,6 +56,7 @@ int main(int argc, char** argv) {
bool
hasTheta
=
false
;
bool
hasTheta
=
false
;
BoundaryPolicy
policy
=
BoundaryPolicy
::
Reject
;
BoundaryPolicy
policy
=
BoundaryPolicy
::
Reject
;
// 手工解析参数,使该工具保持可移植且无额外依赖。
for
(
int
i
=
1
;
i
<
argc
;
++
i
)
{
for
(
int
i
=
1
;
i
<
argc
;
++
i
)
{
std
::
string
arg
=
argv
[
i
];
std
::
string
arg
=
argv
[
i
];
auto
value
=
[
&
](
const
char
*
name
)
->
std
::
string
{
auto
value
=
[
&
](
const
char
*
name
)
->
std
::
string
{
...
@@ -79,6 +84,7 @@ int main(int argc, char** argv) {
...
@@ -79,6 +84,7 @@ int main(int argc, char** argv) {
}
}
if
(
!
calibrateOut
.
empty
())
{
if
(
!
calibrateOut
.
empty
())
{
// 交互式标定用于收集物理值与绝对角度配对。
LutMapper
mapper
(
startAngle
);
LutMapper
mapper
(
startAngle
);
std
::
cout
<<
"Enter calibration points. Empty value line finishes.
\n
"
;
std
::
cout
<<
"Enter calibration points. Empty value line finishes.
\n
"
;
while
(
true
)
{
while
(
true
)
{
...
@@ -95,6 +101,8 @@ int main(int argc, char** argv) {
...
@@ -95,6 +101,8 @@ int main(int argc, char** argv) {
mapper
.
addPoint
(
std
::
stod
(
valueLine
),
std
::
stod
(
thetaLine
));
mapper
.
addPoint
(
std
::
stod
(
valueLine
),
std
::
stod
(
thetaLine
));
}
}
std
::
string
error
;
std
::
string
error
;
// 拒绝保存非单调 LUT,因为它会导致插值结果不安全。
if
(
!
mapper
.
validate
(
&
error
))
{
if
(
!
mapper
.
validate
(
&
error
))
{
std
::
cerr
<<
"bad calibration: "
<<
error
<<
"
\n
"
;
std
::
cerr
<<
"bad calibration: "
<<
error
<<
"
\n
"
;
return
1
;
return
1
;
...
@@ -108,6 +116,7 @@ int main(int argc, char** argv) {
...
@@ -108,6 +116,7 @@ int main(int argc, char** argv) {
}
}
if
(
!
mapPath
.
empty
()
&&
hasTheta
)
{
if
(
!
mapPath
.
empty
()
&&
hasTheta
)
{
// 单次映射模式适合验证实测仪表角度。
LutMapper
mapper
;
LutMapper
mapper
;
std
::
string
error
;
std
::
string
error
;
if
(
!
mapper
.
loadJson
(
mapPath
,
&
error
))
{
if
(
!
mapper
.
loadJson
(
mapPath
,
&
error
))
{
...
@@ -116,6 +125,8 @@ int main(int argc, char** argv) {
...
@@ -116,6 +125,8 @@ int main(int argc, char** argv) {
}
}
mapper
.
setBoundaryPolicy
(
policy
);
mapper
.
setBoundaryPolicy
(
policy
);
MapResult
r
=
mapper
.
map
(
theta
);
MapResult
r
=
mapper
.
map
(
theta
);
// 输出结构与默认示例保持一致。
std
::
cout
<<
"{
\n
"
;
std
::
cout
<<
"{
\n
"
;
std
::
cout
<<
"
\"
valid
\"
: "
<<
(
r
.
valid
?
"true"
:
"false"
)
<<
",
\n
"
;
std
::
cout
<<
"
\"
valid
\"
: "
<<
(
r
.
valid
?
"true"
:
"false"
)
<<
",
\n
"
;
if
(
r
.
valid
)
std
::
cout
<<
"
\"
value
\"
: "
<<
std
::
fixed
<<
std
::
setprecision
(
6
)
<<
r
.
value
<<
",
\n
"
;
if
(
r
.
valid
)
std
::
cout
<<
"
\"
value
\"
: "
<<
std
::
fixed
<<
std
::
setprecision
(
6
)
<<
r
.
value
<<
",
\n
"
;
...
...
apps/clion_demo.cpp
View file @
95f55843
#include "instrument_reader/dispatcher.hpp"
#
include
"instrument_reader/dispatcher.hpp"
#include "instrument_reader/lut_mapper.hpp"
#include "instrument_reader/lut_mapper.hpp"
#include <filesystem>
#include <filesystem>
...
@@ -11,6 +11,7 @@ using namespace instrument_reader;
...
@@ -11,6 +11,7 @@ using namespace instrument_reader;
namespace
{
namespace
{
// 无论 CLion 从仓库根目录还是构建目录启动,都能解析源代码目录。
fs
::
path
projectRootFromCurrentWorkingDirectory
()
{
fs
::
path
projectRootFromCurrentWorkingDirectory
()
{
fs
::
path
cwd
=
fs
::
current_path
();
fs
::
path
cwd
=
fs
::
current_path
();
if
(
fs
::
exists
(
cwd
/
"CMakeLists.txt"
)
&&
fs
::
exists
(
cwd
/
"include"
/
"instrument_reader"
))
{
if
(
fs
::
exists
(
cwd
/
"CMakeLists.txt"
)
&&
fs
::
exists
(
cwd
/
"include"
/
"instrument_reader"
))
{
...
@@ -22,6 +23,7 @@ fs::path projectRootFromCurrentWorkingDirectory() {
...
@@ -22,6 +23,7 @@ fs::path projectRootFromCurrentWorkingDirectory() {
return
fs
::
absolute
(
cwd
);
return
fs
::
absolute
(
cwd
);
}
}
// 优先通过环境变量定位样例数据集,随后尝试常见仓库目录布局。
fs
::
path
findSampleRoot
(
const
fs
::
path
&
projectRoot
)
{
fs
::
path
findSampleRoot
(
const
fs
::
path
&
projectRoot
)
{
if
(
const
char
*
env
=
std
::
getenv
(
"INSTRUMENT_READER_SAMPLE_ROOT"
))
{
if
(
const
char
*
env
=
std
::
getenv
(
"INSTRUMENT_READER_SAMPLE_ROOT"
))
{
fs
::
path
p
(
env
);
fs
::
path
p
(
env
);
...
@@ -39,9 +41,11 @@ fs::path findSampleRoot(const fs::path& projectRoot) {
...
@@ -39,9 +41,11 @@ fs::path findSampleRoot(const fs::path& projectRoot) {
throw
std
::
runtime_error
(
"sample root not found; set INSTRUMENT_READER_SAMPLE_ROOT"
);
throw
std
::
runtime_error
(
"sample root not found; set INSTRUMENT_READER_SAMPLE_ROOT"
);
}
}
// 端到端视觉演示:分类、读取空速表角度、映射物理值并写出叠加图。
void
runVisionDemo
(
const
fs
::
path
&
projectRoot
,
const
fs
::
path
&
sampleRoot
)
{
void
runVisionDemo
(
const
fs
::
path
&
projectRoot
,
const
fs
::
path
&
sampleRoot
)
{
fs
::
path
airspeedBase
=
sampleRoot
/
"generated_scales"
/
"01_airspeed_indicator"
/
"_restored_base_no_pointer.png"
;
fs
::
path
airspeedBase
=
sampleRoot
/
"generated_scales"
/
"01_airspeed_indicator"
/
"_restored_base_no_pointer.png"
;
// 使用明确路径,使演示程序在 CLion 目标中保持一致行为。
InstrumentDispatcher
::
Options
options
;
InstrumentDispatcher
::
Options
options
;
options
.
outputDir
=
projectRoot
/
"output"
;
options
.
outputDir
=
projectRoot
/
"output"
;
options
.
airspeedTemplatePath
=
airspeedBase
;
options
.
airspeedTemplatePath
=
airspeedBase
;
...
@@ -51,6 +55,10 @@ void runVisionDemo(const fs::path& projectRoot, const fs::path& sampleRoot) {
...
@@ -51,6 +55,10 @@ void runVisionDemo(const fs::path& projectRoot, const fs::path& sampleRoot) {
options
.
writeJson
=
true
;
options
.
writeJson
=
true
;
InstrumentDispatcher
dispatcher
(
options
);
InstrumentDispatcher
dispatcher
(
options
);
// 同时处理原始裁剪图和生成的空速表样例。
// 调度器每次只加载一张图像,因此该演示不会在内存中同时持有
// 整个数据集解码后的像素。
DispatchSummary
summary
=
dispatcher
.
run
({
DispatchSummary
summary
=
dispatcher
.
run
({
sampleRoot
/
"original_crops"
,
sampleRoot
/
"original_crops"
,
sampleRoot
/
"generated_scales"
/
"01_airspeed_indicator"
,
sampleRoot
/
"generated_scales"
/
"01_airspeed_indicator"
,
...
@@ -65,6 +73,7 @@ void runVisionDemo(const fs::path& projectRoot, const fs::path& sampleRoot) {
...
@@ -65,6 +73,7 @@ void runVisionDemo(const fs::path& projectRoot, const fs::path& sampleRoot) {
std
::
cout
<<
"output_dir="
<<
summary
.
outputDir
.
string
()
<<
"
\n
"
;
std
::
cout
<<
"output_dir="
<<
summary
.
outputDir
.
string
()
<<
"
\n
"
;
}
}
// 小型 LUT 冒烟测试,通过默认 LUT 验证角度跨越 360 度零点的处理。
void
runLutDemo
(
const
fs
::
path
&
projectRoot
)
{
void
runLutDemo
(
const
fs
::
path
&
projectRoot
)
{
LutMapper
mapper
;
LutMapper
mapper
;
std
::
string
error
;
std
::
string
error
;
...
@@ -89,6 +98,7 @@ void runLutDemo(const fs::path& projectRoot) {
...
@@ -89,6 +98,7 @@ void runLutDemo(const fs::path& projectRoot) {
int
main
()
{
int
main
()
{
try
{
try
{
// 输出解析后的路径,便于 CLion 用户诊断错误的工作目录。
fs
::
path
projectRoot
=
projectRootFromCurrentWorkingDirectory
();
fs
::
path
projectRoot
=
projectRootFromCurrentWorkingDirectory
();
fs
::
path
sampleRoot
=
findSampleRoot
(
projectRoot
);
fs
::
path
sampleRoot
=
findSampleRoot
(
projectRoot
);
...
...
apps/instrument_reader_cli.cpp
View file @
95f55843
#include "instrument_reader/dispatcher.hpp"
#
include
"instrument_reader/dispatcher.hpp"
#include <filesystem>
#include <filesystem>
#include <initializer_list>
#include <initializer_list>
...
@@ -12,6 +12,7 @@ using namespace instrument_reader;
...
@@ -12,6 +12,7 @@ using namespace instrument_reader;
namespace
{
namespace
{
// 无需任何项目状态即可输出 CLI 使用约定。
void
printUsage
()
{
void
printUsage
()
{
std
::
cout
std
::
cout
<<
"usage: instrument_reader_cli --input <file_or_dir> [--input <file_or_dir> ...]
\n
"
<<
"usage: instrument_reader_cli --input <file_or_dir> [--input <file_or_dir> ...]
\n
"
...
@@ -20,6 +21,7 @@ void printUsage() {
...
@@ -20,6 +21,7 @@ void printUsage() {
<<
" [--visual-threshold 0.40] [--no-recursive] [--no-overlays]
\n
"
;
<<
" [--visual-threshold 0.40] [--no-recursive] [--no-overlays]
\n
"
;
}
}
// 从按优先级排序的候选列表中返回第一个存在的路径。
fs
::
path
firstExisting
(
std
::
initializer_list
<
fs
::
path
>
paths
)
{
fs
::
path
firstExisting
(
std
::
initializer_list
<
fs
::
path
>
paths
)
{
for
(
const
fs
::
path
&
p
:
paths
)
{
for
(
const
fs
::
path
&
p
:
paths
)
{
if
(
fs
::
exists
(
p
))
return
p
;
if
(
fs
::
exists
(
p
))
return
p
;
...
@@ -27,6 +29,8 @@ fs::path firstExisting(std::initializer_list<fs::path> paths) {
...
@@ -27,6 +29,8 @@ fs::path firstExisting(std::initializer_list<fs::path> paths) {
return
{};
return
{};
}
}
// 优先使用用户输入目录附近的标定资源,使生成数据集可以携带
// 自己的无指针底图和模板。
fs
::
path
findAirspeedAssetNearInputs
(
const
std
::
vector
<
fs
::
path
>&
inputs
,
std
::
initializer_list
<
fs
::
path
>
relativePaths
)
{
fs
::
path
findAirspeedAssetNearInputs
(
const
std
::
vector
<
fs
::
path
>&
inputs
,
std
::
initializer_list
<
fs
::
path
>
relativePaths
)
{
std
::
vector
<
fs
::
path
>
roots
;
std
::
vector
<
fs
::
path
>
roots
;
for
(
const
fs
::
path
&
input
:
inputs
)
{
for
(
const
fs
::
path
&
input
:
inputs
)
{
...
@@ -36,6 +40,8 @@ fs::path findAirspeedAssetNearInputs(const std::vector<fs::path>& inputs, std::i
...
@@ -36,6 +40,8 @@ fs::path findAirspeedAssetNearInputs(const std::vector<fs::path>& inputs, std::i
if
(
fs
::
is_regular_file
(
root
))
root
=
root
.
parent_path
();
if
(
fs
::
is_regular_file
(
root
))
root
=
root
.
parent_path
();
if
(
!
fs
::
is_directory
(
root
))
continue
;
if
(
!
fs
::
is_directory
(
root
))
continue
;
// 同时搜索目录本身及其父目录,以支持类似
// root/01_airspeed_indicator/_base_no_pointer.png 的布局。
roots
.
push_back
(
root
);
roots
.
push_back
(
root
);
if
(
root
.
has_parent_path
())
roots
.
push_back
(
root
.
parent_path
());
if
(
root
.
has_parent_path
())
roots
.
push_back
(
root
.
parent_path
());
}
}
...
@@ -52,9 +58,12 @@ fs::path findAirspeedAssetNearInputs(const std::vector<fs::path>& inputs, std::i
...
@@ -52,9 +58,12 @@ fs::path findAirspeedAssetNearInputs(const std::vector<fs::path>& inputs, std::i
}
// namespace
}
// namespace
int
main
(
int
argc
,
char
**
argv
)
{
int
main
(
int
argc
,
char
**
argv
)
{
// 此处 CLI 只拥有路径和配置向量。解码后的图像内存稍后才会在
// InstrumentDispatcher::processOne() 内部分配。
std
::
vector
<
fs
::
path
>
inputs
;
std
::
vector
<
fs
::
path
>
inputs
;
InstrumentDispatcher
::
Options
options
;
InstrumentDispatcher
::
Options
options
;
// 简单手工解析使二进制程序保持无额外依赖。
for
(
int
i
=
1
;
i
<
argc
;
++
i
)
{
for
(
int
i
=
1
;
i
<
argc
;
++
i
)
{
std
::
string
arg
=
argv
[
i
];
std
::
string
arg
=
argv
[
i
];
auto
value
=
[
&
](
const
char
*
name
)
->
std
::
string
{
auto
value
=
[
&
](
const
char
*
name
)
->
std
::
string
{
...
@@ -81,9 +90,12 @@ int main(int argc, char** argv) {
...
@@ -81,9 +90,12 @@ int main(int argc, char** argv) {
}
}
if
(
inputs
.
empty
())
{
if
(
inputs
.
empty
())
{
// 适合当前仓库布局的开发者默认值。
inputs
.
push_back
(
firstExisting
({
"../2/original_crops"
,
"D:/chuav/gague/2/original_crops"
}));
inputs
.
push_back
(
firstExisting
({
"../2/original_crops"
,
"D:/chuav/gague/2/original_crops"
}));
}
}
// 与全局回退资源相比,这些本地资源更安全,因为不同生成数据集之间
// 的指针和底图几何参数可能不同。
const
fs
::
path
localAirspeedBase
=
findAirspeedAssetNearInputs
(
inputs
,
{
const
fs
::
path
localAirspeedBase
=
findAirspeedAssetNearInputs
(
inputs
,
{
"_base_no_pointer.png"
,
"_base_no_pointer.png"
,
"_restored_base_no_pointer.png"
,
"_restored_base_no_pointer.png"
,
...
@@ -93,6 +105,7 @@ int main(int argc, char** argv) {
...
@@ -93,6 +105,7 @@ int main(int argc, char** argv) {
});
});
if
(
options
.
airspeedTemplatePath
.
empty
())
{
if
(
options
.
airspeedTemplatePath
.
empty
())
{
// 模板供视觉分类器使用。
options
.
airspeedTemplatePath
=
firstExisting
({
options
.
airspeedTemplatePath
=
firstExisting
({
localAirspeedBase
,
localAirspeedBase
,
"../2/generated_scales/01_airspeed_indicator/_restored_base_no_pointer.png"
,
"../2/generated_scales/01_airspeed_indicator/_restored_base_no_pointer.png"
,
...
@@ -100,9 +113,11 @@ int main(int argc, char** argv) {
...
@@ -100,9 +113,11 @@ int main(int argc, char** argv) {
});
});
}
}
if
(
options
.
airspeedBasePath
.
empty
())
{
if
(
options
.
airspeedBasePath
.
empty
())
{
// 底图供指针读数器抑制静态表盘标记。
options
.
airspeedBasePath
=
firstExisting
({
localAirspeedBase
,
options
.
airspeedTemplatePath
});
options
.
airspeedBasePath
=
firstExisting
({
localAirspeedBase
,
options
.
airspeedTemplatePath
});
}
}
if
(
options
.
airspeedLutPath
.
empty
())
{
if
(
options
.
airspeedLutPath
.
empty
())
{
// 默认 LUT 包含当前实测的非线性空速刻度。
options
.
airspeedLutPath
=
firstExisting
({
options
.
airspeedLutPath
=
firstExisting
({
"configs/airspeed_lut_example.json"
,
"configs/airspeed_lut_example.json"
,
"../configs/airspeed_lut_example.json"
,
"../configs/airspeed_lut_example.json"
,
...
@@ -111,6 +126,8 @@ int main(int argc, char** argv) {
...
@@ -111,6 +126,8 @@ int main(int argc, char** argv) {
}
}
try
{
try
{
// 调度器负责实际图像循环和输出写入。
// 它还拥有空速表射线、模板 cv::Mat 等读数器缓存状态。
InstrumentDispatcher
dispatcher
(
options
);
InstrumentDispatcher
dispatcher
(
options
);
DispatchSummary
summary
=
dispatcher
.
run
(
inputs
);
DispatchSummary
summary
=
dispatcher
.
run
(
inputs
);
std
::
cout
<<
"processed="
<<
summary
.
results
.
size
()
<<
"
\n
"
;
std
::
cout
<<
"processed="
<<
summary
.
results
.
size
()
<<
"
\n
"
;
...
...
apps/non_linear_gauge_demo.cpp
View file @
95f55843
#include "instrument_reader/non_linear_gauge_reader.hpp"
#
include
"instrument_reader/non_linear_gauge_reader.hpp"
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgproc.hpp>
...
@@ -15,8 +15,10 @@
...
@@ -15,8 +15,10 @@
namespace
{
namespace
{
// 使用局部常量,使该独立演示不依赖 angle_math.hpp。
constexpr
float
kPi
=
3.14159265358979323846
f
;
constexpr
float
kPi
=
3.14159265358979323846
f
;
// 为演示产物创建下一个编号的输出目录。
std
::
filesystem
::
path
nextOutputDir
(
const
std
::
filesystem
::
path
&
outputRoot
)
{
std
::
filesystem
::
path
nextOutputDir
(
const
std
::
filesystem
::
path
&
outputRoot
)
{
std
::
filesystem
::
create_directories
(
outputRoot
);
std
::
filesystem
::
create_directories
(
outputRoot
);
...
@@ -28,12 +30,16 @@ std::filesystem::path nextOutputDir(const std::filesystem::path& outputRoot) {
...
@@ -28,12 +30,16 @@ std::filesystem::path nextOutputDir(const std::filesystem::path& outputRoot) {
int
runNumber
=
0
;
int
runNumber
=
0
;
bool
valid
=
true
;
bool
valid
=
true
;
// 只解析严格符合 outputN 形式的目录名。
for
(
std
::
size_t
i
=
std
::
char_traits
<
char
>::
length
(
prefix
);
i
<
name
.
size
();
++
i
)
{
for
(
std
::
size_t
i
=
std
::
char_traits
<
char
>::
length
(
prefix
);
i
<
name
.
size
();
++
i
)
{
if
(
name
[
i
]
<
'0'
||
name
[
i
]
>
'9'
)
{
if
(
name
[
i
]
<
'0'
||
name
[
i
]
>
'9'
)
{
valid
=
false
;
valid
=
false
;
break
;
break
;
}
}
const
int
digit
=
name
[
i
]
-
'0'
;
const
int
digit
=
name
[
i
]
-
'0'
;
// 异常目录名包含过多数字时,避免发生整数溢出。
if
(
runNumber
>
(
std
::
numeric_limits
<
int
>::
max
()
-
digit
)
/
10
)
{
if
(
runNumber
>
(
std
::
numeric_limits
<
int
>::
max
()
-
digit
)
/
10
)
{
valid
=
false
;
valid
=
false
;
break
;
break
;
...
@@ -43,12 +49,14 @@ std::filesystem::path nextOutputDir(const std::filesystem::path& outputRoot) {
...
@@ -43,12 +49,14 @@ std::filesystem::path nextOutputDir(const std::filesystem::path& outputRoot) {
if
(
valid
&&
runNumber
>
maxRunNumber
)
maxRunNumber
=
runNumber
;
if
(
valid
&&
runNumber
>
maxRunNumber
)
maxRunNumber
=
runNumber
;
}
}
// 依次尝试下一个整数编号,直到目录创建成功。
for
(
int
runNumber
=
maxRunNumber
+
1
;;
++
runNumber
)
{
for
(
int
runNumber
=
maxRunNumber
+
1
;;
++
runNumber
)
{
const
std
::
filesystem
::
path
outputDir
=
outputRoot
/
(
"output"
+
std
::
to_string
(
runNumber
));
const
std
::
filesystem
::
path
outputDir
=
outputRoot
/
(
"output"
+
std
::
to_string
(
runNumber
));
if
(
std
::
filesystem
::
create_directory
(
outputDir
))
return
outputDir
;
if
(
std
::
filesystem
::
create_directory
(
outputDir
))
return
outputDir
;
}
}
}
}
// 将图像坐标系角度转换为合成表盘上的点。
cv
::
Point
pointOnDial
(
const
cv
::
Point2f
&
center
,
float
radius
,
float
angleDeg
)
{
cv
::
Point
pointOnDial
(
const
cv
::
Point2f
&
center
,
float
radius
,
float
angleDeg
)
{
const
float
radians
=
angleDeg
*
kPi
/
180.0
f
;
const
float
radians
=
angleDeg
*
kPi
/
180.0
f
;
return
{
return
{
...
@@ -57,6 +65,7 @@ cv::Point pointOnDial(const cv::Point2f& center, float radius, float angleDeg) {
...
@@ -57,6 +65,7 @@ cv::Point pointOnDial(const cv::Point2f& center, float radius, float angleDeg) {
};
};
}
}
// 为生成 PNG 中的标签提供格式化辅助。
void
putCenteredText
(
cv
::
Mat
&
image
,
const
std
::
string
&
text
,
const
cv
::
Point
&
anchor
,
double
scale
,
cv
::
Scalar
color
,
void
putCenteredText
(
cv
::
Mat
&
image
,
const
std
::
string
&
text
,
const
cv
::
Point
&
anchor
,
double
scale
,
cv
::
Scalar
color
,
int
thickness
=
1
)
{
int
thickness
=
1
)
{
int
baseline
=
0
;
int
baseline
=
0
;
...
@@ -65,6 +74,7 @@ void putCenteredText(cv::Mat& image, const std::string& text, const cv::Point& a
...
@@ -65,6 +74,7 @@ void putCenteredText(cv::Mat& image, const std::string& text, const cv::Point& a
color
,
thickness
,
cv
::
LINE_AA
);
color
,
thickness
,
cv
::
LINE_AA
);
}
}
// 绘制小型合成仪表,展示标定节点和被测试指针。
bool
writeResultImage
(
const
std
::
filesystem
::
path
&
imagePath
,
bool
writeResultImage
(
const
std
::
filesystem
::
path
&
imagePath
,
const
std
::
vector
<
instrument_reader
::
nonlinear
::
CalibrationPoint
>&
calibration
,
const
std
::
vector
<
instrument_reader
::
nonlinear
::
CalibrationPoint
>&
calibration
,
float
currentAngle
,
float
currentValue
)
{
float
currentAngle
,
float
currentValue
)
{
...
@@ -74,11 +84,14 @@ bool writeResultImage(const std::filesystem::path& imagePath,
...
@@ -74,11 +84,14 @@ bool writeResultImage(const std::filesystem::path& imagePath,
const
cv
::
Point
centerPoint
(
cvRound
(
center
.
x
),
cvRound
(
center
.
y
));
const
cv
::
Point
centerPoint
(
cvRound
(
center
.
x
),
cvRound
(
center
.
y
));
constexpr
float
radius
=
210.0
f
;
constexpr
float
radius
=
210.0
f
;
// 浅色背景便于脱离真实图像集检查演示输出。
// 这是该演示唯一分配的完整图像缓冲区。
cv
::
Mat
image
(
height
,
width
,
CV_8UC3
,
cv
::
Scalar
(
248
,
249
,
250
));
cv
::
Mat
image
(
height
,
width
,
CV_8UC3
,
cv
::
Scalar
(
248
,
249
,
250
));
cv
::
circle
(
image
,
centerPoint
,
cvRound
(
radius
),
cv
::
Scalar
(
40
,
45
,
52
),
3
,
cv
::
LINE_AA
);
cv
::
circle
(
image
,
centerPoint
,
cvRound
(
radius
),
cv
::
Scalar
(
40
,
45
,
52
),
3
,
cv
::
LINE_AA
);
cv
::
circle
(
image
,
centerPoint
,
8
,
cv
::
Scalar
(
40
,
45
,
52
),
-
1
,
cv
::
LINE_AA
);
cv
::
circle
(
image
,
centerPoint
,
8
,
cv
::
Scalar
(
40
,
45
,
52
),
-
1
,
cv
::
LINE_AA
);
// 绘制标定刻度及其物理值。
for
(
const
auto
&
point
:
calibration
)
{
for
(
const
auto
&
point
:
calibration
)
{
const
cv
::
Point
outer
=
pointOnDial
(
center
,
radius
,
point
.
angle
);
const
cv
::
Point
outer
=
pointOnDial
(
center
,
radius
,
point
.
angle
);
const
cv
::
Point
inner
=
pointOnDial
(
center
,
radius
-
22.0
f
,
point
.
angle
);
const
cv
::
Point
inner
=
pointOnDial
(
center
,
radius
-
22.0
f
,
point
.
angle
);
...
@@ -90,6 +103,7 @@ bool writeResultImage(const std::filesystem::path& imagePath,
...
@@ -90,6 +103,7 @@ bool writeResultImage(const std::filesystem::path& imagePath,
putCenteredText
(
image
,
valueLabel
.
str
(),
labelPoint
,
0.55
,
cv
::
Scalar
(
35
,
40
,
48
),
1
);
putCenteredText
(
image
,
valueLabel
.
str
(),
labelPoint
,
0.55
,
cv
::
Scalar
(
35
,
40
,
48
),
1
);
}
}
// 使用红色绘制被查询的指针角度。
const
cv
::
Point
pointerTip
=
pointOnDial
(
center
,
radius
-
28.0
f
,
currentAngle
);
const
cv
::
Point
pointerTip
=
pointOnDial
(
center
,
radius
-
28.0
f
,
currentAngle
);
cv
::
line
(
image
,
centerPoint
,
pointerTip
,
cv
::
Scalar
(
20
,
20
,
210
),
5
,
cv
::
LINE_AA
);
cv
::
line
(
image
,
centerPoint
,
pointerTip
,
cv
::
Scalar
(
20
,
20
,
210
),
5
,
cv
::
LINE_AA
);
cv
::
circle
(
image
,
centerPoint
,
13
,
cv
::
Scalar
(
20
,
20
,
210
),
-
1
,
cv
::
LINE_AA
);
cv
::
circle
(
image
,
centerPoint
,
13
,
cv
::
Scalar
(
20
,
20
,
210
),
-
1
,
cv
::
LINE_AA
);
...
@@ -111,6 +125,8 @@ int main() {
...
@@ -111,6 +125,8 @@ int main() {
using
instrument_reader
::
nonlinear
::
CalibrationPoint
;
using
instrument_reader
::
nonlinear
::
CalibrationPoint
;
using
instrument_reader
::
nonlinear
::
NonLinearGaugeReader
;
using
instrument_reader
::
nonlinear
::
NonLinearGaugeReader
;
// 真实测量得到的空速刻度:80-140、140-200 和 200-300
// 是展开角度坐标系中的三个独立线性分段。
const
std
::
vector
<
CalibrationPoint
>
airspeedCalibration
=
{
const
std
::
vector
<
CalibrationPoint
>
airspeedCalibration
=
{
{
334.1
f
,
80.0
f
},
{
334.1
f
,
80.0
f
},
{
406.1
f
,
140.0
f
},
{
406.1
f
,
140.0
f
},
...
@@ -118,8 +134,11 @@ int main() {
...
@@ -118,8 +134,11 @@ int main() {
{
586.5
f
,
300.0
f
},
{
586.5
f
,
300.0
f
},
};
};
// 读数器复制或移动该小型标定表并预计算斜率,此过程不涉及图像内存。
const
NonLinearGaugeReader
reader
(
airspeedCalibration
,
BoundaryMode
::
Clamp
);
const
NonLinearGaugeReader
reader
(
airspeedCalibration
,
BoundaryMode
::
Clamp
);
const
float
thetaFromYoloDeg
=
35.99
f
;
const
float
thetaFromYoloDeg
=
35.99
f
;
// 80-300 的扫描范围跨越 360 度零点,因此需要展开 YOLO 输出的模 360 角度。
const
float
thetaUnwrappedDeg
=
const
float
thetaUnwrappedDeg
=
thetaFromYoloDeg
<
airspeedCalibration
.
front
().
angle
?
thetaFromYoloDeg
+
360.0
f
:
thetaFromYoloDeg
;
thetaFromYoloDeg
<
airspeedCalibration
.
front
().
angle
?
thetaFromYoloDeg
+
360.0
f
:
thetaFromYoloDeg
;
const
float
airspeedKmh
=
reader
.
calculate_value
(
thetaUnwrappedDeg
);
const
float
airspeedKmh
=
reader
.
calculate_value
(
thetaUnwrappedDeg
);
...
@@ -128,6 +147,7 @@ int main() {
...
@@ -128,6 +147,7 @@ int main() {
const
std
::
filesystem
::
path
outputPath
=
outputDir
/
"non_linear_gauge_demo_result.json"
;
const
std
::
filesystem
::
path
outputPath
=
outputDir
/
"non_linear_gauge_demo_result.json"
;
const
std
::
filesystem
::
path
imagePath
=
outputDir
/
"non_linear_gauge_demo_result.png"
;
const
std
::
filesystem
::
path
imagePath
=
outputDir
/
"non_linear_gauge_demo_result.png"
;
// 先写出可视化产物,再写 JSON,确保 JSON 不会指向缺失图像。
if
(
!
writeResultImage
(
imagePath
,
airspeedCalibration
,
thetaUnwrappedDeg
,
airspeedKmh
))
{
if
(
!
writeResultImage
(
imagePath
,
airspeedCalibration
,
thetaUnwrappedDeg
,
airspeedKmh
))
{
std
::
cerr
<<
"failed_to_write_image="
<<
std
::
filesystem
::
absolute
(
imagePath
).
string
()
<<
"
\n
"
;
std
::
cerr
<<
"failed_to_write_image="
<<
std
::
filesystem
::
absolute
(
imagePath
).
string
()
<<
"
\n
"
;
return
1
;
return
1
;
...
@@ -139,6 +159,7 @@ int main() {
...
@@ -139,6 +159,7 @@ int main() {
return
1
;
return
1
;
}
}
// 最小 JSON 报告沿用主调度器的输出风格。
out
<<
std
::
fixed
<<
std
::
setprecision
(
3
)
<<
"{
\n
"
out
<<
std
::
fixed
<<
std
::
setprecision
(
3
)
<<
"{
\n
"
<<
"
\"
valid
\"
: true,
\n
"
<<
"
\"
valid
\"
: true,
\n
"
<<
"
\"
theta_deg
\"
: "
<<
thetaFromYoloDeg
<<
",
\n
"
<<
"
\"
theta_deg
\"
: "
<<
thetaFromYoloDeg
<<
",
\n
"
...
...
include/instrument_reader/airspeed_reader.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include "instrument_reader/types.hpp"
#include "instrument_reader/types.hpp"
...
@@ -10,59 +10,118 @@
...
@@ -10,59 +10,118 @@
namespace
instrument_reader
{
namespace
instrument_reader
{
// 使用传统计算机视觉方法,从 BGR 裁剪图中估计空速表指针角度。
// 该类会按图像尺寸缓存预计算射线,从而降低连续帧处理开销。
class
AirspeedReader
{
class
AirspeedReader
{
public
:
public
:
// 射线采样和有效性判定所使用的运行时配置。
struct
Config
{
struct
Config
{
// Config 由固定大小的标量和值类型组成,通常直接内联存放在
// AirspeedReader 对象中,不需要单独分配堆内存。
// 在参考裁剪图上测量的仪表盘几何参数,运行时会按输入尺寸缩放。
ReaderGeometry
geometry
;
ReaderGeometry
geometry
;
// 图像坐标系中的角度搜索范围和搜索分辨率。
float
startAngleDeg
=
0.0
f
;
float
startAngleDeg
=
0.0
f
;
float
endAngleDeg
=
360.0
f
;
float
endAngleDeg
=
360.0
f
;
float
angleStepDeg
=
0.5
f
;
float
angleStepDeg
=
0.5
f
;
// 射线采样点从 innerRadius 沿半径方向延伸到 radius。
int
radialSamples
=
128
;
int
radialSamples
=
128
;
// 横向采样使每条射线能够容忍指针宽度。
int
lateralSamples
=
5
;
int
lateralSamples
=
5
;
float
rayWidthPx
=
14.0
f
;
float
rayWidthPx
=
14.0
f
;
// 单个采样点被视为指针证据时所需的最低信号阈值。
float
signalThreshold
=
0.42
f
;
float
signalThreshold
=
0.42
f
;
// 拒绝过短或过于稀疏、无法构成真实指针的检测结果。
float
minContinuousFraction
=
0.30
f
;
float
minContinuousFraction
=
0.30
f
;
float
minCoverageFraction
=
0.18
f
;
float
minCoverageFraction
=
0.18
f
;
// 为候选不明确情况保留的诊断阈值。
float
minDominanceGap
=
0.004
f
;
float
minDominanceGap
=
0.004
f
;
// 允许文字、刻度等造成少量间断,而不立即切断整条指针线。
int
maxGapSamples
=
3
;
int
maxGapSamples
=
3
;
// 与无指针底图做差,用于抑制静态刻度和文字。
bool
useBaseDifference
=
true
;
bool
useBaseDifference
=
true
;
};
};
AirspeedReader
();
AirspeedReader
();
explicit
AirspeedReader
(
Config
config
);
explicit
AirspeedReader
(
Config
config
);
// 可选的同一仪表无移动指针底图。
void
setNoPointerBase
(
const
cv
::
Mat
&
bgrBase
);
void
setNoPointerBase
(
const
cv
::
Mat
&
bgrBase
);
const
Config
&
config
()
const
{
return
config_
;
}
const
Config
&
config
()
const
{
return
config_
;
}
// 单帧处理热路径;通过 ERR_* 状态码返回错误,而不是抛出异常。
AngleReading
read
(
const
cv
::
Mat
&
bgr
);
AngleReading
read
(
const
cv
::
Mat
&
bgr
);
// 绘制选中的角度和状态文字,用于调试输出图。
cv
::
Mat
drawOverlay
(
const
cv
::
Mat
&
bgr
,
const
AngleReading
&
reading
)
const
;
cv
::
Mat
drawOverlay
(
const
cv
::
Mat
&
bgr
,
const
AngleReading
&
reading
)
const
;
private
:
private
:
// 单个候选角度所对应的预计算像素坐标。
struct
RaySamples
{
struct
RaySamples
{
float
angleDeg
=
0.0
f
;
float
angleDeg
=
0.0
f
;
// 在堆上拥有预计算像素坐标。该数据会被缓存,避免每帧重复分配
// 内存并重新计算射线几何。
// 单条射线的近似数据量:
// 数据量公式:radialSamples * lateralSamples * sizeof(cv::Point)。
std
::
vector
<
cv
::
Point
>
points
;
std
::
vector
<
cv
::
Point
>
points
;
};
};
// 按图像尺寸划分的缓存,使射线生成工作不进入逐帧循环。
struct
RayCacheEntry
{
struct
RayCacheEntry
{
// 缩放到特定图像尺寸后的 Config 副本。
Config
scaledConfig
;
Config
scaledConfig
;
// 保存该尺寸全部采样射线的堆内存。
// 默认配置约为:721 个角度 * 128 个径向采样 * 5 个横向采样
// * 每个 cv::Point 8 字节,约占 3.5 MiB,另加 vector/容器开销。
std
::
vector
<
RaySamples
>
rays
;
std
::
vector
<
RaySamples
>
rays
;
// 与缓存射线一起复用的小型堆向量。
std
::
vector
<
float
>
lateralWeights
;
std
::
vector
<
float
>
lateralWeights
;
};
};
// 将参考几何参数缩放到当前图像尺寸。
Config
scaledConfigForImage
(
cv
::
Size
imageSize
)
const
;
Config
scaledConfigForImage
(
cv
::
Size
imageSize
)
const
;
// 延迟创建或复用当前帧尺寸对应的射线缓存。
RayCacheEntry
&
cacheForSize
(
cv
::
Size
imageSize
);
RayCacheEntry
&
cacheForSize
(
cv
::
Size
imageSize
);
// 类高斯横向权重,使射线中心线拥有更高权重。
static
std
::
vector
<
float
>
makeLateralWeights
(
int
count
);
static
std
::
vector
<
float
>
makeLateralWeights
(
int
count
);
// 将角度、半径和横向偏移转换为限制在图像范围内的像素采样点。
static
std
::
vector
<
RaySamples
>
precomputeRays
(
const
Config
&
cfg
,
cv
::
Size
imageSize
);
static
std
::
vector
<
RaySamples
>
precomputeRays
(
const
Config
&
cfg
,
cv
::
Size
imageSize
);
// 构建浮点信号图,使高亮、低饱和度的移动指针像素获得较高分数。
static
cv
::
Mat
makeWhitePointerSignal
(
const
cv
::
Mat
&
bgr
,
const
cv
::
Mat
*
noPointerBase
,
const
Config
&
cfg
);
static
cv
::
Mat
makeWhitePointerSignal
(
const
cv
::
Mat
&
bgr
,
const
cv
::
Mat
*
noPointerBase
,
const
Config
&
cfg
);
// 在允许少量间断的情况下,测量最长的径向连续支持区间。
static
float
longestRunWithGaps
(
const
std
::
vector
<
float
>&
radial
,
float
threshold
,
int
maxGapSamples
,
int
*
coveredOut
);
static
float
longestRunWithGaps
(
const
std
::
vector
<
float
>&
radial
,
float
threshold
,
int
maxGapSamples
,
int
*
coveredOut
);
// 使用预计算射线执行核心评分循环。
AngleReading
readWithCache
(
const
cv
::
Mat
&
bgr
,
const
RayCacheEntry
&
cache
)
const
;
AngleReading
readWithCache
(
const
cv
::
Mat
&
bgr
,
const
RayCacheEntry
&
cache
)
const
;
Config
config_
;
Config
config_
;
// 静态仪表盘底图的深拷贝,用于指针差分评分。
// cv::Mat 使用引用计数;此处 clone 后,本对象拥有独立像素缓冲区,
// 不再与调用方内存共享。
cv
::
Mat
noPointerBaseBgr_
;
cv
::
Mat
noPointerBaseBgr_
;
// 以“宽x高”为键的小型缓存;典型管线通常只使用一种尺寸。
// 缓存项会在读数器整个生命周期内常驻,以内存换取速度。
// 当前实现不会淘汰旧缓存,因此持续输入大量不同分辨率时,
// 该缓存的内存占用会单调增长。
std
::
map
<
std
::
string
,
RayCacheEntry
>
rayCache_
;
std
::
map
<
std
::
string
,
RayCacheEntry
>
rayCache_
;
};
};
...
...
include/instrument_reader/angle_math.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include <cmath>
#include <cmath>
namespace
instrument_reader
{
namespace
instrument_reader
{
// 角度转换共用常量,避免依赖非标准的 M_PI。
constexpr
double
kPi
=
3.14159265358979323846
;
constexpr
double
kPi
=
3.14159265358979323846
;
// 将角度归一化到 [0, 360),同时正确处理负数输入。
inline
double
normalize360
(
double
deg
)
{
inline
double
normalize360
(
double
deg
)
{
double
out
=
std
::
fmod
(
deg
,
360.0
);
double
out
=
std
::
fmod
(
deg
,
360.0
);
if
(
out
<
0.0
)
out
+=
360.0
;
if
(
out
<
0.0
)
out
+=
360.0
;
...
@@ -13,6 +15,7 @@ inline double normalize360(double deg) {
...
@@ -13,6 +15,7 @@ inline double normalize360(double deg) {
return
out
;
return
out
;
}
}
// 面向 OpenCV 高频热路径的 float 版本。
inline
float
normalize360f
(
float
deg
)
{
inline
float
normalize360f
(
float
deg
)
{
float
out
=
std
::
fmod
(
deg
,
360.0
f
);
float
out
=
std
::
fmod
(
deg
,
360.0
f
);
if
(
out
<
0.0
f
)
out
+=
360.0
f
;
if
(
out
<
0.0
f
)
out
+=
360.0
f
;
...
@@ -20,10 +23,12 @@ inline float normalize360f(float deg) {
...
@@ -20,10 +23,12 @@ inline float normalize360f(float deg) {
return
out
;
return
out
;
}
}
// 将图像坐标系中的绝对角度转换为相对 LUT 起点的扫描角度。
inline
double
relativeFromStart
(
double
thetaAbsDeg
,
double
startAngleDeg
)
{
inline
double
relativeFromStart
(
double
thetaAbsDeg
,
double
startAngleDeg
)
{
return
normalize360
(
thetaAbsDeg
-
startAngleDeg
);
return
normalize360
(
thetaAbsDeg
-
startAngleDeg
);
}
}
// 将角度转换为 std::sin/cos 使用的弧度。
inline
float
degreesToRadians
(
float
deg
)
{
inline
float
degreesToRadians
(
float
deg
)
{
return
deg
*
static_cast
<
float
>
(
kPi
/
180.0
);
return
deg
*
static_cast
<
float
>
(
kPi
/
180.0
);
}
}
...
...
include/instrument_reader/dispatcher.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include "instrument_reader/airspeed_reader.hpp"
#include "instrument_reader/airspeed_reader.hpp"
#include "instrument_reader/instrument_classifier.hpp"
#include "instrument_reader/instrument_classifier.hpp"
...
@@ -10,40 +10,68 @@
...
@@ -10,40 +10,68 @@
namespace
instrument_reader
{
namespace
instrument_reader
{
// 负责文件扫描、仪表分类、读数、叠加图绘制和 JSON 输出的总调度器。
class
InstrumentDispatcher
{
class
InstrumentDispatcher
{
public
:
public
:
// CLI 工具、演示程序和未来 ROS 2 集成共同使用的运行时选项。
struct
Options
{
struct
Options
{
// 输出根目录。每次运行通常会在其下创建 output/outputN。
std
::
filesystem
::
path
outputDir
=
"output"
;
std
::
filesystem
::
path
outputDir
=
"output"
;
// 可选的空速表资源路径;为空时由 CLI/演示程序选择默认值。
std
::
filesystem
::
path
airspeedTemplatePath
;
std
::
filesystem
::
path
airspeedTemplatePath
;
std
::
filesystem
::
path
airspeedBasePath
;
std
::
filesystem
::
path
airspeedBasePath
;
std
::
filesystem
::
path
airspeedLutPath
;
std
::
filesystem
::
path
airspeedLutPath
;
// 将图像路由到空速表读数器时使用的视觉分类阈值。
double
airspeedVisualThreshold
=
0.40
;
double
airspeedVisualThreshold
=
0.40
;
// 输入扫描和输出行为设置。
bool
recursive
=
true
;
bool
recursive
=
true
;
bool
writeOverlays
=
true
;
bool
writeOverlays
=
true
;
bool
writeJson
=
true
;
bool
writeJson
=
true
;
// 为 true 时,每次运行创建下一个编号的 output/outputN 目录。
bool
createIncrementalOutputDir
=
true
;
bool
createIncrementalOutputDir
=
true
;
};
};
InstrumentDispatcher
();
InstrumentDispatcher
();
explicit
InstrumentDispatcher
(
Options
options
);
explicit
InstrumentDispatcher
(
Options
options
);
// 处理全部输入文件或目录,并返回批处理汇总结果。
DispatchSummary
run
(
const
std
::
vector
<
std
::
filesystem
::
path
>&
inputs
);
DispatchSummary
run
(
const
std
::
vector
<
std
::
filesystem
::
path
>&
inputs
);
private
:
private
:
// 将文件和目录展开为稳定排序、去重后的图像列表。
std
::
vector
<
std
::
filesystem
::
path
>
collectImages
(
const
std
::
vector
<
std
::
filesystem
::
path
>&
inputs
)
const
;
std
::
vector
<
std
::
filesystem
::
path
>
collectImages
(
const
std
::
vector
<
std
::
filesystem
::
path
>&
inputs
)
const
;
// 处理单张图像,并按配置选择是否写出叠加图。
ProcessResult
processOne
(
const
std
::
filesystem
::
path
&
path
,
int
index
);
ProcessResult
processOne
(
const
std
::
filesystem
::
path
&
path
,
int
index
);
// 输出机器可读的 dispatch_results.json。
void
writeResultsJson
(
const
DispatchSummary
&
summary
)
const
;
void
writeResultsJson
(
const
DispatchSummary
&
summary
)
const
;
// 目录遍历时使用的文件过滤器。
static
bool
isImageFile
(
const
std
::
filesystem
::
path
&
path
);
static
bool
isImageFile
(
const
std
::
filesystem
::
path
&
path
);
static
bool
isHelperImage
(
const
std
::
filesystem
::
path
&
path
);
static
bool
isHelperImage
(
const
std
::
filesystem
::
path
&
path
);
// 创建 output/outputN,其中 N 比现有最大运行编号大 1。
static
std
::
filesystem
::
path
nextIncrementalOutputDir
(
const
std
::
filesystem
::
path
&
outputRoot
);
static
std
::
filesystem
::
path
nextIncrementalOutputDir
(
const
std
::
filesystem
::
path
&
outputRoot
);
// 生成稳定且不含不安全字符的叠加图文件名。
static
std
::
string
safeOutputName
(
const
std
::
filesystem
::
path
&
path
,
int
index
);
static
std
::
string
safeOutputName
(
const
std
::
filesystem
::
path
&
path
,
int
index
);
Options
options_
;
Options
options_
;
// 完成递增编号解析后,本次运行实际使用的输出目录。
std
::
filesystem
::
path
activeOutputDir_
;
std
::
filesystem
::
path
activeOutputDir_
;
// 核心处理管线组件。
InstrumentClassifier
classifier_
;
InstrumentClassifier
classifier_
;
AirspeedReader
airspeedReader_
;
AirspeedReader
airspeedReader_
;
LutMapper
airspeedLut_
;
LutMapper
airspeedLut_
;
// 为 false 时,即使角度有效,也应报告 ERR_NO_AIRSPEED_LUT。
bool
hasAirspeedLut_
=
false
;
bool
hasAirspeedLut_
=
false
;
};
};
...
...
include/instrument_reader/instrument_classifier.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include "instrument_reader/types.hpp"
#include "instrument_reader/types.hpp"
...
@@ -8,29 +8,51 @@
...
@@ -8,29 +8,51 @@
namespace
instrument_reader
{
namespace
instrument_reader
{
// 轻量视觉路由器,用于判断输入裁剪图是否为已配置的空速表。
class
InstrumentClassifier
{
class
InstrumentClassifier
{
public
:
public
:
// 分类配置刻意保持轻量,适合边缘设备部署。
struct
Config
{
struct
Config
{
// 几何参数定义比较表盘区域时使用的掩码。
ReaderGeometry
airspeedGeometry
;
ReaderGeometry
airspeedGeometry
;
// 视觉匹配所需的最低掩码 NCC 分数。
double
airspeedVisualThreshold
=
0.40
;
double
airspeedVisualThreshold
=
0.40
;
// 当路径强烈提示图像为空速表时,允许使用更低的视觉分数阈值。
double
pathHintThresholdFactor
=
0.50
;
double
pathHintThresholdFactor
=
0.50
;
};
};
InstrumentClassifier
();
InstrumentClassifier
();
explicit
InstrumentClassifier
(
Config
config
);
explicit
InstrumentClassifier
(
Config
config
);
// 从磁盘加载模板,并预计算模板边缘图。
bool
loadAirspeedTemplate
(
const
std
::
filesystem
::
path
&
templatePath
);
bool
loadAirspeedTemplate
(
const
std
::
filesystem
::
path
&
templatePath
);
// 直接设置模板,便于测试或相机标定管线使用。
void
setAirspeedTemplate
(
const
cv
::
Mat
&
bgrTemplate
);
void
setAirspeedTemplate
(
const
cv
::
Mat
&
bgrTemplate
);
// 使用模板 NCC 分数与路径提示,对单张裁剪图进行分类。
ClassificationResult
classify
(
const
std
::
filesystem
::
path
&
path
,
const
cv
::
Mat
&
bgr
)
const
;
ClassificationResult
classify
(
const
std
::
filesystem
::
path
&
path
,
const
cv
::
Mat
&
bgr
)
const
;
private
:
private
:
// 对图像执行缩放、灰度化、模糊和边缘检测,以获得稳定的比较结果。
static
cv
::
Mat
prepareEdgeForClassification
(
const
cv
::
Mat
&
bgr
,
cv
::
Size
targetSize
);
static
cv
::
Mat
prepareEdgeForClassification
(
const
cv
::
Mat
&
bgr
,
cv
::
Size
targetSize
);
// 为圆形表盘建立掩码,忽略背景和图像四角。
static
cv
::
Mat
makeClassificationMask
(
cv
::
Size
size
,
const
ReaderGeometry
&
geometry
);
static
cv
::
Mat
makeClassificationMask
(
cv
::
Size
size
,
const
ReaderGeometry
&
geometry
);
// 仅在 mask8 覆盖区域内计算归一化互相关。
static
double
maskedNcc
(
const
cv
::
Mat
&
a32
,
const
cv
::
Mat
&
b32
,
const
cv
::
Mat
&
mask8
);
static
double
maskedNcc
(
const
cv
::
Mat
&
a32
,
const
cv
::
Mat
&
b32
,
const
cv
::
Mat
&
mask8
);
Config
config_
;
Config
config_
;
// templateEdge32_ 和 mask8_ 初始化完成后为 true。
bool
ready_
=
false
;
bool
ready_
=
false
;
cv
::
Size
targetSize_
;
cv
::
Size
targetSize_
;
// cv::Mat 头部直接存放在本对象中;调用 setAirspeedTemplate() 后,
// 像素数据位于引用计数管理的堆内存中,并由这些成员持有。
cv
::
Mat
templateEdge32_
;
cv
::
Mat
templateEdge32_
;
cv
::
Mat
mask8_
;
cv
::
Mat
mask8_
;
};
};
...
...
include/instrument_reader/lut_mapper.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include <filesystem>
#include <filesystem>
#include <string>
#include <string>
...
@@ -6,52 +6,76 @@
...
@@ -6,52 +6,76 @@
namespace
instrument_reader
{
namespace
instrument_reader
{
// 指针角度落在已标定扫描范围之外时的边界处理策略。
enum
class
BoundaryPolicy
{
enum
class
BoundaryPolicy
{
// 返回无效结果,防止下游控制逻辑使用虚假的物理读数。
Reject
,
Reject
,
// 将结果饱和到最近的标定端点。
Clamp
,
Clamp
,
};
};
// 单个标定节点,同时保存绝对角度和相对起点角度。
struct
CalibrationPoint
{
struct
CalibrationPoint
{
double
value
=
0.0
;
double
value
=
0.0
;
double
thetaAbsDeg
=
0.0
;
double
thetaAbsDeg
=
0.0
;
double
thetaRelDeg
=
0.0
;
double
thetaRelDeg
=
0.0
;
};
};
// 将指针角度转换为仪表物理读数后的结果。
struct
MapResult
{
struct
MapResult
{
bool
valid
=
false
;
bool
valid
=
false
;
double
value
=
0.0
;
double
value
=
0.0
;
// 回传本次查询角度的绝对形式和 LUT 相对形式。
double
thetaAbsDeg
=
0.0
;
double
thetaAbsDeg
=
0.0
;
double
thetaRelDeg
=
0.0
;
double
thetaRelDeg
=
0.0
;
// 机器可读状态码,以及可选的人类可读详情。
std
::
string
code
;
std
::
string
code
;
std
::
string
message
;
std
::
string
message
;
// 本次插值选中的分段左右端点。
CalibrationPoint
left
;
CalibrationPoint
left
;
CalibrationPoint
right
;
CalibrationPoint
right
;
};
};
// 非线性仪表刻度使用的分段线性查找表。
class
LutMapper
{
class
LutMapper
{
public
:
public
:
// startAngleDeg 定义 theta_rel=0,使标定表能够安全跨越 360 度零点。
explicit
LutMapper
(
double
startAngleDeg
=
270.0
);
explicit
LutMapper
(
double
startAngleDeg
=
270.0
);
// 改变角度原点后,重新计算全部相对角度。
void
setStartAngle
(
double
startAngleDeg
);
void
setStartAngle
(
double
startAngleDeg
);
void
setBoundaryPolicy
(
BoundaryPolicy
policy
)
{
boundaryPolicy_
=
policy
;
}
void
setBoundaryPolicy
(
BoundaryPolicy
policy
)
{
boundaryPolicy_
=
policy
;
}
// 供标定工具修改 LUT 使用的辅助接口。
void
clear
();
void
clear
();
void
addPoint
(
double
value
,
double
thetaAbsDeg
);
void
addPoint
(
double
value
,
double
thetaAbsDeg
);
// 验证相对角度和物理值均保持单调。
bool
validate
(
std
::
string
*
errorMessage
=
nullptr
)
const
;
bool
validate
(
std
::
string
*
errorMessage
=
nullptr
)
const
;
// 部署配置使用的轻量 JSON 读写,避免引入大型 JSON 依赖。
bool
saveJson
(
const
std
::
filesystem
::
path
&
path
)
const
;
bool
saveJson
(
const
std
::
filesystem
::
path
&
path
)
const
;
bool
loadJson
(
const
std
::
filesystem
::
path
&
path
,
std
::
string
*
errorMessage
=
nullptr
);
bool
loadJson
(
const
std
::
filesystem
::
path
&
path
,
std
::
string
*
errorMessage
=
nullptr
);
// 高频热路径:归一化角度、二分查找分段、执行插值。
MapResult
map
(
double
thetaAbsDeg
)
const
;
MapResult
map
(
double
thetaAbsDeg
)
const
;
double
startAngleDeg
()
const
{
return
startAngleDeg_
;
}
double
startAngleDeg
()
const
{
return
startAngleDeg_
;
}
const
std
::
vector
<
CalibrationPoint
>&
points
()
const
{
return
points_
;
}
const
std
::
vector
<
CalibrationPoint
>&
points
()
const
{
return
points_
;
}
private
:
private
:
// 保持节点按 thetaRelDeg 排序,以供 lower_bound 二分查找。
void
sortByRelativeAngle
();
void
sortByRelativeAngle
();
double
startAngleDeg_
=
270.0
;
double
startAngleDeg_
=
270.0
;
BoundaryPolicy
boundaryPolicy_
=
BoundaryPolicy
::
Reject
;
BoundaryPolicy
boundaryPolicy_
=
BoundaryPolicy
::
Reject
;
// 本对象拥有完整标定表的堆内存。标定表很小,只排序一次;
// 随后的高频 map() 调用只读访问它。
std
::
vector
<
CalibrationPoint
>
points_
;
std
::
vector
<
CalibrationPoint
>
points_
;
};
};
...
...
include/instrument_reader/non_linear_gauge_reader.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include <cstddef>
#include <cstddef>
#include <vector>
#include <vector>
namespace
instrument_reader
::
nonlinear
{
namespace
instrument_reader
::
nonlinear
{
// 通用非线性仪表使用的最小标定节点。
struct
CalibrationPoint
{
struct
CalibrationPoint
{
// 调用方所选连续角度坐标系中的角度。
float
angle
=
0.0
f
;
float
angle
=
0.0
f
;
// 该角度对应的物理读数。
float
value
=
0.0
f
;
float
value
=
0.0
f
;
};
};
// 通用插值器处理超出标定范围角度时的行为。
enum
class
BoundaryMode
{
enum
class
BoundaryMode
{
//
Saturate to the nearest calibrated endpoint. This is the safest default
//
饱和到最近的标定端点。当检测器短暂偏离表盘时,
//
for real-time control loops when the detector briefly leaves the dial.
//
这是实时控制循环中最安全的默认策略。
Clamp
,
Clamp
,
//
Extend the first or last calibrated segment beyond the table range.
//
使用第一段或最后一段的斜率,将读数外推到标定表范围之外。
Extrapolate
,
Extrapolate
,
};
};
// 无第三方依赖、适用于高频循环的分段线性插值器。
class
NonLinearGaugeReader
{
class
NonLinearGaugeReader
{
public
:
public
:
//
The input table is copied, sorted by angle once, and validated for
//
输入表按值传入并由本对象接管,构造时只排序一次,
//
finite values plus strictly increasing angles.
//
同时验证所有数值有限且角度严格递增。
explicit
NonLinearGaugeReader
(
std
::
vector
<
CalibrationPoint
>
calibration
,
BoundaryMode
boundaryMode
=
BoundaryMode
::
Clamp
);
explicit
NonLinearGaugeReader
(
std
::
vector
<
CalibrationPoint
>
calibration
,
BoundaryMode
boundaryMode
=
BoundaryMode
::
Clamp
);
//
Hot path: no allocation, no exception, O(log N) segment lookup.
//
高频热路径:不分配内存、不抛出异常,使用 O(log N) 查找目标分段。
//
Returns NaN when the upstream angle itself is NaN or infinity.
//
上游角度本身为 NaN 或无穷大时返回 NaN。
[[
nodiscard
]]
float
calculate_value
(
float
current_angle
)
const
noexcept
;
[[
nodiscard
]]
float
calculate_value
(
float
current_angle
)
const
noexcept
;
[[
nodiscard
]]
const
std
::
vector
<
CalibrationPoint
>&
calibration
()
const
noexcept
{
return
calibration_
;
}
[[
nodiscard
]]
const
std
::
vector
<
CalibrationPoint
>&
calibration
()
const
noexcept
{
return
calibration_
;
}
[[
nodiscard
]]
BoundaryMode
boundaryMode
()
const
noexcept
{
return
boundaryMode_
;
}
[[
nodiscard
]]
BoundaryMode
boundaryMode
()
const
noexcept
{
return
boundaryMode_
;
}
private
:
private
:
// 使用预计算的分段斜率,避免 calculate_value() 在热路径中执行除法。
[[
nodiscard
]]
float
interpolateSegment
(
std
::
size_t
leftIndex
,
float
angle
)
const
noexcept
;
[[
nodiscard
]]
float
interpolateSegment
(
std
::
size_t
leftIndex
,
float
angle
)
const
noexcept
;
// 已按角度排序的标定表。
// 构造完成后,读数器拥有该堆向量,调用方原始输入可以安全销毁。
std
::
vector
<
CalibrationPoint
>
calibration_
;
std
::
vector
<
CalibrationPoint
>
calibration_
;
// 每个分段保存一个斜率:
// 斜率公式:(right.value - left.value) / (right.angle - left.angle)。
// 这个额外的堆向量以少量内存换取速度,使高频 calculate_value() 路径不再执行除法。
std
::
vector
<
float
>
slopes_
;
std
::
vector
<
float
>
slopes_
;
BoundaryMode
boundaryMode_
=
BoundaryMode
::
Clamp
;
BoundaryMode
boundaryMode_
=
BoundaryMode
::
Clamp
;
};
};
...
...
include/instrument_reader/types.hpp
View file @
95f55843
#pragma once
#
pragma
once
#include <opencv2/core.hpp>
#include <opencv2/core.hpp>
...
@@ -8,12 +8,14 @@
...
@@ -8,12 +8,14 @@
namespace
instrument_reader
{
namespace
instrument_reader
{
// 高层分发类型,用于将输入仪表裁剪图路由到对应的读数器。
enum
class
InstrumentType
{
enum
class
InstrumentType
{
Unknown
,
Unknown
,
OtherInstrument
,
OtherInstrument
,
AirspeedIndicator
,
AirspeedIndicator
,
};
};
// 稳定的字符串形式,用于 JSON 报告和控制台诊断信息。
inline
const
char
*
toString
(
InstrumentType
type
)
{
inline
const
char
*
toString
(
InstrumentType
type
)
{
switch
(
type
)
{
switch
(
type
)
{
case
InstrumentType
:
:
AirspeedIndicator
:
return
"airspeed_indicator"
;
case
InstrumentType
:
:
AirspeedIndicator
:
return
"airspeed_indicator"
;
...
@@ -22,63 +24,131 @@ inline const char* toString(InstrumentType type) {
...
@@ -22,63 +24,131 @@ inline const char* toString(InstrumentType type) {
}
}
}
}
// 仪表盘共享几何参数。角度采用图像坐标系:
// 0 度向右、90 度向下、180 度向左、270 度向上。
struct
ReaderGeometry
{
struct
ReaderGeometry
{
// Image coordinates: 0 deg points right, 90 deg points down.
// 此结构体只包含固定大小的数值和 OpenCV 值类型。
// 每个实例自身不会进行堆内存分配,跟随 Config 复制时开销很小。
// 参考裁剪图中的仪表盘中心点。
cv
::
Point2f
center
=
cv
::
Point2f
(
198.0
f
,
248.0
f
);
cv
::
Point2f
center
=
cv
::
Point2f
(
198.0
f
,
248.0
f
);
// 测量几何参数时使用的参考图像尺寸。
// 运行时会先按输入裁剪图尺寸进行等比例缩放,再执行射线采样。
cv
::
Size2f
referenceSize
=
cv
::
Size2f
(
400.0
f
,
445.0
f
);
cv
::
Size2f
referenceSize
=
cv
::
Size2f
(
400.0
f
,
445.0
f
);
// 搜索指针和刻度线时使用的外部采样半径。
float
radius
=
210.0
f
;
float
radius
=
210.0
f
;
// 内部半径用于避开中心轴、螺钉和文字等干扰。
float
innerRadius
=
18.0
f
;
float
innerRadius
=
18.0
f
;
};
};
// 指针角度检测结果,尚未将角度转换成物理读数。
struct
AngleReading
{
struct
AngleReading
{
// 大多数数值字段直接内联存放在对象本体中。
// 两个 std::string 在内容超过标准库的小字符串优化容量时,才会在堆上分配内存。
// 只有检测到足够连续的指针证据时才为 true。
bool
valid
=
false
;
bool
valid
=
false
;
// 字符串内部可能拥有堆内存,但正常成功读数时通常为空或很短,
// 不会成为每帧内存占用的主要部分。
// 可供下游控制逻辑和回归测试读取的机器状态码。
std
::
string
errorCode
=
"ERR_UNSET"
;
std
::
string
errorCode
=
"ERR_UNSET"
;
// 面向人的错误详情;与状态码分开,便于控制逻辑只判断稳定状态码。
std
::
string
errorMessage
;
std
::
string
errorMessage
;
// 归一化到 [0, 360) 的图像坐标系指针角度。
float
angleDegMod360
=
0.0
f
;
float
angleDegMod360
=
0.0
f
;
// 最优和次优射线分数,用于诊断指针候选不明确的情况。
float
score
=
0.0
f
;
float
score
=
0.0
f
;
float
secondAngleDegMod360
=
0.0
f
;
float
secondAngleDegMod360
=
0.0
f
;
float
secondScore
=
0.0
f
;
float
secondScore
=
0.0
f
;
float
confidenceGap
=
0.0
f
;
float
confidenceGap
=
0.0
f
;
// 连续性指标用于排除不是真实指针的短高亮线段。
float
continuousLineLengthPx
=
0.0
f
;
float
continuousLineLengthPx
=
0.0
f
;
float
continuousLineFraction
=
0.0
f
;
float
continuousLineFraction
=
0.0
f
;
float
coverageFraction
=
0.0
f
;
float
coverageFraction
=
0.0
f
;
};
};
// 轻量分类结果,在选择具体仪表读数器之前使用。
struct
ClassificationResult
{
struct
ClassificationResult
{
// 只包含标量数据;复制它不会复制图像缓冲区,也不会分配大块内存。
InstrumentType
type
=
InstrumentType
::
Unknown
;
InstrumentType
type
=
InstrumentType
::
Unknown
;
// 当前已完整实现的空速表读数器所使用的便捷布尔值。
bool
isAirspeed
=
false
;
bool
isAirspeed
=
false
;
// 文件名或路径提示当前图像为空速表时为 true。
bool
pathHint
=
false
;
bool
pathHint
=
false
;
// 在仪表盘掩码区域内计算得到的模板匹配分数。
double
visualScore
=
0.0
;
double
visualScore
=
0.0
;
// 用于报告的综合置信度,不是经过概率校准的真实概率。
double
confidence
=
0.0
;
double
confidence
=
0.0
;
};
};
// 每张图像的处理结果记录,最终写入 dispatch_results.json。
struct
ProcessResult
{
struct
ProcessResult
{
// 此记录特意只保存元数据。可变大小内存仅来自路径和字符串文本;
// 图像像素永远不会成为该对象的成员。
// filesystem::path 只保存路径文本,不会延长图像像素缓冲区的生命周期。
std
::
filesystem
::
path
inputPath
;
std
::
filesystem
::
path
inputPath
;
std
::
filesystem
::
path
outputPath
;
std
::
filesystem
::
path
outputPath
;
// 即使物理值映射失败,也会保留分类结果和原始角度。
ClassificationResult
classification
;
ClassificationResult
classification
;
AngleReading
angle
;
AngleReading
angle
;
// LUT 映射后的物理空速读数;false 表示读数为空或被拒绝。
bool
airspeedValueValid
=
false
;
bool
airspeedValueValid
=
false
;
double
airspeedValue
=
0.0
;
double
airspeedValue
=
0.0
;
// 相对角度以 LUT 起始角度为零点,可正确处理跨越 360 度的情况。
bool
airspeedThetaRelValid
=
false
;
bool
airspeedThetaRelValid
=
false
;
double
airspeedThetaRelDeg
=
0.0
;
double
airspeedThetaRelDeg
=
0.0
;
// 分段字段用于展示 lower_bound 选择了哪一段非线性区间。
bool
airspeedSegmentValid
=
false
;
bool
airspeedSegmentValid
=
false
;
double
airspeedSegmentStartValue
=
0.0
;
double
airspeedSegmentStartValue
=
0.0
;
double
airspeedSegmentEndValue
=
0.0
;
double
airspeedSegmentEndValue
=
0.0
;
double
airspeedSegmentStartThetaRelDeg
=
0.0
;
double
airspeedSegmentStartThetaRelDeg
=
0.0
;
double
airspeedSegmentEndThetaRelDeg
=
0.0
;
double
airspeedSegmentEndThetaRelDeg
=
0.0
;
// 映射器状态,例如 OK_INTERPOLATED、ERR_ABOVE_MAX 或 ERR_BAD_LUT。
std
::
string
airspeedValueCode
;
std
::
string
airspeedValueCode
;
std
::
string
airspeedValueMessage
;
std
::
string
airspeedValueMessage
;
// 调度器内部测量的单张图像墙钟耗时。
double
elapsedMs
=
0.0
;
double
elapsedMs
=
0.0
;
};
};
// 调度器返回并由 CLI 输出的批处理汇总结果。
struct
DispatchSummary
{
struct
DispatchSummary
{
std
::
filesystem
::
path
outputDir
;
std
::
filesystem
::
path
outputDir
;
// results 为每张输入图像保存一个 ProcessResult。
// 此处不保存像素缓冲区;图像在 processOne() 内加载、处理、写出并释放。
// 因此批处理内存主要随元数据数量增长,而不是随解码后的图像总数增长。
std
::
vector
<
ProcessResult
>
results
;
std
::
vector
<
ProcessResult
>
results
;
// 调度器处理循环耗时,不包含进程启动时间。
double
totalElapsedMs
=
0.0
;
double
totalElapsedMs
=
0.0
;
// 统计被分类为空速表的图像数量,不要求物理值映射成功。
int
airspeedCount
()
const
;
int
airspeedCount
()
const
;
// 统计具有有效物理读数的空速表图像,而不仅仅是角度有效。
int
validAirspeedCount
()
const
;
int
validAirspeedCount
()
const
;
// 统计未被路由到空速表读数器的图像数量。
int
skippedCount
()
const
;
int
skippedCount
()
const
;
};
};
...
...
scripts/build_msvc.ps1
View file @
95f55843
param
(
param
(
# 与 MSVC 兼容的 OpenCV 包根目录。
[
string
]
$OpenCvDir
=
"C:\opencv\build"
,
[
string
]
$OpenCvDir
=
"C:\opencv\build"
,
# CMake 构建目录;如果不是绝对路径,则相对于项目根目录。
[
string
]
$BuildDir
=
"build"
,
[
string
]
$BuildDir
=
"build"
,
# NMake 单配置构建类型。
[
string
]
$Configuration
=
"Release"
[
string
]
$Configuration
=
"Release"
)
)
$ErrorActionPreference
=
"Stop"
$ErrorActionPreference
=
"Stop"
# CMake 负责驱动配置和构建两个步骤。
if
(
-not
(
Get-Command cmake.exe -ErrorAction SilentlyContinue
))
{
if
(
-not
(
Get-Command cmake.exe -ErrorAction SilentlyContinue
))
{
throw
"cmake.exe not found"
throw
"cmake.exe not found"
}
}
$ProjectRoot
=
Resolve-Path
(
Join-Path
$PSScriptRoot
".."
)
$ProjectRoot
=
Resolve-Path
(
Join-Path
$PSScriptRoot
".."
)
# OpenCV_DIR 指向本次安装中包含 OpenCVConfig.cmake 的目录。
$opencvConfig
=
Join-Path
$OpenCvDir
"x64\vc16\lib"
$opencvConfig
=
Join-Path
$OpenCvDir
"x64\vc16\lib"
if
(
-not
(
Test-Path
$opencvConfig
))
{
if
(
-not
(
Test-Path
$opencvConfig
))
{
throw
"OpenCV MSVC library folder not found:
$opencvConfig
"
throw
"OpenCV MSVC library folder not found:
$opencvConfig
"
}
}
# vswhere 用于可靠定位最新的 Visual Studio Build Tools 安装。
$vswhere
=
Join-Path
${
env
:ProgramFiles
(x86)
}
"Microsoft Visual Studio\Installer\vswhere.exe"
$vswhere
=
Join-Path
${
env
:ProgramFiles
(x86)
}
"Microsoft Visual Studio\Installer\vswhere.exe"
if
(
-not
(
Test-Path
$vswhere
))
{
if
(
-not
(
Test-Path
$vswhere
))
{
throw
"vswhere.exe not found. Install Visual Studio Build Tools 2022."
throw
"vswhere.exe not found. Install Visual Studio Build Tools 2022."
...
@@ -26,18 +35,22 @@ if (-not $vsInstall) {
...
@@ -26,18 +35,22 @@ if (-not $vsInstall) {
throw
"MSVC C++ tools not found. Install workload Microsoft.VisualStudio.Workload.VCTools."
throw
"MSVC C++ tools not found. Install workload Microsoft.VisualStudio.Workload.VCTools."
}
}
# VsDevCmd 为 NMake 设置编译器、链接器、SDK 和 PATH 环境变量。
$vsDevCmd
=
Join-Path
$vsInstall
"Common7\Tools\VsDevCmd.bat"
$vsDevCmd
=
Join-Path
$vsInstall
"Common7\Tools\VsDevCmd.bat"
if
(
-not
(
Test-Path
$vsDevCmd
))
{
if
(
-not
(
Test-Path
$vsDevCmd
))
{
throw
"VsDevCmd.bat not found:
$vsDevCmd
"
throw
"VsDevCmd.bat not found:
$vsDevCmd
"
}
}
# 同时支持绝对构建路径和相对项目根目录的构建路径。
$buildPath
=
if
([
System.IO.Path]::IsPathRooted
(
$BuildDir
))
{
$buildPath
=
if
([
System.IO.Path]::IsPathRooted
(
$BuildDir
))
{
$BuildDir
$BuildDir
}
else
{
}
else
{
Join-Path
$ProjectRoot
$BuildDir
Join-Path
$ProjectRoot
$BuildDir
}
}
# 命令字符串会在 MSVC 开发环境中执行,因此保持其内容明确可见。
$configure
=
"cmake -S
`"
$ProjectRoot
`"
-B
`"
$buildPath
`"
-G
`"
NMake Makefiles
`"
-DOpenCV_DIR=
`"
$opencvConfig
`"
-DCMAKE_BUILD_TYPE=
$Configuration
"
$configure
=
"cmake -S
`"
$ProjectRoot
`"
-B
`"
$buildPath
`"
-G
`"
NMake Makefiles
`"
-DOpenCV_DIR=
`"
$opencvConfig
`"
-DCMAKE_BUILD_TYPE=
$Configuration
"
$build
=
"cmake --build
`"
$buildPath
`"
--config
$Configuration
"
$build
=
"cmake --build
`"
$buildPath
`"
--config
$Configuration
"
# 在同一个 cmd 中执行,确保配置和构建阶段都保留 VsDevCmd 环境。
cmd.exe /c
"
`"
$vsDevCmd
`"
-arch=x64 -host_arch=x64 &&
$configure
&&
$build
"
cmd.exe /c
"
`"
$vsDevCmd
`"
-arch=x64 -host_arch=x64 &&
$configure
&&
$build
"
scripts/build_ubuntu22.sh
View file @
95f55843
#!/usr/bin/env bash
#!/usr/bin/env bash
set
-euo
pipefail
set
-euo
pipefail
# 根据脚本位置解析项目路径,使脚本可从任意当前目录运行。
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
# 允许显式指定构建目录,否则使用文档约定的 Release 目录。
BUILD_DIR
=
"
${
1
:-${
PROJECT_ROOT
}
/cmake-build-ubuntu22-release
}
"
BUILD_DIR
=
"
${
1
:-${
PROJECT_ROOT
}
/cmake-build-ubuntu22-release
}
"
# 使用 CMake/pkg-config 找到的系统 OpenCV 包完成配置。
cmake
-S
"
${
PROJECT_ROOT
}
"
-B
"
${
BUILD_DIR
}
"
\
cmake
-S
"
${
PROJECT_ROOT
}
"
-B
"
${
BUILD_DIR
}
"
\
-DCMAKE_BUILD_TYPE
=
Release
-DCMAKE_BUILD_TYPE
=
Release
# 并行构建,以加快开发迭代。
cmake
--build
"
${
BUILD_DIR
}
"
--parallel
cmake
--build
"
${
BUILD_DIR
}
"
--parallel
# 输出生成的二进制文件路径,便于直接运行。
echo
"Built binaries:"
echo
"Built binaries:"
find
"
${
BUILD_DIR
}
"
-maxdepth
1
-type
f
-executable
-print
find
"
${
BUILD_DIR
}
"
-maxdepth
1
-type
f
-executable
-print
scripts/open_clion.ps1
View file @
95f55843
param
(
param
(
# 要打开的项目目录,默认使用当前仓库根目录。
[
string
]
$ProjectDir
=
(
Resolve-Path
(
Join-Path
$PSScriptRoot
".."
))
.Path
[
string
]
$ProjectDir
=
(
Resolve-Path
(
Join-Path
$PSScriptRoot
".."
))
.Path
)
)
$ErrorActionPreference
=
"Stop"
$ErrorActionPreference
=
"Stop"
# 当前开发机及 Windows 默认位置中常见的 CLion 安装路径。
$candidates
=
@
(
$candidates
=
@
(
"D:\works\Clion\CLion 2025.2.5\bin\clion64.exe"
,
"D:\works\Clion\CLion 2025.2.5\bin\clion64.exe"
,
"
$env
:LOCALAPPDATA\Programs\CLion\bin\clion64.exe"
,
"
$env
:LOCALAPPDATA\Programs\CLion\bin\clion64.exe"
,
...
@@ -11,6 +13,8 @@ $candidates = @(
...
@@ -11,6 +13,8 @@ $candidates = @(
)
)
$clion
=
$null
$clion
=
$null
# 优先使用已知绝对路径,以可靠启动桌面程序。
foreach
(
$candidate
in
$candidates
)
{
foreach
(
$candidate
in
$candidates
)
{
if
(
Test-Path
$candidate
)
{
if
(
Test-Path
$candidate
)
{
$clion
=
$candidate
$clion
=
$candidate
...
@@ -18,6 +22,7 @@ foreach ($candidate in $candidates) {
...
@@ -18,6 +22,7 @@ foreach ($candidate in $candidates) {
}
}
}
}
# 如果 CLion 安装了启动器,则回退到 PATH 查找。
if
(
-not
$clion
)
{
if
(
-not
$clion
)
{
$cmd
=
Get-Command clion64.exe -ErrorAction SilentlyContinue
$cmd
=
Get-Command clion64.exe -ErrorAction SilentlyContinue
if
(
$cmd
)
{
if
(
$cmd
)
{
...
@@ -29,6 +34,7 @@ if (-not $clion) {
...
@@ -29,6 +34,7 @@ if (-not $clion) {
throw
"CLion executable not found."
throw
"CLion executable not found."
}
}
# 如果存在 MSVC 工具,则从 x64 开发环境启动 CLion。
$vswhere
=
Join-Path
${
env
:ProgramFiles
(x86)
}
"Microsoft Visual Studio\Installer\vswhere.exe"
$vswhere
=
Join-Path
${
env
:ProgramFiles
(x86)
}
"Microsoft Visual Studio\Installer\vswhere.exe"
if
(
Test-Path
$vswhere
)
{
if
(
Test-Path
$vswhere
)
{
$vsInstall
=
&
$vswhere
-latest -products
*
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
$vsInstall
=
&
$vswhere
-latest -products
*
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
...
@@ -42,5 +48,6 @@ if (Test-Path $vswhere) {
...
@@ -42,5 +48,6 @@ if (Test-Path $vswhere) {
}
}
}
}
# 回退启动仍会打开项目,但 CLion 需要自行定位工具链。
Start-Process
-FilePath
$clion
-ArgumentList
"
`"
$ProjectDir
`"
"
Start-Process
-FilePath
$clion
-ArgumentList
"
`"
$ProjectDir
`"
"
Write-Host
"Opened CLion project:
$ProjectDir
"
Write-Host
"Opened CLion project:
$ProjectDir
"
scripts/open_clion.sh
View file @
95f55843
#!/usr/bin/env bash
#!/usr/bin/env bash
set
-euo
pipefail
set
-euo
pipefail
# 根据脚本位置解析仓库根目录。
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
# 先尝试标准启动器,再尝试 JetBrains Toolbox。
if
command
-v
clion
>
/dev/null 2>&1
;
then
if
command
-v
clion
>
/dev/null 2>&1
;
then
clion
"
${
PROJECT_ROOT
}
"
>
/dev/null 2>&1 &
clion
"
${
PROJECT_ROOT
}
"
>
/dev/null 2>&1 &
elif
command
-v
jetbrains-toolbox
>
/dev/null 2>&1
;
then
elif
command
-v
jetbrains-toolbox
>
/dev/null 2>&1
;
then
jetbrains-toolbox
"
${
PROJECT_ROOT
}
"
>
/dev/null 2>&1 &
jetbrains-toolbox
"
${
PROJECT_ROOT
}
"
>
/dev/null 2>&1 &
else
else
# 未安装启动器时输出项目路径,供手工打开。
echo
"CLion launcher not found. Open this folder manually:"
>
&2
echo
"CLion launcher not found. Open this folder manually:"
>
&2
echo
"
${
PROJECT_ROOT
}
"
>
&2
echo
"
${
PROJECT_ROOT
}
"
>
&2
exit
1
exit
1
...
...
scripts/run_demo.ps1
View file @
95f55843
param
(
param
(
# scripts/build_msvc.ps1 创建的构建目录。
[
string
]
$BuildDir
=
"build"
,
[
string
]
$BuildDir
=
"build"
,
# 包含 original_crops 和 generated_scales 的数据集根目录。
[
string
]
$DataRoot
=
"..\2"
,
[
string
]
$DataRoot
=
"..\2"
,
# 用于定位运行时 DLL 的 OpenCV 根目录。
[
string
]
$OpenCvDir
=
"C:\opencv\build"
[
string
]
$OpenCvDir
=
"C:\opencv\build"
)
)
$ErrorActionPreference
=
"Stop"
$ErrorActionPreference
=
"Stop"
# 优先使用 Visual Studio 多配置目录布局,其次使用 NMake 单配置布局。
$exe
=
Join-Path
$BuildDir
"Release\instrument_reader_cli.exe"
$exe
=
Join-Path
$BuildDir
"Release\instrument_reader_cli.exe"
if
(
-not
(
Test-Path
$exe
))
{
if
(
-not
(
Test-Path
$exe
))
{
$exe
=
Join-Path
$BuildDir
"instrument_reader_cli.exe"
$exe
=
Join-Path
$BuildDir
"instrument_reader_cli.exe"
...
@@ -14,11 +20,13 @@ if (-not (Test-Path $exe)) {
...
@@ -14,11 +20,13 @@ if (-not (Test-Path $exe)) {
throw
"instrument_reader_cli.exe not found. Build first."
throw
"instrument_reader_cli.exe not found. Build first."
}
}
# 将 OpenCV DLL 所在目录加入子进程的 PATH。
$opencvBin
=
Join-Path
$OpenCvDir
"x64\vc16\bin"
$opencvBin
=
Join-Path
$OpenCvDir
"x64\vc16\bin"
if
(
Test-Path
$opencvBin
)
{
if
(
Test-Path
$opencvBin
)
{
$env
:Path
=
"
$opencvBin
;
$env
:Path"
$env
:Path
=
"
$opencvBin
;
$env
:Path"
}
}
# 处理原始裁剪图和生成的空速表样例,并写入 output/outputN。
&
$exe
`
&
$exe
`
--input
(
Join-Path
$DataRoot
"original_crops"
)
`
--input
(
Join-Path
$DataRoot
"original_crops"
)
`
--input
(
Join-Path
$DataRoot
"generated_scales\01_airspeed_indicator"
)
`
--input
(
Join-Path
$DataRoot
"generated_scales\01_airspeed_indicator"
)
`
...
...
scripts/run_demo_ubuntu22.sh
View file @
95f55843
#!/usr/bin/env bash
#!/usr/bin/env bash
set
-euo
pipefail
set
-euo
pipefail
# 根据脚本位置解析路径,使其可从任意当前目录启动。
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
SCRIPT_DIR
=
"
$(
cd
"
$(
dirname
"
${
BASH_SOURCE
[0]
}
"
)
"
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
PROJECT_ROOT
=
"
$(
cd
"
${
SCRIPT_DIR
}
/.."
&&
pwd
)
"
# 位置参数允许在不修改脚本的情况下覆盖构建目录和数据根目录。
BUILD_DIR
=
"
${
1
:-${
PROJECT_ROOT
}
/cmake-build-ubuntu22-release
}
"
BUILD_DIR
=
"
${
1
:-${
PROJECT_ROOT
}
/cmake-build-ubuntu22-release
}
"
DATA_ROOT
=
"
${
2
:-${
PROJECT_ROOT
}
/../2
}
"
DATA_ROOT
=
"
${
2
:-${
PROJECT_ROOT
}
/../2
}
"
# CLI 会自行在该根目录下创建 output/outputN。
OUT_DIR
=
"
${
PROJECT_ROOT
}
/output"
OUT_DIR
=
"
${
PROJECT_ROOT
}
/output"
EXE
=
"
${
BUILD_DIR
}
/instrument_reader_cli"
EXE
=
"
${
BUILD_DIR
}
/instrument_reader_cli"
# 二进制文件缺失时立即失败,并给出构建命令。
if
[[
!
-x
"
${
EXE
}
"
]]
;
then
if
[[
!
-x
"
${
EXE
}
"
]]
;
then
echo
"instrument_reader_cli not found. Build first:"
>
&2
echo
"instrument_reader_cli not found. Build first:"
>
&2
echo
"
${
SCRIPT_DIR
}
/build_ubuntu22.sh"
>
&2
echo
"
${
SCRIPT_DIR
}
/build_ubuntu22.sh"
>
&2
exit
1
exit
1
fi
fi
# 使用数据集中的空速表无指针底图进行分类和差分评分。
"
${
EXE
}
"
\
"
${
EXE
}
"
\
--input
"
${
DATA_ROOT
}
/original_crops"
\
--input
"
${
DATA_ROOT
}
/original_crops"
\
--input
"
${
DATA_ROOT
}
/generated_scales/01_airspeed_indicator"
\
--input
"
${
DATA_ROOT
}
/generated_scales/01_airspeed_indicator"
\
...
...
scripts/setup_ubuntu22.sh
View file @
95f55843
#!/usr/bin/env bash
#!/usr/bin/env bash
set
-euo
pipefail
set
-euo
pipefail
# 安装最小原生工具链和 OpenCV 开发包。
sudo
apt update
sudo
apt update
sudo
apt
install
-y
\
sudo
apt
install
-y
\
build-essential
\
build-essential
\
...
@@ -10,6 +11,7 @@ sudo apt install -y \
...
@@ -10,6 +11,7 @@ sudo apt install -y \
pkg-config
\
pkg-config
\
libopencv-dev
libopencv-dev
# 输出工具版本,便于从安装日志中发现环境问题。
echo
"Ubuntu 22.04 build dependencies are ready."
echo
"Ubuntu 22.04 build dependencies are ready."
cmake
--version
cmake
--version
pkg-config
--modversion
opencv4
||
true
pkg-config
--modversion
opencv4
||
true
src/airspeed_reader.cpp
View file @
95f55843
This diff is collapsed.
Click to expand it.
src/dispatcher.cpp
View file @
95f55843
This diff is collapsed.
Click to expand it.
src/instrument_classifier.cpp
View file @
95f55843
#include "instrument_reader/instrument_classifier.hpp"
#
include
"instrument_reader/instrument_classifier.hpp"
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/imgproc.hpp>
...
@@ -11,6 +11,7 @@ namespace instrument_reader {
...
@@ -11,6 +11,7 @@ namespace instrument_reader {
namespace
{
namespace
{
// 检查关键词前,先将路径字符串统一转换为小写。
std
::
string
lowerCopy
(
std
::
string
s
)
{
std
::
string
lowerCopy
(
std
::
string
s
)
{
std
::
transform
(
s
.
begin
(),
s
.
end
(),
s
.
begin
(),
[](
unsigned
char
c
)
{
std
::
transform
(
s
.
begin
(),
s
.
end
(),
s
.
begin
(),
[](
unsigned
char
c
)
{
return
static_cast
<
char
>
(
std
::
tolower
(
c
));
return
static_cast
<
char
>
(
std
::
tolower
(
c
));
...
@@ -27,6 +28,9 @@ InstrumentClassifier::InstrumentClassifier(Config config)
...
@@ -27,6 +28,9 @@ InstrumentClassifier::InstrumentClassifier(Config config)
:
config_
(
config
)
{}
:
config_
(
config
)
{}
bool
InstrumentClassifier
::
loadAirspeedTemplate
(
const
std
::
filesystem
::
path
&
templatePath
)
{
bool
InstrumentClassifier
::
loadAirspeedTemplate
(
const
std
::
filesystem
::
path
&
templatePath
)
{
// 模板加载是可选步骤;加载失败时,调用方仍可回退到路径提示。
// imread 会为解码后的模板分配临时像素缓冲区;setAirspeedTemplate()
// 提取紧凑的边缘图和掩码状态,并让它们在本函数返回后继续常驻。
cv
::
Mat
img
=
cv
::
imread
(
templatePath
.
string
(),
cv
::
IMREAD_COLOR
);
cv
::
Mat
img
=
cv
::
imread
(
templatePath
.
string
(),
cv
::
IMREAD_COLOR
);
if
(
img
.
empty
())
return
false
;
if
(
img
.
empty
())
return
false
;
setAirspeedTemplate
(
img
);
setAirspeedTemplate
(
img
);
...
@@ -35,6 +39,7 @@ bool InstrumentClassifier::loadAirspeedTemplate(const std::filesystem::path& tem
...
@@ -35,6 +39,7 @@ bool InstrumentClassifier::loadAirspeedTemplate(const std::filesystem::path& tem
void
InstrumentClassifier
::
setAirspeedTemplate
(
const
cv
::
Mat
&
bgrTemplate
)
{
void
InstrumentClassifier
::
setAirspeedTemplate
(
const
cv
::
Mat
&
bgrTemplate
)
{
if
(
bgrTemplate
.
empty
()
||
bgrTemplate
.
type
()
!=
CV_8UC3
)
{
if
(
bgrTemplate
.
empty
()
||
bgrTemplate
.
type
()
!=
CV_8UC3
)
{
// 模板无效时重置到已知的回退状态。
ready_
=
false
;
ready_
=
false
;
templateEdge32_
.
release
();
templateEdge32_
.
release
();
mask8_
.
release
();
mask8_
.
release
();
...
@@ -42,26 +47,35 @@ void InstrumentClassifier::setAirspeedTemplate(const cv::Mat& bgrTemplate) {
...
@@ -42,26 +47,35 @@ void InstrumentClassifier::setAirspeedTemplate(const cv::Mat& bgrTemplate) {
}
}
targetSize_
=
bgrTemplate
.
size
();
targetSize_
=
bgrTemplate
.
size
();
// 只预计算一次边缘模板和掩码,使 classify() 保持轻量。
// 这些 cv::Mat 在分类器生命周期内持有各自的堆像素缓冲区。
templateEdge32_
=
prepareEdgeForClassification
(
bgrTemplate
,
targetSize_
);
templateEdge32_
=
prepareEdgeForClassification
(
bgrTemplate
,
targetSize_
);
mask8_
=
makeClassificationMask
(
targetSize_
,
config_
.
airspeedGeometry
);
mask8_
=
makeClassificationMask
(
targetSize_
,
config_
.
airspeedGeometry
);
ready_
=
true
;
ready_
=
true
;
}
}
ClassificationResult
InstrumentClassifier
::
classify
(
const
std
::
filesystem
::
path
&
path
,
const
cv
::
Mat
&
bgr
)
const
{
ClassificationResult
InstrumentClassifier
::
classify
(
const
std
::
filesystem
::
path
&
path
,
const
cv
::
Mat
&
bgr
)
const
{
// ClassificationResult 只保存标量元数据,不会让图像数据离开本函数。
ClassificationResult
result
;
ClassificationResult
result
;
result
.
type
=
InstrumentType
::
OtherInstrument
;
result
.
type
=
InstrumentType
::
OtherInstrument
;
// 即使视觉分数处于阈值边缘,文件名提示仍可让生成数据集正常使用。
std
::
string
pathLower
=
lowerCopy
(
path
.
string
());
std
::
string
pathLower
=
lowerCopy
(
path
.
string
());
result
.
pathHint
=
pathLower
.
find
(
"airspeed"
)
!=
std
::
string
::
npos
||
result
.
pathHint
=
pathLower
.
find
(
"airspeed"
)
!=
std
::
string
::
npos
||
pathLower
.
find
(
"01_airspeed_indicator"
)
!=
std
::
string
::
npos
;
pathLower
.
find
(
"01_airspeed_indicator"
)
!=
std
::
string
::
npos
;
if
(
ready_
&&
!
bgr
.
empty
())
{
if
(
ready_
&&
!
bgr
.
empty
())
{
// candidateEdge 是逐图像临时缓冲区,分类完成后即被释放。
cv
::
Mat
candidateEdge
=
prepareEdgeForClassification
(
bgr
,
targetSize_
);
cv
::
Mat
candidateEdge
=
prepareEdgeForClassification
(
bgr
,
targetSize_
);
result
.
visualScore
=
maskedNcc
(
candidateEdge
,
templateEdge32_
,
mask8_
);
result
.
visualScore
=
maskedNcc
(
candidateEdge
,
templateEdge32_
,
mask8_
);
// 强视觉匹配可以直接通过;路径提示只降低阈值,并不会跳过视觉匹配。
result
.
isAirspeed
=
result
.
visualScore
>=
config_
.
airspeedVisualThreshold
||
result
.
isAirspeed
=
result
.
visualScore
>=
config_
.
airspeedVisualThreshold
||
(
result
.
pathHint
&&
result
.
visualScore
>=
config_
.
airspeedVisualThreshold
*
config_
.
pathHintThresholdFactor
);
(
result
.
pathHint
&&
result
.
visualScore
>=
config_
.
airspeedVisualThreshold
*
config_
.
pathHintThresholdFactor
);
result
.
confidence
=
std
::
max
(
result
.
visualScore
,
result
.
pathHint
?
0.55
:
0.0
);
result
.
confidence
=
std
::
max
(
result
.
visualScore
,
result
.
pathHint
?
0.55
:
0.0
);
}
else
{
}
else
{
// 没有模板时只根据路径提示进行路由,并将置信度标记为启发式结果。
result
.
isAirspeed
=
result
.
pathHint
;
result
.
isAirspeed
=
result
.
pathHint
;
result
.
confidence
=
result
.
pathHint
?
0.55
:
0.0
;
result
.
confidence
=
result
.
pathHint
?
0.55
:
0.0
;
}
}
...
@@ -71,15 +85,19 @@ ClassificationResult InstrumentClassifier::classify(const std::filesystem::path&
...
@@ -71,15 +85,19 @@ ClassificationResult InstrumentClassifier::classify(const std::filesystem::path&
}
}
cv
::
Mat
InstrumentClassifier
::
prepareEdgeForClassification
(
const
cv
::
Mat
&
bgr
,
cv
::
Size
targetSize
)
{
cv
::
Mat
InstrumentClassifier
::
prepareEdgeForClassification
(
const
cv
::
Mat
&
bgr
,
cv
::
Size
targetSize
)
{
// 以下每个阶段都拥有临时像素缓冲区。按值返回 edge 的成本很低,
// 因为 cv::Mat 会移动或共享引用计数管理的存储。
cv
::
Mat
resized
;
cv
::
Mat
resized
;
cv
::
resize
(
bgr
,
resized
,
targetSize
,
0
,
0
,
cv
::
INTER_AREA
);
cv
::
resize
(
bgr
,
resized
,
targetSize
,
0
,
0
,
cv
::
INTER_AREA
);
// 提取边缘前使用 CLAHE 归一化照明差异。
cv
::
Mat
gray
;
cv
::
Mat
gray
;
cv
::
cvtColor
(
resized
,
gray
,
cv
::
COLOR_BGR2GRAY
);
cv
::
cvtColor
(
resized
,
gray
,
cv
::
COLOR_BGR2GRAY
);
cv
::
Ptr
<
cv
::
CLAHE
>
clahe
=
cv
::
createCLAHE
(
2.0
,
cv
::
Size
(
8
,
8
));
cv
::
Ptr
<
cv
::
CLAHE
>
clahe
=
cv
::
createCLAHE
(
2.0
,
cv
::
Size
(
8
,
8
));
clahe
->
apply
(
gray
,
gray
);
clahe
->
apply
(
gray
,
gray
);
cv
::
GaussianBlur
(
gray
,
gray
,
cv
::
Size
(
3
,
3
),
0
);
cv
::
GaussianBlur
(
gray
,
gray
,
cv
::
Size
(
3
,
3
),
0
);
// 对同一表盘布局进行匹配时,边缘特征比原始亮度更稳健。
cv
::
Mat
edge
;
cv
::
Mat
edge
;
cv
::
Canny
(
gray
,
edge
,
60
,
160
);
cv
::
Canny
(
gray
,
edge
,
60
,
160
);
edge
.
convertTo
(
edge
,
CV_32F
,
1.0
/
255.0
);
edge
.
convertTo
(
edge
,
CV_32F
,
1.0
/
255.0
);
...
@@ -87,6 +105,7 @@ cv::Mat InstrumentClassifier::prepareEdgeForClassification(const cv::Mat& bgr, c
...
@@ -87,6 +105,7 @@ cv::Mat InstrumentClassifier::prepareEdgeForClassification(const cv::Mat& bgr, c
}
}
cv
::
Mat
InstrumentClassifier
::
makeClassificationMask
(
cv
::
Size
size
,
const
ReaderGeometry
&
geometry
)
{
cv
::
Mat
InstrumentClassifier
::
makeClassificationMask
(
cv
::
Size
size
,
const
ReaderGeometry
&
geometry
)
{
// 将参考几何参数缩放到模板或候选图像尺寸。
float
sx
=
static_cast
<
float
>
(
size
.
width
)
/
std
::
max
(
1.0
f
,
geometry
.
referenceSize
.
width
);
float
sx
=
static_cast
<
float
>
(
size
.
width
)
/
std
::
max
(
1.0
f
,
geometry
.
referenceSize
.
width
);
float
sy
=
static_cast
<
float
>
(
size
.
height
)
/
std
::
max
(
1.0
f
,
geometry
.
referenceSize
.
height
);
float
sy
=
static_cast
<
float
>
(
size
.
height
)
/
std
::
max
(
1.0
f
,
geometry
.
referenceSize
.
height
);
float
s
=
0.5
f
*
(
sx
+
sy
);
float
s
=
0.5
f
*
(
sx
+
sy
);
...
@@ -94,7 +113,10 @@ cv::Mat InstrumentClassifier::makeClassificationMask(cv::Size size, const Reader
...
@@ -94,7 +113,10 @@ cv::Mat InstrumentClassifier::makeClassificationMask(cv::Size size, const Reader
int
radius
=
std
::
max
(
1
,
cvRound
(
geometry
.
radius
*
s
));
int
radius
=
std
::
max
(
1
,
cvRound
(
geometry
.
radius
*
s
));
int
inner
=
std
::
max
(
1
,
cvRound
(
geometry
.
radius
*
0.18
f
*
s
));
int
inner
=
std
::
max
(
1
,
cvRound
(
geometry
.
radius
*
0.18
f
*
s
));
// 掩码每个像素占 1 字节,并按模板尺寸进行缓存。
cv
::
Mat
mask
(
size
,
CV_8U
,
cv
::
Scalar
(
0
));
cv
::
Mat
mask
(
size
,
CV_8U
,
cv
::
Scalar
(
0
));
// 保留表盘圆环区域,去除指针位置会变化的中心轴区域。
cv
::
circle
(
mask
,
center
,
radius
,
cv
::
Scalar
(
255
),
-
1
,
cv
::
LINE_AA
);
cv
::
circle
(
mask
,
center
,
radius
,
cv
::
Scalar
(
255
),
-
1
,
cv
::
LINE_AA
);
cv
::
circle
(
mask
,
center
,
inner
,
cv
::
Scalar
(
0
),
-
1
,
cv
::
LINE_AA
);
cv
::
circle
(
mask
,
center
,
inner
,
cv
::
Scalar
(
0
),
-
1
,
cv
::
LINE_AA
);
cv
::
erode
(
mask
,
mask
,
cv
::
Mat
(),
cv
::
Point
(
-
1
,
-
1
),
1
);
cv
::
erode
(
mask
,
mask
,
cv
::
Mat
(),
cv
::
Point
(
-
1
,
-
1
),
1
);
...
@@ -106,6 +128,7 @@ double InstrumentClassifier::maskedNcc(const cv::Mat& a32, const cv::Mat& b32, c
...
@@ -106,6 +128,7 @@ double InstrumentClassifier::maskedNcc(const cv::Mat& a32, const cv::Mat& b32, c
double
sumB
=
0.0
;
double
sumB
=
0.0
;
int
n
=
0
;
int
n
=
0
;
// 第一遍扫描计算掩码区域内的均值。
for
(
int
y
=
0
;
y
<
a32
.
rows
;
++
y
)
{
for
(
int
y
=
0
;
y
<
a32
.
rows
;
++
y
)
{
const
float
*
a
=
a32
.
ptr
<
float
>
(
y
);
const
float
*
a
=
a32
.
ptr
<
float
>
(
y
);
const
float
*
b
=
b32
.
ptr
<
float
>
(
y
);
const
float
*
b
=
b32
.
ptr
<
float
>
(
y
);
...
@@ -124,6 +147,8 @@ double InstrumentClassifier::maskedNcc(const cv::Mat& a32, const cv::Mat& b32, c
...
@@ -124,6 +147,8 @@ double InstrumentClassifier::maskedNcc(const cv::Mat& a32, const cv::Mat& b32, c
double
dot
=
0.0
;
double
dot
=
0.0
;
double
normA
=
0.0
;
double
normA
=
0.0
;
double
normB
=
0.0
;
double
normB
=
0.0
;
// 第二遍扫描在同一掩码区域内计算归一化互相关。
for
(
int
y
=
0
;
y
<
a32
.
rows
;
++
y
)
{
for
(
int
y
=
0
;
y
<
a32
.
rows
;
++
y
)
{
const
float
*
a
=
a32
.
ptr
<
float
>
(
y
);
const
float
*
a
=
a32
.
ptr
<
float
>
(
y
);
const
float
*
b
=
b32
.
ptr
<
float
>
(
y
);
const
float
*
b
=
b32
.
ptr
<
float
>
(
y
);
...
...
src/lut_mapper.cpp
View file @
95f55843
#include "instrument_reader/lut_mapper.hpp"
#
include
"instrument_reader/lut_mapper.hpp"
#include "instrument_reader/angle_math.hpp"
#include "instrument_reader/angle_math.hpp"
...
@@ -14,6 +14,7 @@ namespace instrument_reader {
...
@@ -14,6 +14,7 @@ namespace instrument_reader {
namespace
{
namespace
{
// 保持各工具之间序列化边界策略名称的稳定性。
std
::
string
policyToString
(
BoundaryPolicy
policy
)
{
std
::
string
policyToString
(
BoundaryPolicy
policy
)
{
return
policy
==
BoundaryPolicy
::
Clamp
?
"clamp"
:
"reject"
;
return
policy
==
BoundaryPolicy
::
Clamp
?
"clamp"
:
"reject"
;
}
}
...
@@ -25,6 +26,8 @@ LutMapper::LutMapper(double startAngleDeg)
...
@@ -25,6 +26,8 @@ LutMapper::LutMapper(double startAngleDeg)
void
LutMapper
::
setStartAngle
(
double
startAngleDeg
)
{
void
LutMapper
::
setStartAngle
(
double
startAngleDeg
)
{
startAngleDeg_
=
normalize360
(
startAngleDeg
);
startAngleDeg_
=
normalize360
(
startAngleDeg
);
// 相对角度依赖角度原点,因此需要重新计算所有缓存值。
for
(
CalibrationPoint
&
p
:
points_
)
{
for
(
CalibrationPoint
&
p
:
points_
)
{
p
.
thetaRelDeg
=
relativeFromStart
(
p
.
thetaAbsDeg
,
startAngleDeg_
);
p
.
thetaRelDeg
=
relativeFromStart
(
p
.
thetaAbsDeg
,
startAngleDeg_
);
}
}
...
@@ -39,6 +42,8 @@ void LutMapper::addPoint(double value, double thetaAbsDeg) {
...
@@ -39,6 +42,8 @@ void LutMapper::addPoint(double value, double thetaAbsDeg) {
CalibrationPoint
p
;
CalibrationPoint
p
;
p
.
value
=
value
;
p
.
value
=
value
;
p
.
thetaAbsDeg
=
normalize360
(
thetaAbsDeg
);
p
.
thetaAbsDeg
=
normalize360
(
thetaAbsDeg
);
// thetaRelDeg 是插值时使用的排序键和查找键。
p
.
thetaRelDeg
=
relativeFromStart
(
thetaAbsDeg
,
startAngleDeg_
);
p
.
thetaRelDeg
=
relativeFromStart
(
thetaAbsDeg
,
startAngleDeg_
);
points_
.
push_back
(
p
);
points_
.
push_back
(
p
);
sortByRelativeAngle
();
sortByRelativeAngle
();
...
@@ -49,6 +54,8 @@ bool LutMapper::validate(std::string* errorMessage) const {
...
@@ -49,6 +54,8 @@ bool LutMapper::validate(std::string* errorMessage) const {
if
(
errorMessage
)
*
errorMessage
=
"LUT needs at least two points"
;
if
(
errorMessage
)
*
errorMessage
=
"LUT needs at least two points"
;
return
false
;
return
false
;
}
}
// 为保证插值安全,物理值和相对角度都必须严格递增。
for
(
size_t
i
=
1
;
i
<
points_
.
size
();
++
i
)
{
for
(
size_t
i
=
1
;
i
<
points_
.
size
();
++
i
)
{
if
(
points_
[
i
].
thetaRelDeg
<=
points_
[
i
-
1
].
thetaRelDeg
)
{
if
(
points_
[
i
].
thetaRelDeg
<=
points_
[
i
-
1
].
thetaRelDeg
)
{
if
(
errorMessage
)
*
errorMessage
=
"theta_rel_deg must be strictly increasing"
;
if
(
errorMessage
)
*
errorMessage
=
"theta_rel_deg must be strictly increasing"
;
...
@@ -66,6 +73,7 @@ bool LutMapper::saveJson(const std::filesystem::path& path) const {
...
@@ -66,6 +73,7 @@ bool LutMapper::saveJson(const std::filesystem::path& path) const {
std
::
ofstream
out
(
path
,
std
::
ios
::
binary
);
std
::
ofstream
out
(
path
,
std
::
ios
::
binary
);
if
(
!
out
)
return
false
;
if
(
!
out
)
return
false
;
// 输出足够的小数精度,以保存手工测量的标定节点。
out
<<
"{
\n
"
;
out
<<
"{
\n
"
;
out
<<
"
\"
type
\"
:
\"
instrument_lut_v1
\"
,
\n
"
;
out
<<
"
\"
type
\"
:
\"
instrument_lut_v1
\"
,
\n
"
;
out
<<
"
\"
start_angle_deg
\"
: "
<<
std
::
fixed
<<
std
::
setprecision
(
10
)
<<
startAngleDeg_
<<
",
\n
"
;
out
<<
"
\"
start_angle_deg
\"
: "
<<
std
::
fixed
<<
std
::
setprecision
(
10
)
<<
startAngleDeg_
<<
",
\n
"
;
...
@@ -92,8 +100,12 @@ bool LutMapper::loadJson(const std::filesystem::path& path, std::string* errorMe
...
@@ -92,8 +100,12 @@ bool LutMapper::loadJson(const std::filesystem::path& path, std::string* errorMe
}
}
std
::
stringstream
buffer
;
std
::
stringstream
buffer
;
buffer
<<
in
.
rdbuf
();
buffer
<<
in
.
rdbuf
();
// 将整个 LUT 文件读入一个字符串。LUT 是很小的标定文件,
// 与逐个令牌流式解析相比,这种方式更简单,开销也更低。
std
::
string
text
=
buffer
.
str
();
std
::
string
text
=
buffer
.
str
();
// 正则解析刻意只匹配本项目的 LUT 结构,避免接受模糊格式。
std
::
smatch
match
;
std
::
smatch
match
;
std
::
regex
startRegex
(
R"json("start_angle_deg"\s*:\s*([-+0-9.eE]+))json"
);
std
::
regex
startRegex
(
R"json("start_angle_deg"\s*:\s*([-+0-9.eE]+))json"
);
if
(
std
::
regex_search
(
text
,
match
,
startRegex
))
{
if
(
std
::
regex_search
(
text
,
match
,
startRegex
))
{
...
@@ -105,12 +117,17 @@ bool LutMapper::loadJson(const std::filesystem::path& path, std::string* errorMe
...
@@ -105,12 +117,17 @@ bool LutMapper::loadJson(const std::filesystem::path& path, std::string* errorMe
std
::
regex
policyRegex
(
R"json("boundary_policy"\s*:\s*"([^"]+)")json"
);
std
::
regex
policyRegex
(
R"json("boundary_policy"\s*:\s*"([^"]+)")json"
);
if
(
std
::
regex_search
(
text
,
match
,
policyRegex
))
{
if
(
std
::
regex_search
(
text
,
match
,
policyRegex
))
{
// 未知策略字符串为保证安全,统一回退为 Reject。
boundaryPolicy_
=
match
[
1
].
str
()
==
"clamp"
?
BoundaryPolicy
::
Clamp
:
BoundaryPolicy
::
Reject
;
boundaryPolicy_
=
match
[
1
].
str
()
==
"clamp"
?
BoundaryPolicy
::
Clamp
:
BoundaryPolicy
::
Reject
;
}
}
std
::
vector
<
CalibrationPoint
>
loaded
;
std
::
vector
<
CalibrationPoint
>
loaded
;
// 同时兼容旧版 "airspeed" 字段名和当前通用的 "value" 字段名。
std
::
regex
pointRegex
(
std
::
regex
pointRegex
(
R"json(\{[^\}]*"(?:value|airspeed)"\s*:\s*([-+0-9.eE]+)[^\}]*"theta_abs_deg"\s*:\s*([-+0-9.eE]+)[^\}]*\})json"
);
R"json(\{[^\}]*"(?:value|airspeed)"\s*:\s*([-+0-9.eE]+)[^\}]*"theta_abs_deg"\s*:\s*([-+0-9.eE]+)[^\}]*\})json"
);
// loaded 是临时堆存储;完成验证准备后,由 points_ 接管其内存。
for
(
std
::
sregex_iterator
it
(
text
.
begin
(),
text
.
end
(),
pointRegex
),
end
;
it
!=
end
;
++
it
)
{
for
(
std
::
sregex_iterator
it
(
text
.
begin
(),
text
.
end
(),
pointRegex
),
end
;
it
!=
end
;
++
it
)
{
CalibrationPoint
p
;
CalibrationPoint
p
;
p
.
value
=
std
::
stod
((
*
it
)[
1
].
str
());
p
.
value
=
std
::
stod
((
*
it
)[
1
].
str
());
...
@@ -120,12 +137,17 @@ bool LutMapper::loadJson(const std::filesystem::path& path, std::string* errorMe
...
@@ -120,12 +137,17 @@ bool LutMapper::loadJson(const std::filesystem::path& path, std::string* errorMe
}
}
points_
=
std
::
move
(
loaded
);
points_
=
std
::
move
(
loaded
);
// 加载后再排序,因此 JSON 中的标定节点可以按任意顺序书写。
sortByRelativeAngle
();
sortByRelativeAngle
();
return
validate
(
errorMessage
);
return
validate
(
errorMessage
);
}
}
MapResult
LutMapper
::
map
(
double
thetaAbsDeg
)
const
{
MapResult
LutMapper
::
map
(
double
thetaAbsDeg
)
const
{
// MapResult 按值返回,只包含少量 double 和字符串字段。
MapResult
result
;
MapResult
result
;
// 查找前,先将指针绝对角度转换为相对 LUT 起点的扫描角度。
result
.
thetaAbsDeg
=
normalize360
(
thetaAbsDeg
);
result
.
thetaAbsDeg
=
normalize360
(
thetaAbsDeg
);
result
.
thetaRelDeg
=
relativeFromStart
(
thetaAbsDeg
,
startAngleDeg_
);
result
.
thetaRelDeg
=
relativeFromStart
(
thetaAbsDeg
,
startAngleDeg_
);
...
@@ -140,6 +162,8 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
...
@@ -140,6 +162,8 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
const
CalibrationPoint
&
last
=
points_
.
back
();
const
CalibrationPoint
&
last
=
points_
.
back
();
if
(
result
.
thetaRelDeg
<
first
.
thetaRelDeg
)
{
if
(
result
.
thetaRelDeg
<
first
.
thetaRelDeg
)
{
// first.thetaRelDeg 为 0 时通常不会进入此分支,
// 但保留它可以让映射器正确支持任意起始角度。
result
.
left
=
first
;
result
.
left
=
first
;
result
.
right
=
first
;
result
.
right
=
first
;
if
(
boundaryPolicy_
==
BoundaryPolicy
::
Clamp
)
{
if
(
boundaryPolicy_
==
BoundaryPolicy
::
Clamp
)
{
...
@@ -156,6 +180,8 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
...
@@ -156,6 +180,8 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
if
(
result
.
thetaRelDeg
>
last
.
thetaRelDeg
)
{
if
(
result
.
thetaRelDeg
>
last
.
thetaRelDeg
)
{
if
(
boundaryPolicy_
==
BoundaryPolicy
::
Clamp
)
{
if
(
boundaryPolicy_
==
BoundaryPolicy
::
Clamp
)
{
result
.
valid
=
true
;
result
.
valid
=
true
;
// 在跨越 360 度零点附近钳制时,选择距离最近的标定扫描端点。
const
double
distanceToMax
=
result
.
thetaRelDeg
-
last
.
thetaRelDeg
;
const
double
distanceToMax
=
result
.
thetaRelDeg
-
last
.
thetaRelDeg
;
const
double
distanceToMinAcrossStart
=
360.0
-
result
.
thetaRelDeg
+
first
.
thetaRelDeg
;
const
double
distanceToMinAcrossStart
=
360.0
-
result
.
thetaRelDeg
+
first
.
thetaRelDeg
;
if
(
distanceToMinAcrossStart
<
distanceToMax
)
{
if
(
distanceToMinAcrossStart
<
distanceToMax
)
{
...
@@ -178,6 +204,7 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
...
@@ -178,6 +204,7 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
return
result
;
return
result
;
}
}
// 使用二分查找定位当前分段线性区间的右端点。
auto
rightIt
=
std
::
lower_bound
(
points_
.
begin
(),
points_
.
end
(),
result
.
thetaRelDeg
,
auto
rightIt
=
std
::
lower_bound
(
points_
.
begin
(),
points_
.
end
(),
result
.
thetaRelDeg
,
[](
const
CalibrationPoint
&
point
,
double
thetaRelDeg
)
{
[](
const
CalibrationPoint
&
point
,
double
thetaRelDeg
)
{
return
point
.
thetaRelDeg
<
thetaRelDeg
;
return
point
.
thetaRelDeg
<
thetaRelDeg
;
...
@@ -201,6 +228,8 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
...
@@ -201,6 +228,8 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
const
CalibrationPoint
&
left
=
*
std
::
prev
(
rightIt
);
const
CalibrationPoint
&
left
=
*
std
::
prev
(
rightIt
);
const
CalibrationPoint
&
right
=
*
rightIt
;
const
CalibrationPoint
&
right
=
*
rightIt
;
const
double
denom
=
right
.
thetaRelDeg
-
left
.
thetaRelDeg
;
const
double
denom
=
right
.
thetaRelDeg
-
left
.
thetaRelDeg
;
// 线性插值公式:value = left + t * (right - left)。
const
double
t
=
denom
>
0.0
?
(
result
.
thetaRelDeg
-
left
.
thetaRelDeg
)
/
denom
:
0.0
;
const
double
t
=
denom
>
0.0
?
(
result
.
thetaRelDeg
-
left
.
thetaRelDeg
)
/
denom
:
0.0
;
result
.
valid
=
true
;
result
.
valid
=
true
;
result
.
value
=
left
.
value
+
t
*
(
right
.
value
-
left
.
value
);
result
.
value
=
left
.
value
+
t
*
(
right
.
value
-
left
.
value
);
...
@@ -211,6 +240,7 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
...
@@ -211,6 +240,7 @@ MapResult LutMapper::map(double thetaAbsDeg) const {
}
}
void
LutMapper
::
sortByRelativeAngle
()
{
void
LutMapper
::
sortByRelativeAngle
()
{
// map() 中的 lower_bound 依赖此排序顺序。
std
::
sort
(
points_
.
begin
(),
points_
.
end
(),
[](
const
CalibrationPoint
&
a
,
const
CalibrationPoint
&
b
)
{
std
::
sort
(
points_
.
begin
(),
points_
.
end
(),
[](
const
CalibrationPoint
&
a
,
const
CalibrationPoint
&
b
)
{
return
a
.
thetaRelDeg
<
b
.
thetaRelDeg
;
return
a
.
thetaRelDeg
<
b
.
thetaRelDeg
;
});
});
...
...
src/non_linear_gauge_reader.cpp
View file @
95f55843
#include "instrument_reader/non_linear_gauge_reader.hpp"
#
include
"instrument_reader/non_linear_gauge_reader.hpp"
#include <algorithm>
#include <algorithm>
#include <cmath>
#include <cmath>
...
@@ -10,12 +10,15 @@ namespace instrument_reader::nonlinear {
...
@@ -10,12 +10,15 @@ namespace instrument_reader::nonlinear {
namespace
{
namespace
{
// 标定节点必须是有限数值,才能安全排序和预计算斜率。
bool
isFinite
(
CalibrationPoint
point
)
noexcept
{
return
std
::
isfinite
(
point
.
angle
)
&&
std
::
isfinite
(
point
.
value
);
}
bool
isFinite
(
CalibrationPoint
point
)
noexcept
{
return
std
::
isfinite
(
point
.
angle
)
&&
std
::
isfinite
(
point
.
value
);
}
}
// namespace
}
// namespace
NonLinearGaugeReader
::
NonLinearGaugeReader
(
std
::
vector
<
CalibrationPoint
>
calibration
,
BoundaryMode
boundaryMode
)
NonLinearGaugeReader
::
NonLinearGaugeReader
(
std
::
vector
<
CalibrationPoint
>
calibration
,
BoundaryMode
boundaryMode
)
:
calibration_
(
std
::
move
(
calibration
)),
boundaryMode_
(
boundaryMode
)
{
:
calibration_
(
std
::
move
(
calibration
)),
boundaryMode_
(
boundaryMode
)
{
// calibration 按值传入,因此调用方可以传入临时对象;在条件允许时,
// move() 会将 vector 的堆存储直接转移给读数器,避免再次复制。
if
(
calibration_
.
size
()
<
2
)
{
if
(
calibration_
.
size
()
<
2
)
{
throw
std
::
invalid_argument
(
"NonLinearGaugeReader requires at least two calibration points"
);
throw
std
::
invalid_argument
(
"NonLinearGaugeReader requires at least two calibration points"
);
}
}
...
@@ -24,7 +27,7 @@ NonLinearGaugeReader::NonLinearGaugeReader(std::vector<CalibrationPoint> calibra
...
@@ -24,7 +27,7 @@ NonLinearGaugeReader::NonLinearGaugeReader(std::vector<CalibrationPoint> calibra
throw
std
::
invalid_argument
(
"NonLinearGaugeReader calibration contains non-finite data"
);
throw
std
::
invalid_argument
(
"NonLinearGaugeReader calibration contains non-finite data"
);
}
}
//
Sorting once at construction keeps the high-frequency read path branch-light.
//
构造时只排序一次,使高频读数路径保持较少分支。
std
::
sort
(
calibration_
.
begin
(),
calibration_
.
end
(),
std
::
sort
(
calibration_
.
begin
(),
calibration_
.
end
(),
[](
const
CalibrationPoint
&
a
,
const
CalibrationPoint
&
b
)
{
return
a
.
angle
<
b
.
angle
;
});
[](
const
CalibrationPoint
&
a
,
const
CalibrationPoint
&
b
)
{
return
a
.
angle
<
b
.
angle
;
});
...
@@ -33,14 +36,20 @@ NonLinearGaugeReader::NonLinearGaugeReader(std::vector<CalibrationPoint> calibra
...
@@ -33,14 +36,20 @@ NonLinearGaugeReader::NonLinearGaugeReader(std::vector<CalibrationPoint> calibra
const
CalibrationPoint
&
left
=
calibration_
[
i
-
1
];
const
CalibrationPoint
&
left
=
calibration_
[
i
-
1
];
const
CalibrationPoint
&
right
=
calibration_
[
i
];
const
CalibrationPoint
&
right
=
calibration_
[
i
];
const
float
deltaAngle
=
right
.
angle
-
left
.
angle
;
const
float
deltaAngle
=
right
.
angle
-
left
.
angle
;
// 重复或反向角度会使该分段的斜率无定义。
if
(
deltaAngle
<=
0.0
f
)
{
if
(
deltaAngle
<=
0.0
f
)
{
throw
std
::
invalid_argument
(
"NonLinearGaugeReader calibration angles must be strictly increasing"
);
throw
std
::
invalid_argument
(
"NonLinearGaugeReader calibration angles must be strictly increasing"
);
}
}
// 提前完成除法,使 calculate_value() 热路径只需要执行乘法。
slopes_
.
push_back
((
right
.
value
-
left
.
value
)
/
deltaAngle
);
slopes_
.
push_back
((
right
.
value
-
left
.
value
)
/
deltaAngle
);
}
}
}
}
float
NonLinearGaugeReader
::
calculate_value
(
float
current_angle
)
const
noexcept
{
float
NonLinearGaugeReader
::
calculate_value
(
float
current_angle
)
const
noexcept
{
// 此热路径不会分配堆内存,只读取 calibration_ 和 slopes_。
// 检测器输出非有限值时应继续返回 NaN,而不是错误地钳制为端点值。
if
(
!
std
::
isfinite
(
current_angle
))
{
if
(
!
std
::
isfinite
(
current_angle
))
{
return
std
::
numeric_limits
<
float
>::
quiet_NaN
();
return
std
::
numeric_limits
<
float
>::
quiet_NaN
();
}
}
...
@@ -49,18 +58,22 @@ float NonLinearGaugeReader::calculate_value(float current_angle) const noexcept
...
@@ -49,18 +58,22 @@ float NonLinearGaugeReader::calculate_value(float current_angle) const noexcept
const
CalibrationPoint
&
last
=
calibration_
.
back
();
const
CalibrationPoint
&
last
=
calibration_
.
back
();
if
(
current_angle
<=
first
.
angle
)
{
if
(
current_angle
<=
first
.
angle
)
{
// 钳制对控制循环最安全;外推模式保留给标定和分析工具使用。
return
boundaryMode_
==
BoundaryMode
::
Clamp
?
first
.
value
:
interpolateSegment
(
0
,
current_angle
);
return
boundaryMode_
==
BoundaryMode
::
Clamp
?
first
.
value
:
interpolateSegment
(
0
,
current_angle
);
}
}
if
(
current_angle
>=
last
.
angle
)
{
if
(
current_angle
>=
last
.
angle
)
{
// 仅在调用方明确要求时,使用最后一个标定分段的斜率向外推算。
return
boundaryMode_
==
BoundaryMode
::
Clamp
?
last
.
value
:
interpolateSegment
(
slopes_
.
size
()
-
1
,
current_angle
);
return
boundaryMode_
==
BoundaryMode
::
Clamp
?
last
.
value
:
interpolateSegment
(
slopes_
.
size
()
-
1
,
current_angle
);
}
}
// 查找第一个角度不小于 current_angle 的标定节点。
const
auto
upper
=
std
::
lower_bound
(
calibration_
.
begin
(),
calibration_
.
end
(),
current_angle
,
const
auto
upper
=
std
::
lower_bound
(
calibration_
.
begin
(),
calibration_
.
end
(),
current_angle
,
[](
const
CalibrationPoint
&
point
,
float
angle
)
{
return
point
.
angle
<
angle
;
});
[](
const
CalibrationPoint
&
point
,
float
angle
)
{
return
point
.
angle
<
angle
;
});
const
std
::
size_t
rightIndex
=
static_cast
<
std
::
size_t
>
(
upper
-
calibration_
.
begin
());
const
std
::
size_t
rightIndex
=
static_cast
<
std
::
size_t
>
(
upper
-
calibration_
.
begin
());
if
(
upper
->
angle
==
current_angle
)
{
if
(
upper
->
angle
==
current_angle
)
{
// 精确命中标定节点时直接返回,避免产生细小的浮点插值漂移。
return
upper
->
value
;
return
upper
->
value
;
}
}
...
@@ -69,6 +82,8 @@ float NonLinearGaugeReader::calculate_value(float current_angle) const noexcept
...
@@ -69,6 +82,8 @@ float NonLinearGaugeReader::calculate_value(float current_angle) const noexcept
float
NonLinearGaugeReader
::
interpolateSegment
(
std
::
size_t
leftIndex
,
float
angle
)
const
noexcept
{
float
NonLinearGaugeReader
::
interpolateSegment
(
std
::
size_t
leftIndex
,
float
angle
)
const
noexcept
{
const
CalibrationPoint
&
left
=
calibration_
[
leftIndex
];
const
CalibrationPoint
&
left
=
calibration_
[
leftIndex
];
// 公式:value = left.value + (angle - left.angle) * 预计算斜率
return
left
.
value
+
(
angle
-
left
.
angle
)
*
slopes_
[
leftIndex
];
return
left
.
value
+
(
angle
-
left
.
angle
)
*
slopes_
[
leftIndex
];
}
}
...
...
src/types.cpp
View file @
95f55843
#include "instrument_reader/types.hpp"
#
include
"instrument_reader/types.hpp"
namespace
instrument_reader
{
namespace
instrument_reader
{
int
DispatchSummary
::
airspeedCount
()
const
{
int
DispatchSummary
::
airspeedCount
()
const
{
int
count
=
0
;
int
count
=
0
;
// 统计被路由到空速表读数器的图像,不要求物理值有效。
for
(
const
ProcessResult
&
r
:
results
)
{
for
(
const
ProcessResult
&
r
:
results
)
{
if
(
r
.
classification
.
type
==
InstrumentType
::
AirspeedIndicator
)
++
count
;
if
(
r
.
classification
.
type
==
InstrumentType
::
AirspeedIndicator
)
++
count
;
}
}
...
@@ -12,6 +14,8 @@ int DispatchSummary::airspeedCount() const {
...
@@ -12,6 +14,8 @@ int DispatchSummary::airspeedCount() const {
int
DispatchSummary
::
validAirspeedCount
()
const
{
int
DispatchSummary
::
validAirspeedCount
()
const
{
int
count
=
0
;
int
count
=
0
;
// 有效空速要求物理值映射成功,不能只有角度检测成功。
for
(
const
ProcessResult
&
r
:
results
)
{
for
(
const
ProcessResult
&
r
:
results
)
{
if
(
r
.
classification
.
type
==
InstrumentType
::
AirspeedIndicator
&&
r
.
airspeedValueValid
)
++
count
;
if
(
r
.
classification
.
type
==
InstrumentType
::
AirspeedIndicator
&&
r
.
airspeedValueValid
)
++
count
;
}
}
...
@@ -20,6 +24,8 @@ int DispatchSummary::validAirspeedCount() const {
...
@@ -20,6 +24,8 @@ int DispatchSummary::validAirspeedCount() const {
int
DispatchSummary
::
skippedCount
()
const
{
int
DispatchSummary
::
skippedCount
()
const
{
int
count
=
0
;
int
count
=
0
;
// 跳过表示图像被分类为其他仪表类型。
for
(
const
ProcessResult
&
r
:
results
)
{
for
(
const
ProcessResult
&
r
:
results
)
{
if
(
r
.
classification
.
type
!=
InstrumentType
::
AirspeedIndicator
)
++
count
;
if
(
r
.
classification
.
type
!=
InstrumentType
::
AirspeedIndicator
)
++
count
;
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment