fix calculation logic, fix api endpoint
This commit is contained in:
@@ -10,7 +10,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
SESSION_ROOT="/home/ubuntu/data/fish/2016-1-22-last"
|
SESSION_ROOT="/home/ubuntu/data/fish/2016-1-22-last"
|
||||||
FISH_NAME="fish1"
|
FISH_NAME="fish9"
|
||||||
fish_dir="${SESSION_ROOT}/${FISH_NAME}/"
|
fish_dir="${SESSION_ROOT}/${FISH_NAME}/"
|
||||||
OUT_PARENT="output_weight_estimator"
|
OUT_PARENT="output_weight_estimator"
|
||||||
save_out="${OUT_PARENT}/${FISH_NAME}"
|
save_out="${OUT_PARENT}/${FISH_NAME}"
|
||||||
|
|||||||
@@ -691,13 +691,12 @@ def _predict_folder_impl(
|
|||||||
effective_top_by_length = False
|
effective_top_by_length = False
|
||||||
selected_topk = selected_topk_by_weight
|
selected_topk = selected_topk_by_weight
|
||||||
preds_g_topk = [float(it["predicted_weight_g"]) for it in selected_topk]
|
preds_g_topk = [float(it["predicted_weight_g"]) for it in selected_topk]
|
||||||
use_max_instead_of_mean = len(candidates_for_avg) < 5
|
# Always arithmetic mean over selected top-K (matches _topk_mean_prediction_g_for_display
|
||||||
|
# and the printed top{k}_avg= line). Do not use max when N<5 — that made pred_weight_g
|
||||||
|
# disagree with "top{k}_avg by length" in logs.
|
||||||
|
use_max_instead_of_mean = False
|
||||||
if preds_g_topk:
|
if preds_g_topk:
|
||||||
avg_g_topk = (
|
avg_g_topk = float(np.mean(preds_g_topk))
|
||||||
float(np.max(preds_g_topk))
|
|
||||||
if use_max_instead_of_mean
|
|
||||||
else float(np.mean(preds_g_topk))
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
avg_g_topk = float(avg_g_all)
|
avg_g_topk = float(avg_g_all)
|
||||||
num_used_for_avg = len(selected_topk)
|
num_used_for_avg = len(selected_topk)
|
||||||
@@ -1483,7 +1482,6 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
if args.remove_outliers and summary.get("num_outliers_removed", 0) > 0:
|
if args.remove_outliers and summary.get("num_outliers_removed", 0) > 0:
|
||||||
print(f"Outliers removed: {summary['num_outliers_removed']} (method={args.outlier_method}, field={args.outlier_field})")
|
print(f"Outliers removed: {summary['num_outliers_removed']} (method={args.outlier_method}, field={args.outlier_field})")
|
||||||
top_label = _summary_top_label(summary, args.top_k, args.top_by_length)
|
|
||||||
tk = int(summary.get("top_k") or args.top_k)
|
tk = int(summary.get("top_k") or args.top_k)
|
||||||
topk_g = summary.get("avg_topk_mean_pred_g")
|
topk_g = summary.get("avg_topk_mean_pred_g")
|
||||||
topk_sel = summary.get("avg_topk_mean_pred_selection") or "pred"
|
topk_sel = summary.get("avg_topk_mean_pred_selection") or "pred"
|
||||||
@@ -1492,8 +1490,9 @@ def main() -> None:
|
|||||||
by = "length" if topk_sel == "by_length" else "pred"
|
by = "length" if topk_sel == "by_length" else "pred"
|
||||||
topk_extra = f" | top{tk}_avg={float(topk_g):.2f} g by {by}"
|
topk_extra = f" | top{tk}_avg={float(topk_g):.2f} g by {by}"
|
||||||
mcol = _mean_column_g_for_log(summary)
|
mcol = _mean_column_g_for_log(summary)
|
||||||
|
# First column is mean over ALL post-filter candidates (not top-K); do not label as top-K.
|
||||||
print(
|
print(
|
||||||
f"Average predicted weight {top_label}: "
|
f"Mean all candidates (after filters): "
|
||||||
f"{mcol:.2f} g, {mcol / 1000.0:.4f} kg{topk_extra}"
|
f"{mcol:.2f} g, {mcol / 1000.0:.4f} kg{topk_extra}"
|
||||||
)
|
)
|
||||||
_print_max_weight_after_filter(summary)
|
_print_max_weight_after_filter(summary)
|
||||||
|
|||||||
@@ -1,19 +1,4 @@
|
|||||||
# Fish API
|
|
||||||
|
|
||||||
FastAPI 网关:分块接收 **SVO2**(FishMeasure)与 **MP4**(FishAction),在后台调用现有脚本;对外提供文档约定的 **GET** 轮询接口,并托管预览视频 URL。
|
|
||||||
|
|
||||||
## 依赖环境
|
|
||||||
|
|
||||||
- Python 3.11+(uv 可管理 3.13 等版本)
|
|
||||||
- **FishMeasure**:需与本机 `FishMeasure/` 一致的环境(ZED SDK、`pyzed`、CUDA、YOLO/SAM 权重等)
|
|
||||||
- **FishAction**:需可运行 `FishAction/predict_video_x3d_3class.py`(PyTorch、PyTorchVideo、checkpoint)
|
|
||||||
|
|
||||||
若 FishMeasure 与 FishAction 使用不同虚拟环境,可设置:
|
|
||||||
|
|
||||||
- `PYTHON_FISH_MEASURE` — 运行 `predict_weigth_from_svo2.py` 的解释器路径
|
|
||||||
- `PYTHON_FISH_ACTION` — 运行 `predict_video_x3d_3class.py` 的解释器路径
|
|
||||||
|
|
||||||
**单一 Conda 环境**:若 FishMeasure 与 FishAction 已与网关停在同一个 env(例如仓库根目录 [`packaging/conda-fishserver.yaml`](../packaging/conda-fishserver.yaml) 定义的 `fishserver`),则**不要**设置上述两个变量,子进程会使用当前 `uvicorn` 的 Python。可用 [`scripts/start_fresh.sh`](../scripts/start_fresh.sh)(清空后启动)或 [`scripts/start_no_fresh.sh`](../scripts/start_no_fresh.sh)(保留缓存)启动。
|
|
||||||
|
|
||||||
## 配置(环境变量)
|
## 配置(环境变量)
|
||||||
|
|
||||||
@@ -37,55 +22,10 @@ cd fish_api
|
|||||||
uv sync
|
uv sync
|
||||||
# 可选:包含 httpx,便于本地用 FastAPI TestClient 做冒烟测试
|
# 可选:包含 httpx,便于本地用 FastAPI TestClient 做冒烟测试
|
||||||
# uv sync --group dev
|
# uv sync --group dev
|
||||||
bash start_fresh.sh # 默认仅重置 client_id 投递进度,保留 SQLite 历史与快照
|
./scripts/start_fresh.sh # 默认仅重置 client_id 投递进度,保留 SQLite 历史与快照
|
||||||
# CLEAR_SQLITE_DATABASE=1 bash start_fresh.sh # 需要时才彻底清 SQLite
|
# CLEAR_SQLITE_DATABASE=1 bash start_fresh.sh # 需要时才彻底清 SQLite
|
||||||
# 或:uv run uvicorn app.main:app --host 0.0.0.0 --port 8000(需自行 prestart)
|
# 或:uv run uvicorn app.main:app --host 0.0.0.0 --port 8000(需自行 prestart)
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenAPI:`http://127.0.0.1:8000/docs`
|
|
||||||
|
|
||||||
## 对外 GET(由其它系统轮询)
|
|
||||||
|
|
||||||
- `GET /api/v1/biomass/real/camera/` — 每次 GET 消费**该客户端**下一条未投递的称重快照(SQLite **按客户端独立游标**);无新数据时 `data.result` 为空,响应头 `X-Fish-Biomass-New: 0`
|
|
||||||
- `GET /api/v1/biomass/health/result/` — 同上,行为 / 健康快照队列
|
|
||||||
|
|
||||||
**客户端区分**:请求头 `X-Fish-Client-Id: <字符串>` 或查询参数 `client_id=<字符串>`(**优先头**)。未携带时等价于 `default`,与旧版「全局一条游标」行为一致。不同 `client_id` 各自从当前队列消费,互不影响。
|
|
||||||
|
|
||||||
`result` 中每条鱼含算法字段:`id`(跟踪 ID)、`weight`(g)、`length`(mm)。不可交付或失败的推理不会进入客户端队列。
|
|
||||||
|
|
||||||
## 流式输入(分块上传)
|
|
||||||
|
|
||||||
1. `POST /api/v1/ingest/svo/session` 或 `POST /api/v1/ingest/mp4/session` → `session_id`
|
|
||||||
2. `PUT /api/v1/ingest/{svo|mp4}/session/{session_id}?offset=0`(多次)
|
|
||||||
- **追加语义**:`offset` 必须等于当前已写入字节数(从 0 开始顺序上传)
|
|
||||||
3. `POST /api/v1/ingest/{svo|mp4}/session/{session_id}/finalize` → `202 Accepted`,后台开始跑对应算法
|
|
||||||
|
|
||||||
示例(小文件一次性,`$API_KEY` 可选):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
BASE=http://127.0.0.1:8000
|
|
||||||
H=()
|
|
||||||
# H=(-H "X-API-Key: your-secret")
|
|
||||||
|
|
||||||
sid=$(curl -sS "${H[@]}" -X POST "$BASE/api/v1/ingest/svo/session" | jq -r .session_id)
|
|
||||||
curl -sS "${H[@]}" -T sample.svo2 -X PUT "$BASE/api/v1/ingest/svo/session/$sid?offset=0"
|
|
||||||
curl -sS "${H[@]}" -X POST "$BASE/api/v1/ingest/svo/session/$sid/finalize"
|
|
||||||
curl -sS "$BASE/api/v1/biomass/real/camera/"
|
|
||||||
```
|
|
||||||
|
|
||||||
MP4 将 `svo` 换成 `mp4`,本地文件换成 `clip.mp4`,轮询 `GET /api/v1/biomass/health/result/`。
|
|
||||||
|
|
||||||
**说明**:`curl -T` 发送 PUT 时 offset 为 0 且一次性传完整文件适合单块场景;多块时请自行递增 `offset`。
|
|
||||||
|
|
||||||
## 行为与健康映射
|
|
||||||
|
|
||||||
- X3D 输出 `feeding` / `normal` / `scared` → 中文 **吃饵** / **正常游行** / **惊吓**
|
|
||||||
- 健康:`scared` → **不健康**,其余 → **健康**(启发式,可后续换专用模型)
|
|
||||||
|
|
||||||
## 视频 URL
|
|
||||||
|
|
||||||
FishMeasure 跑完后在输出目录查找 `*preview*.mp4`,复制到 `MEDIA_ROOT/`,文件名为 `{UTC时间戳}_{svo_stem}_left.mp4` / `_right.mp4`(每次测量不覆盖;仅一个预览文件时可能左右 URL 指向同一逻辑源经 SBS 拆分)。确保 `PUBLIC_BASE_URL` 与前端/文档中的域名端口一致。
|
|
||||||
|
|
||||||
## Weight Rule (Current)
|
## Weight Rule (Current)
|
||||||
|
|
||||||
最终体重 `pred_weight_g` 由以下规则链决定(按优先级从高到低):
|
最终体重 `pred_weight_g` 由以下规则链决定(按优先级从高到低):
|
||||||
@@ -93,7 +33,7 @@ FishMeasure 跑完后在输出目录查找 `*preview*.mp4`,复制到 `MEDIA_RO
|
|||||||
1. **440g 全池均值保护**(规则 B):若 `avg_g_filtered`(所有 candidates 均值)> `--mean-pool-fallback-max-if-over-g`(默认 440g),则 `pred_weight_g = max_predicted_weight_g_after_filter`,`pred_weight_rule = "max_after_filter_high_mean_pool_over_g"`。
|
1. **440g 全池均值保护**(规则 B):若 `avg_g_filtered`(所有 candidates 均值)> `--mean-pool-fallback-max-if-over-g`(默认 440g),则 `pred_weight_g = max_predicted_weight_g_after_filter`,`pred_weight_rule = "max_after_filter_high_mean_pool_over_g"`。
|
||||||
2. **400g mean-all fallback**(规则 A,仅 `--average-all-after-filter` 开启时):若全池 mean > `--average-all-fallback-max-if-mean-over-g`(默认 400g),则 `pred_weight_g = max_predicted_weight_g_after_filter`,`pred_weight_rule = "max_after_filter_high_mean_all"`。
|
2. **400g mean-all fallback**(规则 A,仅 `--average-all-after-filter` 开启时):若全池 mean > `--average-all-fallback-max-if-mean-over-g`(默认 400g),则 `pred_weight_g = max_predicted_weight_g_after_filter`,`pred_weight_rule = "max_after_filter_high_mean_all"`。
|
||||||
3. **`--average-all-after-filter`**(默认关):全部 candidates 均值作为最终值,`pred_weight_rule = "mean_all_filtered"`。
|
3. **`--average-all-after-filter`**(默认关):全部 candidates 均值作为最终值,`pred_weight_rule = "mean_all_filtered"`。
|
||||||
4. **Top-K 聚合**(默认路径):按 `--top-by-length`(默认开)选 top-K 帧,candidates < 5 用 max 否则用 mean,`pred_weight_rule = "top_k_aggregate"`。
|
4. **Top-K 聚合**(默认路径):按 `--top-by-length`(默认开)选 top-K 帧,对选中帧的预测重量取**算术平均**(与日志中 `top{k}_avg` 一致),`pred_weight_rule = "top_k_aggregate"`。
|
||||||
|
|
||||||
DGCNN 明细中同时输出 `mean_all_pred_g_after_filters`、`avg_topk_mean_pred_g` 等供对比参考。
|
DGCNN 明细中同时输出 `mean_all_pred_g_after_filters`、`avg_topk_mean_pred_g` 等供对比参考。
|
||||||
|
|
||||||
@@ -103,16 +43,22 @@ DGCNN 明细中同时输出 `mean_all_pred_g_after_filters`、`avg_topk_mean_pre
|
|||||||
|
|
||||||
调用 [`app/services/measure.py`](app/services/measure.py) 中的 `run_measure_batch_subprocess`,配置与 `fish_api/.env` 相同(`get_settings()`)。
|
调用 [`app/services/measure.py`](app/services/measure.py) 中的 `run_measure_batch_subprocess`,配置与 `fish_api/.env` 相同(`get_settings()`)。
|
||||||
|
|
||||||
|
**必须在 `fish_api` 下执行 `python -m app...`,或从仓库根用下面脚本**;若在仓库根直接运行 `python -m app.measure_debug_cli`,会因找不到 `app` 包报错(`ModuleNotFoundError: No module named 'app'`)。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 方式 A:仓库根(推荐)
|
||||||
|
bash scripts/measure_debug.sh --fish-id 14
|
||||||
|
|
||||||
|
# 方式 B:先进入 fish_api
|
||||||
cd fish_api
|
cd fish_api
|
||||||
uv sync
|
uv sync
|
||||||
# 默认:MEASURE_WATCH_DIR/fish{N}/ 下所有 .svo2 → 输出到 MEASURE_OUTPUT_ROOT/fish{N}(默认 fish_api/.data/measure_output/fish{N})
|
# 默认:MEASURE_WATCH_DIR/fish{N}/ 下所有 .svo2 → 输出到 MEASURE_OUTPUT_ROOT/fish{N}(默认 fish_api/.data/measure_output/fish{N})
|
||||||
uv run python -m app.measure_debug_cli --fish-id 1
|
uv run python -m app.measure_debug_cli --fish-id 1
|
||||||
|
|
||||||
# 或等价入口
|
# 或等价入口(须在 fish_api 目录)
|
||||||
uv run fish-measure-debug --fish-id 1
|
uv run fish-measure-debug --fish-id 1
|
||||||
|
|
||||||
# 指定 SVO 目录或输出目录
|
# 指定 SVO 目录或输出目录(在 fish_api 目录下)
|
||||||
uv run python -m app.measure_debug_cli --batch-folder /path/to/fish1 --fish-id 1 --output-root /path/to/out
|
uv run python -m app.measure_debug_cli --batch-folder /path/to/fish1 --fish-id 1 --output-root /path/to/out
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
FishMeasure / FishAction 子进程不连接、不依赖本库;它们只读写各自文件(如 measure_output 下
|
FishMeasure / FishAction 子进程不连接、不依赖本库;它们只读写各自文件(如 measure_output 下
|
||||||
weight_prediction.json、临时 pred.json 等),由 fish_api 在子进程结束后读文件并写入本表。
|
weight_prediction.json、临时 pred.json 等),由 fish_api 在子进程结束后读文件并写入本表。
|
||||||
预览视频在 media_root;start_fresh 会清空 measure_output、media、ingest 临时目录。
|
预览视频在 media_root;start_fresh 默认仅重置投递游标,设置 CLEAR_* 环境变量可按需清空各目录。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -718,7 +718,7 @@ def _safe_rm_tree(path: Path) -> None:
|
|||||||
def clear_runtime_compute_dirs(settings: Settings) -> None:
|
def clear_runtime_compute_dirs(settings: Settings) -> None:
|
||||||
"""清空 FishMeasure / FishAction 运行时目录、托管预览、ingest 临时文件(保留目录本身)。
|
"""清空 FishMeasure / FishAction 运行时目录、托管预览、ingest 临时文件(保留目录本身)。
|
||||||
|
|
||||||
与 remove_sqlite_database_files 一起在启动脚本中调用,使两条流水线重启后均重新计算。
|
注意:start_fresh 默认不再调用此函数。仅供手动或脚本显式调用。
|
||||||
"""
|
"""
|
||||||
for base in (
|
for base in (
|
||||||
settings.measure_output_root,
|
settings.measure_output_root,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""启动前清空状态:默认仅重置客户端游标,保留 SQLite 历史快照。
|
"""启动前清空状态:默认**仅**重置客户端投递游标,其余全部保留。
|
||||||
|
|
||||||
由 start_fresh.sh 在 uvicorn 之前调用。
|
由 start_fresh.sh 在 uvicorn 之前调用。
|
||||||
- 默认保留 SQLite 历史数据,仅重置 client_id 投递游标(fresh 语义)
|
- 默认保留 SQLite 历史数据,仅重置 client_id 投递游标(fresh 语义)
|
||||||
@@ -6,6 +6,10 @@
|
|||||||
- 默认保留 measure_output 以复用中间步骤(点云等)
|
- 默认保留 measure_output 以复用中间步骤(点云等)
|
||||||
- 设置 CLEAR_MEASURE_OUTPUT=1 清空测量输出目录
|
- 设置 CLEAR_MEASURE_OUTPUT=1 清空测量输出目录
|
||||||
- 设置 CLEAR_ACTION_OUTPUT=1 清空行为输出目录
|
- 设置 CLEAR_ACTION_OUTPUT=1 清空行为输出目录
|
||||||
|
- 默认保留 media_root(预览视频)和 stream_tmp_dir,避免视频链接 404
|
||||||
|
- 设置 CLEAR_MEDIA=1 清空媒体目录
|
||||||
|
- 设置 CLEAR_STREAM_TMP=1 清空流临时目录
|
||||||
|
- 设置 CLEAR_LEGACY_JSON=1 清除旧版 JSON 状态文件
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -65,21 +69,31 @@ def run_prestart_fresh() -> None:
|
|||||||
else:
|
else:
|
||||||
print(f"[prestart-fresh] kept action_output: {s.action_output_root}", flush=True)
|
print(f"[prestart-fresh] kept action_output: {s.action_output_root}", flush=True)
|
||||||
|
|
||||||
# 始终清空媒体根目录和流临时目录(这些是可以重新生成的)
|
# 媒体和流临时目录:默认保留(避免重置游标后视频链接 404)
|
||||||
_safe_rm_tree(s.media_root)
|
clear_media = os.environ.get("CLEAR_MEDIA", "").strip() in ("1", "true", "yes")
|
||||||
_safe_rm_tree(s.stream_tmp_dir)
|
clear_stream_tmp = os.environ.get("CLEAR_STREAM_TMP", "").strip() in ("1", "true", "yes")
|
||||||
print(
|
|
||||||
f"[prestart-fresh] cleared media and stream_tmp: "
|
if clear_media:
|
||||||
f"media={s.media_root}, stream_tmp={s.stream_tmp_dir}",
|
_safe_rm_tree(s.media_root)
|
||||||
flush=True,
|
print(f"[prestart-fresh] cleared media: {s.media_root}", flush=True)
|
||||||
)
|
else:
|
||||||
|
print(f"[prestart-fresh] kept media for reuse: {s.media_root}", flush=True)
|
||||||
|
|
||||||
|
if clear_stream_tmp:
|
||||||
|
_safe_rm_tree(s.stream_tmp_dir)
|
||||||
|
print(f"[prestart-fresh] cleared stream_tmp: {s.stream_tmp_dir}", flush=True)
|
||||||
|
else:
|
||||||
|
print(f"[prestart-fresh] kept stream_tmp for reuse: {s.stream_tmp_dir}", flush=True)
|
||||||
|
|
||||||
# 清理旧版 JSON 状态文件(数据已迁移到 SQLite)
|
# 清理旧版 JSON 状态文件(数据已迁移到 SQLite)
|
||||||
if s.measure_watch_use_state_file and s.measure_watch_dir is not None:
|
clear_legacy_json = os.environ.get("CLEAR_LEGACY_JSON", "").strip() in ("1", "true", "yes")
|
||||||
_rm_legacy_json(s.measure_watch_dir / ".fishmeasure_watch_processed.json")
|
|
||||||
|
|
||||||
if s.action_watch_use_state_file and s.action_watch_dir is not None:
|
if clear_legacy_json:
|
||||||
_rm_legacy_json(s.action_watch_dir / ".fishaction_watch_processed.json")
|
if s.measure_watch_use_state_file and s.measure_watch_dir is not None:
|
||||||
|
_rm_legacy_json(s.measure_watch_dir / ".fishmeasure_watch_processed.json")
|
||||||
|
|
||||||
|
if s.action_watch_use_state_file and s.action_watch_dir is not None:
|
||||||
|
_rm_legacy_json(s.action_watch_dir / ".fishaction_watch_processed.json")
|
||||||
|
|
||||||
print("[prestart-fresh] done.", flush=True)
|
print("[prestart-fresh] done.", flush=True)
|
||||||
|
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ async def get_real_camera(
|
|||||||
"video_left": m.video_left,
|
"video_left": m.video_left,
|
||||||
"video_right": m.video_right,
|
"video_right": m.video_right,
|
||||||
}
|
}
|
||||||
if m.calculation_log:
|
|
||||||
payload["calculation_log"] = m.calculation_log
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
|
|||||||
@@ -62,6 +62,26 @@ def _predict_weigth_from_svo2_extra_args(settings: Settings) -> List[str]:
|
|||||||
# PLY 复用控制:由 .env 中的 MEASURE_REUSE_EXISTING_CLOUDS 决定
|
# PLY 复用控制:由 .env 中的 MEASURE_REUSE_EXISTING_CLOUDS 决定
|
||||||
if not settings.measure_reuse_existing_clouds:
|
if not settings.measure_reuse_existing_clouds:
|
||||||
out.append("--no-reuse-existing-clouds")
|
out.append("--no-reuse-existing-clouds")
|
||||||
|
|
||||||
|
# 体重聚合规则
|
||||||
|
out.extend(["--weight-top-k", str(settings.measure_weight_top_k)])
|
||||||
|
if settings.measure_weight_top_by_length:
|
||||||
|
out.append("--weight-top-by-length")
|
||||||
|
else:
|
||||||
|
out.append("--no-weight-top-by-length")
|
||||||
|
out.extend(["--weight-length-switch-mm", str(settings.measure_weight_length_switch_mm)])
|
||||||
|
out.extend(["--weight-max-length-mm", str(settings.measure_weight_max_length_mm)])
|
||||||
|
out.extend(["--weight-min-length-width-ratio", str(settings.measure_weight_min_length_width_ratio)])
|
||||||
|
if settings.measure_weight_average_all_after_filter:
|
||||||
|
out.append("--weight-average-all-after-filter")
|
||||||
|
else:
|
||||||
|
out.append("--no-weight-average-all-after-filter")
|
||||||
|
out.extend(["--weight-average-all-fallback-max-if-mean-over-g", str(settings.measure_weight_avg_all_fallback_max_g)])
|
||||||
|
out.extend(["--weight-mean-pool-fallback-max-if-over-g", str(settings.measure_weight_mean_pool_fallback_max_g)])
|
||||||
|
if settings.measure_weight_remove_outliers:
|
||||||
|
out.append("--weight-remove-outliers")
|
||||||
|
out.extend(["--weight-outlier-method", settings.measure_weight_outlier_method])
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class Settings(BaseSettings):
|
|||||||
fish_measure_root: Path = fish_repo_root() / "FishMeasure"
|
fish_measure_root: Path = fish_repo_root() / "FishMeasure"
|
||||||
fish_action_root: Path = fish_repo_root() / "FishAction"
|
fish_action_root: Path = fish_repo_root() / "FishAction"
|
||||||
|
|
||||||
#: FishMeasure 推理输出(与 SQLite、媒体缓存同属 fish_api/.data,启动脚本会清空)
|
#: FishMeasure 推理输出(与 SQLite、媒体缓存同属 fish_api/.data;启动脚本默认保留,设置 CLEAR_MEASURE_OUTPUT=1 可清空)
|
||||||
measure_output_root: Path = fish_repo_root() / "fish_api" / ".data" / "measure_output"
|
measure_output_root: Path = fish_repo_root() / "fish_api" / ".data" / "measure_output"
|
||||||
#: 体重推算过程等调试文本写入目录(默认 fish_api/.data/logs/measure)。**MEASURE_DEBUG_LOG_DIR**
|
#: 体重推算过程等调试文本写入目录(默认 fish_api/.data/logs/measure)。**MEASURE_DEBUG_LOG_DIR**
|
||||||
measure_debug_log_dir: Path = Field(
|
measure_debug_log_dir: Path = Field(
|
||||||
@@ -78,7 +78,7 @@ class Settings(BaseSettings):
|
|||||||
default=True,
|
default=True,
|
||||||
validation_alias=AliasChoices("MEASURE_DEBUG_LOG_WRITE", "measure_debug_log_write"),
|
validation_alias=AliasChoices("MEASURE_DEBUG_LOG_WRITE", "measure_debug_log_write"),
|
||||||
)
|
)
|
||||||
#: FishAction 侧预留目录(与 measure 对称;当前推理多为临时文件,仍随启动清空)
|
#: FishAction 侧预留目录(与 measure 对称;启动脚本默认保留,设置 CLEAR_ACTION_OUTPUT=1 可清空)
|
||||||
action_output_root: Path = Field(default_factory=_default_action_output_root)
|
action_output_root: Path = Field(default_factory=_default_action_output_root)
|
||||||
|
|
||||||
python_fish_measure: str = ""
|
python_fish_measure: str = ""
|
||||||
@@ -160,6 +160,78 @@ class Settings(BaseSettings):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ── 体重聚合规则(传给 predict_weigth_from_svo2.py → test_dgcnn_weight_estimator.py) ──
|
||||||
|
|
||||||
|
#: DGCNN top-K 帧数,传给 ``--weight-top-k``。**MEASURE_WEIGHT_TOP_K**
|
||||||
|
measure_weight_top_k: int = Field(
|
||||||
|
default=5,
|
||||||
|
ge=1,
|
||||||
|
validation_alias=AliasChoices("MEASURE_WEIGHT_TOP_K", "measure_weight_top_k"),
|
||||||
|
)
|
||||||
|
#: 按长度选 top-K,传给 ``--weight-top-by-length``。**MEASURE_WEIGHT_TOP_BY_LENGTH**
|
||||||
|
measure_weight_top_by_length: bool = Field(
|
||||||
|
default=True,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_TOP_BY_LENGTH", "measure_weight_top_by_length"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: top-K 按长度选时,若 K 个平均长度 > 此值则切为按重量选,传给 ``--weight-length-switch-mm``。**MEASURE_WEIGHT_LENGTH_SWITCH_MM**
|
||||||
|
measure_weight_length_switch_mm: float = Field(
|
||||||
|
default=319.0,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_LENGTH_SWITCH_MM", "measure_weight_length_switch_mm"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 几何过滤:length > 此值的帧排除,传给 ``--weight-max-length-mm``(0 关闭)。**MEASURE_WEIGHT_MAX_LENGTH_MM**
|
||||||
|
measure_weight_max_length_mm: float = Field(
|
||||||
|
default=400.0,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_MAX_LENGTH_MM", "measure_weight_max_length_mm"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 几何过滤:PCA 长/宽 < 此值的帧排除,传给 ``--weight-min-length-width-ratio``(0 关闭)。**MEASURE_WEIGHT_MIN_LENGTH_WIDTH_RATIO**
|
||||||
|
measure_weight_min_length_width_ratio: float = Field(
|
||||||
|
default=1.5,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_MIN_LENGTH_WIDTH_RATIO", "measure_weight_min_length_width_ratio"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 全池均值模式,传给 ``--weight-average-all-after-filter``。**MEASURE_WEIGHT_AVERAGE_ALL_AFTER_FILTER**
|
||||||
|
measure_weight_average_all_after_filter: bool = Field(
|
||||||
|
default=False,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_AVERAGE_ALL_AFTER_FILTER", "measure_weight_average_all_after_filter"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 全池均值 > 此值时改用 max(规则 A),传给 ``--weight-average-all-fallback-max-if-mean-over-g``(0 关闭)。**MEASURE_WEIGHT_AVG_ALL_FALLBACK_MAX_G**
|
||||||
|
measure_weight_avg_all_fallback_max_g: float = Field(
|
||||||
|
default=400.0,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_AVG_ALL_FALLBACK_MAX_G", "measure_weight_avg_all_fallback_max_g"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 全池 candidates 均值 > 此值时改用 max(规则 B, 440g 保护),传给 ``--weight-mean-pool-fallback-max-if-over-g``(0 关闭)。**MEASURE_WEIGHT_MEAN_POOL_FALLBACK_MAX_G**
|
||||||
|
measure_weight_mean_pool_fallback_max_g: float = Field(
|
||||||
|
default=440.0,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_MEAN_POOL_FALLBACK_MAX_G", "measure_weight_mean_pool_fallback_max_g"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 异常值剔除开关,传给 ``--weight-remove-outliers``。**MEASURE_WEIGHT_REMOVE_OUTLIERS**
|
||||||
|
measure_weight_remove_outliers: bool = Field(
|
||||||
|
default=False,
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_REMOVE_OUTLIERS", "measure_weight_remove_outliers"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
#: 异常值剔除方法(iqr / zscore),传给 ``--weight-outlier-method``。**MEASURE_WEIGHT_OUTLIER_METHOD**
|
||||||
|
measure_weight_outlier_method: str = Field(
|
||||||
|
default="iqr",
|
||||||
|
validation_alias=AliasChoices(
|
||||||
|
"MEASURE_WEIGHT_OUTLIER_METHOD", "measure_weight_outlier_method"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
#: 非空时由 fish_api 在后台持续扫描该目录中的新 MP4 并跑 FishAction(与 ingest 共用 SQLite 最新结果)
|
#: 非空时由 fish_api 在后台持续扫描该目录中的新 MP4 并跑 FishAction(与 ingest 共用 SQLite 最新结果)
|
||||||
action_watch_dir: Optional[Path] = None
|
action_watch_dir: Optional[Path] = None
|
||||||
action_watch_poll_interval: float = Field(default=2.0, ge=0.1)
|
action_watch_poll_interval: float = Field(default=2.0, ge=0.1)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 默认重置 client_id 投递游标后启动 Fish API(uvicorn),保留 SQLite 历史快照。
|
# 默认**仅**重置 client_id 投递游标后启动 Fish API(uvicorn),其余全部保留。
|
||||||
# 默认保留 measure_output 中间步骤(点云等)以加速重新处理。
|
# SQLite 历史快照、measure_output、media(预览视频)、stream_tmp 均保留。
|
||||||
#
|
#
|
||||||
# bash fish_api/start_fresh.sh
|
# bash fish_api/start_fresh.sh
|
||||||
# PORT=8001 HOST=0.0.0.0 bash fish_api/start_fresh.sh
|
# PORT=8001 HOST=0.0.0.0 bash fish_api/start_fresh.sh
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
# CLEAR_MEASURE_OUTPUT=1 bash fish_api/start_fresh.sh
|
# CLEAR_MEASURE_OUTPUT=1 bash fish_api/start_fresh.sh
|
||||||
# CLEAR_MEASURE_OUTPUT=1 CLEAR_ACTION_OUTPUT=1 bash fish_api/start_fresh.sh
|
# CLEAR_MEASURE_OUTPUT=1 CLEAR_ACTION_OUTPUT=1 bash fish_api/start_fresh.sh
|
||||||
#
|
#
|
||||||
|
# 强制清空媒体/流临时目录:
|
||||||
|
# CLEAR_MEDIA=1 bash fish_api/start_fresh.sh
|
||||||
|
# CLEAR_STREAM_TMP=1 bash fish_api/start_fresh.sh
|
||||||
|
#
|
||||||
# 首次使用请先:cd fish_api && uv sync
|
# 首次使用请先:cd fish_api && uv sync
|
||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
11
scripts/measure_debug.sh
Executable file
11
scripts/measure_debug.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 从仓库根目录运行单条鱼测量(与 fish_api / measure-watch 同一子进程逻辑)。
|
||||||
|
# 用法:bash scripts/measure_debug.sh --fish-id 14
|
||||||
|
# 若在 fish_api 目录下,可直接:python -m app.measure_debug_cli --fish-id 14
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT/fish_api"
|
||||||
|
if command -v uv >/dev/null 2>&1; then
|
||||||
|
exec uv run python -m app.measure_debug_cli "$@"
|
||||||
|
fi
|
||||||
|
exec "${PYTHON:-python3}" -m app.measure_debug_cli "$@"
|
||||||
Reference in New Issue
Block a user