import os from dataclasses import dataclass # ========================= # 项目根目录 # ========================= # 例如: # 宿主机: # /data2/zhangw_data/Bird_detect_cascade_service # # Docker: # /Bird_detect_cascade_service # # 会自动适配,不需要写死绝对路径 # ========================= BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) MODELS_DIR = os.path.join(BASE_DIR, "models") RUNS_DIR = os.path.join(BASE_DIR, "runs") def _get_str(name: str, default: str) -> str: return os.getenv(name, default) def _get_int(name: str, default: int) -> int: value = os.getenv(name) if value is None or value == "": return default return int(value) def _get_float(name: str, default: float) -> float: value = os.getenv(name) if value is None or value == "": return default return float(value) def _get_bool(name: str, default: bool) -> bool: value = os.getenv(name) if value is None or value == "": return default return value.lower() in ["1", "true", "yes", "y"] def _get_int_list(name: str, default: str) -> list[int]: value = os.getenv(name, default) if value is None or value.strip() == "": return [] return [ int(x.strip()) for x in value.split(",") if x.strip() != "" ] @dataclass(frozen=True) class Settings: # ========================= # 模型开关 # ========================= enable_detection: bool = _get_bool( "ENABLE_DETECTION", True, ) enable_classification: bool = _get_bool( "ENABLE_CLASSIFICATION", True, ) # ========================= # YOLOv5 模型 # ========================= det_model_path: str = _get_str( "DET_MODEL_PATH", os.path.join( MODELS_DIR, "YNDC_20251010_yolov5_optimize.onnx", ), ) det_classes_path: str = _get_str( "DET_CLASSES_PATH", os.path.join( MODELS_DIR, "YNDC_20251010_yolov5_optimize.txt", ), ) # ========================= # ConvNeXt 分类模型 # ========================= cls_model_path: str = _get_str( "CLS_MODEL_PATH", os.path.join( MODELS_DIR, "JXPYH_20260204_mmcls_optimize.onnx", ), ) cls_classes_path: str = _get_str( "CLS_CLASSES_PATH", os.path.join( MODELS_DIR, "JXPYH_20260204_mmcls_optimize.txt", ), ) # ========================= # 密度估计模型 # ========================= enable_density: bool = _get_bool( "ENABLE_DENSITY", True, ) density_model_path: str = _get_str( "DENSITY_MODEL_PATH", os.path.join( MODELS_DIR, "bird_density.onnx", ), ) # 如果模型输入固定,例如 400×400 # 可以设置 DENSITY_IMG_SIZE=400 # 默认 0 表示自动读取 ONNX 输入尺寸 density_img_size: int = _get_int( "DENSITY_IMG_SIZE", 0, ) # ========================= # 输入尺寸 # ========================= det_img_size: int = _get_int( "DET_IMG_SIZE", 640, ) cls_img_size: int = _get_int( "CLS_IMG_SIZE", 224, ) # ========================= # YOLO 阈值 # ========================= det_conf_thres: float = _get_float( "DET_CONF_THRES", 0.25, ) det_iou_thres: float = _get_float( "DET_IOU_THRES", 0.45, ) # 二次过滤 YOLO 检测框,避免把明显错误的框传给 ConvNeXt det_second_conf_thres: float = _get_float( "DET_SECOND_CONF_THRES", 0.35, ) # 最小框面积占整图比例,太小的噪声框过滤 min_box_area_ratio: float = _get_float( "MIN_BOX_AREA_RATIO", 0.00002, ) # 最大框面积占整图比例,太大的误检框过滤 max_box_area_ratio: float = _get_float( "MAX_BOX_AREA_RATIO", 1.0, ) # 最大长宽比,过细过长的框过滤 max_box_aspect_ratio: float = _get_float( "MAX_BOX_ASPECT_RATIO", 6.0, ) # 可选检测区域:按检测框中心点的纵向位置过滤。 # 默认 0~1 表示整幅画面;可排除固定字幕、时间戳或岸边噪声区域。 detection_roi_y_min_ratio: float = _get_float( "DETECTION_ROI_Y_MIN_RATIO", 0.0, ) detection_roi_y_max_ratio: float = _get_float( "DETECTION_ROI_Y_MAX_RATIO", 1.0, ) # 分类置信度低于该值时,不显示物种名称 cls_show_conf_thres: float = _get_float( "CLS_SHOW_CONF_THRES", 0.50, ) # 大目标判断阈值:框面积占整图比例超过该值,认为是近景大目标 large_box_area_ratio: float = _get_float( "LARGE_BOX_AREA_RATIO", 0.002, ) # 大目标分类显示阈值,近景大目标即使分类置信度稍低,也显示物种名 large_box_cls_show_conf_thres: float = _get_float( "LARGE_BOX_CLS_SHOW_CONF_THRES", 0.25, ) # 数量达到该阈值时,认为是密集场景,不显示物种名 dense_scene_count_thres: int = _get_int( "DENSE_SCENE_COUNT_THRES", 20, ) # 大目标强制显示物种名,只要分类结果非空即可 show_large_box_name_always: bool = _get_bool( "SHOW_LARGE_BOX_NAME_ALWAYS", True, ) # 是否在密集场景隐藏所有物种名 hide_cls_name_in_dense_scene: bool = _get_bool( "HIDE_CLS_NAME_IN_DENSE_SCENE", True, ) # 所有物种是否统一使用同一种标签和检测框颜色 uniform_species_color: bool = _get_bool( "UNIFORM_SPECIES_COLOR", False, ) # 可选:整段素材已人工确认只有一个物种时,覆盖细粒度模型标签。 # 留空表示使用模型输出。 forced_species_name: str = _get_str( "FORCED_SPECIES_NAME", "", ).strip() forced_species_class_id: int = _get_int( "FORCED_SPECIES_CLASS_ID", -1, ) # 只对达到该框面积占比的目标应用人工物种覆盖。 # 可用来统一主要大型鸟类,同时保留画面中的小型伴生鸟类。 forced_species_min_box_area_ratio: float = _get_float( "FORCED_SPECIES_MIN_BOX_AREA_RATIO", 0.0, ) # 视频级人工复核映射,格式示例: # 东方白鹳=白鹤,白鹭=白鹤 species_name_remap: str = _get_str( "SPECIES_NAME_REMAP", "", ).strip() # 只有原始细粒度分类置信度达到该阈值时才应用标签映射。 # 默认 0 保持原有行为;混合物种场景可提高该值以避免低置信度误映射。 species_name_remap_min_confidence: float = _get_float( "SPECIES_NAME_REMAP_MIN_CONFIDENCE", 0.0, ) # 留空表示允许全部;非空时,只有白名单中的物种显示名称和物种色。 allowed_species_names: str = _get_str( "ALLOWED_SPECIES_NAMES", "", ).strip() # 可选双物种明暗规则。两个名称都非空时,不运行细粒度分类模型, # 而是根据检测框内暗色像素比例区分两个外观差异明显的物种。 binary_dark_species_name: str = _get_str( "BINARY_DARK_SPECIES_NAME", "", ).strip() binary_light_species_name: str = _get_str( "BINARY_LIGHT_SPECIES_NAME", "", ).strip() binary_dark_pixel_threshold: int = _get_int( "BINARY_DARK_PIXEL_THRESHOLD", 85, ) binary_dark_ratio_threshold: float = _get_float( "BINARY_DARK_RATIO_THRESHOLD", 0.12, ) # 可选:对画面中的深色目标做人工标签覆盖。 # 适合模型把黑色雁鸭类误判成白骨顶等细粒度类别的场景。 dark_override_species_name: str = _get_str( "DARK_OVERRIDE_SPECIES_NAME", "", ).strip() dark_override_species_class_id: int = _get_int( "DARK_OVERRIDE_SPECIES_CLASS_ID", -1, ) dark_override_pixel_threshold: int = _get_int( "DARK_OVERRIDE_PIXEL_THRESHOLD", 80, ) dark_override_ratio_threshold: float = _get_float( "DARK_OVERRIDE_RATIO_THRESHOLD", 0.35, ) dark_override_min_box_area_ratio: float = _get_float( "DARK_OVERRIDE_MIN_BOX_AREA_RATIO", 0.0, ) dark_override_max_box_area_ratio: float = _get_float( "DARK_OVERRIDE_MAX_BOX_AREA_RATIO", 1.0, ) dark_override_roi_y_min_ratio: float = _get_float( "DARK_OVERRIDE_ROI_Y_MIN_RATIO", 0.0, ) dark_override_roi_y_max_ratio: float = _get_float( "DARK_OVERRIDE_ROI_Y_MAX_RATIO", 1.0, ) # 可选:按画面纵向区域做人工标签覆盖。 # 用于同一视频中上/中/下区域物种分布稳定的场景。 region_override_species_name: str = _get_str( "REGION_OVERRIDE_SPECIES_NAME", "", ).strip() region_override_species_class_id: int = _get_int( "REGION_OVERRIDE_SPECIES_CLASS_ID", -1, ) region_override_source_species_names: str = _get_str( "REGION_OVERRIDE_SOURCE_SPECIES_NAMES", "", ).strip() region_override_roi_y_min_ratio: float = _get_float( "REGION_OVERRIDE_ROI_Y_MIN_RATIO", 0.0, ) region_override_roi_y_max_ratio: float = _get_float( "REGION_OVERRIDE_ROI_Y_MAX_RATIO", 1.0, ) # 可选:鹤类混淆纠正。针对白鹤/白头鹤/白枕鹤相互误判, # 根据检测框内亮白像素和暗色像素比例做保守二次校验。 enable_crane_appearance_correction: bool = _get_bool( "ENABLE_CRANE_APPEARANCE_CORRECTION", False, ) crane_white_ratio_threshold: float = _get_float( "CRANE_WHITE_RATIO_THRESHOLD", 0.30, ) crane_dark_ratio_threshold: float = _get_float( "CRANE_DARK_RATIO_THRESHOLD", 0.28, ) crane_tail_white_ratio_threshold: float = _get_float( "CRANE_TAIL_WHITE_RATIO_THRESHOLD", 0.16, ) crane_uncertain_hide_label: bool = _get_bool( "CRANE_UNCERTAIN_HIDE_LABEL", True, ) # ========================= # 裁剪外扩比例 # ========================= crop_padding_ratio: float = _get_float( "CROP_PADDING_RATIO", 0.08, ) # ========================= # 最多检测目标数 # ========================= max_detections: int = _get_int( "MAX_DETECTIONS", 50, ) # ========================= # 推理设备 # cpu / cuda # ========================= device: str = _get_str( "DEVICE", "cpu", ) # ========================= # 输出目录 # ========================= output_dir: str = _get_str( "OUTPUT_DIR", RUNS_DIR, ) image_output_subdir: str = _get_str( "IMAGE_OUTPUT_SUBDIR", "images", ) video_output_subdir: str = _get_str( "VIDEO_OUTPUT_SUBDIR", "video", ) # ========================= # 视频抽帧间隔 # ========================= video_sample_interval: int = _get_int( "VIDEO_SAMPLE_INTERVAL", 2, ) # ========================= # 是否保存画框视频 # ========================= save_annotated_video: bool = _get_bool( "SAVE_ANNOTATED_VIDEO", True, ) # ========================= # 只保留 YOLO 类别 # 默认只保留 bird # ========================= target_det_class_ids: list[int] = None # ========================= # 视频框平滑 # ========================= smooth_video_boxes: bool = _get_bool( "SMOOTH_VIDEO_BOXES", True, ) smooth_hold_frames: int = _get_int( "SMOOTH_HOLD_FRAMES", 3, ) box_smooth_alpha: float = _get_float( "BOX_SMOOTH_ALPHA", 0.25, ) def __post_init__(self): object.__setattr__( self, "target_det_class_ids", _get_int_list( "TARGET_DET_CLASS_IDS", "0", ), ) settings = Settings()