zorya-classifier 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +2 -0
- package/README.md +18 -6
- package/dist/app/cli-arguments.d.ts +40 -0
- package/dist/app/cli-arguments.js +67 -0
- package/dist/app/create-classifier.d.ts +19 -0
- package/dist/app/create-classifier.js +226 -14
- package/dist/app/extraction-files.d.ts +9 -0
- package/dist/app/extraction-files.js +28 -0
- package/dist/cli.js +58 -14
- package/dist/entities/boundary-view.d.ts +29 -0
- package/dist/entities/boundary-view.js +1 -0
- package/dist/entities/contracts.d.ts +77 -1
- package/dist/entities/contracts.js +7 -0
- package/dist/entities/cutout-raster.d.ts +7 -0
- package/dist/entities/cutout-raster.js +1 -0
- package/dist/entities/object-proposals.d.ts +53 -0
- package/dist/entities/object-proposals.js +8 -0
- package/dist/entities/representation.d.ts +225 -0
- package/dist/entities/representation.js +33 -0
- package/dist/entities/segmentation-metadata.d.ts +11 -0
- package/dist/entities/segmentation-metadata.js +1 -0
- package/dist/entities/segmentation-verification.d.ts +30 -0
- package/dist/entities/segmentation-verification.js +1 -0
- package/dist/entities/segmentation.d.ts +159 -0
- package/dist/entities/segmentation.js +5 -0
- package/dist/entities/whole-object.d.ts +128 -0
- package/dist/entities/whole-object.js +7 -0
- package/dist/features/choice.d.ts +1 -1
- package/dist/features/choice.js +2 -2
- package/dist/features/segmentation/box-check.d.ts +10 -0
- package/dist/features/segmentation/box-check.js +299 -0
- package/dist/features/segmentation/mask.d.ts +6 -0
- package/dist/features/segmentation/mask.js +89 -0
- package/dist/features/segmentation/packed-mask.d.ts +5 -0
- package/dist/features/segmentation/packed-mask.js +53 -0
- package/dist/index.d.ts +6 -1
- package/dist/infrastructure/boundary-view.d.ts +2 -0
- package/dist/infrastructure/boundary-view.js +263 -0
- package/dist/infrastructure/openrouter-common.d.ts +8 -8
- package/dist/infrastructure/openrouter-vision.d.ts +3 -0
- package/dist/infrastructure/openrouter-vision.js +5 -0
- package/dist/infrastructure/raster.d.ts +10 -1
- package/dist/infrastructure/raster.js +99 -4
- package/dist/infrastructure/segmentation/box-presentation.d.ts +2 -0
- package/dist/infrastructure/segmentation/box-presentation.js +136 -0
- package/dist/infrastructure/segmentation/cutout-raster.d.ts +3 -0
- package/dist/infrastructure/segmentation/cutout-raster.js +79 -0
- package/dist/infrastructure/segmentation/grabcut/index.d.ts +13 -0
- package/dist/infrastructure/segmentation/grabcut/index.js +118 -0
- package/dist/infrastructure/segmentation/grabcut/worker-inference.d.ts +47 -0
- package/dist/infrastructure/segmentation/grabcut/worker-inference.js +260 -0
- package/dist/infrastructure/segmentation/index.d.ts +2 -0
- package/dist/infrastructure/segmentation/index.js +2 -0
- package/dist/infrastructure/segmentation/mobile-sam/index.d.ts +58 -0
- package/dist/infrastructure/segmentation/mobile-sam/index.js +472 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-inference.d.ts +149 -0
- package/dist/infrastructure/segmentation/mobile-sam/worker-inference.js +845 -0
- package/dist/infrastructure/segmentation/native-child.d.ts +23 -0
- package/dist/infrastructure/segmentation/native-child.js +297 -0
- package/dist/infrastructure/segmentation/native-protocol.d.ts +923 -0
- package/dist/infrastructure/segmentation/native-protocol.js +242 -0
- package/dist/infrastructure/segmentation/native-worker.d.ts +1 -0
- package/dist/infrastructure/segmentation/native-worker.js +93 -0
- package/dist/infrastructure/segmentation/probe-backend.d.ts +1 -0
- package/dist/infrastructure/segmentation/probe-backend.js +31 -0
- package/dist/infrastructure/segmentation/proposal-presentation.d.ts +3 -0
- package/dist/infrastructure/segmentation/proposal-presentation.js +371 -0
- package/dist/infrastructure/segmentation/verify-asset.d.ts +4 -0
- package/dist/infrastructure/segmentation/verify-asset.js +32 -0
- package/dist/infrastructure/text-representation.d.ts +23 -0
- package/dist/infrastructure/text-representation.js +226 -0
- package/dist/infrastructure/vision-chat.d.ts +1 -1
- package/dist/infrastructure/vision-chat.js +13 -4
- package/dist/openai-compatible.d.ts +3 -0
- package/dist/openai-compatible.js +4 -0
- package/dist/openrouter.d.ts +2 -2
- package/dist/openrouter.js +1 -1
- package/dist/processes/boundary-state.d.ts +12 -0
- package/dist/processes/boundary-state.js +196 -0
- package/dist/processes/classify.d.ts +2 -1
- package/dist/processes/classify.js +21 -3
- package/dist/processes/extract-from-region.d.ts +19 -0
- package/dist/processes/extract-from-region.js +415 -0
- package/dist/processes/extract.d.ts +25 -0
- package/dist/processes/extract.js +223 -0
- package/dist/processes/locate.d.ts +2 -1
- package/dist/processes/locate.js +84 -11
- package/dist/processes/proposal-location.d.ts +19 -0
- package/dist/processes/proposal-location.js +396 -0
- package/dist/processes/whole-object.d.ts +7 -0
- package/dist/processes/whole-object.js +272 -0
- package/dist/segmentation.d.ts +10 -0
- package/dist/segmentation.js +4 -0
- package/dist/shared/input-budget.d.ts +7 -0
- package/dist/shared/input-budget.js +21 -0
- package/docs/0.2.0-plan.md +190 -0
- package/docs/README.ru.md +18 -5
- package/docs/acceptance.md +116 -3
- package/docs/api.md +8 -4
- package/docs/architecture.md +20 -7
- package/docs/benchmarks.md +10 -0
- package/docs/categorical-presence-port.md +26 -0
- package/docs/evidence/v02/baseline.json +1043 -0
- package/docs/evidence/v02/boundary-variants.md +17 -0
- package/docs/evidence/v02/box-confirmation-dev40/README.md +15 -0
- package/docs/evidence/v02/box-confirmation-dev40/component-refinement.json +246 -0
- package/docs/evidence/v02/box-confirmation-dev40/evaluation.json +1663 -0
- package/docs/evidence/v02/box-confirmation-dev40/provenance.json +130 -0
- package/docs/evidence/v02/box-confirmation-dev40/report.json +127 -0
- package/docs/evidence/v02/box-confirmation-dev40-freeze-20261020-v1.json +230 -0
- package/docs/evidence/v02/box-confirmation-method-selection-20261020-v1.json +85 -0
- package/docs/evidence/v02/box-heldout-cancelled-before-dispatch-20261021-v1.json +32 -0
- package/docs/evidence/v02/box-heldout-combined-seal-20261021-v1.json +89 -0
- package/docs/evidence/v02/box-heldout-evaluator-truth-seal-20261021-v1.json +61 -0
- package/docs/evidence/v02/box-heldout-exclusion-audit-20261020-v1.json +81 -0
- package/docs/evidence/v02/box-heldout-map-seal-20261021-v1.json +48 -0
- package/docs/evidence/v02/box-heldout-photo-seal-20261020-v1.json +62 -0
- package/docs/evidence/v02/box-heldout-run-freeze-20261021-v1.json +61 -0
- package/docs/evidence/v02/box-heldout-source-review-20261020-v1.json +134 -0
- package/docs/evidence/v02/box-heldout-ui-seal-20261020-v1.json +44 -0
- package/docs/evidence/v02/calculator-proof-root-review-20261020-v1.json +63 -0
- package/docs/evidence/v02/categorical-dev40-freeze.json +94 -0
- package/docs/evidence/v02/categorical-dev40-preserve.json +460 -0
- package/docs/evidence/v02/categorical-dev40-target-only.json +460 -0
- package/docs/evidence/v02/categorical-heldout-admission-20261020-v3.json +215 -0
- package/docs/evidence/v02/categorical-heldout-admission-20261020-v4.json +270 -0
- package/docs/evidence/v02/categorical-heldout-dispatch-authorization-20261020-v4.json +79 -0
- package/docs/evidence/v02/categorical-heldout-predispatch-20261020-v3.json +57 -0
- package/docs/evidence/v02/categorical-heldout-producer-source-map-v4.json +262 -0
- package/docs/evidence/v02/categorical-heldout-producer-source-map.json +263 -0
- package/docs/evidence/v02/categorical-heldout-run-freeze-20261020-v3.json +35 -0
- package/docs/evidence/v02/categorical-heldout-run-freeze-20261020-v4.json +35 -0
- package/docs/evidence/v02/categorical-heldout-v4/README.md +35 -0
- package/docs/evidence/v02/categorical-heldout-v4/candidate-evaluation.json +2812 -0
- package/docs/evidence/v02/categorical-heldout-v4/candidate-physical-receipts.json +15261 -0
- package/docs/evidence/v02/categorical-heldout-v4/candidate-provenance.json +131 -0
- package/docs/evidence/v02/categorical-heldout-v4/control-evaluation.json +855 -0
- package/docs/evidence/v02/categorical-heldout-v4/control-physical-receipts.json +4402 -0
- package/docs/evidence/v02/categorical-heldout-v4/control-provenance.json +31 -0
- package/docs/evidence/v02/categorical-heldout-v4/mask-normalized-exploratory.json +682 -0
- package/docs/evidence/v02/categorical-heldout-v4/paired-qualification.json +21 -0
- package/docs/evidence/v02/categorical-heldout-v4/report.json +258 -0
- package/docs/evidence/v02/categorical-presence-dev6.json +144 -0
- package/docs/evidence/v02/categorical-presence-dev6.md +18 -0
- package/docs/evidence/v02/categorical-vision-method-selection.json +90 -0
- package/docs/evidence/v02/checked-region-component-v1/README.md +7 -0
- package/docs/evidence/v02/checked-region-component-v1/evidence.json +311 -0
- package/docs/evidence/v02/checked-region-resource-interrupted-2b0d09f.json +102 -0
- package/docs/evidence/v02/checked-region-segmentation-resource-4df544c-receipt.json +167 -0
- package/docs/evidence/v02/checked-region-segmentation-resource-4df544c-root-review.json +58 -0
- package/docs/evidence/v02/checked-region-segmentation-resource-987ac32-receipt.json +158 -0
- package/docs/evidence/v02/ci-012b935.json +17 -0
- package/docs/evidence/v02/ci-0c365cb.json +372 -0
- package/docs/evidence/v02/ci-185bde8.json +32 -0
- package/docs/evidence/v02/ci-24f849f.json +95 -0
- package/docs/evidence/v02/ci-25c19cd.json +375 -0
- package/docs/evidence/v02/ci-2922caf.json +375 -0
- package/docs/evidence/v02/ci-29357d7.json +29 -0
- package/docs/evidence/v02/ci-32aa90d.json +24 -0
- package/docs/evidence/v02/ci-35e3064.json +376 -0
- package/docs/evidence/v02/ci-4331b1b.json +384 -0
- package/docs/evidence/v02/ci-43b7426.json +376 -0
- package/docs/evidence/v02/ci-450439c.json +373 -0
- package/docs/evidence/v02/ci-46283fe.json +29 -0
- package/docs/evidence/v02/ci-4c9c09f.json +34 -0
- package/docs/evidence/v02/ci-5579646.json +27 -0
- package/docs/evidence/v02/ci-6f9cc15.json +127 -0
- package/docs/evidence/v02/ci-7c1051f.json +27 -0
- package/docs/evidence/v02/ci-9837888.json +34 -0
- package/docs/evidence/v02/ci-987ac32.json +29 -0
- package/docs/evidence/v02/ci-9d00fe5.json +25 -0
- package/docs/evidence/v02/ci-9f85acc.json +127 -0
- package/docs/evidence/v02/ci-be275a6.json +201 -0
- package/docs/evidence/v02/ci-c107ddf.json +372 -0
- package/docs/evidence/v02/ci-c35f67d.json +127 -0
- package/docs/evidence/v02/ci-c5eddd8.json +32 -0
- package/docs/evidence/v02/ci-d2042e0.json +372 -0
- package/docs/evidence/v02/ci-d684259.json +375 -0
- package/docs/evidence/v02/ci-e0c190b.json +127 -0
- package/docs/evidence/v02/ci-e551253.json +372 -0
- package/docs/evidence/v02/ci-f9d4633.json +29 -0
- package/docs/evidence/v02/dev1-proposal-evaluation.md +13 -0
- package/docs/evidence/v02/dev2-mobile-sam-proposal-oracle.md +21 -0
- package/docs/evidence/v02/dev2-proposal-evaluation.md +21 -0
- package/docs/evidence/v02/fresh-development.md +16 -0
- package/docs/evidence/v02/fresh-native-numeric/artifact-hashes.json +9 -0
- package/docs/evidence/v02/fresh-native-numeric/evaluation-root-v1.json +568 -0
- package/docs/evidence/v02/fresh-native-numeric/run-manifest.json +342 -0
- package/docs/evidence/v02/fresh-native-numeric/summary.json +5654 -0
- package/docs/evidence/v02/fresh-native-outline-axis/artifact-hashes.json +9 -0
- package/docs/evidence/v02/fresh-native-outline-axis/evaluation-root-v1.json +568 -0
- package/docs/evidence/v02/fresh-native-outline-axis/run-manifest.json +378 -0
- package/docs/evidence/v02/fresh-native-outline-axis/summary.json +5605 -0
- package/docs/evidence/v02/fresh-text/README.md +23 -0
- package/docs/evidence/v02/fresh-text/ascii-contours-v1/classify-followup-run-manifest.json +405 -0
- package/docs/evidence/v02/fresh-text/ascii-contours-v1/classify-followup-summary.json +1646 -0
- package/docs/evidence/v02/fresh-text/ascii-contours-v1/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/ascii-contours-v1/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/ascii-contours-v1/original-summary.json +4416 -0
- package/docs/evidence/v02/fresh-text/ascii-luma-v1/classify-followup-run-manifest.json +405 -0
- package/docs/evidence/v02/fresh-text/ascii-luma-v1/classify-followup-summary.json +1646 -0
- package/docs/evidence/v02/fresh-text/ascii-luma-v1/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/ascii-luma-v1/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/ascii-luma-v1/original-summary.json +4647 -0
- package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/classify-followup-run-manifest.json +405 -0
- package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/classify-followup-summary.json +846 -0
- package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/ascii-svg-layered-v1/original-summary.json +1789 -0
- package/docs/evidence/v02/fresh-text/no-image-control/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/no-image-control/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/no-image-control/original-summary.json +3953 -0
- package/docs/evidence/v02/fresh-text/palette-grid-v1/classify-followup-run-manifest.json +405 -0
- package/docs/evidence/v02/fresh-text/palette-grid-v1/classify-followup-summary.json +1646 -0
- package/docs/evidence/v02/fresh-text/palette-grid-v1/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/palette-grid-v1/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/palette-grid-v1/original-summary.json +3974 -0
- package/docs/evidence/v02/fresh-text/pixel-rle-v1/classify-followup-run-manifest.json +405 -0
- package/docs/evidence/v02/fresh-text/pixel-rle-v1/classify-followup-summary.json +1646 -0
- package/docs/evidence/v02/fresh-text/pixel-rle-v1/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/pixel-rle-v1/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/pixel-rle-v1/original-summary.json +4945 -0
- package/docs/evidence/v02/fresh-text/report.json +284 -0
- package/docs/evidence/v02/fresh-text/svg-contours-v1/classify-followup-run-manifest.json +405 -0
- package/docs/evidence/v02/fresh-text/svg-contours-v1/classify-followup-summary.json +846 -0
- package/docs/evidence/v02/fresh-text/svg-contours-v1/evaluation.json +568 -0
- package/docs/evidence/v02/fresh-text/svg-contours-v1/original-run-manifest.json +404 -0
- package/docs/evidence/v02/fresh-text/svg-contours-v1/original-summary.json +2189 -0
- package/docs/evidence/v02/heldout-evaluation-20261020.md +37 -0
- package/docs/evidence/v02/heldout-input-derivation.md +18 -0
- package/docs/evidence/v02/integration-0c365cb.json +90 -0
- package/docs/evidence/v02/integration-25c19cd.json +52 -0
- package/docs/evidence/v02/integration-35e3064.json +41 -0
- package/docs/evidence/v02/integration-4331b1b.json +42 -0
- package/docs/evidence/v02/integration-4c9c09f.json +46 -0
- package/docs/evidence/v02/integration-8010aad.json +48 -0
- package/docs/evidence/v02/integration-c1aa640.json +41 -0
- package/docs/evidence/v02/integration-eea1790.json +46 -0
- package/docs/evidence/v02/integration-fa82e65.json +42 -0
- package/docs/evidence/v02/jpeg-transport-offline.json +268 -0
- package/docs/evidence/v02/jpeg-transport-offline.md +7 -0
- package/docs/evidence/v02/local-archive-cleanup.json +59 -0
- package/docs/evidence/v02/native-base64-after.json +30 -0
- package/docs/evidence/v02/native-base64-before.json +32 -0
- package/docs/evidence/v02/native-checked-region-windows-46283fe.json +248 -0
- package/docs/evidence/v02/native-checked-region-windows-987ac32.json +141 -0
- package/docs/evidence/v02/native-grabcut-windows-4c494a3-run2.json +148 -0
- package/docs/evidence/v02/native-grabcut-windows-4c494a3-run3.json +148 -0
- package/docs/evidence/v02/native-grabcut-windows-4c494a3.json +56 -0
- package/docs/evidence/v02/native-proposal-windows-0c365cb.json +148 -0
- package/docs/evidence/v02/native-proposal-windows-450439c.json +156 -0
- package/docs/evidence/v02/native-proposal-windows-4c9c09f-retry1.json +147 -0
- package/docs/evidence/v02/native-proposal-windows-4c9c09f.json +52 -0
- package/docs/evidence/v02/native-region-windows-efc9318.json +163 -0
- package/docs/evidence/v02/native-windows-eea1790-baseline.json +198 -0
- package/docs/evidence/v02/native-windows-eea1790.json +228 -0
- package/docs/evidence/v02/ordinary-luna-axis/run-manifest.json +99 -0
- package/docs/evidence/v02/ordinary-luna-axis/summary.json +811 -0
- package/docs/evidence/v02/owned-temp-cleanup-20261020-v1.json +28 -0
- package/docs/evidence/v02/presentation-paired-20261020.json +1438 -0
- package/docs/evidence/v02/proposal-resource-soak-261dc23.json +201 -0
- package/docs/evidence/v02/proposal-resource-soak-261dc23.md +15 -0
- package/docs/evidence/v02/proposal-resource-soak-450439c-receipt.md +13 -0
- package/docs/evidence/v02/reconstructed-c03-box.json +41 -0
- package/docs/evidence/v02/region-seeded-dev-v1/README.md +17 -0
- package/docs/evidence/v02/region-seeded-dev-v1/evaluation.json +260 -0
- package/docs/evidence/v02/region-seeded-dev-v2/README.md +15 -0
- package/docs/evidence/v02/region-seeded-dev-v2/evaluation.json +232 -0
- package/docs/evidence/v02/region-seeded-dev-v3-candidate-only/README.md +9 -0
- package/docs/evidence/v02/region-seeded-dev-v3-candidate-only/control-identity-failure.json +22 -0
- package/docs/evidence/v02/region-seeded-dev-v3-candidate-only/evaluation.json +160 -0
- package/docs/evidence/v02/region-seeded-dev-v3-control-r2/README.md +9 -0
- package/docs/evidence/v02/region-seeded-dev-v3-control-r2/attempt-accounting.json +55 -0
- package/docs/evidence/v02/region-seeded-dev-v3-control-r2/evaluation.json +136 -0
- package/docs/evidence/v02/region-seeded-development-control-r2-freeze-20261021.json +132 -0
- package/docs/evidence/v02/region-seeded-development-freeze-20261021-v1.json +167 -0
- package/docs/evidence/v02/region-seeded-development-freeze-20261021-v2.json +186 -0
- package/docs/evidence/v02/region-seeded-development-freeze-20261021-v3.json +205 -0
- package/docs/evidence/v02/region-seeded-heldout-cancelled-before-dispatch-20261021-v2.json +20 -0
- package/docs/evidence/v02/region-seeded-heldout-dispatch-authorization-20261021-v3.json +47 -0
- package/docs/evidence/v02/region-seeded-heldout-readiness-audit-20261021-v3.json +125 -0
- package/docs/evidence/v02/region-seeded-heldout-run-freeze-20261021-v2.json +234 -0
- package/docs/evidence/v02/region-seeded-heldout-run-freeze-20261021-v3.json +357 -0
- package/docs/evidence/v02/region-seeded-heldout-v3/README.md +19 -0
- package/docs/evidence/v02/region-seeded-heldout-v3/evaluation-summary.json +445 -0
- package/docs/evidence/v02/region-seeded-method-selection-20261021-v2.json +66 -0
- package/docs/evidence/v02/region-seeded-method-selection-20261021-v3.json +120 -0
- package/docs/evidence/v02/region-segmentation-resource-0ba4c87-receipt.json +156 -0
- package/docs/evidence/v02/resource-soak-linux-8010aad.json +125 -0
- package/docs/evidence/v02/screen-v2-terminal-evaluation.md +22 -0
- package/docs/evidence/v02/seed-outline/run-manifest.json +59 -0
- package/docs/evidence/v02/seed-outline/summary.json +487 -0
- package/docs/evidence/v02/seed-unbounded/run-manifest.json +80 -0
- package/docs/evidence/v02/seed-unbounded/summary.json +730 -0
- package/docs/evidence/v02/segmentation/README.md +21 -0
- package/docs/evidence/v02/segmentation/component-comparison-summary.json +240 -0
- package/docs/evidence/v02/segmentation/provenance/MOBILE-SAM-LICENSE.txt +201 -0
- package/docs/evidence/v02/segmentation/provenance/OPENCV-LICENSE.txt +202 -0
- package/docs/evidence/v02/segmentation/provenance/mobile-sam-download-receipt.json +29 -0
- package/docs/evidence/v02/segmentation/provenance/mobile-sam-graph-audit.json +214 -0
- package/docs/evidence/v02/segmentation/provenance/mobile-sam-model-metadata.json +166 -0
- package/docs/evidence/v02/split-choice-noul-dev6.json +72 -0
- package/docs/evidence/v02/split-choice-noul-dev6.md +11 -0
- package/docs/evidence/v02/supported-vision-screen-20261020.md +65 -0
- package/docs/evidence/v02/wesnoth-1.16.11-minimap-projection-source-audit.md +56 -0
- package/docs/evidence/v02/wesnoth-proof-root-review-20261020-v1.json +76 -0
- package/docs/operations.md +6 -0
- package/docs/plan.md +2 -0
- package/docs/publishing.md +10 -1
- package/docs/research/text-transport-audit.md +66 -0
- package/docs/v02-usage.md +162 -0
- package/docs/vision-results.md +29 -2
- package/package.json +30 -5
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { limits, pixelRectSchema } from '../../entities/contracts.js';
|
|
4
|
+
import { ClassifierError, assertActive } from '../../entities/errors.js';
|
|
5
|
+
const rasterSchema = z
|
|
6
|
+
.object({
|
|
7
|
+
data: z.instanceof(Uint8Array),
|
|
8
|
+
width: z.number().int().positive().max(16384),
|
|
9
|
+
height: z.number().int().positive().max(16384),
|
|
10
|
+
format: z.literal('rgba8'),
|
|
11
|
+
})
|
|
12
|
+
.strict();
|
|
13
|
+
const inputSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
image: rasterSchema,
|
|
16
|
+
region: pixelRectSchema,
|
|
17
|
+
seedRegion: pixelRectSchema,
|
|
18
|
+
roi: pixelRectSchema,
|
|
19
|
+
target: z.string().trim().min(1).max(2000),
|
|
20
|
+
exclusions: z.array(z.string().trim().min(1).max(500)).max(16),
|
|
21
|
+
signal: z.custom((value) => value instanceof AbortSignal),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
function inside(rect, width, height) {
|
|
25
|
+
return rect.left + rect.width <= width && rect.top + rect.height <= height;
|
|
26
|
+
}
|
|
27
|
+
function overlaps(first, second) {
|
|
28
|
+
return (first.left < second.left + second.width &&
|
|
29
|
+
second.left < first.left + first.width &&
|
|
30
|
+
first.top < second.top + second.height &&
|
|
31
|
+
second.top < first.top + first.height);
|
|
32
|
+
}
|
|
33
|
+
function cloneRect(rect) {
|
|
34
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
35
|
+
}
|
|
36
|
+
function snapshot(value) {
|
|
37
|
+
const parsed = inputSchema.safeParse(value);
|
|
38
|
+
if (!parsed.success)
|
|
39
|
+
throw new ClassifierError('INVALID_INPUT', 'Invalid segmentation box presentation input', {
|
|
40
|
+
paths: parsed.error.issues.map((issue) => issue.path.join('.')),
|
|
41
|
+
});
|
|
42
|
+
const input = parsed.data;
|
|
43
|
+
const pixels = input.image.width * input.image.height;
|
|
44
|
+
if (!Number.isSafeInteger(pixels) ||
|
|
45
|
+
pixels < 1 ||
|
|
46
|
+
pixels > limits.imagePixels ||
|
|
47
|
+
input.image.data.byteLength !== pixels * 4 ||
|
|
48
|
+
!inside(input.region, input.image.width, input.image.height) ||
|
|
49
|
+
!inside(input.seedRegion, input.image.width, input.image.height) ||
|
|
50
|
+
!inside(input.roi, input.image.width, input.image.height) ||
|
|
51
|
+
input.region.left < input.roi.left ||
|
|
52
|
+
input.region.top < input.roi.top ||
|
|
53
|
+
input.region.left + input.region.width > input.roi.left + input.roi.width ||
|
|
54
|
+
input.region.top + input.region.height > input.roi.top + input.roi.height ||
|
|
55
|
+
!overlaps(input.seedRegion, input.region) ||
|
|
56
|
+
!overlaps(input.seedRegion, input.roi))
|
|
57
|
+
throw new ClassifierError('INVALID_INPUT', 'Box, seed, image, and hard ROI geometry disagree');
|
|
58
|
+
const image = {
|
|
59
|
+
data: Uint8Array.from(input.image.data),
|
|
60
|
+
width: input.image.width,
|
|
61
|
+
height: input.image.height,
|
|
62
|
+
format: 'rgba8',
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
image,
|
|
66
|
+
region: cloneRect(input.region),
|
|
67
|
+
seedRegion: cloneRect(input.seedRegion),
|
|
68
|
+
roi: cloneRect(input.roi),
|
|
69
|
+
target: input.target,
|
|
70
|
+
exclusions: [...input.exclusions],
|
|
71
|
+
signal: input.signal,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function overlaySvg(input) {
|
|
75
|
+
const { image, region, seedRegion, roi } = input;
|
|
76
|
+
const boxStroke = Math.min(3, region.width, region.height);
|
|
77
|
+
const x = region.left + boxStroke / 2;
|
|
78
|
+
const y = region.top + boxStroke / 2;
|
|
79
|
+
const w = Math.max(0, region.width - boxStroke);
|
|
80
|
+
const h = Math.max(0, region.height - boxStroke);
|
|
81
|
+
const roiStroke = Math.min(2, roi.width, roi.height);
|
|
82
|
+
const roiX = roi.left + roiStroke / 2;
|
|
83
|
+
const roiY = roi.top + roiStroke / 2;
|
|
84
|
+
const roiW = Math.max(0, roi.width - roiStroke);
|
|
85
|
+
const roiH = Math.max(0, roi.height - roiStroke);
|
|
86
|
+
// Use the same pixel-center convention as the native seed point: (size - 1) / 2.
|
|
87
|
+
const cx = seedRegion.left + (seedRegion.width - 1) / 2;
|
|
88
|
+
const cy = seedRegion.top + (seedRegion.height - 1) / 2;
|
|
89
|
+
const seedSize = Math.min(seedRegion.width, seedRegion.height);
|
|
90
|
+
const crossArm = Math.min(5, Math.max(0.5, (seedSize - 1) / 2));
|
|
91
|
+
const crossStroke = Math.min(2, Math.max(1, seedSize));
|
|
92
|
+
return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${image.width}" height="${image.height}"><rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="#ff00ff" stroke-width="${boxStroke}"/><rect x="${roiX}" y="${roiY}" width="${roiW}" height="${roiH}" fill="none" stroke="#ffbf00" stroke-width="${roiStroke}" stroke-dasharray="7 5"/><path d="M ${cx - crossArm} ${cy} H ${cx + crossArm} M ${cx} ${cy - crossArm} V ${cy + crossArm}" fill="none" stroke="#00ffff" stroke-width="${crossStroke}"/></svg>`);
|
|
93
|
+
}
|
|
94
|
+
export function createSharpSegmentationBoxPresentation() {
|
|
95
|
+
return {
|
|
96
|
+
async render(value) {
|
|
97
|
+
const input = snapshot(value);
|
|
98
|
+
const owned = input.image.data;
|
|
99
|
+
let pipeline;
|
|
100
|
+
const abort = () => pipeline?.destroy();
|
|
101
|
+
let listenerAdded = false;
|
|
102
|
+
try {
|
|
103
|
+
assertActive(input.signal);
|
|
104
|
+
pipeline = sharp(owned, {
|
|
105
|
+
raw: { width: input.image.width, height: input.image.height, channels: 4 },
|
|
106
|
+
limitInputPixels: limits.imagePixels,
|
|
107
|
+
failOn: 'error',
|
|
108
|
+
})
|
|
109
|
+
.composite([{ input: overlaySvg(input) }])
|
|
110
|
+
.png({ compressionLevel: 6 });
|
|
111
|
+
input.signal.addEventListener('abort', abort, { once: true });
|
|
112
|
+
listenerAdded = true;
|
|
113
|
+
const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
|
114
|
+
assertActive(input.signal);
|
|
115
|
+
if (info.width !== input.image.width ||
|
|
116
|
+
info.height !== input.image.height ||
|
|
117
|
+
data.byteLength < 1 ||
|
|
118
|
+
data.byteLength > limits.imageBytes)
|
|
119
|
+
throw new ClassifierError('RESOURCE_LIMIT', 'Rendered box presentation exceeds image limits');
|
|
120
|
+
return { bytes: Uint8Array.from(data), width: info.width, height: info.height };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
assertActive(input.signal);
|
|
124
|
+
if (error instanceof ClassifierError)
|
|
125
|
+
throw error;
|
|
126
|
+
throw new ClassifierError('PROVIDER', 'Box presentation renderer failed');
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
if (listenerAdded)
|
|
130
|
+
input.signal.removeEventListener('abort', abort);
|
|
131
|
+
pipeline?.destroy();
|
|
132
|
+
owned.fill(0);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { ClassifierError, assertActive } from '../../entities/errors.js';
|
|
3
|
+
const maxPixels = 16_777_216;
|
|
4
|
+
/** Sharp-backed decoder/encoder isolated behind the extraction raster port. */
|
|
5
|
+
export function createSharpCutoutRaster() {
|
|
6
|
+
return {
|
|
7
|
+
async rgba(image, signal) {
|
|
8
|
+
assertActive(signal);
|
|
9
|
+
validateRasterImage(image);
|
|
10
|
+
const pipeline = sharp(image.bytes, { limitInputPixels: maxPixels, failOn: 'error' });
|
|
11
|
+
try {
|
|
12
|
+
const output = await pipeline
|
|
13
|
+
.toColourspace('srgb')
|
|
14
|
+
.ensureAlpha()
|
|
15
|
+
.raw()
|
|
16
|
+
.toBuffer({ resolveWithObject: true });
|
|
17
|
+
assertActive(signal);
|
|
18
|
+
if (output.info.width !== image.width ||
|
|
19
|
+
output.info.height !== image.height ||
|
|
20
|
+
output.info.channels !== 4 ||
|
|
21
|
+
output.data.length !== image.width * image.height * 4)
|
|
22
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Cutout raster decoder returned invalid canonical RGBA pixels');
|
|
23
|
+
return {
|
|
24
|
+
data: new Uint8Array(output.data),
|
|
25
|
+
width: image.width,
|
|
26
|
+
height: image.height,
|
|
27
|
+
format: 'rgba8',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof ClassifierError)
|
|
32
|
+
throw error;
|
|
33
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Cutout raster decoder failed');
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
pipeline.destroy();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async png(image, signal) {
|
|
40
|
+
assertActive(signal);
|
|
41
|
+
validateRgbaRaster(image);
|
|
42
|
+
const pipeline = sharp(image.data, {
|
|
43
|
+
raw: { width: image.width, height: image.height, channels: 4 },
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
const png = await pipeline.png().toBuffer();
|
|
47
|
+
assertActive(signal);
|
|
48
|
+
return new Uint8Array(png);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof ClassifierError)
|
|
52
|
+
throw error;
|
|
53
|
+
throw new ClassifierError('PROVIDER', 'Cutout PNG encoder failed');
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
pipeline.destroy();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function validateRasterImage(image) {
|
|
62
|
+
if (!Number.isSafeInteger(image.width) ||
|
|
63
|
+
!Number.isSafeInteger(image.height) ||
|
|
64
|
+
image.width < 1 ||
|
|
65
|
+
image.height < 1 ||
|
|
66
|
+
image.width * image.height > maxPixels ||
|
|
67
|
+
image.bytes.byteLength < 1)
|
|
68
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Raster normalizer returned invalid source data');
|
|
69
|
+
}
|
|
70
|
+
function validateRgbaRaster(image) {
|
|
71
|
+
if (image.format !== 'rgba8' ||
|
|
72
|
+
!Number.isSafeInteger(image.width) ||
|
|
73
|
+
!Number.isSafeInteger(image.height) ||
|
|
74
|
+
image.width < 1 ||
|
|
75
|
+
image.height < 1 ||
|
|
76
|
+
image.width * image.height > maxPixels ||
|
|
77
|
+
image.data.length !== image.width * image.height * 4)
|
|
78
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Extraction returned invalid canonical RGBA pixels');
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { SegmentationPort } from '../../../entities/segmentation.js';
|
|
3
|
+
declare const optionsSchema: z.ZodObject<{
|
|
4
|
+
assetPath: z.ZodString;
|
|
5
|
+
}, "strict", z.ZodTypeAny, {
|
|
6
|
+
assetPath: string;
|
|
7
|
+
}, {
|
|
8
|
+
assetPath: string;
|
|
9
|
+
}>;
|
|
10
|
+
export type GrabCutSegmenterOptions = z.input<typeof optionsSchema>;
|
|
11
|
+
/** Explicit pinned OpenCV GrabCut adapter. The supplied native asset is never downloaded. */
|
|
12
|
+
export declare function createGrabCutSegmenter(options: GrabCutSegmenterOptions): Promise<SegmentationPort>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { ClassifierError, assertActive } from '../../../entities/errors.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { limits, pixelRectSchema } from '../../../entities/contracts.js';
|
|
5
|
+
import { NativeChildPool } from '../native-child.js';
|
|
6
|
+
import { nativeErrorReplySchema, nativeSegmentationReplySchema } from '../native-protocol.js';
|
|
7
|
+
import { verifyPinnedAsset } from '../verify-asset.js';
|
|
8
|
+
import { probeBackend } from '../probe-backend.js';
|
|
9
|
+
const optionsSchema = z.object({ assetPath: z.string().min(1).max(4096) }).strict();
|
|
10
|
+
/** Explicit pinned OpenCV GrabCut adapter. The supplied native asset is never downloaded. */
|
|
11
|
+
export async function createGrabCutSegmenter(options) {
|
|
12
|
+
const checked = optionsSchema.safeParse(options);
|
|
13
|
+
if (!checked.success)
|
|
14
|
+
throw new ClassifierError('INVALID_INPUT', 'Invalid GrabCut options');
|
|
15
|
+
const parsed = checked.data;
|
|
16
|
+
await verifyPinnedAsset(parsed.assetPath, {
|
|
17
|
+
bytes: 10_964_323,
|
|
18
|
+
sha256: '63366510248adf3a7eddf3e793dd825404efb7df3749f4d6f8557c7fa4ca8aa0',
|
|
19
|
+
}, 'OpenCV.js');
|
|
20
|
+
const workerUrl = new URL(import.meta.url.endsWith('.ts') ? '../native-worker.ts' : '../native-worker.js', import.meta.url);
|
|
21
|
+
await probeBackend(workerUrl, 'grabcut', { assetPath: parsed.assetPath });
|
|
22
|
+
const pool = new NativeChildPool({
|
|
23
|
+
workerUrl,
|
|
24
|
+
maxActive: 1,
|
|
25
|
+
maxWaiting: 2,
|
|
26
|
+
jobTimeoutMs: 10_000,
|
|
27
|
+
abortGraceMs: 250,
|
|
28
|
+
encode: (job) => Buffer.from(JSON.stringify({ id: job.id, ...job.payload })),
|
|
29
|
+
decode: (raw, id) => {
|
|
30
|
+
const error = nativeErrorReplySchema.safeParse(raw);
|
|
31
|
+
if (error.success) {
|
|
32
|
+
if (error.data.id !== id)
|
|
33
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Native child error ID mismatch');
|
|
34
|
+
throw new ClassifierError(error.data.code, error.data.message);
|
|
35
|
+
}
|
|
36
|
+
const reply = nativeSegmentationReplySchema.safeParse(raw);
|
|
37
|
+
if (!reply.success || reply.data.id !== id)
|
|
38
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Native child returned an invalid or mismatched reply');
|
|
39
|
+
return reply.data;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
async segment(input) {
|
|
44
|
+
const started = performance.now();
|
|
45
|
+
assertActive(input.signal);
|
|
46
|
+
let image;
|
|
47
|
+
const raw = await pool.runLazy(input.signal, () => {
|
|
48
|
+
image = snapshotInput(input);
|
|
49
|
+
return {
|
|
50
|
+
kind: 'grabcut',
|
|
51
|
+
width: image.width,
|
|
52
|
+
height: image.height,
|
|
53
|
+
rgba: Buffer.from(image.data).toString('base64'),
|
|
54
|
+
seedRegion: image.seedRegion,
|
|
55
|
+
wholeRegion: image.wholeRegion,
|
|
56
|
+
roi: image.roi,
|
|
57
|
+
prompt: image.prompt,
|
|
58
|
+
assetPath: parsed.assetPath,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
const reply = nativeSegmentationReplySchema.parse(raw);
|
|
62
|
+
const mask = Buffer.from(reply.mask, 'base64');
|
|
63
|
+
if (reply.width !== image.width ||
|
|
64
|
+
reply.height !== image.height ||
|
|
65
|
+
mask.length !== image.width * image.height ||
|
|
66
|
+
mask.toString('base64') !== reply.mask)
|
|
67
|
+
throw new ClassifierError('INVALID_RESPONSE', 'Native child mask dimensions or encoding are invalid');
|
|
68
|
+
return {
|
|
69
|
+
status: 'segmented',
|
|
70
|
+
mask: {
|
|
71
|
+
data: new Uint8Array(mask),
|
|
72
|
+
width: image.width,
|
|
73
|
+
height: image.height,
|
|
74
|
+
format: 'binary-u8',
|
|
75
|
+
},
|
|
76
|
+
metadata: {
|
|
77
|
+
method: reply.method,
|
|
78
|
+
backend: 'grabcut',
|
|
79
|
+
durationMs: Math.max(0, performance.now() - started),
|
|
80
|
+
input: { width: image.width, height: image.height },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
settle: () => pool.settle(),
|
|
85
|
+
dispose: () => pool.dispose(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function snapshotInput(input) {
|
|
89
|
+
const image = input.image;
|
|
90
|
+
const pixels = image.width * image.height;
|
|
91
|
+
const prompt = input.prompt ?? 'whole-region';
|
|
92
|
+
if (image.format !== 'rgba8' ||
|
|
93
|
+
!Number.isSafeInteger(image.width) ||
|
|
94
|
+
!Number.isSafeInteger(image.height) ||
|
|
95
|
+
!Number.isSafeInteger(pixels) ||
|
|
96
|
+
image.width < 1 ||
|
|
97
|
+
image.height < 1 ||
|
|
98
|
+
pixels > limits.imagePixels ||
|
|
99
|
+
image.data.length !== pixels * 4)
|
|
100
|
+
throw new ClassifierError('INVALID_INPUT', 'Segmentation requires canonical RGBA8 within the image pixel limit');
|
|
101
|
+
if ((prompt !== 'whole-region' && prompt !== 'seed-region') ||
|
|
102
|
+
!pixelRectSchema.safeParse(input.seedRegion).success ||
|
|
103
|
+
!pixelRectSchema.safeParse(input.wholeRegion).success ||
|
|
104
|
+
!pixelRectSchema.safeParse(input.roi).success)
|
|
105
|
+
throw new ClassifierError('INVALID_INPUT', 'Segmentation regions must be positive integer rectangles');
|
|
106
|
+
for (const region of [input.seedRegion, input.wholeRegion, input.roi])
|
|
107
|
+
if (region.left + region.width > image.width || region.top + region.height > image.height)
|
|
108
|
+
throw new ClassifierError('INVALID_INPUT', 'Segmentation region lies outside the source image');
|
|
109
|
+
return {
|
|
110
|
+
data: new Uint8Array(image.data),
|
|
111
|
+
width: image.width,
|
|
112
|
+
height: image.height,
|
|
113
|
+
seedRegion: { ...input.seedRegion },
|
|
114
|
+
wholeRegion: { ...input.wholeRegion },
|
|
115
|
+
roi: { ...input.roi },
|
|
116
|
+
prompt,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { NativeSegmentationReply, NativeSegmentationRequest } from '../native-protocol.js';
|
|
2
|
+
export declare function probeGrabCut(assetPath: string): Promise<void>;
|
|
3
|
+
export declare function initializeGrabCutMask(input: {
|
|
4
|
+
flags: Uint8Array;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
seedRegion: {
|
|
8
|
+
left: number;
|
|
9
|
+
top: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
wholeRegion: {
|
|
14
|
+
left: number;
|
|
15
|
+
top: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
roi: {
|
|
20
|
+
left: number;
|
|
21
|
+
top: number;
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
};
|
|
25
|
+
prompt: 'whole-region' | 'seed-region';
|
|
26
|
+
labels: {
|
|
27
|
+
background: number;
|
|
28
|
+
foreground: number;
|
|
29
|
+
probableBackground: number;
|
|
30
|
+
probableForeground: number;
|
|
31
|
+
};
|
|
32
|
+
}): void;
|
|
33
|
+
/** Run native GrabCut, then reacquire Mat.data before copying its post-call flags. */
|
|
34
|
+
export declare function runGrabCutAndCopyMask(mat: {
|
|
35
|
+
data?: unknown;
|
|
36
|
+
}, width: number, height: number, roi: {
|
|
37
|
+
left: number;
|
|
38
|
+
top: number;
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
}, runNative: () => void, labels: {
|
|
42
|
+
foreground: number;
|
|
43
|
+
probableForeground: number;
|
|
44
|
+
}): Uint8Array;
|
|
45
|
+
export declare function runGrabCutTask(request: Extract<NativeSegmentationRequest, {
|
|
46
|
+
kind: 'grabcut';
|
|
47
|
+
}>): Promise<NativeSegmentationReply>;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { Script } from 'node:vm';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
const artifact = {
|
|
8
|
+
bytes: 10_964_323,
|
|
9
|
+
sha256: '63366510248adf3a7eddf3e793dd825404efb7df3749f4d6f8557c7fa4ca8aa0',
|
|
10
|
+
};
|
|
11
|
+
const cvSchema = z.object({
|
|
12
|
+
Mat: z.custom((value) => typeof value === 'function'),
|
|
13
|
+
Rect: z.custom((value) => typeof value === 'function'),
|
|
14
|
+
cvtColor: z.custom((value) => typeof value === 'function'),
|
|
15
|
+
grabCut: z.custom((value) => typeof value === 'function'),
|
|
16
|
+
COLOR_RGBA2RGB: z.number().int(),
|
|
17
|
+
CV_8UC4: z.number().int(),
|
|
18
|
+
CV_8UC3: z.number().int(),
|
|
19
|
+
CV_8UC1: z.number().int(),
|
|
20
|
+
GC_BGD: z.number().int(),
|
|
21
|
+
GC_FGD: z.number().int(),
|
|
22
|
+
GC_PR_BGD: z.number().int(),
|
|
23
|
+
GC_PR_FGD: z.number().int(),
|
|
24
|
+
GC_INIT_WITH_MASK: z.number().int(),
|
|
25
|
+
GC_INIT_WITH_RECT: z.number().int(),
|
|
26
|
+
});
|
|
27
|
+
async function readVerifiedAsset(path) {
|
|
28
|
+
const handle = await open(path, 'r');
|
|
29
|
+
try {
|
|
30
|
+
const stat = await handle.stat();
|
|
31
|
+
if (!stat.isFile() || stat.size !== artifact.bytes)
|
|
32
|
+
throw new Error('Pinned OpenCV size mismatch');
|
|
33
|
+
const source = Buffer.alloc(artifact.bytes);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < source.length) {
|
|
36
|
+
const read = await handle.read(source, offset, source.length - offset, null);
|
|
37
|
+
if (!read.bytesRead)
|
|
38
|
+
throw new Error('Pinned OpenCV asset read was incomplete');
|
|
39
|
+
offset += read.bytesRead;
|
|
40
|
+
}
|
|
41
|
+
if ((await handle.read(Buffer.alloc(1), 0, 1, null)).bytesRead !== 0)
|
|
42
|
+
throw new Error('Pinned OpenCV asset changed while being read');
|
|
43
|
+
if (createHash('sha256').update(source).digest('hex') !== artifact.sha256)
|
|
44
|
+
throw new Error('Pinned OpenCV SHA256 mismatch');
|
|
45
|
+
return source.toString('utf8');
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await handle.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function loadOpenCv(path) {
|
|
52
|
+
path = resolve(path);
|
|
53
|
+
const source = await readVerifiedAsset(path);
|
|
54
|
+
const wrapper = { exports: {} };
|
|
55
|
+
const factory = new Script('(function(exports,require,module,__filename,__dirname){' + source + '\n})', { filename: path }).runInThisContext();
|
|
56
|
+
if (typeof factory !== 'function')
|
|
57
|
+
throw new Error('Pinned OpenCV asset is not a module');
|
|
58
|
+
Reflect.apply(factory, undefined, [
|
|
59
|
+
wrapper.exports,
|
|
60
|
+
createRequire(path),
|
|
61
|
+
wrapper,
|
|
62
|
+
path,
|
|
63
|
+
dirname(path),
|
|
64
|
+
]);
|
|
65
|
+
const exports = wrapper.exports;
|
|
66
|
+
if (typeof exports === 'object' && exports !== null && 'then' in exports) {
|
|
67
|
+
const then = Reflect.get(exports, 'then');
|
|
68
|
+
if (typeof then === 'function') {
|
|
69
|
+
return await new Promise((resolve, reject) => {
|
|
70
|
+
try {
|
|
71
|
+
Reflect.apply(then, exports, [
|
|
72
|
+
(value) => {
|
|
73
|
+
const checked = cvSchema.safeParse(value);
|
|
74
|
+
if (checked.success)
|
|
75
|
+
resolve(checked.data);
|
|
76
|
+
else
|
|
77
|
+
reject(new Error('Pinned OpenCV namespace is invalid'));
|
|
78
|
+
},
|
|
79
|
+
reject,
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
reject(error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return cvSchema.parse(exports);
|
|
89
|
+
}
|
|
90
|
+
export async function probeGrabCut(assetPath) {
|
|
91
|
+
await loadOpenCv(assetPath);
|
|
92
|
+
}
|
|
93
|
+
const matShapeSchema = z
|
|
94
|
+
.object({ delete: z.custom((value) => typeof value === 'function') })
|
|
95
|
+
.passthrough();
|
|
96
|
+
const matSchema = z.custom((value) => matShapeSchema.safeParse(value).success);
|
|
97
|
+
function matrix(cv, args = []) {
|
|
98
|
+
return matSchema.parse(Reflect.construct(cv.Mat, args));
|
|
99
|
+
}
|
|
100
|
+
function getBytes(mat, length) {
|
|
101
|
+
const bytes = z.instanceof(Uint8Array).parse(mat.data);
|
|
102
|
+
if (bytes.length !== length)
|
|
103
|
+
throw new Error('OpenCV matrix dimensions do not match');
|
|
104
|
+
return bytes;
|
|
105
|
+
}
|
|
106
|
+
export function initializeGrabCutMask(input) {
|
|
107
|
+
if (input.flags.length !== input.width * input.height)
|
|
108
|
+
throw new Error('GrabCut mask dimensions do not match source');
|
|
109
|
+
input.flags.fill(input.labels.background);
|
|
110
|
+
if (input.prompt === 'whole-region') {
|
|
111
|
+
for (let y = input.wholeRegion.top; y < input.wholeRegion.top + input.wholeRegion.height; y++)
|
|
112
|
+
for (let x = input.wholeRegion.left; x < input.wholeRegion.left + input.wholeRegion.width; x++)
|
|
113
|
+
input.flags[y * input.width + x] = input.labels.probableForeground;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
for (let y = input.roi.top; y < input.roi.top + input.roi.height; y++)
|
|
117
|
+
for (let x = input.roi.left; x < input.roi.left + input.roi.width; x++)
|
|
118
|
+
input.flags[y * input.width + x] = input.labels.probableBackground;
|
|
119
|
+
}
|
|
120
|
+
const seedLeft = Math.max(input.seedRegion.left, input.roi.left);
|
|
121
|
+
const seedTop = Math.max(input.seedRegion.top, input.roi.top);
|
|
122
|
+
const seedRight = Math.min(input.seedRegion.left + input.seedRegion.width, input.roi.left + input.roi.width);
|
|
123
|
+
const seedBottom = Math.min(input.seedRegion.top + input.seedRegion.height, input.roi.top + input.roi.height);
|
|
124
|
+
if (seedLeft >= seedRight || seedTop >= seedBottom)
|
|
125
|
+
throw new Error('GrabCut seed region does not intersect hard ROI');
|
|
126
|
+
for (let y = seedTop; y < seedBottom; y++)
|
|
127
|
+
for (let x = seedLeft; x < seedRight; x++)
|
|
128
|
+
input.flags[y * input.width + x] = input.labels.foreground;
|
|
129
|
+
}
|
|
130
|
+
/** Run native GrabCut, then reacquire Mat.data before copying its post-call flags. */
|
|
131
|
+
export function runGrabCutAndCopyMask(mat, width, height, roi, runNative, labels) {
|
|
132
|
+
runNative();
|
|
133
|
+
const flags = getBytes(mat, width * height);
|
|
134
|
+
const output = new Uint8Array(width * height);
|
|
135
|
+
for (let y = roi.top; y < roi.top + roi.height; y++) {
|
|
136
|
+
for (let x = roi.left; x < roi.left + roi.width; x++) {
|
|
137
|
+
const at = y * width + x;
|
|
138
|
+
const flag = flags[at];
|
|
139
|
+
if (flag === labels.foreground || flag === labels.probableForeground)
|
|
140
|
+
output[at] = 1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return output;
|
|
144
|
+
}
|
|
145
|
+
function seededComponents(mask, width, height, input) {
|
|
146
|
+
const visited = new Uint8Array(mask.length);
|
|
147
|
+
const output = new Uint8Array(mask.length);
|
|
148
|
+
const stack = [];
|
|
149
|
+
for (let start = 0; start < mask.length; start++) {
|
|
150
|
+
if (mask[start] !== 1 || visited[start] === 1)
|
|
151
|
+
continue;
|
|
152
|
+
stack.push(start);
|
|
153
|
+
visited[start] = 1;
|
|
154
|
+
const component = [];
|
|
155
|
+
let hitsSeed = false;
|
|
156
|
+
while (stack.length) {
|
|
157
|
+
const at = stack.pop();
|
|
158
|
+
if (at === undefined)
|
|
159
|
+
continue;
|
|
160
|
+
component.push(at);
|
|
161
|
+
const x = at % width;
|
|
162
|
+
const y = Math.floor(at / width);
|
|
163
|
+
if (x >= input.seedRegion.left &&
|
|
164
|
+
y >= input.seedRegion.top &&
|
|
165
|
+
x < input.seedRegion.left + input.seedRegion.width &&
|
|
166
|
+
y < input.seedRegion.top + input.seedRegion.height)
|
|
167
|
+
hitsSeed = true;
|
|
168
|
+
const neighbors = [at - 1, at + 1, at - width, at + width];
|
|
169
|
+
for (const neighbor of neighbors) {
|
|
170
|
+
if (neighbor < 0 ||
|
|
171
|
+
neighbor >= mask.length ||
|
|
172
|
+
visited[neighbor] === 1 ||
|
|
173
|
+
mask[neighbor] !== 1)
|
|
174
|
+
continue;
|
|
175
|
+
const nx = neighbor % width;
|
|
176
|
+
if (Math.abs(nx - x) + Math.abs(Math.floor(neighbor / width) - y) !== 1)
|
|
177
|
+
continue;
|
|
178
|
+
visited[neighbor] = 1;
|
|
179
|
+
stack.push(neighbor);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (hitsSeed)
|
|
183
|
+
for (const index of component)
|
|
184
|
+
output[index] = 1;
|
|
185
|
+
}
|
|
186
|
+
return output;
|
|
187
|
+
}
|
|
188
|
+
export async function runGrabCutTask(request) {
|
|
189
|
+
const cv = await loadOpenCv(request.assetPath);
|
|
190
|
+
const pixels = request.width * request.height;
|
|
191
|
+
const rgbaBytes = Buffer.from(request.rgba, 'base64');
|
|
192
|
+
if (rgbaBytes.length !== pixels * 4)
|
|
193
|
+
throw new Error('Invalid GrabCut RGBA bytes');
|
|
194
|
+
const rgba = matrix(cv, [request.height, request.width, cv.CV_8UC4]);
|
|
195
|
+
const rgb = matrix(cv);
|
|
196
|
+
const mask = matrix(cv, [request.height, request.width, cv.CV_8UC1]);
|
|
197
|
+
const background = matrix(cv);
|
|
198
|
+
const foreground = matrix(cv);
|
|
199
|
+
const owned = [rgba, rgb, mask, background, foreground];
|
|
200
|
+
let result;
|
|
201
|
+
let releaseFailed = false;
|
|
202
|
+
try {
|
|
203
|
+
getBytes(rgba, pixels * 4).set(rgbaBytes);
|
|
204
|
+
Reflect.apply(cv.cvtColor, cv, [rgba, rgb, cv.COLOR_RGBA2RGB]);
|
|
205
|
+
const flags = getBytes(mask, pixels);
|
|
206
|
+
initializeGrabCutMask({
|
|
207
|
+
flags,
|
|
208
|
+
width: request.width,
|
|
209
|
+
height: request.height,
|
|
210
|
+
seedRegion: request.seedRegion,
|
|
211
|
+
wholeRegion: request.wholeRegion,
|
|
212
|
+
roi: request.roi,
|
|
213
|
+
prompt: request.prompt,
|
|
214
|
+
labels: {
|
|
215
|
+
background: cv.GC_BGD,
|
|
216
|
+
foreground: cv.GC_FGD,
|
|
217
|
+
probableBackground: cv.GC_PR_BGD,
|
|
218
|
+
probableForeground: cv.GC_PR_FGD,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const rect = Reflect.construct(cv.Rect, [0, 0, 0, 0]);
|
|
222
|
+
const maskResult = runGrabCutAndCopyMask(mask, request.width, request.height, request.roi, () => Reflect.apply(cv.grabCut, cv, [
|
|
223
|
+
rgb,
|
|
224
|
+
mask,
|
|
225
|
+
rect,
|
|
226
|
+
background,
|
|
227
|
+
foreground,
|
|
228
|
+
3,
|
|
229
|
+
cv.GC_INIT_WITH_MASK,
|
|
230
|
+
]), { foreground: cv.GC_FGD, probableForeground: cv.GC_PR_FGD });
|
|
231
|
+
const grounded = seededComponents(maskResult, request.width, request.height, request);
|
|
232
|
+
if (!grounded.some((value) => value === 1))
|
|
233
|
+
throw new Error('GrabCut produced no seed-grounded mask');
|
|
234
|
+
result = {
|
|
235
|
+
id: request.id,
|
|
236
|
+
status: 'segmented',
|
|
237
|
+
width: request.width,
|
|
238
|
+
height: request.height,
|
|
239
|
+
mask: Buffer.from(grounded).toString('base64'),
|
|
240
|
+
method: request.prompt === 'seed-region' ? 'grabcut-seed-mask-v1' : 'grabcut-seeded-box-v1',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
let failed = false;
|
|
245
|
+
for (const item of owned.reverse()) {
|
|
246
|
+
try {
|
|
247
|
+
item.delete();
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
failed = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
releaseFailed = failed;
|
|
254
|
+
}
|
|
255
|
+
if (releaseFailed)
|
|
256
|
+
throw new Error('OpenCV matrices could not all be released');
|
|
257
|
+
if (!result)
|
|
258
|
+
throw new Error('GrabCut completed without a mask result');
|
|
259
|
+
return result;
|
|
260
|
+
}
|