From 6c6ace8e9d44e2ca7e8e22c65f57f530751bc6a4 Mon Sep 17 00:00:00 2001 From: Mathieu Beligon <mathieu@feedly.com> Date: Sun, 10 Jan 2021 12:57:04 +0100 Subject: [PATCH] [all] merge all projects, and explore digit recognition --- .gitignore | 1 + common/doc/change_venv.png | Bin 88731 -> 0 bytes .../digits/.changes | Bin 75097 -> 82433 bytes poetry.lock | Bin 157358 -> 146500 bytes {common => polystar_cv}/README.md | 0 .../common => polystar_cv}/__init__.py | 0 {common => polystar_cv}/config/settings.toml | 2 + .../polystar}/__init__.py | 0 .../polystar/common}/__init__.py | 0 .../common/communication}/__init__.py | 0 .../file_descriptor_target_sender.py | 0 .../communication/print_target_sender.py | 0 .../common/communication/target_sender_abc.py | 0 .../common/communication/usb_target_sender.py | 0 .../polystar/common/constants.py | 0 .../polystar/common/dependency_injection.py | 8 +- .../polystar/common/filters}/__init__.py | 0 .../polystar/common/filters/exclude_filter.py | 0 .../polystar/common/filters/filter_abc.py | 0 .../polystar/common/filters/keep_filter.py | 0 .../common/filters/pass_through_filter.py | 0 .../common/frame_generators}/__init__.py | 0 .../camera_frame_generator.py | 0 .../cv2_frame_generator_abc.py | 0 .../fps_video_frame_generator.py | 0 .../frame_generators/frames_generator_abc.py | 0 .../frame_generators/video_frame_generator.py | 0 .../polystar/common/models}/__init__.py | 0 .../polystar/common/models/box.py | 0 .../polystar/common/models/camera.py | 0 .../polystar/common/models/image.py | 9 + .../common/models/image_annotation.py | 0 .../polystar/common/models/label_map.py | 0 .../polystar/common/models/object.py | 34 +-- .../polystar/common/models/tf_model.py | 0 .../polystar/common/models/trt_model.py | 0 .../polystar/common/pipeline}/__init__.py | 0 .../pipeline/classification}/__init__.py | 0 .../classification/classification_pipeline.py | 23 +- .../pipeline/classification/classifier_abc.py | 0 .../keras_classification_pipeline.py | 179 +++++++++++++ .../pipeline/classification/random_model.py | 0 .../classification/rule_based_classifier.py | 0 .../polystar/common/pipeline/concat.py | 0 .../common/pipeline/featurizers}/__init__.py | 0 .../pipeline}/featurizers/histogram_2d.py | 13 +- .../featurizers/histogram_blocs_2d.py | 30 +++ .../common/pipeline/keras}/__init__.py | 0 .../common/pipeline/keras/classifier.py | 54 ++++ .../polystar/common/pipeline/keras/cnn.py | 27 ++ .../pipeline/keras/compilation_parameters.py | 14 + .../common/pipeline/keras/data_preparator.py | 15 ++ .../common/pipeline/keras/distillation.py | 58 ++++ .../common/pipeline/keras/model_preparator.py | 6 + .../polystar/common/pipeline/keras/trainer.py | 41 +++ .../pipeline/keras/transfer_learning.py | 24 ++ .../polystar/common/pipeline/pipe_abc.py | 0 .../polystar/common/pipeline/pipeline.py | 4 +- .../pipeline/preprocessors}/__init__.py | 0 .../pipeline}/preprocessors/normalise.py | 2 + .../common/pipeline}/preprocessors/resize.py | 2 + .../pipeline}/preprocessors/rgb_to_hsv.py | 0 .../polystar/common/settings.py | 9 +- .../common/target_pipeline}/__init__.py | 0 .../armors_descriptors}/__init__.py | 0 .../armors_color_descriptor.py | 0 .../armors_descriptor_abc.py | 0 .../common/target_pipeline/debug_pipeline.py | 0 .../detected_objects}/__init__.py | 0 .../detected_objects/detected_armor.py | 2 +- .../detected_objects/detected_object.py | 0 .../detected_objects_factory.py | 0 .../detected_objects/detected_robot.py | 0 .../object_selectors}/__init__.py | 0 .../closest_object_selector.py | 0 .../object_selectors/object_selector_abc.py | 0 .../scored_object_selector_abc.py | 0 .../objects_detectors}/__init__.py | 0 .../objects_detectors/objects_detector_abc.py | 0 .../tf_model_objects_detector.py | 0 .../trt_model_object_detector.py | 0 .../objects_linker}/__init__.py | 0 .../objects_linker/objects_linker_abs.py | 0 .../objects_linker/simple_objects_linker.py | 0 .../objects_trackers}/__init__.py | 0 .../objects_trackers/object_track.py | 0 .../objects_trackers/objects_tracker_abc.py | 0 .../objects_validators}/__init__.py | 0 .../confidence_object_validator.py | 0 .../contains_box_validator.py | 0 .../objects_validators/in_box_validator.py | 0 .../objects_validators/negation_validator.py | 0 .../objects_validator_abc.py | 0 .../robot_color_validator.py | 0 .../type_object_validator.py | 0 .../common/target_pipeline/target_abc.py | 0 .../target_factories}/__init__.py | 0 .../ratio_simple_target_factory.py | 0 .../ratio_target_factory_abc.py | 0 .../target_factories/target_factory_abc.py | 0 .../common/target_pipeline/target_pipeline.py | 0 .../tracking_target_pipeline.py | 0 .../polystar/common/utils}/__init__.py | 0 .../polystar/common/utils/dataframe.py | 0 .../polystar/common/utils/git.py | 0 .../polystar/common/utils/iterable_utils.py | 0 .../polystar/common/utils/logs.py | 0 .../polystar/common/utils/markdown.py | 6 +- .../polystar/common/utils/misc.py | 0 .../polystar/common/utils/named_mixin.py | 0 .../polystar/common/utils/no_case_enum.py | 2 +- polystar_cv/polystar/common/utils/registry.py | 18 ++ .../polystar/common/utils/serialization.py | 25 ++ .../polystar/common/utils/singleton.py | 16 ++ .../polystar/common/utils/str_utils.py | 0 .../polystar/common/utils/tensorflow.py | 0 .../polystar/common/utils/time.py | 0 .../polystar/common/utils/tqdm.py | 0 .../common/utils/working_directory.py | 0 .../polystar/common/view}/__init__.py | 0 .../common/view/cv2_results_viewer.py | 0 .../common/view/plt_results_viewer.py | 0 .../common/view/results_viewer_abc.py | 0 .../research}/__init__.py | 0 .../research/common}/__init__.py | 0 .../research/common/constants.py | 0 .../research/common/dataset}/__init__.py | 0 .../common/dataset/cleaning}/__init__.py | 0 .../dataset/cleaning/dataset_changes.py | 8 + .../dataset/cleaning/dataset_cleaner_app.py | 0 .../common/dataset/improvement}/__init__.py | 0 .../common/dataset/improvement/zoom.py | 0 .../common/dataset/perturbations}/__init__.py | 0 .../dataset/perturbations/examples/.gitignore | 0 .../dataset/perturbations/examples/test.png | Bin .../image_modifiers}/__init__.py | 0 .../image_modifiers/brightness.py | 0 .../perturbations/image_modifiers/contrast.py | 0 .../image_modifiers/gaussian_blur.py | 0 .../image_modifiers/gaussian_noise.py | 0 .../image_modifiers/horizontal_blur.py | 0 .../image_modifiers/image_modifier_abc.py | 0 .../image_modifiers/saturation.py | 0 .../dataset/perturbations/perturbator.py | 0 .../common/dataset/perturbations/utils.py | 0 .../common/dataset/tensorflow_record.py | 0 .../common/dataset/twitch}/__init__.py | 0 .../dataset/twitch/aerial_view_detector.py | 0 .../common/dataset/twitch/mask_aerial.jpg | Bin .../common/dataset/twitch/mask_detector.py | 0 .../common/dataset/twitch/mask_robot_view.jpg | Bin .../dataset/twitch/robots_views_extractor.py | 0 .../research/common/dataset/upload.py | 10 +- .../research/common/datasets}/__init__.py | 0 .../research/common/datasets/dataset.py | 0 .../common/datasets/dataset_builder.py | 0 .../common/datasets/filter_dataset.py | 0 .../research/common/datasets/image_dataset.py | 0 .../datasets/image_file_dataset_builder.py | 0 .../common/datasets/iterator_dataset.py | 0 .../research/common/datasets/lazy_dataset.py | 0 .../common/datasets/roco}/__init__.py | 0 .../common/datasets/roco/roco_annotation.py | 0 .../common/datasets/roco/roco_dataset.py | 0 .../datasets/roco/roco_dataset_builder.py | 0 .../datasets/roco/roco_dataset_descriptor.py | 0 .../common/datasets/roco/roco_datasets.py | 3 + .../common/datasets/roco/zoo}/__init__.py | 0 .../research/common/datasets/roco/zoo/dji.py | 0 .../common/datasets/roco/zoo/dji_zoomed.py | 0 .../datasets/roco/zoo/roco_dataset_zoo.py | 0 .../common/datasets/roco/zoo/twitch.py | 0 .../research/common/datasets/slice_dataset.py | 0 .../common/datasets/transform_dataset.py | 0 .../research/common/datasets/union_dataset.py | 0 .../research/common/gcloud/gcloud_storage.py | 57 ++-- .../research/common/scripts}/__init__.py | 0 ...onstruct_dataset_from_manual_annotation.py | 0 ...t_twith_datasets_from_manual_annotation.py | 0 .../common/scripts/correct_annotations.py | 0 .../scripts/create_tensorflow_records.py | 0 .../extract_robots_views_from_video.py | 0 .../common/scripts/improve_roco_by_zooming.py | 0 .../scripts/make_twitch_chunks_to_annotate.py | 0 .../common/scripts/move_aerial_views.py | 0 .../common/scripts/visualize_dataset.py | 0 .../research/common/utils/experiment_dir.py | 15 ++ .../research/robots}/__init__.py | 0 .../research/robots/armor_color}/__init__.py | 0 .../robots/armor_color/benchmarker.py | 16 ++ .../research/robots/armor_color/datasets.py | 49 ++++ .../research/robots/armor_color/pipeline.py | 11 + .../robots/armor_color/scripts}/__init__.py | 0 .../robots/armor_color/scripts/benchmark.py | 64 +++++ .../armor_color/scripts/hyper_tune_cnn.py | 35 +++ .../research/robots}/armor_digit/__init__.py | 0 .../robots/armor_digit/armor_digit_dataset.py | 62 +++++ .../robots/armor_digit/digit_benchmarker.py | 16 ++ .../robots/armor_digit/gcloud}/__init__.py | 0 .../armor_digit/gcloud/gather_performances.py | 41 +++ .../armor_digit/gcloud/hptuning_config.yaml | 33 +++ .../robots/armor_digit/gcloud/train.py | 19 ++ .../robots/armor_digit/gcloud/train_cnn.py | 29 ++ .../robots/armor_digit/gcloud/train_vgg16.py | 31 +++ .../armor_digit/gcloud/train_xception.py | 31 +++ .../robots/armor_digit/gcloud/trainer.sh | 30 +++ .../research/robots/armor_digit/pipeline.py | 13 + .../robots/armor_digit/scripts}/__init__.py | 0 .../robots/armor_digit/scripts/benchmark.py | 56 ++++ .../armor_digit/scripts}/clean_datasets.py | 19 +- .../armor_digit/scripts/hyper_tune_cnn.py | 32 +++ .../hyper_tune_distiled_vgg16_into_cnn.py | 39 +++ .../robots/armor_digit/scripts/train_vgg16.py | 37 +++ .../research/robots/dataset}/__init__.py | 0 .../robots}/dataset/armor_dataset_factory.py | 0 .../dataset/armor_value_dataset_cache.py | 23 +- .../dataset/armor_value_dataset_generator.py | 19 +- .../dataset/armor_value_target_factory.py | 0 .../research/robots/demos}/__init__.py | 0 .../research/robots}/demos/demo_infer.py | 4 +- .../research/robots}/demos/demo_pipeline.py | 11 +- .../robots}/demos/demo_pipeline_camera.py | 2 +- .../research/robots}/demos/utils.py | 0 .../research/robots/evaluation}/__init__.py | 0 .../research/robots/evaluation/benchmarker.py | 50 ++++ .../research/robots/evaluation/evaluator.py | 14 +- .../research/robots/evaluation/hyper_tuner.py | 34 +++ .../robots/evaluation/metrics}/__init__.py | 0 .../robots}/evaluation/metrics/accuracy.py | 4 +- .../research/robots}/evaluation/metrics/f1.py | 4 +- .../robots}/evaluation/metrics/metric_abc.py | 2 +- .../robots}/evaluation/performance.py | 15 +- .../research/robots/evaluation/reporter.py | 184 +++++++------ .../research/robots}/evaluation/set.py | 0 .../research/robots}/evaluation/trainer.py | 0 .../research/runes}/__init__.py | 0 .../research/runes}/constants.py | 0 .../research/runes/dataset}/__init__.py | 0 .../research/runes/dataset/blend/__init__.py | 0 .../runes}/dataset/blend/examples/.gitignore | 0 .../runes}/dataset/blend/examples/back1.jpg | Bin .../runes}/dataset/blend/examples/logo.png | Bin .../runes}/dataset/blend/examples/logo.xml | 0 .../runes}/dataset/blend/image_blender.py | 9 +- .../blend/labeled_image_modifiers/__init__.py | 0 .../labeled_image_modifier_abc.py | 2 +- .../labeled_image_rotator.py | 7 +- .../labeled_image_scaler.py | 5 +- .../runes}/dataset/dataset_generator.py | 10 +- .../research/runes}/dataset/labeled_image.py | 0 .../common/integration_tests/__init__.py | 0 .../integration_tests/datasets/__init__.py | 0 .../datasets/test_dji_dataset.py | 0 .../datasets/test_dji_zoomed_dataset.py | 0 .../datasets/test_twitch_dataset_v1.py | 0 .../tests/common/unittests/__init__.py | 0 .../roco/test_directory_dataset_zoo.py | 0 .../common/unittests/datasets/test_dataset.py | 0 .../roco/test_directory_dataset_zoo.py | 0 .../unittests/datasets_v3/test_dataset.py | 0 .../unittests/filters/test_filters_abc.py | 0 .../test_image_classifier_pipeline.py | 0 .../unittests/object_validators/__init__.py | 0 .../test_in_box_validator.py | 0 pyproject.toml | 26 +- robots-at-robots/Readme.md | 6 - robots-at-robots/config/settings.toml | 6 - .../robots_at_robots/dependency_injection.py | 16 -- .../polystar/robots_at_robots/globals.py | 5 - .../armor_color/armor_color_benchmarker.py | 24 -- .../armor_color/armor_color_dataset.py | 26 -- .../robots_at_robots/armor_color/benchmark.py | 64 ----- .../armor_digit/armor_digit_benchmarker.py | 31 --- .../armor_digit/armor_digit_dataset.py | 32 --- .../robots_at_robots/armor_digit/benchmark.py | 253 ------------------ .../research/robots_at_robots/constants.py | 1 - .../robots_at_robots/evaluation/benchmark.py | 56 ---- robots-at-runes/Readme.md | 6 - robots-at-runes/config/settings.toml | 5 - .../polystar/robots_at_runes/globals.py | 5 - 280 files changed, 1585 insertions(+), 756 deletions(-) delete mode 100644 common/doc/change_venv.png rename {common => polystar_cv}/README.md (100%) rename {common/polystar/common => polystar_cv}/__init__.py (100%) rename {common => polystar_cv}/config/settings.toml (67%) rename {common/polystar/common/communication => polystar_cv/polystar}/__init__.py (100%) rename {common/polystar/common/filters => polystar_cv/polystar/common}/__init__.py (100%) rename {common/polystar/common/frame_generators => polystar_cv/polystar/common/communication}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/communication/file_descriptor_target_sender.py (100%) rename {common => polystar_cv}/polystar/common/communication/print_target_sender.py (100%) rename {common => polystar_cv}/polystar/common/communication/target_sender_abc.py (100%) rename {common => polystar_cv}/polystar/common/communication/usb_target_sender.py (100%) rename {common => polystar_cv}/polystar/common/constants.py (100%) rename {common => polystar_cv}/polystar/common/dependency_injection.py (80%) rename {common/polystar/common/image_pipeline => polystar_cv/polystar/common/filters}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/filters/exclude_filter.py (100%) rename {common => polystar_cv}/polystar/common/filters/filter_abc.py (100%) rename {common => polystar_cv}/polystar/common/filters/keep_filter.py (100%) rename {common => polystar_cv}/polystar/common/filters/pass_through_filter.py (100%) rename {common/polystar/common/image_pipeline/featurizers => polystar_cv/polystar/common/frame_generators}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/frame_generators/camera_frame_generator.py (100%) rename {common => polystar_cv}/polystar/common/frame_generators/cv2_frame_generator_abc.py (100%) rename {common => polystar_cv}/polystar/common/frame_generators/fps_video_frame_generator.py (100%) rename {common => polystar_cv}/polystar/common/frame_generators/frames_generator_abc.py (100%) rename {common => polystar_cv}/polystar/common/frame_generators/video_frame_generator.py (100%) rename {common/polystar/common/image_pipeline/preprocessors => polystar_cv/polystar/common/models}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/models/box.py (100%) rename {common => polystar_cv}/polystar/common/models/camera.py (100%) rename {common => polystar_cv}/polystar/common/models/image.py (82%) rename {common => polystar_cv}/polystar/common/models/image_annotation.py (100%) rename {common => polystar_cv}/polystar/common/models/label_map.py (100%) rename {common => polystar_cv}/polystar/common/models/object.py (81%) rename {common => polystar_cv}/polystar/common/models/tf_model.py (100%) rename {common => polystar_cv}/polystar/common/models/trt_model.py (100%) rename {common/polystar/common/models => polystar_cv/polystar/common/pipeline}/__init__.py (100%) rename {common/polystar/common/pipeline => polystar_cv/polystar/common/pipeline/classification}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/pipeline/classification/classification_pipeline.py (81%) rename {common => polystar_cv}/polystar/common/pipeline/classification/classifier_abc.py (100%) create mode 100644 polystar_cv/polystar/common/pipeline/classification/keras_classification_pipeline.py rename {common => polystar_cv}/polystar/common/pipeline/classification/random_model.py (100%) rename {common => polystar_cv}/polystar/common/pipeline/classification/rule_based_classifier.py (100%) rename {common => polystar_cv}/polystar/common/pipeline/concat.py (100%) rename {common/polystar/common/pipeline/classification => polystar_cv/polystar/common/pipeline/featurizers}/__init__.py (100%) rename {common/polystar/common/image_pipeline => polystar_cv/polystar/common/pipeline}/featurizers/histogram_2d.py (51%) create mode 100644 polystar_cv/polystar/common/pipeline/featurizers/histogram_blocs_2d.py rename {common/polystar/common/target_pipeline => polystar_cv/polystar/common/pipeline/keras}/__init__.py (100%) create mode 100644 polystar_cv/polystar/common/pipeline/keras/classifier.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/cnn.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/compilation_parameters.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/data_preparator.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/distillation.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/model_preparator.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/trainer.py create mode 100644 polystar_cv/polystar/common/pipeline/keras/transfer_learning.py rename {common => polystar_cv}/polystar/common/pipeline/pipe_abc.py (100%) rename {common => polystar_cv}/polystar/common/pipeline/pipeline.py (81%) rename {common/polystar/common/target_pipeline/armors_descriptors => polystar_cv/polystar/common/pipeline/preprocessors}/__init__.py (100%) rename {common/polystar/common/image_pipeline => polystar_cv/polystar/common/pipeline}/preprocessors/normalise.py (74%) rename {common/polystar/common/image_pipeline => polystar_cv/polystar/common/pipeline}/preprocessors/resize.py (82%) rename {common/polystar/common/image_pipeline => polystar_cv/polystar/common/pipeline}/preprocessors/rgb_to_hsv.py (100%) rename {common => polystar_cv}/polystar/common/settings.py (69%) rename {common/polystar/common/target_pipeline/detected_objects => polystar_cv/polystar/common/target_pipeline}/__init__.py (100%) rename {common/polystar/common/target_pipeline/object_selectors => polystar_cv/polystar/common/target_pipeline/armors_descriptors}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/armors_descriptors/armors_color_descriptor.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/armors_descriptors/armors_descriptor_abc.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/debug_pipeline.py (100%) rename {common/polystar/common/target_pipeline/objects_detectors => polystar_cv/polystar/common/target_pipeline/detected_objects}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/detected_objects/detected_armor.py (97%) rename {common => polystar_cv}/polystar/common/target_pipeline/detected_objects/detected_object.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/detected_objects/detected_objects_factory.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/detected_objects/detected_robot.py (100%) rename {common/polystar/common/target_pipeline/objects_linker => polystar_cv/polystar/common/target_pipeline/object_selectors}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/object_selectors/closest_object_selector.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/object_selectors/object_selector_abc.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/object_selectors/scored_object_selector_abc.py (100%) rename {common/polystar/common/target_pipeline/objects_trackers => polystar_cv/polystar/common/target_pipeline/objects_detectors}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_detectors/objects_detector_abc.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_detectors/tf_model_objects_detector.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_detectors/trt_model_object_detector.py (100%) rename {common/polystar/common/target_pipeline/objects_validators => polystar_cv/polystar/common/target_pipeline/objects_linker}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_linker/objects_linker_abs.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_linker/simple_objects_linker.py (100%) rename {common/polystar/common/target_pipeline/target_factories => polystar_cv/polystar/common/target_pipeline/objects_trackers}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_trackers/object_track.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_trackers/objects_tracker_abc.py (100%) rename {common/polystar/common/utils => polystar_cv/polystar/common/target_pipeline/objects_validators}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/confidence_object_validator.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/contains_box_validator.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/in_box_validator.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/negation_validator.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/objects_validator_abc.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/robot_color_validator.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/objects_validators/type_object_validator.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/target_abc.py (100%) rename {common/polystar/common/view => polystar_cv/polystar/common/target_pipeline/target_factories}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/target_factories/ratio_simple_target_factory.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/target_factories/ratio_target_factory_abc.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/target_factories/target_factory_abc.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/target_pipeline.py (100%) rename {common => polystar_cv}/polystar/common/target_pipeline/tracking_target_pipeline.py (100%) rename {common/research/common => polystar_cv/polystar/common/utils}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/utils/dataframe.py (100%) rename {common => polystar_cv}/polystar/common/utils/git.py (100%) rename {common => polystar_cv}/polystar/common/utils/iterable_utils.py (100%) rename common/research/common/dataset/__init__.py => polystar_cv/polystar/common/utils/logs.py (100%) rename {common => polystar_cv}/polystar/common/utils/markdown.py (94%) rename {common => polystar_cv}/polystar/common/utils/misc.py (100%) rename {common => polystar_cv}/polystar/common/utils/named_mixin.py (100%) rename {common => polystar_cv}/polystar/common/utils/no_case_enum.py (72%) create mode 100644 polystar_cv/polystar/common/utils/registry.py create mode 100644 polystar_cv/polystar/common/utils/serialization.py create mode 100644 polystar_cv/polystar/common/utils/singleton.py rename {common => polystar_cv}/polystar/common/utils/str_utils.py (100%) rename {common => polystar_cv}/polystar/common/utils/tensorflow.py (100%) rename {common => polystar_cv}/polystar/common/utils/time.py (100%) rename {common => polystar_cv}/polystar/common/utils/tqdm.py (100%) rename {common => polystar_cv}/polystar/common/utils/working_directory.py (100%) rename {common/research/common/dataset/cleaning => polystar_cv/polystar/common/view}/__init__.py (100%) rename {common => polystar_cv}/polystar/common/view/cv2_results_viewer.py (100%) rename {common => polystar_cv}/polystar/common/view/plt_results_viewer.py (100%) rename {common => polystar_cv}/polystar/common/view/results_viewer_abc.py (100%) rename {common/research/common/dataset/improvement => polystar_cv/research}/__init__.py (100%) rename {common/research/common/dataset/perturbations => polystar_cv/research/common}/__init__.py (100%) rename {common => polystar_cv}/research/common/constants.py (100%) rename {common/research/common/dataset/perturbations/image_modifiers => polystar_cv/research/common/dataset}/__init__.py (100%) rename {common/research/common/dataset/twitch => polystar_cv/research/common/dataset/cleaning}/__init__.py (100%) rename {common => polystar_cv}/research/common/dataset/cleaning/dataset_changes.py (77%) rename {common => polystar_cv}/research/common/dataset/cleaning/dataset_cleaner_app.py (100%) rename {common/research/common/datasets => polystar_cv/research/common/dataset/improvement}/__init__.py (100%) rename {common => polystar_cv}/research/common/dataset/improvement/zoom.py (100%) rename {common/research/common/datasets/roco => polystar_cv/research/common/dataset/perturbations}/__init__.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/examples/.gitignore (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/examples/test.png (100%) rename {common/research/common/datasets/roco/zoo => polystar_cv/research/common/dataset/perturbations/image_modifiers}/__init__.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/brightness.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/contrast.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/gaussian_blur.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/gaussian_noise.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/horizontal_blur.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/image_modifier_abc.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/image_modifiers/saturation.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/perturbator.py (100%) rename {common => polystar_cv}/research/common/dataset/perturbations/utils.py (100%) rename {common => polystar_cv}/research/common/dataset/tensorflow_record.py (100%) rename {common/research/common/scripts => polystar_cv/research/common/dataset/twitch}/__init__.py (100%) rename {common => polystar_cv}/research/common/dataset/twitch/aerial_view_detector.py (100%) rename {common => polystar_cv}/research/common/dataset/twitch/mask_aerial.jpg (100%) rename {common => polystar_cv}/research/common/dataset/twitch/mask_detector.py (100%) rename {common => polystar_cv}/research/common/dataset/twitch/mask_robot_view.jpg (100%) rename {common => polystar_cv}/research/common/dataset/twitch/robots_views_extractor.py (100%) rename {common => polystar_cv}/research/common/dataset/upload.py (70%) rename {common/tests/common/integration_tests => polystar_cv/research/common/datasets}/__init__.py (100%) rename {common => polystar_cv}/research/common/datasets/dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/dataset_builder.py (100%) rename {common => polystar_cv}/research/common/datasets/filter_dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/image_dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/image_file_dataset_builder.py (100%) rename {common => polystar_cv}/research/common/datasets/iterator_dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/lazy_dataset.py (100%) rename {common/tests/common/integration_tests/datasets => polystar_cv/research/common/datasets/roco}/__init__.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/roco_annotation.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/roco_dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/roco_dataset_builder.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/roco_dataset_descriptor.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/roco_datasets.py (95%) rename {common/tests/common/unittests => polystar_cv/research/common/datasets/roco/zoo}/__init__.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/zoo/dji.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/zoo/dji_zoomed.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/zoo/roco_dataset_zoo.py (100%) rename {common => polystar_cv}/research/common/datasets/roco/zoo/twitch.py (100%) rename {common => polystar_cv}/research/common/datasets/slice_dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/transform_dataset.py (100%) rename {common => polystar_cv}/research/common/datasets/union_dataset.py (100%) rename {common => polystar_cv}/research/common/gcloud/gcloud_storage.py (58%) rename {common/tests/common/unittests/object_validators => polystar_cv/research/common/scripts}/__init__.py (100%) rename {common => polystar_cv}/research/common/scripts/construct_dataset_from_manual_annotation.py (100%) rename {common => polystar_cv}/research/common/scripts/construct_twith_datasets_from_manual_annotation.py (100%) rename {common => polystar_cv}/research/common/scripts/correct_annotations.py (100%) rename {common => polystar_cv}/research/common/scripts/create_tensorflow_records.py (100%) rename {common => polystar_cv}/research/common/scripts/extract_robots_views_from_video.py (100%) rename {common => polystar_cv}/research/common/scripts/improve_roco_by_zooming.py (100%) rename {common => polystar_cv}/research/common/scripts/make_twitch_chunks_to_annotate.py (100%) rename {common => polystar_cv}/research/common/scripts/move_aerial_views.py (100%) rename {common => polystar_cv}/research/common/scripts/visualize_dataset.py (100%) create mode 100644 polystar_cv/research/common/utils/experiment_dir.py rename {robots-at-robots/polystar/robots_at_robots => polystar_cv/research/robots}/__init__.py (100%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots/armor_color}/__init__.py (100%) create mode 100644 polystar_cv/research/robots/armor_color/benchmarker.py create mode 100644 polystar_cv/research/robots/armor_color/datasets.py create mode 100644 polystar_cv/research/robots/armor_color/pipeline.py rename {robots-at-robots/research/robots_at_robots/armor_color => polystar_cv/research/robots/armor_color/scripts}/__init__.py (100%) create mode 100644 polystar_cv/research/robots/armor_color/scripts/benchmark.py create mode 100644 polystar_cv/research/robots/armor_color/scripts/hyper_tune_cnn.py rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/armor_digit/__init__.py (100%) create mode 100644 polystar_cv/research/robots/armor_digit/armor_digit_dataset.py create mode 100644 polystar_cv/research/robots/armor_digit/digit_benchmarker.py rename {robots-at-robots/research/robots_at_robots/dataset => polystar_cv/research/robots/armor_digit/gcloud}/__init__.py (100%) create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/gather_performances.py create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/hptuning_config.yaml create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/train.py create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/train_cnn.py create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/train_vgg16.py create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/train_xception.py create mode 100644 polystar_cv/research/robots/armor_digit/gcloud/trainer.sh create mode 100644 polystar_cv/research/robots/armor_digit/pipeline.py rename {robots-at-robots/research/robots_at_robots/evaluation => polystar_cv/research/robots/armor_digit/scripts}/__init__.py (100%) create mode 100644 polystar_cv/research/robots/armor_digit/scripts/benchmark.py rename {robots-at-robots/research/robots_at_robots/armor_digit => polystar_cv/research/robots/armor_digit/scripts}/clean_datasets.py (62%) create mode 100644 polystar_cv/research/robots/armor_digit/scripts/hyper_tune_cnn.py create mode 100644 polystar_cv/research/robots/armor_digit/scripts/hyper_tune_distiled_vgg16_into_cnn.py create mode 100644 polystar_cv/research/robots/armor_digit/scripts/train_vgg16.py rename {robots-at-robots/research/robots_at_robots/evaluation/metrics => polystar_cv/research/robots/dataset}/__init__.py (100%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/dataset/armor_dataset_factory.py (100%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/dataset/armor_value_dataset_cache.py (73%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/dataset/armor_value_dataset_generator.py (70%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/dataset/armor_value_target_factory.py (100%) rename {robots-at-runes/polystar/robots_at_runes => polystar_cv/research/robots/demos}/__init__.py (100%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/demos/demo_infer.py (89%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/demos/demo_pipeline.py (90%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/demos/demo_pipeline_camera.py (97%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/demos/utils.py (100%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/robots/evaluation}/__init__.py (100%) create mode 100644 polystar_cv/research/robots/evaluation/benchmarker.py rename robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluator.py => polystar_cv/research/robots/evaluation/evaluator.py (83%) create mode 100644 polystar_cv/research/robots/evaluation/hyper_tuner.py rename {robots-at-runes/research/robots_at_runes/dataset => polystar_cv/research/robots/evaluation/metrics}/__init__.py (100%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/evaluation/metrics/accuracy.py (59%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/evaluation/metrics/f1.py (79%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/evaluation/metrics/metric_abc.py (77%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/evaluation/performance.py (85%) rename robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluation_reporter.py => polystar_cv/research/robots/evaluation/reporter.py (66%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/evaluation/set.py (100%) rename {robots-at-robots/research/robots_at_robots => polystar_cv/research/robots}/evaluation/trainer.py (100%) rename {robots-at-runes/research/robots_at_runes/dataset/blend => polystar_cv/research/runes}/__init__.py (100%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/constants.py (100%) rename {robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers => polystar_cv/research/runes/dataset}/__init__.py (100%) create mode 100644 polystar_cv/research/runes/dataset/blend/__init__.py rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/examples/.gitignore (100%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/examples/back1.jpg (100%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/examples/logo.png (100%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/examples/logo.xml (100%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/image_blender.py (88%) create mode 100644 polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/__init__.py rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py (94%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py (85%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py (80%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/dataset_generator.py (85%) rename {robots-at-runes/research/robots_at_runes => polystar_cv/research/runes}/dataset/labeled_image.py (100%) create mode 100644 polystar_cv/tests/common/integration_tests/__init__.py create mode 100644 polystar_cv/tests/common/integration_tests/datasets/__init__.py rename {common => polystar_cv}/tests/common/integration_tests/datasets/test_dji_dataset.py (100%) rename {common => polystar_cv}/tests/common/integration_tests/datasets/test_dji_zoomed_dataset.py (100%) rename {common => polystar_cv}/tests/common/integration_tests/datasets/test_twitch_dataset_v1.py (100%) create mode 100644 polystar_cv/tests/common/unittests/__init__.py rename {common => polystar_cv}/tests/common/unittests/datasets/roco/test_directory_dataset_zoo.py (100%) rename {common => polystar_cv}/tests/common/unittests/datasets/test_dataset.py (100%) rename {common => polystar_cv}/tests/common/unittests/datasets_v3/roco/test_directory_dataset_zoo.py (100%) rename {common => polystar_cv}/tests/common/unittests/datasets_v3/test_dataset.py (100%) rename {common => polystar_cv}/tests/common/unittests/filters/test_filters_abc.py (100%) rename {common => polystar_cv}/tests/common/unittests/image_pipeline/test_image_classifier_pipeline.py (100%) create mode 100644 polystar_cv/tests/common/unittests/object_validators/__init__.py rename {common => polystar_cv}/tests/common/unittests/object_validators/test_in_box_validator.py (100%) delete mode 100644 robots-at-robots/Readme.md delete mode 100644 robots-at-robots/config/settings.toml delete mode 100644 robots-at-robots/polystar/robots_at_robots/dependency_injection.py delete mode 100644 robots-at-robots/polystar/robots_at_robots/globals.py delete mode 100644 robots-at-robots/research/robots_at_robots/armor_color/armor_color_benchmarker.py delete mode 100644 robots-at-robots/research/robots_at_robots/armor_color/armor_color_dataset.py delete mode 100644 robots-at-robots/research/robots_at_robots/armor_color/benchmark.py delete mode 100644 robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_benchmarker.py delete mode 100644 robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_dataset.py delete mode 100644 robots-at-robots/research/robots_at_robots/armor_digit/benchmark.py delete mode 100644 robots-at-robots/research/robots_at_robots/constants.py delete mode 100644 robots-at-robots/research/robots_at_robots/evaluation/benchmark.py delete mode 100644 robots-at-runes/Readme.md delete mode 100644 robots-at-runes/config/settings.toml delete mode 100644 robots-at-runes/polystar/robots_at_runes/globals.py diff --git a/.gitignore b/.gitignore index 6ad15ab..436330e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # experiments /experiments /resources/models +/pipelines # TF models /models diff --git a/common/doc/change_venv.png b/common/doc/change_venv.png deleted file mode 100644 index 3e44e4a2ecb8359fc2a5d18ce2a945f2360aa71a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88731 zcmeFZby$>L`zT6DN{N(6hyv1rN;gs>poBCCNOuhlA|fRr2q>+T<d8$m&?PM$0|S!M zIYYyKc)vIB_wjw#{^y*t_ql|Dd3e@yuY0Y#V=*C5mE{QWZ{VY$p%E&`KT<_QyW)<9 zhGB<`4cx)MZ6%I|c2&_*TKcJiv^2w0CwntX8&fni`H)ym9K9GhnmV_a(M1@fQKYDA zG9<VRx8IOUN@Ovx-NL)`j_TU$P#RW^B0JJ6<zF;NWmeFd!*$G`O$<M~{w!zEn4FSr z%2cA@q;<o~x6W%Lbur~g{PrHg3+I&qp&@gpG!p$=JVnRsZ7D4>wE?Y+Hnb~P5|~7< zx+(9zOscH3Kr<~mgTkutNHqMqm0FO!TW4(8QtzeE(5~S)W<9<2EodJN%{@ednH%fX zx3`|~r9fVC<)Be{BFr~hS&d^i^l}?N-O$T~D7;&gf)SwchWEysW2V8cBJ&v~`R4j+ z2#cZWci&^A$-NbL%CJCiE8>m&Bj3{p<ZnUstUClLt96cMM2_1BW71kNVLIPFUkl`j z6yG`HXgiBL7MV{Y#u>FR_IL?nGuNtsR}Fko|A_4v`QSb?g|T$-l)@V;ui$67TGXF1 zisibm;B)0uzmQtxnhTPZv>|e=e8A1Dlh@l7{bq;ZEjw27E!H_Et3|GBtc};ok{)hb zZWRXzDE7=-Eb?v6Th*~04RlQ0iHJLcXf@N!`RQ{gKk@&Fk0#3*eWy(#pP=|XBmb?( zCIMsD!>V`!_?%2(Y`((5V~m!Mg5T0B_C^P_S+AKMgf@5=^A^9MOmEQ$oN1O<@+nHj zs1@=h3#O4#Rqkhql;`2`yhr)@?t>>W!o$qb%$7SXbl>o$XUd(uOg$WRyplLN!k#YK zGVrNgXR=tFF(LTzdT?{LucWc^ky4VoERmmH5;GkP_Yo!+?iG%-H=JZoFa;(0hX)2R z{VZi0(doZQ@{yqP7nnK7hI&52PsK~4IwpFLd-v@^;8!`BG~8uzLd%CL{85>7oT|Ow zuBb5d;5=g>K7F2Rt`G!c3TS(J`yj{-{oO-izM)ZZ*PCBMhOIsXz&L6o6^*HQK2RxK zWf7V+b7xqOlxs4-XG14Ivq$?5#qHHdCMEoX^au9m00;Bjj7O&$9J7)*bHpQNNe8B_ zm6&t8jnbQF--tdrGTUKKZ1Q|3#vy*4Qj->!7p@yCyfQT>_@ROF;mG*23D&!&R7@Xd zc9Bo?DfPj(C^Tg1@*|mtyFTYS1<BT;dQZOeNR7(**oJU6qD;(-V@5qzafh+AMji91 zp>Jq4etdqot5x$ByQU>?wmncJ+HZf>+9#JS#{ulN0Xq9iiy8QZI`@RsA-mnIgK2C= ziUMc-n2|z-jY{zK{Ezh?M6Z^Gzl>!W@C;4iyz%(KJ=utXhQUX8_3F)2Malx4di4E> zXToRwg3L{8Xv@8vWOwIfZqOEbl1$sB;Y{6Cdx2-;PuBGf(;d@~<Z5HR013f0Nw$v| zD;#fEw286;#$KS`#Uy_rsD(Kkp!9-Z6de@clZJN_ouxr#nz0hY?iJMG#KnIxjS_}_ zg8Mp(M5s~X2`PIsouiBh3E#EgH@EuaMWRDrGK>XZf5uq)K2ZKj$u;dr8`<S3`uEb` z6y`+wBPJy27$?7_e#5ygZTXed%3mlg{V89EWcv3<F1!ih6lpCMtku^Gq^$e&EpB^W zs|iL(2!CI+pvS&P`z}P=#Eyz)*0@;fMqFTfag~<f^N=B9LM?*hY_Jin7BPyBW#(#9 zAhhA(%@CF!7%;TdFz$wtS>a9D;|IQke!+V)sdz501sh_g<)%rQua)2p;F-R8+{o7` z*+{+5=Sqqo33<fa_x>m5RI9o%kt2m8o+G9s@fKEb@~Fb<?f3FocQjSeSmT;m$fG;t zSLC7cZ3>zS`WbF7X-mk*0&XfR-&SmAntQyhyv?(%v90DLE|Y4kvX$Pf`sTx@52haq zK2UU+KBmk}wUThnz<x@WBRF_vkj)Acc@;^AjE#th2#)~wV9|#3cqg+Z*9a}T&@IEV zsr&UT^mK%j6m%cMm4zjt(q1yz1GGOIb{}}&@?ous@C`kEb*dcySy(kbn|&}jTekrI z)k|Y9kWQx8ATpg_2CM~EwXZ{%*3kKc)&_`arh>&a>vYbdPB~A+FyA&<1ZV|#2QXm^ z6ZX)E+`mIDMD>;$pHPvof+~e~_2rAJy4dlzUU0m$F-<aZp(YK#8SXACC`&F24Ih8! z8BQ49M#VzYd#{jB%xa>c`6JO2y8Dq;@2Vm_Vh)Txw%qHv*H_fibmOf)Eo%5>r9x52 zw~UFGk?n*r#915z_XhL^$g`BP_PQPJQ<mbH4RrG*B=f9L+dY`@t9;>pu<6s}$0OIR z-!<E9{>h%F?j^$9$dc6PR;iY`W5<`aXiI9FPbO94+aFCU$eY-{KQ=BOU#=Xo=P;oN zifzj^PBug8B<jlPuov*_R%q|R1;+1;eHdLWuCiAwH-(#x6+&#w*UC#vXzcmng@|=Z zuTW9Aflb};Y5s{Wj$F|7AbZ+`n<;_`0^2vkXjf=A1pAU-C*dbqB^L^lC6Dz~_0S}N zE7Q0PxQyei;yVk!=2hf1y9QVv;Wpw<k@l08-?ppjn-H**u|wJs%kj#|MsiS(J=ku! z3w>sp{`{Sg@Ny|M-Yezk`5rW#P%e^2pSRj{H<gZy4kOYkawWy3*P+)Vr8|YYtulf% zd`w^gLfK>u8>uiTFi3^@z~&Kzh)FS!7`50#v5HaIRRPpBl%U~j```_csoS+1C$JOz z6V$=UkBx=YmAZkmF|D@ZFU7iTAnZ}BO-w!<7pxMD`xte^tGFdZGdM%oqWBO(3T%4< z-wwxxIR}uV!JEpUZ_-3>Ve^NMiw-Jpj6aipp8srX^wvnhXr+nn4g8IH&`8jpth-EC zr~s)#@VihwQjak62C~g%_nKMn)^2Jyp-_rgl5(--XJv|IBGslMLcSCG8nYWMqiymg z3C5Bf+#rqB6M?qM*Og6V!DKVc>MHKn#D#o8$75Ksje~EJ@$9cjUyEl#C^<b=WiDr4 zVajG1VX9NYQ_}v_&*M1QEJ((km5}*ZHE}@RvfUEif5;rDq;o%qeUiO~<7DAwTl;<H z)Kp_WCi{-Q98a|awLNnZUR7R@nfF}Vw)KI}Cs<cPmjSwF(eq6`Dh@r)Rf;RY&qGtp zuDmSh@{FU6s$qlb-n>(IeNVnJ8W|lg_+Bv439-<+VAo6gO|cywqtes+P5jQo4-1b~ zR7#WhOmv`K8qb+m?ryScseNaUjJ8)}2yG8tkh<;uqAtTl)i>Q+>$;Y@{;r+p_U_63 zt7h`%dI?Ls&RbpCEk9bsyBf{$%*uNpDK#%pO@c{+iHnTR_ZJ~OrOBE?U~AvzV_Jf* z1WuI4dNT$kC3DEe4}qMS6q!4U!gV40S+!3po)~~ubkFShIU6YzD9SledN8}KMJJ<! zK>WSXj?hR&EJZGF{zGW)26mhor>*Wnjm@mnjMLI~BKzy-t6G*tCGMt+xm$TRG)tey zgMDkvmJ@bjP1yJgG7B{d*S>}pS{ayE*d0_yuC%P!s|TrX<nVyC^>#o34oySh4Ky@- z+BUhPyFa|z_bT}1Z0zz~zZOhLO{kX5w-3)1nESmrds@+LquT;efoH;nLCqTi`ip9s z6$9RjC7xvl+EX>`J|nS{pP`8~6F2xajMj5T2~cc%rzn%wNd7&UHS5V<#FRt*sYJJA zYs0o|hNGE@eN1Iex?p+L$@fa>0?t!T9O6h3*hw(*{c?J%Ts&<$9lI!>18m=Y3U4eV zC1#98ST`Gj5Pi5ch#VkOu1F3Ma$e8gOjS;vPZIUznx1&S<GV<#|H9Cs`lnCvR`f__ zfVkI*cTqRAV5_8eeu2GKQ)j#lLJz&+f}AV=ZY{3As9)!PdqMW7J0r2eK67fhk`s2a zs*FgEue*NI@8@<Z+E}xFbhyV(|55zw$<V=wC@q3kj{dC}Z1?VuzDZ@t#htd$HhSpe z)bFY4V&$GXdtI|*@7Fii+3S=J3s$ODJ^WP0Hn8hPU=E&+(~;BSTu9~BHBVMQ!Og{J z%`7maj&7&AoV#$NA@P|1#J+RtQ!RWe<>YiV=mW9JQMTV5pZvqQ<D7Zt!9_33158Zb z+9a?T{)?i|+S+KxRkUcHY-sP;H;?ZrIQOa%e5p#zCEZuia;8)d<3}SbQllNIVnG`v zNR+-e_g2YOUF9nF*X+fo1EGwa1JX@00sM50P=9|-3bZI4wEa8uR6nQg-P^QezltJ4 zW5Iv0rx;7}V{+Qdjl9~boZg%B3<HzOr=IOQoC?S;LQS<4%#@VS*nn$XG%WNRXjgzM zbl^t}o#yXrS@gSTn19^IKtl_%M8o>?8D-%8{3{ChIfwbzJ7)A-G#ucsYrv0t8pdBw zUvW>v{OcOS4j4m|RFhUv0N&M1oJ>vaoGt8KY7SdZfg5-Z^4iX5XcSE6Kj;docQ%3f zM=aH~T(p#)2$|U1av8m}H#X&Rw{<w52TjCX2)ML0bunUax3#fz7IGJ5{No8B;QIVF zHzUIzkGNQiGHNM3WstUaGG%zk^?>UhqZmE|1A~auOEV$WN3wr{1AmD!TDZ742yt_} zxw&z<-RH7*GUw(I6cpsX$IH#j%LzQe>Fi<WV&u+g=Y0ELll(Q$BU5J+CrbwxOM5$p z^LdSo?Ok0&85z$J{r&Z?b(*?c{)5TR`Oj?u8{|G8;pXAG$Nl%*0I10MU7@Fz?xr?c zk1TBgngKY(cplulC-MitzYYC^@?W4D&ZbV%_O<||i`YL@{}cG);Qs{t1E=;sIQe*a zFQU9K@+YJS_xaW@km6r-{^Kq{Xfb>d?!TEPhX2O0`&<xiT0T-y2i^fGJO4uG2Y%iC z*ZcW33py<?(<B-ibGO1HNp*Mh)yb)_Ay>M#tzIf=DbF8~`pl#WTa4+XY!BOuoUEZ+ z9fUei>q0`P#aYDKm#-^Jg?tv!?JUlT(5ude=r_g!@B4p@S_s5%jVcv<>du$FK%2be z+OtR6EM$&w?m0Sg_d>q3kVA=V#|JLZV_@NuFi5;YL;vN1O2D7g-)-8?%;=Zfe?7>7 zj|)3xyabLx;)TQ&ECjk}QaRpb!wj2;!vBWsd{%#SauRo(U58=j%Z6XouWbB}75=#t zY;}og+V4%;iI)yzfa@?X-``Voe>VwYbK6U{7r<5Iwt?W|;prQm0KsR{^=h3myL);d zx8)+P9;O$Jo?>i-K09K#1QN^c`bSUCT-(v2)7JR$K!PdAl4h2d!A?s*2F6A|U#;~2 zBr~uH3HmE3g6>5H1~t`-WQ7Jb;|87w^C=_V#bvMRp=IG4mr?OMi+^8VA8PfpG{4Ut zrGa;GcW>|1u9v|oKtnH4Z<$Nb)NkA=vKr!CYnUW1<5u<=>q$K`+tLTufu?Hq%4mf_ zW#!f*h}{@2{pvw8GqZ7)RNvu3{UcvTv)NYhawr5bCMIzBLj^p!8zhYSQV%Z<@Y1`4 z0PIYg-%~_wM+)`B==t}C^n@K}b|B7eC_X+uNIrQ_vhYYmulEGR777iY;;G%g-xfh@ zy-{;^TH;$vAdO*9S-47liP+>9)n_Rsj;rAimA!xeehL{wNkK852d)|JNfAAQ)^4QL zeySJ#8ehu)s|^1kRxkWnP#gASx{zJdrm=x+IE{VnSo|Jc89%u@&YCfb>EES|gsaGR zvYPMVLu6iEjn_$q<KjZCm0{|U=T?XvJ+vk~a;i8#H8CZ{Zpr{g#npF)#6q0X9ARBT zjpS=ow6wO7qK6iVNlAL!Rowcu6GZf46Fo_S1NTckb`~J^Q?(FKcS6dD{qf9Pd(7IJ z8^UsDXNSLbqFtN-mMj7dx<nnm`}r5>ZrDVgA-6y)1r;doQRLLb$;b-$jAQ*&z(sWX z^rW4|50wa;^4Zh(S`oJ|SIe$Fb}ZXDW8Vawf&nY^?<#RZR(W=M1aBf19o#`LEp=WC z_p9sM89(#eYMx55AS0U<<*qwIc&(*JiY<HCX4dYvIrk!w>8A(BJ9SGZR}fRp3qhCJ z5v_>&+r;7%p!JEd`-csE>1#iU&rbWrp>7*?#HX7Z;b%9+ePJhilWz9jmMy0on}t;6 z#Ek#W6A1<xiD`WV(=qnOcG45lKXv~i!lKt0r(}ekO%(3&i!VbjF$VtR*Eao1hW#wg z2>;*am$*N2Ww<;iCkJpY+hO!2MMd8i{*)=lbyP7NzQ0e~d04fyw1iq~39H*b)!)ym zhX=BI$zH-Kc8<;F(-T|c&bV6d<<+HuwQ&Ba)lM(s@kD%B0Ep-4+P@Qr3e8_Bk*_?P z=;r>=RBM4)2XgAr2U$KcK2~a;Coc412V>)D%8>LWWZj>=LN{KTy*6HM?fAY5yf7X@ zmRgZ445=tCE;i+KTc4-`c`wXDY~ho(#}QjCse+(YEw7D}y?Eq~KGijMsDo**{J$Hc zX<`zs^5r<x^g7NsWp=I2Vy|CmWdm|#9lDnSF=>l{fY!zn?Od1oGx_e{9}qj(Ed_4u zh25v&Kh2peNA{o^`iT8J*RsSXiYc!kB4{UOFWIy_$+`0N(h0c?Xc)L}weX3H`weV( z6l+RMuOT<d|J_ueW3b=Z>}m+GMJ^ACrh<UqsQymwJ=j86Cg9OBZM}molPWR%C!(T5 zd^TEBJ9v~_-ta8LPwO7tl!^qnFh%%2(Pe0Su8%_9>=QqJw6L~=+HR?F8CLb1nVZAk z-<CVv&uKyqV{AA~4h&ps_-~>6S4g*^zW%!gW8q$ne^oE6x3~M?vOyPlVBg=1McFP} zQ4<rxCeetfDB)#;wAV=7g<WhSAOAZeU)4L{;li3+Ra7qZY(ZF92qIc|`X$_9xTgoW z#Q(FlNi|mMb0ig%GIHsHC0<wn+xaY*iF4}``;=ylaXTg^#y6nCT5}I}83T4@UQD)% z@6G$fd#d@nu~LJ{{#{WSB+~9OY*IA5yL9Z_1}as(5xr>k{$9${>m=@BFIq40o+J!W zj0~GudX03KEsNwX5X#Bio4s^Qo|MF0jmwDm5;KH_dmYdU#*dDdj%DED!dT;ra4wT4 ztWXRLd(tj}OUG0J8h=n64Y)+r&@p5H;m0*gx^#>cn6!gwRq_%cMf3mis$LiCzVfAG z*U|m?=dYnIW5z3qXA;w#uc=x7rt*agiO^T|iB5Q4m#OJ*HvVSgZ*BaojlYfRB_{W` zQT^>3e}_1~L!8S3r{9t4?@0A`!tp!dxHLEXoj3kY|9_|dzpFTx)PMYcS8;w9<9-+8 ze%DklODcYs#($T_e|I>3cQ`Jq#{O<p{Xf!gEb8~)^8?Pfot^n&<1V71NuZq{Y+Fsf zlBC~)Z0ofX8LNasNnS%AV2w{-UsTc6$tb&S>G*ZJzdsHmP>S`s8zB}e(JhOK$w^oK z#N)+@ip4eHxJ*ZBw-U<jAnKE#b5GaAMzY&p%~T?P#aF+xQ(;^j+*fG-`6bbVl>o7b zb*B0B;3<|rKbhX<6?77`|M>F5AaOq#OT)&JK>Lp`V<j{U`+RO<Y#a;*wEy@z#R87J zl}Zx+=cS?HHYcEAA+!wXt`hx6WWYL(@g$~^<^ebU%d$y=AEM*JifcuvZvHoX`((g* zx?b#u|7F<>B-Q{4Onf}<3H&#Fn=EMlBdLT||AkBv48s5kS`W8hIQ%z!(<*2fU~!tk z|H{OCfCS;l`G)ELhOa&q4a>lf?<@L$<Sq%CzZO6Onw8Fg2LFpfQ(q|g0yvVUuC87- zfE{+)K)*u_u1j{sY&^l)qo43|I2#+=qbGHr_q}8Jxm9DZ&hLyi(CX&e>x*+pxUxL` z%D?*z9=;sE<Qh6|r!KymO6~tuFa6MTSoql>DE;vBSr_i3`5Qyrr3lB9bf@rToYPOF z!5t5C4b5syPUV0Rd$;+i3=hRA?(v5-$>tQ*`8!5Q44ZFBr9^+>>)d+@X8U2+im}!u zK7mwQ;jz}wXX+SZPF;Ll<!*XsHiBj}IHx&SCG#I`^{h5No}G^n9joEJ?Bs!04kCEY zN`aA^;OU`ap;Q>(!pL*kD}|dT)`uO|=6WUm9apgsk14+WN+IQ2)1Eu)CFE8Pg|&Ko zyM|O4T*F+tMxJhdKjqFe_Q>2Qt>Xzt>h59b<?BNlcOynp!^%6cPXljF8?Tj+XYEaT zox*h=P?2kfRN>v?Z?FCmAy^ttJi>Q-W@FpGe#NLs{1+*SqCxi<NY-T^lS@C#C7aXJ z#5-=7LK;mx)WkPxIzInaG(6A>9!bbkf}L(2I@g>&nhxhRms*S>^)dC+hjNDm)~_%U z()>!cPp@F;z0RfY6Q$4g(X9OLz7z4-o(?_;0={w3d5J5c^AqX;mZv5=o{{>GJUnY= z&fzZ(c)l<D@qonrw!~|K3)4v;8KSmjg=bjJ$!4t*;6Iv9Nv;jn8otZ(fh<8>43-6C z&T<&vb%#*5qo1w|vU{(pmkxTMc1$dfmiSJL%OZPLATB9B$Kxd@tEftwI<vp-e6RE3 z8cVWC(ZfC)^oldf*h$FjDJ5Q@U!d>BJSu<LONM&iWZxrSle`STpPa+HB>M7<%PqR< z3iXQl+K%~xy!0TSEa^SIB}~>%7V4+kq)Tlo#kpn~%YfJEF~HbwL@sOt4fievD$$|P zX-r*S=lL;HIs7|_whJV-o9M=Mbg&|{cH2XtJT-Z-<mPHG&Y=pq$^$tgrkp{A4p6&d zt}Q5~)2T?Dewz4{mqlP`M;`8)jCv2t81L;_L5-#ocZqk2dl`&6OD&s5{0adwtft+H zRbRei3Y}vb;uG#Q6$GB&jXdCXeyt`NS*AZ?Nv}H{OO>KoD9Bt~C^Q*VmafJP0W5yV zr#$N@oL_ZZ{#q3Oq(VS!@yc4s{5pAyoGRZkETVYrQ=F@AbUYt`q$e!3)9xrR_WW-u z1^tuJ+Oaq$<?McNhF<m#mtpakuaRlUKu)uKhpW)WHhJP2<?}d!<j3#+|355gO6txO zr-oXT4uhq9ZZs1SV9C1f$A^s!B8Ms1e7k6uSJ$yt`CAXfrixH&p3tNf_r~$kl{MbO z-f#&j6)1vhaoFn+M;s~&6^Gg!nd=Z)_ZC)kenOca1(lnRz)hhwPdbg(#*iSvfm55a zBGjrU)TScdQZQc>fHfaaU85gP(YKFZL-BI{0WkmMsPv@i97V~bLDu{q07;g95I-$1 zP>ntW-dT3@4}kk?y(r>8aDlu&#r*-`lyx$8`UkEtJMMv!JW-YD`L}G(S3|$%sl}&t zZWprbcnaAYm)kQYlKATTP|wFRY?^rvO%q@6=S8|?-|drJ5n`5wv$v7qo{gQ{%EouN zek7AD+~Ll#(;Oej*;iQJUKE<l-goLab*l8%Tl;+09ANcj+qjm?5E3AMuCii!&RF0& z{AW#aR0>gf$0IGBk(}_Y!=@D#Ghze3kUic;X#@JM7G!m3M;P4k;PFw?_K)qY70uO< zZykOug=%$>u^wNi>m_~^?g2#9VAPiEg7ef)t8HPJPs^M)!=_dAw}1<7NL>-*7~2rR z07ZK<v_iF2uC_%g952Wnw9Ac*4y7M~9%rVgZ8>J@^#)DaHR>Gr%Plhv@%$7&@x-H7 z)}MGNw`D5c?nS*530VR)tJpy52A$c&3e{(J-StM)mYHHvnM#&Holf$?GqaD&!Of3v zj#wr}EDBMFbs8=U4mk?Rj#C)2LdTt3ENLnlg31~^EOAbb!b-YAh>ci1dM!>#Q&hQg z_VIM#z~kifnrx0BsIlBx?B1R3$OGK_%JU=!s=YZ813VD$2mQ6GnKHr?7F=9uE<Cx- z$f*-K`#y#dg@vNz>cUh#!<-nZx)ox7P25|*M0Ec67~B55SA+ML)Q#r|t6-$@{Clbt z;>n@FmSAP=eC_ZYl-qoZ;c^B?&ZvG4V6Bp?XUde0TbNB1TGk(zN6xJcl4sk_aPzS$ zvVk;neeT$27Yf*t`4uZ@s}@650edLy+G3fwAEwk19Xc6nMGwr#t_*KBv{8*yUoBKW zl1C`(JB~A-8po7m7`tVT%WZ>}LkIY_C=)+0kwe*s#G)(IG?O49b))}xEdiuAKvAIj zWQJSzoHD{n-l;kVOyWm0rm0i7S>EWF{v*smycax+MEM<tncThBs@Jb@EA&2i_N?&F zM#l@#nzT7;6NLc8;ey(hc3#UHxJO^-89xabGo1v_HF+N|>x1BC@tG>gZx8l|psSE> zt`%P<`h{7|jS@H?l*4EFL020XVOn7e(%xe~_#1dhE!#-U7x~Ae)rRdse*r6|3K8yX z<++X7Ch8BLJQn*K_;AqjW29kCryYE?WL<OnZ{YrH<-b?qS{F_H7jT@I>y5uwk$)8H z{WtJ#&i~dHxWyp#>`337<|R*fR%a6B=Xeu9eVu+)Uv><}aZmeb_MQm}i!l!0q<H=j zP=~9p+(+;re9IRE=gv<$5BJ7q+abv|v)}#L{Lh2E_;Ig!>sClmY@gqJXu-YblF0Lj zuZK<%vi6cD==ATvZ|kr(&IIB(o%)zTHv$MA5!%y8>o%l;TH9fZC4I8FA1*8bcl%-> z1|>uvNi?@D-SM6|#?~e(90?j!NYV%UG;RCZPn@jw8)((+2WI-N?T=;F6nSnDD4vXO zl`47}A!jJ#4$EiF<57*vE!-RcmR_#4z@acht>ux3RQ+*+Jj%MFK`^a2w2bmOx35u- zp|CH=k~`nvxKXtZbo~GI#46dISHLj0-DP%&<}?fz&%wo|ybwM#s9sBND!x?5tHgnr z7ZPOH#K+6HNUfoiSFk=al4E8aA`g>7wVW7!xN6*BFBei?@<KidQo%TmdMLJgq!91k zDTX-ed~^RSFT$l*lg%{f?aL}4#>r76%fM#6pi5}DtFN&hy&<F#I%@ws<*f)CguWU7 z%4b=}rbg|jfyz0OIup((c1}?Ku>-_nhrZAEspH2n&x6JV!}<889B@Qq{N^FN3M_tv zzk_d3#T+u|{;?t@c0)}2c>e34;B6k(wH%E)R!{_AxH}JtyX}L!7rn}=9oBB5Y4XTo zJ4kS@yW|5CvQ*KhR;{ksY+U7B#LOeP;h>Um>9fCc4s!OG!BBT70{rfD=l1jSyhGr~ z(+sW*ar`MBD*?G#8m_oT3;rcjNm|!`zG;=%!_uO^bE<~$XRc-7F*T95s9TsqbL7ty z#f9C#`9(H|!qMtN7H@B52|1#S6+hsgflnkGx#Vd5Y0cwL#*0r@OAM22D`r<q*0@dK zy<C9oS+$Ls*0}@MM=fI@;T|sdsVW3`43J<@l0GV|;{RTdh)zjj?EwO87lZMk4TTm8 z=3QfA#2wa`7ZYZXD+Y?jMvT5#fsN0na9cLPXTPX(oc}KPh(x>?2+t=YQ}vI(z_#M~ zRtN2xhA56k7xOv`sd5&z#8!PXS2Y)}<oIQXM$Agb_xa6a$Kz{8M~=R!NOr2RC2PIb znX_OU-#~x7CU|>CW+BQtA|x_TdFIYg?MJ^Hu-ti$*0jRCRa;+cC_m;J*|&GygHVCZ zG1|%yvQ)w@yUZl^^^Cb&$06W2NZ|eBr@Zsdd80ryDmSiDx8DCG(61!?Q7aA3^GzM~ zr&t$a|F0BSb4pcW+Qnn``Q<8uurelqqiQ*e>LZrWIemJ*j0m;5!@HoZ_v#hw2WdjP z#HYSHw?p;4@5Bmb52Oj5=O=Xsq%B-x(aRnzzylX<bR-Q}KBZkS6j`JPPlx+{<n-wB zOG#QId4oj8d*^~|zTDcBZ6j2<P$ltqCnNEHVDVw5$+DK%G7(-5kLRBQ(hI!^r=L{G z@mldp%c~E_3`2%^gf?<16AhO`_jbsk5q%z-HKWI>ny^&xsO!5P=yJ<KeD%zUcT1s( z3qpgNuWpchW04(1o4!-%S$vRL=q_$pyh4@iHWb#)?b>p55USty6P9OS<D0{d=+#^m zl3mnC8t*9TZ~4|5kNh8ms?~B}fm5_0&IBg?MmMHoT%Q}c++r-!!c7{xo$_pt``{(M zf5mI6YZoc1ANq<ytqGgUI1h$r<|o|Cw>9fM`uV70Hp5n-Y5bR}Nd3CxtEsbx3!FFD zh)#T?<_)xs>p^1IHdxu&L}J`O4U&1!`{XS?k!fjR-OwJRVRt9Qx_QxdLy6Hzb?22z zeaxy2wjnBmPBVla^K|})gfP3N-a?=4Pw|fPrQJ`)rOp<z<4TNK$Q(SjulQ}Y9q(L^ z7ODBdYmE&S|Cg9YZro7n!Z+|ZYkR~dGx^4B#m!uyi@vm@g0bFSYzuyM4G5JY@2*}% zUVNKmw?QypSy>igqbL}ozSnS&E9P?6Uom<I2pP+LRskJ>>j5F9`Vrg%-Ub*h(AYdT zpTZiIJ_Hy<xz6hYgM;KOe86B>s^RiBWnzYJu}y`~s?*bab_4jAGSj0`(_Q)}^(~4v z(b(lLjz;Ixv<jUM!j$^c_-yE*H?ISAuc?QI&CHhHZ<d^caRAOE)w(uVN!5#}xIR$? zrcwELAJR!4!76nR{w}n0jirPQQL27yvg{$ljb%W1#p4&wCA>Oe*6lzgny~J)S7RB5 zkWr})=M2gyF~yy@BETGL9s8^5BZ?{T*rxejKCxz%cqZ_Qpnb({Qy|;2N<snE6Z&Dj zorVD22F+#H+!ZEny(*3o1=V6lHeaW;4E@4c>19FgEg(4ZmZwP~4jrhSVJZG8?%I;R zNov+8m#x<pbZ^f4X%dQDJR}x07DSx0C~_oUqg)k1Y#Fd4={T-=eCPSNDZN-m%Fa*v zV^iOs9~QCcd4mM#AJ~_H_Z9NXI>o%@MHhL_?w!5-dvGIxzXS)2LXIm??Hh6{SD0L# zDAHs(`|0s>$T@I+_EQlKm_Zu<ViE}@jpCp4HrQU!MckDW)fDa_=RT2Vvt22fjM#sf zP<_~2M!0{`)jnd0S~}vk55pdSc_Rxo7axO-6|+(tI!R+^suy*OAM<b3Mo$eqCJ6b^ zRP>rZ7Vq&zF@mC_b7pNX$qUA)#ZW0Q4RfvV9}97xCOKUl8T05(JADsX1Fe?eZ;b<{ z$x}0RJpLqu!>6=+y_c&r@}_F`QNL?ayx1y?EB&lP#k%ps!KP6>*>H26D$Z#vnEOZi zr^@YB7H6^@;vJRV#Iyqj-=rC2Je$<m#K-rrRsam{C}|oIg9%jXQVc!q91w-^byupn zENa_t#(-SeT>G;om1ncMMIALM;LH?@%$a(0$p?n<>>I58DykdgnAwO_=$8-jHhpR6 zw|+^)JFhv^_{Z~)Z)1xrw=3K{0{(k8xudhQh8TC8ZZPhh%<=s-@vpXn7YmlnsS-`h zN@_T)o`oePPps*(vp7QXS#D$}s@1ATSI&GfW^1K`Sza}|*a{ex!3!>8Yl<(yEim3w zY)M@a>^-xGL&80gG;@>~!oP-NE*PU<jIs?eu3iZ2BpB2%bl#PlWcI|4*Q$aJN(%Dx z-+As$Nl!%mS}I<}1?t(8ObxFtx@X>73^V!7@}CBt5Ysln?-$j>&FiP1B>Id0f>@mz zsPt>Ry+M0%w{tPCaH}S=*ugD2b-n>JK<D9E0PTgI0)z5-DSpZ71;<5csl_UzT3Ng< zGw9k<Ju~`TM<<jV#`U7;;>68wfeyu}9{yFV3$sa(eS7pBnZjrD+V!1c8PeKt#^ow_ z+SI>dqh8dN>X#6WfrS0Mo!}~XkLsdvxQq2DYN*zDAg9o6zC6u(#3T$m5x3*w+8pno z`SV9;UNOPA;O)O9NJLrW(Gczg)aP0LA|fQ30Ge4((|cSO(LBXq>|f@*ArRcDx;TP6 z0(Tt81}yK*Uo5X9BY;R*6Vv2k+d%$S+7->5B3r$l-6+0e<1sD_Zq#)52d?9bVSH{h zK%3l*@Y;(^EMa7LosR|WP^EN5avi~I`?<1@{j{~Ik<|CJ5@au01~iP~+W=}%JQn6J zdyGR1E0GMMaA-OP^gEbj`Qq#|Qb6lTuMn#h{ep>!l9j+3E5kRzC37wf>9ck6T^TMI zwEs?`srCmeB^dlYk<0z>gJ`9gn3x_|ZmZuAcT`OiXZv$647vd6N|}N#_Iv!<Zj;>E z-DY_7vAjahf$*ufYqFi;a`9_TGATN&8JX$kS>H#%|H(QN<-%Y|&kJ;9oELhssY}g- zeR3P!M;mbZqeS;Mt977uw4#UmY8rjEv8Y=kMzgKqyNAK=HOBdcF#q7{SAfYC(N~-< zG9F5WPHN`5?1s-cfgMsm(_{-Yzi#6R<F0yRP$un>S$$9Q{VAcP<#Uw0XSo%f`MLH0 zBru2z^J-C_UhA()r`9$0+sYZp8hE3iRwCq6MYJ*u&h`^Wy?kzqb!FZ<Nz!LE!gU%V zY@m@uhKqIH4C=xuzlfg)&_Z^CGVh-yb0=>Tltc+&nVFfRUmA$nRvC)O00Z$Y&vm3g zH}$5%Ge^>)9JQe3gtK{rhMC#mim6N{#dxNZT*jD*FP|kb6D%MWAMaT@X}HkjL^cFq ze<w9N!WFBfxgRHXx*I0%2#L`PlEQ->+{hMPpRBoR2R$NhJ@A(h0`z<7$%~8PI4Xl% z<Umu#sG0h<Q}c}>ln}CWW@>6W=r)~FO+5$j(O81viPkC{%Cc6mffy`PAhRzcpybS2 zH1vFkeLJ#^WSa@?t$vVRFQ4ITm+MWaj!5O2mFCsiEB+HeW34;+md-^pcoz+w^yOMs z3m5B%v)C%`w&T!NhXjTDR#f@DlCL=axhy0yS~dgK<jLNR#fVUZQX>C=bfgG{z<0XI zYSn=(rLM<QnGgILzC18Acn|-qFKjhYNoqS*(z4H)P0p^Fbzo{WUBFy9`Ib;Ic&|kN zMe|SO3jZTJ*~qZACNJV3+JkFo>3TR`9NzjuW!8%9Y$yfDimPCv<3fkpA#oz{Oxt49 zS-dS=mY@sv!&Z`4u<@{c!5ehN?$f>3+_<kK1OW2=csG4fK}g6jxSov|_*fGUF(I%I zg2l778mwcasIg`w8FGu<m)lvM2BFqql6A-9i4WWm-ZLi(Dk653=0^+g4bcXONf4?% z2%?lA5{U3PYnlrw*%aChZu(k7%49v18(X#`YCPYeu{$~H_@J*i$Tn=DdnxI@3i3F+ z`=f$=xHmU-=ckuYGw*V%;U~je@rq#-+@JTvk3SCQYbTb$sVPn)gti?vj&~~|c9V~- zM+(z>Wy4WDnr(zf4L=6K_Nj9ya#fbS8b>nbejIuqwuSExehQ5ySq~M;Ooj&=7(?=- z*6zF0B(bVx51fe|-pK^pw0`1L0X{x+8oCg$R?!f=<wZpINa+T@a)jv5@v$x66M2)d zB$b<0?FS4%;q)#CK(E00u;GiAZCn183@$159qz_QQQz6cJnwQP7PjFVC}Db#1XKDm z7(I*J9i^vL5{0_$JV}@JlQHx~a=LDe2S$jaf>Lb9w#f%`O$lhFAoX4gmMn?n2?c82 zF8)`B+`<_|OonZ2vxW<F`n9vqMoX3WF*Xv-=wJA}DTmq|IC(CQQM$@MK7FUA@&#f$ zo-yj;--bMhH|!gAB#T~152CZgLLk~plBw;n5PRZJ$aQV$^@*y95_XQS5QIx0B0{_A z+6ztkKjiH8c+ua<E1@=rh#&<576MR-54|D|zNEznrq0ZLbbU*|ythigZLNk_Oa?yr z?7c2N)^)(l;>!Zowc-00ql-So`x=R4Jp3w|dFIqzUr6L`Ku|uOOa=pqIEG~77rM2j z>?HZMt`1#a{kqL<>t?SltIrWW2jxAedG_ZYaoFQX3C!=CcW)&KF75wFiE6uIP+V1y z8@t-6D_CX?A3m}a8?l|JY{#wKaA&u#>XM79_j*nG5$d|uXjf*oreoX?CDQ<rm0!>( zgjL5x3OX<Lv4bNww-4!Q>nsP%N;8`=bS$2qKx`0QWInoO7VA}TUA4`R2{fs3S-#(< zMMYB2P2tVbl24K3?9UT+41dh0ACtMz+VsG?D6sFF^=$8NQ96lMyFFw#6?Qiq-$G&c z*6lwo*?7{H<MFa%fG&H=yXfR&?=|Occd`7bBLDHk*wN&!#}2;V=;^zV1#XDF?c+$= zjS=D_GcPg6xz@!1Oe~4CO(5E!Na69kXjQ&Ny$a6vHDc_*E*PKJnv7^3wH*hs?gZJd zHdqh*OBdy}K9FuBvcyF^?+!hH(-#RGZKj_gbIZQBvLovb7lcbo_Gs`FvnHXRVr!eo z*nK=6uHu%qVAyY^5!Zx*va}nH0PoUnCGM}e*Q?74<D0Ry&%?o+tDFs@mj+xPLUc96 zJVhmEWQ#*S(tG11gMrgF$)`1^J+>VX;$)ET*lHgTm;?It(~)<vw>yq>%2)h((7=+o zSUc`~h{m^6Z<LIV&j-h4U#EF$0s89ag~tmqT15`;_cHy(K8s?bvYUtmgRKU?{_t_! z{OOnacs)D6jk-M5rYES4jKwa2_&hWU1e|__gH7KrmGZY(Wej=JGA`tj@oIvrLdA+~ zNT6<LI>Ohpa~kFTErA5cDYkBr+~MG}?Ei?3T2VYXLMp81<!LrreGb>mFL|{rL?pZ~ zaihfCICh19%10&D_tZ3jX3rJ1l&xx8T9-c=ms;=j)zS?d3KBn>Q9K-9#k8RhqW2_O zt7>EDJziQ&|CzYadPje^@-P4NGWn&D+iKKJ;f+P%!xj6?4#jlbSFGq_foUsgf?%EU z1UkRvv9jkVxH-3w!<mohQRNod0BR<jw@1%o*>Qa2DY4d(Eg$ii7ogM8GX<yt_-pU| zt?UO`#$i`!=?7H`7n@8%$oRXlfvnNLkrep%Cjp2B^@S*tK^lW2H`G0E2XtSx&2sd| z;!24%&GXH-?`1@BS4A)wIkv6i>Vn?~Y|tOq^HfC^C0FRVBf~3Tw%05310j|J=I-py zV|MhOUT-N4J-TfIioSuiI>ZGRuUZY4u>$@m_Vi#gU4b5umWs~Ct%$Xyy<#9v>U(wS zQv=C*k}PXqcU1QhDs(!pIh9tmLwRTAi`f_4SI@?ySBmUTJP-FwRI=V!^uVsPSlibo ztM&$p)p&l?7?$$@dC?p2$x>%8yeCu<?MXXGJBj1cl~obni{`gGO7+<rk0BCr@aq*Z zntAED5fBN3MG!BujO_gAnQ878_Bh7GV0h{PL}UaJRJ0d+uRw?&Tr|PFIR8RYF1}^G z#4#D-jED%TXUBT~TH+WtxQOZjFJi}asbqe)Rrj{#&H|@`CTCgIBN@l*KjJDRE6+a5 zH7CWTIL$Z?w&#DGmnLZN#Q>Kj4o5}$Yfg)qqq1XXvlo+=*3fVAac#L}KEsmGOtOtx zAz|J;bX*@UFCDW-$r+S#rQV(GiZveuc9oS_^JdC>g_b5rbAgk^eFaa<y?24qz+<v= z#eJm8Q+Xj3Xfnb977KEp*T@5z0J$&n?i0F@%}Au+1~ag;@92HWUw^zj#$2q*?aBq+ zmzXBMCNIh0i}grrZV0*)9-Ku?0XpzERqK*2EGuFc`-E?6B$Z3yZpdqlfQ4?o!Gn39 z{Rk=zq&?o`H}L0ZQ|aMjf1CIaiNdYhko5bZeXMVJ9sN7GIebp2S#r8N?vLfa#zH`B zbq1)jQ&vCsAMGyh-^H+pEH=@~Rn;=yF^8)R?i4RxJx@sjZvhLxr`N}JapCBAn>ec4 zj_f(>$|I$d^GzVsKoFNRo!6$7(6;~yZ(J<AFr)N$Y)_U_I{?3sxA0jfELkb+X)Cd@ z*R<ge-3LiP>@--VCbvUUNQ)3FWPe`h0$DK^*{Y;}Ho}!$v#dEqC0V-Iwd0H|mm@{< z3DA)F?i$=ENYC!>JWp+!<lQmOJ;Hed*aJZ0wj1+@3*(Usq%LA2bmt|lK;Upz#pb&{ z%!?}qNbSJhZ7KQO2DH2#V%&spka(wqfrK7#W5%O*)A7{~jm)M#;(Ye8q6;>o9}{`G zwkh3(If%_dmxcmV;QQA;LwGlK9I@z6v~lZiFM7@MJzkCAEJzxl8uH|W)OU+tF7Y0s zj*3~ibkJ_zw-fQ%-!f-KJ|mDExrM*@1k0cf2;lB_2z&}c5qN(aCk}enOE)R{#+O)| z;MyZ+QTla(4rF&ZefRC3-XbLXIDiwGK6fHFd4zs(A|GBUev8$pjDDO5(q2ufexnz! ze@N!1d~jA$!n@3G5o~wu+?-I6;9jF>dP}MCaT!rSKr4P6WZ(=Fa%+ZfWQl%%x2H2N zs(J8H(3-DC%s3-{(eD^y@pGtDm{8p8<6?#5tSt9i87-fiu5&+{Pj+~W`mY~83F*`q za?6N`5FgH)^%@l3@R5L-z7p#ZS-WmEP#tT2c$m2Hv-DZI>qElTsmQl<9v{sI;J6tA zGIz1`gYG6ZVIls|nh%b=_-k#ZlogVghkZYS=$<^Z8ob9|Rxyx%<`X&$o~mDG-I9E5 z0a!+#mJpgq9N=W5#19}q=M*v7y?D?t3KN~wunI3Z-s$PUYHyGtR-8~sMW<E+`^%_v zQ}BoE4N2)oeQ#a+);lU#XVfHHTDOj&Q)Ut3@lD&rY6`+x&ckkcFj{6A<M9zL+)Vr^ zrU^J8aj2#&GA#13AbqjgZqhU1p%+niVqHC4H~xwV;<f*Vt}evpb9Sm%$Z(-9OSk9J zR#PMohJz+|o#l>Mg1zw~x7M7fcY!{9co3o_<+8BvCw`hSW|zgmRIAZR;ew*h_T-}o zG4Dy3T{G>Ha%R?*{fjtXXS)K;0pi?UW4TAktvzEqS!~pVZ8cR`;HCuRva=d0pM;VT zzI#57N+HCFZdFOdZd5FG3TWQ|`*{5Wkc7XUk(mYL{_ocT^9zDv-x*H$LD5OoDrI^b zzQ5*x#Chh*Ag^_mCWm6V@eQBdO4oogU!D<}RAHX2!Q|2U5q<yF($|{wk{I^D=U)9B zj8WNrBLGz0kBcfdTHj^%%K2n=x5ErXlOE(=C8$WRJB_R5gDX}>q@GTF0&=dH54l@F zkbXq(8*NO$_UGOV)`dF+oPPi+D?+r^MqF#%2dmnvc(bw^K)-z+f@Xho^&W+F&U<GJ zR64|L{pX1Mkpw|0<M+q8P!e&-*i7N*@k|D7#OztIOaO@_5F0Q$oO&#(Dg<CR=NWng zS}_M9W?uBqvF=8Yng>JxnU=**(jX7^2j>Zk%e+@}heqDdBc55nS4NYQe`4^D*H3t8 zD<wzg`&HW(o?tbc|LLl^kz02{IRXKLSR}dde!?bPCbOUa{@F*ke|F}G0=!R)oFB3L zQW~+y$?*0nq6yw1dDyEyO;}CzOh9fcM5xEcLzWuTx%7&TLEmJZJ^h9d?#TAGlRfz! zK%iB9>0;*_<@6!#DDOLBToW%v&ikYIia_{nbnwOXV)!jW^5RyYjP}tBuhAddc%_b_ zXNEdDk5BeS+4Bhofx4+WCI-h>ki|&E<HtAL4Np1rAxZmwb<$wvWHA%w8cWosD=i0u z+dc6Ouwk88fj*S$=?B-A6-QID-V=Vi!Kl!%&~A$B1p4~TjTEj)RolD5+Pc&8HIWBD zCvt8-v>zVq3b8G7=ws9=_Y5~A(;jmcO-=6R%!`pF0d%t!uux|2uA=`fcff^)(yNx| zBe}YCLAZ4g3!3Y*+e3@Z$YXIV1Eur&xc?K}<NEv_+6P5-l)Rv0ti$PamHT)>_FcI9 zcrYFs=C|kWUkT9pf-Fk9Y{Gv;8p?)FYJP%cD9DX+KEP^Gq|H7Shggs#oD=TLH7Z_r z66DGN-0Ou-_>G4!FT9Pu&ZEMaQCDE5NmnqfED^?#KcFbf4zZ3A|17sCjO<0R#`EQl zoZxV^x+ZYX9}(x7R>O)M1qk4RI7U;XV5lc@^?iN{zaM(d?lMg~FE5lCNEx9Eu1#Mh z)2c~;z)ipKK@ZtKksHVngQIes-D#~P=}V{Ph@(MUNu5|_*O|!`aMo+!=%K`Qj37F1 z2G`}$6h3=|R2T(k)-yvNGN!9yJS_FZ-NLI;m^IIO5=7%`5A2?F$-Vml6eQJIfRHF} zU<f!&^7mn<dag$jk90<&GJzi7`PwUN^$@i_P?pV^Th_jBj4mD#48{bO>{y9U^@~fq zg8?C-xqf|aC3gS_w1I8YJfd>_hdO8&jtoG7jz|;T<f0G4y;E<ThX~N@NIIki=|6pk ziWLD*PeN>IkTegP!33ih6KRPT*+7X2I>3#NeZf32FaWLm*b;Vf^yeF&SGG+O_aE1k z0f$?}N@Osx3~mKn|A&?m3=x2kwQl9Ucye)JbU=mXAyM%dW5HUJX*{kC(AJJHNyLSX z2E>O<UtDZ3`}xLec&R8a#sbl31cfLHLe(Xfnd;SR=`#AsBS@PzI+zsJ#WM#IdVutV zuf#S~UMxXk;8niSrk&0;PgLA5_o?bSi;s)D2K4Q7e>um}91ciZr9SC9z?1(~AkP&< zCpDrD=&|OoAr`vRGf!z2<EeaW6Z@Gg?Zw62Jg<#6Y1C8w4G3Id;1!Ls={^WX>qIXc zs_c(Kk<nyBCb`0hHyWU}aK1+A;RmAUg|NS2X3lf@i9*~~K%xF7J&8O}DP?ffyg_2f zs%Qmw)!qhjtM5`5tFk2K=Y?c%*L!>yt<W8GQiCxPnF&*I+9p%6FQcP481}CzPcI%k z3+4wXy;Unlb#XJV0$wR*xK;|b=%HfV70O?5@F{n8bbOUwEG*12`d1C~Pj2~Z0h&Y; z*7*wfAhUm{F^Z-2!t}?GSTmPYzJ9N1olvp0<OqtAUb_KatjMoP`Ae<-l?2B*=~_(d z8U7{%aPb<q=qKBk{0zxcRTkrd4@~82s&OAq1kTN<(%TvaqhF4uNHBoT<!wFsg~UH( zL&E?<@mJ*UHC5Mo#l{PFR#|LEJuIb{O|JlLzyBR^wZyXd!Pniu8~oJzlNy#mj`KlW ze7u*}rqnd~<6qFv$OBxv3C3^5`iE;nNw0#dxUK?y70t!*({as_F_0!TyZb|)U-|tQ zfYbwDZM|{<G762LfMa|-2)Bsw=XlP(A}qruUJE$K2lUI!x_vcDaO`=Wt(mo`ml~0h z53zok(GmVyDNDLGYIn*#r3~*>s&>VREf`Za&2#(q^!-1E2Vl@EIW!DzW{iJ0^-979 zHyF1Qv)@BhLN{#)I7Ga-SfXeMv(f~51X>NcHdvF<h=;)gcjcBk*2*lOdtBGtzsPBI zvU3C19@uL651#sUDT(G$hR<=mLmJ#5QpdoMwa)om689CiLdkMn@`0?edG%TkoQFS+ zni1m-#v85FX7<xVaMPc^&=gQRT-cB|#`Nw#`td^Z76T(kSgE(e7=;zGY7`05_SOeM zGy?14X>Lln+EQgGsO`d=;AOY_ukD>n&86%8HDKrtPk$vY7Q)KC&+$Urrih*F#THPM z0ZPVWd|W$cfrx;L412=cn-pqGxohgRAz$s_#%#Hy-XK$J(D&9QS&~BLw&X?E&k1HK zGpx@1{Y@KOdCcZYgLz7{36Pg7TyC}TbW?Fc<%Nm_akii9Y9RuAv*5y=yL^;rm^pk{ zIv)lzR(h8oaV8FDk*3ED+)vHVFL7>Lfpt7>t4<d<K7k#!t>wge5I!+13Ze7%vW(q) zU9ZdZu@CPe<HP_3$<^F^TfTJ7PCw6A-&X@@bYLNhcD^GIDBS5`88{CuwG`dncg5Sm zm7OVUv$&ZHGv7~vmB#aUch<!BvTn#upIFV@sGi;!$SYZ4TU+@kN;41=RF(naTvp99 zbN#AZV60P9`z|hjREo^ZHVvL)7`5e2ccgEht1Ij-Q)%b2`VQ&hMR6ieBX`x}k=t=e zHb06|TuZ7#MA$GF<f)^-(5H+Oj=ma=r92L1O+F3aLyWTYj!F1=3LTfjGi7Y6*N)fK zZG6>BU51`l?>1-~9G7RirVRu^klSJ1Co4fC;b2~7D162HX||^eQad>ZTbi#e*VWWz z%jZ7t!Y43@cfB$cV`hJECnDALvk8%fLL%hD0C?k*)KoXm)C}c5;N@S`ThRSuUnLlv zF?7DoTd=M?c$}wG8LMTf(q=850|t(8JfCG`%biC!SNLk~sUo}yVDu9;I1jD$bY~qF z4gwCtBfB!}&E^-S@x%NVZ^n{bN1S*viSB*!iXB-;`A=Tio4BphFT>)Rap*cZl4K$q zZm3juEN4c)S_iWSzr#ZGkqv(j+%x0;BJON!xC}3UL%}sA)dU6$J!@+jgtXYw$t0hl zJkqXW1TuWD(AW1oK0H;2dlxq|&b+UqAsg>5&%SYM@9UeWqs%ICeRW5_eh@C~GZiK+ zU0n(PT3_k2e)E<D)f=8eqb~)mm<`t+t%2E2W)AmaG)u&fj!p-^DA*p_b?69fq*z{Q z!|<<|4(>z0C{0{gHb&2%Z#pGH?pg26YJ%5<xVLONE9!OAB}s=YwVym(rg>J8r8^jV z>LHDo5wfGFa3`J=d)>2smcJ<GX3KJDf%v?OSfy8gYaT`BU(uC6tY1>%r1;9Xbi}dg z@L^6yt;eQr%kt;UtkdxzvpG|y4wVQSh4|FI7Mi7<4Q`bF=NtS+T$p9=^?C&=1y4}< zT<Upd#h-c~4J_EE35M~jS&o60`yq6|CllYR@?1%`*X%bXW_sQ(V|m;{bM$Od==2Lu z#j`qPN7d;wr`M#G>)lTt#X0J^yI5EMoKrRn6z!Y6P0?)Sg-vHNvG8p0fZH3WKYhj% z@lm4R@NGIdKFAr{ooi#qfO<Df7<z(irY9>-cYj{>bltV;O${tqaSwaTTyD`!$^KJ2 zm3sq&rniLG4!pym(nN5CEVli**yg&sp3|?)hhbkk{+_3qHT-C*m!{{Zgt%Bqfrofi zz`CassKJl`aMBNic_=PA>8BW6lEXy!fbpranV+9mfBsw+aBwg9Qyr^7VT`#`<z}sp zg=iG+ep<60)fV4wvkrlkg(KFF7UEeUcdwATYr{kj#~m$i9S=<IMA)j(>W=Izw{~mE zEg~L1?B-UJR`<7dJFH(f-UTaq_1B&xx@&;F%kHdY_r<4vkLwZ4s;N+FDH+nW935I3 z`8H&y4kyz@X8DWW@L=qolpXXqjCt|we=+vnK}~L5+^8bTQA7j;6a*ARM4F0#)QAX( zNE7MJMsG@o5CS41AWeiw4N8|1q!U5{(tED~LJ=Y*w9rCG@;$!qdybrYzdLhh@<(Ri znP=~{)?RI|-!Cl)3ywJO`R}37D8B>GIlHEJ&H+X8O}v%9=RB!v=Zt@qnX*2;yw2I; zz12-w)^PlFfFU6k_$I1(75zP-#HJ@KVwT^BoEl<q<i{19mjJ;z^#t$6K=!_&2dgp@ zymjWu*AetxPo}$!<j2y~zs${2cjHK)*K<c)v0qP0jtt0pYYSgv{R`+?Ml}Bk2S?~P zRRdLx$3P?6o_ymvSiv<T34TN-$+X_gyyiU@)2-PeuyWC95w|6w_0>_m`<y5UsLOId z(9LY+{MGf6o*41%#i<UkzD8|QS@Q}bc+a%~itc!=to|gZV>_r;YcA=w(*u1|j6HWP z$V;pcSE87k|B?qGimfy{>1k&YI_xr`#<t*;8OB&~WjVv<bBkqm>V=4k-cYt-RVMd+ zh+9wEVCx5w2Pnwg!qK$4ZV`V8YR5}1Dd*tQ!bg!Ugvqzmy$If^Q!|T1nf^Q(o%XVm zJs5Dl+|~*ogHL`8298V!*n!!BRIQ_qA*IvBvapN!dCT_Q;r$ONw(dL@#A}ajFWlKk z%M4|5c!X{D?%f;Xwx2+Cq5Jgk7?(iRa;gQF4>Qj}XJ<`f$91C#M-WC~O@Ay)%YqB! zC;(BCBFbh~>sVFE&Q&Ak+<hrb*o>Ryj+V*j&t=`DOFw47QeULI90qdcM&y<$d+%#? z3Hap$qAxAhJz*;jlSI}FPAZ$4;A5^n0N>2Xl1Uv1ivpFE+d|0^)*gfx?gRc`xi;Id z+j6ePm7k+<h{RpN;ay4T-k0vUD7ST7A_OdzSz#kCCTS+%F9|u;Wo5u2c-s~pDzU6V z-qQ5n*i9jYbkqkYJtiA^N^<xRhqR@IJ`pI2acHj)47}u4n(BTAf{Tqrd3g0=BPSgq z=I*~TVlz7a_VHuqZVp$t%MQp9dVHyvva;KFc9bWULx1v2Jt)pQ64zshbYn;zTW66g zr4jALnf<|+YZbQN-Ui294zKIjIH)VCuVn=vwcM!`9?bKc{K>(H!c6qgmN>RuX!f?m z^jO2GvuP05SRvknUv-AUAo=-+91<j-?YTjNg8gIUP#ofGX=hrrbJEC0<NX~8Y(=n7 z`4~Z`XPdG<`Q1beNlodVvV6Bam~i}{v~6tRvRe<u^`s)rcb~TorL&|6+gfn>mFLZo zL^$oxTOXYKn6cFMyOUWx-}n?y?HlUephAyC9MpKddgl@Kqzg-zc~#gd-}lA~)Co^W zo;Qp&x=$6Fc0VJ>)nnh<!N2|BN)2X=?#&o(d$6DY?ggC}CM>RGV0|Y+8$0**J47NL zzkBX(j?Vs|Ey?UL%h0}&aK?X~<MLdW%9IPltqAcdk>L4rXIr74d3lb)ZJi(UXkjB& zLj0#q?OZq;wvV~1!6QkkD1JgaScF3yd(Slh{eha7mL9OIBd33r2*;8L8U(vvN@BUr z5pw2RQErmnq>co=xshHbnil&Aa2x3RU`?$pRM_Tv>-XO6Q1==i1eCHrR_x0yW{QsI z_3>4L+_tYhEhg5rgB-*64Jw@8tP9f0v(IQ$xlG!Ye>Uv5Pb?+cRj||`K&5OvNZZn% zmcyNP^W%^Gwjx8i;cA>n$kA8?v1gZ}-kdYQ038V4;p-igRNQM<@YC(GAncB79m{o3 z_M3uTz#I@??)%xyE9OB%kz0*S$57<)P+UQ}2yYr8#PGN)MGm@=VeK(<l1Jg$6JF^y z_vcF!kz3KQ1#*C1^T={DZTVhs=Ts$Yi%=bQ;n8g(Awg(j_Zp>AyVPwSckAokc61Kf zr73z%1C}~9M&=f<LPb_tKW+X`{CG{u+VSlANlojZcpH((q|j2ievMF}G@11}P2w-B zHPLOA#8MtQ*Y3ai4pm)&Ks*D=&3rElNcGae^HCSRipUPY8Di6ov}Qx|=9for*QB^` z`?E8-#8cx&y=|DYYSb-a^^Ek6PhqSEBhPU6PdJish@KoMs`{-G+O~L?t)Iz8kWde+ zHUhUT4HPakZ0NYAJ^;xgBX?igW0cX<w#WOFxcKLJ{!ii0(^jVH-?yW}M?GIrE>voH z8N@0abePzG5PdSaIV#FUqKu#8v^4P=LHPMfbI^8f&{ZmQDKzz+Q;Ray9yzd$a_+9~ z%zr_1<P8{1w~Ry;rY$IM3<Dhc1>k%4Kie9vO?{kPvT#{$%P02+uAcldTK>b>{owh8 zgR5qElGFquOhfIi>~fUwgwuE|V5aY0@<OCuGua2hmP-;_N=~gKI6X2Nb{*2reIxDg zf0e52deg5jz}csWxO(5!w?uBF`V5$A<>zm<-xfVCgwcJ~<o|5YllV6mfXip<61u%M zPRf<zy$}2e<mL7CcFv<nrd0e04P$9`mqvM2EiFVGvy_4GzQ`^lqSO1FvVs?FsGFzu zf_Ln$W`FvGd0XA#1mXubKD7RVg#~QBrW$zCWak5IJQ{Rr!0w~2@@XpjpL%f;%W?d7 z+pFE$MY{NE0s?LKf614+?gfWI8uH&%qPj^+{YBQ3KVe*c{wZ*bhh!=ZFI@K0dEfBX zvyljV@^XOhh_8j+C{>J5lwys+aEh8LIak!!50#y9mclqyEz~bgJtRFkKILCNcM)Yh zI3kry28-`nIbO4G0!j~sXSnPwA)@95C37gW|3qr=Q99Q%>t_#pR^4s-`yV{Rkbbvk zbDUEu2|y9HDYz~@o^f?Unu&Dhp%EedXfh@fdxln9T74nErcc!shc6&Xs<HcQTNajt zZ&9sw(}O#8)Javers}QrnvTav*<A0Fl)25fXX`}Z-O@jJpnGe5Xx{B5UAQ*)_%IPl z%V*!3&wN`Y=Ul(=30~2`;N|s`9h?z)SLWI93Dc5qpLP}p-banrR99kn7#*2WpOjA2 zs`IT`?NJ6jZ#Ng5BRDnADl2k5<}1CG5N%+Sg&Sh-eqa)o4XP?(hE7u(3-}j&&|gE7 zo%wf`1{&E^e3FA0SCc;7@``Gkzf)kPf?Qe=ADsG&-G6k}O5Azbpx#^QW=Hjpn-0KG zr*v<Dc%i1F%%{F*Mwza-XYN0}gu15gzQ{pjWG%?8+6SDdHG(%U1O=SjNW(X*@oE(8 z$KCwt##7Cds9wZ8I@bBZ#T_VcwtkgUYQ@-w6BR?PZh^01Nb}dZM$46W<Z{Cjlf_Wg zUn-^AUQ+p_a#<!l0+?qFN>PM{(1pw3Ia`-Y0ShxzHml4xJC{ZZXA0}6T~<N^Ir^+v zaiB@BB^u#5fOq{GskV|L=gJm&F>c@b1U!1#rw-N3T;LOluryM`C5qi?PJ&kx9X}gE zy=4$Dl(uA-yxgi?c6^It4(eq@?6XZgdk_#==xVeT?Le@fhPG6W>l_vdM#4TDbVTM^ z4~$Q}GWJ0lhfO<)dXOj7m3lDWRzU);d_O4<pA(1O<QCt(w&%~~xCQ5`j#&+m^=t|_ z7X%yoN%nI~-ju^D@RfE+2%u%G&UBiTgq-jaxw)GNM1qINinu-^WM)%ri!?f9qZ!9^ zyRMP9L|tF!_JWtXj<E{oR5DWiJF233N{K5;;+OcH?-{$M3lisCZ!n8^7W&bM63G5u zh?&offRbdszlb5;_WJrlJ`Y*J+?wp!sFXN?E3Ks=o4-20-Y3g0@u{F3dx#sEAg#fM zPDfrkSIgI5ZywIo@11?hl5pGt^1e&3Hym!2+EL`3(gUl*`XG{?G=_iZssC}G)XXzQ zlkfC8*4;q7ESKA+?y-p8)oU1ErAUsz5_&ZxvRx;qol1Y4R|OjO^1jC(rkXYhgOBiH zbe>zEvf{FQ<~605hrPY7V+ggYcriUUGqRB5q<q$s`7x=kJH=9(ZoI3f6LH>`zYhdG z7<`X_9k|<+P(Rm4E=EPLiM*mHJy{+KgGb%mV1ClP%v^+GZ&X2aXQ+#^m>DA6a&|Ao zVvBUnT)nrD(qP(~r=A3N<4TL{Qu=%l$Yaw1Vxo5b#bk6K@|{)uo_>&c4&Ovd5eegB zi5=*4C>wAyuI=D~Y{BZM9#+dnNcV+7AJNvo$(Dc^Ye3cV+sY9CRan|o6WET8@uQF> zNV<Ant!N~c{K@wxt50ca%Sg(2TKApn-=&)L*F?Y8SbdwQ2qGU~Lnd>LavGP$a5V5$ zh@>N1a;-r70cQR4u*ZsW7<*8LP*IjynpP@#Hw70Tb=yp4eFHU?L#)7a`uKJw<HcN) zU$1dT)FK~I`v~jTN0WI|6R)1eJT>HQKQ*I{Gmh!kDH_q*?U`Me)al9Q#2CjbgqU>` z9<}hMRAJU(ASg6IT#>3gfBm^-WPAP~=~Y+Py-Iiw5fM&~4y>KCwfKlBdF5QqZ}n?E z<qoOY<LI66I<xo7ZF8^@S-fpBxv$#$k@gtz6Xx~|#`>V?FTN4b?Nl}Yn&u9of7QT2 z?KF6mUtpEGLtPXmZ*TfeIxmqlkEOZV;%;BgnsRK!!(joq9{&sdb!L6E=?-E)`t<yb zRaye0pRO~}JE3WQW+YmtA|b3U8R7LYHT31y@_P%(T8u52RvyZZ$G`#a(~5oGvi7WE zr~0MYySijC*~Y_z&`*Sqa##=yYP?KN{M;P!{0JN5@E+@~@L#FN`n+dZWQ7Z6)x1PP zmd}jK)qFbRym5Kk{$~8r@GH>mJj<@G!Td1~2YptQH?}{r@Yl)+0L<hQWV^Dy*~6`) zlzE&9RBXMOQ)H!Wti?HLMsi<gsZ6Rp+I32QM4G8Yu|bYlm!?w4uydcK%I9D+2oi6i z=<{=}-zaqRn1|9n_N?nNy`_}LFd-g$+Z=4lAXoc5etkeKu%?4`x(cGyBINRG4KfX{ ze#(~Wrk`~;4^-7{flAcgYoI@)A-ffax1#vi8-h0;2E*V_1EN5v;B#?{r-_neT4H>J zpTx*ZYmN&n^TxL^4Fyw>)zU3WsHDBu76t4(OMBTsd(>n#-h0tEYk92l(Sz{H*@nES z6F<{z7p_1z;me*sE3A2|=`D+TY<QO?IMJyq7-p$TS3aPs;>qbCZ#&&(rWV149xRSz zEjahs^YA5%Oq2%}%=yGmB&wJtuU!f6eAu$>0UtxEp5cKnFZXP#-p6@)E@BqS&48xH zn#!qp>VoOB0$KL26#q1+!36Xtas%Zz9gcp|;FNJ|$O+<(9N&M(_td-NxMHm=-t=gP z^L-W{#`<J(!ienJVi)+>vV=iZS~SpLcmP_E=<lp&zEM*<8xiZMBL$V{<X!XMBY^xH zF7P~Z`hkbrfQCZ(S911cN^ARO*s<Is=dL<PJ0!8vvzme_eT?pGz5l!LR>-~<8o*=i z4#g{#RMW;^L=bU3M$}O_vLaunX!qWZ@6X48eK9IF{^|4r_t+l--<K7g$<$^nt@!N^ zU9eRt2zFF}+3l4Ze6tnWV~@Nq`iL>v$+qzZ7|xsI*YqBD1;ywU{B;S%A7=Ejt)7`> z4*qUAWB|20ElB&Euo*XL)^!~@!=C?ZOPa|Kd_qh~gRFO4oK)u$QQ3bcXY=Q7BBL}{ zP{qf270jXrndyN-*ntr=@ey3d6X72)sc<7V&i82y<);s&erO8+WPgObd>rD#NlX7^ zbdlHS`}0yu!gHM`u0sV;-b}_Hc%+mlwhPLH>Lp<I#v1BvXoTp>bC2|Kcg(u9y2)I+ z?Vul;hOv*q?}&k%dQ6+T*po8E&M)pjNe#muGa9K11IY{cw$1fobA*7*HDasIbe9cp z>1b2_QESjMD(mwPV?U;o^OhGQ5r$a>7YFfeq+Fs^%f`b>psv;j+G3rSgHoswd@;q; z2mED;*pKqn40bXKSSB17mtr<NxHGNn;Ft2TXB(N75<2Fm%sVVdaNi%p?QZSN?~vVQ zLby9my(kSIFNZw%g6+YRY~5)yq|J!AaYu7|lAcZeQI<QN^AR@IUd*N5k~F{Rlw_ga zGQltjzBP)q_nNZTN<QXo-*-LwDdtnJ6n#%`%2}1TiSW2P?<;MuWEdGj?N{?SDFPb+ zP`d6S^z!#qSLASC<H5IkhLIRs-Xh<5jI9}OUXy0@oCJXtY{x)hnq!bkbKDtUP>I$s zjQ}ETM(QJc<bRFIy>o#o;&devu&NvcL6d?^@L>*_#Lv3EZ1vT-yuQFA%yk$cdF|X$ zv-eC(Laif3akp|xuHUhuL*P_~+YMcEW<22;j3+=`9$c1_y*ap3+ik<j^i%0RQ`nD9 zbt0WR$3;3%J%XHGd<^GBeuM80_|<APINN~V@g)*2>`88FMR%tgsUe1+#r`&{qT>ah z-aVRW05NB0M>@oct%twr_^};?A$6b8OrSwjC0v<}rR^>oLtkyPxsrr1-ZW(8NCI4^ zBt!HT2=tAeGU&hVYFFVjt-|{XrmAn*s)<=!BP6$v%n!HNW*4w_WNUG_Q{5+z=-luk zXtp^!MDZnuid&d&^a|EsAV`_?P{b-eaeJxmw*{S-jxI@UL51&rK4$9oJYR`#D$aMd zb9<{%J|Y$D4CK$y+nFa*B@385ovlEH%#-i{h}~3mr-n%o-_CyDTrbHSVfKl5HfUsR z#$WHaB}luv%!(b)=Rxape`Lj(x(imc^n3`GTmIF!BOnn_e5G2kmF!JW7E2n;j<@OA z<TIBwl!@<D|BtI-xP%~{TqyVMPF)R^D#86CcOuSo!t79y4I1p&_SWs${cGyRd<WyT z8@7|E5wSH|z=op!$9vT-aH#6$T0Ys&>et4p9;*pi;Vy~r-{I?4s?5IUk_>#$pR&4U z4kJ)tFM^}04k({&&#&*OfGMnWuDwz!hp!>)T(s}LHImg5j<_M>l{)UJd~@7rlf>$? zTv4d(DSvMHi!Z5*STVr2Z>dABV1$-hvC~OJ+(FUl^PzP3LA@(o2*aF{HREg5%jCyv z-Gai{+pVG2nD{6ADw((*Of~aw?Tb#z;N3ZVBxLAZam|*cu!*?<<9PTbmMg`hbDb6b zGkt}Nu$(J~6$>RxJ2~nVTRYYB24l)1g|~R{mCUOI=h%AYO}X8oJo_2F<AJp`Lpkb2 zi)&5MH<{J;7vXHqZRV%gxM&1-p6dE*#B0gEozBFD;cmXEZUv8mM*lutEL+W3H^W-$ z{)~*suW#yjA8V3O`vIQa-Y9J{eu?Yy!!rTvLkWf0T88kA<x+R7pJn`Sod*yN#~B}5 zkq3?-Agjbc%>&G(4M%~IWsZ3TH<qW2lK}=P&|XTY(Py}=Ve|=}gWiWD!&1&%Bbi1n z7k-Atm39Vy(UxX@Cl|cCs#KXgG7)NHy*IA}ezUejCjlij(Rq#DoODVhc7zXMDkLHb zX{fI<8xlA6I?}Mba8(2}bzd;2@nCLt)uVOV#&ScE7}(a+BD6!F;HNF2l$SBW;VR_@ zALDuxeSuMC;B$3ytpY9Xe+F>w%KH8!Zl1oRmi7>3#T5!P@|@Kwb{}KhO9=qHflc}F zEbY@Ama{C<c1QT@+8<)Cp=SjMn%v{vzRq}mrJd=*XIrp$#~d__+B^nQ-1doSFg*+h zHUC3n;_)f9!fgGXV|-F(>VugZ0*EipSxlL7iNi&e2`A3f3@BteGC;^n1P!pRn&JAw z<~noj?&PtB20YL|U8IWa`+#_M**<^$%Uogg+K&`?T|Sh*i+nRGi36n?TpL|XonG?h zRi@KC>s>TQ2{ucNZ}IOmPuM$IX7uVnfIJ~?RvZ7$f<RsTE+E>p^=*uyIAV+;rgH9S z{Y;Zy{^O-mF@Ngr<-t0q3w3W=@??x$VlNZyqm+NReG=m;sg6>J`j4TA;;$_pt$q0G zeZAJNz#y&M7t3$=+EKKZyt+F{q_6jZf3l)yj+$ZMq}Voo{WMf3`lHqpKV2urb+AzQ zLEsu>{UdJ~tu8yQ!<BY3o+=Yy5d${0FhEgfMb{0RNO(7zM$OZN84!k`3f^&fgj!nX zcc&eMkXVB&Q8Og}c?$3g<SG||@ka>`(z$KQ1J}??W*^sitMEyXA41}z&soAOKZUxM z7<V#%tHBScqU$&FPgya*!@bew+D7>jQJX_iI^JlTF3Dta&Fz#$ppCUpCM}2^%Tvim zmbLr8ewEqN?mZKuc`8VT)9?M^frM+<fFTi2q2~k73yScl!Rq@@0mhsf(G8%H)j=4t zH+r-^o}*+hrXK`T#U53m-Sgxs$UM&WZlM1HLx=E{fr?sCx)%hO!6Z`CJE$Y^Xgv59 zz=384+_;Xo@caz(M`<;DG%%|rTrfUgHCphVM-lR3{A^j>_GYN&uWZ|e98&S+@Edrc z^`+2@iq<<%Q1YRn1*DVK)qjclVH(pJ%6R&7-mcc75S6!XKd5>|S8yW@RKQMIYDul_ zKS!L-lpG9>do%lWu&t+p6ES)Zgeux>TVu^X9+)1w{wtJak)r5c`$xsU9;)7$MH(rY z-h=uf-J;YJY5bO(r|xp3yySbbxNOvb?Ij6$Y=wsy*MqmCu>*+>te}C)7Ks<r2EMjT zS_;Ju9}|tDQ8kagT^m`>#z*3ob<Q`vj(GR&4$XWO)W$d;*XO=fD3Q<yG|}}0+!64a zMuk_KyNk>zpKJiI<q5@?g#7{e-aL@qGLsSLr52V(T%g-0FGtLOy7kbN5;g{-rW;yz z=DaZM6SQ&6NEfjB;e;MP$(5h4jnOWx#%LxdPC>o1Fz`)YYifU&$3!F*!SgKE8BnU3 z8!)@{r*h7{MJM{B#8@Ph!<us695%_0!1iy8uDbvzRW815)MxjHj#AgNle)rlt6XLk z9&)<oU{>&!^)-_Uu>lh$L2cB4T`G?y+F-XQcdm}5%o-w+w1?@4+fEVY7dXOPlk6~J zrB#Yi3N{p*5XhQB#4rcJumO7|X=OT?t5ecU2gSkxc8Rq@MZ67(q0%||JsJlSMzo7o z<X1}JH-z}oR;XJI9@0)zYC!QOKv32z=?^EDfIa}_a{!&bI2|MH!)_wzbozLCcvuM5 zisJdrT@XQCEUaasb7g+D&r)btPFh@5aTVKu6vGb8Q;cwv4u)&!PdP(1omv6yJzv{n zjr`%#C%R%O79+`g00w^10#%z*nJ95W8yO%9TzoXI3Nx#=FUYxZ>n*U5t#3v9h6o_~ z=ZWZz*u<@_0<DSro4xmXUI=jX;w<yeefZi5^>yCl9Yx8MxnMLS#<xbScDK?xyLoIx z)=zZ71y0=l3Km}^K)%<3#Ly&A>94}I$518e3UDc)17lrFfnThSp&q!+KWzHuLuoRZ zE(|C3%v0l1a*tj}3_d`L<iSQ66n-(vx?Dd)ClURkV$5{*N@@99rGOj7ER+5L%-^hJ zHbR!SnYD6Qa90<$UV~-6Buo5StR`|W8BKvw(aCZ9N$4hpCtK5stoh^W{^$>@hQ!ok zJ>26T#g(#3cc)d2Xnvv}@eH074j-u^e0lh-4;kZ+iOuC<#|MQvmp?>nxXWZI+_`N< z@QJit2g(ocNrS7qIE(o;Aq?+J_`W~1`rFuRUOI#pz4ad}I}&8^CQ$S9&@u0!(k{V+ zakP0i{*<Mncq+zwl)YPwt0K|){OScta^`YPX{aA5sa$%X?uARvESI%S$f~MMmdkV1 zt48e{XROV*`}e$yvU#xjMyG|iOK)u+Rf36eRcX|MGCb;a_`L~ht=poaiUE+>8FNuy zh_qMv*fAfTNO<YZ`JC^1Z)HJ2Fmhl1HIlLn?vbVDi>_lna+O7z!G5<l)10~jL?2pF zdKR~~JKKkv_k$fT(_5|@(N4o#c0ZbQ9rTTsmpwHQX2R77^OQmvxhC>iYFKF=dpoqG zLUwfSgy~EcZda07OVep&qr$}68@Ojvw4y{$?LXaIVe(iY#CdvllfdG?)YMleCmiub zLbd|~@PY^S`tF~?!%Bz2_tr|JtZ!GTtBhk;f<&1=&Oxpr{9jg7`Pqw)#B#sC6+J2u z=iB+b=R1Rt;gXm+IwTDYJy~nLGq%!;;@|-T<GkS#8|zYOxE2d}acjP1AP#{JqA{qw zpX8z2^(dvCrJ)$$BH_b$WkH9^PL$K}sbW$F#(bIrB5e~3$M~uvSSlDiByN(=$ZYN7 zLsghCUQS6>@D9%bV)NQIr)?0d{@nfnquBbA!M4BBfpr6IG6kKKvi$RXL|w;RBG3(h zfSkp;Ov^%ce(B9l+8SS*R{@<Hl>7|ktw!Z+*w3<)G-aqYTSGrR2g{u;*l@j0wf%$S zAHjHXb)|NutjP7<rFkpUi2yyF>v>iKSnu6>7{1SR4lidTG4`<pTGXMARhy9O1=kXB z@4pyD4jEjSCk{oeKEY@=r7cn{W^Yx~+&YWacZo7B<ypk2<>}PfWw>}24ir18V$v-O z7p9-`@4s($cnnRJ?JP?Icfapsu~_Sp91nhzu~b7bhcp^I!ka8kK8YF+CGKB#Ox^WQ zH1(h+8FupBbH^x3Esi_ma`NnJHp+aKSjy}+i!&8P<0Jsf^4;xL9{gs{gV5wC*G4|O zC}y%u4eZc%olv+`t&_+3lIlxgBOd+L*3Nua?`<_OC5?qFjH3rD9iPX4*~-wjO!ew` zQEK;d*RfpI8leUX5;h8&Uk_kf{8n0{D=#>ln1>ImmU4VjWD20pqRj8_MjL;;rN&Y< z+RFE>2Q2{6vGt{sM@-d{K$45`eevM>?Ar;}GkcJvek~h=i+tj-CX7c!pLjXq>ew{Q zCrOmSG)QS})J1M<-tT-s3*|LJ+VdB>qbvB+^#z5NlE18+0-P^_?o1E0qWagE9kfgN z_?{8rO;MUUYb%PsuCv7MOSp#h0aF5%TsO^vWZDn<@+b7wW3FvX_#YI8Y)xY|Erv|2 zWE5QwETOjoFuueeOq5*PoD*eUDi?UXnm&KpU`Ik0MX|oz*8bH~(3c+qge#%=rdkvv zZLDRDH)C$@()#8x6>#!$_w)SaX?QzrrCV6o3R%pzw!Hr<9+GR@vZc71#lukz2fMx= zM<cyyC=s4=E5_H1k3ct=^J;$JFMAf%`JXH$IjBeL^$eJ=uJzjtk|oG%r=XxUN#;KJ zQHA@_G%9r|O&Hxn7enk34EcZcQJ$PU%3)?5af0P(^@|Y)^9%n>iuQDy*XOs?xVcNp zQR0Q-viBo7AL9g8GYIqLb`!13xu^p1p@P!b6FiWb-B;x&*DO`DlDUN_4Yx|54i(^< zrJe1E-D^2TS#XMVQjvBCQRj2d4YGI1o@tAdeqE^8dH|(vYN}`FzrA#M#X=_O_YVlW zF^H%xhrUSKNXUsKcH`}(Cq+&R%;xSKuGx5M<=Zr}leH=KfNdK?&91cdJ_$2tX%Z@Y z)=@WCvuJD4<uLp1NsiZ>;Hu)gt@-)1?fo~OLOnDnR^4k4Lz(SY3k&8T0(^$x^pc*Q zXa`kTN|N!SVq3wKqDW7^xAnza(dOXO4o3q+kn=X|<Lk%F9+!s4drYW}Rq)(?wjhU` z53BBWzCJ31U2gsUJ6LYSwka&!w19fqN>+0Df^~&VFI|`7@7)~A+A3ptleT}yQ?+#O zej-gY&C2IFgTp|<nW5aX2PXH^o*I4utuN6bvF}|YxdyLL_FG?Szw6@T6#;b;nDJg4 zh98$lm>Fv#jIJzXQ|8~N2;_`eR$jwzjNx{+?kj+1^bGelmgX9u$=BC8kdSA(t;A@z zGq<9g^v9+`=J@=qc_{<xt*{*mfTDTPHSgy}+mwU4Zk^h1EI=!+jqXgrY&Um9tmFq4 zTr$a)Y4DQTpbx3~)Y*lkQaOFv>Oz3|hIbf2bVb}48NwUzLU|k(WDfn`&7Lf`AIdc# zYh8ZX=#;YZrl5s5^VrWFz)kTwqJLZ8)m^KonGvHDT0lFlIi#bacb&U`Hy1|iRtHiu zzH@yi4o`l?vz%3yelQVV{~qys$`&a~0%kQ`@9dqCLk$Ss61Ckw_@w{K3P(}MD%$<R ztgBH<y6w7hvdn*nI*4q^8GKe}R%Y`Wnk2iQat`-RRADa}C_Dlisc)fRuQQ4}^_veS zi40|D#FlimGMoeCtW`2UJEa)sj*lFmY7GWV%j`S6N<(LwKLlJ&)`BNCuPcQg1Awx& zHkb~3U@8V|)1yBYF^nig6PEgIh?m4E?$1EDdt<*FSaKdcHVcgBzUHsOAFf53!B*Ni zjuer4E+V3gtUUA2lPd?LGZpWPjs?aGZ+Uil@vaTj9^GCu$B|9EuI6WEbV8@xchjk1 zg>vXLq|z-B#?;dDo>aS4#spW-aP#gn*FVT96(KO`8+cPAi*75#b?Pr{yvLYIshcWH zUjc-2%q885-Ie2$bUzlETq*k)e1M#&u?-s|S8!NM;siWPxbMu9scAr;L$8^zhBpbV zNYC$HT(YpnNQ4n(k6&6}h`Ty|m_p+kKp|mS9eiz8oV<8moXFDDtYmO`eRW{R3`u#< z&324P@@{)K?9shU)5vq4+K^u=8DT85*OE^<=o1{IF+j7BOt8MP9;o&*Oeo+woK^Su zajGsfc??>dpl9z3>Yo|m^vZh3p2CW34LlO@mWC`rR7S7bF0^f640WP**^b<@L${mX zjLttTB{!~h@?c{{+hg`wVQndRK0|OTCkMZlZ5uxDoQ{OTthwysS^`F}<oSl{#9nl% zOXB$|YXc=4HX!)v-se6X{0tt}3HBpTOyh;T{!Ef@==Ibjbte;6j`s9cAA^78QpG~B zgW&ZN{ns1PTs~VoQ^Ti;EzCJl$JBchw#Dpc8DQMUYus>#xD!O<dgk@tlYoSwoSWr# zAANkf{+kz@rL<V~4i?>=9v_hFT~xs)5jZLaJejq1ie|fVj_6)H^SLF2Kf)`YF^KrC zB9o7&_oG}ghT%Y!53zvR{+_rso7*0G&ybU2Xi<Uq8j9mt!+?~EFIa$GWWMQ{-)F{1 zgPdsx_c~bs-huRR>BCcKZddE*L>Lh}m%Uh?6a2)<Ltz$-$O_EflcrOa7r;4c#Kq|- z_&~ZLTdrfA>>$c=_Si;}SfqXXw<(VXb?CW~m*Z&K1BU_Pi}CNZC)n9x18+83`uh_Z z1ef!#O9GdYk9W#bZ`mG8eU1J3ZHbrC_@NGHGO9b94QAaO818G@?OIoZF&|Cu%?a~0 zxBFoS^%hFs)t+~sXGo<Sq)kIcy}hpv%^Yn0cT+Tyv&7P5^;;W>@t^*C_Lmn<&`;gd zRZTvuGhe~FuleswKB_I=Z8XJ>RA`z!-og9Z+Y3uw*m=Xl8{w9^pKo8M=M82y#G(tO zBf)qsVqLkPfl@^@_0}jB8v}m~a-LEHMtW0gaL?n9-3|RhzxPGcC}p^fNcNxL^81yV z_2s4kA<)-+sId>Xsy02YFW)-_;qFUf!rD!2ZEx+$tcyfV61Zx*h4^;8C!iKDB`X08 zY{~`x!x#ZC)}t&>Re#1mVGf0qS>VTX#*2yl?*@(LPgyR?50te~qxI*c_3fHcAa<*t z<0P3v4|5XLx!gTMx90MS<@KpQd)MXpYxIZnsBDC;Be{A-+|1Xvx{|@w+-Q9qAM!8g z`}S`R7(Ywz6sbSHv@r=7`7cs-xH)W@>PU99varQk%v(G=j!+W%*U<krq2HtJMC4q- zA1k_)KyPek-8ZZRAQqHIw41DE1+8lu!?oH0Uc4;lod_Z?>XyyFNjsB&BcmXc*E@ff z56~9z|JY3Tuw?G%u=7mqCX=dH`Z*Ef>_MCW^g!!htC;=2Ko2fWU%B+$DMq&=sL1bo z#Cs8*QMCEHIuVb5@jx{u0x!N}#q)vb@LaXV-TTHIwxsD2vB9gUJlg?Oad|Aj6PxsJ zd`<jse2wLW4~&N%t<wl>8YvcNZ85OL0(dK6r#v+tjLT=X|LhBZ!W0hxD3M9~Uwt?K z2t4p^HodXOkcdyiti_K)$00<Ajv;`~V9NO5cepA{)c_RTof(b(Bkm*gDfDGA9cKV; zH_6^x4l{<&vlI#6D4CY@Q<qiV*%|#yQg<&I{zjK9xjnsccuCSn%|6GoqgHGgON)%L zHZ9fTPr2zWIb}x=o9O=spr=l-b^NHZQjr|25TGJzZ*K<}*qAu~Rm&O!<O1CJ9YOVn z@fLe5Pgx9iddl^AGALyhn*rPWn-8yx_<UzT#QclD?X3E5JdlsUjjM+RaSj-X%s#KB z+-QvLo`1Al2EvnQ{r!&%rakLJ{-sRm-}_y7{Pfb{{RYq*JH;E+i1QhxTAjuc8Y*4` zcoye}|6*KijQwpJmh2x+9#$|c@cnmjJtKxNc*X0f3as@;P@Q9(%HdA${~a$TOmF<w zTG@#2x%<hoS-cS*U{b(eUOxr!nEZo{0?=+(0km7U5yOka4W|dJPy2=}%N=1&WApjT z0PnfjYDuc|Z|F>++>?nf671zmYELjMdbO<AzZt-CQ7O4jjD%~GkIG(4Ia}S1_Y(cg zuk!z(z2XH9?dQJ^zxn%>OZ<h7uO8=wm|%PUi{vZ%$c_^1%59zjeffGKk}xCRA;0h; zYqJk=XZh*j>#f{0K*EMS%Tc8#{%Mm1S$qr>Ydn#9KL%kTWUXy&@XFvXl+=z=R5)@? z6^rqU_bRZ^ZemDPQ{4`3n%eAv%ZT_29Z!|}7uD7`mfybR*Ymk2e~kU9k>I5t`5&y> z`8&fca{dec^N;@vrT%%Src1uJ0_HNOcsaK@KuAvyrnqIFIsZLehUw(#KU>u;{0;3K zhVcm<;<wx2zr^p<n|nVnKE6WFze#-@CS8_F9PcLGTdOyF@vA4Y4jb#$nS57%SoF(m zKq%W#fpCLcwmgSU&ZbV9+b|)jIZj6*Ej%K^eq&Z}i<B$j-J<_UKIdvAw?05T<~&g* zTzKZhAL}g!mQWrB;3NJl>nt}Nf9ulGFs+F1$TZ?(JZn&v_VbmJMx(Y<Qvh_jnN_f> zvJSkHn4?eG-%hoFxr*cf^ftYniIVmJ*^V~+9=9uMl}l$jRmxFXabrs|WYs079D`E= z>mw``^GpV^2a98UN7G1eo)M6qWe!8vxfJ&ulWsa1HoqV*R^xRGP0RUPy5JzV8k!vN z5_;#Ki}A)_;1u|nGkl;sbP6s3NmsGxaQhck6A;UYwRaQnLmB{07q-GV<4kHzxa^v} z9yaaClS*a38r7yJsGf~QuNQ5Vb!2Quz;7CuZgkn}t|Y0g&3r^3z#CL9yX+585iAB$ zc8Rk>k5caAAfV<kL(j85&{vH!yb)Yd4tFj%-4gLtM-zD?;dlB>)@)I$OCpl2KDON{ zsya%<0xS38e;6Daf8)_VTb@6C*fC%F{JWdRQsh@r{niQ#>mXUyn+=wUv2RaAhjUeC zewC|#vvkItUzKTs+3ZEoZtM1LZytl#y0$kp+;3|vlVfY9%5Z;60IxbNCLxg{<v4P6 z?z4U^(lf7!1Sk3zSky#W`jf0pi(l#WOJF;x<W&`QX8CqTKFZLD&HOKLcea;z5UgqP z81pF3EX&*l{lpk$7hCCow5;_5u=-{W6QczSQZ2)UW{Y7tY1ttW9P+Z&A2?HK5xPN@ zUIbH(Am}@jJj$SCPj%9pf85)p-|*+=z1gRBe+)^N=Oh4v!xs^gLJoImXgyQlKof(k zWh6*L_tpb#gO}%YSvpNlo>~Ze-t6SqQrn}1Pjl}fPJrQ%oz<zy+DT{ZRo&Ch$$Dqq z@9Qb6varOYNWKqzSw)jsIKXo`4x8s24t=|BXaG^ki>1a+T^Rn1<8tf_9}li{>HGe8 zw^4I;Qq<@o-CC|9?XnEK@=D>sCkeUM3|CJb%?`ombP|nN@+!?SjKAgd`AfDb0p9?d zY@aVQ{{EO3Jln?VWv!K7$9=YDISQNUoGeWj!~;GJU7t~BdAQOfzp&KTpWPKJ?!5ou z0y{e7$7r%kjCiVijU!=O2KP|{lXRiH=g7&J|9*hqh`9@7Hy4F=KGHu3{NKkx>gfBz zits^t<0BfTk&fy9@|wYXI?*j%*<TIUhh@K34|0=4`UH~#WZHSUudMSb7^??Retj=- zf@}sL6S+jSrd6xEQ{~#rUlhfvF>xa~p_GlDUF&b+_$`0>scfy}Ry_#0y~v`*dWsoT zHB?xAd%}2^OMGeAz;sEe60|lUobM^MGVZzTCY)q*3?Oauir?mzZhaFH(ZAWwstgUF zbbz;N{kAQvcp)nrtW6Ajpa*JcpdVQX^XkXVI;~dbm1_HZkWc1*uh&`SU-|6yZ6v6| z|K49Z7fX7bS>zdxnul~(?*9FWVYMp_t7#V2K&e_POh%hkeWjm5s;6G|WJq~o(T)Wb zJ5`v2_u%uJuOd!her&jMcKp1+L&<#HQ2iGwmtJD!P3}pcbq(;x`KiA(69rKIkJ9}2 zFTgly?ro}^?R7^f7Mu71+rKJ!@7PQSJ}8y}&nXd?5%c9ExU!cUm3E)yUDHxB$ve-j z^9e=~@rVeRXzU2iN5nZIESHX{F5u^JflNo3xZCKnP*oE<_u%ldQU2!(GNoTV7@xwY z9o4MnzrvT6#@{h9$4%AxX#ga58Ppfc^edMS#+O}<zo$U{D&YKZesu-!4>HO|#d1{| z^y{i_({^fbRbm&V7a2#W`<0=hBCfQAWY;GfX#HY8BP`WzVZ?W&?bJVZ@h$Dh@u_dN z-5eT!TMF=}cI7(yyXU%8Cqh!KPQ0!%?|-VVN9S5AiGaxw!1R$ub+}`ama#N^S&sGH z%E5qYLqd*w^d}H%u;r^PsrnQu1A#K%-#NKNwOeZ0{6Zz;aa6UaJF~AWG-@;hXe}V2 z3h%W^Z@HW2B?`E6M4o)dt=yg5xg6KfF??0WSFv3>$a@<=BJdXQYrC~VJ^b6)i;HWW zSGxMYzX;8IF76BqFN+hxT*dVy!)*pCbzCn%=Gn!W_@(J*krxb~L*>cxiF-?#Wt;3Q zvirFKZcWxOHHgn_9>(Z?`I|x+akSm1nuOt^p`n_&D!<BmbtSB<myZ7a+@Ytxw$4lU z%jwX$d2k6tr@`<1I5KfXbkg7IM#b$t@a}4S_hfk7w{F=&sw#$ER>4Qlush_|d0t~B z^{C19+=I1z{1wxJG5@NG?Mme^-$Yh-b{|Tu>E&DEW@z)~UrWP)4@7>$LDnFZp#?bY z*Ltz$P=b{9ee-IE_z%#7lZ%=69I?{QSr#=FZeP)MOfFGHffi+VM)iFr_dQGey2f+4 z*nDCGUzEJ+dv22D`K)JpcGku12`0cL;@D52?HS}vBhBd5$yhNUJi)B54+1|mbkDTX zMa%-}kr2g0H{(s-A#DE8WV1+<90sB5B5tQJ6Y;dOqrW2X`+3tAPX@apnSI&P{I3_T z8R{sZU#s)LAxahi8A`4Hw}|$sfR3(y<(4zq`X1XGM%N1lV*hqMPju+c`Q+nYzd-!4 zru1gEz;*f=sZ~*)fU7X!o_>fjr>8NJuPB!twT;-b;@@7fUHT>+aQBRhsbGn3{KbhH zk+Idf0NE<v@|1B0<b-cMzkR=*)z`_W$?f@Z+MSy>CG?+*)2#virZo#@zVi0T`%EYm zfvH+LaS>8x?zPw?{(-${V<>Ah?I`wU9b~WX3F+6Ij`?|LJCZ>nipBECJI)}I%O{uG zR|n~L2jA`O1C%IgW0W$Sk;Uukh!^pzni4u5bgy5gAH`-Lu_t<haJF!r_}$}{PRD~d z(`Que&^4-7kB3dH9-n|+xk0Y5W`cuvOx%`)g=&_c5);JSXvdfn%ERxIm0Ng3GQ{uT zhH3y-J>QoTFT%VYUHOlt0mhcg3mBVW^jxFm$G^oFbGPv7k=H%<O^EJCXBjKe5QWLv zg$9lpMexo8*w?4qIaX-VnSEG(UA{!$LpO#>*I2m$IBY<^`=e!Qg=WJEhL4>;*H4hM zY(okC6{;tW-VrT_7(uF6quGVle-k~P-~kt01b5z@48QE-i~XS*;lVtG{~AEu<n*qF zB?vkXyoz?|={4P5n#_~9O>TfwC9enHH90=TBk$G9M<ZhTm+m&=GQ5VChLB8+Z(D5| zi-{ib>nFw2?6*aleaK=jCZ|x#BM2A+-vv%UUQ?0WGVRBh6<Oy^rVAvsVUpW?6Gq5T zXWyOKa-=`Bb$ZTv)E6(O$1{8Dt+SJr9?NPJ)+Kg_FkbC`ky4oJC-T*`Aow2-zXFIW zA@zdh|AO3qWOk9C?rL39t0Nl6GrG4JaNf&MUBSuII6$><y3l*ZHM5(gt8o7l1_~ea zrClaLB@XWHdy<6X)70&@vm54D89FLek2ObfzDqnNgNiwgmR%Xz?{x2}E<7Vjm}OM| zcEtZf#J-3zg>Yf^>x~Z`*Vmhy%pCcYEbqO8RTk<8?OfJLuU+MuO#7mKsdrBBm1ctY zc7*B-coo^;Ja(2d4-({E|0YCk2cL0^<NPQFQ{;kfRTXjIo5~<>$LnRNo!a4UeqGNv zH3gv^bG#)1ZQTg|=@0qN9kt9gd1@|p!@M?`Q<vlFe+?#x?srf>a)z$vpYR5J(FJ&0 zEna_@HWOxZn3L96)UY>}3WFFBE$YqIJ~(D+hgxp9wANdf&VNNhW4U`wO!*P0lYBk) z0??$<nnz8}5$~Kr*5BNIAJntB!yq3(mF+5X&xvvyU>eZ`VsRrbdxjMfG;;G8gB;e# zGF7zHrbk=E$S}wo$oV%1C20@x8~ABk$yOQw$*$&KsH+pt1$Qu%nP?l!AD9iK<8kof z57ND2iFF%RSMt`XAkf&&5=m}rwqDrF$yw#}4?EQy*}y)4>r4_vXs3OAvDn|Wili9Y zV=D^TqJ%IzNdLm^L(awZw{zLuc(HcKxu~%QPA?%YZZj-Ke9t<GMmuNA#qd_DpnfMc z1aIUZk53T}#mKj}uSJ3f^Ty4AU4&zerru)E!}?Q{Y>UM3<hqEVnzMG0O4l`|`i?#u zs7%h=;$`f!h~P6k;}ICT;l`mtv&weJE@(Qr=GEnsC>e7Zwc^?HtBV+#3WwN9%RR#( zAhp{R@$8wl(Mm?58+Ylw!h8=l5)me)YDQ~HY3k0R|0E^4*8nT#Xpemg`X>MbUs8IL zuB;>-aeU)4A>&a@B*wT{Qxr;fiDFLk&#YL$>%;a_hD(uESl9j#=1w02Dxp9qQzyq+ zaI-zG*j-2Q0vLBT$cn?3Zf&>_ly{aqQgSZRsYjOlNR~3FZ*ibF`S9udw;{T|bE1-k z4%<!5i<0OV8C-zSw;tvL9Bl*&LwYW^&o)_G)i1mD1#;Fk9S5@GMmwu?HA{n2s8L3r z&Y_GfgN}qEGYzb%F|=bD2f-8i7g_}-rEV^k3vYH6xOSMacl;9t&YeAa3U%D$#wh#$ z;_7d2N1%W7_BaD*#a-D?Ts+R=F|s_wpyukj;ovFDt@7?Z+lRGO+Z@lDug9m<aZBR* z=wC6zYT~isXMMh>*46HpNTJqG^Itb4JO`h+(U`!{ksu+_$8I%E0qUuj?cA>M#me`T z=n%AzQY$aW-QYEtoxljJz?$`&$n%I3>oqg`j#)rXT?EyHiN^`0QNdy8g}7G$1#g=# zK5g!sabdnU-~7^~|Em(w|0U7q&i&ql=bh0DfA(-GsQ<6e=aaQ^Re3Z>2`>Ca<?NNJ zDE{M?W%@Y~=DdM<yS#zSc*mVOl>O$KfaOEr>hD%*r^#?95;lZGW(4`m?LMH4$mLsP zcLMv;+ua_q6qt;Nn3QIc_gZ-+Ar^tz>RfA>R&Hr)W$gCWI2$OWKq`atgb-PL$lC+~ zqs0V<S3qkm9A=qxjgeC#YDsN6A=%|qcdC4K*<Qk52IX&dhm&1DY9keoO>DA@{M-;q zbDJ@zdJGoce0Q2x^eJ_tOlQFI6CPG+(0-><XfX|q^Y0j{QQ~{|zXO#n#|C84snNV- z?Y~p#yL9F61H+2X+RmJfW-M2V@(ECIsnA$_kZ%_>GESu6a0kv`>uOw~79{K^UbOwW zsWQTi#PUqlb3HP;dUR_WzayZUWz2ADj}lxQAh)zMO;Ww|Xet}L`{S9&2R;tbvjyfB z0L@i_MP+yArfGe|!y>+nAMU%)jc^st*q31y-|wjo0yQF_xYR8Dmb=CwclQq>B;<pJ zxB%9N5gd)v?0#pej82d9va$xPAy3~LTS3rkPTlV>^V;vfU1fsnPB6Nv6}2h)s@XoL zeatp>@oRqkT1NEiHH#N2f7cHVbbz*{X0ExN{!={YM*MZxU#!czRHHp2=W-O3{X2te z1=<F{!o-sto@tV3X^PjAH4-<kKb%(00N`GV{RJk)o99=k`_n4+&tIZm>`p;)${g4W z>?%-+R#1Vs9F$8R>bT_-5A0}i){B--tkF}?wV!Q>wq4?aI)Ptzj1GbMai?%bG`8ic z!exM*F<g{74~GgxdN`iGgF5SaFsF0hm!ody!YGy#zm*?n8D~;zYsLZj+dzZ3?grAi zGTnK9k?!wG@W{zQ4!X0s2kfY<NbVhU>88fw>l&5nHzR8u2|S@~Z@t4`KHV^s-TmzO zxQ}kl6}qiyKVBAwmDxp;`PP%g-S^r%B;qsnx83Ix#0H!>(;iNF)n9*C=R12+W#any z{`PXs&#F2yhT4L80ECN|eP5e4w?Jv-U+vLhgT^F)NOx{v^Q=9<K362JWQFuv<Y(f# z{eJT`uhVFi-ug$GGH&l$znxbSN-BQYL*J`M8<)$gBrchm+EedXKq3sf7mBJ@SQkes z5i8hAo$P>vutx*6?;zgo8ba;8#BNWGd_zRS>u9a@(hL6!l7OE%3OLx|z~I))fAWO8 zS^`h+6-0K-qFsH*{6~{ZOWu<g6K8?jIqHBlalR}=+LV)xVwB_kvBhB8S(g@wTE~In zDs3uY3|2Z*=*!>VTz`bxN$Tq_Rqw5=oC;7^GMc%~$#AqnHG<<Y!o|X*?4^ddYP<EZ z%Qd-<G8WboPrQ~-%}MQ-+_VxEGs-%yn`?oUF0oy+94nb`q5H-3CxYt^{mucUURS99 zId4~VuhPAKHf*^5^<$MTH{y;y8onA@sL=oI-ZanPi-?0YM|7nP8)e&!yRE_11D1iL zUV){RJfz~|DAuc+6P9%e#?J%=cNQl#i%<u*D#yJ#62vxP47>%I3C3c73?n8`Cy)-I z6a4IN^8wO~pf^G>S6Z9pQ@gGTO9(sDsFV^TBULZqC0+#j-kect#l0dW_mIKDI8*18 z!8+<O&!1tt;V=8Qrn8=ENQd$>Nvz9lwhliYENpe&4pj0#XuAzimq(vb^&Y+Y=f#6^ zKp1zNyo?V#6vq8n1OEczum&2$55nnGzRLE`Y)G_X7x5|7NsXQm7TTTsX}IX+SfgSp zH8xiwO&QeXKsbuJ8{LlUStw7CO6E3+eX~Jpfs%z}SBe|HQVWKinR$6=KId!?RkaV& z-A?LG7n-9BQvc2>fxo8H{q^}8H=@09)JJaCx{F9)X~rlbbsVh`(^^Yl&xhn39MyDb zS+mz`bwb#uZOmN)^&}P9gFckCmkR-@**M?5GlyR0p!B=n;wNKYrs12|eo1k4*8V+k zvo+-dJumck2>`^!OF`zqn!4i85e_E=r``mHeb@L}gi|gh4xEk3X_>Q80e&wc>h)n| z-Ma$3^=$Mh^8G``6Qg#7UzFYgXJY#CuA^7t=Er`@?YF|~T&nwjJlkEmkVk;0xXj11 z6M0Awm-vtLNueENcxC3Un1OnJEVMLPWC}!r*SRkL`{9Sc!(VtW>_q%|*g^Ivn|<63 z<1~rRX2Pv`!K{{nv-nd}cl4V7C^?Y+R4mYIxTkP^h3$_y#N6c%sydr&ckZf9X(Zm{ zBD(NYc3pC$IPqWd2>Jb>Eb9ux;Rn^|uA-8HnzTNbd*Gt+)*^FQ)6alO$NkHs?+F4@ z(v3b%R{bNT@)#hbjlFZBD<~}MY8rMb2*_{HQ6JS&|NQ@Jvk&S{7M*p2z%()=v6s@W z(2@DYon~Ka&YiN1Nf$Vz+iM<xr883g`VKuTd%?GsR-d}A%wEmneuzQw?GS+>0l>XQ z$*<ng%+~p{NZ^^{>VTx{d04&P|B>{AOPq8XKMJhGh576?ez;C>yf&+sx9h!xG5tG= z|JMl9)Bn<)7Rzm@@BZTmkDUDc7b9!Hs1O_HONs)<{G)}veF8Hayx!b)>rlkMqHFg% zOC4u<20g5Vhi3yv!{aMoU9IC!DdXZ?e1*1%VLgZhjMEVCR)CjS<+S~LXrX|<!vVFu zk-NBZ`moyU=^w>(U0pThv2lD&oLdZ-_;JoB@=qB1eJ1W*1(X&OeTJ-dNFHZdZp^Ts ztbSipOzgOH2Byj7bZz|vY7chkmFdO6VG>)JYnb{c83lHK<?MqNKdkoOTmY>0Y@Mu4 z=A$OQL`eN;|Mc2qjgg$Ui1xQ6`@?#=qI>qY6dIpB6*?@1S^7)G_k1GdHDxYKr$zf@ zD`@T>pRB4Kdv)BE)g=4yYtL~3Nkxd){ZYQdr<Rre5;EzUW5CIbxAbg|b0!-e5y{Np zx4SE20{(5LDZF!s*AR05_c`TZ+6g(d$x9+f*`8|sl)_2SbAu+C8@iX?hZdgc0A^(c z?){z6{9bW-%x_`*%xFL#Ue$g2*&oiIVlO|v=d2(vtoR=<@4{?ZG#$zIGUd=)(qn$x zPi96v`mp`np}V?pb!NS3bZ=>*s@8QG-!u?6YhJ(1_TsSS({+Epc~GDs;Ln@yid{MK zh5f7ZMB;zz9Ps~P>$~Hre*gDNwv32l3mFkdvdXT65R!S!3d!DtbFwQ!gls~}-p4%l z&di?2p2y}`=lH$OdwlZ#J-&}e|KyMR@jCZ--q-WGuIKaSx0e~n%gxlE1kVTO*^N3< z8gl}8tWz<y`?F$a&?lxY-6N{lHvo3H%I20VVl}CUz*@xo8icw0GeH{famzEOX_oS5 zc~WDjg|5*xp_^pFD&*Fek7rfglvju8bWrj25QxsUu~+x&dvotOUjek(?#3SYgqVTb zSD1!gBDD@_;!LZUAZp7O%;NTSW4ufSI<RNGx75AcKdAl+(2vaX(Hht3J+Tf9-e)`c ztKA9kb_~pGTf@cQ_MeMD?kDg)F(2O({&xZEn@4RP2%IE*v0vq$8?Trve?LO!i*~QW zW{Ox1JSw-tC~sDu7X)QAo*7GfwRi9&t*o8e^`M3K*6sOnH{%U8T^HOo$1FQO@Lc^& zNY405N@N0VAL~BtWr9e;7(Gt#7Is>U37I0`4C;tz@HcYj-ClBjt{3yyN~RVA>yT&D znERh?@qieJ0*-VPI~5x1;@5it2|(-?ixqGNHK15!z9Vuru|aKNpH|Q=@DRy@u9?6s zqp1q{t1WMIt7YIeW8e7Q;O)JVu$b>vk#MyHu>n2Zk4-4x2yr9sD=+mVn9M5P)wQb} zH46zcd+0B~h=^+e-o`sKEqd_RDexub1dev2Mlr|5Ly>*s(oUI3EDm-g2T5Ruf}}n8 zy$I|D9~fA*m543rirbx^P0Xz5umxAITO7JOC%#)}6W?g@+EGZ)&}s`4^qf9B-kl@^ zA9K@E_e4YT-Dn4H={)!+c0}VYJFY7h2NxHHR`JreoII+~kYeS$X4_ovXH_Pw$GDBG zOzFNks9EKT;#(KDi69^YWm>krSiCk6FJ{Z3U{t*-li%%Pk3<y%4~HB(t`F;VXDWuT zO(^2nRes&P7~m})i`(yZ<UE=?2e`UoI${K5ElSlodGnHXL$mHX+@({Ps_OVOVzT~i zto-ElOny>m?J6NW@Jjicqu)NTB0g$N=YkF^;zQCtb(fQoM@t6q4$S6Rj&4D+r20tS z4AL@|(Qd3baDR13<#1~Tj6=Xf*26IdLH}$!(GVcy#=A=0WBfC}Kd#}czAtOgn?wtM zfmq|LL+2<ft(AyZKzw+s)?#0qVm2$qN06L}mAh73sZo7X?e$Ep7u?i-YQz}UG&YtJ z0%qoFyUI*8re9T*a(pmiu%xEqCIh9GeY0>pP^J=F>SKsTO=NMP_(oa*A?SDK4eBi( zeDM1)4^QUFTzktd{wVI)LjQJ;#)yW2VsjPCU|FgTom|7XQJs90p7|n!I=fTJ&a2&U z7Ky&R9tA8P?e8{e8TP23iEf=8Z?PhpJwyvA_ZgMa9928>Su**PLr7bd3{=)Vx#np1 z5hyH$$h=bU>@s?DNUP`m-c@-?H|g0ET$^!k8J{4RE#upmlSA6X^Ydl&`7FrQY{OGZ zqo4>7s#T_{T+`3GtE5|V^=CQ<)xRrPUHv=Q4cg!^>fz4f%7xf6PV=6ugge00l$*|i z@zr;4AofC}mD20^2mX;DD7u~*jgig{fg(de-Zdg*r`hq<`*URG$ak0N1ZL;Wt$`FU zIR^T7`bS}91$~7Ycir8t*nviponj2D?UHDId2`Zu_*4ViF8kfE(VA!DR}*gKANyUF z)@^IIdOb)#Cw$_GqLe?JHkn-PA<y+o{?>@e9V@vT7aMOJH`&<;k4g@KuiYaF#`vYc z?u<Tbl%?!^Ycz_4!0ot3!WrguenR569uBtL8?Myi;n~Kn*PS=?#_)sdR9j|bJ_{wr z{Jb_<x1IURi_M$9_r>4$K`J3NzzSTU<O{w4G)3d&B5yFU*^4D))MSX{E(GWeW&ZH0 zIbo#jEhj1fzcTAX-hp8$i@jS&HO-Yz4puo86s8i}18PXrWoR){%yWAGpT4ngRO<18 ztN>CcSgg?aHK$;Jwp`kX)wu7V$uH1uNDz8BUF~jfHTEsA3wUv+ND32fX*7y$J|%G^ z^D%_m)ox6eC=TZvt>af!uyoGzpt@oUB2u6y$^f;srh^rGGuW>E35$@}p|$2#+kX7w zyIX|r^v-n-@z5+0bcm#VET?OVk%Xb-sLo3)CE2T1kV%Y=h(KL&WptoD@C@61lbH2J zLa$Q+ag|_-hFAt!Kq&?o31|MciS+~E2<ZCUGODLOr1J9d1wrnI)*f@C7n=cIW-+9| zVhwb=WJ-)u&kXjw4zU?OxwF^1+XhGqY6E}g0Y4gmdt(^s-<5XZvQ*=^bd4^@J<1!F z2t}mW&8i+N1>a5K_xP>5{h;&|*%baNKA&`dN}ww1&2mqY(LnW9tqF&0Z0%E@S=X*` z*-q!)x*J%T465<`f#~LL7i(hXlwdZN*qE(YMcFK$lQ{3~{^xZGf*TX;mm4?d`JSBG z;n?`2zCHZYv^nOAb6=u#3=FedS+145lPVk7+`SxiOUgkG#^8x1#${Los&1=vtO>4= zPeax^)!NelTk)g|%_%d}xez4gJev?w4@q8svAyHZ%)fsH548tI+}0zp$v0~Xa_UkO zf6QTTh-^9b9QeQ^;ha0*qgWt0W8~UmK7jD?iWS+3*438i`85e{6Z#7_${pY!{0Xb% z;RSs1hWs*(`TP~JJ7URU9{PLUUI9Dy;rZAF(rMEW06hGqJ3PZX(6>w!v|N*|D6n{8 zGPPgp;e2hty8&?BDId4}mJ#UhIc^HB#5jH4nLcG}=i!@+-wSLwSRd)~4-YNL6`6ti z7aq1(v~lbVi!=KyE>;=9!&Tny<-yJ0R*(*XQ}U9$+x&ybJ|y&VLn;&<ixRzh^apg% zU)07=@LsH}4OuUw|77d>#?|n|t`3ns_h(|}zG;Hm9otyfg8)j-Y8Okyc1&+b&p%>c zHTxE=@X!z$7(wxwo&rqiy}N2@`1KzY{ss)-$ujh)^TMBtOv9E~&ZZ1VoRgk*c0DIt zbp)wDO#Ew`2=)okBztZiOWbDwqzA*d9SVZd2ud(DFa!Cu7Nz?e{Y^tVW6Qr;yg)lx zWS~_Wi&v=1@n-eC-N<}Zk!7-incgIRaLx6at6jU@XcX{p7-x7WY_fRZ?Cw)Z?H-wR z)!4%W@sd{q)>?+6|I9@z>4yX+Zw9Q(>SPDWR~8pqs~jBxmE5(KkYi)mo&fyYNI`ja zMOwGv(GtgY%yL!5YS4NHWF+5iW&R-0h5w+%MR{`sJw1Q8@`<cQk!?u5pfZH>nonC8 zOBJ3+C4@i<Qb4|)`FD!-!w7e5^5?tz|C5NDzOae>u};~uaV8Qlzq(T~rh!xgc|<is z3fF#D#~S(!MH9z(^fL}PT^rb}T&J*_suKO>?8JY-vpmlub#3gV(fT{IaTJ~)4ns(x zA8Zfg{`e;JF1BOJd;UPMTs5$SL|q!y2tBqzEl9G8=qOE2BKMA*0U}u$@Em1^*e#lY zSDR$IVErhtx32bNpM{{e^EHpVNkn7$Atq~Ns|MvZx2oMxku&Ezy)u5K+XCV<Uw62y z(*^0-uk3e4AH~p*QhF?AYi|-*leyNtx&Jq28kYwU3`(Mhnd?s;_1qed%8Y&Gq-P)! z-=L)HOYE1;EOu32aU&{$@QA?qh>!@2M%D`8&C)3flgF?;gcK^yG9_f!nbz2++6E7- zlNvT8tueb@Tu7oGqmC5V^60Z;cYFxrYfO@ORd`i;%MgybwAM6?H~OO95V3rDts@YC zRnc7A)2?l{CwPrwT*k>3%)jdP4k4LF6Xgu3?cy?EW2oI<b)x`|-_vTv7m}e{%RN!C zwb7e8;J-;q-*{X?+b#K>`P!c!GN^<{)isynHt)#f5t)}_X|`u(-_y8VNn~%&kOvm` z&r$V7pWGQyLT|P_eGYpLSJMM7N|vxhZ<C7dLG|Yg6N}Xf56j(lStw89Lp0*-C1t{G z=H+3<_clNw0wNCGxAiJ`6Mjy5V9EeuDxsu7<@7sLSfIGqUFruf$gXl+JEmR|06}&N zcTaZax@gRM(9YLx&ti|^b(;CXme)Nnc%~oc$8szwr|8n7fj65x7Kb`^0XTB}{c6-- zBIKYP4lE$$7b7l!1#YT$64y-1tmliRj^Yfz*JSK_Fan8Eg5Y}#>Psb<>nv@pph?8p z8h_kwD};+;3<yMPWP?E!F2qnOZ0DR4Eg!H<VEyW~k6oE2GaF$2uI4M49^jkPBP!Bl z$C%4Y>dWaI?CbXOy~TVUKi-4bBRcXV0vjOqb(|@^M;yg@e0HyeKnP&PD5TNM_Y~lH z?-I_NC~;{|RpB|&sa%Td62m4d{fz$zT!l12Bdi8+5&ayQOi;E^{pW<q<4#zre@EJ% z3{XyuhYIwf<db-UtyJ&awym}h!Su	z>M)pFTErKuA8vx)EWE)?xdaPRE(C6*q1A zw*gol<FPZSkp8#|12qH6VeJ=_(9_}B+kq1{jXCzgjmj7Xr?VRjGB!UZ9-E%~UfI>1 z;yG-~gBU^nbIwZ6aB|i{Vo9$PU4oc>4CsK%T{))^0F4@g60i!9*L`yJhj)OCeofRm zJwE3n6XUNydvlT+;Jv`u97U7Wo;#DyGRcQl<FSk$9~gVwCrrczw_nh@SQ}-A*wPmg zG?7k4jk|+PB%XvxTcoO-=+y~%&YU*lPu#!CAf>ncLadYt#tuOoLuKjzW}AQDh|HWY zQ3%IH&WGnRjd5N@%dDl-PK}MZH-t<JnMdyn^6R&Shq?xnK#b1{O?_`0RjKOa8AoyB zO*^3IZwU%Ixk0ow0;lG<R5F3|_X$qyuncE?>fP2kp~&WCSZ;INKQ^bH56qWk;Ld~W z#V*};KprVg5*(<lPy)<?3&nK_cNLA}kFw*ICMif4?y`OX-?Up%x7w`3Y>GP_%?3-} zW#+VzEHnt;*^TD^VpZg#Rqo@e?E=vDiQOM*`4@|Ll5cC;YCCrQHEHzw+iA<Vobv-h zWBj{`4YH=qxJn>i+#Q_mUkIs#VyGb^a%a+;qc52yYjP=aQkQ=n8v{@M2m*OR>N(+k zNBWTo6YPoj7=U)JSx$_ANJ<b%0tKq^%!}+1_D}=!(7*Q-@UAc^@UCz?`5)|aiR8=U z`bVQSZrt_bPbLg)x?9)a_s?7cFfIW=Ou%|=`>ofy&oH3KK1xjfDyaM1p=C_TaRSFd z3qZD3K6OKVEA_bKH?n-PcaD}h5)$YF*_Gtw9-xNvOty?&qOF)Bv~zU2B#g|~B<sA( z-88sUcJvYg8@o@Uy4yaEmh{}$Vq7<_$-MV|jfzg(cHFSB6u7fy2aa^U*M}Sy3}f<6 zR}huT6mv(@wO*MJcs>`tvG=Ou)YqI${SpgJO>cH>xZM<Q812A}_gxiZFU%)ia$nf- z@{BTP?J&}Txm6Y$KsJ%$y_d)O53Q=L0z}23T?^|!_qrUO`3+OQT$APj?>oc2ile)c zk4(Etgm)^U3k3K*V5d1IPXIgE<XXe&Cak_tf(>lL=zQ=R^wa<q9BOD&m?EIV@3uDD zpQrSwvI4O85bGY1z?PpTaY$x79G%LQ#hl}eS(ut*r6<mn`<Dsk@B{#9wdG|Lx7xRp z?GiNbV(NXf+k$dS<6+u5(>r>{4`wYex4MIGylr@YZJ^vXp>*DIv9K5fHcL2KyQ%VI zKj$OF%6rGWi3L72cXEW8tlagplfE3=<rxt|zIGq=CCYXE^K_LHn)qir9x4na>9Tl2 z4bStdjSxgAT<J86-fAa%tQ6amG!;vt<nNDCe`^fG9=jYrK+k-t<`=AfgpS=@-}p-b zI^Eb4iSpk2&G$0kZ;qoA;HmP22W>8Lq^ULdHyTg)1$;EcqTVNbu|u6MVhU3zeL*jj zm+e|&RAtris4{`=%6{;?-5`TQhnuL{&Zs(jdt49xVl<L>&4MpMKw5A+zHhWd(Va)| zWy^x_6aZdZKaFMIayCZH-y59llrj|%0uV9nzGQ_h(~U1+gPMwFL}%s@Bzn^69$01F zUSWC0r_FZYyBd?P5H?sAW7)d4<0olzugG~>FizCV?XhIn>nyx8R_PIp?bF+oP$0Az z{d1)}C&wL&rntO6k@phGb0OcGX2bRz{}R7D<Hdd>mO8(i;_#>AW$eNV3W<?q1RLpO z==pF64Vr@$hx;Vf@Zi*MGxK@bWYZR)ErFtZhe3bM8&U(v>b!VYOKkrT1vh-D0hL}M zi0zPk-Y++0IaZ~ZU!Z$;b_C#K<~QCrCF0qu$y5zl+tgJp+ri^mYK;*u0&n+&NatGo z6q%s`h|qjE^H71w{y^30U5oYv`#MN1Vf*yo01fD62mGdw37`IR)PpZI>r8xWz}Fj$ zDrFp5L!)k!L#3oy#hLPv2S0~Gf$JsSk=eSDw?_&ia!(sQW80T1-#Uw&#_vauEt~v$ z-E{Sz&%XqWE=Bq027ZU1O?#Q<72y9AyHQeBalGRqYiCAU^EhYeXq3inv~O?n9o=m` z-W2Z>{cZ+k#b+queLyn9;aaPqV)OXEklH%9{fyuq?8ukjJ!eYOJQ$~BAp59*S$kbk zqJ44JgsA~$y2a_5yV-KAJhNb|$fyRA++zn!5+S!pvoe*F<<&oPh>~=At7))^elPG( z=;Og%AHVxwm|T=dn0;@479&yP2KeO}P^bVo*4PK=<zm;OtbO;QRs(SBemHJj;_3$e zA1D<emv{+Zhq%Dea+SNeb?;j57t2*dDia(xn`#_gA{W2;0gi|33nM;#c!6Tq@VoR# zm&UvsZ1yF>^o6Yyf3lt9o>};Ivb0x*iYD5_@jr5oQmQGABmVlZA?)Hl1;RJJ9*t)= zx~rj89o<w|a(~V&r)d{Zo!|Fel)iXh;kg2grb}UjWcz<8>Oo^X?qR=ofoku%A9LB! zghTWJs)Z8<$dPsg_NPycekICW_|2C{t}_E`PvEXJ%6{Po0_vNdXTrkVgIxQUo;%vr zg+)>`(*t?ZCBEUbHKX5r@$3`n|39uhwa3abxtq2o(B;u6=+96;TJTk0f8qp|IKJRb z0YeghK_%itbXgU~Lqqd*UUHXze(o%WZ+mmRObdYb(M8f07iS})J`E5v&>C!)%P)#M zfS7TOj+8&Z5Jp?D7$?Vc-A{svx7dPhG5Co?CmxsI+uxnpEZB$AMB0NxkIPV38tBNY z-Brj)Yc307xh~=jC14kk?G_el{kc3eR7mjo@|bE!s@~rW<YXUtsLj#+0iNuu3u-oO zuX4zT9>siP81;8YZyH_qU}49yUmWi_ZoGr`LX!*Qk&zrduzPbfyEg8ikdG>0f*-U@ zneqqkJ6UXPJYXLK`yUoMiEk@V{HU*1AuzV<M@DQ@URWVHUmSsyN)$A85h)P^G_>`E zsGd|0(JD7K``LQA!Mm+j#&8tggN8R#&-J?%i%+8fLr4_`vjlIoD<v>)#J3izi{sAX zOy3@D0n!)8Rk~r?Xm=CHwh@I1H}N3%zDh!ljP*gG_{Pn6{jvNW50&;WMhVh*+&Pkw z+F<@O^59cE?(y*zfz6PznB~(QZ{4TlehO6$q$Z=)dGxA_LqoMhF0#|65Te)|Fr<le z`pV?D%Mur;Lf~}%AEN@IWAzQQ*_-U*AHI;(XxJMu<PMfOR6HB$%v@8=LoTWehf@5{ z%mFNk@BbxT-i&xE*P;(`Nl5<8szOp`cE2;QQSjNoZtZ=oReAnXG>}ZLzS;{<g09^f z$TxzFW!ad-iB$fgU4kHfkr)5ZXbRjWS8(B{LH-5kPxwg{yC#%2bokr#B|1Q_+26;* z6+(dN?I=onXKnroH0LTZN7nRHM2#Fv`74k7=!6nz6FFTAF)1D%$j%YPn#UU61gvsy z^O&P3D`9&qC9UuwQ$d~1$0}DXU-y(VfNM<q<+`MW9k+hmbxo0(9Z+GSEK^`bzidk` zeAsn9;4}xmeKiXHa|CbrzJEt!UZiZdLKR+}Au{?!m9pIQUDdko`~-VwZ0=C20HcA3 zy`cMyXEl%q)UyNc#sTSjql)9?;>TsqzHbfQo#&e)bzivE;@bg_T!#<(yvz{OtMfy* zm(^)kndg4g){-~)ZXK38;ojZ@yoFXR-+!dAT|n9whXv*S`+wZlN%bR^(<9ee{%-s} zDBEX8R7;D?CUrXK6jh~NlNQgU++;jiW{s*4CVGx+A)Pj=@`G!C=eB-z^4<&wsS~L_ z*#eBlZrk};0dKPU4K`mo)RGpWm|!~UDLqG1FuAM8_uiui)83~^-Wo&k#S1#t6rx0b zI|wyC04bRchoO$z#h|T&7h$5s@2WZe$b_sZIHcT|IM6WTn4=&%WI(C`RNiA{aN~#& z#VX|?7v&EsW|2tc_i!IV2rzC|&5K!0d{<*c!)H8oWfEjK19YHZ;Cu3xw36}ckHaD6 zF&XucI2V2Kzs`o7Ejw^$lrvj}Mg227z-(kQT&8*5Z%bo^8LA!S964N7>!chQfSU>7 z3xcWL{h#MNWNKO>FLNTW$GP0am)`sYATzF%qRM6S_QBdvlzCTdR{&3nn0Mw0YNG6S zagyN}Q0CPRWN}7uh6Ll@T|BOZQh4^uPgybns;C&+M5qsx96c-3H1G`Q1mp%K%@I4G z^XA<tCXed*v-#mX&7?}gu(gRcR9ubI&>Q_?7pvA6^y*n*EaqF&wZ)IZxBfp(M&(7r zzc(ZAO<eR_eO(B?5dFGH<X(}NkE@xqklmIyF02|BibGoOC}ou4A0Z;vgE3NSZIMWS zqG#)#l-ciUtU=|ke@FwF`&nd2ChR<&rFP_djbIy4&2qogeha^$PI~|S7jBtDUcngW z9vPK!xc%~@7rLLZV-=3+kjqkX>ph2&W!_y7FJ-8hFX*)1`0fdx?#Az4nb?bUAmD4; zjnW%=r$8Y{hBj;`Fn!^x?-dTou2N^mr4bYBIB|io<I!c0!S=mOpu(izxUIduYgs4X zU~wGKMVt*79?fBF02yiiLu^|^_laA1jj)vyNi}K$-bM0|P(@|SaJs-twLq^@xG2wX z!tv8u3ZPz5RgQVA{7rd8$hqR+Llw)N!wd5J8{x}5rG#_&B(D(fnM{hvu5+I~Hp^h2 z!#*e@hno?vm%lZt?CV_HhQEGy2|$#P;cwC&u)Qv?3?Og9liyZHV-$jt-0k+~3VU5c zg5WMWL8=rSGvp(mlxeQ{Kyn$UYRp4$@7I&Wou6#Mwo8(fzN#OWc2aYU;R8=FzU^A! zFL1~#bYf`a*SW(}X>SSRp)pCiZf^8;Cs5d-{v9tXB@)nJv~Q!QY+u>~br6zA%2u;w z)*~HF^Q~E#>~m;mK<M?0?m^u39f3m%9nxI3rj6Fj>3jK%4kR~sNT8zk;4cF^?<PN3 zu$WK#_ckB;1Y96%Zq1`6f8rIu>8rl(7I{Qt8MTX%;vropos?S<wQe40c{!5&oshaP zpJG_pj;kP^)d3*(#{Av;9NRhrIl&HvOWi5p-rDU*8lx@5o*TT=1*m=8MRLwxwIDfP zVk&TN<Q2X_1^$~p5Kv_G%&>FrYdO{-=GUGF-kw;@+f;?-<AFOLh9=9N6Ez%C>phz; zP-nZvx3;9S%3<FarLHjX)a6+kkiwPY?gyWHQ&b~o%tou#uXm_H#&D?7tB`;@x+a<Y zHeOXV0(>6lg{EncvJe!_TlA)Y0HoBx6L>i@ChL4Bmu!N~q(XtQ{FXf1NFD@Sjo$bS z%w^}K=iYC81OAYgMc&7x$E#T>8NhkBTeMGq{K-)4<aDlC_gCqoV^vhDoM)TX`$>*V z|442Ta-z5`70)8%c5zEdUAE7(^=`3~GWHA18&>Ngel>li3gbru)s#)Nv~Db>`OtJ` zN??z==25`r{3(C`w%coCZ_jVPj-mNvGXNnlppTvlY)22yoX;LExiU<rybNdHJ?fBV zE*JJVj@C_bu;TvYO6FT+pq`_XXZXwz*$uBLi|X#xMz%Pf|E06o4Iw4x-<7)0c-1|E z3VL;%;GtsJU|*J#Ve-RyGgP8i`uF&ZlqXf;LG_hKfa7ss7qh#vT&gY`MJW@{7!7}C zA1p##9iGFBs%G~OkDYzk>wJ21esuc0-oKxY8G39u0SJ42DG%nB_RRh@QDoBqci_<K z>!N}Saj1#pYhS#WDsg_*$ALww$Gp9dxuFaJKC!GLu2uQjsrf$=xvS0%H8EFAKqvQo z-E~n^9!z33ymiAU2F9P~>7?Q32P1U~^u4%DYW>$;!?WtGrFIp*>DH<CJ;ip@sREE4 zP+bV9{#<Y;UO~P{>jMJ2QNA03+e;9c>68o)U|$`QLFLd)Vbvu)5=K3PFb9+FW<=Gh z*Ul2h^9LHztIafqEWp7`^Oizsfez0TWxc;nfI(JUwdcOx-#GH`nRs-Ue;bESR61u# z32zoD)s$5s!x6hqMtKu$T(!r;6T+Afu+e|i1gZ0Xz_gqw&hEy@D&iLR=kj@T<uZ*W zM!VXI-(j9AQG-dn`GHZ^WEE|7ImS<)z%%U|6JNDm0-@-;`jFV%va7mD266E#DL%t- zPJkJ>Dy>`R8i?Pa`*;p#oX3HJp<<5n6KmH4d&j~Pw3X3f_3MsEUce=PsEe)w#kK&i zZgid6z9L=b+x}Nxp&q)rF=HZFyTAdw85j)uP)Qs;Vtm)&?cvJo+R?_|s-1RehzD%D zv2;F6x;3+zz}3V91Kl+AoXz}T-0{2*4k@wtBqfa9dHx;Lc&Du>`1B+}Ul=ge3vR^r z4nIZA%fAG~H}d3vjaaFSI4Dg{$ylxW2LXsi%avw}iOJDe-6g4s34h_pB-@)!K7eO7 z()j(#OA>G+5ygti@Ef44dcwX^|Go>c@ktqAUl>b2tvtyG>=Ssi*X)32n|BX#^`YgH z&Lb=XRj$^$n={z$wo)ls;7z&TniC($YK~*j6YbY~w2^Am_L`O{)3pn0Ir%O_JYx$S z&&MV19^6SrI4$xq$j*M?9RnFhotOq8l2BPTo`~?+4zsP773N(rT>)9iZe}>v4WR28 z`ET0UZJ+&{e-1?earBKx->CV8IQj|yM)O(^NyGgKhpr%r;K&ztH-_Vravt*ExieDm zd+asgE0YW&ye&bie&JE^;k6%D&^8K`D{PW!$~d<&)VBYH&+*9JRq%P$@}^{FS-wk< zP@*=HB?qry^Dg88vb)4v5F>cg7qkpAexw@@$pNc-EsZU6lo$r;&Il&dPh>jtxNr1d z3c&mkN%ZljcgyGZ+PU@8W&dYIV!`=r!W2LG{LhDc!<SL@635el@~b=1l`?qzSLy<O zm+}r#CZws5-PV{~tJfiybVq%O5j8~CIxG+fuT;EbA%7os>SF!zW-usI6hN`I5XTt| zWku}IxGD0Hu+MXI93US+4}8@zkSWM~9%U60<lWUaq5Ki#S8ok2Zc^!H9kQhXUP+G~ z9169nB#lNGAs`~JXddmcP!h(6)F=F{jC2quGyD@&G>`zAq&vF&0(~(d!s{1Js3&{u z_<1Be3xEE~n-wf&VLXz|TvwL>B3<YJ)}P1&>s$8=7v`nGwlFP51KH*(V+G&l19+0Z zMCa)6gt`10(p4@<mNuFm443_k3xZ>xs!RizWqCb17!PB7PN&e{q{yzju5*v98rLcV z(gqN6KX(^A@kMtPAR=$s*Jyu}T&85p25T_Q4P-%u)ntIUN-Z`<{eP$;-0znpSwn|F z9zctKn~k`CeboP#!SxNx<=l)snh#~lO>o+BE6+4Jh%oqvI=^b(+>qa`>gQfRz)F<m zuJwFfQzwySbbQd^=k6|b2&ZH7*ecrJ>n@V-f>~K&9e=d3-qjF9IDF|cseSuN>ST2O zGwO!Vt<gY0;5ZVbZ83T4`R%r0sYGX6$haMgR}}Nl)BVO4`0IAK_n=0MoH<#IKM@NL zCs<<Upnhhve(TwJdU#|PxVD+gtZW}J7wbLHs8hQ4qRynk-ol4}s|JlAn-2$1TF@B# zNPbaS>y4Nv>ex1WI5o^ng+4#2_umB|uTXC#PKz!l{lIkMFQ9^}_5eysBEqwL;>(}a za_njnvHX+WJ{JLQ7FxA02mfK=fL=5@5ZjO^XC6<!92jRT<<*ou_K^fgn<uT>NOG*= z%cuCQ_l*ZH_ge#{?ix{OWbH{d|8vz1)V6BMorgfGV#KEXVcQ<+87^Wy{439n6+IeI zDnMvgxk|V?Sy{qgVmsjmS8Mth&5tX?0d%;WQq37dL!`pnzaa}SDO7~^)#ka@tX|gu z$nm#8@{@kkz6EKoSfov$Jt|;a{Cu3*Gs9(dKpz)f^DpPt1inX@cZK&yc+ZyK#sl-| z0oAj&O%M+NW#l``<@08X!oUwrPF=LSGjPw*n7Epqb(FD;F8!m8^d`Y)_)z2J3K|wZ zk`UuKk+DFOI^tM|^4}c%2aa0dkK|sz$S(Z2gx8R5Q1`9k_4=|tz1Uc}<Mw)C;qlW! z^2I5CB7a-v8{3;>wJPx=NC8IdD!h(vBd^+h%h=On(qpXDs$^DmzW{LM-+Q8qY3x2b zVTzMlJZR51@1k>RavgaRveca}BKuCP@JU4pVw7JQ2S#Reo>deBB}SeFh}-$m2%8>v z$ESMT-v3A)-X*0%M>v~P4Z0!NYPFKI05Mf{u%6Vu33Hs&t$G4qphq9!%l=fwvzM7K zdQ29`bg~CNKa;e|Xd|Zhc68L1;$U_fWsey8UnvY<BoKx_W^`0tj0t?9ayJoc(-p)C za^_}T;C|BzYLJSv=np2DAHQXZiOl`+>!cnTSu&Le7TZw*884AN`P4VFLhK{h!Z9Z^ z;^yaG^Jc7^L^C2!f)2nWU+SdKl;RbD7_qQX(&I9h$pWUgnJiWQo@1(SXt;s@;(aa7 zto2!k?z4AS?2kT`BUAzO7GO5KmK)!qGUpxa)@CR@&c&5C{^+rw*l6!zGfj{Zkl<xP zF(>&5K=62!J6wxAn;ods%4(-rrE(LupT5nehS5W$h_fkM3C7B#z5{Q+$OP-4SnoQ# z1B{qAvniLwb@b{EPnfs@FoqLFot=`b{3g7UeJOv*4_uq;VFZWq$%kXh!)nXR2`M@Z zoc~5qoJ^`irKI!kMTYx(gzRM+BOgbqGQviiCo7_obuIO+&k}RjJIzZ1e~_7jSPft0 zSP?znp9+-ru-*ntD}%J>aIH3qr#1K^+Et{~6W71AEJ6^sZMB00bnd`k_czvR*!4$p zF(kY*g6A7)d)Vee?${__2VPd}iM$5#;6dE)&_{3dPkrd38jW3bBWDt``}Is^tYnv^ zH*Lv+({7~v(}w0_>>+&&k?f3jo#4M_@l+0+d_w&3PoXW2;AkYl_p`k*_-$9odq@c1 zRMzejPiAH;d4?g+1|h@e3Z{aW=U5+LmAj1^tmiXL_K06mdst5^-TPze1ZeWEJz%K+ z{Y!Nqmh;r!8BPT68c0OFw>C#Ails#|wO3iB6(M0Na=t)<Wa3P*qCV<Av>lt9c}eJb z6z<cM6{BAMLs0q-;B3W7*988I3h)nK-+ic5?tVOH`{4Uxrq0oWaW&f!B*@YBHJ-O+ zro~gr$A;%DeX+Ty{8_O6o~MwZGj7y-ZIPV+j%o<}p*xV;_CKQryrVIyrL7s}9hi-v zB^2QAhVNQz32WXc&wR+o2B473)pRwT=$!g>;L%;mX_3~@>oS)&%%p5qJoD4Z=x%>e zm%-$2OLnvV&z$ll0XSDq=`ZE~pqm@MAGuBO@;WPgx7R+@#>7%v_TPV{KQX|5HTYv* z)=-sQYuCNIG@(uH^0cm3?i{__m|@Gs(S*!0|D|j-aHPe4eIevRvcOOBbuHUpTpq+6 z*|_RwE7vtJ$M0k0IY{%hvd$tziBHzN2sQ%+4k=v3zQ2*b<;EZO69C-T>2MsJtJEmd zpRfHf1k|an<&Be@;`2MJMW~?)LR!jQ6D1M|yLINX?T>YaH=g+kK5LAwBTloacsq(} z$N_Ky(6{;0hp>zLKn}NveOH?7F5W=+_^OG4dSsS6H%3rDQmq6P&u<bVEa*>G7ya{Y zB(~?cR=gRc5F5=m8_72^Y+#yi<Gt{`a%MOlSg*~$_9BzrE>|C*4~b?He~9={AMxf& z|4517>ve@|rT6_`6@-qzAOI6g(+fTR!+rv+Z5Xgdev&sg{=+g?=`j%(ac{(@JnF!Q zoR?W!fCd?wDVR0xj0(G$3pE?^Py}-b*#K7O3cnGd{{JW(s<@Tet0>g{vodmc?3Yb8 z4w_bTT#Cd%k+v&2aycJuPe`XUY7Piq48}Ui>#Il>!s<50^YHAQ5w8SV-n0K%1b`Ig z)UX^aG{feW_now1|D!a#<1}rwW)23A8W;m*Atfb3Ha?QFp*1*dVzN?;k+mOR^KV1Z zzkEOM|0N!H0cqx1x$3r%-u+0`T+x}k&fjdG-M2Zre^wZ+7@zqvB&gO4{iL>uQD5xe zn-W*YbeWo00&ntL8j^xU@`6wFCKV4*RU~-S*xEc=QM<S*Nd87kSe!3d70zJa=tdDA ztX<Pk=hHwoZfNwAG%s1tsRHuIbQ!*NLgUioN=nyYTrYpxPDw^|{Dvx&ePoBIjG$Uu z54EKDDNw_dX+fXqwIp%%k`dy=Vk;Y$4NqvK!x(~dxsTC(Pn4&b#(@(i`Pl<I8@XSg z;7(Ub_Gu(#ipf7D7|rHx5Wq$g7L;^*Em)l1-MZ*B?D(h6G%QpZxrc2!RoCCFo7L8F zI`LPz{VTLbh+NOW#pgS>hTw0(k@}vN#*iVXC*fUd(e{D3_|sp7!rSY^IXu@rqACAH zS-52Zc4hsmiIhGAsqGyhYE=&bmRiM8_n2h>wBH`3rI0F7k0$@VNav0iXDh6FC|fvi z|J#SEGJBsww+Cz}Nr-j-u2E2zGmRU0TuSlP7E6J-?Dxm59S5|YYtN!!0@kiU^AxxD z3&`<E=yYcAxh%5|^o<ymN=hUqC{v?kpR&3SKUxlzjP<}ONQH){zZzlYpj$d<I}L|E z9$TjD0Zqe>k3l}=9%vGGFv%8ZlWxujF>$Zy)Sk8nHmNo_HRf+km*QoI_-XZ)WuAVn zLV~C_%she`lc$v>80uePbpC^%kLkKwo0t?<({-GcV49fg%Ee6{s8p{OdPh?Sq#D^& zS#*9Q*V9fgkH)wJ?V~Gk_R=-=Qlpuzp!j>DAdBSz!_!d&dv>$vSHAFlJs+X+IQUX) zpbnZUNFOm|b%i)Vs5jafMf9@dN&0S@8I|J0*Sbt%Gor|rCaA)p8MJR`&_7@Xc$3mE zJ5TUn47$Djy6x7S)nvE01sxB}om8>rfsE9--#VEg(gPESrmr>jI9E=hsW~TdCxv3S z_jtaB<EPa3FDs8P+c@M`SaS=o+(LpTt^D=Yl9@7=py!V0jl!QRh`h4u{Zy}pti+L| z=cf&{NivpXqy$S9=Ra50ncp%GGxm@)g|4tF`~LqbCokSp(}MstuXgxT1L%!n!k-P< zgj%YiYCH#aPjU~A++xCR`AF2d9sk@l=*ejf*CWw2;m!EYD&q2FQ(CElPh@lLVBAkl z!{><V-3uab$$e@1sGenv-Qy1pNB&alTH_BNNL(si7|jgzlQ|g*@@C#FoNAwhlR*?y z{0^^eSAlllpUid^`|MKo(6|c{@q|l8eRB?7Ht~=S1VL722ZBQ+2sGWh<_==33LbfT zoHu;Z@1=D2r|3<(&SMOdn=2ua#dx02V%oZY@9)=l-`p3Tf%zY2oEpMOB4Ql(q;$hS zwEf_%z&=C&*rqNMGvsWzx7wM`+NC91Vd4KSuBy4vc%h<7lxFePwjc3-&PM}{ko*<I zy#x)&q<4v0h&4V}p7+}^(-T`y9l_l)HJjfN#@wRU+$o35F!{6zj_Cz<o14)J$@g_* z(GI3=yi{rn{&ob~2B&t{%~;+r=qz`BwFzxb#nw~M;`2Wi;mp1JzHPS&``D+@=HQdn zV9@tt*VJB3)(IZid+z?@roHeHwjQEs*rdYcJCD!#9euX3WJ0q%DlU5tm!a4IXX35g zonCe*mag%vsb>tiCrJDI_x-a0f)S}O?US?HN+qYhCFEr3dJYrKJMicld5d@>)5pHz z3y;GuUUv1wUq{!}U*v`KH?gw5f4I#3%XsobzMUKM`zRq5Y5diuvhg5i%WjhIQ_$L& zf|56uOk(p=Gd*SggMAdPjS4S#4EoCr$>fEEh$mdiSIABnPYR_yw%=h}I1`r}-^Zuj zEA!<_94Bbu^cpdaSjB!Lx}5&}+Yo9wjn$=lC~x-Y;ms#m8)Hzps54LOY<$B3wS+P3 z7Tt}=D$9U(gDOQ&rV>2P9Sc3q$-&%EYzBgu*RytePrM@G`N49*boIXJ=zbRqNJWVy zW1BD-U;62hjRw;sQMybpd&cNluXd=*)){5DVY->EywRJNwh&LCitP2B|MVx>#&>zZ zq!xZhzZGA6AA{gT)%#wtayIXRlL*hrADarIcBe9`GE&zqmhVTqedDn~mXeE;jHIeK zn)_#@s{6{V+n8PE{Nfuj-Y*mX%XiHsA*h|3mueI;I{x^Lo;H<+pw8g&G`(qX`K%#> zipC5$Kovx(s)acqEx}NQ6+IF`uBNL>@{q&hf)4W&P2L03uyaDTrTMl9Sw6oRTXT*a zzBUE+Q~F4=CHo{G1%_25k%jTOVSfV**gdm$Q@|hmNknQx$Q|YDxW>wLe`XTwx;`9b zRPELgbilOG6=%{8NKpRkVl@_bXq9}<h<scQUMp;dT$O&mIjVn4x1O|t(ojP^!iu{A z_00Mn>Q@OW%<6FeBIr;;M$icD(6Q_^#m!~9jt==&Nj0S!4;Jj%CEaR*mwS7WC1_@c z2?aPA*XDCH-#L98lp}fdYbGu61UV}`-sOpt7cF_z&Hhz>+1Cz|wBy!3@n3UkBD7*D zh6d1%`v;a0%t*VWUe=|vB|HeM-A@P$%;T|PGnKo%tnsAK=@$E4a%<M(^(zIOes!1G zl40yL#<0yVNDR>k!R9ZwPZh5lQ;<(c^V7HA%wZJeM}0=TQ-2F>)t#O7J9Dn?WofVL zIQ9P^%QVKE@F58yH<2USn|lUi%{@1DiDcF(vyrE33mtsBc23J*6CSqmxbfPgxW1OH zvC+!<c~^t!)yh<_d8%q>CeKfDg6ZqbK^Gx7X@uOqY5ZD(T1?VwI&LDGO@|=nI6|72 zkGjQ*_twg4OgYHc2b;&^(rVPsYn3HBGqW4;rH2tMWmUZNfmbfm=2cdey&jwxyeiB4 zzC!yQo<qC^s&e{7OpyKdn$1D@5ZicVITgiSs$i8I70%bg(t3rMOA#0@STbx@^s`&{ zBha-1S@$g=)|zvCwEHw{DzKq9W!t$d$Ok)|r>lE%Ao&EKA=q(}$lezLNIB5Sj?8?z zX3I4*XPRoiRl_Lkyw5rKi6eRr2{K!$!Zu+LVIOPt%0j#hfwH5-qjjig(4>QL8K9DQ z?sbc&OORF{Tk3@*=vqmf(f1pOvYiXTiJN*Tw-{y@u3F@i@Ph{DBX4aJ(*L*MwU7AG zTxP=Uthd1~;+H8b;a{m<QYUt_pnjuNQ-=&R*tmzyYM0WXs=59i5h;@vda>~kSYncH z0Xdf{DbL}pw2|@f&dPFsSL<0$O`y_|K%n8iOa2RTMHb@Igu{1KOTkt`Wkdl#j6lTJ z?nCP-0gjK<+0FJe6H!Eu8uvPO%3k^>UaETe1S@s!fvHRg{SBV<i2>kMc;UpOqsswg zEj|>p!>Gi*0`DcA&y;iv8qzfRTb2~(^tzf6X0y7bE)8^S(?zrNk1cr|ud08;5p zi57jItanTb+lx`#1^StE0YaT2)D;Q)O)lO(8_XtnIwj#Eb?SPM59!FUeCn|Kj&a*e z!cLGNi2fAtDZOf>q1i`#AF(xMo}3>XHfqwOojnE_f6p&2>_L`XKF+^0LOjr@r>Cib z2dnfGZ}IyNy2j%&rLkB~_9E_A5;|^41pS_>uk0uQ!Chv=y)4&bzTp41oZ+UW2$*;- z^}3gP<?BzP0|hNRaT52#*?=jvncv)7@k~<WkIKdHEUiDKT`SvTqlhqf40uR`pY+hP zfahnSdBCwySgb+^((KV*qOY~gQF^OjceHvLRY6m{^4Rw^rzFQk&3hB!C7i%UtigKk z_@UX*nsiW|jLn}l=RqlaTry2Z$NuWq(&q!oLzmdu@;PK-YePAlv4&1ROYiNro0eI# zO!VBKlC(80x7y>5lW_dD5v-Bz<FxGF$AW=)pwB1lYP-9)`e0}N&JYn(v5cLn6Z^XS zg}n561wot1C&orYAw|-=?VPg>#GO^7jhs}I>zH)f_{R1)B6FcS7Zg#T|2)$Y=Tu7X zGbA~Ce^<C1(8<(%zEus!M4D&+sQ95&4mjrk2I96>rD|)Eq^k?h`N!xS;kcXMbzNks znap04@n}r%<&0A%UXgf(FQoh?&r7yD;r!vS%vI(-6GBSHo9n{`vA673)O|^t4`fex zfgT{?2nSQRYVz*e!5r1q&+_yV^QBt2{&*)P`3krWMDt_8QZmSubn|Vbe6d;6V^5$n zCWR;L%}XMif%Why?kZ?JTe)!0$W^bt9a|xO^(W}{*2(*YJGVtwS{nI&(|oo!?^d^- z;P4O@x^nbhFTSKml^+=@#RQ&V)Yj4sl%Wdn{j&9zb=bEs`cB=)@vVuh*i)<J&8cdf zn_c}N{o^?0Yn_cF$x{1eR;*5ezLwCLy5!<3%(0ELe#PseU#=;4`h8pVBj;GTChpCr z*td&-k|q1^ef0RsHk5XYvGs}-pr~ZH_p%mi$+7_*!TkFU`%+*HuS=tbE%$PEjzOOY zzPL&uO@55P)MjT2+}RjBeqBOGX1Y7F?SaEd7iQq@_NzZf`hg>`2vjGKh4b@T)L}i& zrUE0XD9%Fg&8j!M3*N2QJv=?!+&3<9Gs>Pq`VH>?NV{R9xz{y{eQ(y3k!%;e-9H$v zZQbxjf5auz`1~tPig)L&O>D9KjLh+i)~m;6{SU-Tr=`6*BLK!t+J{7c>g~ZoRJd%+ z7UEo{CC#tR<n!`5i~Hp4qsc2(Tlv1tF;|JUA58XDdSwSC73Pe<jeL|~E3?5OJg%1_ zEP{uQZ@tWUiV-0*UrQUP$|WYPh&Mwfk~U2Gi@%aD_=Qgh*d_>B+;b-&nLxj^6M<h) zoTB_{Co)r?%_V$97)Qk$l)vHn6fV(dM6CC{?#1F0oU~@Xskmh<y%_piJ&LY7q3fCf z+hnio`R&p5#VNEbL00v`zr6sy0PYi>Vf}uLl1;|d4~pn@{^cb1{OY$U*tU{c2<WaZ z)tIH2<2|5-N-WStNU(+Pe8U%W+BIgGvU(!BSwvF&FkGJ7y{lXN31FRmwK?f&C+r6% z21+5I+<VRYAY*T3U@P!Pcc%f?6{qQYmg~pk&%*jrERo+s_tx9k;GINl(3?Onr>7ex z+C0W0Z!)ffK$RB#b&74Wn~i5$7-Bgp7N@fvpz$S<)o5WFQ$650H&1{vlzZ-3c%CMq zpLpML`@1TUWfHUBk(JWnF_ZI#@nNsD;?sM_H_=+Tve;#qLbv6U@T-mWfN!2FZ-3ki z(+yt;0oQ%BWZON;6Q8-6v>&4I9@=|0U+GuL#_S|SI|(>vX>zm=zY-=m(^yIgnL8qs z5?(DWSDzGCji@F2zn$9qN*8M2NYX$`YCXd}vqXe?{o#iORq3sJYv_rxV0#hkd3AEK zXuXcYr`Eh{S^V^e6OUI#HzRYyZq%t4usWTrW>vq(WKhfIG@sdpf6O})hJNVTSpw^& zis_Wkjf-;}y;weyZ>tp7l-+Ny7(9AODJ5UPkMP;&l*pKU_~r4Wqrl5kpm)~7Le$eQ zxVCF4?sF12mZN;_#d|>Z-~(#0DT1glEjuI>8D%p-u`7ejhLjKQa0;B`j*DAzT6+E$ zRy5GRr-O*iCzsAlTAoYirYhw(=_>bF&HG!8j?mS`55<gr{?S@9<L<TnN}jE3I+3F? z)V&fY5eDHS5Am$G8sW~`@Ra+(c_CcKx=Tl6h^dEw#;*J@NC}3SW0v+dAF!@^Z^fBU z`l<3R$A15ozu$5S&GU-J1M)kzX~w~Y)l1p@Q)ib(M!RTCqBFZ<)7~xfS)661yv?o- z+rEw2u4pU84iucH($M}!h=Pt-Q?8;@%$AdQjH+F99=T3pwzuxPlNx@sAHZ1tkl}Pk zjkm-B9#oZN=#oCUb=>nix|Uxe(lCX04zexIq(6o%G}EZRmm%tdt~n#`VwejRtbY17 zx6M>KTQc17=KXj-*5$nII5;{(;(Ys_!{TBrX|mb!`L@4cVLcxH;8yiVwsZ5?)wL-x z+o|Q2!Jk@-r<KmNgp{3{Rp2d#`)zXS!}fXwIUXj;U6f(UL*Cdgi$^oABSxPctoQD0 zH#>fmS*|xBaP5-)F=t=+$-mv7QXVO=I(e;7wn1K()BY?TbQ4m)<`8(B`d9+20l3cs z0(^_9B(&K^d!^=Gtol-Y`3ZL#8kOq17XaV)ZA~t2CyzpLh?EF&mR*9qB~K>ds558H zo>x=smRX_9C7={@49}N=0#grv@Rhj9%vDH7yZihyCFd<&;H(cBVyT*U$rO!|e7HWs z#cnLz6LCrQDPvDw{nOa6-QpR$C(l)e+kg(aw02Sel!sQupTg^^bmP~q^ukN(qCONn zrS_h8AIk*A{$vAMrH+5li|&~Hdilz$(LU#NFogrOVeB}Bgb)#ZzP4`PIf(u!MpzdD zX3U%!9Cp@z8Mz!{MLWg1o+E{4xfZLPwU*5%+13}SMwdHeJ5k={RW)ukyfGGYQ+Fj} zZ6*Dr60F1Oz4EyftcNS!q=7ktJ@mH<v{!{mD&qz)2-j!6UK?uSovgK+&~rA}ZV4&3 z_c0ep<s7S+BUhxw`gUKlq39={J8!tPeyiSROsEOj4)g|F4=eOn_(Y|LkhR+gXb#u3 zcYJ*<K2s&OBcr3h>|@@~)o-?g(9x%+vu~V-zv?Mck{e^bI#=_mHaTdJ!pc;)>r{%5 zH6(J>jm`~xhgi|5d!idULRnAV%t+IGS$UTO`b631><LsAf2-XMO*y8Q#j2g|-;LX8 ziPeO4{+5y1n<%s9IiemvAG%!SF&t+p;R~W{pj^THx+6akAAD9}l6+Ve-<~GpeED0% zXI6>xuWb`$9?wqOen<qg5LLv!Ob!`pKM*xEk<DU1LOL_%M1fY<#%xd^kZTl9P(8Tf zBX6|rW4ZjR=6G+)zh>u|gV}l09+*Bc1ZPBSg8`*}i=W`rVc9u9cC4G;77D||2^)y^ z91Dr@rvXY{ke;87nR1>59d#5t3@zWI#5@I??4T6uMUiaX*$ehQrl*fNT(M@@I|c*> z%;94|TSSCjc#1n+-R^D|&i1q^g`H>$e~jk$_8oxe{@j|*^Ky_m_|oMr(40TxeLU|u zna^2=8OWDqaz&tG7f;qlvI=PU9NGMv{I2hh8GhfZOUZx6`rX837W(ltC7Nu*e;`OS z_B5r8s7|;|Rzp_HF+~z6#-6NY$}#AdWrA(r+fM#itYR1|{E02)O02QB=J08qk04UP zB8pqR9}K$R0eEy5lF9F6>DRdYEKd`l=N);`5_UdZ^m0+i=&K(>O2*9ZG;+8)Y3*C| zW2Zee@yeJTRcYwi(UZk+=n-o9K33)}A~QebzSE(;<?2@#HnS-|c2OFVON1$P@gIk3 z#_8wetIN?2Ea+c^J5y_kvi8aAPqr1iW^Z0qUy57?3;$|mARt#%xGnGl{|;%*H4Z{2 zu6NqNu|k73xliZAWvf!O&HP5wEA(_zlQ~hYjTGX?Ww(U)67)68S`-wVx>xv(?h(IC zQBO%bo>AhT3h>(AY5OVR_@$axNZw~NF!r0%7+xI$&1yAW6L;Eoww?ZSckx$2CQe`l zigdexn(KMH0KEpXJAH=c`%P;-ZRo(xV$vFq(Z=A%krNd_%Ls8r7@lT_Ae%3!?dpr{ zt-?m&=@q0yK619*U9US>yHN)Iamis_&nRi6D1G7v24~C+m%%o>UN&(^%;yg+tfH3v z&_<g6aMdQ7v&R?LB5&OnZ!1lTW6_$a&W1kIWEV-sr|KP3pf}qbZjZp|Y!ysp#$pZ9 z)KjK4u{7$^0zglX4~*+Mp4P6kI^H>n6Rp!5I#*sM_**8IMN?}QJt2QZ?Q}QuG|9Km zB{KhQ{aVmYxa_PY?Q;nlbm!(mnpD0y17z{wZu~XHsUl_3vto(Sm!e|jSM-7Y3Q&1W z4S(1hA)avEEairqGp_8Sag^hfsJsXhUtL`d#q8Ks$GY?OJIjn$Ir5!}4U`XRDHLOd z5jotYjx9A#v%d*g9|u!1t>L%#9%MzI9<DY07#Hgc{=Ia<e@p7sIJkWJ6!|bN6a9U+ z9EK@-$!woX3|*zool`y_;`d<JL$YHV6dQxj2jBIbI^+^(bS_FlnYDFOSR`{;p$rps zsF)(4iJ}(#8JRPUH+!&2JUN5ZD9P&41G+jxGP;L)i(Ehp)QqBWv?66<Qxh9j{$^0q zPQ4VdUqh^k4{a`T2n+etPjXe&G}H4dB+2<u-){P-RPYjUxpIhOz`aENENJ$FSmr{p zD+M#N$1%c`<5$;6n)3Bn8bI-1B_AvhKVmINPK|zIEjw|%+^W-os-CBo7d6lphn8<8 zZSBlc8J~}sWTAhQ+nPsVoz`|p)Pt5^c*xhc60EmNyHCyPtUT0fdSj9(>-NZNb(lo2 zbpLmG%6{k**>f#Ww0`W?(S&P&g4$*Xbpa-0eI{wV;%U0^(g^f<_)F^0{x_j;SP0ZA z+*0N?VC@y6dgBUvMs#=Y$X-*kuTy?us@xWGG=}`d8GM*-UH!2Y)@BQ@Fv*?kdLpIx zBByAnN}%V19(z9ID;Jc)lzB-=%2kb#DCNq|sqEZWcab-P8m{3ljeL%nJ%ApWVJo7# zCR{8BBR<yqpn_hhtUbn@>d00YddfYg&r_~9*MGO~A?3Omc@I&wP9N6Ded=dN11P$o zQ%4da%yz!=oScueb$0Z5&raQ5k0guxwkyS?NT?pmRD8oB1x2f`Pk=wDeGF9CU?^m? zp&+dk?4_i;Psk1$+BGP#=G^sTuU`6pES-fz6W;g!X-0QQ3lh@ZFaZH+P^23v>25|h zBbDwF1L^LT22mOYBcw+U*v60d=llB~?mg$+=bq>F49a6wP5=Fq+IB54PB7t}6q^R< zF2E}&14<0eyzuZ2a~YsGb+|BSbNbD3ktB2GF+lTq_ra9k%9oSFi?o#EKQ!>S&G`FM zeDhL|rj2737tMf+nx>#o83Nm8I)fkm-4};DO@Cb8bcRie&36aaQ=mV>_aD-B%#?2j z52qIY@7A|3?XULEkMFa=IR2y*%bI`PlpT;eU)Z!eXs}bWcfxR2_6iS3`ZNOnd+m&5 zwaF-I?4d6u>E}GuY4DH-!XSg}uf;Z7ga@BI!*5$<rAEvpmY%W#>R@&H7Eg_=#4|!L z8wE`&c3JfrItrIFGak=scDJh+NOBF~rK0M+rRiW)nE335A^5&m6Rrz7r12+}^EkC8 z7gD{g4LY}b+}H9vVzCIs$ZvD?n^muZ=BSNLWu2z}$9yG4@IEC$2<%L?Zk{))M^6Ny zN-;t#wg}SsOv8Bfn8~^C3%Hy^E0?o(+ddl@vW7E|d)Kt`B41}#GScL;tPaWWF=-Fz zr51xd2#~_?(H{Y0;?=1vZqwjjmkQ5<p(!2n-Q7<<<x<(7rf8LBj&WFpnzmGNI9<mu zYm3Z*yTFy9brGLC(qmZoeR+|84y5z0UE=rX%w6kOT2k3IV<(3Lmncu{6I{8)t}m>O zIu}YZAB@ZEy_P0|bEeN`al%lE0)mAEukcz3+MWP8>kRG*wKzd)AirZAsz1+iI3Km# zA3L1uBAb4MwFoQ*bv@*?VE1DHorWEr8NhI?j^13_Pp#eH)P@DuDQrS`nTE=GYm?oW z_gG@8a!1baW3D5BSSvq(u1PEYUC@(Zs0m)rRsW&f!g*{D-`a^td=zC1=nqwPXE7bL z=dPqRh;O6EcX6b60ktcw!_Cd@rFbEUWF>Sz*7jpxwe0l!A<KxAmFC?|*<1lCD(@8g zTC2Q$$OVJE{h4?=$0}DOP)lX#mfH$Bi3aEFO~tMVwUcUa;6fYD{OXJ!sx+$;HLy(! zyeCW_Ipyi6)pdQNb{QIX#PF^8-EG0uO*v|4eCvMH&p+&8dBpuj4}{RJi240g=->dP z?_|%@pa`zcwQwMv9Z1KvT?n(6L1|rGQU>>N6oUHCmkwi(J9b7GZaBrmpa}w5EgeS& z8sVNVPadE%yKskX_me1~fyIh9&z=d-H9;=?9H>|9tRo|~<V|_@EdJ3()E6*+H@Z4{ zw+)X;GDbkp!1YY2nus|a+0=?2E<6)gw_5?4sg0iN-_xAE$cBs7j(KF1uwri$&;;ZO zK%x;4dlU~v3izi=K%<X9(Ws1uUU`Z#1~}87pYBbOi{NY!;>TB?-m7z4dU?RK2bB=2 zrz7a0j6k*Bd+%Ce!^`{0Id)AvPL*wO=#zK?k0dDe_P~SA#5_`UFe*J6Mr3g7SZw^^ zQXUyaG)8IBM;CLRslR4|NC=E#WQEN@yNr+P+#4S;-6juK$t&P8WJaS2&IhG&@3C&o zrA95uMmK%gTpnq9;W0Nl`#~$NE-6?{0jC2}@kgX5&zVBz5)oF)9lOh+7`wW7+h6EB z5y235koQ!(3k^O>lAdTlWlAYdSDjbIMpyetrJMiMM8#kh2isya5;Xn_Z5067>rj3o ze3wZiwRJv#SA8Epv1xqopbLf)%`4xo^-nmWZaf0mF7f>j*3{e#=nED^t~t+81c5NG zZ{Us$av+{)+{v^1>yvL3S#k+HVFG{ixebQ4s&3pt@N%l(acvj3Y+ozLpLPg-moj~f zpxLB^PT+@h-uKk%4}=BwM9up3$8VkPfd!-ypR#()?PBYN(SSrzMzE>x#-|Qe;=g#3 z`jXz?E<w+pOiz{dVH(LKjfK*``Y{odg<0@#)iccU3tF>(57g%b9HxCJOtYeNW8=sH z7*AObY@Q@bwdNiFUPCEcx#_-vXNfr~@_jx@g_U(W{oW&Q(_T#QPGwp*->e%I=ZMO= z0zI6#+-Vy6jWQa9<s!GFjR%$REzlC&ryOQ-Gwm%tdsb6@j~?^np0}*9kz)uWJ1;v; z$oaMY|DhbgONghif#10bf^sTn{=kC`aBB2}0JPe8-M4sH<=zf^+BPIG_JkNx^vC_e z^>MV~;hV@fteSUJC^Iep<+}TY<<qL&Q}X9%ae(llbKnfHm%+j-Iu!gwirV5Z|IaKE zpXa-uDm<8U;?ZN>kb3Dgw>>O;o#~Wbh@H`9%7kbrNUfXM4Vk%aGx&Q~=-l0Y)z}1$ z<P%K1PFfKQntc0k6`>EQ^ZJ@@vHxVd|NZdaz>xQvEhMk!7}N4<o9W3&6Z~wCe=}5q zvjb%2HOGYvbd``YJ5y^*wVtjs>}?wyVhrwvZiF2u(>$&I;qhbDSDaLzw)uWP8g(cR z<@ntTAD2V3nwPs;Ypnb^{Z9NJeyx}ZHAxfP7k^KxUrst_px9>s@2#2H&V52K+i6mp z`_$jCX<y*3hSpBA$$sQ$tVbEMK6GAaouq=6w9i)IpBYO4Oz~kZD#AW472q}5hwhey zvjap8z3Wijh%M=I0L8fd2!Fu8NgobN4G4HG^?jxP=TC9Bm7Krw27Tt9%j|9f9Y1^W z4r+gFZ`(M1u`Jp~$Fs~horgw>TL$`Od^gy5Dtc#u;U%%SwoT}B2e3hUsa2vC0mMfF zfv*aSYp({aZFs8-d@?#!x4jug8LuHp3?X{5T84VVqabh(5N*_{d|HP_z?hY^qdtXF z-t;_j?EN>xUrzv&{2S#br0;_K>3(<cwb$i4-d7&eP~ZiE%<Y<bTGhqL4pH7AN-D=v zCMZ0B+g{QDX|n;U^ZN85887W!)}uPqWl(ToGMkYnI>cYssz~84Wc$v=@}Ug4hymhl zXXj8$97)I}<Nj0Mp7!BM%^XQZ#@6q%ofg9p!$^wWE7P*oBE;iKVPF@#@~3_G2vghE z2DV5axd?;L)UVgjB5Wv<-kU5dkm_UMH@S8O@15n&XbQCG@~%YkHgIE5=N~6=HQ;kO z*1vz=bQ3QpxQ!91d-+f_W@*0`lItJ%{wEhp`QiFx^2zH0Nixsvp#%zG*NHec`L!@g z$^?uyg<<iv{nK&6KxesJSbq!+HYjZol=8>m==o${%T}TPMh&XUHv$mqJGcDZwG|w! z;Mhs?Iu$15sAd^@a(%c6B7}E_><k_{M;(v^6l~l>a5omAZ(kfLhFxhKLu^f;HJrs5 zE|jjpPD-7{BE7RSrG_i8iP7S;JYHwe;wF-!$JM6n@G)7zrRh<>tFQ1-BoO(FP&q## zS41qXTPi>tMi@HSxbF4spXsB=^wYh6dslKc$^_!o)?|HE8FB?Rb?eq@5#GnudALc) zopXTDIC^WV*l6=r_cA6Tir{}J>i^oxo7XHA?4c-Dq8rY5hl_m0P&lX;v=Pj<jJLKa z&eN$Gc~3djVWQMZKrZ9Hv-Zt0<eSjrH?49=zbGQn9(gj~O``#&m7=B@*~{-MsPhf6 z4j)1abv$<Dl>>rS{%T(ZTLNIdT=D{_t++>1BhAebUqS^N%ac3<%}Q_-gz?CRK?}S; z1xB8Q*&`GLU~I{8SJ_Voj}0<KjmqhTOgNEPy&}2&Do0o>?9P^7wTJ8tom1{UX(^=g zrwR9uj$0z>F_aZie1G`98h9^NEn;Bq`{&EIedfc08DQ{4G8xSyj$9=7ex-kB3?1-U zqvwdg*@CUkRC)4yE>6K(uMD&mxpST+PO3llpIF@)uAn|S<F5b~l@GVvT0`1R`qhsB z)-xQ0H~wxnT@-{&cG~me+HVS)%=mIS+33j1FHWO*3C(_;{F-Nc5<Vy!x0$-&psQ(R zvGWD!mFPpG4MffXb;lC|Aw!wpIsjy~y<45Vug%Li*lWg%&ZW`m*ETABm#-z(DCd$N zXEclxIy^IW9`3)jg6NT%zqnxK^XT=9N|#j9BhQDDuR<!4%PdsmVEVd$MlA2f4YOoF zHPs+TV~THqi_9=7M9sqEn`KD1rSllQdq>SA|3!6p7iBFm=&$(dmH0{k^lgcTOjWEw z<hI;B*#Rbc&6jE6pCI(IWJBFo0sF%xVTB2?<fiKr83TPZ^8T8<R)<y8VEK!9D@x|z z<MWM!`B_)5$CI;1e(I|`J)39}T;_mRgxa*k?j}28yVuJzAzxcNX!|=}1%BOijI4iw zlty<4id%j}7pRol{v9`Z&PyHRNVf0x7<zl~+$v@nxb@`SI|xJ^1UxoH<MYZFEhs{x zGkeMN3hggcs9>yI2ztIHGHJnIeU3{8ly)PUXau1$O|2W(iyco%>aho}186hf2?>#r z6ckiJUD^#e=|0D)vt<sm5^t$#Dk@7;WABGvM)ph$l7e0NaF)fJy(M{DPUnRx&Me+L z`ITMAa75)2SiuXiQ8Dv<0k0=H-PVFs%bXC)w@_w+OM<Lt_N`jqvRM19*ggcG57PwQ zI_t$MnG#lIWA}NI(?Z558i_~H;ZTzOq@@u?%Td=+aRs-05T!QI1>$+m>LYYp@pDP0 z3Ab7iRAuV2Q(HGsJj@!UNycy7*ZISdNZ~E`rxg}es$aV?#zu#MFU5xWlIOp4(en8R zt`lkVD>?0+@+@zo&bj~#_jO-(s|UN(2^nu(zDFocFDb!1M~Z*+F7tc<DWO+-8aAnG z0@^JTUR`b0>?xEy&6+daPgN!=1WDW*@#+KfH9LQ08AB04JZ>m#rwLav{LGn6aFcM& z_hx#9bdv?k2tY^1f*Zp3_fad7ITje5zO*i`d0{K0VoaO5e=z3WQJ|VmwWl1uWvCgp znZfeM==>oB-3VS;Dq-rujh&>W4G8BP@xTh|>sJoxKJA!LeNixUdc|DkUVHahEC}}v zefC9TTUV`-nF?#=dRV6`^dHxC#BF}__22G<gxm-M7!93Jji3;2-IoowRaeEnq!NPX zBm=@SkTha+cujTjbNBJ;AS$tSb&d{s76r0%E*Ut7JV+8t*(rU(@av=7R|@+9T6fY! z*yz$LChZwBxYp@st_x!Z1oK5gR3y;uRDQSV_e8_{HtiFv@8dedDPOCCAUe#ACo}IB zfV@K$_FI&k%@zZscdjpj5M$Fa%IkuaH^1*lvS1Dq%(8U}<0_b!GAFhLn3+<04CYvf z&dtZtH`%b%18%IYp=P$8N5SS84|66vF(ZUY8mLTLk&Gs2AOwRSYj$yy_AJ6LVnweC zveCF~uR!B4rb&h&gvHSGToy0zl>|&kvge?6zwq&gql~yS2M5k?=Fw8AmB|FUm10>l zfg%V?^vp*fT}&X$_2p)g--hnA0ATmuew(8hcWKvG`+~WLl4xhskKZF_&=(~~@HP_s zTkjW|58u@SSIW%H)Z1l?^f<=jH-C`-4P%yt;u`^)oeZ%Kns^TWkyNk|K?(N?mjuU< zfz^-J<<BlL`6Zz%GR<V+ENO^7vAed^^jJiR)_9C}Pqe4g#~s5f;6TefE#Of-m&<bF zag6Ps>cC4o%DXMq{3w5{IeuIeKkix1fi1L#=<yoP&(nrU-LkyAP&K7g^D=72DRYoS z?!`zL))zyIP%*KTi}OJzfOrzAi}!%wcx{NoH#u6RbB7H`suykCtN^_^1%=39YS-Gx zEagdmLbv{ma<{8kRf8Ue+nth8GMf&{qt_6$TQNjI9t+L<D#RvEfHMWmc0?I-F_1r& zAaAn^@5L}`N3o$Zk`9GobN>8zIS@gGvuj;VSkSJsB~UBd(EVG_V^N8kGpyr#I?XQi zY!va50Nk9vMV@_r>d=)Rn|`Hh=DP-7%>alP=njGw*F&$%EX6y+T@gwkfJg3QH#qQ{ z_U&=&%!ZJJ$c(x7I#EPi*}I`VS`P9O)2uK>Z{Rl&@g^Lo*)&KyC=2tgbovU~<;;?P z{&Rfwq<3cY@UqW46b>|_$^ZGi*)RudUi#a3)l+zyRZY`4Gr}<J1V&X4IKBo;Uwpwb z?d2>S+OzWtM7Gxv1~<9P7kX?bD9gPqj4y0`Y=gZs#xGHV>8#<WMr|?k{ie88>kH20 z2M@kKz{rP3gP23^TpOFQx(<&xHPT%7o33hJiJ+esU6i57R5$EuBl;+XVw}4w5ihjH z_-v}<3&Uy5eBBbP(Na`I>7Q%*NzZZBT`Hz1+)a4vH2L36YVp@$hK(=kqzZz2>n37s z`(p51yd2R4dJ)bQa<G;qA?O!&3R2ZzTimWUqJO+Z`^_@K3d38O^Cu$1{XaLwTUA)M zShwUbc$)R}&3Q7c0Pe8ax7sPgW@#hlRA&Qgk`HV*XYARmI#j7(4I<P0BAU-lUS1hD zar&*fb&^WXXB&?pG?QKxU<-E*5|%Eb+dkwi@-mx92(D_%6VS+;IP#J<(zLddtI+wi zoOL|7Eoa?dhNpD9Fbt3#OK*#ZE_O!Zsmz{NP_G}ymoF#U&saK3`0Rc%#81H)!8^va z84RT1la5cVS|iC?93-H=5r4M<I5Da#Z<jE9%2@F{VoazyA01sX4i7zOYqwRRd*SG_ zaW|5So-ExXE#G<p{tb4GToVTkZUID06s}VSYUVAAz{9*I^dv`K<@?2t?;NicIGN`V z%9ajA?HX4&u8opP9zBUoGFEGIn(d}57k;h*%-Kw%mHrTtkBbLoXEi%AUkV0ShA*43 zyX=#ZhzEfXdFq=rGdKE!=x4<MCR*SugB`-^d%4z}xGh=AEUhaeLks#AeM!<~-FrjH zrQJ|p5E_QX9vW6xEQvpH6!Oj%3>J#j3ODQPe5h@-eLmARN;`pp?yB<dRERH+tb@fR z>a@*>YHJcB#+HD_`Wf`^Z*mO%QFv5gYK9(v&p-_WcDChrTJlUCBdVY3`Q+!TXSiWG zs$2rsulrW_v2d+VO~r6o35Q>x1v2nPtmQRasxnUROh+|s$5%jn|GYPxJKwXsn*QYy z4#If&yz&Kuc~YpIfP#qp1_FWpOU*%I@JEtiVe<~mz3DJsS*@WH5hZbX5#iM_mYt%q z$FWlQ&S4=%ng7n*`{X(%w@x2~Wzd8Tp6X-%${2Rj7cw`;kxm_i&Iw2)y_!FL&v?zW zOeuLrpaHL5n-FO2g2y&1Hq!j2%z~OYt$B5P$Y3KeiyejgZ3LB(jaJG)(3x)U>D?2# zkaildcW!!b@S-wWn6Dmp+X2i;UPn^_CAG7XCOHuIz^OC9lM3zL9e(>|<v73cS*CKC zVeZZBxpTDch>VlOisN8TV?T4`OIT5HMJcZ%$w1={{Vp!sf*!p$>li(EEszsg!s()3 z#HS!KR@{9#ttWrKL^OYFe;li7C1Q=dUQB9HiXy(`@r(MG8kU4cA=^h`jr*9w)1@cc zpfF2@f`vIv8ZT(jEz@I9=UC3_`T#*@R~gVvSfCqk9`uS5$&A{un8SDiop}q-tn6_~ zS{2xs^}S{aP=N&8%H66wxV*RzyX}-9>Swy7G+7ppQau`=PiH1J+e9Euha`|DzJZ1d zoETS8Rh&?o5P)?HS>oP{>UK?2K=RgLAExK8vqyDr5QS51X6(gQjz%FfeIv0gLo(v+ zs9x`STWgf|^9bC3aY3beggsj%rOb!E*J6w8_b=}wuoq{!uQYp+2Z-Y9jqYDnrOH!X zTC~;tVW_2<_hpHH%fqn|S4GGSpf)n~4itA{uPk3681NLpt1esjk^niMJ@b3%<_Woz z!j>WOd7yFL;MeDXrw4?}@)Fm>#uPCArf-c7NeF^F!ba`0R--(VQQE;~p6ml37%(jE zuO3(#<w@kPbRE$i^-LNZNu&HPzTI+)2m7I5;0XVL98ihrc-jnbSB?!5BlJrr91o@H zzbW^W=m(;i=@d;#C;KeuMnV4Fu(sncPtbUf4reWcV{=Hym;;ds7F#Ng-sE|2`OiMM z8T-g?zEViZGeNuEBlLe~iR@+B4tx|-%MN8U8c+}wn_E7!nBzX$HY7VrhsmC>LmtA9 zWp8#rED*LVF{8&lsQQ~;3i|?Iyfzot(RmNuvBzfteP6or=T|pl#<{Kyji-bHnMuro z?XlnJn|t)MN=t1&7(AZHZbf*+n{0ilE6U<g=b(l3Vj`niYcHoublJ|1niC__s(T6= z1wl(rbtAJDSA$2rE!8eHUq%bi2X#}Idb{uFVP(F4fB<Ze^AsQbm&dh`%c=f+UCxoE zA0PZ2d$i3%bx;dFPY~j6BC%-^c@*Tc>Fal?oK5&XuW*@FUPaJ5Vi<05%U{3ylr1=V zlw=C2qJYsktb5lB^@UG-hz+k)gOa5XV)02*FIva<gyT~TP8%^G(=5%Mlax2qSJFSA z5fX*77Bb(R+dl}EvI=eo=$%qmGX%<O+=qH%bqS&~`2XzOFAkP(QaVKaKJVsTf)o<V z_7zySF0ZhRFivueuDHR<#?dvl)PPd@bgy_(%(FHS1VP377ZOzHKTitET+h#{)dyL; z&Agfs7ZZ3RN|k%g{AulHJrBN8U>8CAX^dw%Cu^eP!JCc7ytT`antTJvy?<=wAa6pZ z&eS%fPq2`Qub?OIS8w{sBehwK!2zQ|I}FG3)6P~=D__dQuo-R1ygWa^ncu2dE>QB( zlqQ<!r{~d0SJlxkJ#T6@2IbZS?7_%qFTVehtH9==!*PvNnI|N#Ba?RtVF$0boh)&T zxn4>XNU5?14;ha6j56AooK&fU0y_dbwA%Rfg}ObnG6`CkvIWP95oevZ8>M#-kSLPt zX$qp{=B-e(AW?+g0V6zW#kb>MF!voX_kNw<d4LW2&+}7kL3`QRu&>Ur({dsVEE#}l z&-F=?Vp395g)L%9qni4ul?*8)(SWeh0o$Z7VQpF*ot58tL!M<3cjA9vCj3Hm&M?z^ zM~eky7H$-YPG*GDEX@k5IuBAEJgWGUqzOR(8h0GyZ)x;j#|b*LfG^lyx4(h%?!GN$ zm9)#CM&swS-ujpf46FH4^ZhP}mHkVY#3$0M6ymiHL>tdK`ff1mA@-i<eNiMgdrsbD z@Ku-YPoiX-%!;(pdI$5h76su_Mv-RJ#el6`5u!&5>0G;rAh!$-YUOPjJ!-3Shk_9s z)jER=2=O~pfb=Z&AaUJbALk_w^ijE^ZK^7B%=@HylRM&H(foy3>1f;C$>ixz8pMwG z9hG4$+CaeYsUSw!agCyb6p4DssPo_>2YK|==kS!aCiWT0SiY3Mo%GS2NZ0p#+i!3Q zy@Sk5l2wqubsoiY<>>2?dPA?`BWB0)yyqbI3T)))(x+i~vizUQaIOc@L1(EafAVvr zMDD2)>EL8wYDC9Tj~sZ|(&v1*dz5{r4w^N#8FEz~;Xk6ozu>L6{5sA*`t(Pa0<x7_ z&blSKYcBRdAomwKb@fFF=fCaXvT9TI5A4JPCRhSGvgb#$<o>xYF(Y5VbK;mSNL$Bf z9QE`3?xUDh3CJ0&-OwBmAQjHex^sM;fh7!HtO4qhhOL<q#nUgD!trgfsWkqn;SwXE z1S1UoimnrKlnYc8W{c{X%jdb({FFtYm(L|&FX<C;M?^xbr}O7(0UyX-&kHb-^B>Qi zM)kfz1LaLVN2=sy@E_@^hDdAs3!z31)cPaoE>Mb_yEf`nFB*0}JZtj~bF}EtF2?p3 zChAwo4U{9dPIQnfjK?H#%$;{P7ZBw%{~5<_*3BYfiM=882N%rWiRgTe=Ci!X!A^M3 zmpA9qC3I6EJ5i<*cm(ACwsjc|9K6E3v3!<vLM#XG6C@f?)6-Oo;aSPOE%aYez}V1I zT+N!8Ap~xcmM}7hy@vCwRg=hwu3$3d-N2mqS7;HQc@aqt{mfxIzdAmLML)E=<n7Di zhfOM71+El($(LSiOLmVUFfFGMmz|r)9v2>Q3YA=t;8T7%Wpt~AlQ(u)itw`aJ){qL zOVr3byQ_S;_p*yd(YxvP;Nfl$d_Lm@Z1Oh_o;#&cYtvT5>H@@OI>l97pY=Qw6hU+n zPB(%<1IzIpI~Rvws%@LZ$a<;}V`CgzQ;_3C&VGsOo|Lw5#=hx`FS`rq-sIj`O`l~h zlBeGNVE;9G`RaQdr9?6A{ltnqDiav^RGAI-x-tcQ=X61EeesrD|0Tz<JWc`ou2cfx zmVQlv6K2Rofrx{)v3o0K6doJCTEgJ2UDq?!E9aH)vpVB%@l<wt?^A0Ax9A1b-%^Wl zn?00by*D$*GI1f_C`<XwSsWxRPMvUZFbHICG;@!iN)9Y@5?=S7k+Vy@aup&eddgaU zO~B-^`^kW~UqJS^mc)7%K3d5a(CZ}T!^}>7gO3G>q!)HsJ@+&{zjQ)W9HLr>-rY9P z(!zQ8DrS!rAVZ4SCp2zEQ;dpc{9>=7BBxWTa8;i+X>O>sAhdp}jw6cU!~MT3ktRhU z5Sh+Q!5^gJNpci&Hy3W5$mt$}rK&~-OaiGF9#5^o)p#++#r6Di^kV}H#Pyph7?&2` zjndT%fLYtIw;~)LgWh>L`+|6i$&p4nZe)z+1T3qS8$f5tdlfFEh&h#Arls6>Br3?j zVE0s0i?6P;gT#2#B61%tJiRBN8sJf*K6|3md~(_Hl=(f$jg~|1Z{r)=aoYd}Itze< zr)t>VyN2Cv6JCg_(8PuDu)}#=iyl9(;IRPeWIwY1^&s@)@(23XjcJFDB5!ckt9OwE z{O9nXP2%86<Qi6f$J>2z1vV!fyrsMghDCQ>nHD^+RYb&;B9&|W{?O~k-@PF<PYHyT zz-rt6jf&^4_f8_1JMwWVwv}4TE_6{CGQD~So@PO~BPD)}**&U^G^11gMpALjN#_<c zunYnIMM$?E2J?tr!TB#Ch~5avyis3LRi{|Ml*R&fm#+#vkwpNBsCxaMf<z%UEPHu6 z^N;jYlhSL*tzubG@fNA)=`L4#<YwClV0DO8&}2>Ak|w2<mO@R@c}5xa%khFFjEXU3 z%4{jYtbL{JfqH<QQf46djwP|wpW4JsGM0nU`l*w0sjYnP-Z;|tcPwH<x^ygSVzzKd z1pAvL7qh(M*?p@FBaw%MlYU`iIQ$KHk6ME4wuys%Tx@oCQCHT*$>$g6qmSkOZlzS3 z8*!kEw7SZ8zPHit5xjTw{Pa-(QHW5kjFv#B8rlTA`~uJN5Z`D0ximlk7@&~7|L~^T zOQm#7Hj0cn0SyC%Gv8s`g&F6<&4LABGC~}f13RkQ!uj4-GVdN;RQY|U?(4(ky*y^x zZYPu84BR&_MK`E=o=qrUObkuX#eL{=iMRdx+#hgC3M&)xZ_u6?j*(SA`N|rt6t8Wb z6CC35m`P5~I);8m6MjpFNYxNT+$7-TYCibRv+x{7SS9-5oK;d6hb!o)s<(~J9ju<U zu`$LODEQCYX)r{^8p?n(!%1Vf?Mg{}BRI&kA76v6ZzP!BNW`PQ2H>GxI~1AP?Y{T% zI*elP_4?aBzx|HZUV{`CJBycT-h3|u1g?XN?<@X2y|V^Sy=Qr-Nb9&Qu!~y<;{{~d z%(V9u<8{ieUZIf`k;y7-)&h5J8599NUTZGJktMbxR^p>`1u0XzgeZA(CauYWXLLzw zBEZVVSM(89fKT}FtaL=3s%Dt=XBGnQM_c5mO76p^br~~ULj10`k_Z`uY<G^NpbSMC z?D}+*658hB!?*AeGg24vkcWS*OWVFZjxx4;<FLYH&ZZa@yB(@K-oumnRX5Kv04l<> zzz4vh*Hi+uwY;x73k8vfWWCTOS4hL%w1V?X*i&R6m%m~`Qc-xQw~ycNnIlzKFy(iA z;IHv(G?%#o&w?dVy@WpV&*F7>kk&G^L<7G)cydY5ySCQokPQVS3;-@kpn+}Qk#SSN zWY_QA#%xHVl@v3sXSIF6S8KNi^Z@04?A;BEycC!1>+4W*8d&M>^`fbc=)<bofsuPv z%WVCv{%}AYczX4d=igJQmG_)j3c2U0HVbMWXGEu|gmzm`JKnO+RZ+P<xNcpdy8<$% zFAGL1B|W)FWyW0yq2xZa)mV-9nLL%R9#$^Vx+2$qF3_BI9i4t;;yNUe70U_xdQ(lR z|MH)KKn~v8yL>js_RlMwcG4MD_agEul-k^upGnTWC2$b0)G^3>lZ7)4m5}=kp-qXI z;^m1ZFIT*<KM2<xsu$kb7g@WiOHax@9ies<v`?p<lPmO&lAVjW`F>`_&onnLx8@z$ zBF<E4I3T3SYqK6@IgGeUvix_UY*%+<;(WW&dbih&%N)qH$AUj$Ps2g#zvjtV=whzB z0`q0mXd@En`{hL2)PWkYQ^)aPXx4-uV0A?#o;r96hP$%jDCRKZd~~CW`Q_~8{Jkg7 zZ$zG2+~IJGfyCNlCo)#m_w~+X<AZC_UubKn2&8|kZ%3CS+QY%wL3nY@gl^Z7$1?wM z8xSDHJ;HCeZtdXV<dnltfU%(mvzqFVu7$^jJ??ITlUTqEryM6}v@|)pJe8lw==AaJ zS}9F^0WAFw{&qUY{6c9OgPvD8UP9;VU)H^tHE>#D1OQ>(N3!2FdDQbUIK)~#-vD=P zYC7%88!Q9}K3H|QnAQ?QdmjgBY@a(@e;t%^i=###%_7Yhl-=d0fuW*YoLPC>l(N6* ztIl04A<rByo&%okNp{5k*z`P-<J}x4YL&ASA9GP!3~cykk8U5g&h4_b-G+rvpQ!P! zPe%Lys(DT3fn+r<!t;Msd@jp5slqI{+xp7>Qt2uMbiWAlZZPOd)kqzLWTQ1^KaDY- z2f+)To@*g5PfNUrowN0@yBw!jAk7cKqQT99$+7%U)oH?g!ba%en{ui|m6sT*1Obfx zpk-)ubzevp&0oP4zjhn}42$Yj+g9^P=9e*y*n_9TI4!}?TEov`lCi^gBABfK^89Tu zn<)CNg8}x~%_O`**9kOqffYOc-mP~yG9z4I!#K31q97X*KN}l+mkrbW6POZvpM~om zQmg^`Js649zdrI!GZPB_9~ME>2;-6rjBl3zFo@(t(Y?`8`~B5>L@R|UvH}5SAS`}{ zJC*p%eEi0~M89zz-EiE)>6mYTwp@xiIMZp~d2bOGuGtq+A#c`bept0j^!>nu&9#BO zJG@f$JW_=rZy2z2h*3sa`Ai<J3W=;Ed`{N-V;|feTYd$wcA=1P58lrETc7Z;krec; zQ^JCCOHACmeU^7Cf9aKG%bU55@}4`K{MFp4%beT98_tvyf<|#kD82B*^HGk;Llf0# z7T7BJ3mP|NCov|Lf}B)z_vAm0sT({ei=Dn7(;9T?Z0$*r>3kfKaOZ8qKD^6!9g^P+ zKf)T|``i8a>R)>g1mZ{tfAa)%s_)^(Ha}LtKV(R`^hEAQ>)`bfAb!_6!Z?U}-ZfHA z)^w`Mk%?l<eF!Z~5+)O$$GI~nJQa8!`i=M38RLAJYL2C=LDcm#>#qbW-}W8u>s{aN z+uv7jHmH<oU4|vu3m?fHUcYx)`^?{bq1ORR2y;fzJ(|VFz-GZWS8)1#TyknCVR6`m zbap_F)4&Tl*<>d=g%;Q5Dp?9dY6yI=G6Cm!n65``x8KsTUAZ;b33C{()R%t?%%(@| zs-FDlPqKG9w5+;FMC~!$Ezc^BbPZN!gCr7vjeJ-|C}E{OYF(1h6S<%vVxkQTWMvvf zjbDvpJ(@pBS}&Y_c#%gD>1GomjH4tr*YsircL)wV(x=5MueGGH1}BN^I^vC8E#}-L z|EhIesusU$Q;R<bI)yzqS9nbJo*edf#GHaM1bVG}v|)7(YSR(q7n&($>66{R?|iI0 z`-s@umK2@gw4Y864EKxL@9T=%;>pZF1-N?^;VyGQm_Cm`o2Ws~$!@%DInXP+q-#yg z@3y-hUA!tDraf<FmcK6czA8|($`|$0P}BuGp7y{avD$<D^U>oob!UMfZyhUH`afE+ zI;P=7aiV(*x{v-n(8BH$Wp9@%o1p!Uquk9{DWzO8`e*cx$z=kb)(;`MX*Dv34tXi4 zS}#f5?O02>xwJFwJ{hd1Rd*{w7Or)}C`K7d%|r&*i4$kBN)^F=J@n)|Z++ZSx+pro z##uCLnrx^Ra)nn*is(jZ5M2gLIP059T483gOKUdH?em}7L3146n>C%ysu%0+<MYp& z@69TMi6H=v(|qZ(42eN6dYP}RMZ7oX_o>YYo!({bG}NY4A*<RN*AEO%S5kE~0R<x1 zucy<cMCGj<=MKl$rB@iN6%^$pGe)BIB;wLWXEw>c)=McCH$gsKU1>T0A>YKv<xr@& zyc||_+<=pdx^R1y*Ku->kLC1pU`a+Zc|{Znm{ow6=9u;vm^mH-`B+!iY*QcOz>0@o z47XH%v_f?jGs_(TX-k4|=EtLAoX9QVjFZ-UDo-oHIU^I3r=qr6ua&mCr&DS5dC?;H zN6A^xs?H}=L9JGw^Sy%OTt)f}28C-F7G?WEx_H-rQ|;WjYNbC+^o@tQq?`UdldqX( zJGe;T9QV`H?<gTPjQ<chxaq|92IFm0h8Oxz?BhF*Od6hpU)%l7YC2e5&IgiG^ns2s zLEl*y=(-qEuFLp!rtr|g85_I`m$yx6V!X^L_Fa3DvVnWB_N>o7j-;j5L)$o`HhpIy zB5vk%qi<gNm^5v<XA78-C(}^r4~wkD>kPjKk4xz+^Z)g<5f=$hrOwgP(zT}^gpAM^ zzQ4zEqHy|Z{BWzktYYbQx9d|s9Z|0ql)!(fm@j9B&Wuh^bE8k!#jiU5ro6YF3y6Ac zzM!b9{(bYAwE+&gd!W8?&ujGJSxn^8&6?WYy{!gCDF(s%M~bY8Jo{UM#6)47&sW|% z4)W#g=UUGti4_=f?ZE;3+gxx<OT^1a!IicqcRDG`lebaDiy%zbn-l&Mb4W?Eo$H|9 zmTAeTQBJf*g{Ih&c~$Z(L>V@ee!-Oz<^?YXa=5CO)J!QWN&YLI@`!#8k(<-fqlppO zU?U{xYD}=0WzLKHgd=cso+OIVR)cl!A}+{TmeNrWjW$amSuS8z{s*kTfFTJJGHZ!# zX~Ncj^7kpWc<8IZaOaq--Uz;Qu!#4?w*^+KPyXzq-^H-NN}&$;d*psUv2{6(LxKc0 zvqeUIMP&)~T-hRSM|IjbnHQPwaf+|<Loka{IIz@VuAUf`eLyFvP`u5tUDL`}Eq!F- zw`8!9k<S4s(YuEqBC)^4>V!E4{KjdIZ97U0CnpLLr{PB5WxZatE$`Nx(cAitattne zjj@@tNTA@Y{j+c8VKaT+#N2aOVss=qQ12oZWJ5IGW>BfdNfqu{%fI*)`Y2K?&8wNf zXVfOBjcfEErpfw7GTZAgZU!;2LL3wCWos<xKO=z}5X}>cSMeoJvn7$jTl;pS`oPrh zWBzK|X|OMKk5qIu|6l4mKG;*>ir|kZ5jmm9K8Yxqf_-L?#1t?`-TE@BqmVOGg)0W) zg(xotvl7y)z{JU_+OgTOPlc=tiF<OZosw3?!XV$5wWUY0L>;QyGksh#t}8P$DB!px z-_K}8XgM!}X5;iub?w<v2PB-2GSx9W2+U$h7wI~F>w<412xaU1JSlS8SwSc{=kynP zlUYY4iLK2hm5oFOt0l>j?jPud$8R~|LkULr&(*xTDw^m4xCPrKMUZ$IeGe(rpb`-F z)5RsS(dXj%Zu&rBb@2t?xFn$Ga!*Z`rXEi|OqJPJKWoZWv7_%@xR$Gixd&KDdX05% z(#(A1bIFcfDkL2uu?4L3>qcTFZ#ct&inhOOg+X4!pA4%$+}rWP4p`qpGd9nzKaaOh z952tKS*UW_RNM({hL>f3)l&OpW^9RF_4Rg9>>gc@po{Vs#?M;If!=w44!Y!`Qz?F& z*_~xe6H11&@bh-{r}=Z;`>bs_>s;#d#4etozVFnK@Y|cn^>r#k6Cv8K9dgR5wig3# z^O4vDgFR_TT)9!c(UPtayx?|%fYP{fUxvh+&Xb!}(`K`r+0ba_y!Q8*VVU{-=<XEX zIe<A*;Cs>KD&G)-GOMA(mSYA7H6ULbt(c`|;S$p}my@F^ILmV_jnT=B|3&?)*qD_^ z#O9;yOO2-dc0QUomQNGfq_J!{RRU#1DSYK2-z9e-m`dJ28kkAi6xpoz%ujyVJghoc z_>H9SsIn~7siTrG_D?6#X~u=B$9Vk*hLw1ifMzLY(>>Q&4#yX%3K%Wov|6ZlPXAjy z0eZ~YE7KFMCbg6vTxiCi-W>V3Vpy!c8d0I9X;>)qoG$y*!kX|q_SW-%LcxvX|JF>@ zd!ojrzMJzxT<PMZN}~+OBMZWcto0ntYz!No5RAgTHfd9&Q5uM6Ge7!+Pw$p6bdDvH z+r$Du*WWk?YP&C=^X<MG%xq6hX%?Jg0nr9x@g!FQ6iIX5YnR@GqN&%{%8-4SBxCj* z^g0y{q;0*)?^pCUx)6c1XZ*}Z;UUkhAFJSfdp7KjdG))5PFgLl46VDirk@<Q^+-g2 z{PV62E;^l!BEb(~6V&FQODzAwEt-wv*-osu3)OiITRfY!Qtq4BGpOx<$Q=|~uk)(e zGX4p~;K6$F$wOpUL*_YG1F&ssLj8diqD(<bv?2Sw_rpO_rnvjt5G+8u++SJ7(a~HA z8uhfI<9~+NKL=;5t=y@f;|`?0b<9$o?c7>8N(Cwg?7_|cMFUS#OP*8`aZ9rBMW<v6 zzbkC&ZGcEy4gh!Kz`Y<1Ps?>T`!MYvNk3DhA_15!J*=fTCL#947*LzeWcK5GKhf$; zBBOr^&@RCKk82}={L7tRZP7$}n%Evryn(D%iWD;kEB{i7FplS0388OU`HQ$+9eCUw zNo4ze!YfK8yflID8F6lbP@>Z3d284Ao!@I$$pFv9ECg%C&ZYjst^$-AGpeym;pJ+1 zGW?F)a7ntZ>NjZ?pF&lf%9|}&OjDfQl4OBc20L+m@P*$q064M#qpevD))R03>R_;Z zGcSz%L?602sti0z3Oj_0ZRBQW?)gOSNL!i2oG<6y-uMS1hAeaF;z7p;D{F*)k++-v z#s6>%PA6QyWK?gl0Cx1TLd7;A-ob6a9T*==b?u!K#sxNrI&FXD)+Nj1C0&56?9K<J z0qc*TfYzBQDQ>I;SauKK3ILl%u=U`s4h;TF6^1MUwqMXVgR}*`5_$b=dnE+lA-TJD zJOdqMOS>#H8|FbZ8(F^Ccr&C|TkieLo=7cPa91zKk3h@MAA7q{WcBe$z2`Hsw=<Ua zUOVZnKyLzdn!bOl1-!yG$9qf2LTPJB+xA`COK!ZRcbi^FIg;5kBZB44-7PhXWU(h& z;Bn!%6pYbxk<=sqfi~F55Ar&1o5tE-$4&1a9l}=YWOKJd5HG?k9-}+IK1vl8Pj7Bs zR}T^u!)*5^e;t1QH^1cu8t;=#g)xMUpXh{n>p&D|cM5uYk96W>Kyj;^n_=6)@2H2f z)?zFa&y}_evgyqTdr9!GJ%06+%jV@bOd$4Bnalm*25*$cGT*4r$n!XcuXk6u8|~6+ zwQrP%G0pA`3a`ln??>S?6FYS+)r}kl4L%qf;+noFOrgFHTN*@?CVhI6eipb=w5lAT zI16W!olRX$znqqGqx-9rihZX8t!>AxwRFJl-AaWuCtoBZ9in{*$+J$p%snDa=XV{s zSM?DVrx+H8ldp%j6A5sqvszw1rdLK&S#s=@zvg|Bx2C}di3wC9h>%7&Tep;2XoDp@ zUyuenE`2tk1DnGaozdraNE7QNOP@@dneuCNCOkbAr7x#&D^B`Pg+^VyML33<tFA-v znA`plIz$kjX{~?-Zyo;y+upCEp}o*ld##|qKcG6<W`%>{p9D4utd^3!8r0(wb$gob z&Sx-~e;k^<nTjMj2{}wMprzBqJrN*UYSkG7px#+G&ILXs{5^8EyzO9;Fk#cB{Xe<Y zzRwAmvlp0cCY|7fa}zt?wEW)oWghn3lUn@r^Jdg{vt*k(iY*G)8rG_0T*Yd4MVW1n z3^Tc&NK)OG*J_WJ)u?gVD6kR;iHf=|<~0EbTt+JR1ziRo+O8z|yWiC+{I8OJ3i;ni zT4Fwe(e6~FIswjKw2qyFOilgqXo#PBO$|(*x`Wpre*NZH+GTjwOm{omavtCVFO7>< zc?s36Il(Ra1k4jocAkyH2HOt+xwL+tR7gc%ZZFfxQZqE9{Uk7m2ZirFYmzBaMC0vG zx{R)mJJ?iA@*8mweNXd-^o_AiddV!R?ZT0iFjxV)tF$w&j<(-*bw!TJXO{2y3N5P2 z2%A%b;-?xQZ(KcABq+E9ox7h-Jkz-8M~yNm&1=sNg6u)nC~~b(QOIm`g+6g>AI1+J z%e_y+x`dyKQR4wv7SnH6cI>Yt1EbKQvBk1Ukt!!tyM|cQbG$3LjZ$6(E;;yL4^x-f z2m;6`cYXH?Q?RhQ+Qkj2;@(1e7qYB06-D@iqfjN;(kQ77#4jac{w9gv%B6wGZ(T2o z&ex7SkC05AXLPfqMjU**zhG(<?Bunqp&{%Do690-$D~N<@b|VM9Ql@)Pbve>55c`B z`X)jInckH1^?V?J`5em7T@4EN|MyH)k|yJNs~f_e<>~mgEbxs?c1x<cz1;1ZtKqsn z|5o`w(xAwny*dOR!8wMzCJVu5!5b7vtcYHLf5IWmO06)#Pwg~xU0PSI)2n|!llu2^ z$w(3tIelU;7*Ec{Dwb5I>rxX0<6#l7yv*NHm3nuW(NnnC)A7?vkuGi)uNo)pN3hzO z!<M%oLEoI!$V>6iQg?B#68FDqv;H67&)w#ehbry6Bo8)931FE7-Vs`oDMpc|ywT)- z(&4GOK69Xd$(~PRrknVL^FoTlq<j)K%_U#JO~Dd~tnW`M0b+IK;G+K=&n!n3SP3Hq zA3F*+5LLai!$G*Od+ktS0_r`%Pqc0@!&$8eyShCdKKj}FMa_ZUt$Y-;Mp!xbWq!ft zK_d(u_A%LbmiQ>DUD`U$>=Rg26057FKRCn1MCMT*p@zp6H?!2gLXDVXdK#($Fe&4B z6)}vBXf}RhjoTPf{~kwn{ul2;pcqpI+OJ8c1D<hMUxkIA4l88lqVC`C74s4~<?k_z zZ?t??#30^H&3EgO+E_=$CUDZzF8!D)9NgL@+ioy`1;r$7gk9!W53e$iA2Dn<6xD}k zU)V<C{_u|upLxS}tt>Yqe-?6`u$k4y$~0Z~R->vYviLkTB;1!!7R}QPG$LiGe%t%F ztASmVP(rJI=9!Q}vPSpqCzE*mvur|&$L?W=@6|n|RZsg&76*eb1)B16albJ8l}Xj_ zUCDYb44=i$$o3gc?TPR9-UK!IbN(*AwCQ<$7aL~gMo8)VeCCTp-I%h6cg|hs7LN&a zoPHa(IRz?JYL9kU4Z|$%o%_n-bRD9Cw+88O1uo`6|2d(l&2FI*MMAhJ0FnAhcGMfQ zH#sG-X_hTz!EcR-9NUiSFiX=a-W@LAD%oXr61n)h>XuSg%VQCJz4AnAsp;`@9(s25 z?$66%VXZx<HF@}X(MKvvc5T*T?I@ZGA2&0?zMMubg*S0p>Ee8ovYWnQfoIhUvGiKc zC<Xqc%dIj(;b@vng^UUmd2D`~RmV-Xs(4!K9}Isy#qUO|Npgfnk5F@Y9UU^+5E?F) zOh7zFB&NAz4Bzp^O^BG!7)FNEj6@GJo&tR%#(Wpob@4CeVt1=tb20%mQExX3!QtEc zmDDiSF*r<HM#ZT_&F{Fq1Orz~*pp3n=uawc+`a}Z%AOb*Kdk7B@bOUKt+3zj)om%~ z1qAk9EbAdNQ0mOL;9hLuo$(JU>%E`}5=>6d!49mNiU@Q$<x!uL?}5p>856IqAnh>H z6EYXPZ7*~I_@-yviLE9Jksp$@!kg_@TjsbVUwskW6S{G?iSNw<@E~gt+_J_*kTWV^ z;<yg8J|VDp+pZdTf^Nr{HruZK<f<;6N;w6K{?@k!i*2m)Z2>TXz{on?rH>PQit#f` z6P3@`jFO!xM9E`S#3mUf+`Wag-PpQaL)gtT>024itEIJC?p8(Bdur0MZNFaa|2I=R z(%f4UD?aRgG;bM0P+nDqg{$CRDOti5>`7{g&Y<(qwku+7oaQ5+xm*1UM*V5)lSr5k z=N9v*J#n?tQvAKMD~@eBi2?zE-0{^e2b~RK9^Q{Ztc4HG5DD{8bcj1?Il80eRP|&| zzhMBgRa1yKr~nnpv*3PBBm*BtXqu?&iHJj8c(*WUzL!j4zDo(0pb<4ii(sjkM}X~A zJ`@tfMZ~dWc_v}=Ch0~$-${B*J$p!)pNv=5?$O1wm!}QQZb5yA)YIzv4G)BWZx}8g zXyd{`7igMRObaaaQkGZ$jZ^H2<LZAgJXbs|d&E11EB^d_e{0&!vcn^7wHy=ju3WwJ z{+IPT@(8TR-f@mV?Bd_Hfask3Wi(BK!Xh_GkYx@A2LFKNJ&(>FE!fVxf;OnMP|$Mc zKFS%Zt1{MB@oiQNvnaZ2RCZGI%HbK7qekhjiWF&!c~uN(Tc_PKGzBoYKyD|cI*>3# zATS?tjpJm0!tD2K4z7-2w%@^}-JXtWU4hG{Pju~=;g04ZcwyA&NdrCmqc<x&^Flfs z)oiQ3JaZRnp93n8FX`zH)&L}Ok*j>7Gu+lN|GnQ4Gw_7j(STm<NNcPg$;HXv0MK8l zGSB-uR|yp4G1&HSMFZAReY!8EOpk^V1jeJ-K=3=u@tw#^G*rDR^Rv*jW^8A4=gV## zx}8R>;wc8JM@U=)I-Rj$K9<(i=mAjNwEfH7K+=8;%cpXbi)6i11XREOfSUleKJz2} z8QFO8LaobvsaB#&t7}k0>47J<k(o9?t1;HPKr#Le%0-1Yz^8Ntf@Ck+s^3B{DQt~! zn3M1!u7yl+Uz*jJm%59Z4_>JVS)@tOmA8jWBo?Mo(v<_UGDl8w@ztRu^sJ5#nU+hR zr$(3qI#3Ai`^H$IlOZ@p$jTh@Z@%$lZ<7#I4B7J2qHJ_b?|}EqL^G#G(-AujjDv#c z4_rxhP{--37dOj4fx8F)baCwY8qAO!)3!+x>#t68Ldd4@*7?smFf9RO-YA~@N%|_p z*Mi%XOS~>!F?dOz)VI!wczYh@qFJJOuIf7Unt3(=-%UzkRHba<q5rnGRYGIHn&Fb< zCI)v3c%<Fc-8#zD850`poPG-xZ1{@&5b&On{G&sB>dc8U+l!g)W(f2Bs@mmAu=pzP zBvIX+1QmP)=+tjWx9^s6?w>@`_48J~klTt;iCLs*;^O6$<;Nmh6e}NS7DHWY*5#Cx zoQ>Ch4Z||IbrKT-(#qwusWn%EO4&k}0UKiW+qCYvxU>Ra155f+A)svtW)!Dg6mI8$ z+YE!>brC<F9W+kZLviZ-!Bc_`<<u8%e07JC<^J-=bo|x`FCbxp8NQ8o+D8O-?}ZkR z=ggCf-$u75ighD&c8c*uY975;_8?Kjf6cCCwqT~+4#k17Ideb%nnC&Gcmcrkp`R~k z9^SzR^UJOuIu^Sv?0J$Vura-{IOSD8J^h3<ZLjb2;kEUNwOW7r`Wy1YW3D?T9-SaP zR(aIV35j!sY&Pvi-5)8xl@`q0aJGDqKG{YZ!~1voHFq%p&IhEWwSRWY*Q3$TT6G=` zFh{*Sk5dZyq%kO9G15U9oi$l(IzYy=_Hch)RO*|DwDoaWRD)I2qMf|1%)n9YgCA%F z;{^Me&OWsv$(v(Mww*@7`s9M{t_2K>#7Hd_tOQBXlw#^op<j<NqHmX!N1lfap|WCn zx9-T1ZSH?#oT3sjF~@~W3Xb3_i#;4o>^Uw^Iuy2FC7v3{7Pm;!@e*RC?s073ZT)Kc z*r+_+18Z<8-uAXAp~&BPXy+!Zeay|<X)oYy@LsM1m4Nx1*YduXM7-K$k=s@|q{L|g z$$VhjG{<hp$PfP)Cip=4Tr1s$s3e~a_z2Z_kf`hTW<0uIoIiP>`~l~~@=+s(+5fMt zE02e&efwFmWGj`Ou{@G!5RG9lQAoC=P<F~vWH&~bNMx@hvW>D#o)WSZ!(f=PuNBz_ z6S5CunPHZ9p69oGe!us9&tK>BIrll|I_J9X>%Q;n{(i4Z4v{|jH!Pd!nSS6WW!Owh z-rU=u4NvR*j%c_P=yVX$2x3!vgMFs?X+8&HkuB3ORCP7{y23RYfBPYL=c*TR<gh5b zA8WEab}NkwW2s2cvn*gF)R}+Mx9oN~PWMnbKF1gRq&nU*S+aE6?wFnX<4zvLFDwLS z^jzay-BOKWrD0yM>gl|whpuai!JPR4+hz1+rBqKJIr$?dhnnrT1pO9o2(EO9bvbxi zddwd=$;K)1V#ryUN4dw8737tjXp52rQX{8~W}n#oQdfH3qtJPQMv^TGEM--(>l@XG z`TV=Xeg70&fNk!x>xV%GNZqxe;~qCe<Lyuda656$!IZRUlI7v?Y<P;mS<WXvz*r#x z2V##nHAhb(Pi9mA5Q&{pfxi@NdQvM;6>ALLO9E~y6!;I0Sb>z>9ZJYH0eL=w4h*d* z+Eq_B(B`7`8^@pgRng-}#*TM0Jlol>-VsPL{UCVKyaXjfy5i$+lUPz0c3IG1MdSV< z;Df>t;*+hW*@IMb9$UL1mE1xq<r?4#PFeHbTg%^~Ol3Cj4Kcpqjna>=UPKIOY<_9l z{Av%Pjy_GaGKEZ@GL3=1#=?Vzh1SisD*)>-J)fk0|DNs$eExFX5xoR$hg9<sfp^d! zJS^78X%*hS9L;H^w@NeBZ4WFFBs<ZF8#9+a1}=sl73KNqV-LY8*R}?h7|zlXw9y8E zX@Ds6alwVl=?q^3rA`D0cL~S0LP+%Pk)=Ge2dS?tb_{(nmQ68x+BkAl0{s=|bI$O> zuv;2zUo<vpMSgaHVh{}lQF+hiUwrOsiWAKr9S$hZp6nv4tuIA7%Y__arRMtG;Htmy z?J;uOBARRT9M7W0*09%|P7*na49n(*f*xv=9-UFP6-GlPhRIR+T!m7F@2n68KW)^s z#TVFN6Il2OlShGbz&QMkleP@MXa41%4OvB9o;H*L)VYj4fDd*QFMQY@!e(?dAaHLG zf-gQFS#*Do8Pfv-Y6YxhMx!oTA9LzR;_99t1jVL3&aUHL&!4;%{*~4@I=d9pOL<vo znM)#Y5h&bB9~U08wFJ7bz{!VD%r%sQy!zps$6c#avoB>Sl5FXlpZnU|4Cf`sXWQMe zhrYgNF^d;_v%}kyQu60L3MOjE5pCM+uv*MI=mfO7cddW}=Rcv^w3i^M@V<hPpPsMK zk{c6B7tr=0q6n%Ug#+r_-nX--YUcC@Q5GBm$7K^Ai%Re|4mBY6$PJ?}ua(<M41W&_ zIYFv#awnNBqsdO&p<gisvQ}P~)>%pU@w}F`?cR2$>!;OTKnVGF3+XR@Mt=IEkn|{M zScw65i6WAU%ZmQ&{*E2hHaj=9*R1jk%;KT&<`obw@kbbf)L*iTgIC#IMp`*P=fvKw zE%x-B++ZR9^R=i&3yDQ(ynIBGXf!t@EQ@@UP3Dy}`(-ZjL?zqY&{nTaX?~%$-!ab! zL{80P?F{Dk%7S-K9dyrKP^FLXcxTH<-m_3$aQmax;@6KVw-Je7Lyu#J+X}YQYA%0x ztIP-`vPCAoHG1|OBC<2nHb8gnfpMO4s$%oJ@F$nr!n9R536!}g65Y3cwC0HjkiBB6 z<H;d3RXIDq75!3>+iK`4Wv-$@+oirsBUCGRYX3Gk?sCrc^q+KAD#u=P19{Vf<$-1v zN0=CKp8uHV(db7E4xv3@=JBcxF`;>OQ(g}@slXxWdbpZOIxnOjyc?cdx)r~5FE(CX z8O9T?lMvoi%e{IgsP*I>0oB>4cs=Ws=_@%FqZ)St{7M8QUCDYh11re8+b>0AFsn`a z&D{WhwY93trczA*U>309H~SpN&TZctRtb-vi=OY$eqi&Ai<{k8#5=>_fNa+8d_zc> zBy1q4Uq-_f@7ISbR{l+JnY0_QnYcHd#H-N@u*>@>=5I7T6!+D9VwH#Uw-Qgp@a#RB zM=A+ipEB}xIcYChR5&#w6~MzMHD+CFO^%5p4M`5X;;5L@evv)<9AIjWC`se-;cJu5 zy~L9%KuGb%ne*A91K-MoH>GU+7#{CR`;{63##K<sOZz~~_6p@p!G-h-sIe7&`Mb~r z>t^{+F?W6l-8;7}B#9Q5pSPkO_(n%{z9;=e8y<@6bNJf1R@VFesX2GJ(#2XD0NXG& zUYtLDUdmNf%?_qeqzuZ=gAFe`@kMkGrpNFK#Ys=00U@=zq8qvf6okEofrHzEdSQL$ zv!br-i#9Gq?Tp^0t-**mQtE*Y(0$15Q@6P94z^hsITRGScysaNrc<!E;T%{d{fq+f z1W430*0uenApCHXUfpa2kz#AHo_OpUaUHOLgfe5l{O%(|SU{(x3gP2LyOW%NIR(16 z;(5QeL!?$i$eIjVyMOGV_bdIOE4{rr_X{`;c>;ob6z?3?2?9n>lAefk2m{<z+eIhz zmOOc|b;GXl-F>~TE0jj+Y04>!N39LAJ9k5vp<89(1D%OTP^Zz)Kpd2;y53`g9sGtO zhbzkDJz}j^a1O7@Izk4N=LZPk`zGl}`{nr4DB{tY`_MC*7T@Xw31zloi7Pv4aJ`i$ z?MLe^Lhxo6Ez$zbq(NI*LEm^Az1E(}tdx20O@!>vE*%$}1Oo4&{P%HJIM}Z(WCtvA zWP)?N-w_tco1=Z0cSsfW8vE7^N+e3^qmr_zi`p0<Z#I<<$0+(|hKT32lN6*VJ}|7Q zKJ}Na+)Z5520wwt0=e{1DJwV+5cE|5xi@QsMhCov&gGmTc0nmMdtmn+1wYc~Pe!)V z74SvlMdjb87v4YG_}YN(NY(l(8OG3|Rb3{^hyI=jU5O$U&wRAWwA@N&IBVk<eA(1! zQ8{xjTy2>!2_lmEG|liJJkM>M0%Lx_dV97?R+89jkFMUDf0WPKWP;R;3fww`SabH< zrGH&`4Z!KyzrGdGssp5NO6PK;%@UyZN;dK>yexWqe{abnxt$PJSDM56FM104&hN(N zjS)OmDH657zPU`(Y4Mg-PJWDavf~G+Z-)Nf_=Vin0HKzC<j+cm9)m(GW{NL<VQt2W zRGCyT({&9S+qfjp9$maHa|-dLXuoe0$vfdZmxd_Y#a;=(3J}Uc4pwo8hhY-ey|m>) z*Cz^yoGWKlWtO>(k{cmm(uh44;iJ^D(5iJZF>Axq*W<|I<6^HteP(|8b%wX<7p^uX zU{i~~;N&aSQ^F_ftDIidb+U+x3xSOrI#L`weqfKDk^~W>=%t7V!Cou=HJ$c|dGL0U zWi6}npXcuP`QIl}9+%~flm`IaYR=oHXEvMR?PH<{^aUVAb}A4Z314@sari9KT11I= z&qy!!<%tdHpREr~73GOEO@dx)o}9HnK}EH$2Z(%3eUbCPK`QYLH?SSfVsR%cwB34$ z#f_cw+685Wf4pHGL*_EhEFx5AbJBGx4XsHAw)E)O_*u(1HdNj1Tu@shG3|0jiFo1+ zcby8{QZzavZ?h`+#t*}E<BWA{1IiHg$va#-HbqMQoms5Eoe!UYP2xhz(fO@3F9iv) z-53w9e6*k1=L)GXE;P-K(bhC3dZ}}N<#Tr;Ri!5>;qGmNV<_+dt6rZdvmM?>^j@wM ztrV4l#(og-@jY*|pmr%5o+;}4OZtl>C!jwJ<n!~rq3sLfHCakn<FoqP{1iBS*%YDN z@;rtn>8B-f_07P+5{V<=z_5!Ckz94JE$(SE63P%0^Y?f4)BQo6w%H<ZV$u<{tza)E zx+f%k&E;L%HI5M9(?*P_%j`3WRJzj01yAA_wF!FaYy~Y#C^~B^<Si$OX?-SPg@kXz zXqy$GGVi{g7ZSe7{|<^%3{0!qJux@2u%kH<pTW+={ua_yC8?0ANGiqOklR1Sf!67s zqo4eDxdTW#nBy6@mqM;yuGgVu@f`^2T_z;YlFm?ow<M!b+|P-xsamarM6plEd%DfX z2P0Y$Vo^AUeMeATL75A4ISx~@!nE;M4JfHr=VVIoInTK7x(bu*Z{Jh&um5=R+9|^Z zqj_~oAC%NzJc1;fuiN$m{C74f=EV#{tpkl|Y7+E}#CX@dmNI*I`*@ez24)ilO&Qez zs%!<l(m`R&$oZhV(};<cLA|Qipk*G{D$?)`E4HX3VExCB2ld=E&B7?~=o{PTq6LwB z3gWQqH^8RhEw)n>frw>Gi~xzZChuHSHkph(bZWiao7c~V<Nb9}kG>zbSiF!d@NlK- zYLNG-WL^BAzD(_vsn*P6uNz+ArJ=pSqFYXawDOb~(@)R`RV?_t7I6L74Wk}s(W@K{ zXWg<YY3dCyCVl^@$Yzc1(`#=OJ{3Mh(Iz)vsjs{_*6U4m-p^>AuvVI_kSjS{Qy+3K z!d^;@4d>V59iQ-o$w_mW;9K22t_)v)(1;%Vs%pN2PY}wL<>43>{_p_$wQrhSif>g< z0DIT=eyPsAs`to+Lo5&<)Ob#B92R|WJH8~#>~zK6V$?SJXot#+XZ|@+zcv@N8BwQz zBFBtnqPMoNS$y2*ORljfX|}PL*PyWd(XDW;DfQT`bv`vgusI*Y+?knuGAdnAGvRGp zLcgz6oKX6VqwKBWtI+=4I@Cy~w)F+&sh35Vx-pjzB}!Vk(QyI3ZHV$!LYt78#r2yr zi&RMLs4*ht3F1IR>BIQVr4ch=h3dJNg`?^W;4-5D%(4PHqH#9FMv)bl=WveOXAdeK zz>Fs>#_3vl1-f<|&0Pv+lNUg%egiB>TD8B+>QhYjd+42I8T#Jl(bR!;DN}CtVoVxf zyN$X`YAvAAE%Vg?FsF9k6NDh1ya5I@WbsOXcR@~s{+Z-I{J4Nd_xU20aX6&fSg8dN zNeuaue&6|BL!hC%eCZm~hE4I=&8X+O83y)v9jZh?^of)VmXnOn8mRnBFM=G>ToM{p zs=VV8eB<yIKMhibE6vE~5JBJ3JmahO^+xY8`dk0LYIn^br~ARBISm8n9mU>jc2Z#s zt8#ilVUlm9=J4)j{X~7IW~!8nqf9YniF&cKGI5DOePlafL(9iEg~{_}9+To@MSA(J zx#|)PjAGJV5rCt1w)uJ(n|3ng9}N>0Y=j0e<b4~ri=E=HP4)GdCRc-sfDDhf$?Kk0 z4lIOD-YMU>2N*r*;lf~K1fu}6<QtJ#2o7#}SzB5JsD%xVC|(I`!aUiQMgD#HB<$Qn z2bo<G4T3-(^BdvsEtV-nENx2D%H4|u5=q?SY<NQqYzl6_dCplXn}s_=aWy|+x`RI^ z9!g50bDm<S`p**cC-FpKFX)ib!WHaKT{as)wiJPs+p_AOt~n6mAd@J)oivl%Sam}4 zcurS0acPs-+D8=p=bPY#M6}R9&;8ZyUHx}w3@&`HK}Se96;y`EP*CbX4$DoR*zcFA z@^al_aZ7OjNLR5lcKIOoJ25MyJMW&tWVCx~zJqGE1(1fmO;vv>OP4=XEF#PE$Yj8H zE|xK_j4EK9!<*RC6_eQfKFf(U8iF8`Q=g3xK8t~`-lQ-^RGrkj%RC&Z5N2s&tUG_G zBbX7EtvLwKq0{@YZX51<s1+x{?E&ZJsc6Eh=4XT@r1y1nhOpH)o7sSxUAyLrhKX~* zF(#(JE5|z3red$Nz=-Mim+`Lc@T3)H@Y4)|?yS3$5{@{Yh-&d<I;?Lb|Iaj#)9nUI zuIkN|2YAJJrjY+v<A7Bm#mIvv=>{Qixt;jcC-zJiBa#3sK5xSmjJdy*t-`1-Pvw2Q zRQSln%SdL~Ir!)4o27%_m%pf82gDO~4;;720OC}FB-)mJ2Gtt`d|p7;nnSEs)UUBp z#@F)hA5jNR?-n<G0Nugi0Kn?I9GerX0bTcByFd=i7yZu2m647>n+9e|8(M0M+T_?B z&u$r0o2=(EzH-IW&59hISVLaa5~D<pu5e*jGhhY{s`8{}q9&`YDhs$a9p10W!^D37 z%a#2@3+O3zH<K^rQ*Z~y#+N;EO*bKiTI11L=Z2^jZdnY%MSH(^{+i)g)+d7WEL%&N z;Y93yx3dhluG>m%o;1-;BT3GE*N0H9rmQ%KA2yZKO?&S0)3U=ujjdImg*0f*E|FGP zrg|@cR^DJk+!xy!%V{C4v63NAGWtSRyLNE40>ly7+@z{>h6{*1SXA$HhfY8j_s$0t zhA*1Je>k)JuV=~O#(xXVG96>LR*NbXTc%a-_YN(HDnhqFd~L;1`LdUzug!6jI}lVk zv^Q}6(6dCTT7?U9_R^b?z-t-vrTrM001Av7gGHDk?m^3W{U;7?=ONw<cBwftR}Wmr zm^~XKyBLGFzLVh5Mc4VDU)c|7w_g&4^u<#_UnBFrV+wE#y`V)XgsoEJn?8hP#Q*g_ zU53TFo;G+TE7^a!)1}oZ1XWCS=+w|zda?Zx2&_g-j`BBR>C$VFFJQ~|p7UnJs>n!$ z(4+d&%~|eX&$hc#TOIHdolV#X{C7-dz+r%X3kE{tz5t<_Kf5(@#S}68qLE>r^wdI9 z#(jmb7x3r@@vun}5nug~v5q6$3#FAJ1m{_z1x!90qyAq_&uh5J0Z;c6RtX}iRXp_} zQHgV|z?To$-+&NHS&2(j9W=u)1)2<ne|N7f{eaztInr!s5Q;Pda`1sOY$vfkhRCo( z)OP|pk!W2Sj56r^XWGMUF!j-2?YdFLm~@88Ta=y^!{AuKg4S!i5>hX9Qx7d;5;0x( z_f0G66C)Sf>Hu(X0iCkJiN=}i|9fElw@<j`)|wjRdM`x*shs-!+Vj;2EBvTnf`s;2 zzjJc~+85MfxZ1={{b&PVqFFNRAorWyqu;M0`rEmPfljngdgUsjOd85PJXBQW9^Igf z{!1zgSd8GiYD0I+ou{r=p1V8n@v|gp(^Ma&+4C=koBx*zb@|b{>O)F>>!L&LvMFR1 z0Xme3bFc4zp9w?^VhYq^1}1~(p;7pt+%if>+X*}CO-ekZ53XaW-{&heNUiV01C}D- zk(iS+Z2_Wj3tuz_bZPc<HGHS+oeMH#Fq$Z}h$i(0^*0g0Ekv-TZPfg1?g+$SZea5N zea7Yi(wCJ0=#4?}L>FC331v*%rbH3*VTCiB@eQTri8_{onutpLlH^z$QZGh4dlrRd zsx&Z{)~9?)M(YKu|EJ^J@@P$wv2S7cGq;uBR_?+=%5noy{Yc8Axn)=(JYNqvMc7(j z3e^i57+aQBLo9C51~fB>J7EYs%qfxF5k|@r{e}}Al(LRldV^mkx%#FoL};|Ed84;F z!gj`nT?;_Lxw5LLlNfTSE_4e^Z7Tc`{f*Jl@BV#MS2bg4zw8TTPM1PA0u!sT2xggP z?MQsy)L`v>C~EG_=$jzNeOMGe3~ZfG4vWP1BH?-%avu>KJx$n>9-Gq(stl#*xZFtF zg|IV!@<y2Zg{l2xDEW_GSZT-qD}^V=r$VO3m<PPfhm?PryUhdL9&`2hJ*wzWoqq}r z_d3Yi4wfpkB2xAq^kkQM{^bzfWu>zUyj31x4uzw;=;f;jv*X-Wx%oj}Q77==UL*xZ zwAN;XlQZzIAJU>rGcc*}aH@ktdR9s|BAngDv*A(rfBOQKA^oW_^v5fn_*48ah2T?m z=dJQQuK0+%CCFsZrU`oHCP*PCZa~Q&PTvp^mMJUc=6v(Mrn`KAtg>~#Oz-@%f<qn? zm3=SCTvic?F3&?WQc-z`;sA?-+gw!E+y_#grJM1uZu1}Czs|>Kbo&FJg{A$`w;8h- zbL{-DhOFZ_>Ir~%ti^`w{)fbiZTGiVXJHiU?YwlxR3dHdz*U=ZiEp<W1TeK!iP7z| zrqMUNH^$O1)0D|HOy*(EA<y6^=I&rW70ezJY&y>5P79kE3&$!u9AO=1##UE88M(#% E59`UU-~a#s diff --git a/dataset/dji_roco/robomaster_Final Tournament/digits/.changes b/dataset/dji_roco/robomaster_Final Tournament/digits/.changes index 094f578582ac0df9123e392b0be3aec31d5a695b..9edf650edeb7e75cd18f7d73a48f7889cafe0cf2 100644 GIT binary patch delta 1823 zcmYjSJ8u<76xQtSjRi>HB{q+kV4Fu_%)-uNXJ#w7e;~+0sghuBg%lzM6bUIv2w#Mt z#hhXq3NB3q6xcFFiZrw+l6{2`qCvJ4x-^bbMMB}+Ju|a*VSV@PH|P1ynfI@={@u>f zOLp-je1CKAm|o;s@CvTqoO2J3vi<Pao39(dEU@cG0^Axq<Tj_-0anB8r3OwWqJ)KC z8gkBrJ3GlvXJS#5ZZN^-8wCCoQaNm2497UEt~HMF7;KIm9yvyeaQOr~ouUhOp0T5& z$ayAY!J+r_RP!qlGUSveLb)#TJIGfB3_k^m+DU=IBZ`!W3KsT;46xictvyf4j@c~V zqk7z(FIh241s*>lOCk&mz6rS+7+-4bTbnKB=OV|IlWe|ma{GQDSQxx(aej?(N7#ZJ zw>`<`8vge0fm6fEE^^orb-{JR88x?t{tA@<<CzaBOPKwYPG+*i#^rxfSD1vAHTsa^ z2k7rPNKKbGi@uK~MLeBI3H?RiEve$jJ5m?e_gBZ+d?HPxXVMe`_)|XuX<_{nIc3tF zpI`+hENkdK50PpZ{zteY>q2nXqsmo`nySLM{sA_JM^zc=i4t*isS<4^OhGBXqN0?9 znLm6e#IX>>VblrY!5uFFrFPg``Jl`0Pr+pc{9~Y3%R}E9WuiH)+Jvjp&75Fo;H;c# zGRi86{Tcp4{Yfj-YnVV<*`n=SWr0gSOvLe;R4_A6W+;^hy^27pePC3MhVI`~c&f2E zv-|v*H7ffVrO!0{zaBVpM{F%&bt7_kkTjaZUCFd8NPu`#9{e^<cTyV|-teu_rqMjk zOk1}wg(?w?z`vtJnSq{&aN`3df!6XP!OW<F2Z0;9-dL1@^$UIsrh;%~GE<?zx2{r! zFnuXdG%S2eQ4*u!#bfe4!eIlA5(&*S8ryWk3)dLv_ULS8EY|7;Xe=z<_a!3Ep9GA{ zFeiR$>H@a@4UTH)8PZ7eAxt~LVTYllApn+W+f8Z9Rstg&RA)JCT<~=vj<*(Z6gc9T zrxkJEWmdF(MSGB#%NX-Rw4xc@7nq7Dgj=0rr<$~aR1zz9gqQH@t#7N9VdBVW6H##k zBv!%Ymw~MsHtuYTcObUQ;_dRX$5&^W`%$7fZd}B1<60e;Vq*0}FWSVK=0O<MfZH@I kQ+&nN=q*XCjS@J76u}F1=ErMSj_sbozaQ}bt=+di1FsDMDgXcg delta 18 ZcmZo{VZC{ZrC|$Wuqqpuf<i4<EdW231+xGE diff --git a/poetry.lock b/poetry.lock index deddcd7ff49e9e80124967f26303ac8ffa6db9b9..d853b41c27feb9d0eaed9b23e008abaeed50ccfb 100644 GIT binary patch delta 19592 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 delta 25339 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 diff --git a/common/README.md b/polystar_cv/README.md similarity index 100% rename from common/README.md rename to polystar_cv/README.md diff --git a/common/polystar/common/__init__.py b/polystar_cv/__init__.py similarity index 100% rename from common/polystar/common/__init__.py rename to polystar_cv/__init__.py diff --git a/common/config/settings.toml b/polystar_cv/config/settings.toml similarity index 67% rename from common/config/settings.toml rename to polystar_cv/config/settings.toml index 9e84f3b..c81c609 100644 --- a/common/config/settings.toml +++ b/polystar_cv/config/settings.toml @@ -3,6 +3,8 @@ CAMERA_WIDTH = 1920 CAMERA_HEIGHT = 1080 CAMERA_HORIZONTAL_FOV = 120 +MODEL_NAME = 'robots/TRT_ssd_mobilenet_v2_roco.bin' + [development] [production] diff --git a/common/polystar/common/communication/__init__.py b/polystar_cv/polystar/__init__.py similarity index 100% rename from common/polystar/common/communication/__init__.py rename to polystar_cv/polystar/__init__.py diff --git a/common/polystar/common/filters/__init__.py b/polystar_cv/polystar/common/__init__.py similarity index 100% rename from common/polystar/common/filters/__init__.py rename to polystar_cv/polystar/common/__init__.py diff --git a/common/polystar/common/frame_generators/__init__.py b/polystar_cv/polystar/common/communication/__init__.py similarity index 100% rename from common/polystar/common/frame_generators/__init__.py rename to polystar_cv/polystar/common/communication/__init__.py diff --git a/common/polystar/common/communication/file_descriptor_target_sender.py b/polystar_cv/polystar/common/communication/file_descriptor_target_sender.py similarity index 100% rename from common/polystar/common/communication/file_descriptor_target_sender.py rename to polystar_cv/polystar/common/communication/file_descriptor_target_sender.py diff --git a/common/polystar/common/communication/print_target_sender.py b/polystar_cv/polystar/common/communication/print_target_sender.py similarity index 100% rename from common/polystar/common/communication/print_target_sender.py rename to polystar_cv/polystar/common/communication/print_target_sender.py diff --git a/common/polystar/common/communication/target_sender_abc.py b/polystar_cv/polystar/common/communication/target_sender_abc.py similarity index 100% rename from common/polystar/common/communication/target_sender_abc.py rename to polystar_cv/polystar/common/communication/target_sender_abc.py diff --git a/common/polystar/common/communication/usb_target_sender.py b/polystar_cv/polystar/common/communication/usb_target_sender.py similarity index 100% rename from common/polystar/common/communication/usb_target_sender.py rename to polystar_cv/polystar/common/communication/usb_target_sender.py diff --git a/common/polystar/common/constants.py b/polystar_cv/polystar/common/constants.py similarity index 100% rename from common/polystar/common/constants.py rename to polystar_cv/polystar/common/constants.py diff --git a/common/polystar/common/dependency_injection.py b/polystar_cv/polystar/common/dependency_injection.py similarity index 80% rename from common/polystar/common/dependency_injection.py rename to polystar_cv/polystar/common/dependency_injection.py index fd22dcb..88eca3a 100644 --- a/common/polystar/common/dependency_injection.py +++ b/polystar_cv/polystar/common/dependency_injection.py @@ -1,16 +1,16 @@ +from dataclasses import dataclass from math import pi -from dataclasses import dataclass from dynaconf import LazySettings -from injector import Module, provider, singleton, multiprovider, Injector +from injector import Injector, Module, multiprovider, provider, singleton from polystar.common.constants import LABEL_MAP_PATH from polystar.common.models.camera import Camera from polystar.common.models.label_map import LabelMap -from polystar.robots_at_robots.globals import settings +from polystar.common.settings import settings -def make_common_injector() -> Injector: +def make_injector() -> Injector: return Injector(modules=[CommonModule(settings)]) diff --git a/common/polystar/common/image_pipeline/__init__.py b/polystar_cv/polystar/common/filters/__init__.py similarity index 100% rename from common/polystar/common/image_pipeline/__init__.py rename to polystar_cv/polystar/common/filters/__init__.py diff --git a/common/polystar/common/filters/exclude_filter.py b/polystar_cv/polystar/common/filters/exclude_filter.py similarity index 100% rename from common/polystar/common/filters/exclude_filter.py rename to polystar_cv/polystar/common/filters/exclude_filter.py diff --git a/common/polystar/common/filters/filter_abc.py b/polystar_cv/polystar/common/filters/filter_abc.py similarity index 100% rename from common/polystar/common/filters/filter_abc.py rename to polystar_cv/polystar/common/filters/filter_abc.py diff --git a/common/polystar/common/filters/keep_filter.py b/polystar_cv/polystar/common/filters/keep_filter.py similarity index 100% rename from common/polystar/common/filters/keep_filter.py rename to polystar_cv/polystar/common/filters/keep_filter.py diff --git a/common/polystar/common/filters/pass_through_filter.py b/polystar_cv/polystar/common/filters/pass_through_filter.py similarity index 100% rename from common/polystar/common/filters/pass_through_filter.py rename to polystar_cv/polystar/common/filters/pass_through_filter.py diff --git a/common/polystar/common/image_pipeline/featurizers/__init__.py b/polystar_cv/polystar/common/frame_generators/__init__.py similarity index 100% rename from common/polystar/common/image_pipeline/featurizers/__init__.py rename to polystar_cv/polystar/common/frame_generators/__init__.py diff --git a/common/polystar/common/frame_generators/camera_frame_generator.py b/polystar_cv/polystar/common/frame_generators/camera_frame_generator.py similarity index 100% rename from common/polystar/common/frame_generators/camera_frame_generator.py rename to polystar_cv/polystar/common/frame_generators/camera_frame_generator.py diff --git a/common/polystar/common/frame_generators/cv2_frame_generator_abc.py b/polystar_cv/polystar/common/frame_generators/cv2_frame_generator_abc.py similarity index 100% rename from common/polystar/common/frame_generators/cv2_frame_generator_abc.py rename to polystar_cv/polystar/common/frame_generators/cv2_frame_generator_abc.py diff --git a/common/polystar/common/frame_generators/fps_video_frame_generator.py b/polystar_cv/polystar/common/frame_generators/fps_video_frame_generator.py similarity index 100% rename from common/polystar/common/frame_generators/fps_video_frame_generator.py rename to polystar_cv/polystar/common/frame_generators/fps_video_frame_generator.py diff --git a/common/polystar/common/frame_generators/frames_generator_abc.py b/polystar_cv/polystar/common/frame_generators/frames_generator_abc.py similarity index 100% rename from common/polystar/common/frame_generators/frames_generator_abc.py rename to polystar_cv/polystar/common/frame_generators/frames_generator_abc.py diff --git a/common/polystar/common/frame_generators/video_frame_generator.py b/polystar_cv/polystar/common/frame_generators/video_frame_generator.py similarity index 100% rename from common/polystar/common/frame_generators/video_frame_generator.py rename to polystar_cv/polystar/common/frame_generators/video_frame_generator.py diff --git a/common/polystar/common/image_pipeline/preprocessors/__init__.py b/polystar_cv/polystar/common/models/__init__.py similarity index 100% rename from common/polystar/common/image_pipeline/preprocessors/__init__.py rename to polystar_cv/polystar/common/models/__init__.py diff --git a/common/polystar/common/models/box.py b/polystar_cv/polystar/common/models/box.py similarity index 100% rename from common/polystar/common/models/box.py rename to polystar_cv/polystar/common/models/box.py diff --git a/common/polystar/common/models/camera.py b/polystar_cv/polystar/common/models/camera.py similarity index 100% rename from common/polystar/common/models/camera.py rename to polystar_cv/polystar/common/models/camera.py diff --git a/common/polystar/common/models/image.py b/polystar_cv/polystar/common/models/image.py similarity index 82% rename from common/polystar/common/models/image.py rename to polystar_cv/polystar/common/models/image.py index 29a0b13..2653d6e 100644 --- a/common/polystar/common/models/image.py +++ b/polystar_cv/polystar/common/models/image.py @@ -5,6 +5,8 @@ from typing import Iterable, List import cv2 import numpy as np +from polystar.common.constants import PROJECT_DIR + Image = np.ndarray @@ -20,6 +22,13 @@ class FileImage: def __array__(self) -> np.ndarray: return self.image + def __getstate__(self) -> str: + return str(self.path.relative_to(PROJECT_DIR)) + + def __setstate__(self, rel_path: str): + self.path = PROJECT_DIR / rel_path + self.image = load_image(self.path) + def load_image(image_path: Path, conversion: int = cv2.COLOR_BGR2RGB) -> Image: return cv2.cvtColor(cv2.imread(str(image_path), cv2.IMREAD_UNCHANGED), conversion) diff --git a/common/polystar/common/models/image_annotation.py b/polystar_cv/polystar/common/models/image_annotation.py similarity index 100% rename from common/polystar/common/models/image_annotation.py rename to polystar_cv/polystar/common/models/image_annotation.py diff --git a/common/polystar/common/models/label_map.py b/polystar_cv/polystar/common/models/label_map.py similarity index 100% rename from common/polystar/common/models/label_map.py rename to polystar_cv/polystar/common/models/label_map.py diff --git a/common/polystar/common/models/object.py b/polystar_cv/polystar/common/models/object.py similarity index 81% rename from common/polystar/common/models/object.py rename to polystar_cv/polystar/common/models/object.py index 06727be..68860b6 100644 --- a/common/polystar/common/models/object.py +++ b/polystar_cv/polystar/common/models/object.py @@ -11,39 +11,41 @@ ArmorNumber = NewType("ArmorNumber", int) class ArmorColor(NoCaseEnum): - Grey = auto() - Blue = auto() - Red = auto() + GREY = auto() + BLUE = auto() + RED = auto() - Unknown = auto() + UNKNOWN = auto() def __str__(self): return self.name.lower() @property def short(self) -> str: - return self.name[0] if self != ArmorColor.Unknown else "?" + return self.name[0] if self != ArmorColor.UNKNOWN else "?" class ArmorDigit(NoCaseEnum): # CHANGING # Those have real numbers - HERO = 1 - ENGINEER = 2 - STANDARD_1 = 3 - STANDARD_2 = 4 - STANDARD_3 = 5 + HERO = auto() + # ENGINEER = 2 + STANDARD_1 = auto() + STANDARD_2 = auto() + # STANDARD_3 = 5 + # Those have symbols - OUTPOST = auto() - BASE = auto() - SENTRY = auto() + # OUTPOST = auto() + # BASE = auto() + # SENTRY = auto() UNKNOWN = auto() OUTDATED = auto() # Old labelisation def __str__(self) -> str: - if self.value <= 5: - return f"{self.value} ({self.name.title()})" - return self.name.title() + # if self.value <= 5: + # return f"{self.value} ({self.name.title()})" + # return self.name.title() + return f"{self.value + (self.value >= 2)} ({self.name.title()})" # hacky, but a number is missing (2) @property def short(self) -> str: diff --git a/common/polystar/common/models/tf_model.py b/polystar_cv/polystar/common/models/tf_model.py similarity index 100% rename from common/polystar/common/models/tf_model.py rename to polystar_cv/polystar/common/models/tf_model.py diff --git a/common/polystar/common/models/trt_model.py b/polystar_cv/polystar/common/models/trt_model.py similarity index 100% rename from common/polystar/common/models/trt_model.py rename to polystar_cv/polystar/common/models/trt_model.py diff --git a/common/polystar/common/models/__init__.py b/polystar_cv/polystar/common/pipeline/__init__.py similarity index 100% rename from common/polystar/common/models/__init__.py rename to polystar_cv/polystar/common/pipeline/__init__.py diff --git a/common/polystar/common/pipeline/__init__.py b/polystar_cv/polystar/common/pipeline/classification/__init__.py similarity index 100% rename from common/polystar/common/pipeline/__init__.py rename to polystar_cv/polystar/common/pipeline/classification/__init__.py diff --git a/common/polystar/common/pipeline/classification/classification_pipeline.py b/polystar_cv/polystar/common/pipeline/classification/classification_pipeline.py similarity index 81% rename from common/polystar/common/pipeline/classification/classification_pipeline.py rename to polystar_cv/polystar/common/pipeline/classification/classification_pipeline.py index 56086c9..c85c7d6 100644 --- a/common/polystar/common/pipeline/classification/classification_pipeline.py +++ b/polystar_cv/polystar/common/pipeline/classification/classification_pipeline.py @@ -1,6 +1,6 @@ from abc import ABC from enum import IntEnum -from typing import ClassVar, Generic, List, Sequence, Tuple, TypeVar +from typing import ClassVar, Generic, List, Sequence, Tuple, Type, TypeVar from numpy import asarray, ndarray, pad @@ -12,11 +12,19 @@ EnumT = TypeVar("EnumT", bound=IntEnum) class ClassificationPipeline(Pipeline, Generic[IT, EnumT], ABC): - enum: ClassVar[EnumT] + enum: ClassVar[Type[EnumT]] + + classes: ClassVar[List[EnumT]] + n_classes: ClassVar[int] + + def __init_subclass__(cls): + if hasattr(cls, "enum"): + cls.classes = [klass for klass in cls.enum if klass.name not in {"OUTDATED", "UNKNOWN"}] + cls.n_classes = len(cls.classes) def __init__(self, steps: List[Tuple[str, PipeABC]]): super().__init__(steps) - self.classifier.n_classes = len(self.enum) + self.classifier.n_classes = self.n_classes @property def classifier(self) -> ClassifierABC: @@ -41,20 +49,13 @@ class ClassificationPipeline(Pipeline, Generic[IT, EnumT], ABC): def predict_proba_and_classes(self, x: Sequence[IT]) -> Tuple[ndarray, List[EnumT]]: proba = asarray(self.predict_proba(x)) indices = proba.argmax(axis=1) - classes = [self.classes_[i] for i in indices] + classes = [self.classes[i] for i in indices] return proba, classes def score(self, x: Sequence[IT], y: List[EnumT], **score_params) -> float: """It is needed to have a proper CV""" return super().score(x, _labels_to_indices(y), **score_params) - @property - def classes_(self) -> List[EnumT]: - return list(self.enum) - - def __init_subclass__(cls, **kwargs): - assert hasattr(cls, "enum"), f"You need to provide an `enum` ClassVar for {cls.__name__}" - def _labels_to_indices(labels: List[EnumT]) -> ndarray: return asarray([label.value - 1 for label in labels]) diff --git a/common/polystar/common/pipeline/classification/classifier_abc.py b/polystar_cv/polystar/common/pipeline/classification/classifier_abc.py similarity index 100% rename from common/polystar/common/pipeline/classification/classifier_abc.py rename to polystar_cv/polystar/common/pipeline/classification/classifier_abc.py diff --git a/polystar_cv/polystar/common/pipeline/classification/keras_classification_pipeline.py b/polystar_cv/polystar/common/pipeline/classification/keras_classification_pipeline.py new file mode 100644 index 0000000..388e33d --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/classification/keras_classification_pipeline.py @@ -0,0 +1,179 @@ +from os.path import join +from typing import Callable, Dict, List, Sequence, Tuple, Union + +from hypertune import HyperTune +from tensorflow.python.keras.callbacks import Callback, EarlyStopping, TensorBoard +from tensorflow.python.keras.losses import CategoricalCrossentropy +from tensorflow.python.keras.metrics import categorical_accuracy +from tensorflow.python.keras.models import Model +from tensorflow.python.keras.optimizer_v2.adam import Adam + +from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline +from polystar.common.pipeline.keras.classifier import KerasClassifier +from polystar.common.pipeline.keras.cnn import make_cnn_model +from polystar.common.pipeline.keras.compilation_parameters import KerasCompilationParameters +from polystar.common.pipeline.keras.data_preparator import KerasDataPreparator +from polystar.common.pipeline.keras.distillation import DistillationLoss, DistillationMetric, Distiller +from polystar.common.pipeline.keras.trainer import KerasTrainer +from polystar.common.pipeline.keras.transfer_learning import make_transfer_learning_model +from polystar.common.pipeline.preprocessors.normalise import Normalise +from polystar.common.pipeline.preprocessors.resize import Resize + + +class KerasClassificationPipeline(ClassificationPipeline): + @classmethod + def from_model(cls, model: Model, trainer: KerasTrainer, input_shape: Tuple[int, int], name: str): + return cls.from_pipes( + [Resize(input_shape), Normalise(), KerasClassifier(model=model, trainer=trainer)], name=name + ) + + @classmethod + def from_transfer_learning( + cls, + logs_dir: str, + input_size: int, + model_factory: Callable[..., Model], + dropout: float, + dense_size: int, + lr: float, + verbose: int = 0, + name: str = None, + ): + input_shape = (input_size, input_size) + name = name or f"{model_factory.__name__} ({input_size}) - lr {lr:.1e} - drop {dropout:.1%} - {dense_size}" + return cls.from_model( + model=make_transfer_learning_model( + input_shape=input_shape, + n_classes=cls.n_classes, + model_factory=model_factory, + dropout=dropout, + dense_size=dense_size, + ), + trainer=make_classification_trainer( + lr=lr, logs_dir=logs_dir, name=name, verbose=verbose, batch_size=32, steps_per_epoch=100 + ), + name=name, + input_shape=input_shape, + ) + + @classmethod + def from_custom_cnn( + cls, + logs_dir: str, + input_size: int, + conv_blocks: Sequence[Sequence[int]], + dropout: float, + dense_size: int, + lr: float, + verbose: int = 0, + name: str = None, + batch_size: int = 32, + steps_per_epoch: Union[str, int] = 100, + ) -> ClassificationPipeline: + name = name or ( + f"cnn - ({input_size}) - lr {lr:.1e} - drop {dropout:.1%} - " + + " ".join("_".join(map(str, sizes)) for sizes in conv_blocks) + + f" - {dense_size}" + ) + input_shape = (input_size, input_size) + return cls.from_model( + make_cnn_model( + input_shape=input_shape, + conv_blocks=conv_blocks, + dense_size=dense_size, + output_size=cls.n_classes, + dropout=dropout, + ), + trainer=make_classification_trainer( + lr=lr, + logs_dir=logs_dir, + name=name, + verbose=verbose, + batch_size=batch_size, + steps_per_epoch=steps_per_epoch, + ), + name=name, + input_shape=input_shape, + ) + + @classmethod + def from_distillation( + cls, + teacher_pipeline: ClassificationPipeline, + logs_dir: str, + conv_blocks: Sequence[Sequence[int]], + dropout: float, + dense_size: int, + lr: float, + temperature: float, + verbose: int = 0, + name: str = None, + ): + input_shape: Tuple[int, int] = teacher_pipeline.named_steps["Resize"].size + name = name or ( + f"distiled - temp {temperature:.1e}" + f" - cnn - ({input_shape[0]}) - lr {lr:.1e} - drop {dropout:.1%} - " + + " ".join("_".join(map(str, sizes)) for sizes in conv_blocks) + + f" - {dense_size}" + ) + return cls.from_model( + model=make_cnn_model( + input_shape, conv_blocks=conv_blocks, dense_size=dense_size, output_size=cls.n_classes, dropout=dropout, + ), + trainer=KerasTrainer( + model_preparator=Distiller(temperature=temperature, teacher_model=teacher_pipeline.classifier.model), + compilation_parameters=KerasCompilationParameters( + loss=DistillationLoss(temperature=temperature, n_classes=cls.n_classes), + metrics=[DistillationMetric(categorical_accuracy, n_classes=cls.n_classes)], + optimizer=Adam(lr), + ), + callbacks=make_classification_callbacks(join(logs_dir, name)), + verbose=verbose, + ), + name=name, + input_shape=input_shape, + ) + + +def make_classification_callbacks(log_dir: str) -> List[Callback]: + return [ + EarlyStopping(verbose=0, patience=7, restore_best_weights=True, monitor="val_categorical_accuracy"), + TensorBoard(log_dir=log_dir), + # HyperTuneClassificationCallback(), + ] + + +def make_classification_trainer( + lr: float, logs_dir: str, name: str, verbose: int, batch_size: int, steps_per_epoch: Union[str, int] +) -> KerasTrainer: + return KerasTrainer( + compilation_parameters=KerasCompilationParameters( + loss=CategoricalCrossentropy(from_logits=False), metrics=[categorical_accuracy], optimizer=Adam(lr) + ), + data_preparator=KerasDataPreparator(batch_size=batch_size, steps=steps_per_epoch), + callbacks=make_classification_callbacks(join(logs_dir, name)), + verbose=verbose, + ) + + +class HyperTuneClassificationCallback(Callback): + def __init__(self): + super().__init__() + self.hpt = HyperTune() + self.best_accuracy_epoch = (0, -1) + + def on_epoch_end(self, epoch: int, logs: Dict = None): + accuracy = logs["val_categorical_accuracy"] + self._report(accuracy, epoch) + self.best_accuracy_epoch = max(self.best_accuracy_epoch, (accuracy, epoch)) + + def on_train_begin(self, logs=None): + self.best_accuracy_epoch = (0, -1) + + def on_train_end(self, logs=None): + self._report(*self.best_accuracy_epoch) + + def _report(self, accuracy: float, epoch: int): + self.hpt.report_hyperparameter_tuning_metric( + hyperparameter_metric_tag="val_accuracy", metric_value=accuracy, global_step=epoch + ) diff --git a/common/polystar/common/pipeline/classification/random_model.py b/polystar_cv/polystar/common/pipeline/classification/random_model.py similarity index 100% rename from common/polystar/common/pipeline/classification/random_model.py rename to polystar_cv/polystar/common/pipeline/classification/random_model.py diff --git a/common/polystar/common/pipeline/classification/rule_based_classifier.py b/polystar_cv/polystar/common/pipeline/classification/rule_based_classifier.py similarity index 100% rename from common/polystar/common/pipeline/classification/rule_based_classifier.py rename to polystar_cv/polystar/common/pipeline/classification/rule_based_classifier.py diff --git a/common/polystar/common/pipeline/concat.py b/polystar_cv/polystar/common/pipeline/concat.py similarity index 100% rename from common/polystar/common/pipeline/concat.py rename to polystar_cv/polystar/common/pipeline/concat.py diff --git a/common/polystar/common/pipeline/classification/__init__.py b/polystar_cv/polystar/common/pipeline/featurizers/__init__.py similarity index 100% rename from common/polystar/common/pipeline/classification/__init__.py rename to polystar_cv/polystar/common/pipeline/featurizers/__init__.py diff --git a/common/polystar/common/image_pipeline/featurizers/histogram_2d.py b/polystar_cv/polystar/common/pipeline/featurizers/histogram_2d.py similarity index 51% rename from common/polystar/common/image_pipeline/featurizers/histogram_2d.py rename to polystar_cv/polystar/common/pipeline/featurizers/histogram_2d.py index adbf0e4..d20d6ca 100644 --- a/common/polystar/common/image_pipeline/featurizers/histogram_2d.py +++ b/polystar_cv/polystar/common/pipeline/featurizers/histogram_2d.py @@ -12,12 +12,15 @@ class Histogram2D(PipeABC): bins: int = 8 def transform_single(self, image: Image) -> ndarray: - return array([self._channel_hist(image, channel) for channel in range(3)]).ravel() - - def _channel_hist(self, image: Image, channel: int) -> ndarray: - hist = cv2.calcHist([image], [channel], None, [self.bins], [0, 256], accumulate=False).ravel() - return hist / hist.sum() + return array( + [calculate_normalized_channel_histogram(image, channel, self.bins) for channel in range(3)] + ).ravel() @property def name(self) -> str: return "hist" + + +def calculate_normalized_channel_histogram(image: Image, channel: int, bins: int) -> ndarray: + hist = cv2.calcHist([image], [channel], None, [bins], [0, 256], accumulate=False).ravel() + return hist / hist.sum() diff --git a/polystar_cv/polystar/common/pipeline/featurizers/histogram_blocs_2d.py b/polystar_cv/polystar/common/pipeline/featurizers/histogram_blocs_2d.py new file mode 100644 index 0000000..dcef9d0 --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/featurizers/histogram_blocs_2d.py @@ -0,0 +1,30 @@ +from dataclasses import dataclass +from itertools import chain +from typing import Iterable + +from numpy import array_split +from numpy.core._multiarray_umath import array, ndarray + +from polystar.common.models.image import Image +from polystar.common.pipeline.featurizers.histogram_2d import calculate_normalized_channel_histogram +from polystar.common.pipeline.pipe_abc import PipeABC + + +@dataclass +class HistogramBlocs2D(PipeABC): + bins: int = 8 + rows: int = 2 + cols: int = 3 + + def transform_single(self, image: Image) -> ndarray: + return array( + [ + calculate_normalized_channel_histogram(bloc, channel, self.bins) + for channel in range(3) + for bloc in _split_images_in_blocs(image, self.rows, self.cols) + ] + ).ravel() + + +def _split_images_in_blocs(image: Image, n_rows: int, n_cols: int) -> Iterable[Image]: + return chain.from_iterable(array_split(column, n_rows, axis=0) for column in array_split(image, n_cols, axis=1)) diff --git a/common/polystar/common/target_pipeline/__init__.py b/polystar_cv/polystar/common/pipeline/keras/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/__init__.py rename to polystar_cv/polystar/common/pipeline/keras/__init__.py diff --git a/polystar_cv/polystar/common/pipeline/keras/classifier.py b/polystar_cv/polystar/common/pipeline/keras/classifier.py new file mode 100644 index 0000000..8568a87 --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/classifier.py @@ -0,0 +1,54 @@ +from copy import copy +from tempfile import NamedTemporaryFile +from typing import Dict, List, Optional, Sequence + +from numpy import asarray +from tensorflow.python.keras.models import Model, load_model +from tensorflow.python.keras.utils.np_utils import to_categorical + +from polystar.common.models.image import Image +from polystar.common.pipeline.classification.classifier_abc import ClassifierABC +from polystar.common.pipeline.keras.trainer import KerasTrainer +from polystar.common.utils.registry import registry + + +@registry.register() +class KerasClassifier(ClassifierABC): + def __init__(self, model: Model, trainer: KerasTrainer): + self.model = model + self.trainer: Optional[KerasTrainer] = trainer + + def fit(self, images: List[Image], labels: List[int], validation_size: int) -> "KerasClassifier": + assert self.trainable, "You can't train an un-pickled classifier" + images = asarray(images) + labels = to_categorical(asarray(labels), self.n_classes) + train_images, train_labels = images[:-validation_size], labels[:-validation_size] + val_images, val_labels = images[-validation_size:], labels[-validation_size:] + + self.trainer.train(self.model, train_images, train_labels, val_images, val_labels) + + return self + + def predict_proba(self, examples: List[Image]) -> Sequence[float]: + return self.model.predict(asarray(examples)) + + def __getstate__(self) -> Dict: + with NamedTemporaryFile(suffix=".hdf5", delete=True) as fd: + self.model.save(fd.name, overwrite=True, include_optimizer=False) + model_str = fd.read() + state = copy(self.__dict__) + state.pop("model") + state.pop("trainer") + return {**state, "model_str": model_str} + + def __setstate__(self, state: Dict): + self.__dict__.update(state) + with NamedTemporaryFile(suffix=".hdf5", delete=True) as fd: + fd.write(state.pop("model_str")) + fd.flush() + self.model = load_model(fd.name) + self.trainer = None + + @property + def trainable(self) -> bool: + return self.trainer is not None diff --git a/polystar_cv/polystar/common/pipeline/keras/cnn.py b/polystar_cv/polystar/common/pipeline/keras/cnn.py new file mode 100644 index 0000000..8e3cecf --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/cnn.py @@ -0,0 +1,27 @@ +from typing import Sequence, Tuple + +from tensorflow.python.keras import Input, Sequential +from tensorflow.python.keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D, Softmax + + +def make_cnn_model( + input_shape: Tuple[int, int], + conv_blocks: Sequence[Sequence[int]], + dense_size: int, + output_size: int, + dropout: float, +) -> Sequential: + model = Sequential() + model.add(Input((*input_shape, 3))) + + for conv_sizes in conv_blocks: + for size in conv_sizes: + model.add(Conv2D(size, (3, 3), activation="relu")) + model.add(MaxPooling2D()) + + model.add(Flatten()) + model.add(Dense(dense_size)) + model.add(Dropout(dropout)) + model.add(Dense(output_size)) + model.add(Softmax()) + return model diff --git a/polystar_cv/polystar/common/pipeline/keras/compilation_parameters.py b/polystar_cv/polystar/common/pipeline/keras/compilation_parameters.py new file mode 100644 index 0000000..7bca53c --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/compilation_parameters.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import Callable, Dict, List, Optional, Union + +from tensorflow.python.keras.losses import Loss +from tensorflow.python.keras.metrics import Metric +from tensorflow.python.keras.optimizer_v2.optimizer_v2 import OptimizerV2 + + +@dataclass +class KerasCompilationParameters: + optimizer: Union[str, OptimizerV2] + loss: Union[str, Callable, Loss] + metrics: List[Union[str, Callable, Metric]] + loss_weights: Optional[Dict[str, float]] = None diff --git a/polystar_cv/polystar/common/pipeline/keras/data_preparator.py b/polystar_cv/polystar/common/pipeline/keras/data_preparator.py new file mode 100644 index 0000000..1120ac0 --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/data_preparator.py @@ -0,0 +1,15 @@ +from typing import Any, Tuple, Union + +from numpy.core._multiarray_umath import ndarray +from tensorflow.python.keras.preprocessing.image import ImageDataGenerator + + +class KerasDataPreparator: + def __init__(self, batch_size: int, steps: Union[str, int]): + self.steps = steps + self.batch_size = batch_size + + def prepare_training_data(self, images: ndarray, labels: ndarray) -> Tuple[Any, int]: + train_datagen = ImageDataGenerator() + steps = self.steps if isinstance(self.steps, int) else len(images) / self.batch_size + return train_datagen.flow(images, labels, batch_size=self.batch_size, shuffle=True), steps diff --git a/polystar_cv/polystar/common/pipeline/keras/distillation.py b/polystar_cv/polystar/common/pipeline/keras/distillation.py new file mode 100644 index 0000000..79b42c6 --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/distillation.py @@ -0,0 +1,58 @@ +from typing import Callable + +from tensorflow.python.keras import Input, Model, Sequential +from tensorflow.python.keras.layers import Softmax, concatenate +from tensorflow.python.keras.losses import KLDivergence +from tensorflow.python.keras.models import Model +from tensorflow.python.ops.nn_ops import softmax + +from polystar.common.pipeline.keras.model_preparator import KerasModelPreparator + + +class DistillationLoss(KLDivergence): + def __init__(self, temperature: float, n_classes: int): + super().__init__() + self.n_classes = n_classes + self.temperature = temperature + + def __call__(self, y_true, y_pred, sample_weight=None): + teacher_logits, student_logits = y_pred[:, : self.n_classes], y_pred[:, self.n_classes :] + return super().__call__( + softmax(teacher_logits / self.temperature, axis=1), + softmax(student_logits / self.temperature, axis=1), + sample_weight=sample_weight, + ) + + +class DistillationMetric: + def __init__(self, metric: Callable, n_classes: int): + self.n_classes = n_classes + self.metric = metric + self.__name__ = metric.__name__ + + def __call__(self, y_true, y_pred): + teacher_logits, student_logits = y_pred[:, : self.n_classes], y_pred[:, self.n_classes :] + return self.metric(y_true, student_logits) + + +class Distiller(KerasModelPreparator): + def __init__( + self, teacher_model: Model, temperature: float, + ): + self.teacher_model = teacher_model + self.temperature = temperature + assert isinstance(teacher_model.layers[-1], Softmax) + + def prepare_model(self, model: Model) -> Model: + assert isinstance(model.layers[-1], Softmax) + + self.teacher_model.trainable = False + + inputs = Input(shape=model.input.shape[1:]) + + return Model( + inputs=inputs, + outputs=concatenate( + [Sequential(self.teacher_model.layers[:-1])(inputs), Sequential(model.layers[:-1])(inputs)] + ), + ) diff --git a/polystar_cv/polystar/common/pipeline/keras/model_preparator.py b/polystar_cv/polystar/common/pipeline/keras/model_preparator.py new file mode 100644 index 0000000..e41e03f --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/model_preparator.py @@ -0,0 +1,6 @@ +from tensorflow.python.keras.models import Model + + +class KerasModelPreparator: + def prepare_model(self, model: Model) -> Model: + return model diff --git a/polystar_cv/polystar/common/pipeline/keras/trainer.py b/polystar_cv/polystar/common/pipeline/keras/trainer.py new file mode 100644 index 0000000..9db4ba7 --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/trainer.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass, field +from typing import List + +from numpy.core._multiarray_umath import ndarray +from tensorflow.python.keras.callbacks import Callback +from tensorflow.python.keras.models import Model + +from polystar.common.pipeline.keras.compilation_parameters import KerasCompilationParameters +from polystar.common.pipeline.keras.data_preparator import KerasDataPreparator +from polystar.common.pipeline.keras.model_preparator import KerasModelPreparator + + +@dataclass +class KerasTrainer: + compilation_parameters: KerasCompilationParameters + callbacks: List[Callback] + data_preparator: KerasDataPreparator = field(default_factory=KerasDataPreparator) + model_preparator: KerasModelPreparator = field(default_factory=KerasModelPreparator) + max_epochs: int = 300 + verbose: int = 0 + + def train( + self, + model: Model, + train_images: ndarray, + train_labels: ndarray, + validation_images: ndarray, + validation_labels: ndarray, + ): + model = self.model_preparator.prepare_model(model) + model.compile(**self.compilation_parameters.__dict__) + train_data, steps = self.data_preparator.prepare_training_data(train_images, train_labels) + + model.fit( + x=train_data, + validation_data=(validation_images, validation_labels), + steps_per_epoch=steps, + epochs=self.max_epochs, + callbacks=self.callbacks, + verbose=self.verbose, + ) diff --git a/polystar_cv/polystar/common/pipeline/keras/transfer_learning.py b/polystar_cv/polystar/common/pipeline/keras/transfer_learning.py new file mode 100644 index 0000000..a6a7b47 --- /dev/null +++ b/polystar_cv/polystar/common/pipeline/keras/transfer_learning.py @@ -0,0 +1,24 @@ +from typing import Callable, Tuple + +from tensorflow.python.keras import Input, Model, Sequential +from tensorflow.python.keras.layers import Dense, Dropout, Flatten, Softmax +from tensorflow.python.keras.models import Model + + +def make_transfer_learning_model( + input_shape: Tuple[int, int], n_classes: int, model_factory: Callable[..., Model], dropout: float, dense_size: int, +) -> Sequential: + input_shape = (*input_shape, 3) + base_model: Model = model_factory(weights="imagenet", input_shape=input_shape, include_top=False) + + return Sequential( + [ + Input(input_shape), + base_model, + Flatten(), + Dense(dense_size, activation="relu"), + Dropout(dropout), + Dense(n_classes), + Softmax(), + ] + ) diff --git a/common/polystar/common/pipeline/pipe_abc.py b/polystar_cv/polystar/common/pipeline/pipe_abc.py similarity index 100% rename from common/polystar/common/pipeline/pipe_abc.py rename to polystar_cv/polystar/common/pipeline/pipe_abc.py diff --git a/common/polystar/common/pipeline/pipeline.py b/polystar_cv/polystar/common/pipeline/pipeline.py similarity index 81% rename from common/polystar/common/pipeline/pipeline.py rename to polystar_cv/polystar/common/pipeline/pipeline.py index 52ed2e2..e09d130 100644 --- a/common/polystar/common/pipeline/pipeline.py +++ b/polystar_cv/polystar/common/pipeline/pipeline.py @@ -6,10 +6,12 @@ from polystar.common.pipeline.classification.classifier_abc import ClassifierABC from polystar.common.pipeline.pipe_abc import PipeABC, get_pipes_names_without_repetitions from polystar.common.utils.named_mixin import NamedMixin +Pipes = List[Union[PipeABC, ClassifierABC]] + class Pipeline(Pipeline, NamedMixin): @classmethod - def from_pipes(cls, pipes: List[Union[PipeABC, ClassifierABC]], name: str = None) -> "Pipeline": + def from_pipes(cls, pipes: Pipes, name: str = None) -> "Pipeline": names = get_pipes_names_without_repetitions(pipes) rv = cls(list(zip(names, pipes))) rv.name = name or "-".join(names) diff --git a/common/polystar/common/target_pipeline/armors_descriptors/__init__.py b/polystar_cv/polystar/common/pipeline/preprocessors/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/armors_descriptors/__init__.py rename to polystar_cv/polystar/common/pipeline/preprocessors/__init__.py diff --git a/common/polystar/common/image_pipeline/preprocessors/normalise.py b/polystar_cv/polystar/common/pipeline/preprocessors/normalise.py similarity index 74% rename from common/polystar/common/image_pipeline/preprocessors/normalise.py rename to polystar_cv/polystar/common/pipeline/preprocessors/normalise.py index a00c8d0..0f84029 100644 --- a/common/polystar/common/image_pipeline/preprocessors/normalise.py +++ b/polystar_cv/polystar/common/pipeline/preprocessors/normalise.py @@ -1,7 +1,9 @@ from polystar.common.models.image import Image from polystar.common.pipeline.pipe_abc import PipeABC +from polystar.common.utils.registry import registry +@registry.register() class Normalise(PipeABC): def transform_single(self, image: Image) -> Image: return image / 255 diff --git a/common/polystar/common/image_pipeline/preprocessors/resize.py b/polystar_cv/polystar/common/pipeline/preprocessors/resize.py similarity index 82% rename from common/polystar/common/image_pipeline/preprocessors/resize.py rename to polystar_cv/polystar/common/pipeline/preprocessors/resize.py index 6afbc2b..c239e0a 100644 --- a/common/polystar/common/image_pipeline/preprocessors/resize.py +++ b/polystar_cv/polystar/common/pipeline/preprocessors/resize.py @@ -4,8 +4,10 @@ from cv2.cv2 import resize from polystar.common.models.image import Image from polystar.common.pipeline.pipe_abc import PipeABC +from polystar.common.utils.registry import registry +@registry.register() class Resize(PipeABC): def __init__(self, size: Tuple[int, int]): self.size = size diff --git a/common/polystar/common/image_pipeline/preprocessors/rgb_to_hsv.py b/polystar_cv/polystar/common/pipeline/preprocessors/rgb_to_hsv.py similarity index 100% rename from common/polystar/common/image_pipeline/preprocessors/rgb_to_hsv.py rename to polystar_cv/polystar/common/pipeline/preprocessors/rgb_to_hsv.py diff --git a/common/polystar/common/settings.py b/polystar_cv/polystar/common/settings.py similarity index 69% rename from common/polystar/common/settings.py rename to polystar_cv/polystar/common/settings.py index 943fa5f..23b13f2 100644 --- a/common/polystar/common/settings.py +++ b/polystar_cv/polystar/common/settings.py @@ -17,12 +17,15 @@ class Settings(LazySettings): def _config_file_for_project(project_name: str) -> Path: - return PROJECT_DIR / project_name / "config" / "settings.toml" + return PROJECT_DIR / "config" / "settings.toml" -def make_settings(project_name: str) -> LazySettings: +def make_settings() -> LazySettings: return LazySettings( SILENT_ERRORS_FOR_DYNACONF=False, - SETTINGS_FILE_FOR_DYNACONF=f"{_config_file_for_project('common')},{_config_file_for_project(project_name)}", + SETTINGS_FILE_FOR_DYNACONF=f"{PROJECT_DIR / 'config' / 'settings.toml'}", ENV_SWITCHER_FOR_DYNACONF="POLYSTAR_ENV", ) + + +settings = make_settings() diff --git a/common/polystar/common/target_pipeline/detected_objects/__init__.py b/polystar_cv/polystar/common/target_pipeline/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/detected_objects/__init__.py rename to polystar_cv/polystar/common/target_pipeline/__init__.py diff --git a/common/polystar/common/target_pipeline/object_selectors/__init__.py b/polystar_cv/polystar/common/target_pipeline/armors_descriptors/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/object_selectors/__init__.py rename to polystar_cv/polystar/common/target_pipeline/armors_descriptors/__init__.py diff --git a/common/polystar/common/target_pipeline/armors_descriptors/armors_color_descriptor.py b/polystar_cv/polystar/common/target_pipeline/armors_descriptors/armors_color_descriptor.py similarity index 100% rename from common/polystar/common/target_pipeline/armors_descriptors/armors_color_descriptor.py rename to polystar_cv/polystar/common/target_pipeline/armors_descriptors/armors_color_descriptor.py diff --git a/common/polystar/common/target_pipeline/armors_descriptors/armors_descriptor_abc.py b/polystar_cv/polystar/common/target_pipeline/armors_descriptors/armors_descriptor_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/armors_descriptors/armors_descriptor_abc.py rename to polystar_cv/polystar/common/target_pipeline/armors_descriptors/armors_descriptor_abc.py diff --git a/common/polystar/common/target_pipeline/debug_pipeline.py b/polystar_cv/polystar/common/target_pipeline/debug_pipeline.py similarity index 100% rename from common/polystar/common/target_pipeline/debug_pipeline.py rename to polystar_cv/polystar/common/target_pipeline/debug_pipeline.py diff --git a/common/polystar/common/target_pipeline/objects_detectors/__init__.py b/polystar_cv/polystar/common/target_pipeline/detected_objects/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_detectors/__init__.py rename to polystar_cv/polystar/common/target_pipeline/detected_objects/__init__.py diff --git a/common/polystar/common/target_pipeline/detected_objects/detected_armor.py b/polystar_cv/polystar/common/target_pipeline/detected_objects/detected_armor.py similarity index 97% rename from common/polystar/common/target_pipeline/detected_objects/detected_armor.py rename to polystar_cv/polystar/common/target_pipeline/detected_objects/detected_armor.py index 613385b..3bfaf2b 100644 --- a/common/polystar/common/target_pipeline/detected_objects/detected_armor.py +++ b/polystar_cv/polystar/common/target_pipeline/detected_objects/detected_armor.py @@ -27,7 +27,7 @@ class DetectedArmor(DetectedObject): self._color = ArmorColor(self.colors_proba.argmax() + 1) return self._color - return ArmorColor.Unknown + return ArmorColor.UNKNOWN @property def digit(self) -> ArmorDigit: diff --git a/common/polystar/common/target_pipeline/detected_objects/detected_object.py b/polystar_cv/polystar/common/target_pipeline/detected_objects/detected_object.py similarity index 100% rename from common/polystar/common/target_pipeline/detected_objects/detected_object.py rename to polystar_cv/polystar/common/target_pipeline/detected_objects/detected_object.py diff --git a/common/polystar/common/target_pipeline/detected_objects/detected_objects_factory.py b/polystar_cv/polystar/common/target_pipeline/detected_objects/detected_objects_factory.py similarity index 100% rename from common/polystar/common/target_pipeline/detected_objects/detected_objects_factory.py rename to polystar_cv/polystar/common/target_pipeline/detected_objects/detected_objects_factory.py diff --git a/common/polystar/common/target_pipeline/detected_objects/detected_robot.py b/polystar_cv/polystar/common/target_pipeline/detected_objects/detected_robot.py similarity index 100% rename from common/polystar/common/target_pipeline/detected_objects/detected_robot.py rename to polystar_cv/polystar/common/target_pipeline/detected_objects/detected_robot.py diff --git a/common/polystar/common/target_pipeline/objects_linker/__init__.py b/polystar_cv/polystar/common/target_pipeline/object_selectors/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_linker/__init__.py rename to polystar_cv/polystar/common/target_pipeline/object_selectors/__init__.py diff --git a/common/polystar/common/target_pipeline/object_selectors/closest_object_selector.py b/polystar_cv/polystar/common/target_pipeline/object_selectors/closest_object_selector.py similarity index 100% rename from common/polystar/common/target_pipeline/object_selectors/closest_object_selector.py rename to polystar_cv/polystar/common/target_pipeline/object_selectors/closest_object_selector.py diff --git a/common/polystar/common/target_pipeline/object_selectors/object_selector_abc.py b/polystar_cv/polystar/common/target_pipeline/object_selectors/object_selector_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/object_selectors/object_selector_abc.py rename to polystar_cv/polystar/common/target_pipeline/object_selectors/object_selector_abc.py diff --git a/common/polystar/common/target_pipeline/object_selectors/scored_object_selector_abc.py b/polystar_cv/polystar/common/target_pipeline/object_selectors/scored_object_selector_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/object_selectors/scored_object_selector_abc.py rename to polystar_cv/polystar/common/target_pipeline/object_selectors/scored_object_selector_abc.py diff --git a/common/polystar/common/target_pipeline/objects_trackers/__init__.py b/polystar_cv/polystar/common/target_pipeline/objects_detectors/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_trackers/__init__.py rename to polystar_cv/polystar/common/target_pipeline/objects_detectors/__init__.py diff --git a/common/polystar/common/target_pipeline/objects_detectors/objects_detector_abc.py b/polystar_cv/polystar/common/target_pipeline/objects_detectors/objects_detector_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_detectors/objects_detector_abc.py rename to polystar_cv/polystar/common/target_pipeline/objects_detectors/objects_detector_abc.py diff --git a/common/polystar/common/target_pipeline/objects_detectors/tf_model_objects_detector.py b/polystar_cv/polystar/common/target_pipeline/objects_detectors/tf_model_objects_detector.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_detectors/tf_model_objects_detector.py rename to polystar_cv/polystar/common/target_pipeline/objects_detectors/tf_model_objects_detector.py diff --git a/common/polystar/common/target_pipeline/objects_detectors/trt_model_object_detector.py b/polystar_cv/polystar/common/target_pipeline/objects_detectors/trt_model_object_detector.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_detectors/trt_model_object_detector.py rename to polystar_cv/polystar/common/target_pipeline/objects_detectors/trt_model_object_detector.py diff --git a/common/polystar/common/target_pipeline/objects_validators/__init__.py b/polystar_cv/polystar/common/target_pipeline/objects_linker/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/__init__.py rename to polystar_cv/polystar/common/target_pipeline/objects_linker/__init__.py diff --git a/common/polystar/common/target_pipeline/objects_linker/objects_linker_abs.py b/polystar_cv/polystar/common/target_pipeline/objects_linker/objects_linker_abs.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_linker/objects_linker_abs.py rename to polystar_cv/polystar/common/target_pipeline/objects_linker/objects_linker_abs.py diff --git a/common/polystar/common/target_pipeline/objects_linker/simple_objects_linker.py b/polystar_cv/polystar/common/target_pipeline/objects_linker/simple_objects_linker.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_linker/simple_objects_linker.py rename to polystar_cv/polystar/common/target_pipeline/objects_linker/simple_objects_linker.py diff --git a/common/polystar/common/target_pipeline/target_factories/__init__.py b/polystar_cv/polystar/common/target_pipeline/objects_trackers/__init__.py similarity index 100% rename from common/polystar/common/target_pipeline/target_factories/__init__.py rename to polystar_cv/polystar/common/target_pipeline/objects_trackers/__init__.py diff --git a/common/polystar/common/target_pipeline/objects_trackers/object_track.py b/polystar_cv/polystar/common/target_pipeline/objects_trackers/object_track.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_trackers/object_track.py rename to polystar_cv/polystar/common/target_pipeline/objects_trackers/object_track.py diff --git a/common/polystar/common/target_pipeline/objects_trackers/objects_tracker_abc.py b/polystar_cv/polystar/common/target_pipeline/objects_trackers/objects_tracker_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_trackers/objects_tracker_abc.py rename to polystar_cv/polystar/common/target_pipeline/objects_trackers/objects_tracker_abc.py diff --git a/common/polystar/common/utils/__init__.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/__init__.py similarity index 100% rename from common/polystar/common/utils/__init__.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/__init__.py diff --git a/common/polystar/common/target_pipeline/objects_validators/confidence_object_validator.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/confidence_object_validator.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/confidence_object_validator.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/confidence_object_validator.py diff --git a/common/polystar/common/target_pipeline/objects_validators/contains_box_validator.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/contains_box_validator.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/contains_box_validator.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/contains_box_validator.py diff --git a/common/polystar/common/target_pipeline/objects_validators/in_box_validator.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/in_box_validator.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/in_box_validator.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/in_box_validator.py diff --git a/common/polystar/common/target_pipeline/objects_validators/negation_validator.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/negation_validator.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/negation_validator.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/negation_validator.py diff --git a/common/polystar/common/target_pipeline/objects_validators/objects_validator_abc.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/objects_validator_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/objects_validator_abc.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/objects_validator_abc.py diff --git a/common/polystar/common/target_pipeline/objects_validators/robot_color_validator.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/robot_color_validator.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/robot_color_validator.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/robot_color_validator.py diff --git a/common/polystar/common/target_pipeline/objects_validators/type_object_validator.py b/polystar_cv/polystar/common/target_pipeline/objects_validators/type_object_validator.py similarity index 100% rename from common/polystar/common/target_pipeline/objects_validators/type_object_validator.py rename to polystar_cv/polystar/common/target_pipeline/objects_validators/type_object_validator.py diff --git a/common/polystar/common/target_pipeline/target_abc.py b/polystar_cv/polystar/common/target_pipeline/target_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/target_abc.py rename to polystar_cv/polystar/common/target_pipeline/target_abc.py diff --git a/common/polystar/common/view/__init__.py b/polystar_cv/polystar/common/target_pipeline/target_factories/__init__.py similarity index 100% rename from common/polystar/common/view/__init__.py rename to polystar_cv/polystar/common/target_pipeline/target_factories/__init__.py diff --git a/common/polystar/common/target_pipeline/target_factories/ratio_simple_target_factory.py b/polystar_cv/polystar/common/target_pipeline/target_factories/ratio_simple_target_factory.py similarity index 100% rename from common/polystar/common/target_pipeline/target_factories/ratio_simple_target_factory.py rename to polystar_cv/polystar/common/target_pipeline/target_factories/ratio_simple_target_factory.py diff --git a/common/polystar/common/target_pipeline/target_factories/ratio_target_factory_abc.py b/polystar_cv/polystar/common/target_pipeline/target_factories/ratio_target_factory_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/target_factories/ratio_target_factory_abc.py rename to polystar_cv/polystar/common/target_pipeline/target_factories/ratio_target_factory_abc.py diff --git a/common/polystar/common/target_pipeline/target_factories/target_factory_abc.py b/polystar_cv/polystar/common/target_pipeline/target_factories/target_factory_abc.py similarity index 100% rename from common/polystar/common/target_pipeline/target_factories/target_factory_abc.py rename to polystar_cv/polystar/common/target_pipeline/target_factories/target_factory_abc.py diff --git a/common/polystar/common/target_pipeline/target_pipeline.py b/polystar_cv/polystar/common/target_pipeline/target_pipeline.py similarity index 100% rename from common/polystar/common/target_pipeline/target_pipeline.py rename to polystar_cv/polystar/common/target_pipeline/target_pipeline.py diff --git a/common/polystar/common/target_pipeline/tracking_target_pipeline.py b/polystar_cv/polystar/common/target_pipeline/tracking_target_pipeline.py similarity index 100% rename from common/polystar/common/target_pipeline/tracking_target_pipeline.py rename to polystar_cv/polystar/common/target_pipeline/tracking_target_pipeline.py diff --git a/common/research/common/__init__.py b/polystar_cv/polystar/common/utils/__init__.py similarity index 100% rename from common/research/common/__init__.py rename to polystar_cv/polystar/common/utils/__init__.py diff --git a/common/polystar/common/utils/dataframe.py b/polystar_cv/polystar/common/utils/dataframe.py similarity index 100% rename from common/polystar/common/utils/dataframe.py rename to polystar_cv/polystar/common/utils/dataframe.py diff --git a/common/polystar/common/utils/git.py b/polystar_cv/polystar/common/utils/git.py similarity index 100% rename from common/polystar/common/utils/git.py rename to polystar_cv/polystar/common/utils/git.py diff --git a/common/polystar/common/utils/iterable_utils.py b/polystar_cv/polystar/common/utils/iterable_utils.py similarity index 100% rename from common/polystar/common/utils/iterable_utils.py rename to polystar_cv/polystar/common/utils/iterable_utils.py diff --git a/common/research/common/dataset/__init__.py b/polystar_cv/polystar/common/utils/logs.py similarity index 100% rename from common/research/common/dataset/__init__.py rename to polystar_cv/polystar/common/utils/logs.py diff --git a/common/polystar/common/utils/markdown.py b/polystar_cv/polystar/common/utils/markdown.py similarity index 94% rename from common/polystar/common/utils/markdown.py rename to polystar_cv/polystar/common/utils/markdown.py index 791bef0..77530fe 100644 --- a/common/polystar/common/utils/markdown.py +++ b/polystar_cv/polystar/common/utils/markdown.py @@ -3,6 +3,7 @@ from typing import Any, Iterable, TextIO from markdown.core import markdown from matplotlib.figure import Figure +from matplotlib.pyplot import close from pandas import DataFrame from tabulate import tabulate from xhtml2pdf.document import pisaDocument @@ -40,9 +41,10 @@ class MarkdownFile: self.paragraph(f".replace(' ', '%20')})") return self - def figure(self, figure: Figure, name: str, alt: str = "img"): - name = name.replace(" ", "_") + def figure(self, figure: Figure, name: str, alt: str = "img", close_after: bool = True): + name = name.replace(" ", "_").replace("%", "p") figure.savefig(self.markdown_path.parent / name) + close(figure) return self.image(name, alt) def table(self, data: DataFrame) -> "MarkdownFile": diff --git a/common/polystar/common/utils/misc.py b/polystar_cv/polystar/common/utils/misc.py similarity index 100% rename from common/polystar/common/utils/misc.py rename to polystar_cv/polystar/common/utils/misc.py diff --git a/common/polystar/common/utils/named_mixin.py b/polystar_cv/polystar/common/utils/named_mixin.py similarity index 100% rename from common/polystar/common/utils/named_mixin.py rename to polystar_cv/polystar/common/utils/named_mixin.py diff --git a/common/polystar/common/utils/no_case_enum.py b/polystar_cv/polystar/common/utils/no_case_enum.py similarity index 72% rename from common/polystar/common/utils/no_case_enum.py rename to polystar_cv/polystar/common/utils/no_case_enum.py index b89a03f..b55371d 100644 --- a/common/polystar/common/utils/no_case_enum.py +++ b/polystar_cv/polystar/common/utils/no_case_enum.py @@ -4,4 +4,4 @@ from enum import IntEnum class NoCaseEnum(IntEnum): @classmethod def _missing_(cls, key): - return cls[key.capitalize()] + return cls[key.upper()] diff --git a/polystar_cv/polystar/common/utils/registry.py b/polystar_cv/polystar/common/utils/registry.py new file mode 100644 index 0000000..d3eb717 --- /dev/null +++ b/polystar_cv/polystar/common/utils/registry.py @@ -0,0 +1,18 @@ +from itertools import chain +from typing import Dict, Sequence, Type + +from polystar.common.utils.singleton import Singleton + + +class Registry(Dict[str, Type], Singleton): + def register(self, previous_names: Sequence[str] = ()): + def decorator(class_: Type): + for name in chain((class_.__name__,), previous_names): + assert name not in self, f"{name} is already registered" + self[name] = class_ + return class_ + + return decorator + + +registry = Registry() diff --git a/polystar_cv/polystar/common/utils/serialization.py b/polystar_cv/polystar/common/utils/serialization.py new file mode 100644 index 0000000..b7aff56 --- /dev/null +++ b/polystar_cv/polystar/common/utils/serialization.py @@ -0,0 +1,25 @@ +import logging +import pickle +from pathlib import Path +from typing import Any, Type + +from polystar.common.utils.registry import registry + + +class UnpicklerWithRegistry(pickle.Unpickler): + def find_class(self, module: str, name: str) -> Type: + try: + return registry[name] + except KeyError: + return super().find_class(module, name) + + +def pkl_load(file_path: Path): + with file_path.with_suffix(".pkl").open("rb") as f: + return UnpicklerWithRegistry(f).load() + + +def pkl_dump(obj: Any, file_path: Path): + file_path_with_suffix = file_path.with_suffix(".pkl") + file_path_with_suffix.write_bytes(pickle.dumps(obj)) + logging.info(f"{obj} saved at {file_path_with_suffix}") diff --git a/polystar_cv/polystar/common/utils/singleton.py b/polystar_cv/polystar/common/utils/singleton.py new file mode 100644 index 0000000..488b92c --- /dev/null +++ b/polystar_cv/polystar/common/utils/singleton.py @@ -0,0 +1,16 @@ +class SingletonMetaclass(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(SingletonMetaclass, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +class Singleton: + _instance = None + + def __new__(cls, *args, **kwargs): + if not isinstance(cls._instance, cls): + cls._instance = object.__new__(cls, *args, **kwargs) + return cls._instance diff --git a/common/polystar/common/utils/str_utils.py b/polystar_cv/polystar/common/utils/str_utils.py similarity index 100% rename from common/polystar/common/utils/str_utils.py rename to polystar_cv/polystar/common/utils/str_utils.py diff --git a/common/polystar/common/utils/tensorflow.py b/polystar_cv/polystar/common/utils/tensorflow.py similarity index 100% rename from common/polystar/common/utils/tensorflow.py rename to polystar_cv/polystar/common/utils/tensorflow.py diff --git a/common/polystar/common/utils/time.py b/polystar_cv/polystar/common/utils/time.py similarity index 100% rename from common/polystar/common/utils/time.py rename to polystar_cv/polystar/common/utils/time.py diff --git a/common/polystar/common/utils/tqdm.py b/polystar_cv/polystar/common/utils/tqdm.py similarity index 100% rename from common/polystar/common/utils/tqdm.py rename to polystar_cv/polystar/common/utils/tqdm.py diff --git a/common/polystar/common/utils/working_directory.py b/polystar_cv/polystar/common/utils/working_directory.py similarity index 100% rename from common/polystar/common/utils/working_directory.py rename to polystar_cv/polystar/common/utils/working_directory.py diff --git a/common/research/common/dataset/cleaning/__init__.py b/polystar_cv/polystar/common/view/__init__.py similarity index 100% rename from common/research/common/dataset/cleaning/__init__.py rename to polystar_cv/polystar/common/view/__init__.py diff --git a/common/polystar/common/view/cv2_results_viewer.py b/polystar_cv/polystar/common/view/cv2_results_viewer.py similarity index 100% rename from common/polystar/common/view/cv2_results_viewer.py rename to polystar_cv/polystar/common/view/cv2_results_viewer.py diff --git a/common/polystar/common/view/plt_results_viewer.py b/polystar_cv/polystar/common/view/plt_results_viewer.py similarity index 100% rename from common/polystar/common/view/plt_results_viewer.py rename to polystar_cv/polystar/common/view/plt_results_viewer.py diff --git a/common/polystar/common/view/results_viewer_abc.py b/polystar_cv/polystar/common/view/results_viewer_abc.py similarity index 100% rename from common/polystar/common/view/results_viewer_abc.py rename to polystar_cv/polystar/common/view/results_viewer_abc.py diff --git a/common/research/common/dataset/improvement/__init__.py b/polystar_cv/research/__init__.py similarity index 100% rename from common/research/common/dataset/improvement/__init__.py rename to polystar_cv/research/__init__.py diff --git a/common/research/common/dataset/perturbations/__init__.py b/polystar_cv/research/common/__init__.py similarity index 100% rename from common/research/common/dataset/perturbations/__init__.py rename to polystar_cv/research/common/__init__.py diff --git a/common/research/common/constants.py b/polystar_cv/research/common/constants.py similarity index 100% rename from common/research/common/constants.py rename to polystar_cv/research/common/constants.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/__init__.py b/polystar_cv/research/common/dataset/__init__.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/__init__.py rename to polystar_cv/research/common/dataset/__init__.py diff --git a/common/research/common/dataset/twitch/__init__.py b/polystar_cv/research/common/dataset/cleaning/__init__.py similarity index 100% rename from common/research/common/dataset/twitch/__init__.py rename to polystar_cv/research/common/dataset/cleaning/__init__.py diff --git a/common/research/common/dataset/cleaning/dataset_changes.py b/polystar_cv/research/common/dataset/cleaning/dataset_changes.py similarity index 77% rename from common/research/common/dataset/cleaning/dataset_changes.py rename to polystar_cv/research/common/dataset/cleaning/dataset_changes.py index 3375476..bdeb474 100644 --- a/common/research/common/dataset/cleaning/dataset_changes.py +++ b/polystar_cv/research/common/dataset/cleaning/dataset_changes.py @@ -1,4 +1,5 @@ import json +from contextlib import suppress from pathlib import Path from typing import Dict, List, Set @@ -6,6 +7,7 @@ from more_itertools import flatten from polystar.common.utils.git import get_git_username from polystar.common.utils.time import create_time_id +from research.common.gcloud.gcloud_storage import GCStorages INVALIDATED_KEY: str = "invalidated" @@ -13,6 +15,8 @@ INVALIDATED_KEY: str = "invalidated" class DatasetChanges: def __init__(self, dataset_directory: Path): self.changes_file: Path = dataset_directory / ".changes" + with suppress(FileNotFoundError): + GCStorages.DEV.download_file_if_missing(self.changes_file) @property def invalidated(self) -> Set[str]: @@ -30,3 +34,7 @@ class DatasetChanges: changes[INVALIDATED_KEY][entry_id] = names self.changes_file.write_text(json.dumps(changes, indent=2)) print(f"changes saved, see entry {entry_id} in file://{self.changes_file}") + self.upload() + + def upload(self): + GCStorages.DEV.upload_file(self.changes_file) diff --git a/common/research/common/dataset/cleaning/dataset_cleaner_app.py b/polystar_cv/research/common/dataset/cleaning/dataset_cleaner_app.py similarity index 100% rename from common/research/common/dataset/cleaning/dataset_cleaner_app.py rename to polystar_cv/research/common/dataset/cleaning/dataset_cleaner_app.py diff --git a/common/research/common/datasets/__init__.py b/polystar_cv/research/common/dataset/improvement/__init__.py similarity index 100% rename from common/research/common/datasets/__init__.py rename to polystar_cv/research/common/dataset/improvement/__init__.py diff --git a/common/research/common/dataset/improvement/zoom.py b/polystar_cv/research/common/dataset/improvement/zoom.py similarity index 100% rename from common/research/common/dataset/improvement/zoom.py rename to polystar_cv/research/common/dataset/improvement/zoom.py diff --git a/common/research/common/datasets/roco/__init__.py b/polystar_cv/research/common/dataset/perturbations/__init__.py similarity index 100% rename from common/research/common/datasets/roco/__init__.py rename to polystar_cv/research/common/dataset/perturbations/__init__.py diff --git a/common/research/common/dataset/perturbations/examples/.gitignore b/polystar_cv/research/common/dataset/perturbations/examples/.gitignore similarity index 100% rename from common/research/common/dataset/perturbations/examples/.gitignore rename to polystar_cv/research/common/dataset/perturbations/examples/.gitignore diff --git a/common/research/common/dataset/perturbations/examples/test.png b/polystar_cv/research/common/dataset/perturbations/examples/test.png similarity index 100% rename from common/research/common/dataset/perturbations/examples/test.png rename to polystar_cv/research/common/dataset/perturbations/examples/test.png diff --git a/common/research/common/datasets/roco/zoo/__init__.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/__init__.py similarity index 100% rename from common/research/common/datasets/roco/zoo/__init__.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/__init__.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/brightness.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/brightness.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/brightness.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/brightness.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/contrast.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/contrast.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/contrast.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/contrast.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/gaussian_blur.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/gaussian_blur.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/gaussian_blur.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/gaussian_blur.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/gaussian_noise.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/gaussian_noise.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/gaussian_noise.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/gaussian_noise.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/horizontal_blur.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/horizontal_blur.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/horizontal_blur.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/horizontal_blur.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/image_modifier_abc.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/image_modifier_abc.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/image_modifier_abc.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/image_modifier_abc.py diff --git a/common/research/common/dataset/perturbations/image_modifiers/saturation.py b/polystar_cv/research/common/dataset/perturbations/image_modifiers/saturation.py similarity index 100% rename from common/research/common/dataset/perturbations/image_modifiers/saturation.py rename to polystar_cv/research/common/dataset/perturbations/image_modifiers/saturation.py diff --git a/common/research/common/dataset/perturbations/perturbator.py b/polystar_cv/research/common/dataset/perturbations/perturbator.py similarity index 100% rename from common/research/common/dataset/perturbations/perturbator.py rename to polystar_cv/research/common/dataset/perturbations/perturbator.py diff --git a/common/research/common/dataset/perturbations/utils.py b/polystar_cv/research/common/dataset/perturbations/utils.py similarity index 100% rename from common/research/common/dataset/perturbations/utils.py rename to polystar_cv/research/common/dataset/perturbations/utils.py diff --git a/common/research/common/dataset/tensorflow_record.py b/polystar_cv/research/common/dataset/tensorflow_record.py similarity index 100% rename from common/research/common/dataset/tensorflow_record.py rename to polystar_cv/research/common/dataset/tensorflow_record.py diff --git a/common/research/common/scripts/__init__.py b/polystar_cv/research/common/dataset/twitch/__init__.py similarity index 100% rename from common/research/common/scripts/__init__.py rename to polystar_cv/research/common/dataset/twitch/__init__.py diff --git a/common/research/common/dataset/twitch/aerial_view_detector.py b/polystar_cv/research/common/dataset/twitch/aerial_view_detector.py similarity index 100% rename from common/research/common/dataset/twitch/aerial_view_detector.py rename to polystar_cv/research/common/dataset/twitch/aerial_view_detector.py diff --git a/common/research/common/dataset/twitch/mask_aerial.jpg b/polystar_cv/research/common/dataset/twitch/mask_aerial.jpg similarity index 100% rename from common/research/common/dataset/twitch/mask_aerial.jpg rename to polystar_cv/research/common/dataset/twitch/mask_aerial.jpg diff --git a/common/research/common/dataset/twitch/mask_detector.py b/polystar_cv/research/common/dataset/twitch/mask_detector.py similarity index 100% rename from common/research/common/dataset/twitch/mask_detector.py rename to polystar_cv/research/common/dataset/twitch/mask_detector.py diff --git a/common/research/common/dataset/twitch/mask_robot_view.jpg b/polystar_cv/research/common/dataset/twitch/mask_robot_view.jpg similarity index 100% rename from common/research/common/dataset/twitch/mask_robot_view.jpg rename to polystar_cv/research/common/dataset/twitch/mask_robot_view.jpg diff --git a/common/research/common/dataset/twitch/robots_views_extractor.py b/polystar_cv/research/common/dataset/twitch/robots_views_extractor.py similarity index 100% rename from common/research/common/dataset/twitch/robots_views_extractor.py rename to polystar_cv/research/common/dataset/twitch/robots_views_extractor.py diff --git a/common/research/common/dataset/upload.py b/polystar_cv/research/common/dataset/upload.py similarity index 70% rename from common/research/common/dataset/upload.py rename to polystar_cv/research/common/dataset/upload.py index cc857b1..5cbc168 100644 --- a/common/research/common/dataset/upload.py +++ b/polystar_cv/research/common/dataset/upload.py @@ -2,6 +2,7 @@ import logging from tqdm import tqdm +from research.common.dataset.cleaning.dataset_changes import DatasetChanges from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder from research.common.datasets.roco.roco_datasets import ROCODatasets from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo @@ -13,6 +14,11 @@ def upload_all_digit_datasets(roco_datasets: ROCODatasets): upload_digit_dataset(roco_dataset) +def upload_all_color_datasets(roco_datasets: ROCODatasets): + for roco_dataset in tqdm(roco_datasets, desc="Uploading datasets"): + upload_color_dataset(roco_dataset) + + def upload_digit_dataset(roco_dataset: ROCODatasetBuilder): _upload_armor_dataset(roco_dataset, "digits") @@ -23,9 +29,11 @@ def upload_color_dataset(roco_dataset: ROCODatasetBuilder): def _upload_armor_dataset(roco_dataset: ROCODatasetBuilder, name: str): GCStorages.DEV.upload_directory(roco_dataset.main_dir / name, extensions_to_exclude={".changes"}) + DatasetChanges(roco_dataset.main_dir / name).upload() if __name__ == "__main__": logging.getLogger().setLevel("INFO") - upload_all_digit_datasets(ROCODatasetsZoo.DJI) + upload_all_digit_datasets(ROCODatasetsZoo.TWITCH) + upload_digit_dataset(ROCODatasetsZoo.DJI.FINAL) diff --git a/common/tests/common/integration_tests/__init__.py b/polystar_cv/research/common/datasets/__init__.py similarity index 100% rename from common/tests/common/integration_tests/__init__.py rename to polystar_cv/research/common/datasets/__init__.py diff --git a/common/research/common/datasets/dataset.py b/polystar_cv/research/common/datasets/dataset.py similarity index 100% rename from common/research/common/datasets/dataset.py rename to polystar_cv/research/common/datasets/dataset.py diff --git a/common/research/common/datasets/dataset_builder.py b/polystar_cv/research/common/datasets/dataset_builder.py similarity index 100% rename from common/research/common/datasets/dataset_builder.py rename to polystar_cv/research/common/datasets/dataset_builder.py diff --git a/common/research/common/datasets/filter_dataset.py b/polystar_cv/research/common/datasets/filter_dataset.py similarity index 100% rename from common/research/common/datasets/filter_dataset.py rename to polystar_cv/research/common/datasets/filter_dataset.py diff --git a/common/research/common/datasets/image_dataset.py b/polystar_cv/research/common/datasets/image_dataset.py similarity index 100% rename from common/research/common/datasets/image_dataset.py rename to polystar_cv/research/common/datasets/image_dataset.py diff --git a/common/research/common/datasets/image_file_dataset_builder.py b/polystar_cv/research/common/datasets/image_file_dataset_builder.py similarity index 100% rename from common/research/common/datasets/image_file_dataset_builder.py rename to polystar_cv/research/common/datasets/image_file_dataset_builder.py diff --git a/common/research/common/datasets/iterator_dataset.py b/polystar_cv/research/common/datasets/iterator_dataset.py similarity index 100% rename from common/research/common/datasets/iterator_dataset.py rename to polystar_cv/research/common/datasets/iterator_dataset.py diff --git a/common/research/common/datasets/lazy_dataset.py b/polystar_cv/research/common/datasets/lazy_dataset.py similarity index 100% rename from common/research/common/datasets/lazy_dataset.py rename to polystar_cv/research/common/datasets/lazy_dataset.py diff --git a/common/tests/common/integration_tests/datasets/__init__.py b/polystar_cv/research/common/datasets/roco/__init__.py similarity index 100% rename from common/tests/common/integration_tests/datasets/__init__.py rename to polystar_cv/research/common/datasets/roco/__init__.py diff --git a/common/research/common/datasets/roco/roco_annotation.py b/polystar_cv/research/common/datasets/roco/roco_annotation.py similarity index 100% rename from common/research/common/datasets/roco/roco_annotation.py rename to polystar_cv/research/common/datasets/roco/roco_annotation.py diff --git a/common/research/common/datasets/roco/roco_dataset.py b/polystar_cv/research/common/datasets/roco/roco_dataset.py similarity index 100% rename from common/research/common/datasets/roco/roco_dataset.py rename to polystar_cv/research/common/datasets/roco/roco_dataset.py diff --git a/common/research/common/datasets/roco/roco_dataset_builder.py b/polystar_cv/research/common/datasets/roco/roco_dataset_builder.py similarity index 100% rename from common/research/common/datasets/roco/roco_dataset_builder.py rename to polystar_cv/research/common/datasets/roco/roco_dataset_builder.py diff --git a/common/research/common/datasets/roco/roco_dataset_descriptor.py b/polystar_cv/research/common/datasets/roco/roco_dataset_descriptor.py similarity index 100% rename from common/research/common/datasets/roco/roco_dataset_descriptor.py rename to polystar_cv/research/common/datasets/roco/roco_dataset_descriptor.py diff --git a/common/research/common/datasets/roco/roco_datasets.py b/polystar_cv/research/common/datasets/roco/roco_datasets.py similarity index 95% rename from common/research/common/datasets/roco/roco_datasets.py rename to polystar_cv/research/common/datasets/roco/roco_datasets.py index d824c69..dbecdcd 100644 --- a/common/research/common/datasets/roco/roco_datasets.py +++ b/polystar_cv/research/common/datasets/roco/roco_datasets.py @@ -19,6 +19,9 @@ class ROCODatasetsMeta(type): def __iter__(cls) -> Iterator[ROCODatasetBuilder]: return (cls._make_builder_from_name(name) for name in dir(cls) if _is_builder_name(cls, name)) + def __len__(cls): + return sum(_is_builder_name(cls, name) for name in dir(cls)) + def union(cls) -> UnionLazyDataset[Path, ROCOAnnotation]: return UnionLazyDataset(cls, cls.name) diff --git a/common/tests/common/unittests/__init__.py b/polystar_cv/research/common/datasets/roco/zoo/__init__.py similarity index 100% rename from common/tests/common/unittests/__init__.py rename to polystar_cv/research/common/datasets/roco/zoo/__init__.py diff --git a/common/research/common/datasets/roco/zoo/dji.py b/polystar_cv/research/common/datasets/roco/zoo/dji.py similarity index 100% rename from common/research/common/datasets/roco/zoo/dji.py rename to polystar_cv/research/common/datasets/roco/zoo/dji.py diff --git a/common/research/common/datasets/roco/zoo/dji_zoomed.py b/polystar_cv/research/common/datasets/roco/zoo/dji_zoomed.py similarity index 100% rename from common/research/common/datasets/roco/zoo/dji_zoomed.py rename to polystar_cv/research/common/datasets/roco/zoo/dji_zoomed.py diff --git a/common/research/common/datasets/roco/zoo/roco_dataset_zoo.py b/polystar_cv/research/common/datasets/roco/zoo/roco_dataset_zoo.py similarity index 100% rename from common/research/common/datasets/roco/zoo/roco_dataset_zoo.py rename to polystar_cv/research/common/datasets/roco/zoo/roco_dataset_zoo.py diff --git a/common/research/common/datasets/roco/zoo/twitch.py b/polystar_cv/research/common/datasets/roco/zoo/twitch.py similarity index 100% rename from common/research/common/datasets/roco/zoo/twitch.py rename to polystar_cv/research/common/datasets/roco/zoo/twitch.py diff --git a/common/research/common/datasets/slice_dataset.py b/polystar_cv/research/common/datasets/slice_dataset.py similarity index 100% rename from common/research/common/datasets/slice_dataset.py rename to polystar_cv/research/common/datasets/slice_dataset.py diff --git a/common/research/common/datasets/transform_dataset.py b/polystar_cv/research/common/datasets/transform_dataset.py similarity index 100% rename from common/research/common/datasets/transform_dataset.py rename to polystar_cv/research/common/datasets/transform_dataset.py diff --git a/common/research/common/datasets/union_dataset.py b/polystar_cv/research/common/datasets/union_dataset.py similarity index 100% rename from common/research/common/datasets/union_dataset.py rename to polystar_cv/research/common/datasets/union_dataset.py diff --git a/common/research/common/gcloud/gcloud_storage.py b/polystar_cv/research/common/gcloud/gcloud_storage.py similarity index 58% rename from common/research/common/gcloud/gcloud_storage.py rename to polystar_cv/research/common/gcloud/gcloud_storage.py index d83de5b..4b47e20 100644 --- a/common/research/common/gcloud/gcloud_storage.py +++ b/polystar_cv/research/common/gcloud/gcloud_storage.py @@ -3,11 +3,12 @@ import shutil import tarfile from contextlib import contextmanager from enum import Enum +from io import FileIO from pathlib import Path, PurePath from tempfile import TemporaryDirectory from typing import Iterable, Optional -from google.cloud.storage import Blob, Bucket, Client +from google.cloud.storage import Bucket, Client from polystar.common.constants import PROJECT_DIR @@ -18,14 +19,19 @@ EXTENSIONS_TO_EXCLUDE = (".changes",) class GCStorage: def __init__(self, bucket_name: str): self.bucket_name = bucket_name - self.client: Optional[Client] = None + self._client: Optional[Client] = None self._bucket: Optional[Bucket] = None - self.url = f"https://console.cloud.google.com/storage/browser/{bucket_name}" + self.bucket_url = f"https://console.cloud.google.com/storage/browser/{bucket_name}" + self.storage_url = f"https://storage.cloud.google.com/{bucket_name}" def upload_file(self, local_path: Path, remote_path: Optional[PurePath] = None): - blob = self._make_remote_blob(local_path, remote_path) + remote_path = _make_remote_path(local_path, remote_path) + blob = self.bucket.blob(str(remote_path), chunk_size=10 * 1024 * 1024) blob.upload_from_filename(str(local_path), timeout=60 * 5) - logger.info(f"File file:///{local_path} uploaded") + logger.info( + f"File {local_path.name} uploaded to {self.bucket_url}/{remote_path.parent}. " + f"Download link: {self.storage_url}/{remote_path}" + ) def upload_directory(self, local_path: Path, extensions_to_exclude: Iterable[str] = EXTENSIONS_TO_EXCLUDE): extensions_to_exclude = set(extensions_to_exclude) @@ -35,14 +41,18 @@ class GCStorage: tar.add( str(local_path), arcname="", - exclude=lambda name: any(name.endswith(ext) for ext in extensions_to_exclude), + filter=lambda f: None if any(f.name.endswith(ext) for ext in extensions_to_exclude) else f, ) return self.upload_file(tar_path, _make_remote_path(local_path.with_suffix(".tar.gz"))) def download_file(self, local_path: Path, remote_path: Optional[PurePath] = None): - blob = self._make_remote_blob(local_path, remote_path) + local_path.parent.mkdir(exist_ok=True, parents=True) + remote_path = _make_remote_path(local_path, remote_path) + blob = self.bucket.get_blob(str(remote_path)) + if blob is None: + raise FileNotFoundError(f"{remote_path} is not on {self.bucket_url}") blob.download_to_filename(str(local_path), timeout=60 * 5) - logger.info(f"File file:///{local_path} downloaded") + logger.info(f"File {local_path.name} downloaded to file:///{local_path}") def download_file_if_missing(self, local_path: Path, remote_path: Optional[PurePath] = None): if not local_path.exists(): @@ -69,7 +79,7 @@ class GCStorage: self.download_directory(local_path) @contextmanager - def open(self, local_path: Path, mode: str): + def open(self, local_path: Path, mode: str) -> FileIO: if "r" in mode: self.download_file_if_missing(local_path) with local_path.open(mode) as f: @@ -82,23 +92,38 @@ class GCStorage: else: raise ValueError(f"mode {mode} is not supported") - def _make_remote_blob(self, local_path: Path, remote_path: Optional[PurePath]) -> Blob: - if remote_path is None: - remote_path = _make_remote_path(local_path) + @staticmethod + def open_from_str(remote_path: str, mode: str): + assert remote_path.startswith("gs://") + remote_path = remote_path[5:] + bucket_name, relative_path = remote_path.split("/", maxsplit=1) + return GCStorage(bucket_name).open(Path(PROJECT_DIR / relative_path), mode) + + def glob(self, local_path: Path, remote_path: Optional[PurePath] = None, extension: str = None) -> Iterable[Path]: + remote_path = _make_remote_path(local_path, remote_path) + blobs = self.client.list_blobs(self.bucket, prefix=str(remote_path),) + if extension is None: + return (PROJECT_DIR / b.name for b in blobs) + for blob in blobs: + if blob.name.endswith(extension): + yield PROJECT_DIR / blob.name - return self.bucket.blob(str(remote_path), chunk_size=10 * 1024 * 1024) + @property + def client(self) -> Client: + if self._client is None: + self._client = Client() + return self._client @property def bucket(self) -> Bucket: if self._bucket is not None: return self._bucket - self.client = Client() self._bucket = self.client.bucket(self.bucket_name) return self._bucket -def _make_remote_path(local_path: Path): - return local_path.relative_to(PROJECT_DIR) +def _make_remote_path(local_path: Path, remote_path: Optional[PurePath] = None) -> PurePath: + return remote_path or local_path.relative_to(PROJECT_DIR) class GCStorages(GCStorage, Enum): diff --git a/common/tests/common/unittests/object_validators/__init__.py b/polystar_cv/research/common/scripts/__init__.py similarity index 100% rename from common/tests/common/unittests/object_validators/__init__.py rename to polystar_cv/research/common/scripts/__init__.py diff --git a/common/research/common/scripts/construct_dataset_from_manual_annotation.py b/polystar_cv/research/common/scripts/construct_dataset_from_manual_annotation.py similarity index 100% rename from common/research/common/scripts/construct_dataset_from_manual_annotation.py rename to polystar_cv/research/common/scripts/construct_dataset_from_manual_annotation.py diff --git a/common/research/common/scripts/construct_twith_datasets_from_manual_annotation.py b/polystar_cv/research/common/scripts/construct_twith_datasets_from_manual_annotation.py similarity index 100% rename from common/research/common/scripts/construct_twith_datasets_from_manual_annotation.py rename to polystar_cv/research/common/scripts/construct_twith_datasets_from_manual_annotation.py diff --git a/common/research/common/scripts/correct_annotations.py b/polystar_cv/research/common/scripts/correct_annotations.py similarity index 100% rename from common/research/common/scripts/correct_annotations.py rename to polystar_cv/research/common/scripts/correct_annotations.py diff --git a/common/research/common/scripts/create_tensorflow_records.py b/polystar_cv/research/common/scripts/create_tensorflow_records.py similarity index 100% rename from common/research/common/scripts/create_tensorflow_records.py rename to polystar_cv/research/common/scripts/create_tensorflow_records.py diff --git a/common/research/common/scripts/extract_robots_views_from_video.py b/polystar_cv/research/common/scripts/extract_robots_views_from_video.py similarity index 100% rename from common/research/common/scripts/extract_robots_views_from_video.py rename to polystar_cv/research/common/scripts/extract_robots_views_from_video.py diff --git a/common/research/common/scripts/improve_roco_by_zooming.py b/polystar_cv/research/common/scripts/improve_roco_by_zooming.py similarity index 100% rename from common/research/common/scripts/improve_roco_by_zooming.py rename to polystar_cv/research/common/scripts/improve_roco_by_zooming.py diff --git a/common/research/common/scripts/make_twitch_chunks_to_annotate.py b/polystar_cv/research/common/scripts/make_twitch_chunks_to_annotate.py similarity index 100% rename from common/research/common/scripts/make_twitch_chunks_to_annotate.py rename to polystar_cv/research/common/scripts/make_twitch_chunks_to_annotate.py diff --git a/common/research/common/scripts/move_aerial_views.py b/polystar_cv/research/common/scripts/move_aerial_views.py similarity index 100% rename from common/research/common/scripts/move_aerial_views.py rename to polystar_cv/research/common/scripts/move_aerial_views.py diff --git a/common/research/common/scripts/visualize_dataset.py b/polystar_cv/research/common/scripts/visualize_dataset.py similarity index 100% rename from common/research/common/scripts/visualize_dataset.py rename to polystar_cv/research/common/scripts/visualize_dataset.py diff --git a/polystar_cv/research/common/utils/experiment_dir.py b/polystar_cv/research/common/utils/experiment_dir.py new file mode 100644 index 0000000..d0a1c57 --- /dev/null +++ b/polystar_cv/research/common/utils/experiment_dir.py @@ -0,0 +1,15 @@ +from pathlib import Path + +from polystar.common.utils.time import create_time_id +from research.common.constants import EVALUATION_DIR + + +def prompt_experiment_dir(project_name: str) -> Path: + experiment_name: str = input(f"Experiment name for {project_name}: ") + return make_experiment_dir(project_name, experiment_name) + + +def make_experiment_dir(project_name: str, experiment_name: str) -> Path: + experiment_dir = EVALUATION_DIR / project_name / f"{create_time_id()}_{experiment_name}" + experiment_dir.mkdir(exist_ok=True, parents=True) + return experiment_dir diff --git a/robots-at-robots/polystar/robots_at_robots/__init__.py b/polystar_cv/research/robots/__init__.py similarity index 100% rename from robots-at-robots/polystar/robots_at_robots/__init__.py rename to polystar_cv/research/robots/__init__.py diff --git a/robots-at-robots/research/robots_at_robots/__init__.py b/polystar_cv/research/robots/armor_color/__init__.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/__init__.py rename to polystar_cv/research/robots/armor_color/__init__.py diff --git a/polystar_cv/research/robots/armor_color/benchmarker.py b/polystar_cv/research/robots/armor_color/benchmarker.py new file mode 100644 index 0000000..79f0a91 --- /dev/null +++ b/polystar_cv/research/robots/armor_color/benchmarker.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from research.robots.armor_color.datasets import make_armor_color_datasets +from research.robots.armor_color.pipeline import ArmorColorPipeline +from research.robots.evaluation.benchmarker import Benchmarker + + +def make_armor_color_benchmarker(report_dir: Path, include_dji: bool = True) -> Benchmarker: + train_datasets, validation_datasets, test_datasets = make_armor_color_datasets() + return Benchmarker( + report_dir=report_dir, + classes=ArmorColorPipeline.classes, + train_datasets=train_datasets, + validation_datasets=validation_datasets, + test_datasets=test_datasets, + ) diff --git a/polystar_cv/research/robots/armor_color/datasets.py b/polystar_cv/research/robots/armor_color/datasets.py new file mode 100644 index 0000000..5c53b9e --- /dev/null +++ b/polystar_cv/research/robots/armor_color/datasets.py @@ -0,0 +1,49 @@ +from typing import List, Tuple + +from polystar.common.models.object import Armor, ArmorColor +from research.common.datasets.image_dataset import FileImageDataset +from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo +from research.robots.dataset.armor_value_dataset_generator import ArmorValueDatasetGenerator +from research.robots.dataset.armor_value_target_factory import ArmorValueTargetFactory + + +class ArmorColorTargetFactory(ArmorValueTargetFactory[ArmorColor]): + def from_str(self, label: str) -> ArmorColor: + return ArmorColor(label) + + def from_armor(self, armor: Armor) -> ArmorColor: + return armor.color + + +def make_armor_color_dataset_generator() -> ArmorValueDatasetGenerator[ArmorColor]: + return ArmorValueDatasetGenerator("colors", ArmorColorTargetFactory()) + + +def make_armor_color_datasets( + include_dji: bool = True, +) -> Tuple[List[FileImageDataset], List[FileImageDataset], List[FileImageDataset]]: + color_dataset_generator = make_armor_color_dataset_generator() + + train_roco_datasets = [ + ROCODatasetsZoo.TWITCH.T470150052, + ROCODatasetsZoo.TWITCH.T470152730, + ROCODatasetsZoo.TWITCH.T470153081, + ROCODatasetsZoo.TWITCH.T470158483, + ] + if include_dji: + train_roco_datasets.extend( + [ + ROCODatasetsZoo.DJI.FINAL, + ROCODatasetsZoo.DJI.CENTRAL_CHINA, + ROCODatasetsZoo.DJI.NORTH_CHINA, + ROCODatasetsZoo.DJI.SOUTH_CHINA, + ] + ) + + train_datasets, validation_datasets, test_datasets = color_dataset_generator.from_roco_datasets( + train_roco_datasets, + [ROCODatasetsZoo.TWITCH.T470149568, ROCODatasetsZoo.TWITCH.T470152289], + [ROCODatasetsZoo.TWITCH.T470152838, ROCODatasetsZoo.TWITCH.T470151286], + ) + + return train_datasets, validation_datasets, test_datasets diff --git a/polystar_cv/research/robots/armor_color/pipeline.py b/polystar_cv/research/robots/armor_color/pipeline.py new file mode 100644 index 0000000..50cd441 --- /dev/null +++ b/polystar_cv/research/robots/armor_color/pipeline.py @@ -0,0 +1,11 @@ +from polystar.common.models.object import ArmorColor +from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline +from polystar.common.pipeline.classification.keras_classification_pipeline import KerasClassificationPipeline + + +class ArmorColorPipeline(ClassificationPipeline): + enum = ArmorColor + + +class ArmorColorKerasPipeline(ArmorColorPipeline, KerasClassificationPipeline): + pass diff --git a/robots-at-robots/research/robots_at_robots/armor_color/__init__.py b/polystar_cv/research/robots/armor_color/scripts/__init__.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/armor_color/__init__.py rename to polystar_cv/research/robots/armor_color/scripts/__init__.py diff --git a/polystar_cv/research/robots/armor_color/scripts/benchmark.py b/polystar_cv/research/robots/armor_color/scripts/benchmark.py new file mode 100644 index 0000000..b76be32 --- /dev/null +++ b/polystar_cv/research/robots/armor_color/scripts/benchmark.py @@ -0,0 +1,64 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from nptyping import Array +from sklearn.linear_model import LogisticRegression + +from polystar.common.models.image import Image +from polystar.common.models.object import ArmorColor +from polystar.common.pipeline.classification.random_model import RandomClassifier +from polystar.common.pipeline.classification.rule_based_classifier import RuleBasedClassifierABC +from polystar.common.pipeline.featurizers.histogram_2d import Histogram2D +from polystar.common.pipeline.featurizers.histogram_blocs_2d import HistogramBlocs2D +from polystar.common.pipeline.pipe_abc import PipeABC +from polystar.common.pipeline.preprocessors.rgb_to_hsv import RGB2HSV +from research.common.utils.experiment_dir import prompt_experiment_dir +from research.robots.armor_color.benchmarker import make_armor_color_benchmarker +from research.robots.armor_color.pipeline import ArmorColorPipeline + + +@dataclass +class MeanChannels(PipeABC): + def transform_single(self, image: Image) -> Array[float, float, float]: + return image.mean(axis=(0, 1)) + + +class RedBlueComparisonClassifier(RuleBasedClassifierABC): + """A very simple model that compares the blue and red values obtained by the MeanChannels""" + + def predict_single(self, features: Array[float, float, float]) -> ArmorColor: + return ArmorColor.RED if features[0] >= features[2] else ArmorColor.BLUE + + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.info("Benchmarking") + + _report_dir: Path = prompt_experiment_dir("armor-color") + _benchmarker = make_armor_color_benchmarker(_report_dir, include_dji=False) + + _pipelines = [ + ArmorColorPipeline.from_pipes([MeanChannels(), RedBlueComparisonClassifier()], name="rb-comparison"), + ArmorColorPipeline.from_pipes([RandomClassifier()], name="random"), + ArmorColorPipeline.from_pipes( + [RGB2HSV(), Histogram2D(), LogisticRegression(max_iter=200)], name="hsv-hist-lr", + ), + ArmorColorPipeline.from_pipes( + [RGB2HSV(), HistogramBlocs2D(rows=1, cols=3), LogisticRegression(max_iter=200)], name="hsv-hist-blocs-lr", + ), + ArmorColorPipeline.from_pipes([Histogram2D(), LogisticRegression(max_iter=200)], name="rgb-hist-lr"), + # ArmorColorKerasPipeline.from_custom_cnn( + # logs_dir=str(_report_dir), + # input_size=16, + # conv_blocks=((32, 32), (64, 64)), + # dropout=0.5, + # dense_size=64, + # lr=7.2e-4, + # name="cnn", + # batch_size=128, + # steps_per_epoch="auto", + # ), + ] + + _benchmarker.benchmark(_pipelines) diff --git a/polystar_cv/research/robots/armor_color/scripts/hyper_tune_cnn.py b/polystar_cv/research/robots/armor_color/scripts/hyper_tune_cnn.py new file mode 100644 index 0000000..656459f --- /dev/null +++ b/polystar_cv/research/robots/armor_color/scripts/hyper_tune_cnn.py @@ -0,0 +1,35 @@ +import logging +import warnings +from pathlib import Path + +from optuna import Trial + +from research.common.utils.experiment_dir import make_experiment_dir +from research.robots.armor_color.benchmarker import make_armor_color_benchmarker +from research.robots.armor_color.pipeline import ArmorColorKerasPipeline +from research.robots.evaluation.hyper_tuner import HyperTuner + + +def cnn_pipeline_factory(report_dir: Path, trial: Trial) -> ArmorColorKerasPipeline: + return ArmorColorKerasPipeline.from_custom_cnn( + input_size=32, + conv_blocks=((32, 32), (64, 64)), + logs_dir=str(report_dir), + dropout=trial.suggest_uniform("dropout", 0, 0.99), + lr=trial.suggest_loguniform("lr", 1e-5, 1e-1), + dense_size=2 ** round(trial.suggest_discrete_uniform("dense_size_log2", 3, 10, 1)), + batch_size=64, + steps_per_epoch="auto", + verbose=0, + ) + + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + logging.info("Hyperparameter tuning for CNN pipeline on color task") + HyperTuner(make_armor_color_benchmarker(make_experiment_dir("armor-color", "cnn_tuning"), include_dji=False)).tune( + cnn_pipeline_factory, n_trials=50 + ) diff --git a/robots-at-robots/research/robots_at_robots/armor_digit/__init__.py b/polystar_cv/research/robots/armor_digit/__init__.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/armor_digit/__init__.py rename to polystar_cv/research/robots/armor_digit/__init__.py diff --git a/polystar_cv/research/robots/armor_digit/armor_digit_dataset.py b/polystar_cv/research/robots/armor_digit/armor_digit_dataset.py new file mode 100644 index 0000000..f7f7437 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/armor_digit_dataset.py @@ -0,0 +1,62 @@ +from itertools import islice +from typing import List, Set, Tuple + +from polystar.common.filters.exclude_filter import ExcludeFilter +from polystar.common.models.object import Armor, ArmorDigit +from research.common.datasets.image_dataset import FileImageDataset +from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo +from research.robots.dataset.armor_value_dataset_generator import ArmorValueDatasetGenerator +from research.robots.dataset.armor_value_target_factory import ArmorValueTargetFactory + +VALID_NUMBERS_2021: Set[int] = {1, 3, 4} # University League + + +def make_armor_digit_dataset_generator() -> ArmorValueDatasetGenerator[ArmorDigit]: + return ArmorValueDatasetGenerator("digits", ArmorDigitTargetFactory(), ExcludeFilter({ArmorDigit.OUTDATED})) + + +def default_armor_digit_datasets() -> Tuple[List[FileImageDataset], List[FileImageDataset], List[FileImageDataset]]: + digit_dataset_generator = make_armor_digit_dataset_generator() + train_datasets, validation_datasets, test_datasets = digit_dataset_generator.from_roco_datasets( + [ + ROCODatasetsZoo.TWITCH.T470150052, + ROCODatasetsZoo.TWITCH.T470152730, + ROCODatasetsZoo.TWITCH.T470153081, + ROCODatasetsZoo.TWITCH.T470158483, + ROCODatasetsZoo.DJI.FINAL, + ROCODatasetsZoo.DJI.CENTRAL_CHINA, + ROCODatasetsZoo.DJI.NORTH_CHINA, + ROCODatasetsZoo.DJI.SOUTH_CHINA, + ], + [ROCODatasetsZoo.TWITCH.T470149568, ROCODatasetsZoo.TWITCH.T470152289], + [ROCODatasetsZoo.TWITCH.T470152838, ROCODatasetsZoo.TWITCH.T470151286], + ) + # train_datasets.append( + # digit_dataset_generator.from_roco_dataset(ROCODatasetsZoo.DJI.FINAL).to_file_images() + # # .cap(2133 + 1764 + 1436) + # # .skip(2133 + 176 + 1436) + # # .cap(5_000) + # .build() + # ) + return train_datasets, validation_datasets, test_datasets + + +class ArmorDigitTargetFactory(ArmorValueTargetFactory[ArmorDigit]): + def from_str(self, label: str) -> ArmorDigit: + n = int(label) + + if n in VALID_NUMBERS_2021: # CHANGING + return ArmorDigit(n - (n >= 3)) # hacky, but digit 2 is absent + + return ArmorDigit.OUTDATED + + def from_armor(self, armor: Armor) -> ArmorDigit: + return ArmorDigit(armor.number) if armor.number else ArmorDigit.UNKNOWN + + +if __name__ == "__main__": + _roco_dataset_builder = ROCODatasetsZoo.DJI.CENTRAL_CHINA + _armor_digit_dataset = make_armor_digit_dataset_generator().from_roco_dataset(_roco_dataset_builder) + + for p, c, _name in islice(_armor_digit_dataset, 20, 30): + print(p, c, _name) diff --git a/polystar_cv/research/robots/armor_digit/digit_benchmarker.py b/polystar_cv/research/robots/armor_digit/digit_benchmarker.py new file mode 100644 index 0000000..96473be --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/digit_benchmarker.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from research.robots.armor_digit.armor_digit_dataset import default_armor_digit_datasets +from research.robots.armor_digit.pipeline import ArmorDigitPipeline +from research.robots.evaluation.benchmarker import Benchmarker + + +def make_default_digit_benchmarker(report_dir: Path) -> Benchmarker: + train_datasets, validation_datasets, test_datasets = default_armor_digit_datasets() + return Benchmarker( + report_dir=report_dir, + train_datasets=train_datasets, + validation_datasets=validation_datasets, + test_datasets=test_datasets, + classes=ArmorDigitPipeline.classes, + ) diff --git a/robots-at-robots/research/robots_at_robots/dataset/__init__.py b/polystar_cv/research/robots/armor_digit/gcloud/__init__.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/dataset/__init__.py rename to polystar_cv/research/robots/armor_digit/gcloud/__init__.py diff --git a/polystar_cv/research/robots/armor_digit/gcloud/gather_performances.py b/polystar_cv/research/robots/armor_digit/gcloud/gather_performances.py new file mode 100644 index 0000000..67e0b93 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/gather_performances.py @@ -0,0 +1,41 @@ +import logging +import pickle +from pathlib import Path +from typing import List + +from polystar.common.models.object import ArmorDigit +from polystar.common.utils.iterable_utils import flatten +from research.common.constants import EVALUATION_DIR +from research.common.gcloud.gcloud_storage import GCStorages +from research.robots.evaluation.metrics.f1 import F1Metric +from research.robots.evaluation.performance import ClassificationPerformances +from research.robots.evaluation.reporter import ImagePipelineEvaluationReporter + + +def load_performances(performances_paths: List[Path]) -> ClassificationPerformances: + return ClassificationPerformances(flatten(pickle.loads(perf_path.read_bytes()) for perf_path in performances_paths)) + + +def gather_performances(task_name: str, job_id: str): + logging.info(f"gathering performances for {job_id} on task {task_name}") + experiment_dir = EVALUATION_DIR / task_name / job_id + performances_paths = download_performances(experiment_dir) + performances = load_performances(performances_paths) + ImagePipelineEvaluationReporter( + report_dir=EVALUATION_DIR / task_name / job_id, classes=list(ArmorDigit), other_metrics=[F1Metric()] + ).report(performances) + + +def download_performances(experiment_dir: Path) -> List[Path]: + performances_paths = list(GCStorages.DEV.glob(experiment_dir, extension=".pkl")) + logging.info(f"Found {len(performances_paths)} performances") + for performance_path in performances_paths: + GCStorages.DEV.download_file_if_missing(performance_path) + return performances_paths + + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + + gather_performances("armor-digit", "cnn_20201220_224525") + gather_performances("armor-digit", "vgg16_20201220_224417") diff --git a/polystar_cv/research/robots/armor_digit/gcloud/hptuning_config.yaml b/polystar_cv/research/robots/armor_digit/gcloud/hptuning_config.yaml new file mode 100644 index 0000000..0a9fd93 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/hptuning_config.yaml @@ -0,0 +1,33 @@ +trainingInput: + pythonVersion: "3.7" + runtimeVersion: "2.3" + scaleTier: BASIC_GPU + region: europe-west6 + + hyperparameters: + goal: MAXIMIZE + hyperparameterMetricTag: val_accuracy + maxTrials: 50 + maxParallelTrials: 5 + params: + - parameterName: lr + type: DOUBLE + minValue: 0.00001 + maxValue: 0.1 + scaleType: UNIT_LOG_SCALE + - parameterName: dropout + type: DOUBLE + minValue: 0 + maxValue: .99 + - parameterName: dense-size + type: DISCRETE + discreteValues: + - 16 + - 32 + - 64 + - 128 + - 256 + - 512 + - 1024 + - 2048 + scaleType: UNIT_LOG_SCALE diff --git a/polystar_cv/research/robots/armor_digit/gcloud/train.py b/polystar_cv/research/robots/armor_digit/gcloud/train.py new file mode 100644 index 0000000..fecfaa7 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/train.py @@ -0,0 +1,19 @@ +import pickle +from os.path import join + +from research.common.gcloud.gcloud_storage import GCStorage +from research.robots.armor_digit.armor_digit_dataset import default_armor_digit_datasets +from research.robots.armor_digit.pipeline import ArmorDigitPipeline +from research.robots.evaluation.evaluator import ImageClassificationPipelineEvaluator +from research.robots.evaluation.trainer import ImageClassificationPipelineTrainer + + +def train_evaluate_digit_pipeline(pipeline: ArmorDigitPipeline, job_dir: str): + train_datasets, val_datasets, test_datasets = default_armor_digit_datasets() + trainer = ImageClassificationPipelineTrainer(train_datasets, val_datasets) + evaluator = ImageClassificationPipelineEvaluator(train_datasets, val_datasets, test_datasets) + + trainer.train_pipeline(pipeline) + + with GCStorage.open_from_str(join(job_dir, pipeline.name, "perfs.pkl"), "wb") as f: + pickle.dump(evaluator.evaluate_pipeline(pipeline), f) diff --git a/polystar_cv/research/robots/armor_digit/gcloud/train_cnn.py b/polystar_cv/research/robots/armor_digit/gcloud/train_cnn.py new file mode 100644 index 0000000..c9c7437 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/train_cnn.py @@ -0,0 +1,29 @@ +import logging +import warnings +from argparse import ArgumentParser + +from research.robots.armor_digit.gcloud.train import train_evaluate_digit_pipeline +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + _parser = ArgumentParser() + _parser.add_argument("--job-dir", type=str, required=True) + _parser.add_argument("--lr", type=float, required=True) + _parser.add_argument("--dropout", type=float, required=True) + _parser.add_argument("--dense-size", type=int, required=True) + _args = _parser.parse_args() + + _pipeline = ArmorDigitKerasPipeline.from_custom_cnn( + input_size=32, + conv_blocks=((32, 32), (64, 64)), + logs_dir=_args.job_dir, + lr=_args.lr, + dense_size=_args.dense_size, + dropout=_args.dropout, + ) + + train_evaluate_digit_pipeline(_pipeline, _args.job_dir) diff --git a/polystar_cv/research/robots/armor_digit/gcloud/train_vgg16.py b/polystar_cv/research/robots/armor_digit/gcloud/train_vgg16.py new file mode 100644 index 0000000..b8cc89e --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/train_vgg16.py @@ -0,0 +1,31 @@ +import logging +import warnings +from argparse import ArgumentParser + +from tensorflow.python.keras.applications.vgg16 import VGG16 + +from research.robots.armor_digit.gcloud.train import train_evaluate_digit_pipeline +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + _parser = ArgumentParser() + _parser.add_argument("--job-dir", type=str, required=True) + _parser.add_argument("--lr", type=float, required=True) + _parser.add_argument("--dropout", type=float, required=True) + _parser.add_argument("--dense-size", type=int, required=True) + _args = _parser.parse_args() + + _pipeline = ArmorDigitKerasPipeline.from_transfer_learning( + model_factory=VGG16, + logs_dir=_args.job_dir, + input_size=32, + lr=_args.lr, + dense_size=_args.dense_size, + dropout=_args.dropout, + ) + + train_evaluate_digit_pipeline(_pipeline, _args.job_dir) diff --git a/polystar_cv/research/robots/armor_digit/gcloud/train_xception.py b/polystar_cv/research/robots/armor_digit/gcloud/train_xception.py new file mode 100644 index 0000000..c3ce07b --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/train_xception.py @@ -0,0 +1,31 @@ +import logging +import warnings +from argparse import ArgumentParser + +from tensorflow.python.keras.applications.xception import Xception + +from research.robots.armor_digit.gcloud.train import train_evaluate_digit_pipeline +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + _parser = ArgumentParser() + _parser.add_argument("--job-dir", type=str, required=True) + _parser.add_argument("--lr", type=float, required=True) + _parser.add_argument("--dropout", type=float, required=True) + _parser.add_argument("--dense-size", type=int, required=True) + _args = _parser.parse_args() + + _pipeline = ArmorDigitKerasPipeline.from_transfer_learning( + model_factory=Xception, + logs_dir=_args.job_dir, + input_size=72, + lr=_args.lr, + dense_size=_args.dense_size, + dropout=_args.dropout, + ) + + train_evaluate_digit_pipeline(_pipeline, _args.job_dir) diff --git a/polystar_cv/research/robots/armor_digit/gcloud/trainer.sh b/polystar_cv/research/robots/armor_digit/gcloud/trainer.sh new file mode 100644 index 0000000..d4fe0ec --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/gcloud/trainer.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -e + +cd ../../../../../ + +# 1. Params +task_name="armor-digit" + +read -rp "Experiment name:" experiment_name +job_id=${experiment_name}_$(date +'%Y%m%d_%H%M%S') +job_dir="gs://poly-cv-dev/experiments/$task_name/$job_id" +author=$(git config user.name | tr " " - | tr '[:upper:]' '[:lower:]') +echo Running job "$job_id" for task "$task_name" by "$author" + +# 2. build source +poetry build -f wheel + +# 3. start job +gcloud ai-platform jobs submit training "${job_id}" \ + --config polystar_cv/research/robots/armor_digit/gcloud/hptuning_config.yaml \ + --job-dir="${job_dir}" \ + --packages ./dist/polystar_cv-0.2.0-py3-none-any.whl \ + --module-name=research.robots.armor_digit.gcloud.train_cnn \ + --labels task=${task_name},author="${author}" + +# 4. logs +echo "logs: https://console.cloud.google.com/logs/query;query=resource.labels.job_id%3D%22${job_id}%22?project=polystar-cv" +echo "job: https://console.cloud.google.com/ai-platform/jobs/${job_id}/charts/cpu?project=polystar-cv" + +tensorboard --logdir="${job_dir}" diff --git a/polystar_cv/research/robots/armor_digit/pipeline.py b/polystar_cv/research/robots/armor_digit/pipeline.py new file mode 100644 index 0000000..24467ba --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/pipeline.py @@ -0,0 +1,13 @@ +from polystar.common.models.object import ArmorDigit +from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline +from polystar.common.pipeline.classification.keras_classification_pipeline import KerasClassificationPipeline +from polystar.common.utils.registry import registry + + +class ArmorDigitPipeline(ClassificationPipeline): + enum = ArmorDigit + + +@registry.register() +class ArmorDigitKerasPipeline(ArmorDigitPipeline, KerasClassificationPipeline): + pass diff --git a/robots-at-robots/research/robots_at_robots/evaluation/__init__.py b/polystar_cv/research/robots/armor_digit/scripts/__init__.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/evaluation/__init__.py rename to polystar_cv/research/robots/armor_digit/scripts/__init__.py diff --git a/polystar_cv/research/robots/armor_digit/scripts/benchmark.py b/polystar_cv/research/robots/armor_digit/scripts/benchmark.py new file mode 100644 index 0000000..2dde34d --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/scripts/benchmark.py @@ -0,0 +1,56 @@ +import logging +import warnings +from pathlib import Path + +from polystar.common.constants import PROJECT_DIR +from polystar.common.pipeline.classification.random_model import RandomClassifier +from polystar.common.utils.serialization import pkl_load +from research.common.utils.experiment_dir import prompt_experiment_dir +from research.robots.armor_digit.digit_benchmarker import make_default_digit_benchmarker +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline, ArmorDigitPipeline + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + _report_dir: Path = prompt_experiment_dir("armor-digit") + + logging.info(f"Running benchmarking {_report_dir.name}") + + _benchmarker = make_default_digit_benchmarker(report_dir=_report_dir) + + _random_pipeline = ArmorDigitPipeline.from_pipes([RandomClassifier()], name="random") + _cnn_pipeline = ArmorDigitKerasPipeline.from_custom_cnn( + input_size=32, + conv_blocks=((32, 32), (64, 64)), + logs_dir=str(_report_dir), + dropout=0.66, + lr=0.00078, + dense_size=1024, + name="cnn", + ) + # _vgg16_pipeline = ArmorDigitKerasPipeline.from_transfer_learning( + # input_size=32, logs_dir=_report_dir, dropout=0, lr=0.00021, dense_size=64, model_factory=VGG16 + # ) + + _vgg16_pipeline = pkl_load( + PROJECT_DIR / "pipelines/armor-digit/20201225_131957_vgg16/VGG16 (32) - lr 2.1e-04 - drop 0.pkl" + ) + _vgg16_pipeline.name = "vgg16_tl" + + _distiled_vgg16_into_cnn_pipeline = ArmorDigitKerasPipeline.from_distillation( + teacher_pipeline=_vgg16_pipeline, + conv_blocks=((32, 32), (64, 64)), + logs_dir=_report_dir, + dropout=0.63, + lr=0.000776, + dense_size=1024, + temperature=41.2, + name="cnn_kd", + ) + + _benchmarker.benchmark( + pipelines=[_random_pipeline, _distiled_vgg16_into_cnn_pipeline, _cnn_pipeline], + trained_pipelines=[_vgg16_pipeline], + ) diff --git a/robots-at-robots/research/robots_at_robots/armor_digit/clean_datasets.py b/polystar_cv/research/robots/armor_digit/scripts/clean_datasets.py similarity index 62% rename from robots-at-robots/research/robots_at_robots/armor_digit/clean_datasets.py rename to polystar_cv/research/robots/armor_digit/scripts/clean_datasets.py index f78124e..39a1f9c 100644 --- a/robots-at-robots/research/robots_at_robots/armor_digit/clean_datasets.py +++ b/polystar_cv/research/robots/armor_digit/scripts/clean_datasets.py @@ -1,6 +1,6 @@ from research.common.dataset.cleaning.dataset_cleaner_app import DatasetCleanerApp from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.armor_digit.armor_digit_dataset import make_armor_digit_dataset_generator +from research.robots.armor_digit.armor_digit_dataset import make_armor_digit_dataset_generator if __name__ == "__main__": # _roco_dataset = ROCODatasetsZoo.TWITCH.T470149568 @@ -16,22 +16,7 @@ if __name__ == "__main__": _roco_dataset = ROCODatasetsZoo.DJI.FINAL _armor_digit_dataset = ( - make_armor_digit_dataset_generator() - .from_roco_dataset(_roco_dataset) - .skip( - (1009 - 117) - + (1000 - 86) - + (1000 - 121) - + (1000 - 138) - + (1000 - 137) - + (1000 - 154) - + (1000 - 180) - + (1000 - 160) - + (1000 - 193) - + (1000 - 80) - + (1000 - 154) - ) - .cap(1000) + make_armor_digit_dataset_generator().from_roco_dataset(_roco_dataset).skip(2133 + 1764 + 1436).cap(1000) ) DatasetCleanerApp(_armor_digit_dataset, invalidate_key="u", validate_key="h").run() diff --git a/polystar_cv/research/robots/armor_digit/scripts/hyper_tune_cnn.py b/polystar_cv/research/robots/armor_digit/scripts/hyper_tune_cnn.py new file mode 100644 index 0000000..26ce661 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/scripts/hyper_tune_cnn.py @@ -0,0 +1,32 @@ +import logging +import warnings +from pathlib import Path + +from optuna import Trial + +from research.common.utils.experiment_dir import make_experiment_dir +from research.robots.armor_digit.digit_benchmarker import make_default_digit_benchmarker +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline, ArmorDigitPipeline +from research.robots.evaluation.hyper_tuner import HyperTuner + + +def cnn_pipeline_factory(report_dir: Path, trial: Trial) -> ArmorDigitPipeline: + return ArmorDigitKerasPipeline.from_custom_cnn( + input_size=32, + conv_blocks=((32, 32), (64, 64)), + logs_dir=str(report_dir), + dropout=trial.suggest_uniform("dropout", 0, 0.99), + lr=trial.suggest_loguniform("lr", 1e-5, 1e-1), + dense_size=2 ** round(trial.suggest_discrete_uniform("dense_size_log2", 3, 10, 1)), + ) + + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + logging.info("Hyperparameter tuning for CNN pipeline on digit task") + HyperTuner(make_default_digit_benchmarker(make_experiment_dir("armor-digit", "cnn_tuning"))).tune( + cnn_pipeline_factory, n_trials=50 + ) diff --git a/polystar_cv/research/robots/armor_digit/scripts/hyper_tune_distiled_vgg16_into_cnn.py b/polystar_cv/research/robots/armor_digit/scripts/hyper_tune_distiled_vgg16_into_cnn.py new file mode 100644 index 0000000..3b035e7 --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/scripts/hyper_tune_distiled_vgg16_into_cnn.py @@ -0,0 +1,39 @@ +import logging +import warnings +from pathlib import Path + +from optuna import Trial + +from polystar.common.constants import PROJECT_DIR +from polystar.common.utils.serialization import pkl_load +from research.common.utils.experiment_dir import make_experiment_dir +from research.robots.armor_digit.digit_benchmarker import make_default_digit_benchmarker +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline, ArmorDigitPipeline +from research.robots.evaluation.hyper_tuner import HyperTuner + + +class DistilledPipelineFactory: + def __init__(self, teacher_name: str): + self.teacher: ArmorDigitKerasPipeline = pkl_load(PROJECT_DIR / "pipelines/armor-digit" / teacher_name) + + def __call__(self, report_dir: Path, trial: Trial) -> ArmorDigitPipeline: + return ArmorDigitKerasPipeline.from_distillation( + teacher_pipeline=self.teacher, + conv_blocks=((32, 32), (64, 64)), + logs_dir=str(report_dir), + dropout=trial.suggest_uniform("dropout", 0, 0.99), + lr=trial.suggest_loguniform("lr", 5e-4, 1e-3), + dense_size=1024, # 2 ** round(trial.suggest_discrete_uniform("dense_size_log2", 3, 10, 1)), + temperature=trial.suggest_loguniform("temperature", 1, 100), + ) + + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + + logging.info("Hyperparameter tuning for VGG16 distilation into CNN pipeline on digit task") + HyperTuner(make_default_digit_benchmarker(make_experiment_dir("armor-digit", "distillation_tuning"))).tune( + DistilledPipelineFactory("20201225_131957_vgg16/VGG16 (32) - lr 2.1e-04 - drop 0.pkl"), n_trials=50 + ) diff --git a/polystar_cv/research/robots/armor_digit/scripts/train_vgg16.py b/polystar_cv/research/robots/armor_digit/scripts/train_vgg16.py new file mode 100644 index 0000000..650aabc --- /dev/null +++ b/polystar_cv/research/robots/armor_digit/scripts/train_vgg16.py @@ -0,0 +1,37 @@ +import logging +import warnings + +from tensorflow.python.keras.applications.vgg16 import VGG16 + +from polystar.common.constants import PROJECT_DIR +from polystar.common.utils.serialization import pkl_dump +from polystar.common.utils.time import create_time_id +from research.robots.armor_digit.digit_benchmarker import make_default_digit_benchmarker +from research.robots.armor_digit.pipeline import ArmorDigitKerasPipeline + +PIPELINES_DIR = PROJECT_DIR / "pipelines" + +if __name__ == "__main__": + logging.getLogger().setLevel("INFO") + logging.getLogger("tensorflow").setLevel("ERROR") + warnings.filterwarnings("ignore") + logging.info("Training vgg16") + + _training_dir = PIPELINES_DIR / "armor-digit" / f"{create_time_id()}_vgg16_full_dset" + + _vgg16_pipeline = ArmorDigitKerasPipeline.from_transfer_learning( + input_size=32, + logs_dir=str(_training_dir), + dropout=0, + lr=0.00021, + dense_size=64, + model_factory=VGG16, + verbose=1, + ) + + logging.info(f"Run `tensorboard --logdir={_training_dir}` for realtime logs") + + _benchmarker = make_default_digit_benchmarker(_training_dir) + _benchmarker.benchmark([_vgg16_pipeline]) + + pkl_dump(_vgg16_pipeline, _training_dir / _vgg16_pipeline.name) diff --git a/robots-at-robots/research/robots_at_robots/evaluation/metrics/__init__.py b/polystar_cv/research/robots/dataset/__init__.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/evaluation/metrics/__init__.py rename to polystar_cv/research/robots/dataset/__init__.py diff --git a/robots-at-robots/research/robots_at_robots/dataset/armor_dataset_factory.py b/polystar_cv/research/robots/dataset/armor_dataset_factory.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/dataset/armor_dataset_factory.py rename to polystar_cv/research/robots/dataset/armor_dataset_factory.py diff --git a/robots-at-robots/research/robots_at_robots/dataset/armor_value_dataset_cache.py b/polystar_cv/research/robots/dataset/armor_value_dataset_cache.py similarity index 73% rename from robots-at-robots/research/robots_at_robots/dataset/armor_value_dataset_cache.py rename to polystar_cv/research/robots/dataset/armor_value_dataset_cache.py index 68f1b15..b826aa2 100644 --- a/robots-at-robots/research/robots_at_robots/dataset/armor_value_dataset_cache.py +++ b/polystar_cv/research/robots/dataset/armor_value_dataset_cache.py @@ -3,6 +3,8 @@ from pathlib import Path from shutil import rmtree from typing import ClassVar, Generic, Optional +from google.cloud.exceptions import Forbidden + from polystar.common.models.image import Image, save_image from polystar.common.utils.misc import identity from polystar.common.utils.time import create_time_id @@ -10,8 +12,9 @@ from polystar.common.utils.tqdm import smart_tqdm from research.common.datasets.lazy_dataset import LazyDataset, TargetT from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder from research.common.datasets.transform_dataset import TransformDataset -from research.robots_at_robots.dataset.armor_dataset_factory import ArmorDataset -from research.robots_at_robots.dataset.armor_value_target_factory import ArmorValueTargetFactory +from research.common.gcloud.gcloud_storage import GCStorages +from research.robots.dataset.armor_dataset_factory import ArmorDataset +from research.robots.dataset.armor_value_target_factory import ArmorValueTargetFactory class ArmorValueDatasetCache(Generic[TargetT]): @@ -30,11 +33,23 @@ class ArmorValueDatasetCache(Generic[TargetT]): self.roco_dataset_builder = roco_dataset_builder self.lock_file = cache_dir / ".lock" - def generate_if_needed(self): + self.cache_dir.mkdir(parents=True, exist_ok=True) + + def generate_or_download_if_needed(self): cause = self._get_generation_cause() if cause is None: return self._clean_cache_dir() + try: + GCStorages.DEV.download_directory(self.cache_dir) + cause = self._get_generation_cause() + if cause is None: + return + self._clean_cache_dir() + except FileNotFoundError: + cause += " and not on gcloud" + except Forbidden: + pass self.save(self._generate(), cause) def _clean_cache_dir(self): @@ -44,7 +59,7 @@ class ArmorValueDatasetCache(Generic[TargetT]): def save(self, dataset: LazyDataset[Image, TargetT], cause: str): desc = f"Generating dataset {self.dataset_name} (cause: {cause})" for img, target, name in smart_tqdm(dataset, desc=desc, unit="img"): - save_image(img, self.cache_dir / f"{name}-{target}.jpg") + save_image(img, self.cache_dir / f"{name}-{str(target)}.jpg") self.lock_file.write_text(json.dumps({"version": self.VERSION, "date": create_time_id()})) def _generate(self) -> LazyDataset[Image, TargetT]: diff --git a/robots-at-robots/research/robots_at_robots/dataset/armor_value_dataset_generator.py b/polystar_cv/research/robots/dataset/armor_value_dataset_generator.py similarity index 70% rename from robots-at-robots/research/robots_at_robots/dataset/armor_value_dataset_generator.py rename to polystar_cv/research/robots/dataset/armor_value_dataset_generator.py index 4aafd34..a296d9c 100644 --- a/robots-at-robots/research/robots_at_robots/dataset/armor_value_dataset_generator.py +++ b/polystar_cv/research/robots/dataset/armor_value_dataset_generator.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Generic, List +from typing import Generic, Iterable, List from polystar.common.filters.exclude_filter import ExcludeFilter from polystar.common.filters.filter_abc import FilterABC @@ -9,8 +9,8 @@ from research.common.datasets.image_dataset import FileImageDataset from research.common.datasets.image_file_dataset_builder import DirectoryDatasetBuilder from research.common.datasets.lazy_dataset import TargetT from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder -from research.robots_at_robots.dataset.armor_value_dataset_cache import ArmorValueDatasetCache -from research.robots_at_robots.dataset.armor_value_target_factory import ArmorValueTargetFactory +from research.robots.dataset.armor_value_dataset_cache import ArmorValueDatasetCache +from research.robots.dataset.armor_value_target_factory import ArmorValueTargetFactory class ExcludeFilesFilter(ExcludeFilter[Path]): @@ -30,14 +30,21 @@ class ArmorValueDatasetGenerator(Generic[TargetT]): self.targets_filter = targets_filter or PassThroughFilter() # FIXME signature inconsistency across methods - def from_roco_datasets(self, roco_datasets: List[ROCODatasetBuilder]) -> List[FileImageDataset[TargetT]]: - return [self.from_roco_dataset(roco_dataset).to_file_images().build() for roco_dataset in roco_datasets] + def from_roco_datasets( + self, *roco_datasets_list: List[ROCODatasetBuilder] + ) -> Iterable[List[FileImageDataset[TargetT]]]: + return ( + [self.from_roco_dataset(roco_dataset).to_file_images().build() for roco_dataset in roco_datasets] + for roco_datasets in roco_datasets_list + ) def from_roco_dataset(self, roco_dataset_builder: ROCODatasetBuilder) -> DirectoryDatasetBuilder[TargetT]: cache_dir = roco_dataset_builder.main_dir / self.task_name dataset_name = roco_dataset_builder.name - ArmorValueDatasetCache(roco_dataset_builder, cache_dir, dataset_name, self.target_factory).generate_if_needed() + ArmorValueDatasetCache( + roco_dataset_builder, cache_dir, dataset_name, self.target_factory + ).generate_or_download_if_needed() return ( DirectoryDatasetBuilder(cache_dir, self.target_factory.from_file, dataset_name) diff --git a/robots-at-robots/research/robots_at_robots/dataset/armor_value_target_factory.py b/polystar_cv/research/robots/dataset/armor_value_target_factory.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/dataset/armor_value_target_factory.py rename to polystar_cv/research/robots/dataset/armor_value_target_factory.py diff --git a/robots-at-runes/polystar/robots_at_runes/__init__.py b/polystar_cv/research/robots/demos/__init__.py similarity index 100% rename from robots-at-runes/polystar/robots_at_runes/__init__.py rename to polystar_cv/research/robots/demos/__init__.py diff --git a/robots-at-robots/research/robots_at_robots/demos/demo_infer.py b/polystar_cv/research/robots/demos/demo_infer.py similarity index 89% rename from robots-at-robots/research/robots_at_robots/demos/demo_infer.py rename to polystar_cv/research/robots/demos/demo_infer.py index 40c567f..51c1cc2 100644 --- a/robots-at-robots/research/robots_at_robots/demos/demo_infer.py +++ b/polystar_cv/research/robots/demos/demo_infer.py @@ -1,12 +1,12 @@ +from polystar.common.dependency_injection import make_injector from polystar.common.models.label_map import LabelMap from polystar.common.target_pipeline.detected_objects.detected_objects_factory import DetectedObjectFactory from polystar.common.target_pipeline.objects_detectors.tf_model_objects_detector import TFModelObjectsDetector from polystar.common.target_pipeline.objects_validators.confidence_object_validator import ConfidenceObjectValidator from polystar.common.utils.tensorflow import patch_tf_v2 from polystar.common.view.plt_results_viewer import PltResultViewer -from polystar.robots_at_robots.dependency_injection import make_injector from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.demos.utils import load_tf_model +from research.robots.demos.utils import load_tf_model if __name__ == "__main__": patch_tf_v2() diff --git a/robots-at-robots/research/robots_at_robots/demos/demo_pipeline.py b/polystar_cv/research/robots/demos/demo_pipeline.py similarity index 90% rename from robots-at-robots/research/robots_at_robots/demos/demo_pipeline.py rename to polystar_cv/research/robots/demos/demo_pipeline.py index c3a4d34..ff5475b 100644 --- a/robots-at-robots/research/robots_at_robots/demos/demo_pipeline.py +++ b/polystar_cv/research/robots/demos/demo_pipeline.py @@ -1,6 +1,7 @@ import cv2 from polystar.common.communication.print_target_sender import PrintTargetSender +from polystar.common.dependency_injection import make_injector from polystar.common.models.camera import Camera from polystar.common.models.label_map import LabelMap from polystar.common.target_pipeline.armors_descriptors.armors_color_descriptor import ArmorsColorDescriptor @@ -14,14 +15,10 @@ from polystar.common.target_pipeline.target_factories.ratio_simple_target_factor from polystar.common.target_pipeline.target_pipeline import NoTargetFoundException from polystar.common.utils.tensorflow import patch_tf_v2 from polystar.common.view.plt_results_viewer import PltResultViewer -from polystar.robots_at_robots.dependency_injection import make_injector from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.armor_color.benchmark import ( - ArmorColorPipeline, - MeanChannels, - RedBlueComparisonClassifier, -) -from research.robots_at_robots.demos.utils import load_tf_model +from research.robots.armor_color.pipeline import ArmorColorPipeline +from research.robots.armor_color.scripts.benchmark import MeanChannels, RedBlueComparisonClassifier +from research.robots.demos.utils import load_tf_model if __name__ == "__main__": patch_tf_v2() diff --git a/robots-at-robots/research/robots_at_robots/demos/demo_pipeline_camera.py b/polystar_cv/research/robots/demos/demo_pipeline_camera.py similarity index 97% rename from robots-at-robots/research/robots_at_robots/demos/demo_pipeline_camera.py rename to polystar_cv/research/robots/demos/demo_pipeline_camera.py index 0db0dd1..660e09e 100644 --- a/robots-at-robots/research/robots_at_robots/demos/demo_pipeline_camera.py +++ b/polystar_cv/research/robots/demos/demo_pipeline_camera.py @@ -5,6 +5,7 @@ import pycuda.autoinit # This is needed for initializing CUDA driver from polystar.common.communication.file_descriptor_target_sender import FileDescriptorTargetSender from polystar.common.constants import MODELS_DIR +from polystar.common.dependency_injection import make_injector from polystar.common.frame_generators.camera_frame_generator import CameraFrameGenerator from polystar.common.models.camera import Camera from polystar.common.models.label_map import LabelMap @@ -18,7 +19,6 @@ from polystar.common.target_pipeline.objects_validators.type_object_validator im from polystar.common.target_pipeline.target_factories.ratio_simple_target_factory import RatioSimpleTargetFactory from polystar.common.utils.tensorflow import patch_tf_v2 from polystar.common.view.cv2_results_viewer import CV2ResultViewer -from polystar.robots_at_robots.dependency_injection import make_injector from polystar.robots_at_robots.globals import settings [pycuda.autoinit] # So pycharm won't remove the import diff --git a/robots-at-robots/research/robots_at_robots/demos/utils.py b/polystar_cv/research/robots/demos/utils.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/demos/utils.py rename to polystar_cv/research/robots/demos/utils.py diff --git a/robots-at-runes/research/robots_at_runes/__init__.py b/polystar_cv/research/robots/evaluation/__init__.py similarity index 100% rename from robots-at-runes/research/robots_at_runes/__init__.py rename to polystar_cv/research/robots/evaluation/__init__.py diff --git a/polystar_cv/research/robots/evaluation/benchmarker.py b/polystar_cv/research/robots/evaluation/benchmarker.py new file mode 100644 index 0000000..0c3123f --- /dev/null +++ b/polystar_cv/research/robots/evaluation/benchmarker.py @@ -0,0 +1,50 @@ +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import List + +from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline +from research.common.datasets.image_dataset import FileImageDataset +from research.robots.evaluation.evaluator import ImageClassificationPipelineEvaluator +from research.robots.evaluation.metrics.f1 import F1Metric +from research.robots.evaluation.performance import ClassificationPerformances +from research.robots.evaluation.reporter import ImagePipelineEvaluationReporter +from research.robots.evaluation.trainer import ImageClassificationPipelineTrainer + +logger = logging.getLogger(__name__) + + +@dataclass +class Benchmarker: + def __init__( + self, + train_datasets: List[FileImageDataset], + validation_datasets: List[FileImageDataset], + test_datasets: List[FileImageDataset], + classes: List, + report_dir: Path, + ): + report_dir.mkdir(exist_ok=True, parents=True) + self.trainer = ImageClassificationPipelineTrainer(train_datasets, validation_datasets) + self.evaluator = ImageClassificationPipelineEvaluator(train_datasets, validation_datasets, test_datasets) + self.reporter = ImagePipelineEvaluationReporter( + report_dir=report_dir, classes=classes, other_metrics=[F1Metric()] + ) + self.performances = ClassificationPerformances() + logger.info(f"Run `tensorboard --logdir={report_dir}` for realtime logs when using keras") + + def train_and_evaluate(self, pipeline: ClassificationPipeline) -> ClassificationPerformances: + self.trainer.train_pipeline(pipeline) + pipeline_performances = self.evaluator.evaluate_pipeline(pipeline) + self.performances += pipeline_performances + return pipeline_performances + + def benchmark( + self, pipelines: List[ClassificationPipeline], trained_pipelines: List[ClassificationPipeline] = None + ): + self.trainer.train_pipelines(pipelines) + self.performances += self.evaluator.evaluate_pipelines(pipelines + (trained_pipelines or [])) + self.make_report() + + def make_report(self): + self.reporter.report(self.performances) diff --git a/robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluator.py b/polystar_cv/research/robots/evaluation/evaluator.py similarity index 83% rename from robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluator.py rename to polystar_cv/research/robots/evaluation/evaluator.py index 9f11ae3..d03c1c9 100644 --- a/robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluator.py +++ b/polystar_cv/research/robots/evaluation/evaluator.py @@ -9,12 +9,12 @@ from polystar.common.pipeline.classification.classification_pipeline import Clas from polystar.common.utils.iterable_utils import flatten from research.common.datasets.image_dataset import FileImageDataset from research.common.datasets.lazy_dataset import TargetT -from research.robots_at_robots.evaluation.performance import ( +from research.robots.evaluation.performance import ( ClassificationPerformance, ClassificationPerformances, ContextualizedClassificationPerformance, ) -from research.robots_at_robots.evaluation.set import Set +from research.robots.evaluation.set import Set class ImageClassificationPipelineEvaluator(Generic[TargetT]): @@ -27,11 +27,13 @@ class ImageClassificationPipelineEvaluator(Generic[TargetT]): self.set2datasets = {Set.TRAIN: train_datasets, Set.VALIDATION: validation_datasets, Set.TEST: test_datasets} def evaluate_pipelines(self, pipelines: Iterable[ClassificationPipeline]) -> ClassificationPerformances: - return ClassificationPerformances(flatten(self._evaluate_pipeline(pipeline) for pipeline in pipelines)) + rv = ClassificationPerformances() + for pipeline in pipelines: + rv += self.evaluate_pipeline(pipeline) + return rv - def _evaluate_pipeline(self, pipeline: ClassificationPipeline) -> Iterable[ContextualizedClassificationPerformance]: - for set_ in Set: - yield from self._evaluate_pipeline_on_set(pipeline, set_) + def evaluate_pipeline(self, pipeline: ClassificationPipeline) -> ClassificationPerformances: + return ClassificationPerformances(flatten(self._evaluate_pipeline_on_set(pipeline, set_) for set_ in Set)) def _evaluate_pipeline_on_set( self, pipeline: ClassificationPipeline, set_: Set diff --git a/polystar_cv/research/robots/evaluation/hyper_tuner.py b/polystar_cv/research/robots/evaluation/hyper_tuner.py new file mode 100644 index 0000000..4953894 --- /dev/null +++ b/polystar_cv/research/robots/evaluation/hyper_tuner.py @@ -0,0 +1,34 @@ +from pathlib import Path +from typing import Callable, Optional + +from optuna import Trial, create_study + +from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline +from polystar.common.utils.serialization import pkl_dump +from research.robots.evaluation.benchmarker import Benchmarker +from research.robots.evaluation.metrics.accuracy import AccuracyMetric +from research.robots.evaluation.metrics.metric_abc import MetricABC + +PipelineFactory = Callable[[Path, Trial], ClassificationPipeline] + + +class HyperTuner: + def __init__(self, benchmarker: Benchmarker, metric: MetricABC = AccuracyMetric(), report_frequency: int = 5): + self.report_frequency = report_frequency + self.metric = metric + self.benchmarker = benchmarker + self._pipeline_factory: Optional[PipelineFactory] = None + + def tune(self, pipeline_factory: PipelineFactory, n_trials: int, minimize: bool = False): + self._pipeline_factory = pipeline_factory + study = create_study(direction="minimize" if minimize else "maximize") + study.optimize(self._objective, n_trials=n_trials, show_progress_bar=True) + self.benchmarker.make_report() + pkl_dump(study, self.benchmarker.reporter.report_dir / "study") + + def _objective(self, trial: Trial) -> float: + pipeline = self._pipeline_factory(self.benchmarker.reporter.report_dir, trial) + performances = self.benchmarker.train_and_evaluate(pipeline) + if not trial.number % self.report_frequency: + self.benchmarker.make_report() + return self.metric(performances.validation.collapse()) diff --git a/robots-at-runes/research/robots_at_runes/dataset/__init__.py b/polystar_cv/research/robots/evaluation/metrics/__init__.py similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/__init__.py rename to polystar_cv/research/robots/evaluation/metrics/__init__.py diff --git a/robots-at-robots/research/robots_at_robots/evaluation/metrics/accuracy.py b/polystar_cv/research/robots/evaluation/metrics/accuracy.py similarity index 59% rename from robots-at-robots/research/robots_at_robots/evaluation/metrics/accuracy.py rename to polystar_cv/research/robots/evaluation/metrics/accuracy.py index ccfe9c7..60716f2 100644 --- a/robots-at-robots/research/robots_at_robots/evaluation/metrics/accuracy.py +++ b/polystar_cv/research/robots/evaluation/metrics/accuracy.py @@ -1,5 +1,5 @@ -from research.robots_at_robots.evaluation.metrics.metric_abc import MetricABC -from research.robots_at_robots.evaluation.performance import ClassificationPerformance +from research.robots.evaluation.metrics.metric_abc import MetricABC +from research.robots.evaluation.performance import ClassificationPerformance class AccuracyMetric(MetricABC): diff --git a/robots-at-robots/research/robots_at_robots/evaluation/metrics/f1.py b/polystar_cv/research/robots/evaluation/metrics/f1.py similarity index 79% rename from robots-at-robots/research/robots_at_robots/evaluation/metrics/f1.py rename to polystar_cv/research/robots/evaluation/metrics/f1.py index dd5f48a..0730c42 100644 --- a/robots-at-robots/research/robots_at_robots/evaluation/metrics/f1.py +++ b/polystar_cv/research/robots/evaluation/metrics/f1.py @@ -2,8 +2,8 @@ from enum import Enum, auto from sklearn.metrics import f1_score -from research.robots_at_robots.evaluation.metrics.metric_abc import MetricABC -from research.robots_at_robots.evaluation.performance import ClassificationPerformance +from research.robots.evaluation.metrics.metric_abc import MetricABC +from research.robots.evaluation.performance import ClassificationPerformance class F1Strategy(Enum): diff --git a/robots-at-robots/research/robots_at_robots/evaluation/metrics/metric_abc.py b/polystar_cv/research/robots/evaluation/metrics/metric_abc.py similarity index 77% rename from robots-at-robots/research/robots_at_robots/evaluation/metrics/metric_abc.py rename to polystar_cv/research/robots/evaluation/metrics/metric_abc.py index f25a0c3..f939585 100644 --- a/robots-at-robots/research/robots_at_robots/evaluation/metrics/metric_abc.py +++ b/polystar_cv/research/robots/evaluation/metrics/metric_abc.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from research.robots_at_robots.evaluation.performance import ClassificationPerformance +from research.robots.evaluation.performance import ClassificationPerformance class MetricABC(ABC): diff --git a/robots-at-robots/research/robots_at_robots/evaluation/performance.py b/polystar_cv/research/robots/evaluation/performance.py similarity index 85% rename from robots-at-robots/research/robots_at_robots/evaluation/performance.py rename to polystar_cv/research/robots/evaluation/performance.py index 52c014c..1521fbd 100644 --- a/robots-at-robots/research/robots_at_robots/evaluation/performance.py +++ b/polystar_cv/research/robots/evaluation/performance.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Dict, Iterable, List, Sequence import numpy as np @@ -7,7 +7,7 @@ from memoized_property import memoized_property from polystar.common.filters.filter_abc import FilterABC from polystar.common.models.image import FileImage from polystar.common.utils.iterable_utils import flatten, group_by -from research.robots_at_robots.evaluation.set import Set +from research.robots.evaluation.set import Set @dataclass @@ -39,7 +39,7 @@ class ContextualizedClassificationPerformance(ClassificationPerformance): @dataclass class ClassificationPerformances(Iterable[ContextualizedClassificationPerformance]): - performances: List[ContextualizedClassificationPerformance] + performances: List[ContextualizedClassificationPerformance] = field(default_factory=list) @property def train(self) -> "ClassificationPerformances": @@ -62,7 +62,7 @@ class ClassificationPerformances(Iterable[ContextualizedClassificationPerformanc for name, performances in group_by(self, lambda p: p.pipeline_name).items() } - def merge(self) -> ClassificationPerformance: + def collapse(self) -> ClassificationPerformance: return ClassificationPerformance( examples=flatten(p.examples for p in self), labels=np.concatenate([p.labels for p in self]), @@ -74,6 +74,13 @@ class ClassificationPerformances(Iterable[ContextualizedClassificationPerformanc def __iter__(self): return iter(self.performances) + def __len__(self): + return len(self.performances) + + def __iadd__(self, other: "ClassificationPerformances"): + self.performances.extend(other.performances) + return self + @dataclass class SetClassificationPerformanceFilter(FilterABC[ContextualizedClassificationPerformance]): diff --git a/robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluation_reporter.py b/polystar_cv/research/robots/evaluation/reporter.py similarity index 66% rename from robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluation_reporter.py rename to polystar_cv/research/robots/evaluation/reporter.py index 497ed62..e7f6d47 100644 --- a/robots-at-robots/research/robots_at_robots/evaluation/image_pipeline_evaluation_reporter.py +++ b/polystar_cv/research/robots/evaluation/reporter.py @@ -2,6 +2,7 @@ from collections import Counter from dataclasses import InitVar, dataclass, field from math import log from os.path import relpath +from pathlib import Path from typing import Generic, List, Optional, Tuple import matplotlib.pyplot as plt @@ -10,46 +11,49 @@ import seaborn as sns from matplotlib.axes import Axes, logging from matplotlib.figure import Figure from pandas import DataFrame -from sklearn.metrics import classification_report, confusion_matrix +from sklearn.metrics import ConfusionMatrixDisplay, classification_report, confusion_matrix from polystar.common.pipeline.classification.classification_pipeline import EnumT from polystar.common.utils.dataframe import Format, format_df_row, format_df_rows, make_formater -from polystar.common.utils.markdown import MarkdownFile, markdown_to_pdf -from polystar.common.utils.time import create_time_id -from research.common.constants import DSET_DIR, EVALUATION_DIR -from research.robots_at_robots.evaluation.metrics.accuracy import AccuracyMetric -from research.robots_at_robots.evaluation.metrics.metric_abc import MetricABC -from research.robots_at_robots.evaluation.performance import ClassificationPerformance, ClassificationPerformances -from research.robots_at_robots.evaluation.set import Set +from polystar.common.utils.markdown import MarkdownFile +from research.common.constants import DSET_DIR +from research.robots.evaluation.metrics.accuracy import AccuracyMetric +from research.robots.evaluation.metrics.metric_abc import MetricABC +from research.robots.evaluation.performance import ClassificationPerformance, ClassificationPerformances +from research.robots.evaluation.set import Set + +logger = logging.getLogger(__name__) @dataclass class ImagePipelineEvaluationReporter(Generic[EnumT]): - evaluation_project: str - experiment_name: str + report_dir: Path classes: List[EnumT] main_metric: MetricABC = field(default_factory=AccuracyMetric) other_metrics: InitVar[List[MetricABC]] = None _mf: MarkdownFile = field(init=False) _performances: ClassificationPerformances = field(init=False) + _has_validation: bool = field(init=False) + _sorted_pipeline_names: List[str] = field(init=False) def __post_init__(self, other_metrics: List[MetricABC]): - self.report_dir = EVALUATION_DIR / self.evaluation_project / f"{create_time_id()}_{self.experiment_name}" self.all_metrics: List[MetricABC] = [self.main_metric] + (other_metrics or []) def report(self, performances: ClassificationPerformances): sns.set() + self._performances = performances - report_path = self.report_dir / "report.md" - with MarkdownFile(report_path) as self._mf: + self._has_validation = bool(self._performances.validation) + self._sorted_pipeline_names = self._make_sorted_pipeline_names() + + with MarkdownFile(self.report_dir / "report.md") as self._mf: self._mf.title(f"Evaluation report") self._report_datasets() self._report_aggregated_results() self._report_pipelines_results() - logging.info(f"Report generated at file:///{self.report_dir/'report.md'}") - markdown_to_pdf(report_path) + logger.info(f"Report generated at file:///{self.report_dir/'report.md'}") def _report_datasets(self): self._mf.title("Datasets", level=2) @@ -57,8 +61,9 @@ class ImagePipelineEvaluationReporter(Generic[EnumT]): self._mf.title("Train-val", level=3) self._mf.paragraph("Train") self._report_dataset(self._performances.train) - self._mf.paragraph("Val") - self._report_dataset(self._performances.validation) + if self._has_validation: + self._mf.paragraph("Val") + self._report_dataset(self._performances.validation) self._mf.title("Testing", level=3) self._report_dataset(self._performances.test) @@ -91,40 +96,48 @@ class ImagePipelineEvaluationReporter(Generic[EnumT]): self._mf.paragraph("On test set:") self._mf.table(self._make_aggregated_results_for_set(Set.TEST)) - self._mf.paragraph("On validation set:") - self._mf.table(self._make_aggregated_results_for_set(Set.VALIDATION)) + if self._has_validation: + self._mf.paragraph("On validation set:") + self._mf.table(self._make_aggregated_results_for_set(Set.VALIDATION)) self._mf.paragraph("On train set:") self._mf.table(self._make_aggregated_results_for_set(Set.TRAIN)) + def _make_sorted_pipeline_names(self) -> List[str]: + pipeline_name2score = { + pipeline_name: self.main_metric( + (performances.validation if self._has_validation else performances.test).collapse() + ) + for pipeline_name, performances in self._performances.group_by_pipeline().items() + } + return sorted(pipeline_name2score, key=pipeline_name2score.get, reverse=True) + def _report_pipelines_results(self): - for pipeline_name, performances in sorted( - self._performances.group_by_pipeline().items(), - key=lambda name_perfs: self.main_metric(name_perfs[1].test.merge()), - reverse=True, - ): - self._report_pipeline_results(pipeline_name, performances) + pipeline_name2perfs = self._performances.group_by_pipeline() + for pipeline_name in self._sorted_pipeline_names: + self._report_pipeline_results(pipeline_name, pipeline_name2perfs[pipeline_name]) def _report_pipeline_results(self, pipeline_name: str, performances: ClassificationPerformances): self._mf.title(pipeline_name, level=2) self._mf.title("Test results", level=3) - self._report_pipeline_set_results(performances, Set.TEST) + self._report_pipeline_set_results(pipeline_name, performances, Set.TEST) - self._mf.title("Validation results", level=3) - self._report_pipeline_set_results(performances, Set.VALIDATION) + if self._has_validation: + self._mf.title("Validation results", level=3) + self._report_pipeline_set_results(pipeline_name, performances, Set.VALIDATION) self._mf.title("Train results", level=3) - self._report_pipeline_set_results(performances, Set.TRAIN) + self._report_pipeline_set_results(pipeline_name, performances, Set.TRAIN) - def _report_pipeline_set_results(self, performances: ClassificationPerformances, set_: Set): + def _report_pipeline_set_results(self, pipeline_name: str, performances: ClassificationPerformances, set_: Set): performances = performances.on_set(set_) - perf = performances.merge() + perf = performances.collapse() self._mf.title("Metrics", level=4) self._report_pipeline_set_metrics(performances, perf, set_) self._mf.title("Confusion Matrix:", level=4) - self._report_pipeline_set_confusion_matrix(perf) + self._report_pipeline_set_confusion_matrix(pipeline_name, perf, set_) self._mf.title("25 Mistakes examples", level=4) self._report_pipeline_set_mistakes(perf) @@ -164,12 +177,15 @@ class ImagePipelineEvaluationReporter(Generic[EnumT]): format_df_row(df, "support", int) self._mf.table(df) - def _report_pipeline_set_confusion_matrix(self, perf: ClassificationPerformance): - self._mf.table( - DataFrame( - confusion_matrix(perf.labels, perf.predictions), index=perf.unique_labels, columns=perf.unique_labels - ) + def _report_pipeline_set_confusion_matrix(self, pipeline_name: str, perf: ClassificationPerformance, set_: Set): + sns.reset_defaults() + cm = ConfusionMatrixDisplay( + confusion_matrix(perf.labels, perf.predictions, labels=perf.unique_labels), + display_labels=perf.unique_labels, ) + cm.plot(cmap=plt.cm.Blues, values_format=".4g") + self._mf.figure(cm.figure_, f"{pipeline_name}_{set_}_cm.png") + sns.set() def _report_pipeline_set_mistakes(self, perf: ClassificationPerformance): mistakes = perf.mistakes @@ -211,70 +227,70 @@ class ImagePipelineEvaluationReporter(Generic[EnumT]): } for perf in self._performances ] - ).sort_values(["set", self.main_metric.name], ascending=[True, False]) + ) df[f"{self.main_metric.name} "] = list(zip(df[self.main_metric.name], df.support)) df["time "] = list(zip(df.time, df.support)) return ( - _cat_pipeline_results(df, f"{self.main_metric.name} ", "{:.1%}", limits=(0, 1)), - _cat_pipeline_results(df, "time ", "{:.2e}", log_scale=True), + self._cat_pipeline_results(df, f"{self.main_metric.name} ", "{:.1%}", limits=(0, 1)), + self._cat_pipeline_results(df, "time ", "{:.2e}", log_scale=True), ) def _make_aggregated_results_for_set(self, set_: Set) -> DataFrame: - pipeline2performances = self._performances.on_set(set_).group_by_pipeline() - pipeline2performance = { - pipeline_name: performances.merge() for pipeline_name, performances in pipeline2performances.items() + pipeline_name2performances = self._performances.on_set(set_).group_by_pipeline() + pipeline_name2performance = { + pipeline_name: performances.collapse() for pipeline_name, performances in pipeline_name2performances.items() } - return ( - DataFrame( - [ - { - "pipeline": pipeline_name, - self.main_metric.name: self.main_metric(performance), - "inference time": performance.mean_inference_time, - } - for pipeline_name, performance in pipeline2performance.items() - ] - ) - .set_index("pipeline") - .sort_values(self.main_metric.name, ascending=False) + return DataFrame( + [ + { + "pipeline": pipeline_name, + self.main_metric.name: self.main_metric(pipeline_name2performance[pipeline_name]), + "inference time": pipeline_name2performance[pipeline_name].mean_inference_time, + } + for pipeline_name in self._sorted_pipeline_names + ] + ).set_index("pipeline") + + def _cat_pipeline_results( + self, df: DataFrame, y: str, fmt: str, limits: Optional[Tuple[float, float]] = None, log_scale: bool = False + ) -> Figure: + cols = ["test"] + if self._has_validation: + cols.append("validation") + cols.append("train") + grid: sns.FacetGrid = sns.catplot( + data=df, + x="pipeline", + y=y, + col="set", + kind="bar", + sharey=True, + legend=False, + col_order=cols, + height=8, + estimator=weighted_mean, + orient="v", + order=self._sorted_pipeline_names, ) + fig: Figure = grid.fig + + grid.set_xticklabels(rotation=30, ha="right") + _format_axes(fig.get_axes(), fmt, limits=limits, log_scale=log_scale) + + fig.suptitle(y) + fig.tight_layout() + + return fig + def weighted_mean(x, **kws): val, weight = map(np.asarray, zip(*x)) return (val * weight).sum() / weight.sum() -def _cat_pipeline_results( - df: DataFrame, y: str, fmt: str, limits: Optional[Tuple[float, float]] = None, log_scale: bool = False -) -> Figure: - grid: sns.FacetGrid = sns.catplot( - data=df, - x="pipeline", - y=y, - col="set", - kind="bar", - sharey=True, - legend=False, - col_order=["test", "validation", "train"], - height=8, - estimator=weighted_mean, - orient="v", - ) - - fig: Figure = grid.fig - - grid.set_xticklabels(rotation=30, ha="right") - _format_axes(fig.get_axes(), fmt, limits=limits, log_scale=log_scale) - - fig.suptitle(y) - fig.tight_layout() - - return fig - - def bar_plot_with_secondary( df: DataFrame, title: str, diff --git a/robots-at-robots/research/robots_at_robots/evaluation/set.py b/polystar_cv/research/robots/evaluation/set.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/evaluation/set.py rename to polystar_cv/research/robots/evaluation/set.py diff --git a/robots-at-robots/research/robots_at_robots/evaluation/trainer.py b/polystar_cv/research/robots/evaluation/trainer.py similarity index 100% rename from robots-at-robots/research/robots_at_robots/evaluation/trainer.py rename to polystar_cv/research/robots/evaluation/trainer.py diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/__init__.py b/polystar_cv/research/runes/__init__.py similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/blend/__init__.py rename to polystar_cv/research/runes/__init__.py diff --git a/robots-at-runes/research/robots_at_runes/constants.py b/polystar_cv/research/runes/constants.py similarity index 100% rename from robots-at-runes/research/robots_at_runes/constants.py rename to polystar_cv/research/runes/constants.py diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/__init__.py b/polystar_cv/research/runes/dataset/__init__.py similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/__init__.py rename to polystar_cv/research/runes/dataset/__init__.py diff --git a/polystar_cv/research/runes/dataset/blend/__init__.py b/polystar_cv/research/runes/dataset/blend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/examples/.gitignore b/polystar_cv/research/runes/dataset/blend/examples/.gitignore similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/blend/examples/.gitignore rename to polystar_cv/research/runes/dataset/blend/examples/.gitignore diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/examples/back1.jpg b/polystar_cv/research/runes/dataset/blend/examples/back1.jpg similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/blend/examples/back1.jpg rename to polystar_cv/research/runes/dataset/blend/examples/back1.jpg diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/examples/logo.png b/polystar_cv/research/runes/dataset/blend/examples/logo.png similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/blend/examples/logo.png rename to polystar_cv/research/runes/dataset/blend/examples/logo.png diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/examples/logo.xml b/polystar_cv/research/runes/dataset/blend/examples/logo.xml similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/blend/examples/logo.xml rename to polystar_cv/research/runes/dataset/blend/examples/logo.xml diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/image_blender.py b/polystar_cv/research/runes/dataset/blend/image_blender.py similarity index 88% rename from robots-at-runes/research/robots_at_runes/dataset/blend/image_blender.py rename to polystar_cv/research/runes/dataset/blend/image_blender.py index 976aba5..87eeb6d 100644 --- a/robots-at-runes/research/robots_at_runes/dataset/blend/image_blender.py +++ b/polystar_cv/research/runes/dataset/blend/image_blender.py @@ -6,9 +6,8 @@ import cv2 import numpy as np from polystar.common.models.image import Image -from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import \ - LabeledImageModifierABC -from research.robots_at_runes.dataset.labeled_image import LabeledImage, PointOfInterest +from research.runes.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import LabeledImageModifierABC +from research.runes.dataset.labeled_image import LabeledImage, PointOfInterest @dataclass @@ -70,8 +69,8 @@ if __name__ == "__main__": import matplotlib.pyplot as plt - from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_rotator import LabeledImageRotator - from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_scaler import LabeledImageScaler + from research.runes.dataset.blend import LabeledImageScaler + from research.runes.dataset.blend.labeled_image_modifiers.labeled_image_rotator import LabeledImageRotator EXAMPLES_DIR = Path(__file__).parent / "examples" diff --git a/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/__init__.py b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py similarity index 94% rename from robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py rename to polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py index cf6f90b..5072c90 100644 --- a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py +++ b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_modifier_abc.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from random import random from polystar.common.models.image import Image -from research.robots_at_runes.dataset.labeled_image import LabeledImage, PointOfInterest +from research.runes.dataset.labeled_image import LabeledImage, PointOfInterest class LabeledImageModifierABC(ABC): diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py similarity index 85% rename from robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py rename to polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py index 6e241c2..3dd178a 100644 --- a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py +++ b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_rotator.py @@ -1,12 +1,11 @@ from dataclasses import dataclass import numpy as np - from imutils import rotate_bound + from polystar.common.models.image import Image -from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import \ - LabeledImageModifierABC -from research.robots_at_runes.dataset.labeled_image import PointOfInterest +from research.runes.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import LabeledImageModifierABC +from research.runes.dataset.labeled_image import PointOfInterest @dataclass diff --git a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py similarity index 80% rename from robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py rename to polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py index a7dacd8..a5ac388 100644 --- a/robots-at-runes/research/robots_at_runes/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py +++ b/polystar_cv/research/runes/dataset/blend/labeled_image_modifiers/labeled_image_scaler.py @@ -3,9 +3,8 @@ from dataclasses import dataclass import cv2 from polystar.common.models.image import Image -from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import \ - LabeledImageModifierABC -from research.robots_at_runes.dataset.labeled_image import PointOfInterest +from research.runes.dataset.blend.labeled_image_modifiers.labeled_image_modifier_abc import LabeledImageModifierABC +from research.runes.dataset.labeled_image import PointOfInterest @dataclass diff --git a/robots-at-runes/research/robots_at_runes/dataset/dataset_generator.py b/polystar_cv/research/runes/dataset/dataset_generator.py similarity index 85% rename from robots-at-runes/research/robots_at_runes/dataset/dataset_generator.py rename to polystar_cv/research/runes/dataset/dataset_generator.py index d5a871c..9dcf261 100644 --- a/robots-at-runes/research/robots_at_runes/dataset/dataset_generator.py +++ b/polystar_cv/research/runes/dataset/dataset_generator.py @@ -12,11 +12,11 @@ from research.common.dataset.perturbations.image_modifiers.gaussian_noise import from research.common.dataset.perturbations.image_modifiers.horizontal_blur import HorizontalBlurrer from research.common.dataset.perturbations.image_modifiers.saturation import SaturationModifier from research.common.dataset.perturbations.perturbator import ImagePerturbator -from research.robots_at_runes.constants import RUNES_DATASET_DIR -from research.robots_at_runes.dataset.blend.image_blender import ImageBlender -from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_rotator import LabeledImageRotator -from research.robots_at_runes.dataset.blend.labeled_image_modifiers.labeled_image_scaler import LabeledImageScaler -from research.robots_at_runes.dataset.labeled_image import load_labeled_images_in_directory +from research.runes.constants import RUNES_DATASET_DIR +from research.runes.dataset.blend import LabeledImageScaler +from research.runes.dataset.blend.image_blender import ImageBlender +from research.runes.dataset.blend.labeled_image_modifiers.labeled_image_rotator import LabeledImageRotator +from research.runes.dataset.labeled_image import load_labeled_images_in_directory class DatasetGenerator: diff --git a/robots-at-runes/research/robots_at_runes/dataset/labeled_image.py b/polystar_cv/research/runes/dataset/labeled_image.py similarity index 100% rename from robots-at-runes/research/robots_at_runes/dataset/labeled_image.py rename to polystar_cv/research/runes/dataset/labeled_image.py diff --git a/polystar_cv/tests/common/integration_tests/__init__.py b/polystar_cv/tests/common/integration_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/polystar_cv/tests/common/integration_tests/datasets/__init__.py b/polystar_cv/tests/common/integration_tests/datasets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/tests/common/integration_tests/datasets/test_dji_dataset.py b/polystar_cv/tests/common/integration_tests/datasets/test_dji_dataset.py similarity index 100% rename from common/tests/common/integration_tests/datasets/test_dji_dataset.py rename to polystar_cv/tests/common/integration_tests/datasets/test_dji_dataset.py diff --git a/common/tests/common/integration_tests/datasets/test_dji_zoomed_dataset.py b/polystar_cv/tests/common/integration_tests/datasets/test_dji_zoomed_dataset.py similarity index 100% rename from common/tests/common/integration_tests/datasets/test_dji_zoomed_dataset.py rename to polystar_cv/tests/common/integration_tests/datasets/test_dji_zoomed_dataset.py diff --git a/common/tests/common/integration_tests/datasets/test_twitch_dataset_v1.py b/polystar_cv/tests/common/integration_tests/datasets/test_twitch_dataset_v1.py similarity index 100% rename from common/tests/common/integration_tests/datasets/test_twitch_dataset_v1.py rename to polystar_cv/tests/common/integration_tests/datasets/test_twitch_dataset_v1.py diff --git a/polystar_cv/tests/common/unittests/__init__.py b/polystar_cv/tests/common/unittests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/tests/common/unittests/datasets/roco/test_directory_dataset_zoo.py b/polystar_cv/tests/common/unittests/datasets/roco/test_directory_dataset_zoo.py similarity index 100% rename from common/tests/common/unittests/datasets/roco/test_directory_dataset_zoo.py rename to polystar_cv/tests/common/unittests/datasets/roco/test_directory_dataset_zoo.py diff --git a/common/tests/common/unittests/datasets/test_dataset.py b/polystar_cv/tests/common/unittests/datasets/test_dataset.py similarity index 100% rename from common/tests/common/unittests/datasets/test_dataset.py rename to polystar_cv/tests/common/unittests/datasets/test_dataset.py diff --git a/common/tests/common/unittests/datasets_v3/roco/test_directory_dataset_zoo.py b/polystar_cv/tests/common/unittests/datasets_v3/roco/test_directory_dataset_zoo.py similarity index 100% rename from common/tests/common/unittests/datasets_v3/roco/test_directory_dataset_zoo.py rename to polystar_cv/tests/common/unittests/datasets_v3/roco/test_directory_dataset_zoo.py diff --git a/common/tests/common/unittests/datasets_v3/test_dataset.py b/polystar_cv/tests/common/unittests/datasets_v3/test_dataset.py similarity index 100% rename from common/tests/common/unittests/datasets_v3/test_dataset.py rename to polystar_cv/tests/common/unittests/datasets_v3/test_dataset.py diff --git a/common/tests/common/unittests/filters/test_filters_abc.py b/polystar_cv/tests/common/unittests/filters/test_filters_abc.py similarity index 100% rename from common/tests/common/unittests/filters/test_filters_abc.py rename to polystar_cv/tests/common/unittests/filters/test_filters_abc.py diff --git a/common/tests/common/unittests/image_pipeline/test_image_classifier_pipeline.py b/polystar_cv/tests/common/unittests/image_pipeline/test_image_classifier_pipeline.py similarity index 100% rename from common/tests/common/unittests/image_pipeline/test_image_classifier_pipeline.py rename to polystar_cv/tests/common/unittests/image_pipeline/test_image_classifier_pipeline.py diff --git a/polystar_cv/tests/common/unittests/object_validators/__init__.py b/polystar_cv/tests/common/unittests/object_validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/tests/common/unittests/object_validators/test_in_box_validator.py b/polystar_cv/tests/common/unittests/object_validators/test_in_box_validator.py similarity index 100% rename from common/tests/common/unittests/object_validators/test_in_box_validator.py rename to polystar_cv/tests/common/unittests/object_validators/test_in_box_validator.py diff --git a/pyproject.toml b/pyproject.toml index 5e88305..82b6310 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,10 @@ [tool.poetry] -name = "polystar.cv" +name = "polystar_cv" version = "0.2.0" description = "CV code for Polystar's RoboMaster team" authors = ["Polystar"] +packages = [{ include = "polystar", from = "polystar_cv" }, { include = "research", from = "polystar_cv" }] +include = ["**/.changes"] [tool.poetry.dependencies] python = "^3.6" @@ -24,15 +26,25 @@ dataclasses = "^0.6.0" imutils = "^0.5.3" more-itertools = "^8.4.0" -[tool.poetry.dev-dependencies] -tensorflow = "2.1.x" -tensorflow-estimator = "2.1.x" opencv-python = "4.1.x" matplotlib = "^3.1.3" -kivy = "^1.11.1" markdown = "^3.3.3" xhtml2pdf = "^0.2.5" google-cloud-storage = "^1.35.0" +pyyaml = "^5.3.1" +six = "1.15.0" # https://github.com/googleapis/python-bigquery/issues/70 + +[tool.poetry.dev-dependencies] +tensorflow = "2.3.x" +tensorflow-estimator = "2.3.x" +kivy = "^1.11.1" +cloudml-hypertune = "^0.1.0-alpha.6" +google-api-python-client = "^1.12.8" +wheel = "^0.36.2" +optuna = "^2.3.0" +hyperopt = "^0.2.5" +plotly = "^4.14.1" +pydot = "^1.4.1" [tool.black] line-length = 120 @@ -43,3 +55,7 @@ profile='black' line_length = 120 known_first_party = ['polystar','tests','research','tools','scripts'] skip = ['.eggs','.git','.hg','.mypy_cache','.nox','.pants.d','.tox','.venv','_build','buck-out','build','dist','node_modules','venv','__init__.py'] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/robots-at-robots/Readme.md b/robots-at-robots/Readme.md deleted file mode 100644 index fcca02b..0000000 --- a/robots-at-robots/Readme.md +++ /dev/null @@ -1,6 +0,0 @@ - -# Robots@Robots - -## Goal - -The goal of this project is to detect the other robots, from our robots, to be able to assist the pilot into shooting them. diff --git a/robots-at-robots/config/settings.toml b/robots-at-robots/config/settings.toml deleted file mode 100644 index fe532b0..0000000 --- a/robots-at-robots/config/settings.toml +++ /dev/null @@ -1,6 +0,0 @@ -[default] -MODEL_NAME = 'robots/TRT_ssd_mobilenet_v2_roco.bin' - -[development] - -[production] diff --git a/robots-at-robots/polystar/robots_at_robots/dependency_injection.py b/robots-at-robots/polystar/robots_at_robots/dependency_injection.py deleted file mode 100644 index cc76fa1..0000000 --- a/robots-at-robots/polystar/robots_at_robots/dependency_injection.py +++ /dev/null @@ -1,16 +0,0 @@ -from dataclasses import dataclass - -from dynaconf import LazySettings -from injector import Injector, Module - -from polystar.common.dependency_injection import CommonModule -from polystar.robots_at_robots.globals import settings - - -def make_injector() -> Injector: - return Injector(modules=[CommonModule(settings), RobotsAtRobotsModule(settings)]) - - -@dataclass -class RobotsAtRobotsModule(Module): - settings: LazySettings diff --git a/robots-at-robots/polystar/robots_at_robots/globals.py b/robots-at-robots/polystar/robots_at_robots/globals.py deleted file mode 100644 index bf1c91a..0000000 --- a/robots-at-robots/polystar/robots_at_robots/globals.py +++ /dev/null @@ -1,5 +0,0 @@ -from polystar.common.settings import make_settings - -PROJECT_NAME = "robots-at-robots" - -settings = make_settings(PROJECT_NAME) diff --git a/robots-at-robots/research/robots_at_robots/armor_color/armor_color_benchmarker.py b/robots-at-robots/research/robots_at_robots/armor_color/armor_color_benchmarker.py deleted file mode 100644 index 37a9e35..0000000 --- a/robots-at-robots/research/robots_at_robots/armor_color/armor_color_benchmarker.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import List - -from polystar.common.models.object import ArmorColor -from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder -from research.robots_at_robots.armor_color.armor_color_dataset import make_armor_color_dataset_generator -from research.robots_at_robots.evaluation.benchmark import make_armor_value_benchmarker - - -def make_armor_color_benchmarker( - train_roco_datasets: List[ROCODatasetBuilder], - validation_roco_datasets: List[ROCODatasetBuilder], - test_roco_datasets: List[ROCODatasetBuilder], - experiment_name: str, -): - dataset_generator = make_armor_color_dataset_generator() - return make_armor_value_benchmarker( - train_roco_datasets=train_roco_datasets, - validation_roco_datasets=validation_roco_datasets, - test_roco_datasets=test_roco_datasets, - evaluation_project="armor-color", - experiment_name=experiment_name, - classes=list(ArmorColor), - dataset_generator=dataset_generator, - ) diff --git a/robots-at-robots/research/robots_at_robots/armor_color/armor_color_dataset.py b/robots-at-robots/research/robots_at_robots/armor_color/armor_color_dataset.py deleted file mode 100644 index 02985e4..0000000 --- a/robots-at-robots/research/robots_at_robots/armor_color/armor_color_dataset.py +++ /dev/null @@ -1,26 +0,0 @@ -from itertools import islice - -from polystar.common.models.object import Armor, ArmorColor -from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.dataset.armor_value_dataset_generator import ArmorValueDatasetGenerator -from research.robots_at_robots.dataset.armor_value_target_factory import ArmorValueTargetFactory - - -class ArmorColorTargetFactory(ArmorValueTargetFactory[ArmorColor]): - def from_str(self, label: str) -> ArmorColor: - return ArmorColor(label) - - def from_armor(self, armor: Armor) -> ArmorColor: - return armor.color - - -def make_armor_color_dataset_generator() -> ArmorValueDatasetGenerator[ArmorColor]: - return ArmorValueDatasetGenerator("colors", ArmorColorTargetFactory()) - - -if __name__ == "__main__": - _roco_dataset_builder = ROCODatasetsZoo.DJI.CENTRAL_CHINA - _armor_color_dataset = make_armor_color_dataset_generator().from_roco_dataset(_roco_dataset_builder) - - for p, c, _name in islice(_armor_color_dataset, 20, 25): - print(p, c, _name) diff --git a/robots-at-robots/research/robots_at_robots/armor_color/benchmark.py b/robots-at-robots/research/robots_at_robots/armor_color/benchmark.py deleted file mode 100644 index 441fb0d..0000000 --- a/robots-at-robots/research/robots_at_robots/armor_color/benchmark.py +++ /dev/null @@ -1,64 +0,0 @@ -import logging -from dataclasses import dataclass - -from nptyping import Array -from sklearn.linear_model import LogisticRegression - -from polystar.common.image_pipeline.featurizers.histogram_2d import Histogram2D -from polystar.common.image_pipeline.preprocessors.rgb_to_hsv import RGB2HSV -from polystar.common.models.image import Image -from polystar.common.models.object import ArmorColor -from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline -from polystar.common.pipeline.classification.random_model import RandomClassifier -from polystar.common.pipeline.classification.rule_based_classifier import RuleBasedClassifierABC -from polystar.common.pipeline.pipe_abc import PipeABC -from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.armor_color.armor_color_benchmarker import make_armor_color_benchmarker - - -class ArmorColorPipeline(ClassificationPipeline): - enum = ArmorColor - - -@dataclass -class MeanChannels(PipeABC): - def transform_single(self, image: Image) -> Array[float, float, float]: - return image.mean(axis=(0, 1)) - - -class RedBlueComparisonClassifier(RuleBasedClassifierABC): - """A very simple model that compares the blue and red values obtained by the MeanChannels""" - - def predict_single(self, features: Array[float, float, float]) -> ArmorColor: - return ArmorColor.Red if features[0] >= features[2] else ArmorColor.Blue - - -if __name__ == "__main__": - logging.getLogger().setLevel("INFO") - - _benchmarker = make_armor_color_benchmarker( - train_roco_datasets=[ - ROCODatasetsZoo.TWITCH.T470150052, - ROCODatasetsZoo.TWITCH.T470152289, - ROCODatasetsZoo.TWITCH.T470149568, - ROCODatasetsZoo.TWITCH.T470151286, - ], - validation_roco_datasets=[], - test_roco_datasets=[ - ROCODatasetsZoo.TWITCH.T470152838, - ROCODatasetsZoo.TWITCH.T470153081, - ROCODatasetsZoo.TWITCH.T470158483, - ROCODatasetsZoo.TWITCH.T470152730, - ], - experiment_name="test", - ) - - red_blue_comparison_pipeline = ArmorColorPipeline.from_pipes( - [MeanChannels(), RedBlueComparisonClassifier()], name="rb-comparison", - ) - random_pipeline = ArmorColorPipeline.from_pipes([RandomClassifier()], name="random") - hsv_hist_lr_pipeline = ArmorColorPipeline.from_pipes( - [RGB2HSV(), Histogram2D(), LogisticRegression()], name="hsv-hist-lr", - ) - - _benchmarker.benchmark([random_pipeline, red_blue_comparison_pipeline, hsv_hist_lr_pipeline]) diff --git a/robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_benchmarker.py b/robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_benchmarker.py deleted file mode 100644 index 8e77cab..0000000 --- a/robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_benchmarker.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import List - -from polystar.common.models.object import ArmorDigit -from research.common.datasets.image_dataset import FileImageDataset -from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder -from research.robots_at_robots.armor_digit.armor_digit_dataset import make_armor_digit_dataset_generator -from research.robots_at_robots.evaluation.benchmark import make_armor_value_benchmarker - - -def make_armor_digit_benchmarker( - train_roco_datasets: List[ROCODatasetBuilder], - validation_roco_datasets: List[ROCODatasetBuilder], - test_roco_datasets: List[ROCODatasetBuilder], - experiment_name: str, - train_digit_datasets: List[FileImageDataset[ArmorDigit]] = None, - validation_digit_datasets: List[FileImageDataset[ArmorDigit]] = None, - test_digit_datasets: List[FileImageDataset[ArmorDigit]] = None, -): - dataset_generator = make_armor_digit_dataset_generator() - return make_armor_value_benchmarker( - train_roco_datasets=train_roco_datasets, - validation_roco_datasets=validation_roco_datasets, - test_roco_datasets=test_roco_datasets, - evaluation_project="armor-digit", - experiment_name=experiment_name, - classes=list(ArmorDigit), - dataset_generator=dataset_generator, - train_datasets=train_digit_datasets, - validation_datasets=validation_digit_datasets, - test_datasets=test_digit_datasets, - ) diff --git a/robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_dataset.py b/robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_dataset.py deleted file mode 100644 index bd35645..0000000 --- a/robots-at-robots/research/robots_at_robots/armor_digit/armor_digit_dataset.py +++ /dev/null @@ -1,32 +0,0 @@ -from itertools import islice - -from polystar.common.filters.exclude_filter import ExcludeFilter -from polystar.common.models.object import Armor, ArmorDigit -from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.dataset.armor_value_dataset_generator import ArmorValueDatasetGenerator -from research.robots_at_robots.dataset.armor_value_target_factory import ArmorValueTargetFactory - - -class ArmorDigitTargetFactory(ArmorValueTargetFactory[ArmorDigit]): - def from_str(self, label: str) -> ArmorDigit: - n = int(label) - - if 1 <= n <= 5: # CHANGING - return ArmorDigit(n) - - return ArmorDigit.OUTDATED - - def from_armor(self, armor: Armor) -> ArmorDigit: - return ArmorDigit(armor.number) if armor.number else ArmorDigit.UNKNOWN - - -def make_armor_digit_dataset_generator() -> ArmorValueDatasetGenerator[ArmorDigit]: - return ArmorValueDatasetGenerator("digits", ArmorDigitTargetFactory(), ExcludeFilter({ArmorDigit.OUTDATED})) - - -if __name__ == "__main__": - _roco_dataset_builder = ROCODatasetsZoo.DJI.CENTRAL_CHINA - _armor_digit_dataset = make_armor_digit_dataset_generator().from_roco_dataset(_roco_dataset_builder) - - for p, c, _name in islice(_armor_digit_dataset, 20, 30): - print(p, c, _name) diff --git a/robots-at-robots/research/robots_at_robots/armor_digit/benchmark.py b/robots-at-robots/research/robots_at_robots/armor_digit/benchmark.py deleted file mode 100644 index 81e750a..0000000 --- a/robots-at-robots/research/robots_at_robots/armor_digit/benchmark.py +++ /dev/null @@ -1,253 +0,0 @@ -import logging -import warnings -from pathlib import Path -from typing import Callable, List, Sequence, Tuple - -from keras_preprocessing.image import ImageDataGenerator -from numpy import asarray -from tensorflow_core.python.keras import Input, Model, Sequential -from tensorflow_core.python.keras.applications.vgg16 import VGG16 -from tensorflow_core.python.keras.applications.xception import Xception -from tensorflow_core.python.keras.callbacks import EarlyStopping, TensorBoard -from tensorflow_core.python.keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D -from tensorflow_core.python.keras.optimizer_v2.adam import Adam -from tensorflow_core.python.keras.optimizer_v2.gradient_descent import SGD -from tensorflow_core.python.keras.utils.np_utils import to_categorical - -from polystar.common.image_pipeline.preprocessors.normalise import Normalise -from polystar.common.image_pipeline.preprocessors.resize import Resize -from polystar.common.models.image import Image -from polystar.common.models.object import ArmorDigit -from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline -from polystar.common.pipeline.classification.classifier_abc import ClassifierABC -from polystar.common.pipeline.classification.random_model import RandomClassifier -from research.common.datasets.roco.zoo.roco_dataset_zoo import ROCODatasetsZoo -from research.robots_at_robots.armor_digit.armor_digit_benchmarker import make_armor_digit_benchmarker -from research.robots_at_robots.armor_digit.armor_digit_dataset import make_armor_digit_dataset_generator -from research.robots_at_robots.evaluation.benchmark import Benchmarker - - -class ArmorDigitPipeline(ClassificationPipeline): - enum = ArmorDigit - - -class KerasClassifier(ClassifierABC): - def __init__(self, model: Model, optimizer, logs_dir: Path, with_data_augmentation: bool, batch_size: int = 32): - self.batch_size = batch_size - self.logs_dir = logs_dir - self.with_data_augmentation = with_data_augmentation - self.model = model - self.model.compile(optimizer=optimizer, loss="categorical_crossentropy", metrics=["accuracy"]) - - @property - def train_data_gen(self) -> ImageDataGenerator: - if not self.with_data_augmentation: - return ImageDataGenerator() - return ImageDataGenerator(rotation_range=45, zoom_range=[0.8, 1]) # brightness_range=[0.7, 1.4] - - def fit(self, images: List[Image], labels: List[int], validation_size: int) -> "KerasClassifier": - images = asarray(images) - labels = to_categorical(asarray(labels), 5) # FIXME - train_images, train_labels = images[:-validation_size], labels[:-validation_size] - val_images, val_labels = images[-validation_size:], labels[-validation_size:] - - train_generator = self.train_data_gen.flow(train_images, train_labels, batch_size=self.batch_size, shuffle=True) - - self.model.fit( - x=train_generator, - steps_per_epoch=len(train_images) / self.batch_size, - validation_data=(val_images, val_labels), - epochs=300, - callbacks=[ - EarlyStopping(verbose=0, patience=15, restore_best_weights=True), - TensorBoard(log_dir=self.logs_dir, histogram_freq=4, write_graph=True, write_images=False), - ], - verbose=0, - ) - return self - - def predict_proba(self, examples: List[Image]) -> Sequence[float]: - return self.model.predict_proba(asarray(examples)) - - -class CNN(Sequential): - def __init__( - self, input_size: Tuple[int, int], conv_blocks: Sequence[Sequence[int]], dense_size: int, output_size: int, - ): - super().__init__() - self.add(Input((*input_size, 3))) - - for conv_sizes in conv_blocks: - for size in conv_sizes: - self.add(Conv2D(size, (3, 3), activation="relu")) - self.add(MaxPooling2D()) - - self.add(Flatten()) - self.add(Dense(dense_size)) - self.add(Dropout(0.5)) - self.add(Dense(output_size, activation="softmax")) - - -def make_digits_cnn_pipeline( - input_size: int, conv_blocks: Sequence[Sequence[int]], report_dir: Path, with_data_augmentation: bool, lr: float -) -> ArmorDigitPipeline: - name = ( - f"cnn - ({input_size}) - lr {lr} - " - + " ".join("_".join(map(str, sizes)) for sizes in conv_blocks) - + (" - with_data_augm" * with_data_augmentation) - ) - input_size = (input_size, input_size) - return ArmorDigitPipeline.from_pipes( - [ - Resize(input_size), - Normalise(), - KerasClassifier( - CNN(input_size=input_size, conv_blocks=conv_blocks, dense_size=128, output_size=5), - logs_dir=report_dir / name, - with_data_augmentation=with_data_augmentation, - optimizer=SGD(lr, momentum=0.9), - ), - ], - name=name, - ) - - -class TransferLearning(Sequential): - def __init__(self, input_size: Tuple[int, int], n_classes: int, model_factory: Callable[..., Model]): - input_shape = (*input_size, 3) - base_model: Model = model_factory(weights="imagenet", input_shape=input_shape, include_top=False) - - super().__init__( - [ - Input(input_shape), - base_model, - Flatten(), - Dense(128, activation="relu"), - Dropout(0.5), - Dense(n_classes, activation="softmax"), - ] - ) - - -def make_tl_pipeline( - report_dir: Path, input_size: int, with_data_augmentation: bool, lr: float, model_factory: Callable[..., Model] -): - name = f"{model_factory.__name__} ({input_size}) - lr {lr}" + (" - with_data_augm" * with_data_augmentation) - input_size = (input_size, input_size) - return ArmorDigitPipeline.from_pipes( - [ - Resize(input_size), - Normalise(), - KerasClassifier( - model=TransferLearning(input_size=input_size, n_classes=5, model_factory=model_factory), # FIXME - optimizer=Adam(lr), # FIXME - logs_dir=report_dir, - with_data_augmentation=with_data_augmentation, - ), - ], - name=name, - ) - - -def make_vgg16_pipeline( - report_dir: Path, input_size: int, with_data_augmentation: bool, lr: float -) -> ArmorDigitPipeline: - return make_tl_pipeline( - model_factory=VGG16, - input_size=input_size, - with_data_augmentation=with_data_augmentation, - lr=lr, - report_dir=report_dir, - ) - - -def make_xception_pipeline( - report_dir: Path, input_size: int, with_data_augmentation: bool, lr: float -) -> ArmorDigitPipeline: - return make_tl_pipeline( - model_factory=Xception, - input_size=input_size, - with_data_augmentation=with_data_augmentation, - lr=lr, - report_dir=report_dir, - ) - - -def make_default_digit_benchmarker(exp_name: str) -> Benchmarker: - return make_armor_digit_benchmarker( - train_digit_datasets=[ - # Only the start of the dataset is cleaned as of now - make_armor_digit_dataset_generator() - .from_roco_dataset(ROCODatasetsZoo.DJI.FINAL) - .to_file_images() - .cap( - (1009 - 117) - + (1000 - 86) - + (1000 - 121) - + (1000 - 138) - + (1000 - 137) - + (1000 - 154) - + (1000 - 180) - + (1000 - 160) - + (1000 - 193) - + (1000 - 80) - ) - .build() - ], - train_roco_datasets=[ - # ROCODatasetsZoo.DJI.CENTRAL_CHINA, - # ROCODatasetsZoo.DJI.FINAL, - # ROCODatasetsZoo.DJI.NORTH_CHINA, - # ROCODatasetsZoo.DJI.SOUTH_CHINA, - ROCODatasetsZoo.TWITCH.T470150052, - ROCODatasetsZoo.TWITCH.T470149568, - ROCODatasetsZoo.TWITCH.T470151286, - ], - validation_roco_datasets=[ROCODatasetsZoo.TWITCH.T470152289], - test_roco_datasets=[ - ROCODatasetsZoo.TWITCH.T470152838, - ROCODatasetsZoo.TWITCH.T470153081, - ROCODatasetsZoo.TWITCH.T470158483, - ROCODatasetsZoo.TWITCH.T470152730, - ], - experiment_name=exp_name, - ) - - -if __name__ == "__main__": - logging.getLogger().setLevel("INFO") - logging.getLogger("tensorflow").setLevel("ERROR") - warnings.filterwarnings("ignore") - - _benchmarker = make_default_digit_benchmarker("xception") - - _report_dir = _benchmarker.reporter.report_dir - - _random_pipeline = ArmorDigitPipeline.from_pipes([RandomClassifier()], name="random") - _cnn_pipelines = [ - make_digits_cnn_pipeline( - 32, ((32, 32), (64, 64)), _report_dir, with_data_augmentation=with_data_augmentation, lr=lr, - ) - for with_data_augmentation in [False] - for lr in [2.5e-2, 1.6e-2, 1e-2, 6.3e-3, 4e-4] - ] - # cnn_pipelines = [ - # make_digits_cnn_pipeline( - # 64, ((32,), (64, 64), (64, 64)), reporter.report_dir, with_data_augmentation=True, lr=lr - # ) - # for with_data_augmentation in [True, False] - # for lr in (5.6e-2, 3.1e-2, 1.8e-2, 1e-2, 5.6e-3, 3.1e-3, 1.8e-3, 1e-3) - # ] - - vgg16_pipelines = [ - make_vgg16_pipeline(_report_dir, input_size=32, with_data_augmentation=False, lr=lr) - for lr in (1e-5, 5e-4, 2e-4, 1e-4, 5e-3) - ] - - xception_pipelines = [ - make_xception_pipeline(_report_dir, input_size=71, with_data_augmentation=False, lr=lr) for lr in (2e-4, 5e-5) - ] - - logging.info(f"Run `tensorboard --logdir={_report_dir}` for realtime logs") - - _benchmarker.benchmark([_random_pipeline] + xception_pipelines) diff --git a/robots-at-robots/research/robots_at_robots/constants.py b/robots-at-robots/research/robots_at_robots/constants.py deleted file mode 100644 index 8b13789..0000000 --- a/robots-at-robots/research/robots_at_robots/constants.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/robots-at-robots/research/robots_at_robots/evaluation/benchmark.py b/robots-at-robots/research/robots_at_robots/evaluation/benchmark.py deleted file mode 100644 index 958a2bf..0000000 --- a/robots-at-robots/research/robots_at_robots/evaluation/benchmark.py +++ /dev/null @@ -1,56 +0,0 @@ -from dataclasses import dataclass -from typing import List - -from polystar.common.pipeline.classification.classification_pipeline import ClassificationPipeline -from research.common.datasets.image_dataset import FileImageDataset -from research.common.datasets.roco.roco_dataset_builder import ROCODatasetBuilder -from research.robots_at_robots.dataset.armor_value_dataset_generator import ArmorValueDatasetGenerator -from research.robots_at_robots.evaluation.image_pipeline_evaluation_reporter import ImagePipelineEvaluationReporter -from research.robots_at_robots.evaluation.image_pipeline_evaluator import ImageClassificationPipelineEvaluator -from research.robots_at_robots.evaluation.metrics.f1 import F1Metric -from research.robots_at_robots.evaluation.trainer import ImageClassificationPipelineTrainer - - -@dataclass -class Benchmarker: - def __init__( - self, - train_datasets: List[FileImageDataset], - validation_datasets: List[FileImageDataset], - test_datasets: List[FileImageDataset], - evaluation_project: str, - experiment_name: str, - classes: List, - ): - self.trainer = ImageClassificationPipelineTrainer(train_datasets, validation_datasets) - self.evaluator = ImageClassificationPipelineEvaluator(train_datasets, validation_datasets, test_datasets) - self.reporter = ImagePipelineEvaluationReporter( - evaluation_project, experiment_name, classes, other_metrics=[F1Metric()] - ) - - def benchmark(self, pipelines: List[ClassificationPipeline]): - self.trainer.train_pipelines(pipelines) - self.reporter.report(self.evaluator.evaluate_pipelines(pipelines)) - - -def make_armor_value_benchmarker( - train_roco_datasets: List[ROCODatasetBuilder], - validation_roco_datasets: List[ROCODatasetBuilder], - test_roco_datasets: List[ROCODatasetBuilder], - evaluation_project: str, - experiment_name: str, - dataset_generator: ArmorValueDatasetGenerator, - classes: List, - train_datasets: List[FileImageDataset] = None, - validation_datasets: List[FileImageDataset] = None, - test_datasets: List[FileImageDataset] = None, -): - return Benchmarker( - train_datasets=dataset_generator.from_roco_datasets(train_roco_datasets) + (train_datasets or []), - validation_datasets=dataset_generator.from_roco_datasets(validation_roco_datasets) - + (validation_datasets or []), - test_datasets=dataset_generator.from_roco_datasets(test_roco_datasets) + (test_datasets or []), - evaluation_project=evaluation_project, - experiment_name=experiment_name, - classes=classes, - ) diff --git a/robots-at-runes/Readme.md b/robots-at-runes/Readme.md deleted file mode 100644 index f2b9dad..0000000 --- a/robots-at-runes/Readme.md +++ /dev/null @@ -1,6 +0,0 @@ - -# Robots@Runes - -## Goal - -The goal of this project is to detect the runes' rotation, from our robots, to be able to assist the pilot into shooting them. diff --git a/robots-at-runes/config/settings.toml b/robots-at-runes/config/settings.toml deleted file mode 100644 index bbefeb5..0000000 --- a/robots-at-runes/config/settings.toml +++ /dev/null @@ -1,5 +0,0 @@ -[default] - -[development] - -[production] diff --git a/robots-at-runes/polystar/robots_at_runes/globals.py b/robots-at-runes/polystar/robots_at_runes/globals.py deleted file mode 100644 index 30aaa4d..0000000 --- a/robots-at-runes/polystar/robots_at_runes/globals.py +++ /dev/null @@ -1,5 +0,0 @@ -from polystar.common.settings import make_settings - -PROJECT_NAME = "robots-at-runes" - -settings = make_settings(PROJECT_NAME) -- GitLab