?
This commit is contained in:
@@ -383,17 +383,8 @@ def run_weight_estimation(
|
||||
print(f" WARNING: High CV detected ({cv_pct:.1f}% > {max_cv_length}%) - results may be unreliable")
|
||||
summary["cv_warning"] = True
|
||||
|
||||
# Print results
|
||||
# Print summary (skip per-file details to reduce log noise)
|
||||
if verbose:
|
||||
for it in per_file:
|
||||
ply_name = Path(it["ply"]).name
|
||||
g = float(it["predicted_weight_g"])
|
||||
length = float(it.get("length_input", float("nan")))
|
||||
is_outlier = it.get("is_outlier", False)
|
||||
outlier_marker = " [OUTLIER]" if is_outlier else ""
|
||||
length_str = f"{length:.1f}mm" if np.isfinite(length) else "nan"
|
||||
print(f" {ply_name}: len={length_str} | {g:.2f}g{outlier_marker}")
|
||||
|
||||
print(f"\n Files processed: {summary.get('num_files_predicted', len(ply_list))}")
|
||||
if summary.get('num_outliers_removed', 0) > 0:
|
||||
print(f" Outliers removed: {summary['num_outliers_removed']}")
|
||||
@@ -628,17 +619,43 @@ def build_track_weights_minute_top5(
|
||||
return tid_max, tid_len_mm, minute_avg, top5
|
||||
|
||||
|
||||
def _build_per_frame_weight_lookup(
|
||||
per_file: List[Dict[str, Any]],
|
||||
) -> Dict[Tuple[int, int], Tuple[float, float]]:
|
||||
"""Build (frame_idx, track_id) -> (weight_g, length_mm) from DGCNN per_file results.
|
||||
|
||||
frame_idx is 0-based (matches the idx used in process_single_svo2).
|
||||
"""
|
||||
lookup: Dict[Tuple[int, int], Tuple[float, float]] = {}
|
||||
for it in per_file:
|
||||
ply = str(it.get("ply", ""))
|
||||
name = Path(ply).name
|
||||
tid = _parse_tid_from_ply_name(name)
|
||||
fn = _parse_frame_num_from_ply_name(name)
|
||||
if tid is None or fn is None:
|
||||
continue
|
||||
frame_idx = fn - 1
|
||||
wg = float(it.get("predicted_weight_g", float("nan")))
|
||||
ln = float(it.get("length_input", float("nan")))
|
||||
if np.isfinite(wg):
|
||||
lookup[(frame_idx, tid)] = (wg, ln if np.isfinite(ln) else float("nan"))
|
||||
return lookup
|
||||
|
||||
|
||||
def finalize_preview_video_with_weights(
|
||||
video_buffer: List[Dict[str, Any]],
|
||||
*,
|
||||
fps_video: float,
|
||||
weights_by_track: Dict[int, float],
|
||||
lengths_by_track_mm: Dict[int, float],
|
||||
per_frame_lookup: Dict[Tuple[int, int], Tuple[float, float]],
|
||||
class_names: Dict[int, str],
|
||||
svo_name: str,
|
||||
output_images_folder: Path,
|
||||
) -> None:
|
||||
"""Redraw buffered frames with DGCNN weight/length labels; write side-by-side mp4."""
|
||||
"""Redraw buffered frames with per-frame, per-fish DGCNN weight/length labels.
|
||||
|
||||
Only (frame_idx, track_id) keys present in per_frame_lookup get numeric labels;
|
||||
missing keys show weight as ``--`` (see draw_fish_boxes_from_arrays).
|
||||
"""
|
||||
if not video_buffer:
|
||||
return
|
||||
out_frames: List[np.ndarray] = []
|
||||
@@ -655,14 +672,26 @@ def finalize_preview_video_with_weights(
|
||||
tids = np.zeros(len(boxes), dtype=np.int64)
|
||||
else:
|
||||
tids = np.asarray(_tid, dtype=np.int64)
|
||||
|
||||
frame_wdict: Dict[int, float] = {}
|
||||
frame_ldict: Dict[int, float] = {}
|
||||
for t in tids:
|
||||
t = int(t)
|
||||
key = (frame_idx, t)
|
||||
if key in per_frame_lookup:
|
||||
wg, ln = per_frame_lookup[key]
|
||||
frame_wdict[t] = wg
|
||||
if np.isfinite(ln):
|
||||
frame_ldict[t] = ln
|
||||
|
||||
left_disp = draw_fish_boxes_from_arrays(
|
||||
left_raw.copy(),
|
||||
boxes,
|
||||
cls_ids,
|
||||
tids,
|
||||
class_names,
|
||||
weights_by_track=weights_by_track,
|
||||
lengths_by_track_mm=lengths_by_track_mm,
|
||||
weights_by_track=frame_wdict,
|
||||
lengths_by_track_mm=frame_ldict,
|
||||
)
|
||||
info = f"[{frame_idx + 1}] {frame_name} | Detections: {num_dets}"
|
||||
cv2.putText(left_disp, info, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2, cv2.LINE_AA)
|
||||
@@ -678,7 +707,6 @@ def finalize_preview_video_with_weights(
|
||||
for fr in out_frames:
|
||||
vw.write(fr)
|
||||
vw.release()
|
||||
print(f"✓ Saved video (weight overlay): {video_path.name} ({len(out_frames)} frames @ {fps_video} fps)")
|
||||
|
||||
|
||||
def segment_with_sam(sam_predictor, image_bgr, boxes_xyxy, device):
|
||||
@@ -1325,9 +1353,6 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
if save_raw_pointclouds and output_raw_pc_folder:
|
||||
output_raw_pc_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"Reading from SVO2 file: {svo_path.name}")
|
||||
print(f"Output folder: {output_base.resolve()}")
|
||||
|
||||
# Initialize ZED reader
|
||||
zed_reader = ZEDReader(svo_path=str(svo_path), camera_mode=False, use_yolo_detector=False)
|
||||
if not zed_reader.open():
|
||||
@@ -1355,19 +1380,6 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
next_track_id = 0
|
||||
STATIONARY_THRESHOLD = 10
|
||||
MOVEMENT_THRESHOLD = 5.0
|
||||
|
||||
try:
|
||||
cam_cfg = zed_reader.zed.get_camera_information().camera_configuration
|
||||
_fps_raw = getattr(cam_cfg, "fps", None)
|
||||
try:
|
||||
fps_timeline = float(_fps_raw) if _fps_raw is not None else 30.0
|
||||
except (TypeError, ValueError):
|
||||
fps_timeline = 30.0
|
||||
if fps_timeline <= 0:
|
||||
fps_timeline = 30.0
|
||||
except Exception:
|
||||
fps_timeline = 30.0
|
||||
print(f" Timeline FPS (for per-minute buckets): {fps_timeline:.2f}")
|
||||
|
||||
# 只要跑过 fish 内 DGCNN,就延迟写预览,以便叠加真实 weight/length(不再依赖 --weight-overlay-video)
|
||||
defer_video = bool(do_weight_estimation and not save_images)
|
||||
@@ -1412,8 +1424,6 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
defer_left_payload: Optional[Dict[str, Any]] = None
|
||||
|
||||
frame_name = f"frame_{idx+1:06d}"
|
||||
if idx % 30 == 0:
|
||||
print(f"[{idx + 1}] {frame_name}")
|
||||
|
||||
# Run YOLO tracking (fish)
|
||||
results = yolo_model.track(img, conf=conf, imgsz=imgsz, verbose=False, persist=True)[0]
|
||||
@@ -1677,8 +1687,7 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
print(f" Point cloud {fish_idx + 1}: PREDICTED={prediction.upper()} (class_id={class_id}, confidence={confidence:.3f}) - SKIPPED (BAD quality)")
|
||||
continue
|
||||
else:
|
||||
# Point cloud is good - proceed to save
|
||||
print(f" Point cloud {fish_idx + 1}: PREDICTED={prediction.upper()} (class_id={class_id}, confidence={confidence:.3f}) - SAVING (GOOD quality, confidence >= {pointcloud_classifier_threshold:.3f})")
|
||||
pass
|
||||
else:
|
||||
# Classifier requested but not available
|
||||
print(f" Point cloud {fish_idx + 1}: WARNING - Classifier requested but not loaded, saving without classification")
|
||||
@@ -1688,7 +1697,7 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
if use_pointcloud_classifier:
|
||||
print(f" Point cloud {fish_idx + 1}: ERROR - Classifier flag was set but classifier is None. Check startup logs for loading errors.")
|
||||
else:
|
||||
print(f" Point cloud {fish_idx + 1}: Classifier not enabled")
|
||||
pass
|
||||
|
||||
# Evaluate flatness if enabled
|
||||
if use_flatness_filter:
|
||||
@@ -1704,7 +1713,7 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
print(f" Point cloud {fish_idx + 1}: Flatness score {flatness_score:.2f}% < threshold {flatness_threshold:.2f}% - SKIPPED (not flat enough)")
|
||||
continue
|
||||
else:
|
||||
print(f" Point cloud {fish_idx + 1}: Flatness score {flatness_score:.2f}% >= threshold {flatness_threshold:.2f}% - PASSED")
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f" Point cloud {fish_idx + 1}: WARNING - Flatness evaluation failed: {e}")
|
||||
# If flatness check is required and fails, skip saving
|
||||
@@ -1749,14 +1758,11 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
# Classifier not enabled, track all saved point clouds
|
||||
kept_pointclouds.append(str(ply_path))
|
||||
|
||||
if filter_pointcloud:
|
||||
print(f" Saved point cloud {fish_idx + 1}: {ply_path.name} ({original_count} -> {filtered_count} points)")
|
||||
else:
|
||||
print(f" Saved point cloud {fish_idx + 1}: {ply_path.name} ({filtered_count} points)")
|
||||
pass
|
||||
|
||||
idx += 1
|
||||
|
||||
# DGCNN on saved clouds first (needed for deferred weight overlay on video)
|
||||
# DGCNN on saved clouds first (needed for deferred weight overlay on video)
|
||||
if do_weight_estimation and kept_pointclouds:
|
||||
weight_output_dir = output_base / "weight_estimation"
|
||||
weight_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -1767,47 +1773,28 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
remove_outliers=weight_remove_outliers,
|
||||
outlier_method=weight_outlier_method,
|
||||
max_cv_length=max_cv_length,
|
||||
verbose=True,
|
||||
verbose=False,
|
||||
top_k=weight_top_k,
|
||||
top_by_length=weight_top_by_length,
|
||||
length_switch_to_weight_mm=weight_length_switch_mm,
|
||||
)
|
||||
if wres is None:
|
||||
print(f" WARNING: Weight estimation failed")
|
||||
elif do_weight_estimation and not kept_pointclouds:
|
||||
if use_pointcloud_classifier:
|
||||
print(f" Warning: Weight estimation requested but no point clouds passed PointNet++ classifier")
|
||||
else:
|
||||
print(f" Warning: Weight estimation requested but no point clouds were saved")
|
||||
|
||||
# Preview video after weights (so labels show mass in g, not depth mm)
|
||||
if not save_images:
|
||||
if defer_video and video_defer_buffer:
|
||||
wdict: Dict[int, float] = {}
|
||||
ldict: Dict[int, float] = {}
|
||||
per_frame_lookup: Dict[Tuple[int, int], Tuple[float, float]] = {}
|
||||
wjson = output_base / "weight_estimation" / "weight_estimation_results.json"
|
||||
if wjson.is_file():
|
||||
try:
|
||||
wr = json.loads(wjson.read_text(encoding="utf-8"))
|
||||
summary = wr.get("summary") or {}
|
||||
summary_wg = float(summary.get("avg_predicted_weight_g", float("nan")))
|
||||
summary_lmm = float(summary.get("avg_length_input_topk", float("nan")))
|
||||
all_tids: set = set()
|
||||
for _e in video_defer_buffer:
|
||||
_t = _e.get("track_ids")
|
||||
if _t is not None:
|
||||
all_tids.update(int(x) for x in np.asarray(_t).ravel())
|
||||
if np.isfinite(summary_wg):
|
||||
wdict = {tid: summary_wg for tid in all_tids}
|
||||
if np.isfinite(summary_lmm):
|
||||
ldict = {tid: summary_lmm for tid in all_tids}
|
||||
except Exception as e:
|
||||
print(f" WARNING: Could not parse weight results for video overlay: {e}")
|
||||
per_file_list = wr.get("per_file") or []
|
||||
per_frame_lookup = _build_per_frame_weight_lookup(per_file_list)
|
||||
except Exception:
|
||||
per_frame_lookup = {}
|
||||
finalize_preview_video_with_weights(
|
||||
video_defer_buffer,
|
||||
fps_video=10.0,
|
||||
weights_by_track=wdict,
|
||||
lengths_by_track_mm=ldict,
|
||||
per_frame_lookup=per_frame_lookup,
|
||||
class_names=class_names,
|
||||
svo_name=svo_name,
|
||||
output_images_folder=output_images_folder,
|
||||
@@ -1821,9 +1808,6 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
for frame in video_frames:
|
||||
video_writer.write(frame)
|
||||
video_writer.release()
|
||||
print(f"✓ Saved video: {video_path.name} ({len(video_frames)} frames)")
|
||||
elif save_images:
|
||||
print(f"✓ Saved {idx} frames as individual images")
|
||||
|
||||
# Save tracking stats
|
||||
if fish_tracks:
|
||||
@@ -1848,7 +1832,6 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
}
|
||||
with open(stats_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(tracking_data, f, indent=2)
|
||||
print(f"✓ Saved tracking stats: {stats_path.name}")
|
||||
|
||||
# Save list of point clouds kept for template matching
|
||||
if kept_pointclouds:
|
||||
@@ -1856,17 +1839,7 @@ def process_single_svo2(svo_path, output_base, yolo_model, sam_predictor, sam_de
|
||||
with open(pointcloud_list_path, 'w', encoding='utf-8') as f:
|
||||
for ply_path in kept_pointclouds:
|
||||
f.write(f"{ply_path}\n")
|
||||
if use_pointcloud_classifier:
|
||||
print(f"✓ Saved list of {len(kept_pointclouds)} point clouds that passed PointNet++ classifier: {pointcloud_list_path.name}")
|
||||
else:
|
||||
print(f"✓ Saved list of {len(kept_pointclouds)} point clouds for template matching: {pointcloud_list_path.name}")
|
||||
else:
|
||||
if use_pointcloud_classifier:
|
||||
print(f" Note: PointNet++ classifier was enabled but no point clouds passed the filter")
|
||||
else:
|
||||
print(f" Note: No point clouds were saved")
|
||||
|
||||
print(f"✓ Processed {idx} frames from {svo_path.name}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -74,6 +74,7 @@ def run_action_subprocess(mp4_path: Path, settings: Settings) -> str:
|
||||
cwd=str(settings.fish_action_root),
|
||||
env=os.environ.copy(),
|
||||
log_name="FishAction",
|
||||
stream_to_logger=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
err = proc.stdout or ""
|
||||
@@ -87,7 +88,7 @@ def run_action_subprocess(mp4_path: Path, settings: Settings) -> str:
|
||||
if not rows:
|
||||
raise RuntimeError("Empty prediction JSON")
|
||||
pred_en = str(rows[0].get("pred_3class", "")).strip().lower()
|
||||
logger.info(
|
||||
logger.debug(
|
||||
"[FishAction] prediction row:\n{}",
|
||||
format_json_pretty(rows[0]),
|
||||
)
|
||||
|
||||
@@ -102,6 +102,7 @@ def run_measure_subprocess(svo_path: Path, settings: Settings) -> None:
|
||||
cwd=str(settings.fish_measure_root),
|
||||
env=os.environ.copy(),
|
||||
log_name="FishMeasure",
|
||||
stream_to_logger=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
err = proc.stdout or ""
|
||||
@@ -581,28 +582,28 @@ def build_measure_snapshot(svo_path: Path, settings: Settings) -> MeasureSnapsho
|
||||
data = _load_weight_json(svo_path, settings)
|
||||
summary = data.get("dgcnn_summary") or data.get("weight_summary") or {}
|
||||
|
||||
weight_g = summary.get("avg_predicted_weight_g")
|
||||
length_mm = summary.get("avg_length_input_topk")
|
||||
if weight_g is None:
|
||||
weight_g = data.get("avg_predicted_weight_g")
|
||||
if length_mm is None:
|
||||
length_mm = summary.get("avg_length_input") or data.get("avg_length_input")
|
||||
result = _result_from_weight_prediction(data)
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
if weight_g is not None and length_mm is not None:
|
||||
try:
|
||||
w = float(weight_g)
|
||||
l = float(length_mm)
|
||||
if math.isfinite(w) and math.isfinite(l):
|
||||
result = [{"id": 1, "weight": w, "length": l}]
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if not result:
|
||||
weight_g = summary.get("avg_predicted_weight_g")
|
||||
length_mm = summary.get("avg_length_input_topk")
|
||||
if weight_g is None:
|
||||
weight_g = data.get("avg_predicted_weight_g")
|
||||
if length_mm is None:
|
||||
length_mm = summary.get("avg_length_input") or data.get("avg_length_input")
|
||||
if weight_g is not None and length_mm is not None:
|
||||
try:
|
||||
w = float(weight_g)
|
||||
l = float(length_mm)
|
||||
if math.isfinite(w) and math.isfinite(l):
|
||||
result = [{"id": 1, "weight": w, "length": l}]
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
|
||||
logger.info(
|
||||
"[FishMeasure] parsed {}\navg_weight_g(top5)={} avg_length_mm(top5)={}\nresult:\n{}\ndgcnn_summary:\n{}",
|
||||
"[FishMeasure] parsed {}\nresult ({} fish):\n{}\ndgcnn_summary:\n{}",
|
||||
svo_path.name,
|
||||
weight_g,
|
||||
length_mm,
|
||||
len(result),
|
||||
format_json_pretty(result),
|
||||
format_json_pretty(summary if summary else {}),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""子进程运行并把 stdout/stderr 流式写入 loguru,便于查看 FishMeasure / FishAction 中间输出。"""
|
||||
"""子进程运行并把 stdout/stderr 合并;可选将逐行输出写入 loguru(测量/行为推理可关闭以减少日志)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,10 +15,12 @@ def run_subprocess_with_log(
|
||||
cwd: str,
|
||||
env: Optional[Dict[str, str]] = None,
|
||||
log_name: str,
|
||||
stream_to_logger: bool = True,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
"""运行子进程,合并 stderr 到 stdout,按行输出到 loguru。
|
||||
"""运行子进程,合并 stderr 到 stdout,可选按行输出到 loguru。
|
||||
|
||||
返回 CompletedProcess,stdout 为完整输出,便于失败时拼进异常信息。
|
||||
``stream_to_logger=False`` 时不把子进程逐行写入日志(仍完整收集 stdout)。
|
||||
"""
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
@@ -33,9 +35,10 @@ def run_subprocess_with_log(
|
||||
if proc.stdout is not None:
|
||||
for line in proc.stdout:
|
||||
lines.append(line)
|
||||
s = line.rstrip()
|
||||
if s:
|
||||
logger.info("[{}] {}", log_name, s)
|
||||
if stream_to_logger:
|
||||
s = line.rstrip()
|
||||
if s:
|
||||
logger.info("[{}] {}", log_name, s)
|
||||
rc = proc.wait()
|
||||
out = "".join(lines)
|
||||
return subprocess.CompletedProcess(cmd, rc, out, "")
|
||||
|
||||
Reference in New Issue
Block a user