diff --git a/IMPORTS.json b/IMPORTS.json index 33e34aa..389278e 100644 --- a/IMPORTS.json +++ b/IMPORTS.json @@ -18,8 +18,8 @@ { "project": "YoloRuntime", "directory": "YoloRuntime", - "source_head": "df794f144df90d867f6ec4ec94c55eb3435e1a83", - "source_tree": "93f2652c042bbc68079935c49c59de44b928137f", + "source_head": "9be44d557eb05c2fab5fd8f69974a6101532cc7c", + "source_tree": "395864996ded1983011a3c9375fce0537295dcd8", "working_source_snapshot": false } ] diff --git a/YoloRuntime/CMakeLists.txt b/YoloRuntime/CMakeLists.txt index 56882eb..01cdf09 100644 --- a/YoloRuntime/CMakeLists.txt +++ b/YoloRuntime/CMakeLists.txt @@ -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 $ $ 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 + $ $ + 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 $ DESTINATION lib) + endif() + endforeach() else() install(TARGETS yolo_runtime rk3588_yolo_runner EXPORT YoloRuntimeTargets RUNTIME DESTINATION . diff --git a/YoloRuntime/README.md b/YoloRuntime/README.md index a5302c7..fd5821e 100644 --- a/YoloRuntime/README.md +++ b/YoloRuntime/README.md @@ -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. diff --git a/YoloRuntime/VERIFICATION.md b/YoloRuntime/VERIFICATION.md index 09f311d..f160f60 100644 --- a/YoloRuntime/VERIFICATION.md +++ b/YoloRuntime/VERIFICATION.md @@ -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 `.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. diff --git a/YoloRuntime/include/yolo_runtime.h b/YoloRuntime/include/yolo_runtime.h index 4be5f2b..78270ba 100644 --- a/YoloRuntime/include/yolo_runtime.h +++ b/YoloRuntime/include/yolo_runtime.h @@ -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, diff --git a/YoloRuntime/include/yolo_runtime_types.h b/YoloRuntime/include/yolo_runtime_types.h index b57e9f3..f17ebe9 100644 --- a/YoloRuntime/include/yolo_runtime_types.h +++ b/YoloRuntime/include/yolo_runtime_types.h @@ -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 { diff --git a/YoloRuntime/scripts/verify.ps1 b/YoloRuntime/scripts/verify.ps1 index 540a56c..8b6875d 100644 --- a/YoloRuntime/scripts/verify.ps1 +++ b/YoloRuntime/scripts/verify.ps1 @@ -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 ' + "`n" + 'int main(void){return yolo_runtime_get_abi_version()==5 ? 0 : 1;}' | Set-Content (Join-Path $consumer 'main.c') -Encoding ASCII +'#include ' + "`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'} diff --git a/YoloRuntime/src/main.cpp b/YoloRuntime/src/main.cpp index ba3198d..dc56351 100644 --- a/YoloRuntime/src/main.cpp +++ b/YoloRuntime/src/main.cpp @@ -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 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(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(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(geometry.center_x), static_cast(geometry.center_y), std::max(1, static_cast(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; } - render_results(source, report); + try { + const auto directory = fs::path(json_path).parent_path() / (fs::path(output).stem().string()+".masks"); + for (size_t i=0;i +#include +#include +#include + +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.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((pixel[c]*155+color[c]*100+127)/255); + } + return true; +} +} diff --git a/YoloRuntime/src/yolo_mask_ids.cpp b/YoloRuntime/src/yolo_mask_ids.cpp new file mode 100644 index 0000000..d4fdd9e --- /dev/null +++ b/YoloRuntime/src/yolo_mask_ids.cpp @@ -0,0 +1,65 @@ +#include "yolo_runtime.h" +#include +#include +#include +#include +#include + +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::max() || + packed_size > std::numeric_limits::max()) + return finish(YOLO_STATUS_BUFFER_TOO_SMALL, "RGB output capacity is too small"); + for (uint32_t i=0; i 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::max()) + return finish(YOLO_STATUS_INVALID_ARGUMENT, "Invalid mask bounds or row stride"); + } + try { + std::vector rgb(static_cast(packed_size), 0); + for (uint32_t i=0; i(i+1); + for (int32_t y=0; y 退化检查 -> 按弧长均匀采样 -> 初始圆 -> Tukey IRLS -> 质量门限。 +// “最小外接圆”要求包住所有轮廓点;“拟合圆”追求大多数边界点到圆周的距离小。 +// 所以少量突出点会推大外接圆,本算法通过降低异常点权重来抑制其对拟合的影响。 +// Shared Runtime reports fitted=false as an explicit minimum-enclosing fallback. + +#include +#include +#include +#include +#include + +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 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 SampleBoundary(const std::vector& contour) +{ + // 闭合轮廓的第 i 段连接 contour[i] 与下一点,最后一段回到第 0 点。 + // lengths[i] 表示到第 i 个顶点的累计弧长,lengths.back() 为总周长。 + std::vector 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 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& 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 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 residuals; + for (const auto& point : points) residuals.push_back(cv::norm(point - center) - radius); + const double median = Median(residuals); + std::vector 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 sectors{}; + // 4. 优化能算出一个圆,不等于轮廓确实近似圆。以下门限联合决定 fitted=true。 + // 把方向分为 12 个扇区,仅由误差 <= max(1px,0.10R) 的点提供扇区支持。 + // 至少 10 个扇区有支持,用于抑制“只有短弧却拟合出大圆”的情况。 + std::vector 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(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(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 diff --git a/YoloRuntime/src/yolo_runtime.cpp b/YoloRuntime/src/yolo_runtime.cpp index fd1b153..8730781 100644 --- a/YoloRuntime/src/yolo_runtime.cpp +++ b/YoloRuntime/src/yolo_runtime.cpp @@ -158,7 +158,8 @@ static std::unique_ptr copy_public_results( item.mask_data + static_cast(row) * item.mask_stride_bytes, static_cast(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::mask_boundary( result.mask.data(), result.mask_width, result.mask_height, @@ -168,12 +169,20 @@ static std::unique_ptr 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(circle.center_x); result.seg_geometry.center_y = static_cast(circle.center_y); result.seg_geometry.radius = static_cast(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) { diff --git a/YoloRuntime/src/yolo_seg_geometry.cpp b/YoloRuntime/src/yolo_seg_geometry.cpp index 3f93df5..3a660d2 100644 --- a/YoloRuntime/src/yolo_seg_geometry.cpp +++ b/YoloRuntime/src/yolo_seg_geometry.cpp @@ -1,4 +1,5 @@ #include "yolo_seg_geometry.h" +#include "yolo_robust_circle.h" #include #include @@ -201,4 +202,24 @@ RotatedRect minimum_area_rect(const std::vector& input) { return best; } +Circle mean_diameter_circle(const std::vector& 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(mask), stride); + std::vector> 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 diff --git a/YoloRuntime/src/yolo_seg_geometry.h b/YoloRuntime/src/yolo_seg_geometry.h index 693f6a4..46d020d 100644 --- a/YoloRuntime/src/yolo_seg_geometry.h +++ b/YoloRuntime/src/yolo_seg_geometry.h @@ -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 mask_boundary( Circle minimum_enclosing_circle(const std::vector& points); RotatedRect minimum_area_rect(const std::vector& points); +Circle mean_diameter_circle(const std::vector& 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 diff --git a/YoloRuntime/tests/c_api_contract.c b/YoloRuntime/tests/c_api_contract.c index 9c66a1b..4011e5f 100644 --- a/YoloRuntime/tests/c_api_contract.c +++ b/YoloRuntime/tests/c_api_contract.c @@ -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}; diff --git a/YoloRuntime/tests/mask_ids_test.cpp b/YoloRuntime/tests/mask_ids_test.cpp new file mode 100644 index 0000000..861b577 --- /dev/null +++ b/YoloRuntime/tests/mask_ids_test.cpp @@ -0,0 +1,74 @@ +#include "yolo_runtime.h" +#include +#include +#include +#include +#include + +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 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 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 expected,actual; + for(uint32_t i=0;i=m.origin_x && x=m.origin_y && y +#include +#include +#include + +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 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 bytes((std::istreambuf_iterator(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); +} diff --git a/YoloRuntime/tests/seg_geometry_test.cpp b/YoloRuntime/tests/seg_geometry_test.cpp index 1f5323f..4ce4224 100644 --- a/YoloRuntime/tests/seg_geometry_test.cpp +++ b/YoloRuntime/tests/seg_geometry_test.cpp @@ -1,4 +1,5 @@ #include "yolo_seg_geometry.h" +#include #include #include @@ -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(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 rotated_square = { {0.0, -2.0}, {2.0, 0.0}, {0.0, 2.0}, {-2.0, 0.0}}; diff --git a/YoloRuntime/tests/verify_four_geometry.py b/YoloRuntime/tests/verify_four_geometry.py new file mode 100644 index 0000000..8140e5b --- /dev/null +++ b/YoloRuntime/tests/verify_four_geometry.py @@ -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() diff --git a/YoloRuntime/yolo_runtime.def b/YoloRuntime/yolo_runtime.def index f42d750..cbce252 100644 --- a/YoloRuntime/yolo_runtime.def +++ b/YoloRuntime/yolo_runtime.def @@ -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