feat: synchronize YoloRuntime shared segmentation and RGB mask output

This commit is contained in:
Ben 2026-10-21 20:56:29 +08:00
commit bdf5f124ab
20 changed files with 809 additions and 23 deletions

View File

@ -18,8 +18,8 @@
{
"project": "YoloRuntime",
"directory": "YoloRuntime",
"source_head": "df794f144df90d867f6ec4ec94c55eb3435e1a83",
"source_tree": "93f2652c042bbc68079935c49c59de44b928137f",
"source_head": "9be44d557eb05c2fab5fd8f69974a6101532cc7c",
"source_tree": "395864996ded1983011a3c9375fce0537295dcd8",
"working_source_snapshot": false
}
]

View File

@ -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 .

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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 {

View File

@ -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'}

View File

@ -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;
}
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<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);

View 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;
}
}

View 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");
}
}

View 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

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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};

View 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");
}

View 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);
}

View File

@ -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}};

View 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()

View File

@ -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