feat: synchronize YoloRuntime shared segmentation and RGB mask output
This commit is contained in:
commit
bdf5f124ab
@ -18,8 +18,8 @@
|
||||
{
|
||||
"project": "YoloRuntime",
|
||||
"directory": "YoloRuntime",
|
||||
"source_head": "df794f144df90d867f6ec4ec94c55eb3435e1a83",
|
||||
"source_tree": "93f2652c042bbc68079935c49c59de44b928137f",
|
||||
"source_head": "9be44d557eb05c2fab5fd8f69974a6101532cc7c",
|
||||
"source_tree": "395864996ded1983011a3c9375fce0537295dcd8",
|
||||
"working_source_snapshot": false
|
||||
}
|
||||
]
|
||||
|
||||
@ -10,13 +10,36 @@ set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Robust contour fitting uses the same OpenCV core/imgproc implementation as MatchVerify.
|
||||
if(WIN32 AND NOT OpenCV_DIR AND EXISTS "E:/06_EnvConfig/opencv/build/x64/vc16/lib")
|
||||
set(OpenCV_DIR "E:/06_EnvConfig/opencv/build/x64/vc16/lib" CACHE PATH "OpenCV package")
|
||||
endif()
|
||||
if(NOT WIN32 AND NOT OpenCV_DIR AND RKNN_MODEL_ZOO_ROOT AND
|
||||
EXISTS "${RKNN_MODEL_ZOO_ROOT}/3rdparty/opencv/opencv-linux-aarch64/share/OpenCV")
|
||||
set(OpenCV_DIR "${RKNN_MODEL_ZOO_ROOT}/3rdparty/opencv/opencv-linux-aarch64/share/OpenCV")
|
||||
endif()
|
||||
find_package(OpenCV REQUIRED COMPONENTS core imgproc)
|
||||
|
||||
add_library(yolo_runtime SHARED
|
||||
src/yolo_mask_ids.cpp
|
||||
src/yolo_runtime.cpp
|
||||
src/yolo_seg_geometry.cpp)
|
||||
target_include_directories(yolo_runtime
|
||||
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include>
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_library(YoloRuntime::Runtime ALIAS yolo_runtime)
|
||||
target_include_directories(yolo_runtime PRIVATE ${OpenCV_INCLUDE_DIRS})
|
||||
target_link_libraries(yolo_runtime PRIVATE ${OpenCV_LIBS})
|
||||
if(WIN32)
|
||||
foreach(opencv_target IN LISTS OpenCV_LIBS)
|
||||
if(TARGET ${opencv_target})
|
||||
add_custom_command(TARGET yolo_runtime POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:${opencv_target}> $<TARGET_FILE_DIR:yolo_runtime>
|
||||
VERBATIM)
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
set_target_properties(yolo_runtime PROPERTIES EXPORT_NAME Runtime)
|
||||
find_package(Threads REQUIRED)
|
||||
if(WIN32)
|
||||
@ -148,6 +171,9 @@ endif()
|
||||
|
||||
if(YOLO_RUNTIME_BUILD_CONTRACT_TESTS)
|
||||
enable_testing()
|
||||
add_executable(yolo_mask_ids_test tests/mask_ids_test.cpp)
|
||||
target_link_libraries(yolo_mask_ids_test PRIVATE yolo_runtime)
|
||||
add_test(NAME yolo_mask_ids_test COMMAND yolo_mask_ids_test)
|
||||
add_executable(yolo_runtime_c_api_contract tests/c_api_contract.c)
|
||||
target_include_directories(yolo_runtime_c_api_contract PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_link_libraries(yolo_runtime_c_api_contract PRIVATE yolo_runtime)
|
||||
@ -175,7 +201,13 @@ if(YOLO_RUNTIME_BUILD_CONTRACT_TESTS)
|
||||
tests/seg_geometry_test.cpp
|
||||
src/yolo_seg_geometry.cpp)
|
||||
target_include_directories(yolo_seg_geometry_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
target_include_directories(yolo_seg_geometry_test PRIVATE ${OpenCV_INCLUDE_DIRS})
|
||||
target_link_libraries(yolo_seg_geometry_test PRIVATE ${OpenCV_LIBS})
|
||||
add_test(NAME yolo_seg_geometry_test COMMAND yolo_seg_geometry_test)
|
||||
add_executable(yolo_runner_mask_output_test tests/runner_mask_output_test.cpp)
|
||||
target_include_directories(yolo_runner_mask_output_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
add_test(NAME yolo_runner_mask_output_test COMMAND yolo_runner_mask_output_test)
|
||||
add_executable(yolo_runner_label_layout_test tests/runner_label_layout_test.cpp)
|
||||
target_include_directories(yolo_runner_label_layout_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
@ -209,6 +241,11 @@ if(WIN32)
|
||||
"${ONNXRUNTIME_ROOT}/runtimes/win-x64/native/onnxruntime_providers_shared.dll"
|
||||
DESTINATION lib)
|
||||
install(FILES "${ONNXRUNTIME_ROOT}/ThirdPartyNotices.txt" DESTINATION licenses)
|
||||
foreach(opencv_target IN LISTS OpenCV_LIBS)
|
||||
if(TARGET ${opencv_target})
|
||||
install(FILES $<TARGET_FILE:${opencv_target}> DESTINATION lib)
|
||||
endif()
|
||||
endforeach()
|
||||
else()
|
||||
install(TARGETS yolo_runtime rk3588_yolo_runner EXPORT YoloRuntimeTargets
|
||||
RUNTIME DESTINATION .
|
||||
|
||||
@ -2,8 +2,9 @@
|
||||
|
||||
Shared C++17 Runtime, C ABI 5, for Windows x64 CPU and Linux AArch64 RK3588.
|
||||
This is the sole production source for Studio, its SDK packages and MatchVerify.
|
||||
Detect, CLS, Seg, OBB and Pose are retained. Seg supports minimum enclosing circle
|
||||
and minimum-area rotated rectangle; no robust-circle algorithm is added.
|
||||
Detect, CLS, Seg, OBB and Pose are retained. Seg supports four geometry modes:
|
||||
minimum-area rotated rectangle (1), minimum enclosing circle (2), rectangle mean-diameter
|
||||
circle (3), and robust contour-fit circle (4). Existing ABI 5 mode values and structure sizes remain unchanged.
|
||||
|
||||
## Build and consume
|
||||
|
||||
@ -32,3 +33,53 @@ Binary hashes differ between platforms and build toolchains by design.
|
||||
|
||||
Historical origin: RK3588_AI_Platform Runtime ABI 5 at repository HEAD
|
||||
868a5f22f00ff2c81c3d0a6d2037400f037ac924, with its working source preserved during migration.
|
||||
|
||||
## Seg geometry modes
|
||||
|
||||
- `YOLO_SEG_GEOMETRY_MIN_AREA_RECT`: all mask boundary points, minimum-area rotated rectangle.
|
||||
- `YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE`: all mask boundary points, minimum enclosing circle.
|
||||
- `YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE`: same rectangle center, radius `(width + height) / 4`.
|
||||
- `YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE`: largest external contour, arc-length sampling and
|
||||
Tukey IRLS fit with quality checks, migrated from MatchVerify's tested `SegCircleGeometry::Fit`.
|
||||
Failure returns that contour's minimum enclosing circle. `type` remains the requested fit mode;
|
||||
inspect `fit_status` (`YOLO_SEG_FIT_SUCCEEDED` or `YOLO_SEG_FIT_ENCLOSING_FALLBACK`).
|
||||
Other modes return `YOLO_SEG_FIT_NOT_REQUESTED`. Empty masks fail geometry calculation.
|
||||
|
||||
`fit_status` uses the first formerly reserved geometry word; existing fields, structure size
|
||||
and ABI number remain unchanged. Query capability/mode bits before requesting a new mode
|
||||
from an older Runtime. Linux runner: `--seg-geometry rect|circle|mean|fit`; JSON includes fit status.
|
||||
|
||||
OpenCV core/imgproc is now required to build the robust fit. Windows uses `OpenCV_DIR`;
|
||||
installation includes its matching DLL(s) in the dependency manifest. Linux builds require
|
||||
a target-compatible OpenCV package (`OpenCV_DIR`); deploy its shared dependencies as for
|
||||
the existing Seg backend. This change does not update already deployed consumer applications.
|
||||
|
||||
|
||||
## RGB instance-ID masks (2026-10-21)
|
||||
|
||||
`yolo_masks_compose_rgb_ids` is an additive public C API (ABI 5 structures
|
||||
unchanged). Obtain each binary mask with `yolo_result_set_get_seg_mask`, keep
|
||||
its result set alive, and pass the mask views in the intended instance order.
|
||||
The function assigns ID = array index + 1 (not class ID), supports 0..255 masks,
|
||||
and places each mask using its original-image origin. Pass the original image
|
||||
width/height and a caller-owned RGB888 destination with its byte stride/capacity.
|
||||
No dilation, fitting, resizing, or contour filling is performed.
|
||||
|
||||
| Membership | RGB bytes |
|
||||
|---|---|
|
||||
| Background | (0,0,0) |
|
||||
| One instance | (ID,ID,ID) |
|
||||
| Two instances | (ID1,ID2,ID1) |
|
||||
| Three instances | (ID1,ID2,ID3) |
|
||||
|
||||
IDs in a shared pixel are ascending. Remove zero and duplicate channel values
|
||||
to decode the instance set. Holes and disconnected regions are retained.
|
||||
More than 255 instances or more than three at a pixel returns
|
||||
`YOLO_STATUS_UNSUPPORTED` with a diagnostic when an error object is supplied.
|
||||
Invalid bounds/stride/nonbinary pixels are rejected. Failure leaves the complete
|
||||
destination unchanged; successful writes leave row padding unchanged. Zero masks
|
||||
produce a black canvas. Source memory must remain readable during the call and
|
||||
must not overlap output memory. Save as a lossless RGB image; when using a BGR
|
||||
writer, convert RGB to BGR first. Existing per-instance binary-mask access and
|
||||
runner PGM exports are unchanged. Apps must explicitly adopt the new API; this
|
||||
batch does not redeploy any app or device runtime.
|
||||
|
||||
@ -1,8 +1,47 @@
|
||||
# Verification and consumer handoff
|
||||
|
||||
- Public C ABI headers and geometry sources are byte-identical to the extracted Studio snapshot. Windows UTF-8 filesystem boundaries were repaired; all model weights and user samples remain outside this library.
|
||||
- Initial extraction preserved the Studio snapshot. The four-mode update below adds two geometry modes while retaining ABI 5 sizes, old field offsets, and existing mode values. All model weights and user samples remain outside this library.
|
||||
- `scripts/verify.ps1`: standalone Windows build, seven CTest contracts with Release assertions enabled, dependency negative checks, installation and an independent installed CMake consumer.
|
||||
- Native Linux: configure with RKNN backends disabled, build and run `ctest --output-on-failure` from the build directory. Seven contracts pass; this is host-native logic validation, not NPU execution.
|
||||
- Studio cross-builds the Linux AArch64 Runtime using its explicitly supplied Model Zoo. The manifest identifies ELF architecture; only matching Windows/ARM source revisions can be published together.
|
||||
- MatchVerify's scripts/verify-runtime-update.ps1 exercises both real consumer build paths against a disposable ABI-compatible source update and restores the original source dependency. SDK decoding/masks/geometry parity uses the real business sample without changing it.
|
||||
- Final current-source completion and command evidence: .codex/delivery-integrity.json. There is no physical RK3588/camera/robot acceptance and no automatic update service for already deployed programs.
|
||||
|
||||
## Four Seg geometry modes (2026-10-21)
|
||||
|
||||
- Scope: shared Runtime only; rectangle=1, enclosing circle=2 unchanged; mean diameter circle=3, robust fit circle=4 added. No consumer app or deployed Runtime was changed.
|
||||
- Robust fit is migrated without algorithm changes from the MatchVerify header used in the preceding 1981-target comparison. It chooses the largest external contour; failed fitting returns that contour's enclosing circle. `fit_status` reports success/fallback in a formerly reserved ABI word.
|
||||
- Windows `scripts/verify.ps1`: seven Release CTest contracts, install, dependency-manifest negative checks and independent installed C consumer pass. Synthetic tests cover non-square mean radius, origin offsets, detached noise, protrusions, elongated/point fallback and empty inputs.
|
||||
- `tests/verify_four_geometry.py`: installed public C API, four modes on rotor `10_origin.png` (22 targets) and stator `135_origin.png` (32 targets), using the preceding experiment directories as explicit `--reference` arguments. All masks match their recorded SHA256 values; rectangle/mean output exactly matches prior float results; robust fit maximum discrepancy is below 0.000061 pixel. Invalid modes 0/5 are rejected. Evidence: `build/four-geometry/four-mode-api.json`.
|
||||
- OpenCV runtime DLL deployment is included in build and installation; the initial missing-DLL test startup failure was fixed before the successful full rerun.
|
||||
- Linux runner accepts rect/circle/mean/fit and includes fit status in JSON. Linux/AArch64 rebuild and physical RK3588 validation are not performed in this batch; target-compatible OpenCV core/imgproc is required for that build.
|
||||
- Completion record and exact commands: `.codex/delivery-integrity.json`. Next consumer step is explicit selection of one of the four public modes after rebuilding against this library; it is outside this batch.
|
||||
|
||||
## Consumer four-mode and board mask update (2026-10-21)
|
||||
|
||||
- Runner now saves an unpadded P5 binary PGM for each Seg instance under `<output-stem>.masks/`, with relative maskPath in JSON and the unchanged mask metadata/original-image offsets. RGB/RGBA render overlays use the same class colors and alpha as Studio, before the requested geometry is drawn.
|
||||
- `yolo_runner_mask_output_test` adds padded-row/offset/hole preservation, alpha/unchanged-background and file-byte tests. Windows baseline now includes eight contracts.
|
||||
- Studio's matching ARM64 Runtime/runner build completed using the supplied Model Zoo OpenCV 3.4.5. Readelf confirms AArch64 and no external OpenCV shared-library dependency. Matching Windows/ARM manifests are checked during Studio publication. No real-device execution was performed.
|
||||
- Current consumer checkpoints: MatchVerify/SHARED_RUNTIME_HANDOFF.md and RK3588_AI_Platform/docs/codex/CURRENT_BATCH.md.
|
||||
|
||||
|
||||
## RGB instance-ID mask batch (2026-10-21)
|
||||
|
||||
Scope: additive public `yolo_masks_compose_rgb_ids`; preserve existing ABI 5
|
||||
structures and per-instance masks; no dilation, consumer migration, runner
|
||||
export change, deployment, commit, push, or physical-device validation.
|
||||
Pre-existing dirty library changes are retained; baseline copies for edited
|
||||
existing files are in `build/rgb-id-mask-baseline`.
|
||||
|
||||
Acceptance: exact background/one/two/three membership RGB rules; independent
|
||||
membership reconstruction over the full fixture canvas; holes/disconnected
|
||||
pixels, input origins and padded input/output rows; all 255 instance IDs;
|
||||
explicit four-overlap/count/binary/ABI/bounds/capacity errors with untouched
|
||||
caller output. `yolo_mask_ids_test` exercises the exported DLL function;
|
||||
`yolo_runtime_c_api_contract` calls it from C. The installed C consumer also
|
||||
calls the new symbol. Run `powershell -ExecutionPolicy Bypass -File scripts/verify.ps1`
|
||||
for the complete native build/CTest/install/manifest/consumer baseline.
|
||||
Actual completion and command evidence are in `.codex/delivery-integrity.json`.
|
||||
Next action after successful verification: consumers can pass their ordered
|
||||
mask views and original canvas size to the new API; existing applications stay
|
||||
on their current export path until separately migrated.
|
||||
|
||||
@ -17,6 +17,23 @@ extern "C" {
|
||||
typedef struct yolo_runtime yolo_runtime_t;
|
||||
typedef struct yolo_result_set yolo_result_set_t;
|
||||
|
||||
/* Compose mask_count binary (0/255) masks into an RGB888 canvas, without dilation.
|
||||
IDs are input index + 1 (1..255); origins locate masks on the canvas.
|
||||
Background=(0,0,0), single=(id,id,id), two=(id1,id2,id1),
|
||||
three=(id1,id2,id3), where id1 < id2 < id3. Remove zero/duplicate
|
||||
channels to decode membership. Holes and disconnected regions are preserved.
|
||||
Zero masks (masks may be NULL) produces a black canvas.
|
||||
Caller owns output; capacity must cover (height-1)*stride + width*3 bytes.
|
||||
Padding and the entire output on failure remain unchanged. Output must not
|
||||
overlap mask data. More than 255 masks or >3 memberships at any pixel returns
|
||||
UNSUPPORTED; malformed binary data/bounds return INVALID_ARGUMENT.
|
||||
This additive API does not change the existing ABI 5 structures/interfaces. */
|
||||
YOLO_RUNTIME_API yolo_status_t yolo_masks_compose_rgb_ids(
|
||||
const yolo_mask_view_v1_t* masks, uint32_t mask_count,
|
||||
int32_t width, int32_t height, unsigned char* output,
|
||||
int32_t row_stride_bytes, uint64_t output_capacity_bytes,
|
||||
yolo_error_info_v1_t* error);
|
||||
|
||||
YOLO_RUNTIME_API uint32_t yolo_runtime_get_abi_version(void);
|
||||
YOLO_RUNTIME_API yolo_status_t yolo_runtime_create(
|
||||
const char* config_path,
|
||||
|
||||
@ -51,10 +51,18 @@ typedef int32_t yolo_seg_geometry_mode_t;
|
||||
#define YOLO_SEG_GEOMETRY_NONE ((yolo_seg_geometry_mode_t)0)
|
||||
#define YOLO_SEG_GEOMETRY_MIN_AREA_RECT ((yolo_seg_geometry_mode_t)1)
|
||||
#define YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE ((yolo_seg_geometry_mode_t)2)
|
||||
#define YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE ((yolo_seg_geometry_mode_t)3)
|
||||
#define YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE ((yolo_seg_geometry_mode_t)4)
|
||||
|
||||
#define YOLO_SEG_FIT_NOT_REQUESTED 0u
|
||||
#define YOLO_SEG_FIT_SUCCEEDED 1u
|
||||
#define YOLO_SEG_FIT_ENCLOSING_FALLBACK 2u
|
||||
|
||||
#define YOLO_CAPABILITY_INFER_IMAGE (1u << 0)
|
||||
#define YOLO_CAPABILITY_SEG_MIN_AREA_RECT (1u << 1)
|
||||
#define YOLO_CAPABILITY_SEG_MIN_ENCLOSING_CIRCLE (1u << 2)
|
||||
#define YOLO_CAPABILITY_SEG_MEAN_DIAMETER_CIRCLE (1u << 3)
|
||||
#define YOLO_CAPABILITY_SEG_ROBUST_FIT_CIRCLE (1u << 4)
|
||||
|
||||
#define YOLO_PIXEL_FORMAT_BIT(format) (1u << (uint32_t)(format))
|
||||
#define YOLO_SEG_GEOMETRY_BIT(mode) (1u << (uint32_t)(mode))
|
||||
@ -149,7 +157,10 @@ typedef struct yolo_seg_geometry_v1 {
|
||||
float height;
|
||||
float angle_radians;
|
||||
float radius;
|
||||
uint32_t reserved[8];
|
||||
/* Uses the first formerly reserved word; ABI 5 size and prior fields unchanged.
|
||||
NOT_REQUESTED for modes 1-3; SUCCEEDED or ENCLOSING_FALLBACK for mode 4. */
|
||||
uint32_t fit_status;
|
||||
uint32_t reserved[7];
|
||||
} yolo_seg_geometry_v1_t;
|
||||
|
||||
typedef struct yolo_mask_view_v1 {
|
||||
|
||||
@ -18,7 +18,7 @@ find_package(YoloRuntime 5 CONFIG REQUIRED)
|
||||
add_executable(consumer main.c)
|
||||
target_link_libraries(consumer PRIVATE YoloRuntime::Runtime)
|
||||
'@ | Set-Content (Join-Path $consumer 'CMakeLists.txt') -Encoding ASCII
|
||||
'#include <yolo_runtime.h>' + "`n" + 'int main(void){return yolo_runtime_get_abi_version()==5 ? 0 : 1;}' | Set-Content (Join-Path $consumer 'main.c') -Encoding ASCII
|
||||
'#include <yolo_runtime.h>' + "`n" + 'int main(void){unsigned char rgb[3]={1,2,3}; if(yolo_masks_compose_rgb_ids(0,0,1,1,rgb,3,3,0)!=YOLO_STATUS_OK || rgb[0] || rgb[1] || rgb[2]) return 2; return yolo_runtime_get_abi_version()==5 ? 0 : 1;}' | Set-Content (Join-Path $consumer 'main.c') -Encoding ASCII
|
||||
$consumerBuild=Join-Path $root 'build/consumer'
|
||||
& cmd /d /c "call `"$vcvars`" && cmake -S `"$consumer`" -B `"$consumerBuild`" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=`"$install`" && cmake --build `"$consumerBuild`""
|
||||
if($LASTEXITCODE -ne 0){throw 'Installed package consumer build failed'}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#include "yolo_runtime.h"
|
||||
#include "runner_label_layout.h"
|
||||
#include "runner_mask_output.h"
|
||||
|
||||
#if defined(YOLO_RUNNER_IN_PROCESS)
|
||||
#include "common.h"
|
||||
@ -29,6 +30,7 @@ struct runner_result {
|
||||
yolo_rotated_rect_v1_t obb{};
|
||||
yolo_mask_view_v1_t mask{};
|
||||
yolo_seg_geometry_v1_t seg_geometry{};
|
||||
std::string mask_path;
|
||||
std::vector<yolo_keypoint_v1_t> keypoints;
|
||||
};
|
||||
|
||||
@ -160,19 +162,24 @@ std::string to_json(const runner_report& report, const std::string& model, const
|
||||
<< ", \"strideBytes\": " << result.mask.row_stride_bytes
|
||||
<< ", \"originX\": " << result.mask.origin_x
|
||||
<< ", \"originY\": " << result.mask.origin_y << '}';
|
||||
json << ", \"maskPath\": \"" << escape_json(image) << "\"";
|
||||
json << ", \"maskPath\": \"" << escape_json(result.mask_path) << "\"";
|
||||
const auto& geometry = result.seg_geometry;
|
||||
json << ", \"segGeometry\": {\"type\": \""
|
||||
<< (geometry.type == YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE
|
||||
? "minimumEnclosingCircle" : "minimumAreaRectangle")
|
||||
? "minimumEnclosingCircle" : (geometry.type == YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE
|
||||
? "meanDiameterCircle" : (geometry.type == YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE
|
||||
? "robustFitCircle" : "minimumAreaRectangle")))
|
||||
<< "\", \"centerX\": " << geometry.center_x
|
||||
<< ", \"centerY\": " << geometry.center_y;
|
||||
if (geometry.type == YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE)
|
||||
if (geometry.type != YOLO_SEG_GEOMETRY_MIN_AREA_RECT)
|
||||
json << ", \"radius\": " << geometry.radius;
|
||||
else
|
||||
json << ", \"width\": " << geometry.width
|
||||
<< ", \"height\": " << geometry.height
|
||||
<< ", \"angle\": " << geometry.angle_radians;
|
||||
if (geometry.type == YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE)
|
||||
json << ", \"fitStatus\": \"" << (geometry.fit_status == YOLO_SEG_FIT_SUCCEEDED
|
||||
? "fitted" : "enclosingFallback") << '"';
|
||||
json << '}';
|
||||
}
|
||||
if (result.common.task == YOLO_TASK_POSE) {
|
||||
@ -216,6 +223,15 @@ bool make_image_view(const image_buffer_t& source, yolo_image_view_v1_t& view) {
|
||||
|
||||
void render_results(image_buffer_t& image, const runner_report& report) {
|
||||
char text[128];
|
||||
for (const auto& result : report.results) {
|
||||
if (result.common.task != YOLO_TASK_SEG) continue;
|
||||
const int channels = image.format == IMAGE_FORMAT_RGBA8888 ? 4 : 3;
|
||||
const int stride = (image.width_stride > image.width ? image.width_stride : image.width)*channels;
|
||||
if (image.format != IMAGE_FORMAT_RGB888 && image.format != IMAGE_FORMAT_RGBA8888)
|
||||
throw std::runtime_error("Seg mask overlay requires RGB or RGBA source");
|
||||
if (!runner_mask_output::blend(image.virt_addr,image.width,image.height,stride,channels,result.mask,result.common.class_id))
|
||||
throw std::runtime_error("Invalid Seg mask overlay coordinates");
|
||||
}
|
||||
for (size_t index = 0; index < report.results.size(); ++index) {
|
||||
const auto& result = report.results[index];
|
||||
const int x = std::max(0, static_cast<int>(result.bbox.x));
|
||||
@ -224,7 +240,7 @@ void render_results(image_buffer_t& image, const runner_report& report) {
|
||||
const int height = std::max(1, static_cast<int>(result.bbox.height));
|
||||
if (result.common.task == YOLO_TASK_SEG) {
|
||||
const auto& geometry = result.seg_geometry;
|
||||
if (geometry.type == YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE)
|
||||
if (geometry.type != YOLO_SEG_GEOMETRY_MIN_AREA_RECT)
|
||||
draw_circle(&image, static_cast<int>(geometry.center_x), static_cast<int>(geometry.center_y),
|
||||
std::max(1, static_cast<int>(std::round(geometry.radius))), COLOR_GREEN, 3);
|
||||
else
|
||||
@ -264,9 +280,9 @@ int main(int argc, char** argv) {
|
||||
else if (argument == "--json") json_path = argv[index + 1];
|
||||
else if (argument == "--seg-geometry") geometry_name = argv[index + 1];
|
||||
}
|
||||
if (config.empty() || image.empty() || (geometry_name != "rect" && geometry_name != "circle")) {
|
||||
if (config.empty() || image.empty() || (geometry_name != "rect" && geometry_name != "circle" && geometry_name != "mean" && geometry_name != "fit")) {
|
||||
std::cerr << "Usage: rk3588_yolo_runner --config model.yaml --image image.png "
|
||||
"[--output out.png] [--json result.json] [--seg-geometry rect|circle]\n";
|
||||
"[--output out.png] [--json result.json] [--seg-geometry rect|circle|mean|fit]\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
@ -304,7 +320,8 @@ int main(int argc, char** argv) {
|
||||
options.seg_geometry_mode = info.task == YOLO_TASK_SEG
|
||||
? (geometry_name == "circle"
|
||||
? YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE
|
||||
: YOLO_SEG_GEOMETRY_MIN_AREA_RECT)
|
||||
: (geometry_name == "mean" ? YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE
|
||||
: (geometry_name == "fit" ? YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE : YOLO_SEG_GEOMETRY_MIN_AREA_RECT)))
|
||||
: YOLO_SEG_GEOMETRY_NONE;
|
||||
yolo_result_set_t* result_set = nullptr;
|
||||
initialize(error, YOLO_ERROR_INFO_VERSION);
|
||||
@ -319,7 +336,25 @@ int main(int argc, char** argv) {
|
||||
yolo_runtime_destroy(runtime);
|
||||
return 4;
|
||||
}
|
||||
try {
|
||||
const auto directory = fs::path(json_path).parent_path() / (fs::path(output).stem().string()+".masks");
|
||||
for (size_t i=0;i<report.results.size();++i) {
|
||||
auto& result=report.results[i];
|
||||
if(result.common.task!=YOLO_TASK_SEG) continue;
|
||||
fs::create_directories(directory);
|
||||
const std::string filename="mask_"+std::to_string(i+1)+".pgm";
|
||||
if(!runner_mask_output::save(directory/filename,result.mask))
|
||||
throw std::runtime_error("Cannot save instance mask");
|
||||
result.mask_path=(directory.filename()/filename).generic_string();
|
||||
}
|
||||
render_results(source, report);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << e.what() << '\n';
|
||||
yolo_result_set_release(result_set);
|
||||
std::free(source.virt_addr);
|
||||
yolo_runtime_destroy(runtime);
|
||||
return 5;
|
||||
}
|
||||
if (write_image(output.c_str(), &source) < 0) {
|
||||
yolo_result_set_release(result_set);
|
||||
std::free(source.virt_addr);
|
||||
|
||||
31
YoloRuntime/src/runner_mask_output.h
Normal file
31
YoloRuntime/src/runner_mask_output.h
Normal file
@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "yolo_runtime_types.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace runner_mask_output {
|
||||
inline bool save(const std::filesystem::path& path, const yolo_mask_view_v1_t& mask) {
|
||||
if (!mask.data || mask.width <= 0 || mask.height <= 0 || mask.row_stride_bytes < mask.width) return false;
|
||||
std::ofstream file(path, std::ios::binary);
|
||||
file << "P5\n" << mask.width << ' ' << mask.height << "\n255\n";
|
||||
for (int y=0; y<mask.height; ++y)
|
||||
file.write(reinterpret_cast<const char*>(mask.data + size_t(y)*mask.row_stride_bytes), mask.width);
|
||||
return bool(file);
|
||||
}
|
||||
inline bool blend(unsigned char* image, int width, int height, int stride, int channels,
|
||||
const yolo_mask_view_v1_t& mask, int class_id) {
|
||||
if (!image || channels < 3 || channels > 4 || width <= 0 || height <= 0 || stride < width*channels ||
|
||||
!mask.data || mask.width <= 0 || mask.height <= 0 || mask.row_stride_bytes < mask.width ||
|
||||
mask.origin_x < 0 || mask.origin_y < 0 || mask.width > width-mask.origin_x || mask.height > height-mask.origin_y) return false;
|
||||
const unsigned char palette[][3]={{0,220,120},{0,170,255},{255,120,0},{220,60,220}};
|
||||
const auto& color=palette[(class_id%4+4)%4];
|
||||
for(int y=0;y<mask.height;++y) for(int x=0;x<mask.width;++x) {
|
||||
if(!mask.data[size_t(y)*mask.row_stride_bytes+x]) continue;
|
||||
auto* pixel=image+size_t(y+mask.origin_y)*stride+(x+mask.origin_x)*channels;
|
||||
for(int c=0;c<3;++c) pixel[c]=static_cast<unsigned char>((pixel[c]*155+color[c]*100+127)/255);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
65
YoloRuntime/src/yolo_mask_ids.cpp
Normal file
65
YoloRuntime/src/yolo_mask_ids.cpp
Normal file
@ -0,0 +1,65 @@
|
||||
#include "yolo_runtime.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
extern "C" yolo_status_t yolo_masks_compose_rgb_ids(
|
||||
const yolo_mask_view_v1_t* masks, uint32_t count,
|
||||
int32_t width, int32_t height, unsigned char* output,
|
||||
int32_t stride, uint64_t capacity, yolo_error_info_v1_t* error) {
|
||||
if (error && (error->struct_size < sizeof(*error) || error->version != YOLO_ERROR_INFO_VERSION))
|
||||
return YOLO_STATUS_ABI_MISMATCH;
|
||||
const auto finish = [error](yolo_status_t status, const char* message) {
|
||||
if (error) {
|
||||
error->status = status;
|
||||
std::snprintf(error->message, sizeof(error->message), "%s", message);
|
||||
}
|
||||
return status;
|
||||
};
|
||||
if (count > 255) return finish(YOLO_STATUS_UNSUPPORTED, "RGB instance IDs support at most 255 masks");
|
||||
if (!output || (count && !masks) || width <= 0 || height <= 0 || stride <= 0 ||
|
||||
uint64_t(width)*3 > uint64_t(stride))
|
||||
return finish(YOLO_STATUS_INVALID_ARGUMENT, "Invalid RGB canvas or mask array");
|
||||
const uint64_t required = uint64_t(height-1)*uint64_t(stride) + uint64_t(width)*3;
|
||||
const uint64_t packed_size = uint64_t(width)*uint64_t(height)*3;
|
||||
if (required > capacity || required > std::numeric_limits<size_t>::max() ||
|
||||
packed_size > std::numeric_limits<size_t>::max())
|
||||
return finish(YOLO_STATUS_BUFFER_TOO_SMALL, "RGB output capacity is too small");
|
||||
for (uint32_t i=0; i<count; ++i) {
|
||||
const auto& m = masks[i];
|
||||
if (m.struct_size < sizeof(m) || m.version != YOLO_MASK_VIEW_VERSION)
|
||||
return finish(YOLO_STATUS_ABI_MISMATCH, "Incompatible mask view");
|
||||
if (!m.data || m.width <= 0 || m.height <= 0 || m.row_stride_bytes < m.width ||
|
||||
m.origin_x < 0 || m.origin_y < 0 || m.origin_x > width || m.origin_y > height ||
|
||||
m.width > width-m.origin_x || m.height > height-m.origin_y ||
|
||||
uint64_t(m.height-1)*uint64_t(m.row_stride_bytes)+uint64_t(m.width) > std::numeric_limits<size_t>::max())
|
||||
return finish(YOLO_STATUS_INVALID_ARGUMENT, "Invalid mask bounds or row stride");
|
||||
}
|
||||
try {
|
||||
std::vector<unsigned char> rgb(static_cast<size_t>(packed_size), 0);
|
||||
for (uint32_t i=0; i<count; ++i) {
|
||||
const auto& m = masks[i];
|
||||
const auto id = static_cast<unsigned char>(i+1);
|
||||
for (int32_t y=0; y<m.height; ++y) {
|
||||
const auto* row = m.data + size_t(y)*m.row_stride_bytes;
|
||||
for (int32_t x=0; x<m.width; ++x) {
|
||||
if (!row[x]) continue;
|
||||
if (row[x] != 255)
|
||||
return finish(YOLO_STATUS_INVALID_ARGUMENT, "Mask pixels must be 0 or 255");
|
||||
auto* p = rgb.data() + (size_t(y+m.origin_y)*width+x+m.origin_x)*3;
|
||||
if (!p[0]) p[0]=p[1]=p[2]=id;
|
||||
else if (p[0]==p[1] && p[1]==p[2]) p[1]=id;
|
||||
else if (p[0]==p[2]) p[2]=id;
|
||||
else return finish(YOLO_STATUS_UNSUPPORTED, "More than three masks overlap at one pixel");
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int32_t y=0; y<height; ++y)
|
||||
std::memcpy(output+size_t(y)*stride, rgb.data()+size_t(y)*width*3, size_t(width)*3);
|
||||
return finish(YOLO_STATUS_OK, "");
|
||||
} catch (const std::exception&) {
|
||||
return finish(YOLO_STATUS_BACKEND_FAILED, "Cannot allocate RGB mask canvas");
|
||||
}
|
||||
}
|
||||
174
YoloRuntime/src/yolo_robust_circle.h
Normal file
174
YoloRuntime/src/yolo_robust_circle.h
Normal file
@ -0,0 +1,174 @@
|
||||
#pragma once
|
||||
|
||||
// 【学习入口:根据分割 mask 的外轮廓拟合圆】
|
||||
// 输入不是灰度图,而是一圈按边界顺序排列的轮廓点;输出为圆心、半径和质量标志。
|
||||
// 计算流程:最小外接圆备用 -> 退化检查 -> 按弧长均匀采样 -> 初始圆 -> Tukey IRLS -> 质量门限。
|
||||
// “最小外接圆”要求包住所有轮廓点;“拟合圆”追求大多数边界点到圆周的距离小。
|
||||
// 所以少量突出点会推大外接圆,本算法通过降低异常点权重来抑制其对拟合的影响。
|
||||
// Shared Runtime reports fitted=false as an explicit minimum-enclosing fallback.
|
||||
|
||||
#include <opencv2/imgproc.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace yolo_robust_circle {
|
||||
|
||||
struct Circle
|
||||
{
|
||||
// center.x/center.y 与 radius 的单位都是原图像素;double 保留亚像素精度。
|
||||
cv::Point2d center;
|
||||
double radius = 0.0;
|
||||
bool fitted = false;
|
||||
};
|
||||
|
||||
// 几何辅助函数:求非空数组的中位数,按值接收以免重排调用方的数据。
|
||||
// nth_element 只把中间元素放到正确位置,无需对整个数组完整排序。
|
||||
// 偶数长度时,右中位数为 *middle,左中位数为左半区最大值,二者取平均。
|
||||
// 这里统计的是点到圆心的距离/拟合残差,不是像素灰度的中位数。
|
||||
inline double Median(std::vector<double> values)
|
||||
{
|
||||
const auto middle = values.begin() + values.size() / 2;
|
||||
std::nth_element(values.begin(), middle, values.end());
|
||||
const double upper = *middle;
|
||||
return values.size() % 2 ? upper
|
||||
: (upper + *std::max_element(values.begin(), middle)) * 0.5;
|
||||
}
|
||||
|
||||
// Uniform arc-length sampling prevents CHAIN_APPROX_SIMPLE vertices from
|
||||
// overweighting pixel staircases and underweighting long straight sections.
|
||||
inline std::vector<cv::Point2d> SampleBoundary(const std::vector<cv::Point>& contour)
|
||||
{
|
||||
// 闭合轮廓的第 i 段连接 contour[i] 与下一点,最后一段回到第 0 点。
|
||||
// lengths[i] 表示到第 i 个顶点的累计弧长,lengths.back() 为总周长。
|
||||
std::vector<double> lengths(contour.size() + 1, 0.0);
|
||||
for (size_t i = 0; i < contour.size(); ++i)
|
||||
lengths[i + 1] = lengths[i] + cv::norm(contour[(i + 1) % contour.size()] - contour[i]);
|
||||
if (lengths.back() <= 0.0) return {};
|
||||
constexpr int count = 128;
|
||||
// 固定采样 128 点,让每段边界按长度贡献,而不是按原始轮廓顶点密度贡献。
|
||||
// 该数值是当前实现的固定参数,不代表相机分辨率或目标数量。
|
||||
std::vector<cv::Point2d> points;
|
||||
points.reserve(count);
|
||||
size_t edge = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const double distance = lengths.back() * i / count;
|
||||
// 沿周长依次定位采样点在哪条线段上。edge 只向前走,无需每次重新搜索全部线段。
|
||||
while (edge + 1 < contour.size() && lengths[edge + 1] <= distance) ++edge;
|
||||
const double span = lengths[edge + 1] - lengths[edge];
|
||||
const double t = span > 0.0 ? (distance - lengths[edge]) / span : 0.0;
|
||||
// 线性插值 P=A+t(B-A),得到亚像素坐标;span=0 时避免除零。
|
||||
const cv::Point2d a = contour[edge], b = contour[(edge + 1) % contour.size()];
|
||||
points.push_back(a + (b - a) * t);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// Fit the boundary, not a prescribed radius reduction. Tukey IRLS suppresses
|
||||
// minority protrusions. Noncircular/degenerate inputs retain the enclosing
|
||||
// circle rather than inventing a confident measurement of an arbitrary shape.
|
||||
inline Circle Fit(const std::vector<cv::Point>& contour)
|
||||
{
|
||||
// 1. 准备失败回退。所有后续门限失败都返回 fallback,其 fitted 保持默认 false。
|
||||
Circle fallback;
|
||||
if (contour.empty()) return fallback;
|
||||
cv::Point2f enclosingCenter;
|
||||
float enclosingRadius = 0.0f;
|
||||
cv::minEnclosingCircle(contour, enclosingCenter, enclosingRadius);
|
||||
fallback.center = enclosingCenter;
|
||||
fallback.radius = enclosingRadius;
|
||||
// 轮廓点太少或面积不足时,圆参数不够稳定;面积单位为像素平方。
|
||||
const double area = std::abs(cv::contourArea(contour));
|
||||
if (contour.size() < 5 || area < 16.0) return fallback;
|
||||
const auto bounds = cv::minAreaRect(contour);
|
||||
// minAreaRect 是可旋转的最小面积矩形,不是这里最终要拟合的圆。
|
||||
// 长短边比超过 1.5 时按明显狭长轮廓拒绝;这是粗筛,不是椭圆拟合。
|
||||
const double longSide = std::max(bounds.size.width, bounds.size.height);
|
||||
const double shortSide = std::min(bounds.size.width, bounds.size.height);
|
||||
if (shortSide <= 0.0 || longSide > 1.5 * shortSide) return fallback;
|
||||
const auto points = SampleBoundary(contour);
|
||||
if (points.empty()) return fallback;
|
||||
const auto moments = cv::moments(contour);
|
||||
// 2. 用轮廓面积矩初始化圆心:cx=m10/m00,cy=m01/m00。
|
||||
// 半径初值取 128 个采样点到这个圆心的距离中位数,以减弱少数突出点的影响。
|
||||
if (std::abs(moments.m00) < 1e-9) return fallback;
|
||||
cv::Point2d center(moments.m10 / moments.m00, moments.m01 / moments.m00);
|
||||
std::vector<double> distances;
|
||||
for (const auto& point : points) distances.push_back(cv::norm(point - center));
|
||||
double radius = Median(distances);
|
||||
for (int iteration = 0; iteration < 30; ++iteration) {
|
||||
// 3. IRLS(迭代重加权最小二乘):每轮重新计算残差与权重,再求圆参数的小修正。
|
||||
// 参数 theta=(cx,cy,R),第 i 点残差 e_i=||P_i-C||-R;正值在圆外,负值在圆内。
|
||||
std::vector<double> residuals;
|
||||
for (const auto& point : points) residuals.push_back(cv::norm(point - center) - radius);
|
||||
const double median = Median(residuals);
|
||||
std::vector<double> deviations;
|
||||
for (double residual : residuals) deviations.push_back(std::abs(residual - median));
|
||||
// MAD=median(|e_i-median(e)|) 估计残差尺度;1.4826 为尺度换算因子。
|
||||
// cutoff 由该尺度和 Tukey 系数 4.685 得到,并设置 0.75 像素下限,避免尺度过小。
|
||||
// 下方 u 使用 e_i/cutoff(未减去 median);median 在这里用于估计尺度。
|
||||
const double cutoff = std::max(0.75, 4.685 * 1.4826 * Median(deviations));
|
||||
cv::Matx33d normal = cv::Matx33d::zeros();
|
||||
cv::Vec3d gradient(0, 0, 0);
|
||||
int contributors = 0;
|
||||
for (size_t i = 0; i < points.size(); ++i) {
|
||||
const cv::Point2d delta = center - points[i];
|
||||
const double distance = cv::norm(delta);
|
||||
const double u = residuals[i] / cutoff;
|
||||
// Tukey 权重:|u|>=1 的点本轮权重为 0;其余点 w=(1-u^2)^2。
|
||||
// 残差越小,权重越接近 1;这样少数异常边界点不会主导拟合。
|
||||
if (distance <= 1e-9 || std::abs(u) >= 1.0) continue;
|
||||
const double weight = (1.0 - u * u) * (1.0 - u * u);
|
||||
const cv::Vec3d jacobian(delta.x / distance, delta.y / distance, -1.0);
|
||||
// 残差对三个参数的导数 J=[(cx-px)/d, (cy-py)/d, -1]。
|
||||
// 累加 H=Σw*J*J^T、g=Σw*e*J,随后解 H*step=-g,求本轮参数增量。
|
||||
normal += weight * (jacobian * jacobian.t());
|
||||
gradient += weight * residuals[i] * jacobian;
|
||||
++contributors;
|
||||
}
|
||||
cv::Vec3d step;
|
||||
// 至少需要 16 个参与点;用 SVD 解 3×3 方程,求解失败或出现非有限数则回退。
|
||||
if (contributors < 16 || !cv::solve(normal, -gradient, step, cv::DECOMP_SVD)) return fallback;
|
||||
if (!std::isfinite(step[0]) || !std::isfinite(step[1]) || !std::isfinite(step[2])) return fallback;
|
||||
const double stepSize = cv::norm(step);
|
||||
// 一次修正的总长度最多为当前半径的 25%,避免初值较差时一步跳得太远。
|
||||
if (stepSize > radius * 0.25) step *= radius * 0.25 / stepSize;
|
||||
center += cv::Point2d(step[0], step[1]);
|
||||
radius += step[2];
|
||||
if (!std::isfinite(radius) || radius <= 1.0) return fallback;
|
||||
if (cv::norm(step) < 1e-5) break;
|
||||
}
|
||||
// Require support around the entire circle and a predominantly circular
|
||||
// boundary. A short arc or elongated object must not masquerade as a fit.
|
||||
std::array<bool, 12> sectors{};
|
||||
// 4. 优化能算出一个圆,不等于轮廓确实近似圆。以下门限联合决定 fitted=true。
|
||||
// 把方向分为 12 个扇区,仅由误差 <= max(1px,0.10R) 的点提供扇区支持。
|
||||
// 至少 10 个扇区有支持,用于抑制“只有短弧却拟合出大圆”的情况。
|
||||
std::vector<double> errors;
|
||||
for (const auto& point : points) {
|
||||
const auto delta = point - center;
|
||||
const double error = std::abs(cv::norm(delta) - radius);
|
||||
errors.push_back(error);
|
||||
if (error <= std::max(1.0, radius * 0.10)) {
|
||||
const double angle = std::atan2(delta.y, delta.x) + CV_PI;
|
||||
// atan2 角度从 [-π,π] 平移到 [0,2π];min(11,...) 处理末端落到索引 12 的边界。
|
||||
const int sector = std::min(11, static_cast<int>(angle * 12.0 / (2.0 * CV_PI)));
|
||||
sectors[sector] = true;
|
||||
}
|
||||
}
|
||||
std::sort(errors.begin(), errors.end());
|
||||
// 这里只对 128 个几何误差排序。约 80% 分位误差要求 <= max(1px,0.10R),
|
||||
// 同时要求:轮廓面积/圆面积在 [0.65,1.35]、圆心位于轮廓内或边上、
|
||||
// 拟合半径不超过备用外接圆半径的 1.10 倍。任一失败都返回 fitted=false。
|
||||
// 面积使用输入的外轮廓面积;内部孔洞不参与这项外形检查。
|
||||
const double areaRatio = area / (CV_PI * radius * radius);
|
||||
if (errors[static_cast<size_t>(0.8 * (errors.size() - 1))] > std::max(1.0, radius * 0.10)
|
||||
|| std::count(sectors.begin(), sectors.end(), true) < 10
|
||||
|| areaRatio < 0.65 || areaRatio > 1.35
|
||||
|| cv::pointPolygonTest(contour, center, false) < 0.0
|
||||
|| radius > fallback.radius * 1.10) return fallback;
|
||||
return {center, radius, true};
|
||||
}
|
||||
|
||||
} // namespace yolo_robust_circle
|
||||
@ -158,7 +158,8 @@ static std::unique_ptr<yolo_result_set> copy_public_results(
|
||||
item.mask_data + static_cast<size_t>(row) * item.mask_stride_bytes,
|
||||
static_cast<size_t>(item.mask_width));
|
||||
}
|
||||
const auto boundary = yolo_seg_geometry::mask_boundary(
|
||||
const auto boundary = geometry_mode == YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE
|
||||
? std::vector<yolo_seg_geometry::Point>{} : yolo_seg_geometry::mask_boundary(
|
||||
result.mask.data(),
|
||||
result.mask_width,
|
||||
result.mask_height,
|
||||
@ -168,12 +169,20 @@ static std::unique_ptr<yolo_result_set> copy_public_results(
|
||||
result.seg_geometry.struct_size = sizeof(result.seg_geometry);
|
||||
result.seg_geometry.version = YOLO_SEG_GEOMETRY_VERSION;
|
||||
result.seg_geometry.type = geometry_mode;
|
||||
if (geometry_mode == YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE) {
|
||||
const auto circle = yolo_seg_geometry::minimum_enclosing_circle(boundary);
|
||||
if (!circle.valid) throw std::runtime_error("cannot calculate Seg minimum enclosing circle");
|
||||
if (geometry_mode != YOLO_SEG_GEOMETRY_MIN_AREA_RECT) {
|
||||
const auto circle = geometry_mode == YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE
|
||||
? yolo_seg_geometry::robust_fit_circle(result.mask.data(), result.mask_width,
|
||||
result.mask_height, result.mask_stride_bytes, result.mask_origin_x, result.mask_origin_y)
|
||||
: (geometry_mode == YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE
|
||||
? yolo_seg_geometry::mean_diameter_circle(boundary)
|
||||
: yolo_seg_geometry::minimum_enclosing_circle(boundary));
|
||||
if (!circle.valid) throw std::runtime_error("cannot calculate Seg circle");
|
||||
result.seg_geometry.center_x = static_cast<float>(circle.center_x);
|
||||
result.seg_geometry.center_y = static_cast<float>(circle.center_y);
|
||||
result.seg_geometry.radius = static_cast<float>(circle.radius);
|
||||
if (geometry_mode == YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE)
|
||||
result.seg_geometry.fit_status = circle.fitted
|
||||
? YOLO_SEG_FIT_SUCCEEDED : YOLO_SEG_FIT_ENCLOSING_FALLBACK;
|
||||
} else {
|
||||
const auto rectangle = yolo_seg_geometry::minimum_area_rect(boundary);
|
||||
if (!rectangle.valid) throw std::runtime_error("cannot calculate Seg minimum-area rectangle");
|
||||
@ -765,10 +774,14 @@ extern "C" yolo_status_t yolo_runtime_get_info(
|
||||
info->capabilities = YOLO_CAPABILITY_INFER_IMAGE;
|
||||
if (runtime->task == YOLO_TASK_SEG) {
|
||||
info->capabilities |= YOLO_CAPABILITY_SEG_MIN_AREA_RECT |
|
||||
YOLO_CAPABILITY_SEG_MIN_ENCLOSING_CIRCLE;
|
||||
YOLO_CAPABILITY_SEG_MIN_ENCLOSING_CIRCLE |
|
||||
YOLO_CAPABILITY_SEG_MEAN_DIAMETER_CIRCLE |
|
||||
YOLO_CAPABILITY_SEG_ROBUST_FIT_CIRCLE;
|
||||
info->supported_seg_geometry_modes =
|
||||
YOLO_SEG_GEOMETRY_BIT(YOLO_SEG_GEOMETRY_MIN_AREA_RECT) |
|
||||
YOLO_SEG_GEOMETRY_BIT(YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE);
|
||||
YOLO_SEG_GEOMETRY_BIT(YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE) |
|
||||
YOLO_SEG_GEOMETRY_BIT(YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE) |
|
||||
YOLO_SEG_GEOMETRY_BIT(YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE);
|
||||
}
|
||||
info->supported_pixel_formats =
|
||||
YOLO_PIXEL_FORMAT_BIT(YOLO_PIXEL_FORMAT_GRAY8) |
|
||||
@ -803,8 +816,10 @@ extern "C" yolo_status_t yolo_runtime_infer_image(
|
||||
}
|
||||
if (runtime->task == YOLO_TASK_SEG) {
|
||||
if (options->seg_geometry_mode != YOLO_SEG_GEOMETRY_MIN_AREA_RECT &&
|
||||
options->seg_geometry_mode != YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE) {
|
||||
set_error(error, YOLO_STATUS_INVALID_ARGUMENT, "Seg requires minimum-area rectangle or minimum enclosing circle");
|
||||
options->seg_geometry_mode != YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE &&
|
||||
options->seg_geometry_mode != YOLO_SEG_GEOMETRY_MEAN_DIAMETER_CIRCLE &&
|
||||
options->seg_geometry_mode != YOLO_SEG_GEOMETRY_ROBUST_FIT_CIRCLE) {
|
||||
set_error(error, YOLO_STATUS_INVALID_ARGUMENT, "Seg requires rectangle, enclosing circle, mean circle or robust fit circle");
|
||||
return YOLO_STATUS_INVALID_ARGUMENT;
|
||||
}
|
||||
} else if (options->seg_geometry_mode != YOLO_SEG_GEOMETRY_NONE) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "yolo_seg_geometry.h"
|
||||
#include "yolo_robust_circle.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@ -201,4 +202,24 @@ RotatedRect minimum_area_rect(const std::vector<Point>& input) {
|
||||
return best;
|
||||
}
|
||||
|
||||
Circle mean_diameter_circle(const std::vector<Point>& points) {
|
||||
const auto rect = minimum_area_rect(points);
|
||||
return {rect.center_x, rect.center_y, (rect.width + rect.height) * 0.25, rect.valid};
|
||||
}
|
||||
|
||||
Circle robust_fit_circle(const unsigned char* mask, int width, int height,
|
||||
int stride, int origin_x, int origin_y) {
|
||||
if (!mask || width <= 0 || height <= 0 || stride < width) return {};
|
||||
cv::Mat view(height, width, CV_8U, const_cast<unsigned char*>(mask), stride);
|
||||
std::vector<std::vector<cv::Point>> contours;
|
||||
cv::findContours(view.clone(), contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
|
||||
if (contours.empty()) return {};
|
||||
auto best = std::max_element(contours.begin(), contours.end(), [](const auto& a, const auto& b) {
|
||||
return cv::contourArea(a) < cv::contourArea(b);
|
||||
});
|
||||
for (auto& point : *best) point += cv::Point(origin_x, origin_y);
|
||||
const auto circle = yolo_robust_circle::Fit(*best);
|
||||
return {circle.center.x, circle.center.y, circle.radius, true, circle.fitted};
|
||||
}
|
||||
|
||||
} // namespace yolo_seg_geometry
|
||||
|
||||
@ -14,6 +14,7 @@ struct Circle {
|
||||
double center_y = 0.0;
|
||||
double radius = 0.0;
|
||||
bool valid = false;
|
||||
bool fitted = false;
|
||||
};
|
||||
|
||||
struct RotatedRect {
|
||||
@ -35,5 +36,9 @@ std::vector<Point> mask_boundary(
|
||||
|
||||
Circle minimum_enclosing_circle(const std::vector<Point>& points);
|
||||
RotatedRect minimum_area_rect(const std::vector<Point>& points);
|
||||
Circle mean_diameter_circle(const std::vector<Point>& points);
|
||||
// Largest external contour; returns its enclosing circle when robust fitting fails.
|
||||
Circle robust_fit_circle(const unsigned char* mask, int width, int height,
|
||||
int stride, int origin_x, int origin_y);
|
||||
|
||||
} // namespace yolo_seg_geometry
|
||||
|
||||
@ -23,6 +23,11 @@ _Static_assert(YOLO_RUNTIME_ABI_VERSION == 5u, "Runtime ABI version changed");
|
||||
_Static_assert(YOLO_IMAGE_VIEW_ABI_VERSION == 1u, "Image view version changed");
|
||||
_Static_assert(YOLO_INFER_OPTIONS_VERSION == 1u, "Infer-options version changed");
|
||||
_Static_assert(YOLO_LABEL_LENGTH == 64u, "Label length changed");
|
||||
_Static_assert(YOLO_SEG_GEOMETRY_MIN_AREA_RECT == 1, "Rectangle mode changed");
|
||||
_Static_assert(YOLO_SEG_GEOMETRY_MIN_ENCLOSING_CIRCLE == 2, "Enclosing mode changed");
|
||||
_Static_assert(sizeof(yolo_seg_geometry_v1_t) == 68, "Geometry ABI 5 size changed");
|
||||
_Static_assert(offsetof(yolo_seg_geometry_v1_t, radius) == 32, "Radius ABI offset changed");
|
||||
_Static_assert(offsetof(yolo_seg_geometry_v1_t, fit_status) == 36, "Fit status must use reserved word");
|
||||
|
||||
_Static_assert(YOLO_STATUS_OK == 0, "OK status changed");
|
||||
_Static_assert(YOLO_STATUS_INVALID_ARGUMENT == -1, "Invalid-argument status changed");
|
||||
@ -51,6 +56,9 @@ ASSERT_FIELD_ORDER(yolo_mask_view_v1_t, data, width);
|
||||
ASSERT_FIELD_ORDER(yolo_mask_view_v1_t, row_stride_bytes, origin_x);
|
||||
|
||||
int main(void) {
|
||||
unsigned char rgb_ids[3] = {1, 2, 3};
|
||||
if (yolo_masks_compose_rgb_ids(NULL, 0, 1, 1, rgb_ids, 3, sizeof(rgb_ids), NULL) != YOLO_STATUS_OK ||
|
||||
rgb_ids[0] || rgb_ids[1] || rgb_ids[2]) return 2;
|
||||
yolo_image_view_v1_t image = {0};
|
||||
yolo_infer_options_v1_t options = {0};
|
||||
yolo_error_info_v1_t error = {0};
|
||||
|
||||
74
YoloRuntime/tests/mask_ids_test.cpp
Normal file
74
YoloRuntime/tests/mask_ids_test.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
#include "yolo_runtime.h"
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
static yolo_mask_view_v1_t view(const unsigned char* data, int w, int h, int stride, int x=0, int y=0) {
|
||||
yolo_mask_view_v1_t m{};
|
||||
m.struct_size=sizeof(m); m.version=YOLO_MASK_VIEW_VERSION;
|
||||
m.data=data; m.width=w; m.height=h; m.row_stride_bytes=stride; m.origin_x=x; m.origin_y=y;
|
||||
return m;
|
||||
}
|
||||
int main() {
|
||||
// Input padding contains nonbinary sentinels; holes and disconnected pixels remain intact.
|
||||
const unsigned char a[]={255,0,255,99, 0,255,0,99, 255,0,255,99};
|
||||
const unsigned char b[]={255,255,0,99, 0,255,0,99, 255,0,0,99};
|
||||
const unsigned char c[]={255,0,0,99, 0,255,0,99, 0,0,255,99};
|
||||
std::vector<yolo_mask_view_v1_t> masks={view(a,3,3,4,1,1),view(b,3,3,4,1,1),view(c,3,3,4,1,1)};
|
||||
const int w=5,h=5,stride=17;
|
||||
std::vector<unsigned char> rgb(stride*h,77);
|
||||
yolo_error_info_v1_t error{};error.struct_size=sizeof(error);error.version=YOLO_ERROR_INFO_VERSION;
|
||||
const auto compose=[&](uint32_t n) {
|
||||
return yolo_masks_compose_rgb_ids(masks.data(),n,w,h,rgb.data(),stride,rgb.size(),&error);
|
||||
};
|
||||
// Independently reconstruct each membership from the channels and compare every input pixel.
|
||||
for(uint32_t n=1;n<=3;++n) {
|
||||
assert(compose(n)==YOLO_STATUS_OK);
|
||||
for(int y=0;y<h;++y) for(int x=0;x<w;++x) {
|
||||
std::set<int> expected,actual;
|
||||
for(uint32_t i=0;i<n;++i) {
|
||||
const auto& m=masks[i];
|
||||
if(x>=m.origin_x && x<m.origin_x+m.width && y>=m.origin_y && y<m.origin_y+m.height &&
|
||||
m.data[(y-m.origin_y)*m.row_stride_bytes+x-m.origin_x]) expected.insert(i+1);
|
||||
}
|
||||
const auto* p=&rgb[y*stride+x*3];
|
||||
for(int k=0;k<3;++k) if(p[k]) actual.insert(p[k]);
|
||||
assert(actual==expected);
|
||||
if(expected.empty()) assert(p[0]==0 && p[1]==0 && p[2]==0);
|
||||
else if(expected.size()==1) assert(p[0]==*expected.begin() && p[0]==p[1] && p[1]==p[2]);
|
||||
else if(expected.size()==2) assert(p[0]==*expected.begin() && p[1]==*expected.rbegin() && p[2]==p[0]);
|
||||
else assert(p[0]==1 && p[1]==2 && p[2]==3);
|
||||
}
|
||||
for(int y=0;y<h;++y) assert(rgb[y*stride+15]==77 && rgb[y*stride+16]==77);
|
||||
}
|
||||
// Every error must preserve all caller bytes, including partial-overlap overflow.
|
||||
masks.push_back(masks[0]);
|
||||
const auto saved=rgb;
|
||||
assert(compose(4)==YOLO_STATUS_UNSUPPORTED && error.message[0]); assert(rgb==saved);
|
||||
masks[0].origin_x=4;
|
||||
assert(compose(1)==YOLO_STATUS_INVALID_ARGUMENT); assert(rgb==saved);
|
||||
masks[0].origin_x=1; masks[0].row_stride_bytes=2;
|
||||
assert(compose(1)==YOLO_STATUS_INVALID_ARGUMENT); assert(rgb==saved);
|
||||
masks[0].row_stride_bytes=4; masks[0].version=0;
|
||||
assert(compose(1)==YOLO_STATUS_ABI_MISMATCH); assert(rgb==saved);
|
||||
masks[0]=view(a,3,3,4,1,1);
|
||||
assert(yolo_masks_compose_rgb_ids(masks.data(),1,w,h,rgb.data(),stride,1,&error)==YOLO_STATUS_BUFFER_TOO_SMALL);
|
||||
assert(rgb==saved);
|
||||
const unsigned char bad[]={255,1};masks[0]=view(bad,2,1,2);
|
||||
assert(compose(1)==YOLO_STATUS_INVALID_ARGUMENT); assert(rgb==saved);
|
||||
assert(yolo_masks_compose_rgb_ids(nullptr,1,w,h,rgb.data(),stride,rgb.size(),nullptr)==YOLO_STATUS_INVALID_ARGUMENT);
|
||||
assert(yolo_masks_compose_rgb_ids(nullptr,256,w,h,rgb.data(),stride,rgb.size(),nullptr)==YOLO_STATUS_UNSUPPORTED);
|
||||
assert(rgb==saved);
|
||||
assert(yolo_masks_compose_rgb_ids(nullptr,0,w,h,rgb.data(),stride,rgb.size(),nullptr)==YOLO_STATUS_OK);
|
||||
for(int y=0;y<h;++y) for(int x=0;x<w*3;++x) assert(rgb[y*stride+x]==0);
|
||||
// Full byte ID range, without assigning IDs from semantic class IDs.
|
||||
const unsigned char foreground=255;
|
||||
masks.clear();
|
||||
for(int i=0;i<255;++i) masks.push_back(view(&foreground,1,1,1,i,0));
|
||||
rgb.assign(255*3,0);
|
||||
assert(yolo_masks_compose_rgb_ids(masks.data(),255,255,1,rgb.data(),765,rgb.size(),nullptr)==YOLO_STATUS_OK);
|
||||
for(int i=0;i<255;++i) for(int k=0;k<3;++k) assert(rgb[i*3+k]==i+1);
|
||||
std::puts("RGB_ID_MASK_OK: exact encoding, independent decode, offsets/strides/holes, 255 IDs, atomic errors");
|
||||
}
|
||||
26
YoloRuntime/tests/runner_mask_output_test.cpp
Normal file
26
YoloRuntime/tests/runner_mask_output_test.cpp
Normal file
@ -0,0 +1,26 @@
|
||||
#include "runner_mask_output.h"
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <iterator>
|
||||
|
||||
int main() {
|
||||
unsigned char data[]={255,0,99, 0,255,99}; // Padded rows; preserve holes.
|
||||
yolo_mask_view_v1_t mask{};mask.data=data;mask.width=mask.height=2;
|
||||
mask.row_stride_bytes=3;mask.origin_x=1;mask.origin_y=1;
|
||||
std::vector<unsigned char> image(4*4*4,100);
|
||||
assert(runner_mask_output::blend(image.data(),4,4,16,4,mask,0));
|
||||
assert(image[20]==61 && image[21]==147 && image[22]==108 && image[23]==100);
|
||||
assert(image[24]==100 && image[36]==100 && image[40]!=100);
|
||||
auto invalid=mask;invalid.origin_x=4;
|
||||
assert(!runner_mask_output::blend(image.data(),4,4,16,4,invalid,0));
|
||||
const auto path=std::filesystem::temp_directory_path()/"yolo-instance-mask-contract.pgm";
|
||||
assert(runner_mask_output::save(path,mask));
|
||||
std::ifstream file(path,std::ios::binary);
|
||||
std::vector<unsigned char> bytes((std::istreambuf_iterator<char>(file)),{});
|
||||
const std::string header="P5\n2 2\n255\n";
|
||||
assert(bytes.size()==header.size()+4);
|
||||
assert(std::equal(header.begin(),header.end(),bytes.begin()));
|
||||
assert(bytes[header.size()]==255 && bytes[header.size()+1]==0 && bytes.back()==255);
|
||||
file.close();std::filesystem::remove(path);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
#include "yolo_seg_geometry.h"
|
||||
#include <opencv2/imgproc.hpp>
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
@ -35,6 +36,37 @@ int main() {
|
||||
assert(close_to(circle.center_x, 12.5));
|
||||
assert(close_to(circle.center_y, 22.0));
|
||||
assert(close_to(circle.radius, std::sqrt(13.0) * 0.5));
|
||||
const auto mean = yolo_seg_geometry::mean_diameter_circle(boundary);
|
||||
assert(mean.valid && !mean.fitted);
|
||||
assert(close_to(mean.center_x, rect.center_x));
|
||||
assert(close_to(mean.center_y, rect.center_y));
|
||||
assert(close_to(mean.radius, 1.25)); // Not half the short side, nor enclosing radius.
|
||||
|
||||
cv::Mat disk = cv::Mat::zeros(121, 121, CV_8U);
|
||||
cv::circle(disk, {60, 60}, 30, cv::Scalar(255), -1);
|
||||
auto fit = yolo_seg_geometry::robust_fit_circle(disk.data, disk.cols, disk.rows, int(disk.step), 10, 20);
|
||||
assert(fit.valid && fit.fitted);
|
||||
assert(close_to(fit.center_x, 70, .1) && close_to(fit.center_y, 80, .1));
|
||||
assert(close_to(fit.radius, 30, 1));
|
||||
// Disconnected noise must not replace or enlarge the largest-contour fit.
|
||||
disk.at<unsigned char>(2, 2) = 255;
|
||||
const auto noisy = yolo_seg_geometry::robust_fit_circle(disk.data, disk.cols, disk.rows, int(disk.step), 10, 20);
|
||||
assert(noisy.fitted && close_to(noisy.radius, fit.radius));
|
||||
cv::line(disk, {87, 60}, {103, 60}, cv::Scalar(255), 2);
|
||||
const auto spiked = yolo_seg_geometry::robust_fit_circle(disk.data, disk.cols, disk.rows, int(disk.step), 0, 0);
|
||||
assert(spiked.valid && spiked.fitted && close_to(spiked.radius, 30, 1));
|
||||
cv::Mat elongated = cv::Mat::zeros(50, 100, CV_8U);
|
||||
cv::rectangle(elongated, {10, 10}, {80, 20}, cv::Scalar(255), -1);
|
||||
const auto fallback = yolo_seg_geometry::robust_fit_circle(elongated.data, elongated.cols, elongated.rows, int(elongated.step), 0, 0);
|
||||
assert(fallback.valid && !fallback.fitted);
|
||||
assert(close_to(fallback.radius, std::sqrt(70.*70.+10.*10.)*.5, .001));
|
||||
const unsigned char point[] = {255};
|
||||
const auto single = yolo_seg_geometry::robust_fit_circle(point, 1, 1, 1, 5, 8);
|
||||
assert(single.valid && !single.fitted && close_to(single.center_x, 5) && close_to(single.center_y, 8));
|
||||
assert(!yolo_seg_geometry::robust_fit_circle(nullptr, 1, 1, 1, 0, 0).valid);
|
||||
const unsigned char empty[] = {0};
|
||||
assert(!yolo_seg_geometry::robust_fit_circle(empty, 1, 1, 1, 0, 0).valid);
|
||||
assert(!yolo_seg_geometry::mean_diameter_circle({}).valid);
|
||||
|
||||
const std::vector<yolo_seg_geometry::Point> rotated_square = {
|
||||
{0.0, -2.0}, {2.0, 0.0}, {0.0, 2.0}, {-2.0, 0.0}};
|
||||
|
||||
144
YoloRuntime/tests/verify_four_geometry.py
Normal file
144
YoloRuntime/tests/verify_four_geometry.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""Public C API real-sample regression against a saved geometry comparison run."""
|
||||
import argparse
|
||||
import ctypes as C
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
class Image(C.Structure):
|
||||
_fields_ = [('struct_size', C.c_uint32), ('version', C.c_uint32), ('data', C.c_void_p),
|
||||
('width', C.c_int32), ('height', C.c_int32), ('stride', C.c_int32),
|
||||
('format', C.c_int32), ('reserved', C.c_uint32 * 8)]
|
||||
|
||||
class Options(C.Structure):
|
||||
_fields_ = [('struct_size', C.c_uint32), ('version', C.c_uint32), ('mode', C.c_int32), ('reserved', C.c_uint32 * 13)]
|
||||
|
||||
class Error(C.Structure):
|
||||
_fields_ = [('struct_size', C.c_uint32), ('version', C.c_uint32), ('status', C.c_int32), ('message', C.c_char * 512), ('reserved', C.c_uint32 * 8)]
|
||||
|
||||
class Info(C.Structure):
|
||||
_fields_ = [('struct_size', C.c_uint32), ('version', C.c_uint32)] + [(n, C.c_uint32) for n in
|
||||
['abi', 'task', 'capabilities', 'formats', 'modes', 'max_results', 'max_keypoints']] + [('reserved', C.c_uint32 * 8)]
|
||||
|
||||
class Geometry(C.Structure):
|
||||
_fields_ = [('struct_size', C.c_uint32), ('version', C.c_uint32), ('type', C.c_int32)] + [(n, C.c_float) for n in
|
||||
['x', 'y', 'width', 'height', 'angle', 'radius']] + [('fit_status', C.c_uint32), ('reserved', C.c_uint32 * 7)]
|
||||
|
||||
class Mask(C.Structure):
|
||||
_fields_ = [('struct_size', C.c_uint32), ('version', C.c_uint32), ('data', C.c_void_p)] + [(n, C.c_int32) for n in
|
||||
['width', 'height', 'stride', 'x', 'y']] + [('reserved', C.c_uint32 * 8)]
|
||||
|
||||
def versioned(cls):
|
||||
value = cls(); value.struct_size = C.sizeof(cls); value.version = 1
|
||||
return value
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument('--install', type=Path, required=True)
|
||||
p.add_argument('--evidence', type=Path, required=True)
|
||||
p.add_argument('--reference', type=Path, action='append', required=True,
|
||||
help='Prior run directory containing raw.json and provenance.json; tests its first image')
|
||||
args = p.parse_args(); args.evidence.mkdir(parents=True, exist_ok=True)
|
||||
libdir = args.install.resolve() / 'lib'
|
||||
cookie = os.add_dll_directory(str(libdir)) if os.name == 'nt' else None
|
||||
lib = C.CDLL(str(libdir / ('yolo_runtime.dll' if os.name == 'nt' else 'libyolo_runtime.so')))
|
||||
signatures = {
|
||||
'yolo_runtime_create': ([C.c_char_p, C.POINTER(C.c_void_p), C.POINTER(Error)], C.c_int32),
|
||||
'yolo_runtime_get_info': ([C.c_void_p, C.POINTER(Info)], C.c_int32),
|
||||
'yolo_runtime_infer_image': ([C.c_void_p, C.POINTER(Image), C.POINTER(Options), C.POINTER(C.c_void_p), C.POINTER(Error)], C.c_int32),
|
||||
'yolo_result_set_get_count': ([C.c_void_p, C.POINTER(C.c_uint32)], C.c_int32),
|
||||
'yolo_result_set_get_seg_geometry': ([C.c_void_p, C.c_uint32, C.POINTER(Geometry)], C.c_int32),
|
||||
'yolo_result_set_get_seg_mask': ([C.c_void_p, C.c_uint32, C.POINTER(Mask)], C.c_int32),
|
||||
'yolo_result_set_release': ([C.c_void_p], None), 'yolo_runtime_destroy': ([C.c_void_p], None)}
|
||||
for name, (types, result) in signatures.items():
|
||||
f = getattr(lib, name); f.argtypes = types; f.restype = result
|
||||
assert C.sizeof(Geometry) == 68 and Geometry.radius.offset == 32 and Geometry.fit_status.offset == 36
|
||||
reports = []
|
||||
for ri, reference in enumerate(args.reference):
|
||||
provenance = json.loads((reference / 'provenance.json').read_text(encoding='utf-8'))
|
||||
raw = json.loads((reference / 'raw.json').read_text(encoding='utf-8'))
|
||||
first = raw['images'][0]
|
||||
model = Path(provenance['model']); image_path = Path(provenance['input']) / first['image']
|
||||
assert hashlib.sha256(model.read_bytes()).hexdigest() == provenance['model_sha256']
|
||||
# Labels do not affect segmentation geometry; preserve the actual model metadata.
|
||||
import onnx
|
||||
import ast
|
||||
metadata = {v.key: v.value for v in onnx.load(str(model), load_external_data=False).metadata_props}
|
||||
labels = list(ast.literal_eval(metadata['names']).values())
|
||||
config = args.evidence / f'config-{ri}.yaml'
|
||||
config.write_text(f'''schema_version: 1
|
||||
name: four_geometry_test
|
||||
version: 1
|
||||
task: seg
|
||||
model: {json.dumps(str(model), ensure_ascii=False)}
|
||||
target_platform: rk3588
|
||||
model_family: yolo8
|
||||
output_contract: task_specific
|
||||
input:
|
||||
width: {raw['imgsz']}
|
||||
height: {raw['imgsz']}
|
||||
format: rgb
|
||||
layout: nchw
|
||||
normalization:
|
||||
mean: [0, 0, 0]
|
||||
std: [255, 255, 255]
|
||||
postprocess:
|
||||
adapter: ultralytics_yolo8_seg
|
||||
confidence_threshold: {raw['conf']}
|
||||
nms_threshold: {raw['iou']}
|
||||
class_agnostic_nms: false
|
||||
labels: {json.dumps(labels)}
|
||||
''', encoding='utf-8')
|
||||
runtime = C.c_void_p(); error = versioned(Error)
|
||||
assert lib.yolo_runtime_create(str(config.resolve()).encode('utf-8'), C.byref(runtime), C.byref(error)) == 0, bytes(error.message)
|
||||
try:
|
||||
info = versioned(Info); assert lib.yolo_runtime_get_info(runtime, C.byref(info)) == 0
|
||||
assert info.abi == 5 and info.modes == 30 and info.capabilities & 30 == 30
|
||||
image = cv2.imdecode(np.fromfile(str(image_path), np.uint8), cv2.IMREAD_COLOR)
|
||||
image = np.ascontiguousarray(image)
|
||||
view = versioned(Image); view.data = image.ctypes.data; view.width = image.shape[1]; view.height = image.shape[0]
|
||||
view.stride = image.strides[0]; view.format = 2
|
||||
maxima = {mode: 0.0 for mode in range(1, 5)}
|
||||
for mode in [1, 2, 3, 4, 0, 5]:
|
||||
options = versioned(Options); options.mode = mode; results = C.c_void_p()
|
||||
status = lib.yolo_runtime_infer_image(runtime, C.byref(view), C.byref(options), C.byref(results), C.byref(error))
|
||||
if mode in (0, 5):
|
||||
assert status == -1 and not results.value
|
||||
continue
|
||||
assert status == 0, bytes(error.message)
|
||||
try:
|
||||
count = C.c_uint32(); assert lib.yolo_result_set_get_count(results, C.byref(count)) == 0
|
||||
assert count.value == len(first['targets'])
|
||||
for i, expected in enumerate(first['targets']):
|
||||
g = versioned(Geometry); m = versioned(Mask)
|
||||
assert lib.yolo_result_set_get_seg_geometry(results, i, C.byref(g)) == 0
|
||||
assert lib.yolo_result_set_get_seg_mask(results, i, C.byref(m)) == 0
|
||||
expected_status = (1 if expected['fitted'] else 2) if mode == 4 else 0
|
||||
assert g.type == mode and g.fit_status == expected_status
|
||||
local = np.ctypeslib.as_array((C.c_uint8 * (m.stride*m.height)).from_address(m.data)).reshape(m.height, m.stride)[:, :m.width]
|
||||
full = np.zeros(image.shape[:2], np.uint8); full[m.y:m.y+m.height, m.x:m.x+m.width] = local
|
||||
assert hashlib.sha256(full.tobytes()).hexdigest() == expected['mask_sha256']
|
||||
if mode == 4: want = [expected['fit_x'], expected['fit_y'], expected['fit_r']]; got = [g.x, g.y, g.radius]
|
||||
elif mode == 3: want = [expected['mean_x'], expected['mean_y'], expected['mean_r']]; got = [g.x, g.y, g.radius]
|
||||
elif mode == 1: want = [expected['mean_x'], expected['mean_y'], expected['rect_w'], expected['rect_h']]; got = [g.x, g.y, g.width, g.height]
|
||||
else:
|
||||
yy, xx = np.nonzero(full)
|
||||
center, radius = cv2.minEnclosingCircle(np.column_stack([xx, yy]).astype(np.float32))
|
||||
want = [*center, radius]; got = [g.x, g.y, g.radius]
|
||||
diff = max(abs(a-b) for a,b in zip(got,want)); assert diff < .002, (mode, i, diff)
|
||||
maxima[mode] = max(maxima[mode], diff)
|
||||
finally:
|
||||
lib.yolo_result_set_release(results)
|
||||
reports.append(dict(reference=str(reference), image=first['image'], targets=len(first['targets']), max_error_px=maxima,
|
||||
masks_unchanged=True, modes=[1,2,3,4], invalid_modes_rejected=[0,5]))
|
||||
finally:
|
||||
lib.yolo_runtime_destroy(runtime)
|
||||
(args.evidence / 'four-mode-api.json').write_text(json.dumps(reports, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
print('FOUR_GEOMETRY_PUBLIC_API_PASS', json.dumps(reports, ensure_ascii=False))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -1,5 +1,6 @@
|
||||
LIBRARY yolo_runtime
|
||||
EXPORTS
|
||||
yolo_masks_compose_rgb_ids
|
||||
yolo_runtime_get_abi_version
|
||||
yolo_runtime_create
|
||||
yolo_runtime_get_info
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user