初始提交:algpkg-sdk(算法包开发 SDK)—— 从 sophon-edge 仓库拆分独立建库

本仓库原先内嵌在 sophon-edge 的 `algpkg-sdk/`(该仓库历史上的三次相关提交:
e8101ba「算法开发能力独立成项目」、090c406「契约名落地为真实模块 + 多模型 ctx」、
b666161「sail 存根 + 编写风格修正」)。现拆出独立建库,便于算法作者单独 clone/迭代、
也便于单独发版(打成 wheel 分发给第三方或进 worker 交付包的 vendor/)。

- 内容:`src/algpkg_sdk/`(api/manifest/loader/builder/testkit/cli)+ 契约名模块
  `src/video_algpkg_api/`、`examples/person-count`(完整示例)、`tests/`(pytest,无设备自动跳过)、
  `README.md`(已改成独立仓库口径:命令在项目根执行,配套 worker 用路径依赖/`ALGPKG_SDK_DIR`)、
  `pyproject.toml`(console script `algpkg`)、`uv.lock`;
- 不含 `.venv/`(`.gitignore` 已覆盖,clone 后 `uv sync` 重建)。

配套 worker 仓库(sophon-edge):
`workers/video/pyproject.toml` 以路径依赖指向本仓库(`path = "../../../algpkg-sdk"`,可用
`ALGPKG_SDK_DIR` 覆盖);交付打包 `deploy/pack_worker.py` 会把本仓库打成 wheel 放进 worker 包 `vendor/`。
This commit is contained in:
linaro
2026-10-16 16:15:59 +08:00
commit 170e4a10a4
18 changed files with 3792 additions and 0 deletions
@@ -0,0 +1,146 @@
"""人员计数示例算法包(M8.2/M8.4)——**自带推理实现**,不依赖 worker 内部模块。
这是"第三方算法包"的样板:只依赖
- `video_algpkg_api`(worker 提供的稳定接口:Algorithm / AlgoContext / AlgoResult);
- 设备框架 `sophon.sail` 与 `numpy`;
- 包内模型(`ctx.model_bytes()` 从 `.mbank` 读字节,**不落盘**)。
算法语义:YOLOv26s 检测 → 只保留 person(COCO 类 0)→ 置信度/最小人高过滤 →
人数作为 `AlgoResult(counts={"person": n}, value=n)` 返回,并把人员框一并给出(供绘制/告警)。
与内置包(`builtin.yolov26-detect`)的区别:kind=counter、默认抽帧更慢(0.5s)、
只报人员、带最小人高参数 —— 用来验证一个任务同时挂"检测 + 计数"两个算法。
"""
from __future__ import annotations
import time
from typing import List
import numpy as np
import sophon.sail as sail
from video_algpkg_api import Algorithm as AlgorithmBase, AlgoResult
PERSON_CLASS = 0 # COCO 类别索引(person)
_BATCH_SUPPORTED = (1, 2, 3, 4, 8, 16, 32, 64, 128, 256)
class Algorithm(AlgorithmBase):
"""画面人员计数(单 bmodel,SAIL 引擎 + bmcv 前处理)。"""
kind = "counter"
#: 引擎与输入张量:load() 赋值、close() 释放。类级声明为非 Optional,infer() 里可直接用
#: (不必判空);close() 置 None 与声明冲突,那两处显式 `type: ignore`——运行时语义就是"丢引用"。
_net: sail.Engine
_input_tensor: sail.Tensor
# ------------------------------------------------------------------ 生命周期
def load(self) -> None:
self._conf = float(self.ctx.param("conf", 0.3))
self._min_height = int(self.ctx.param("min_height", 0))
data = self.ctx.model_bytes()
self._bytes = data # 引擎构造后仍持有引用(不依赖 SDK 拷贝语义)
self._handle = sail.Handle(self.ctx.dev_id)
self._bmcv = sail.Bmcv(self._handle)
self._net = sail.Engine(data, len(data), self.ctx.dev_id, sail.IOMode.SYSO)
self._graph = self._net.get_graph_names()[0]
name = self._net.get_input_names(self._graph)[0]
self._input_name = name
self._input_dtype = self._net.get_input_dtype(self._graph, name)
self._img_dtype = self._bmcv.get_bm_image_data_format(self._input_dtype)
self._input_scale = self._net.get_input_scale(self._graph, name)
self._input_shape = self._net.get_input_shape(self._graph, name)
self._input_shapes = {name: self._input_shape}
if self._input_shape[0] not in _BATCH_SUPPORTED:
raise RuntimeError(f"batch={self._input_shape[0]} 不受支持")
self._net_h, self._net_w = self._input_shape[2], self._input_shape[3]
self._output_names = self._net.get_output_names(self._graph)
self._output_tensors = {}
for out in self._output_names:
shape = self._net.get_output_shape(self._graph, out)
dtype = self._net.get_output_dtype(self._graph, out)
self._output_tensors[out] = sail.Tensor(self._handle, shape, dtype, True, True)
# scale/255,均值 0(与训练一致的 letterbox 归一化)
self._ab = [v * self._input_scale / 255.0 for v in (1, 0, 1, 0, 1, 0)]
self._input_tensor = sail.Tensor(self._handle, self._input_shape,
self._input_dtype, False, False)
self.ctx.logger.info("[%s] 模型就绪(%.1f MB, conf=%.2f, min_height=%d)",
self.ctx.algo_id, len(data) / 1048576.0, self._conf, self._min_height)
# ------------------------------------------------------------------ 推理
def infer(self, frame) -> AlgoResult:
started = time.perf_counter()
pre, ratio, tx, ty = self._preprocess(frame)
self._bmcv.bm_image_to_tensor(pre, self._input_tensor)
self._net.process(self._graph, {self._input_name: self._input_tensor},
self._input_shapes, self._output_tensors)
preds = self._output_tensors[self._output_names[0]].asnumpy()[:1]
persons = self._postprocess(preds[0], frame.width(), frame.height(), ratio, tx, ty)
boxes = np.zeros((len(persons), 6), dtype=np.float32) if not persons else \
np.stack(persons).astype(np.float32)
count = int(boxes.shape[0])
return AlgoResult(boxes=boxes if count else None,
counts={"person": count} if count else {},
value=float(count),
meta={"infer_ms": round((time.perf_counter() - started) * 1000, 1)})
def close(self) -> None:
self._net = None # type: ignore[assignment]
self._output_tensors = {}
self._input_tensor = None # type: ignore[assignment]
self._bytes = b""
# ------------------------------------------------------------------ 内部
def _preprocess(self, bmimg):
"""BGRA/NV12 → RGB_PLANAR → letterbox → 归一化 到网络输入。"""
rgb = sail.BMImage(self._handle, bmimg.height(), bmimg.width(),
sail.Format.FORMAT_RGB_PLANAR, sail.DATA_TYPE_EXT_1N_BYTE)
self._bmcv.convert_format(bmimg, rgb)
img_w, img_h = rgb.width(), rgb.height()
r = min(self._net_w / img_w, self._net_h / img_h)
tw, th = int(round(r * img_w)), int(round(r * img_h))
tx, ty = (self._net_w - tw) / 2, (self._net_h - th) / 2
attr = sail.PaddingAtrr()
attr.set_stx(int(round(tx - 0.1)))
attr.set_sty(int(round(ty - 0.1)))
attr.set_w(tw)
attr.set_h(th)
attr.set_r(114)
attr.set_g(114)
attr.set_b(114)
resized = self._bmcv.crop_and_resize_padding(
rgb, 0, 0, img_w, img_h, self._net_w, self._net_h, attr,
sail.bmcv_resize_algorithm.BMCV_INTER_LINEAR,
)
pre = sail.BMImage(self._handle, self._net_h, self._net_w,
sail.Format.FORMAT_RGB_PLANAR, self._img_dtype)
self._bmcv.convert_to(
resized, pre,
((self._ab[0], self._ab[1]), (self._ab[2], self._ab[3]), (self._ab[4], self._ab[5])),
)
return pre, r, tx, ty
def _postprocess(self, batch, org_w: int, org_h: int, ratio: float,
tx: float, ty: float) -> List[list]:
"""(N,6) 端到端输出 → 人员框(原图坐标;conf / 最小人高过滤)。"""
rows = batch[batch[:, 4] > self._conf] if batch.size else batch
out: List[list] = []
for row in rows:
if int(row[5]) != PERSON_CLASS:
continue
x1 = float((row[0] - tx) / ratio)
y1 = float((row[1] - ty) / ratio)
x2 = float((row[2] - tx) / ratio)
y2 = float((row[3] - ty) / ratio)
x1, x2 = max(0.0, min(x1, org_w - 1)), max(0.0, min(x2, org_w - 1))
y1, y2 = max(0.0, min(y1, org_h - 1)), max(0.0, min(y2, org_h - 1))
if self._min_height and (y2 - y1) < self._min_height:
continue
out.append([x1, y1, x2, y2, float(row[4]), PERSON_CLASS])
return out
+80
View File
@@ -0,0 +1,80 @@
person
bicycle
car
motorcycle
airplane
bus
train
truck
boat
traffic light
fire hydrant
stop sign
parking meter
bench
bird
cat
dog
horse
sheep
cow
elephant
bear
zebra
giraffe
backpack
umbrella
handbag
tie
suitcase
frisbee
skis
snowboard
sports ball
kite
baseball bat
baseball glove
skateboard
surfboard
tennis racket
bottle
wine glass
cup
fork
knife
spoon
bowl
banana
apple
sandwich
orange
broccoli
carrot
hot dog
pizza
donut
cake
chair
couch
potted plant
bed
dining table
toilet
tv
laptop
mouse
remote
keyboard
cell phone
microwave
oven
toaster
sink
refrigerator
book
clock
vase
scissors
teddy bear
hair drier
toothbrush
+41
View File
@@ -0,0 +1,41 @@
{
"manifest_format": 1,
"id": "demo.person-count",
"name": "人员计数(示例算法包)",
"version": "1.0.0",
"api_version": 1,
"kind": "counter",
"entry": "Algorithm",
"code_package": "alg_demo_person_count",
"vendor": "chongming",
"description": "画面人员计数示例包(M8.2/M8.4):自带推理实现(不依赖 worker 内部模块),只用到 video_algpkg_api 的 ctx.model_bytes/param 与 AlgoResult;输出人数(counts/value)与人员框,用于验证\"一个任务挂多个算法\"。",
"models": [
{
"name": "yolo26s_fp16_1b",
"file": "models/yolo26s_fp16_1b.bmodel",
"precision": "fp16",
"resolution": [640, 640],
"batch": 1,
"default": true
}
],
"input": {"width": 640, "height": 640, "color": "rgb", "layout": "planar",
"mean": [0, 0, 0], "std": [255, 255, 255]},
"output": {"type": "counter", "format": "N6", "classes_file": "assets/labels.txt"},
"params_schema": [
{"key": "conf", "type": "float", "default": 0.3, "min": 0.01, "max": 0.99, "step": 0.01,
"label": "人员置信度阈值", "desc": "低于该分数的人员不计入"},
{"key": "min_height", "type": "int", "default": 0, "min": 0, "max": 2000, "step": 10,
"label": "最小人高(像素)", "desc": "过滤远处的误检(0 = 不过滤)"},
{"key": "interval", "type": "float", "default": 0.5, "min": 0.02, "max": 60, "step": 0.02,
"label": "抽帧间隔(秒)", "desc": "计数类算法可放慢,省算力"},
{"key": "draw", "type": "bool", "default": false,
"label": "绘制叠加", "desc": "把人员框画到输出帧上"}
],
"defaults": {"conf": 0.3, "min_height": 0, "interval": 0.5, "draw": false},
"alarm": {"type_template": "person_count", "cooldown_key": "algo",
"need_snapshot": true, "need_record": false},
"requires": {"license_features": [], "devices": ["BM1688", "BM1684X", "CV186X"], "frameworks": ["sail"]},
"ui": {"order": 20, "tags": ["示例", "计数"], "icon": "mdi-account-multiple"},
"legacy": {}
}