initial commit
This commit is contained in:
commit
0bafbee31f
54 files changed
+6317
No files matched your search
@@ -0,0 +1,233 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app import batch_runner
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class BatchRunnerCliTests(unittest.TestCase):
|
||||
def test_readme_options_are_parsed(self):
|
||||
args = batch_runner.parse_args(
|
||||
[
|
||||
"--input",
|
||||
"/tmp/input",
|
||||
"--output",
|
||||
"/tmp/output",
|
||||
"--sample-interval",
|
||||
"5",
|
||||
"--no-recursive",
|
||||
"--disable-detection",
|
||||
"--disable-classification",
|
||||
"--disable-density",
|
||||
"--det-conf-thres",
|
||||
"0.15",
|
||||
"--det-iou-thres",
|
||||
"0.40",
|
||||
"--det-second-conf-thres",
|
||||
"0.45",
|
||||
"--min-box-area-ratio",
|
||||
"0.0001",
|
||||
"--max-box-area-ratio",
|
||||
"0.10",
|
||||
"--max-box-aspect-ratio",
|
||||
"4.0",
|
||||
"--detection-roi-y-min-ratio",
|
||||
"0.08",
|
||||
"--detection-roi-y-max-ratio",
|
||||
"0.82",
|
||||
"--cls-show-conf-thres",
|
||||
"0.60",
|
||||
"--large-box-area-ratio",
|
||||
"0.003",
|
||||
"--large-box-cls-show-conf-thres",
|
||||
"0.30",
|
||||
"--show-large-box-name-always",
|
||||
"false",
|
||||
"--dense-scene-count-thres",
|
||||
"30",
|
||||
"--hide-cls-name-in-dense-scene",
|
||||
"false",
|
||||
"--crop-padding-ratio",
|
||||
"0.10",
|
||||
"--max-detections",
|
||||
"75",
|
||||
"--uniform-species-color",
|
||||
"true",
|
||||
"--species-name-remap-min-confidence",
|
||||
"0.80",
|
||||
"--binary-dark-species-name",
|
||||
"东方白鹳",
|
||||
"--binary-light-species-name",
|
||||
"白琵鹭",
|
||||
"--binary-dark-pixel-threshold",
|
||||
"85",
|
||||
"--binary-dark-ratio-threshold",
|
||||
"0.12",
|
||||
"--dark-override-species-name",
|
||||
"雁鸭类",
|
||||
"--dark-override-species-class-id",
|
||||
"60",
|
||||
"--dark-override-pixel-threshold",
|
||||
"75",
|
||||
"--dark-override-ratio-threshold",
|
||||
"0.30",
|
||||
"--dark-override-min-box-area-ratio",
|
||||
"0.00001",
|
||||
"--dark-override-max-box-area-ratio",
|
||||
"0.002",
|
||||
"--dark-override-roi-y-min-ratio",
|
||||
"0.60",
|
||||
"--dark-override-roi-y-max-ratio",
|
||||
"0.95",
|
||||
"--region-override-species-name",
|
||||
"白头鹤",
|
||||
"--region-override-species-class-id",
|
||||
"28",
|
||||
"--region-override-source-species-names",
|
||||
"白鹤,白头鹤,白枕鹤",
|
||||
"--region-override-roi-y-min-ratio",
|
||||
"0.24",
|
||||
"--region-override-roi-y-max-ratio",
|
||||
"0.45",
|
||||
"--enable-crane-appearance-correction",
|
||||
"--crane-white-ratio-threshold",
|
||||
"0.33",
|
||||
"--crane-dark-ratio-threshold",
|
||||
"0.29",
|
||||
"--crane-tail-white-ratio-threshold",
|
||||
"0.14",
|
||||
"--crane-uncertain-hide-label",
|
||||
"false",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(args.input, "/tmp/input")
|
||||
self.assertEqual(args.output, "/tmp/output")
|
||||
self.assertEqual(args.sample_interval, 5)
|
||||
self.assertFalse(args.recursive)
|
||||
self.assertFalse(args.enable_detection)
|
||||
self.assertFalse(args.enable_classification)
|
||||
self.assertFalse(args.enable_density)
|
||||
self.assertEqual(args.det_conf_thres, 0.15)
|
||||
self.assertEqual(args.det_iou_thres, 0.40)
|
||||
self.assertEqual(args.det_second_conf_thres, 0.45)
|
||||
self.assertEqual(args.min_box_area_ratio, 0.0001)
|
||||
self.assertEqual(args.max_box_area_ratio, 0.10)
|
||||
self.assertEqual(args.max_box_aspect_ratio, 4.0)
|
||||
self.assertEqual(args.detection_roi_y_min_ratio, 0.08)
|
||||
self.assertEqual(args.detection_roi_y_max_ratio, 0.82)
|
||||
self.assertEqual(args.cls_show_conf_thres, 0.60)
|
||||
self.assertEqual(args.large_box_area_ratio, 0.003)
|
||||
self.assertEqual(args.large_box_cls_show_conf_thres, 0.30)
|
||||
self.assertFalse(args.show_large_box_name_always)
|
||||
self.assertEqual(args.dense_scene_count_thres, 30)
|
||||
self.assertFalse(args.hide_cls_name_in_dense_scene)
|
||||
self.assertEqual(args.crop_padding_ratio, 0.10)
|
||||
self.assertEqual(args.max_detections, 75)
|
||||
self.assertTrue(args.uniform_species_color)
|
||||
self.assertEqual(args.species_name_remap_min_confidence, 0.80)
|
||||
self.assertEqual(args.binary_dark_species_name, "东方白鹳")
|
||||
self.assertEqual(args.binary_light_species_name, "白琵鹭")
|
||||
self.assertEqual(args.binary_dark_pixel_threshold, 85)
|
||||
self.assertEqual(args.binary_dark_ratio_threshold, 0.12)
|
||||
self.assertEqual(args.dark_override_species_name, "雁鸭类")
|
||||
self.assertEqual(args.dark_override_species_class_id, 60)
|
||||
self.assertEqual(args.dark_override_pixel_threshold, 75)
|
||||
self.assertEqual(args.dark_override_ratio_threshold, 0.30)
|
||||
self.assertEqual(args.dark_override_min_box_area_ratio, 0.00001)
|
||||
self.assertEqual(args.dark_override_max_box_area_ratio, 0.002)
|
||||
self.assertEqual(args.dark_override_roi_y_min_ratio, 0.60)
|
||||
self.assertEqual(args.dark_override_roi_y_max_ratio, 0.95)
|
||||
self.assertEqual(args.region_override_species_name, "白头鹤")
|
||||
self.assertEqual(args.region_override_species_class_id, 28)
|
||||
self.assertEqual(
|
||||
args.region_override_source_species_names,
|
||||
"白鹤,白头鹤,白枕鹤",
|
||||
)
|
||||
self.assertEqual(args.region_override_roi_y_min_ratio, 0.24)
|
||||
self.assertEqual(args.region_override_roi_y_max_ratio, 0.45)
|
||||
self.assertTrue(args.enable_crane_appearance_correction)
|
||||
self.assertEqual(args.crane_white_ratio_threshold, 0.33)
|
||||
self.assertEqual(args.crane_dark_ratio_threshold, 0.29)
|
||||
self.assertEqual(args.crane_tail_white_ratio_threshold, 0.14)
|
||||
self.assertFalse(args.crane_uncertain_hide_label)
|
||||
|
||||
def test_readme_defaults_enable_recursive_and_all_models(self):
|
||||
args = batch_runner.parse_args(["--input", "/tmp/input"])
|
||||
|
||||
self.assertTrue(args.recursive)
|
||||
self.assertEqual(args.enable_detection, settings.enable_detection)
|
||||
self.assertEqual(
|
||||
args.enable_classification,
|
||||
settings.enable_classification,
|
||||
)
|
||||
self.assertEqual(args.enable_density, settings.enable_density)
|
||||
self.assertEqual(args.sample_interval, settings.video_sample_interval)
|
||||
self.assertEqual(args.max_box_area_ratio, 1.0)
|
||||
self.assertEqual(
|
||||
args.uniform_species_color,
|
||||
settings.uniform_species_color,
|
||||
)
|
||||
|
||||
def test_main_applies_cli_values_before_running_batch(self):
|
||||
setting_names = (
|
||||
"enable_detection",
|
||||
"enable_classification",
|
||||
"enable_density",
|
||||
"det_conf_thres",
|
||||
"hide_cls_name_in_dense_scene",
|
||||
"max_detections",
|
||||
"uniform_species_color",
|
||||
)
|
||||
original_values = {
|
||||
name: getattr(settings, name)
|
||||
for name in setting_names
|
||||
}
|
||||
|
||||
try:
|
||||
with patch.object(
|
||||
batch_runner,
|
||||
"run_batch",
|
||||
return_value={"success": True},
|
||||
) as run_batch_mock:
|
||||
result = batch_runner.main(
|
||||
[
|
||||
"--input",
|
||||
"/tmp/input",
|
||||
"--output",
|
||||
"/tmp/output",
|
||||
"--disable-detection",
|
||||
"--disable-classification",
|
||||
"--disable-density",
|
||||
"--det-conf-thres",
|
||||
"0.15",
|
||||
"--hide-cls-name-in-dense-scene",
|
||||
"false",
|
||||
"--max-detections",
|
||||
"75",
|
||||
"--uniform-species-color",
|
||||
"true",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"success": True})
|
||||
self.assertFalse(settings.enable_detection)
|
||||
self.assertFalse(settings.enable_classification)
|
||||
self.assertFalse(settings.enable_density)
|
||||
self.assertEqual(settings.det_conf_thres, 0.15)
|
||||
self.assertFalse(settings.hide_cls_name_in_dense_scene)
|
||||
self.assertEqual(settings.max_detections, 75)
|
||||
self.assertTrue(settings.uniform_species_color)
|
||||
run_batch_mock.assert_called_once_with(
|
||||
input_path="/tmp/input",
|
||||
output_base="/tmp/output",
|
||||
recursive=True,
|
||||
sample_interval=2,
|
||||
)
|
||||
finally:
|
||||
for name, value in original_values.items():
|
||||
object.__setattr__(settings, name, value)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,24 @@
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.inference.convnext_onnx import softmax, to_probabilities
|
||||
|
||||
|
||||
class ClassifierProbabilityTests(unittest.TestCase):
|
||||
def test_existing_probabilities_are_not_softmaxed_again(self):
|
||||
output = np.array([0.01, 0.09, 0.90], dtype=np.float32)
|
||||
probabilities = to_probabilities(output)
|
||||
np.testing.assert_allclose(probabilities, output, atol=1e-6)
|
||||
|
||||
def test_logits_are_converted_with_softmax(self):
|
||||
logits = np.array([-1.0, 0.0, 3.0], dtype=np.float32)
|
||||
np.testing.assert_allclose(
|
||||
to_probabilities(logits),
|
||||
softmax(logits),
|
||||
atol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,114 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.config import settings
|
||||
from app.pipeline import BirdCascadePipeline
|
||||
|
||||
|
||||
class PipelineSwitchTests(unittest.TestCase):
|
||||
setting_names = (
|
||||
"enable_detection",
|
||||
"enable_classification",
|
||||
"enable_density",
|
||||
"uniform_species_color",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.original_values = {
|
||||
name: getattr(settings, name)
|
||||
for name in self.setting_names
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
for name, value in self.original_values.items():
|
||||
object.__setattr__(settings, name, value)
|
||||
|
||||
def test_all_models_can_be_disabled_without_loading_onnx(self):
|
||||
object.__setattr__(settings, "enable_detection", False)
|
||||
object.__setattr__(settings, "enable_classification", False)
|
||||
object.__setattr__(settings, "enable_density", False)
|
||||
|
||||
with (
|
||||
patch("app.pipeline.YOLOv5ONNXDetector") as detector_mock,
|
||||
patch("app.pipeline.ConvNeXtONNXClassifier") as classifier_mock,
|
||||
patch("app.pipeline.DensityEstimatorONNX") as density_mock,
|
||||
):
|
||||
pipeline = BirdCascadePipeline()
|
||||
|
||||
self.assertIsNone(pipeline.detector)
|
||||
self.assertIsNone(pipeline.classifier)
|
||||
self.assertIsNone(pipeline.density_estimator)
|
||||
detector_mock.assert_not_called()
|
||||
classifier_mock.assert_not_called()
|
||||
density_mock.assert_not_called()
|
||||
|
||||
output = pipeline.predict(
|
||||
np.zeros((32, 32, 3), dtype=np.uint8),
|
||||
)
|
||||
self.assertEqual(output["results"], [])
|
||||
self.assertIsNone(output["density"])
|
||||
|
||||
def test_detection_without_classification_keeps_detection_fields(self):
|
||||
object.__setattr__(settings, "enable_detection", True)
|
||||
object.__setattr__(settings, "enable_classification", False)
|
||||
object.__setattr__(settings, "enable_density", False)
|
||||
|
||||
fake_detector_result = [
|
||||
{
|
||||
"bbox_xyxy": [4.0, 4.0, 20.0, 20.0],
|
||||
"det_confidence": 0.9,
|
||||
"det_class_id": 0,
|
||||
"det_class_name": "bird",
|
||||
}
|
||||
]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"app.pipeline.read_classes",
|
||||
return_value=["bird"],
|
||||
),
|
||||
patch(
|
||||
"app.pipeline.os.path.exists",
|
||||
return_value=True,
|
||||
),
|
||||
patch("app.pipeline.YOLOv5ONNXDetector") as detector_class,
|
||||
patch("app.pipeline.ConvNeXtONNXClassifier") as classifier_class,
|
||||
):
|
||||
detector_class.return_value.infer.return_value = fake_detector_result
|
||||
pipeline = BirdCascadePipeline()
|
||||
output = pipeline.predict(
|
||||
np.zeros((32, 32, 3), dtype=np.uint8),
|
||||
)
|
||||
|
||||
classifier_class.assert_not_called()
|
||||
self.assertEqual(len(output["results"]), 1)
|
||||
result = output["results"][0]
|
||||
self.assertEqual(result["det_class_name"], "bird")
|
||||
self.assertEqual(result["cls_class_id"], -1)
|
||||
self.assertEqual(result["cls_class_name"], "")
|
||||
self.assertEqual(result["cls_confidence"], 0.0)
|
||||
self.assertFalse(result["show_cls_name"])
|
||||
|
||||
def test_uniform_species_color_uses_one_color_for_every_class(self):
|
||||
pipeline = object.__new__(BirdCascadePipeline)
|
||||
|
||||
object.__setattr__(settings, "uniform_species_color", False)
|
||||
first_color = pipeline._get_color_by_class_id(1)
|
||||
second_color = pipeline._get_color_by_class_id(2)
|
||||
self.assertNotEqual(first_color, second_color)
|
||||
|
||||
object.__setattr__(settings, "uniform_species_color", True)
|
||||
self.assertEqual(
|
||||
pipeline._get_color_by_class_id(1),
|
||||
pipeline._get_color_by_class_id(2),
|
||||
)
|
||||
self.assertEqual(
|
||||
pipeline._get_color_by_class_id(999),
|
||||
(0, 190, 0),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,38 @@
|
||||
import unittest
|
||||
|
||||
from app.video_pipeline import VideoResultSmoother
|
||||
|
||||
|
||||
def result(class_id, class_name, confidence, box):
|
||||
return {
|
||||
"bbox_xyxy": box,
|
||||
"cls_class_id": class_id,
|
||||
"cls_class_name": class_name,
|
||||
"cls_confidence": confidence,
|
||||
"show_cls_name": True,
|
||||
}
|
||||
|
||||
|
||||
class VideoResultSmootherTests(unittest.TestCase):
|
||||
def test_one_low_confidence_label_does_not_flip_track(self):
|
||||
smoother = VideoResultSmoother(hold_frames=4, alpha=0.5)
|
||||
smoother.update([result(1, "白鹤", 0.95, [0, 0, 20, 20])], 0)
|
||||
second = smoother.update(
|
||||
[result(2, "白枕鹤", 0.20, [1, 1, 21, 21])],
|
||||
1,
|
||||
)
|
||||
self.assertEqual(second[0]["cls_class_name"], "白鹤")
|
||||
self.assertNotIn("_smoother_class_votes", second[0])
|
||||
|
||||
def test_matching_does_not_require_same_predicted_class(self):
|
||||
smoother = VideoResultSmoother(hold_frames=4, alpha=0.5)
|
||||
smoother.update([result(1, "白鹤", 0.9, [0, 0, 20, 20])], 0)
|
||||
second = smoother.update(
|
||||
[result(2, "白枕鹤", 0.9, [2, 2, 22, 22])],
|
||||
1,
|
||||
)
|
||||
self.assertEqual(second[0]["bbox_xyxy"], [1.0, 1.0, 21.0, 21.0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user