From 7e9a8e2d4bb69671090261b9d5c756f05b7f4715 Mon Sep 17 00:00:00 2001 From: dx-tan Date: Thu, 30 Nov 2023 18:22:16 +0700 Subject: [PATCH] Add everything --- .gitignore | 4 +- .gitmodules | 3 + cope2n-ai-fi/._gitmodules | 6 + cope2n-ai-fi/.dockerignore | 7 + cope2n-ai-fi/.gitignore | 21 + cope2n-ai-fi/Dockerfile | 43 + cope2n-ai-fi/Dockerfile-dev | 21 + cope2n-ai-fi/Dockerfile_fwd | 8 + cope2n-ai-fi/LICENSE | 674 ++++++++++ cope2n-ai-fi/NOTE.md | 7 + cope2n-ai-fi/README.md | 36 + cope2n-ai-fi/TODO.md | 26 + cope2n-ai-fi/api/Kie_AHung/prediction.py | 149 ++ cope2n-ai-fi/api/Kie_AHung_ID/prediction.py | 145 ++ cope2n-ai-fi/api/Kie_Hoanglv/prediction.py | 57 + cope2n-ai-fi/api/Kie_Hoanglv/serve_model.py | 83 ++ .../Kie_Invoice_AP/AnyKey_Value/__init__.py | 0 .../AnyKey_Value/anyKeyValue.py | 137 ++ .../lightning_modules/__init__.py | 0 .../lightning_modules/classifier.py | 133 ++ .../lightning_modules/classifier_module.py | 390 ++++++ .../data_modules/__init__.py | 0 .../data_modules/data_module.py | 135 ++ .../data_modules/kvu_dataset.py | 728 ++++++++++ .../lightning_modules/schedulers.py | 53 + .../AnyKey_Value/lightning_modules/utils.py | 218 +++ .../AnyKey_Value/model/__init__.py | 15 + .../AnyKey_Value/model/combined_model.py | 224 ++++ .../AnyKey_Value/model/document_kvu_model.py | 285 ++++ .../AnyKey_Value/model/kvu_model.py | 248 ++++ .../AnyKey_Value/model/relation_extractor.py | 48 + .../AnyKey_Value/ocr-engine/.gitignore | 133 ++ .../AnyKey_Value/ocr-engine/.gitmodules | 6 + .../AnyKey_Value/ocr-engine/README.md | 47 + .../AnyKey_Value/ocr-engine/__init__.py | 11 + .../AnyKey_Value/ocr-engine/requirements.txt | 82 ++ .../AnyKey_Value/ocr-engine/run.py | 143 ++ .../ocr-engine/scripts/run_ocr.sh | 23 + .../AnyKey_Value/ocr-engine/settings.yml | 17 + .../AnyKey_Value/ocr-engine/src/dto.py | 453 +++++++ .../AnyKey_Value/ocr-engine/src/ocr.py | 207 +++ .../AnyKey_Value/ocr-engine/src/utils.py | 346 +++++ .../ocr-engine/src/word_formation.py | 673 ++++++++++ .../Kie_Invoice_AP/AnyKey_Value/predictor.py | 230 ++++ .../Kie_Invoice_AP/AnyKey_Value/preprocess.py | 601 +++++++++ .../AnyKey_Value/requirements.txt | 19 + .../api/Kie_Invoice_AP/AnyKey_Value/run.sh | 1 + .../api/Kie_Invoice_AP/AnyKey_Value/tmp.txt | 106 ++ .../AnyKey_Value/utils/__init__.py | 127 ++ .../AnyKey_Value/utils/ema_callbacks.py | 346 +++++ .../AnyKey_Value/utils/functions.py | 459 +++++++ .../AnyKey_Value/utils/kvu_dictionary.py | 138 ++ .../AnyKey_Value/utils/run_ocr.py | 30 + .../AnyKey_Value/utils/utils.py | 808 +++++++++++ .../Kie_Invoice_AP/AnyKey_Value/word2line.py | 226 ++++ .../AnyKey_Value/word_preprocess.py | 388 ++++++ cope2n-ai-fi/api/Kie_Invoice_AP/prediction.py | 58 + .../api/Kie_Invoice_AP/prediction_fi.py | 77 ++ .../api/Kie_Invoice_AP/prediction_sap.py | 146 ++ cope2n-ai-fi/api/Kie_Invoice_AP/tmp.txt | 106 ++ .../Kie_Invoice_AP/tmp_image/{image_url}.jpg | Bin 0 -> 1176111 bytes cope2n-ai-fi/api/OCRBase/prediction.py | 155 +++ cope2n-ai-fi/api/OCRBase/text_detection.py | 22 + cope2n-ai-fi/api/OCRBase/text_recognition.py | 39 + cope2n-ai-fi/api/manulife/predict_manulife.py | 98 ++ cope2n-ai-fi/api/sdsap_sbt/prediction_sbt.py | 94 ++ cope2n-ai-fi/celery_worker/__init__.py | 0 .../celery_worker/client_connector.py | 81 ++ .../celery_worker/client_connector_fi.py | 56 + .../celery_worker/mock_process_tasks.py | 220 +++ .../celery_worker/mock_process_tasks_fi.py | 74 + cope2n-ai-fi/celery_worker/worker.py | 40 + cope2n-ai-fi/celery_worker/worker_fi.py | 37 + .../common/AnyKey_Value/anyKeyValue.py | 101 ++ .../lightning_modules/__init__.py | 0 .../lightning_modules/classifier.py | 133 ++ .../lightning_modules/classifier_module.py | 390 ++++++ .../lightning_modules/schedulers.py | 53 + .../AnyKey_Value/lightning_modules/utils.py | 161 +++ .../common/AnyKey_Value/model/__init__.py | 15 + .../AnyKey_Value/model/combined_model.py | 76 ++ .../AnyKey_Value/model/document_kvu_model.py | 185 +++ .../common/AnyKey_Value/model/kvu_model.py | 248 ++++ .../AnyKey_Value/model/relation_extractor.py | 48 + cope2n-ai-fi/common/AnyKey_Value/predictor.py | 228 ++++ .../common/AnyKey_Value/preprocess.py | 456 +++++++ .../common/AnyKey_Value/requirements.txt | 30 + cope2n-ai-fi/common/AnyKey_Value/run.sh | 1 + cope2n-ai-fi/common/AnyKey_Value/tmp.txt | 393 ++++++ .../common/AnyKey_Value/utils/__init__.py | 127 ++ .../AnyKey_Value/utils/ema_callbacks.py | 346 +++++ .../AnyKey_Value/utils/kvu_dictionary.py | 68 + .../common/AnyKey_Value/utils/run_ocr.py | 33 + .../common/AnyKey_Value/utils/split_docs.py | 101 ++ .../common/AnyKey_Value/utils/utils.py | 548 ++++++++ .../common/AnyKey_Value/word_preprocess.py | 224 ++++ cope2n-ai-fi/common/crop_location.py | 172 +++ cope2n-ai-fi/common/dates_gplx.json | 622 +++++++++ cope2n-ai-fi/common/json2xml.py | 196 +++ cope2n-ai-fi/common/ocr.py | 37 + .../common/post_processing_datetime.py | 113 ++ cope2n-ai-fi/common/post_processing_driver.py | 51 + cope2n-ai-fi/common/post_processing_id.py | 51 + cope2n-ai-fi/common/process_pdf.py | 252 ++++ cope2n-ai-fi/common/serve_model.py | 93 ++ cope2n-ai-fi/common/utils/blurry_detection.py | 35 + cope2n-ai-fi/common/utils/global_variables.py | 71 + cope2n-ai-fi/common/utils/layoutLM_utils.py | 78 ++ cope2n-ai-fi/common/utils/merge_box.py | 163 +++ cope2n-ai-fi/common/utils/ocr_yolox.py | 77 ++ cope2n-ai-fi/common/utils/process_label.py | 139 ++ cope2n-ai-fi/common/utils/utils.py | 180 +++ cope2n-ai-fi/common/utils/word_formation.py | 599 +++++++++ .../common/utils_invoice/load_model.py | 61 + cope2n-ai-fi/common/utils_invoice/run_ocr.py | 13 + cope2n-ai-fi/common/utils_kvu/split_docs.py | 149 ++ .../common/utils_ocr/create_kie_labels.py | 67 + cope2n-ai-fi/configs/config_id_dr/__init__.py | 8 + cope2n-ai-fi/configs/config_id_dr/config.py | 212 +++ .../config_invoice/layoutxlm_base_invoice.py | 67 + cope2n-ai-fi/configs/config_ocr/__init__.py | 8 + cope2n-ai-fi/configs/config_ocr/config.py | 79 ++ cope2n-ai-fi/configs/default_env.py | 3 + cope2n-ai-fi/configs/manulife/__init__.py | 3 + cope2n-ai-fi/configs/manulife/configs.py | 35 + cope2n-ai-fi/configs/sdsap_sbt/__init__.py | 3 + cope2n-ai-fi/configs/sdsap_sbt/configs.py | 35 + cope2n-ai-fi/docker-compose.yaml | 48 + cope2n-ai-fi/dockerfile_old | 27 + .../modules/TemplateMatching/setting.yml | 21 + .../TemplateMatching/src/ocr_master.py | 87 ++ .../templatebasedextraction/.gitignore | 206 +++ .../templatebasedextraction/setting.yml | 17 + .../src/config/line_parser.py | 236 ++++ .../src/config/sift_based_aligner.py | 178 +++ .../src/modules/field_module.py | 1194 +++++++++++++++++ .../src/modules/line_parser.py | 34 + .../src/modules/ocr_module.py | 126 ++ .../src/modules/satrn_classifier.py | 15 + .../src/modules/sift_based_aligner.py | 385 ++++++ .../infer_img_template_aligner_delete.py | 86 ++ .../src/samples/run_crop_lines.py | 93 ++ .../src/samples/run_ocr.py | 46 + .../src/serve_model.py | 139 ++ .../src/utils/common.py | 24 + .../src/utils/image_calib.py | 803 +++++++++++ .../src/utils/pdf2image.py | 45 + .../src/utils/visualize.py | 271 ++++ .../textdetection/serve_model.py | 87 ++ .../textdetection/setting.yml | 7 + .../textrecognition/configs/satrn_big.py | 1115 +++++++++++++++ .../textrecognition/setting.yml | 6 + .../textrecognition/src/serve_model.py | 20 + cope2n-ai-fi/modules/__init__.py | 1 + cope2n-ai-fi/modules/_sdsvkvu/.gitignore | 24 + cope2n-ai-fi/modules/_sdsvkvu/.gitmodules | 4 + cope2n-ai-fi/modules/_sdsvkvu/LICENSE | 13 + cope2n-ai-fi/modules/_sdsvkvu/MANIFEST.in | 2 + cope2n-ai-fi/modules/_sdsvkvu/README.md | 122 ++ cope2n-ai-fi/modules/_sdsvkvu/__init__.py | 8 + cope2n-ai-fi/modules/_sdsvkvu/draw_img.jpg | Bin 0 -> 405763 bytes cope2n-ai-fi/modules/_sdsvkvu/pyproject.toml | 6 + .../modules/_sdsvkvu/requirements.txt | 28 + cope2n-ai-fi/modules/_sdsvkvu/scripts/run.sh | 26 + .../_sdsvkvu/sdsvkvu.egg-info/PKG-INFO | 122 ++ .../_sdsvkvu/sdsvkvu.egg-info/SOURCES.txt | 49 + .../sdsvkvu.egg-info/dependency_links.txt | 1 + .../_sdsvkvu/sdsvkvu.egg-info/not-zip-safe | 1 + .../_sdsvkvu/sdsvkvu.egg-info/requires.txt | 21 + .../_sdsvkvu/sdsvkvu.egg-info/top_level.txt | 1 + .../modules/_sdsvkvu/sdsvkvu/__init__.py | 4 + .../_sdsvkvu/sdsvkvu/externals/__init__.py | 0 .../sdsvkvu/externals/basic_ocr/.gitignore | 6 + .../sdsvkvu/externals/basic_ocr/README.md | 47 + .../sdsvkvu/externals/basic_ocr/TODO.todo | 10 + .../sdsvkvu/externals/basic_ocr/__init__.py | 11 + .../externals/sdsv_dewarp/.gitignore | 9 + .../basic_ocr/externals/sdsv_dewarp/README.md | 29 + .../externals/sdsv_dewarp/config/cls.yaml | 3 + .../externals/sdsv_dewarp/config/det.yaml | 8 + .../externals/sdsv_dewarp/requirements.txt | 7 + .../sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO | 45 + .../sdsv_dewarp.egg-info/SOURCES.txt | 15 + .../sdsv_dewarp.egg-info/dependency_links.txt | 1 + .../sdsv_dewarp.egg-info/not-zip-safe | 1 + .../sdsv_dewarp.egg-info/requires.txt | 6 + .../sdsv_dewarp.egg-info/top_level.txt | 1 + .../sdsv_dewarp/sdsv_dewarp/__init__.py | 0 .../externals/sdsv_dewarp/sdsv_dewarp/api.py | 200 +++ .../sdsv_dewarp/sdsv_dewarp/config.py | 41 + .../sdsv_dewarp/sdsv_dewarp/factory.py | 75 ++ .../sdsv_dewarp/sdsv_dewarp/models.py | 73 + .../sdsv_dewarp/sdsv_dewarp/utils.py | 212 +++ .../sdsv_dewarp/sdsv_dewarp/version.py | 1 + .../basic_ocr/externals/sdsv_dewarp/setup.py | 187 +++ .../basic_ocr/externals/sdsv_dewarp/test.py | 47 + .../externals/basic_ocr/requirements.txt | 82 ++ .../sdsvkvu/externals/basic_ocr/run.py | 200 +++ .../externals/basic_ocr/scripts/run_deskew.sh | 9 + .../externals/basic_ocr/scripts/run_ocr.sh | 49 + .../sdsvkvu/externals/basic_ocr/settings.yml | 35 + .../sdsvkvu/externals/basic_ocr/src/dto.py | 534 ++++++++ .../sdsvkvu/externals/basic_ocr/src/ocr.py | 258 ++++ .../sdsvkvu/externals/basic_ocr/src/utils.py | 369 +++++ .../externals/basic_ocr/src/word_formation.py | 903 +++++++++++++ cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/main.py | 147 ++ .../_sdsvkvu/sdsvkvu/model/__init__.py | 45 + .../_sdsvkvu/sdsvkvu/model/combined_model.py | 71 + .../sdsvkvu/model/document_kvu_model.py | 162 +++ .../_sdsvkvu/sdsvkvu/model/kvu_model.py | 300 +++++ .../sdsvkvu/model/relation_extractor.py | 48 + .../_sdsvkvu/sdsvkvu/model/sbt_model.py | 156 +++ .../_sdsvkvu/sdsvkvu/modules/__init__.py | 0 .../_sdsvkvu/sdsvkvu/modules/predictor.py | 225 ++++ .../_sdsvkvu/sdsvkvu/modules/preprocess.py | 479 +++++++ .../_sdsvkvu/sdsvkvu/modules/run_ocr.py | 25 + .../modules/_sdsvkvu/sdsvkvu/settings.yml | 21 + .../_sdsvkvu/sdsvkvu/sources/__init__.py | 0 .../modules/_sdsvkvu/sdsvkvu/sources/kvu.py | 73 + .../modules/_sdsvkvu/sdsvkvu/sources/utils.py | 610 +++++++++ .../_sdsvkvu/sdsvkvu/utils/__init__.py | 0 .../sdsvkvu/utils/dictionary/__init__.py | 0 .../utils/dictionary/list_retailers.txt | 167 +++ .../sdsvkvu/utils/dictionary/manulife.py | 69 + .../_sdsvkvu/sdsvkvu/utils/dictionary/sbt.py | 32 + .../sdsvkvu/utils/dictionary/sbt_v2.py | 116 ++ .../_sdsvkvu/sdsvkvu/utils/dictionary/vat.py | 69 + .../_sdsvkvu/sdsvkvu/utils/dictionary/vtb.py | 33 + .../_sdsvkvu/sdsvkvu/utils/post_processing.py | 362 +++++ .../_sdsvkvu/sdsvkvu/utils/query/__init__.py | 0 .../_sdsvkvu/sdsvkvu/utils/query/all.py | 75 ++ .../_sdsvkvu/sdsvkvu/utils/query/manulife.py | 133 ++ .../_sdsvkvu/sdsvkvu/utils/query/sbt.py | 186 +++ .../_sdsvkvu/sdsvkvu/utils/query/sbt_v2.py | 320 +++++ .../_sdsvkvu/sdsvkvu/utils/query/vat.py | 237 ++++ .../_sdsvkvu/sdsvkvu/utils/query/vtb.py | 153 +++ .../modules/_sdsvkvu/sdsvkvu/utils/utils.py | 129 ++ .../_sdsvkvu/sdsvkvu/utils/word2line.py | 226 ++++ cope2n-ai-fi/modules/_sdsvkvu/setup.cfg | 49 + cope2n-ai-fi/modules/_sdsvkvu/setup.py | 181 +++ cope2n-ai-fi/modules/_sdsvkvu/test.py | 18 + cope2n-ai-fi/modules/ocr_engine/.gitignore | 6 + cope2n-ai-fi/modules/ocr_engine/README.md | 47 + cope2n-ai-fi/modules/ocr_engine/TODO.todo | 10 + cope2n-ai-fi/modules/ocr_engine/__init__.py | 19 + .../externals/sdsv_dewarp/.gitignore | 9 + .../externals/sdsv_dewarp/README.md | 29 + .../externals/sdsv_dewarp/config/cls.yaml | 3 + .../externals/sdsv_dewarp/config/det.yaml | 8 + .../externals/sdsv_dewarp/requirements.txt | 7 + .../sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO | 45 + .../sdsv_dewarp.egg-info/SOURCES.txt | 15 + .../sdsv_dewarp.egg-info/dependency_links.txt | 1 + .../sdsv_dewarp.egg-info/not-zip-safe | 1 + .../sdsv_dewarp.egg-info/requires.txt | 6 + .../sdsv_dewarp.egg-info/top_level.txt | 1 + .../sdsv_dewarp/sdsv_dewarp/__init__.py | 0 .../externals/sdsv_dewarp/sdsv_dewarp/api.py | 200 +++ .../sdsv_dewarp/sdsv_dewarp/config.py | 41 + .../sdsv_dewarp/sdsv_dewarp/factory.py | 75 ++ .../sdsv_dewarp/sdsv_dewarp/models.py | 73 + .../sdsv_dewarp/sdsv_dewarp/utils.py | 212 +++ .../sdsv_dewarp/sdsv_dewarp/version.py | 1 + .../ocr_engine/externals/sdsv_dewarp/setup.py | 187 +++ .../ocr_engine/externals/sdsv_dewarp/test.py | 47 + .../modules/ocr_engine/requirements.txt | 82 ++ cope2n-ai-fi/modules/ocr_engine/run.py | 200 +++ .../modules/ocr_engine/scripts/run_deskew.sh | 9 + .../modules/ocr_engine/scripts/run_ocr.sh | 49 + cope2n-ai-fi/modules/ocr_engine/settings.yml | 36 + cope2n-ai-fi/modules/ocr_engine/src/dto.py | 534 ++++++++ cope2n-ai-fi/modules/ocr_engine/src/ocr.py | 258 ++++ cope2n-ai-fi/modules/ocr_engine/src/utils.py | 369 +++++ .../modules/ocr_engine/src/word_formation.py | 903 +++++++++++++ cope2n-ai-fi/modules/sdsvkvu | 1 + cope2n-ai-fi/requirements.txt | 10 + cope2n-ai-fi/run.sh | 9 + 277 files changed, 36106 insertions(+), 1 deletion(-) create mode 100644 .gitmodules create mode 100644 cope2n-ai-fi/._gitmodules create mode 100755 cope2n-ai-fi/.dockerignore create mode 100755 cope2n-ai-fi/.gitignore create mode 100755 cope2n-ai-fi/Dockerfile create mode 100755 cope2n-ai-fi/Dockerfile-dev create mode 100644 cope2n-ai-fi/Dockerfile_fwd create mode 100755 cope2n-ai-fi/LICENSE create mode 100755 cope2n-ai-fi/NOTE.md create mode 100755 cope2n-ai-fi/README.md create mode 100644 cope2n-ai-fi/TODO.md create mode 100755 cope2n-ai-fi/api/Kie_AHung/prediction.py create mode 100755 cope2n-ai-fi/api/Kie_AHung_ID/prediction.py create mode 100755 cope2n-ai-fi/api/Kie_Hoanglv/prediction.py create mode 100755 cope2n-ai-fi/api/Kie_Hoanglv/serve_model.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/__init__.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/anyKeyValue.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/__init__.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier_module.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/__init__.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/data_module.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/kvu_dataset.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/schedulers.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/utils.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/__init__.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/combined_model.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/document_kvu_model.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/kvu_model.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/relation_extractor.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitignore create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitmodules create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/README.md create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/__init__.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/requirements.txt create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/run.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/scripts/run_ocr.sh create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/settings.yml create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/dto.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/ocr.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/utils.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/word_formation.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/predictor.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/preprocess.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/requirements.txt create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/run.sh create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/tmp.txt create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/__init__.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/ema_callbacks.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/functions.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/kvu_dictionary.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/run_ocr.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/utils.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word2line.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word_preprocess.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/prediction.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/prediction_fi.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/prediction_sap.py create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/tmp.txt create mode 100755 cope2n-ai-fi/api/Kie_Invoice_AP/tmp_image/{image_url}.jpg create mode 100755 cope2n-ai-fi/api/OCRBase/prediction.py create mode 100755 cope2n-ai-fi/api/OCRBase/text_detection.py create mode 100755 cope2n-ai-fi/api/OCRBase/text_recognition.py create mode 100644 cope2n-ai-fi/api/manulife/predict_manulife.py create mode 100755 cope2n-ai-fi/api/sdsap_sbt/prediction_sbt.py create mode 100755 cope2n-ai-fi/celery_worker/__init__.py create mode 100755 cope2n-ai-fi/celery_worker/client_connector.py create mode 100755 cope2n-ai-fi/celery_worker/client_connector_fi.py create mode 100755 cope2n-ai-fi/celery_worker/mock_process_tasks.py create mode 100755 cope2n-ai-fi/celery_worker/mock_process_tasks_fi.py create mode 100755 cope2n-ai-fi/celery_worker/worker.py create mode 100755 cope2n-ai-fi/celery_worker/worker_fi.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/anyKeyValue.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/lightning_modules/__init__.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier_module.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/lightning_modules/schedulers.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/lightning_modules/utils.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/model/__init__.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/model/combined_model.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/model/document_kvu_model.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/model/kvu_model.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/model/relation_extractor.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/predictor.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/preprocess.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/requirements.txt create mode 100755 cope2n-ai-fi/common/AnyKey_Value/run.sh create mode 100755 cope2n-ai-fi/common/AnyKey_Value/tmp.txt create mode 100755 cope2n-ai-fi/common/AnyKey_Value/utils/__init__.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/utils/ema_callbacks.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/utils/kvu_dictionary.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/utils/run_ocr.py create mode 100644 cope2n-ai-fi/common/AnyKey_Value/utils/split_docs.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/utils/utils.py create mode 100755 cope2n-ai-fi/common/AnyKey_Value/word_preprocess.py create mode 100755 cope2n-ai-fi/common/crop_location.py create mode 100755 cope2n-ai-fi/common/dates_gplx.json create mode 100755 cope2n-ai-fi/common/json2xml.py create mode 100755 cope2n-ai-fi/common/ocr.py create mode 100755 cope2n-ai-fi/common/post_processing_datetime.py create mode 100755 cope2n-ai-fi/common/post_processing_driver.py create mode 100755 cope2n-ai-fi/common/post_processing_id.py create mode 100755 cope2n-ai-fi/common/process_pdf.py create mode 100755 cope2n-ai-fi/common/serve_model.py create mode 100755 cope2n-ai-fi/common/utils/blurry_detection.py create mode 100755 cope2n-ai-fi/common/utils/global_variables.py create mode 100755 cope2n-ai-fi/common/utils/layoutLM_utils.py create mode 100755 cope2n-ai-fi/common/utils/merge_box.py create mode 100755 cope2n-ai-fi/common/utils/ocr_yolox.py create mode 100755 cope2n-ai-fi/common/utils/process_label.py create mode 100755 cope2n-ai-fi/common/utils/utils.py create mode 100755 cope2n-ai-fi/common/utils/word_formation.py create mode 100755 cope2n-ai-fi/common/utils_invoice/load_model.py create mode 100755 cope2n-ai-fi/common/utils_invoice/run_ocr.py create mode 100755 cope2n-ai-fi/common/utils_kvu/split_docs.py create mode 100755 cope2n-ai-fi/common/utils_ocr/create_kie_labels.py create mode 100755 cope2n-ai-fi/configs/config_id_dr/__init__.py create mode 100755 cope2n-ai-fi/configs/config_id_dr/config.py create mode 100755 cope2n-ai-fi/configs/config_invoice/layoutxlm_base_invoice.py create mode 100755 cope2n-ai-fi/configs/config_ocr/__init__.py create mode 100755 cope2n-ai-fi/configs/config_ocr/config.py create mode 100644 cope2n-ai-fi/configs/default_env.py create mode 100644 cope2n-ai-fi/configs/manulife/__init__.py create mode 100644 cope2n-ai-fi/configs/manulife/configs.py create mode 100644 cope2n-ai-fi/configs/sdsap_sbt/__init__.py create mode 100644 cope2n-ai-fi/configs/sdsap_sbt/configs.py create mode 100755 cope2n-ai-fi/docker-compose.yaml create mode 100755 cope2n-ai-fi/dockerfile_old create mode 100755 cope2n-ai-fi/modules/TemplateMatching/setting.yml create mode 100755 cope2n-ai-fi/modules/TemplateMatching/src/ocr_master.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/.gitignore create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/setting.yml create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/line_parser.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/sift_based_aligner.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/field_module.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/line_parser.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/ocr_module.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/satrn_classifier.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/sift_based_aligner.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/infer_img_template_aligner_delete.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_crop_lines.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_ocr.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/serve_model.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/common.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/image_calib.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/pdf2image.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/visualize.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/textdetection/serve_model.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/textdetection/setting.yml create mode 100755 cope2n-ai-fi/modules/TemplateMatching/textrecognition/configs/satrn_big.py create mode 100755 cope2n-ai-fi/modules/TemplateMatching/textrecognition/setting.yml create mode 100755 cope2n-ai-fi/modules/TemplateMatching/textrecognition/src/serve_model.py create mode 100644 cope2n-ai-fi/modules/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/.gitignore create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/.gitmodules create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/LICENSE create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/MANIFEST.in create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/README.md create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/draw_img.jpg create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/pyproject.toml create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/requirements.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/scripts/run.sh create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/PKG-INFO create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/SOURCES.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/dependency_links.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/not-zip-safe create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/requires.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/top_level.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/.gitignore create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/README.md create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/TODO.todo create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/.gitignore create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/README.md create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/cls.yaml create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/det.yaml create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/requirements.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/api.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/config.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/factory.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/models.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/utils.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/version.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/setup.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/test.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/requirements.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/run.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_deskew.sh create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_ocr.sh create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/settings.yml create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/dto.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/ocr.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/utils.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/word_formation.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/main.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/combined_model.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/document_kvu_model.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/kvu_model.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/relation_extractor.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/sbt_model.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/predictor.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/preprocess.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/run_ocr.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/settings.yml create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/kvu.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/utils.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/list_retailers.txt create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/manulife.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt_v2.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vat.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vtb.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/post_processing.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/__init__.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/all.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/manulife.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt_v2.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vat.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vtb.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/utils.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/word2line.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/setup.cfg create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/setup.py create mode 100644 cope2n-ai-fi/modules/_sdsvkvu/test.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/.gitignore create mode 100644 cope2n-ai-fi/modules/ocr_engine/README.md create mode 100644 cope2n-ai-fi/modules/ocr_engine/TODO.todo create mode 100644 cope2n-ai-fi/modules/ocr_engine/__init__.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/.gitignore create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/README.md create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/requirements.txt create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/__init__.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/api.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/config.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/factory.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/models.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/utils.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/version.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/setup.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/test.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/requirements.txt create mode 100644 cope2n-ai-fi/modules/ocr_engine/run.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/scripts/run_deskew.sh create mode 100644 cope2n-ai-fi/modules/ocr_engine/scripts/run_ocr.sh create mode 100644 cope2n-ai-fi/modules/ocr_engine/settings.yml create mode 100644 cope2n-ai-fi/modules/ocr_engine/src/dto.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/src/ocr.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/src/utils.py create mode 100644 cope2n-ai-fi/modules/ocr_engine/src/word_formation.py create mode 160000 cope2n-ai-fi/modules/sdsvkvu create mode 100755 cope2n-ai-fi/requirements.txt create mode 100755 cope2n-ai-fi/run.sh diff --git a/.gitignore b/.gitignore index 8004166..1ae0e35 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ backup/ *.sqlite3 *.log __pycache__ -migrations/ \ No newline at end of file +migrations/ +test/ +._git/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c1f5013 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cope2n-ai-fi/modules/sdsvkvu"] + path = cope2n-ai-fi/modules/sdsvkvu + url = https://code.sdsdev.co.kr/tuanlv/sdsvkvu diff --git a/cope2n-ai-fi/._gitmodules b/cope2n-ai-fi/._gitmodules new file mode 100644 index 0000000..9483200 --- /dev/null +++ b/cope2n-ai-fi/._gitmodules @@ -0,0 +1,6 @@ +[submodule "modules/sdsvkvu"] + path = modules/sdsvkvu + url = https://code.sdsdev.co.kr/tuanlv/sdsvkvu.git +[submodule "modules/ocr_engine"] + path = modules/ocr_engine + url = https://code.sdsdev.co.kr/tuanlv/IDP-BasicOCR.git diff --git a/cope2n-ai-fi/.dockerignore b/cope2n-ai-fi/.dockerignore new file mode 100755 index 0000000..70a2b49 --- /dev/null +++ b/cope2n-ai-fi/.dockerignore @@ -0,0 +1,7 @@ +.github +.git +.vscode +__pycache__ +DataBase/image_temp/ +DataBase/json_temp/ +DataBase/template.db \ No newline at end of file diff --git a/cope2n-ai-fi/.gitignore b/cope2n-ai-fi/.gitignore new file mode 100755 index 0000000..f2c8e28 --- /dev/null +++ b/cope2n-ai-fi/.gitignore @@ -0,0 +1,21 @@ +.vscode +__pycache__ +DataBase/image_temp/ +DataBase/json_temp/ +DataBase/template.db +sdsvtd/ +sdsvtr/ +sdsvkie/ +detectron2/ +output/ +data/ +models/ +server/ +image_logs/ +experiments/ +weights/ +packages/ +tmp_results/ +.env +.zip +.json \ No newline at end of file diff --git a/cope2n-ai-fi/Dockerfile b/cope2n-ai-fi/Dockerfile new file mode 100755 index 0000000..135713e --- /dev/null +++ b/cope2n-ai-fi/Dockerfile @@ -0,0 +1,43 @@ +# FROM thucpd2408/env-cope2n:v1 +FROM thucpd2408/env-deskew + +COPY ./packages/cudnn-linux*.tar.xz /tmp/cudnn-linux*.tar.xz + +RUN tar -xvf /tmp/cudnn-linux*.tar.xz -C /tmp/ \ + && cp /tmp/cudnn-*-archive/include/cudnn*.h /usr/local/cuda/include \ + && cp -P /tmp/cudnn-*-archive/lib/libcudnn* /usr/local/cuda/lib64 \ + && chmod a+r /usr/local/cuda/include/cudnn*.h /usr/local/cuda/lib64/libcudnn* \ + && rm -rf /tmp/cudnn-*-archive + +RUN apt-get update && apt-get install -y gcc g++ ffmpeg libsm6 libxext6 +# RUN pip install torch==1.13.1+cu116 torchvision==0.14.1+cu116 torchaudio==0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116 + +WORKDIR /workspace + + +COPY ./modules/ocr_engine/externals/ /workspace/cope2n-ai-fi/modules/ocr_engine/externals/ +COPY ./modules/ocr_engine/requirements.txt /workspace/cope2n-ai-fi/modules/ocr_engine/requirements.txt +COPY ./modules/sdsvkie/ /workspace/cope2n-ai-fi/modules/sdsvkie/ +COPY ./modules/sdsvkvu/ /workspace/cope2n-ai-fi/modules/sdsvkvu/ +COPY ./requirements.txt /workspace/cope2n-ai-fi/requirements.txt + +RUN cd /workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp && pip3 install -v -e . +RUN cd /workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsvtd && pip3 install -v -e . +RUN cd /workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsvtr && pip3 install -v -e . + +# RUN cd /workspace/cope2n-ai-fi/modules/ocr_engine/ && pip3 install -r requirements.txt +RUN cd /workspace/cope2n-ai-fi/modules/sdsvkie && pip3 install -v -e . +RUN cd /workspace/cope2n-ai-fi/modules/sdsvkvu && pip3 install -v -e . +RUN cd /workspace/cope2n-ai-fi && pip3 install -r requirements.txt + +RUN rm -f /usr/local/lib/python3.10/dist-packages/nvidia/cublas/lib/libcublasLt.so.11 && \ + rm -f /usr/local/lib/python3.10/dist-packages/nvidia/cublas/lib/libcublas.so.11 && \ + rm -f /usr/local/lib/python3.10/dist-packages/nvidia/cublas/lib/libnvblas.so.11 && \ + ln -s /usr/local/cuda-11.8/targets/x86_64-linux/lib/libcublasLt.so.11 /usr/local/lib/python3.10/dist-packages/nvidia/cublas/lib/libcublasLt.so.11 && \ + ln -s /usr/local/cuda-11.8/targets/x86_64-linux/lib/libcublas.so.11 /usr/local/lib/python3.10/dist-packages/nvidia/cublas/lib/libcublas.so.11 && \ + ln -s /usr/local/cuda-11.8/targets/x86_64-linux/lib/libnvblas.so.11 /usr/local/lib/python3.10/dist-packages/nvidia/cublas/lib/libnvblas.so.11 + +ENV PYTHONPATH="." + +CMD [ "sh", "run.sh"] +# CMD ["tail -f > /dev/null"] \ No newline at end of file diff --git a/cope2n-ai-fi/Dockerfile-dev b/cope2n-ai-fi/Dockerfile-dev new file mode 100755 index 0000000..f58c519 --- /dev/null +++ b/cope2n-ai-fi/Dockerfile-dev @@ -0,0 +1,21 @@ +FROM thucpd2408/env-cope2n:v1 + +RUN apt-get update && apt-get install -y gcc g++ ffmpeg libsm6 libxext6 + +WORKDIR /workspace + +COPY ./requirements.txt /workspace/cope2n-ai-fi/requirements.txt +COPY ./sdsvkie/ /workspace/cope2n-ai-fi/sdsvkie/ +COPY ./sdsvtd /workspace/cope2n-ai-fi/sdsvtd/ +COPY ./sdsvtr/ /workspace/cope2n-ai-fi/sdsvtr/ +COPY ./models/ /models + +RUN cd /workspace/cope2n-ai-fi && pip3 install -r requirements.txt +RUN cd /workspace/cope2n-ai-fi/sdsvkie && pip3 install -v -e . +RUN cd /workspace/cope2n-ai-fi/sdsvtd && pip3 install -v -e . +RUN cd /workspace/cope2n-ai-fi/sdsvtr && pip3 install -v -e . + +ENV PYTHONPATH="." + +CMD [ "sh", "run.sh"] +# CMD ["tail -f > /dev/null"] \ No newline at end of file diff --git a/cope2n-ai-fi/Dockerfile_fwd b/cope2n-ai-fi/Dockerfile_fwd new file mode 100644 index 0000000..e93fc0c --- /dev/null +++ b/cope2n-ai-fi/Dockerfile_fwd @@ -0,0 +1,8 @@ +FROM hisiter/fwd_env:1.0.0 +RUN apt-get update && apt-get install -y \ + libssl-dev \ + libxml2-dev \ + libxslt-dev \ + && rm -rf /var/lib/apt/lists/* +RUN pip install paddlepaddle-gpu==2.4.2.post116 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +RUN pip install paddleocr>=2.0.1 \ No newline at end of file diff --git a/cope2n-ai-fi/LICENSE b/cope2n-ai-fi/LICENSE new file mode 100755 index 0000000..f288702 --- /dev/null +++ b/cope2n-ai-fi/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/cope2n-ai-fi/NOTE.md b/cope2n-ai-fi/NOTE.md new file mode 100755 index 0000000..9d8e22d --- /dev/null +++ b/cope2n-ai-fi/NOTE.md @@ -0,0 +1,7 @@ +#### Environment +``` +docker run -itd --privileged --name=TannedCungnoCope2n-ai-fi \ + -v /mnt/hdd2T/dxtan/TannedCung/OCR/cope2n-ai-fi:/workspace \ + tannedcung/mmocr:latest \ + tail -f > /dev/null +``` \ No newline at end of file diff --git a/cope2n-ai-fi/README.md b/cope2n-ai-fi/README.md new file mode 100755 index 0000000..72c209b --- /dev/null +++ b/cope2n-ai-fi/README.md @@ -0,0 +1,36 @@ +# AI-core + +## Add your files + +- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files +- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: + +```bash +cd existing_repo +git remote add origin http://code.sdsrv.vn/c-ope2n/ai-core.git +git branch -M main +git push -uf origin main +``` + +## Develop + +Assume you are at root folder with struct: + +```bash +. +├── cope2n-ai-fi +├── cope2n-api +├── cope2n-fe +├── .env +└── docker-compose-dev.yml +``` + +Run: `docker-compose -f docker-compose-dev.yml up --build -d` to bring the project alive + +## Environment Variables + +Variable | Default | Usage | Note +------------- | ------- | ----- | ---- +CELERY_BROKER | $250 | | | +SAP_KIE_MODEL | $80 | | | +FI_KIE_MODEL | $420 | | | diff --git a/cope2n-ai-fi/TODO.md b/cope2n-ai-fi/TODO.md new file mode 100644 index 0000000..9a1c625 --- /dev/null +++ b/cope2n-ai-fi/TODO.md @@ -0,0 +1,26 @@ +## Bring abs path to relative + +- [x] save_dir `Kie_Invoice_AP/prediction_sap.py:18` +- [x] detector `Kie_Invoice_AP/AnyKey_Value/ocr-engine/settings.yml` [_fixed_](#refactor) +- [x] rotator_version `Kie_Invoice_AP/AnyKey_Value/ocr-engine/settings.yml` [_fixed_](#refactor) +- [x] cfg `Kie_Invoice_AP/prediction_fi.py` +- [x] weight `Kie_Invoice_AP/prediction_fi.py` +- [x] save_dir `Kie_Invoice_AP/prediction_fi.py:18` + +## Bring abs path to .env + +- [x] CELERY_BROKER: +- [ ] SAP_KIE_MODEL: `Kie_Invoice_AP/prediction_sap.py:20` [_NEED_REFACTOR_](#refactor) +- [ ] FI_KIE_MODEL: `Kie_Invoice_AP/prediction_fi.py:20` [_NEED_REFACTOR_](#refactor) + +## Possible logic confict + +### Refactor + +- [ ] Each model should be loaded in a docker container and serve as a service +- [ ] Some files (weights, ...) should be mounted in container in a format for endurability +- [ ] `Kie_Invoice_AP/prediction_fi.py` and `Kie_Invoice_AP/prediction_fi.py` should be merged into a single file as it shared resources with different logic +- [ ] `Kie_Invoice_AP/prediction.py` seems to be the base function, this should act as a proxy which import all other `predict_{anything else}` functions +- [ ] There should be a unique folder to keep all models with different versions then mount as /models in container. Currently, `fi` is loading from `/models/Kie_invoice_fi` while `sap` is loading from `Kie_Invoice_AP/AnyKey_Value/experiments/key_value_understanding-20231003-171748`. Another model weight is at `sdsvtd/hub` for unknown reason +- [ ] Env variables should have its description in README +- [ ] diff --git a/cope2n-ai-fi/api/Kie_AHung/prediction.py b/cope2n-ai-fi/api/Kie_AHung/prediction.py new file mode 100755 index 0000000..4755df2 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_AHung/prediction.py @@ -0,0 +1,149 @@ +from PIL import Image +import cv2 + +import numpy as np +from transformers import ( + LayoutXLMTokenizer, + LayoutLMv2FeatureExtractor, + LayoutXLMProcessor, + LayoutLMv2ForTokenClassification, +) + +from common.utils.word_formation import * + +from common.utils.global_variables import * +from common.utils.process_label import * +import ssl + +ssl._create_default_https_context = ssl._create_unverified_context +os.environ["CURL_CA_BUNDLE"] = "" +from PIL import ImageFile + +ImageFile.LOAD_TRUNCATED_IMAGES = True + + +# config +IGNORE_KIE_LABEL = "others" +KIE_LABELS = [ + "Number", + "Name", + "Birthday", + "Home Town", + "Address", + "Sex", + "Nationality", + "Expiry Date", + "Nation", + "Religion", + "Date Range", + "Issued By", + IGNORE_KIE_LABEL, + "Rank" +] +DEVICE = "cuda:0" + +# MAX_SEQ_LENGTH = 512 # TODO Fix this hard code + +# tokenizer = LayoutXLMTokenizer.from_pretrained( +# "Kie_AHung/model/pretrained/layoutxlm-base/tokenizer", model_max_length=MAX_SEQ_LENGTH +# ) + +# feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) +# processor = LayoutXLMProcessor(feature_extractor, tokenizer) + +model = LayoutLMv2ForTokenClassification.from_pretrained( + "Kie_AHung/model/driver_license", num_labels=len(KIE_LABELS), local_files_only=True +).to( + DEVICE +) # TODO FIX this hard code + + +def load_ocr_labels(list_lines): + words, boxes, labels = [], [], [] + for line in list_lines: + for word_group in line.list_word_groups: + for word in word_group.list_words: + xmin, ymin, xmax, ymax = ( + word.boundingbox[0], + word.boundingbox[1], + word.boundingbox[2], + word.boundingbox[3], + ) + text = word.text + label = "seller_name_value" # TODO ??? fix this + x1, y1, x2, y2 = float(xmin), float(ymin), float(xmax), float(ymax) + if text != " ": + words.append(text) + boxes.append([x1, y1, x2, y2]) + labels.append(label) + return words, boxes, labels + + +def _normalize_box(box, width, height): + return [ + int(1000 * (box[0] / width)), + int(1000 * (box[1] / height)), + int(1000 * (box[2] / width)), + int(1000 * (box[3] / height)), + ] + + +def infer_driving_license(image_crop, list_lines, max_n_words, processor): + # Load inputs + # image = Image.open(image_path) + image = cv2.cvtColor(image_crop, cv2.COLOR_BGR2RGB) + image = Image.fromarray(image) + batch_words, batch_boxes, _ = load_ocr_labels(list_lines) + batch_preds, batch_true_boxes = [], [] + list_words = [] + for i in range(0, len(batch_words), max_n_words): + words = batch_words[i : i + max_n_words] + boxes = batch_boxes[i : i + max_n_words] + boxes_norm = [ + _normalize_box(bbox, image.size[0], image.size[1]) for bbox in boxes + ] + + # Preprocess + dummy_word_labels = [0] * len(words) + encoding = processor( + image, + text=words, + boxes=boxes_norm, + word_labels=dummy_word_labels, + return_tensors="pt", + padding="max_length", + truncation=True, + max_length=512, + ) + + # Run model + for k, v in encoding.items(): + encoding[k] = v.to(DEVICE) + outputs = model(**encoding) + predictions = outputs.logits.argmax(-1).squeeze().tolist() + + # Postprocess + is_subword = ( + (encoding["labels"] == -100).detach().cpu().numpy()[0] + ) # remove padding + true_predictions = [ + pred for idx, pred in enumerate(predictions) if not is_subword[idx] + ] + true_boxes = ( + boxes # TODO check assumption that layourlm do not change box order + ) + + for i, word in enumerate(words): + bndbox = [int(j) for j in true_boxes[i]] + list_words.append( + Word( + text=word, bndbox=bndbox, kie_label=KIE_LABELS[true_predictions[i]] + ) + ) + + batch_preds.extend(true_predictions) + batch_true_boxes.extend(true_boxes) + + batch_preds = np.array(batch_preds) + batch_true_boxes = np.array(batch_true_boxes) + return batch_words, batch_preds, batch_true_boxes, list_words diff --git a/cope2n-ai-fi/api/Kie_AHung_ID/prediction.py b/cope2n-ai-fi/api/Kie_AHung_ID/prediction.py new file mode 100755 index 0000000..ec1b22c --- /dev/null +++ b/cope2n-ai-fi/api/Kie_AHung_ID/prediction.py @@ -0,0 +1,145 @@ +from PIL import Image +import numpy as np +import cv2 + +from transformers import LayoutLMv2ForTokenClassification + +from common.utils.word_formation import * + +from common.utils.global_variables import * +from common.utils.process_label import * +import ssl + +ssl._create_default_https_context = ssl._create_unverified_context +os.environ["CURL_CA_BUNDLE"] = "" +from PIL import ImageFile + +ImageFile.LOAD_TRUNCATED_IMAGES = True + + +# config +IGNORE_KIE_LABEL = "others" +KIE_LABELS = [ + "Number", + "Name", + "Birthday", + "Home Town", + "Address", + "Sex", + "Nationality", + "Expiry Date", + "Nation", + "Religion", + "Date Range", + "Issued By", + IGNORE_KIE_LABEL +] +DEVICE = "cuda:0" + +# MAX_SEQ_LENGTH = 512 # TODO Fix this hard code + +# tokenizer = LayoutXLMTokenizer.from_pretrained( +# "Kie_AHung_ID/model/pretrained/layoutxlm-base/tokenizer", +# model_max_length=MAX_SEQ_LENGTH, +# ) + +# feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) +# processor = LayoutXLMProcessor(feature_extractor, tokenizer) + +model = LayoutLMv2ForTokenClassification.from_pretrained( + "Kie_AHung_ID/model/ID_CARD_145_train_300_val_0.02_char_0.06_word", + num_labels=len(KIE_LABELS), + local_files_only=True, +).to( + DEVICE +) # TODO FIX this hard code + + +def load_ocr_labels(list_lines): + words, boxes, labels = [], [], [] + for line in list_lines: + for word_group in line.list_word_groups: + for word in word_group.list_words: + xmin, ymin, xmax, ymax = ( + word.boundingbox[0], + word.boundingbox[1], + word.boundingbox[2], + word.boundingbox[3], + ) + text = word.text + label = "seller_name_value" # TODO ??? fix this + x1, y1, x2, y2 = float(xmin), float(ymin), float(xmax), float(ymax) + if text != " ": + words.append(text) + boxes.append([x1, y1, x2, y2]) + labels.append(label) + return words, boxes, labels + + +def _normalize_box(box, width, height): + return [ + int(1000 * (box[0] / width)), + int(1000 * (box[1] / height)), + int(1000 * (box[2] / width)), + int(1000 * (box[3] / height)), + ] + + +def infer_id_card(image_crop, list_lines, max_n_words, processor): + # Load inputs + image = cv2.cvtColor(image_crop, cv2.COLOR_BGR2RGB) + image = Image.fromarray(image) + batch_words, batch_boxes, _ = load_ocr_labels(list_lines) + batch_preds, batch_true_boxes = [], [] + list_words = [] + for i in range(0, len(batch_words), max_n_words): + words = batch_words[i : i + max_n_words] + boxes = batch_boxes[i : i + max_n_words] + boxes_norm = [ + _normalize_box(bbox, image.size[0], image.size[1]) for bbox in boxes + ] + + # Preprocess + dummy_word_labels = [0] * len(words) + encoding = processor( + image, + text=words, + boxes=boxes_norm, + word_labels=dummy_word_labels, + return_tensors="pt", + padding="max_length", + truncation=True, + max_length=512, + ) + + # Run model + for k, v in encoding.items(): + encoding[k] = v.to(DEVICE) + outputs = model(**encoding) + predictions = outputs.logits.argmax(-1).squeeze().tolist() + + # Postprocess + is_subword = ( + (encoding["labels"] == -100).detach().cpu().numpy()[0] + ) # remove padding + true_predictions = [ + pred for idx, pred in enumerate(predictions) if not is_subword[idx] + ] + true_boxes = ( + boxes # TODO check assumption that layourlm do not change box order + ) + + for i, word in enumerate(words): + bndbox = [int(j) for j in true_boxes[i]] + list_words.append( + Word( + text=word, bndbox=bndbox, kie_label=KIE_LABELS[true_predictions[i]] + ) + ) + + batch_preds.extend(true_predictions) + batch_true_boxes.extend(true_boxes) + + batch_preds = np.array(batch_preds) + batch_true_boxes = np.array(batch_true_boxes) + return batch_words, batch_preds, batch_true_boxes, list_words diff --git a/cope2n-ai-fi/api/Kie_Hoanglv/prediction.py b/cope2n-ai-fi/api/Kie_Hoanglv/prediction.py new file mode 100755 index 0000000..7fbe734 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Hoanglv/prediction.py @@ -0,0 +1,57 @@ +from sdsvkie import Predictor +import cv2 +import numpy as np +import urllib +from common import serve_model +from common import ocr + +model = Predictor( + cfg = "./models/kie_invoice/config.yaml", + device = "cuda:0", + weights = "./models/models/kie_invoice/last", + proccessor = serve_model.processor, + ocr_engine = ocr.engine + ) + +def predict(page_numb, image_url): + """ + module predict function + + Args: + image_url (str): image url + + Returns: + example output: + "data": { + "document_type": "invoice", + "fields": [ + { + "label": "Invoice Number", + "value": "INV-12345", + "box": [0, 0, 0, 0], + "confidence": 0.98 + }, + ... + ] + } + dict: output of model + """ + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + image = cv2.imdecode(arr, -1) + out = model(image) + output = out["end2end_results"] + output_dict = { + "document_type": "invoice", + "fields": [] + } + for key in output.keys(): + field = { + "label": key, + "value": output[key]['value'] if output[key]['value'] else "", + "box": output[key]['box'], + "confidence": output[key]['conf'], + "page": page_numb + } + output_dict['fields'].append(field) + return output_dict \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Hoanglv/serve_model.py b/cope2n-ai-fi/api/Kie_Hoanglv/serve_model.py new file mode 100755 index 0000000..e6ef934 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Hoanglv/serve_model.py @@ -0,0 +1,83 @@ +from common.utils_invoice.run_ocr import ocr_predict +import os +from Kie_Hoanglv.prediction2 import KIEInvoiceInfer +from configs.config_invoice.layoutxlm_base_invoice import * + +from PIL import Image +import requests +from io import BytesIO +import numpy as np +import cv2 + +model = KIEInvoiceInfer( + weight_dir=TRAINED_DIR, + tokenizer_dir=TOKENIZER_DIR, + max_seq_len=MAX_SEQ_LENGTH, + classes=KIE_LABELS, + device=DEVICE, + outdir_visualize=VISUALIZE_DIR, +) + +def format_result(result): + """ + return: + [ + { + key: 'name', + value: 'Nguyen Hoang Hiep', + true_box: [ + 373, + 113, + 700, + 420 + ] + }, + { + key: 'name', + value: 'Nguyen Hoang Hiep 1', + true_box: [ + 10, + 10, + 20, + 20, + ] + }, + ] + """ + new_result = [] + for i, item in enumerate(result[0]): + new_result.append( + { + "key": item, + "value": result[0][item], + "true_box": result[1][i], + } + ) + return new_result + +def predict(image_url): + if not os.path.exists(PRED_DIR): + os.makedirs(PRED_DIR, exist_ok=True) + + if not os.path.exists(VISUALIZE_DIR): + os.makedirs(VISUALIZE_DIR, exist_ok=True) + + + response = requests.get(image_url) + image = Image.open(BytesIO(response.content)) + + cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + + bboxes, texts = ocr_predict(cv_image) + + texts_replaced = [] + for text in texts: + if "✪" in text: + text_replaced = text.replace("✪", " ") + texts_replaced.append(text_replaced) + else: + texts_replaced.append(text) + inputs = model.prepare_kie_inputs(image, ocr_info=[bboxes, texts_replaced]) + result = model(inputs) + result = format_result(result) + return result \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/__init__.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/anyKeyValue.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/anyKeyValue.py new file mode 100755 index 0000000..dd21d2f --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/anyKeyValue.py @@ -0,0 +1,137 @@ +import os +import glob +import cv2 +import argparse +from tqdm import tqdm +import urllib +import numpy as np +import imagesize +# from omegaconf import OmegaConf +import sys +cur_dir = os.path.dirname(__file__) +sys.path.append(cur_dir) +# sys.path.append('/cope2n-ai-fi/Kie_Invoice_AP/AnyKey_Value/') +from predictor import KVUPredictor +from preprocess import KVUProcess, DocumentKVUProcess +from utils.utils import create_dir, visualize, get_colormap, export_kvu_outputs, export_kvu_for_manulife + + +def get_args(): + args = argparse.ArgumentParser(description='Main file') + args.add_argument('--img_dir', default='/home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/visualize/test/', type=str, + help='Input image directory') + args.add_argument('--save_dir', default='/home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/visualize/test/', type=str, + help='Save directory') + # args.add_argument('--exp_dir', default='/home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/experiments/key_value_understanding-20230608-171900', type=str, + # help='Checkpoint and config of model') + args.add_argument('--exp_dir', default='/home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/experiments/key_value_understanding-20230608-171900', type=str, + help='Checkpoint and config of model') + args.add_argument('--export_img', default=0, type=int, + help='Save visualize on image') + args.add_argument('--mode', default=3, type=int, + help="0:'normal' - 1:'full_tokens' - 2:'sliding' - 3: 'document'") + args.add_argument('--dir_level', default=0, type=int, + help='Number of subfolders contains image') + + return args.parse_args() + + +def load_engine(exp_dir: str, class_names: list, mode: int) -> KVUPredictor: + configs = { + 'cfg': glob.glob(f'{exp_dir}/*.yaml')[0], + 'ckpt': f'{exp_dir}/checkpoints/best_model.pth' + } + dummy_idx = 512 + predictor = KVUPredictor(configs, class_names, dummy_idx, mode) + + # processor = KVUProcess(predictor.net.tokenizer_layoutxlm, + # predictor.net.feature_extractor, predictor.backbone_type, class_names, + # predictor.slice_interval, predictor.window_size, run_ocr=1, mode=mode) + + processor = DocumentKVUProcess(predictor.net.tokenizer, predictor.net.feature_extractor, + predictor.backbone_type, class_names, + predictor.max_window_count, predictor.slice_interval, predictor.window_size, + run_ocr=1, mode=mode) + return predictor, processor + +def revert_box(box, width, height): + return [ + int((box[0] / 1000) * width), + int((box[1] / 1000) * height), + int((box[2] / 1000) * width), + int((box[3] / 1000) * height) + ] + +def predict_image(img_path: str, save_dir: str, predictor: KVUPredictor, processor) -> None: + fname = os.path.basename(img_path) + img_ext = img_path.split('.')[-1] + inputs = processor(img_path, ocr_path='') + width, height = imagesize.get(img_path) + + bbox, lwords, pr_class_words, pr_relations = predictor.predict(inputs) + # slide_window = False if len(bbox) == 1 else True + + if len(bbox) == 0: + bbox, lwords, pr_class_words, pr_relations = [bbox], [lwords], [pr_class_words], [pr_relations] + + for i in range(len(bbox)): + bbox[i] = [revert_box(bb, width, height) for bb in bbox[i]] + # vat_outputs_invoice = export_kvu_for_VAT_invoice(os.path.join(save_dir, fname.replace(f'.{img_ext}', '.json')), lwords[i], pr_class_words[i], pr_relations[i], predictor.class_names) + # vat_outputs_receipt = export_kvu_for_SDSAP(os.path.join(save_dir, fname.replace(f'.{img_ext}', '.json')), lwords[i], pr_class_words[i], pr_relations[i], predictor.class_names) + # vat_outputs_invoice = export_kvu_for_all(os.path.join(save_dir, fname.replace(img_ext, '.json')), lwords[i], bbox[i], pr_class_words[i], pr_relations[i], predictor.class_names) + vat_outputs_invoice = export_kvu_for_manulife(os.path.join(save_dir, fname.replace(img_ext, '.json')), lwords[i], bbox[i], pr_class_words[i], pr_relations[i], predictor.class_names) + + print(vat_outputs_invoice) + return vat_outputs_invoice + + +def load_groundtruth(img_path: str, json_dir: str, save_dir: str, predictor: KVUPredictor, processor: KVUProcess, export_img: int) -> None: + fname = os.path.basename(img_path) + img_ext = img_path.split('.')[-1] + inputs = processor.load_ground_truth(os.path.join(json_dir, fname.replace(f".{img_ext}", ".json"))) + bbox, lwords, pr_class_words, pr_relations = predictor.get_ground_truth_label(inputs) + + export_kvu_outputs(os.path.join(save_dir, fname.replace(f'.{img_ext}', '.json')), lwords, pr_class_words, pr_relations, predictor.class_names) + + if export_img == 1: + save_path = os.path.join(save_dir, 'kvu_results') + create_dir(save_path) + color_map = get_colormap() + image = cv2.imread(img_path) + image = visualize(image, bbox, pr_class_words, pr_relations, color_map, class_names, thickness=1) + cv2.imwrite(os.path.join(save_path, fname), image) + +def show_groundtruth(dir_path: str, json_dir: str, save_dir: str, predictor: KVUPredictor, processor, export_img: int) -> None: + list_images = [] + for ext in ['JPG', 'PNG', 'jpeg', 'jpg', 'png']: + list_images += glob.glob(os.path.join(dir_path, f'*.{ext}')) + print('No. images:', len(list_images)) + for img_path in tqdm(list_images): + load_groundtruth(img_path, json_dir, save_dir, predictor, processor, export_img) + +def Predictor_KVU(image_url: str, save_dir: str, predictor: KVUPredictor, processor) -> None: + + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + image_path = "./Kie_Invoice_AP/tmp_image/{image_url}.jpg" + cv2.imwrite(image_path, img) + vat_outputs = predict_image(image_path, save_dir, predictor, processor) + return vat_outputs + + +if __name__ == "__main__": + args = get_args() + class_names = ['others', 'title', 'key', 'value', 'header'] + predict_mode = { + 'normal': 0, + 'full_tokens': 1, + 'sliding': 2, + 'document': 3 + } + predictor, processor = load_engine(args.exp_dir, class_names, args.mode) + create_dir(args.save_dir) + image_path = "/root/thucpd/20230322144639VUzu_16794962527791962785161104697882.jpg" + save_dir = "/home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/visualize/test" + predict_image(image_path, save_dir, predictor, processor) + print('[INFO] Done') \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/__init__.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier.py new file mode 100755 index 0000000..45b668f --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier.py @@ -0,0 +1,133 @@ +import time + +import torch +import torch.utils.data +from overrides import overrides +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger +from pytorch_lightning.utilities.distributed import rank_zero_only +from torch.optim import SGD, Adam, AdamW +from torch.optim.lr_scheduler import LambdaLR + +from lightning_modules.schedulers import ( + cosine_scheduler, + linear_scheduler, + multistep_scheduler, +) +from model import get_model +from utils import cfg_to_hparams, get_specific_pl_logger + + +class ClassifierModule(LightningModule): + def __init__(self, cfg): + super().__init__() + self.cfg = cfg + self.net = get_model(self.cfg) + self.ignore_index = -100 + + self.time_tracker = None + + self.optimizer_types = { + "sgd": SGD, + "adam": Adam, + "adamw": AdamW, + } + + @overrides + def setup(self, stage): + self.time_tracker = time.time() + + @overrides + def configure_optimizers(self): + optimizer = self._get_optimizer() + scheduler = self._get_lr_scheduler(optimizer) + scheduler = { + "scheduler": scheduler, + "name": "learning_rate", + "interval": "step", + } + return [optimizer], [scheduler] + + def _get_lr_scheduler(self, optimizer): + cfg_train = self.cfg.train + lr_schedule_method = cfg_train.optimizer.lr_schedule.method + lr_schedule_params = cfg_train.optimizer.lr_schedule.params + + if lr_schedule_method is None: + scheduler = LambdaLR(optimizer, lr_lambda=lambda _: 1) + elif lr_schedule_method == "step": + scheduler = multistep_scheduler(optimizer, **lr_schedule_params) + elif lr_schedule_method == "cosine": + total_samples = cfg_train.max_epochs * cfg_train.num_samples_per_epoch + total_batch_size = cfg_train.batch_size * self.trainer.world_size + max_iter = total_samples / total_batch_size + scheduler = cosine_scheduler( + optimizer, training_steps=max_iter, **lr_schedule_params + ) + elif lr_schedule_method == "linear": + total_samples = cfg_train.max_epochs * cfg_train.num_samples_per_epoch + total_batch_size = cfg_train.batch_size * self.trainer.world_size + max_iter = total_samples / total_batch_size + scheduler = linear_scheduler( + optimizer, training_steps=max_iter, **lr_schedule_params + ) + else: + raise ValueError(f"Unknown lr_schedule_method={lr_schedule_method}") + + return scheduler + + def _get_optimizer(self): + opt_cfg = self.cfg.train.optimizer + method = opt_cfg.method.lower() + + if method not in self.optimizer_types: + raise ValueError(f"Unknown optimizer method={method}") + + kwargs = dict(opt_cfg.params) + kwargs["params"] = self.net.parameters() + optimizer = self.optimizer_types[method](**kwargs) + + return optimizer + + @rank_zero_only + @overrides + def on_fit_end(self): + hparam_dict = cfg_to_hparams(self.cfg, {}) + metric_dict = {"metric/dummy": 0} + + tb_logger = get_specific_pl_logger(self.logger, TensorBoardLogger) + + if tb_logger: + tb_logger.log_hyperparams(hparam_dict, metric_dict) + + @overrides + def training_epoch_end(self, training_step_outputs): + avg_loss = torch.tensor(0.0).to(self.device) + for step_out in training_step_outputs: + avg_loss += step_out["loss"] + + log_dict = {"train_loss": avg_loss} + self._log_shell(log_dict, prefix="train ") + + def _log_shell(self, log_info, prefix=""): + log_info_shell = {} + for k, v in log_info.items(): + new_v = v + if type(new_v) is torch.Tensor: + new_v = new_v.item() + log_info_shell[k] = new_v + + out_str = prefix.upper() + if prefix.upper().strip() in ["TRAIN", "VAL"]: + out_str += f"[epoch: {self.current_epoch}/{self.cfg.train.max_epochs}]" + + if self.training: + lr = self.trainer._lightning_optimizers[0].param_groups[0]["lr"] + log_info_shell["lr"] = lr + + for key, value in log_info_shell.items(): + out_str += f" || {key}: {round(value, 5)}" + out_str += f" || time: {round(time.time() - self.time_tracker, 1)}" + out_str += " secs." + self.print(out_str) + self.time_tracker = time.time() diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier_module.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier_module.py new file mode 100755 index 0000000..5786cc5 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/classifier_module.py @@ -0,0 +1,390 @@ +import numpy as np +import torch +import torch.utils.data +from overrides import overrides + +from lightning_modules.classifier import ClassifierModule +from utils import get_class_names + + +class KVUClassifierModule(ClassifierModule): + def __init__(self, cfg): + super().__init__(cfg) + + class_names = get_class_names(self.cfg.dataset_root_path) + + self.window_size = cfg.train.max_num_words + self.slice_interval = cfg.train.slice_interval + self.eval_kwargs = { + "class_names": class_names, + "dummy_idx": self.cfg.train.max_seq_length, # update dummy_idx in next step + } + self.stage = cfg.stage + + @overrides + def training_step(self, batch, batch_idx, *args): + if self.stage == 1: + _, loss = self.net(batch['windows']) + elif self.stage == 2: + _, loss = self.net(batch) + else: + raise ValueError( + f"Not supported stage: {self.stage}" + ) + + log_dict_input = {"train_loss": loss} + self.log_dict(log_dict_input, sync_dist=True) + return loss + + @torch.no_grad() + @overrides + def validation_step(self, batch, batch_idx, *args): + if self.stage == 1: + step_out_total = { + "loss": 0, + "ee":{ + "n_batch_gt": 0, + "n_batch_pr": 0, + "n_batch_correct": 0, + }, + "el":{ + "n_batch_gt": 0, + "n_batch_pr": 0, + "n_batch_correct": 0, + }, + "el_from_key":{ + "n_batch_gt": 0, + "n_batch_pr": 0, + "n_batch_correct": 0, + }} + for window in batch['windows']: + head_outputs, loss = self.net(window) + step_out = do_eval_step(window, head_outputs, loss, self.eval_kwargs) + for key in step_out_total: + if key == 'loss': + step_out_total[key] += step_out[key] + else: + for subkey in step_out_total[key]: + step_out_total[key][subkey] += step_out[key][subkey] + return step_out_total + + elif self.stage == 2: + head_outputs, loss = self.net(batch) + # self.eval_kwargs['dummy_idx'] = batch['itc_labels'].shape[1] + # step_out = do_eval_step(batch, head_outputs, loss, self.eval_kwargs) + self.eval_kwargs['dummy_idx'] = batch['documents']['itc_labels'].shape[1] + step_out = do_eval_step(batch['documents'], head_outputs, loss, self.eval_kwargs) + return step_out + + @torch.no_grad() + @overrides + def validation_epoch_end(self, validation_step_outputs): + scores = do_eval_epoch_end(validation_step_outputs) + self.print( + f"[EE] Precision: {scores['ee']['precision']:.4f}, Recall: {scores['ee']['recall']:.4f}, F1-score: {scores['ee']['f1']:.4f}" + ) + self.print( + f"[EL] Precision: {scores['el']['precision']:.4f}, Recall: {scores['el']['recall']:.4f}, F1-score: {scores['el']['f1']:.4f}" + ) + self.print( + f"[ELK] Precision: {scores['el_from_key']['precision']:.4f}, Recall: {scores['el_from_key']['recall']:.4f}, F1-score: {scores['el_from_key']['f1']:.4f}" + ) + self.log('val_f1', (scores['ee']['f1'] + scores['el']['f1'] + scores['el_from_key']['f1']) / 3.) + tensorboard_logs = {'val_precision_ee': scores['ee']['precision'], 'val_recall_ee': scores['ee']['recall'], 'val_f1_ee': scores['ee']['f1'], + 'val_precision_el': scores['el']['precision'], 'val_recall_el': scores['el']['recall'], 'val_f1_el': scores['el']['f1'], + 'val_precision_el_from_key': scores['el_from_key']['precision'], 'val_recall_el_from_key': scores['el_from_key']['recall'], \ + 'val_f1_el_from_key': scores['el_from_key']['f1'],} + return {'log': tensorboard_logs} + + +def do_eval_step(batch, head_outputs, loss, eval_kwargs): + class_names = eval_kwargs["class_names"] + dummy_idx = eval_kwargs["dummy_idx"] + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_labels = torch.argmax(itc_outputs, -1) + pr_stc_labels = torch.argmax(stc_outputs, -1) + pr_el_labels = torch.argmax(el_outputs, -1) + pr_el_labels_from_key = torch.argmax(el_outputs_from_key, -1) + + ( + n_batch_gt_classes, + n_batch_pr_classes, + n_batch_correct_classes, + ) = eval_ee_spade_batch( + pr_itc_labels, + batch["itc_labels"], + batch["are_box_first_tokens"], + pr_stc_labels, + batch["stc_labels"], + batch["attention_mask_layoutxlm"], + class_names, + dummy_idx, + ) + + n_batch_gt_rel, n_batch_pr_rel, n_batch_correct_rel = eval_el_spade_batch( + pr_el_labels, + batch["el_labels"], + batch["are_box_first_tokens"], + dummy_idx, + ) + + n_batch_gt_rel_from_key, n_batch_pr_rel_from_key, n_batch_correct_rel_from_key = eval_el_spade_batch( + pr_el_labels_from_key, + batch["el_labels_from_key"], + batch["are_box_first_tokens"], + dummy_idx, + ) + + step_out = { + "loss": loss, + "ee":{ + "n_batch_gt": n_batch_gt_classes, + "n_batch_pr": n_batch_pr_classes, + "n_batch_correct": n_batch_correct_classes, + }, + "el":{ + "n_batch_gt": n_batch_gt_rel, + "n_batch_pr": n_batch_pr_rel, + "n_batch_correct": n_batch_correct_rel, + }, + "el_from_key":{ + "n_batch_gt": n_batch_gt_rel_from_key, + "n_batch_pr": n_batch_pr_rel_from_key, + "n_batch_correct": n_batch_correct_rel_from_key, + } + + } + + return step_out + + +def eval_ee_spade_batch( + pr_itc_labels, + gt_itc_labels, + are_box_first_tokens, + pr_stc_labels, + gt_stc_labels, + attention_mask, + class_names, + dummy_idx, +): + n_batch_gt_classes, n_batch_pr_classes, n_batch_correct_classes = 0, 0, 0 + + bsz = pr_itc_labels.shape[0] + for example_idx in range(bsz): + n_gt_classes, n_pr_classes, n_correct_classes = eval_ee_spade_example( + pr_itc_labels[example_idx], + gt_itc_labels[example_idx], + are_box_first_tokens[example_idx], + pr_stc_labels[example_idx], + gt_stc_labels[example_idx], + attention_mask[example_idx], + class_names, + dummy_idx, + ) + + n_batch_gt_classes += n_gt_classes + n_batch_pr_classes += n_pr_classes + n_batch_correct_classes += n_correct_classes + + return ( + n_batch_gt_classes, + n_batch_pr_classes, + n_batch_correct_classes, + ) + + +def eval_ee_spade_example( + pr_itc_label, + gt_itc_label, + box_first_token_mask, + pr_stc_label, + gt_stc_label, + attention_mask, + class_names, + dummy_idx, +): + gt_first_words = parse_initial_words( + gt_itc_label, box_first_token_mask, class_names + ) + gt_class_words = parse_subsequent_words( + gt_stc_label, attention_mask, gt_first_words, dummy_idx + ) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, dummy_idx + ) + + n_gt_classes, n_pr_classes, n_correct_classes = 0, 0, 0 + for class_idx in range(len(class_names)): + # Evaluate by ID + gt_parse = set(gt_class_words[class_idx]) + pr_parse = set(pr_class_words[class_idx]) + + n_gt_classes += len(gt_parse) + n_pr_classes += len(pr_parse) + n_correct_classes += len(gt_parse & pr_parse) + + return n_gt_classes, n_pr_classes, n_correct_classes + + +def parse_initial_words(itc_label, box_first_token_mask, class_names): + itc_label_np = itc_label.cpu().numpy() + box_first_token_mask_np = box_first_token_mask.cpu().numpy() + + outputs = [[] for _ in range(len(class_names))] + + for token_idx, label in enumerate(itc_label_np): + if box_first_token_mask_np[token_idx] and label != 0: + outputs[label].append(token_idx) + + return outputs + + +def parse_subsequent_words(stc_label, attention_mask, init_words, dummy_idx): + max_connections = 50 + + valid_stc_label = stc_label * attention_mask.bool() + valid_stc_label = valid_stc_label.cpu().numpy() + stc_label_np = stc_label.cpu().numpy() + + valid_token_indices = np.where( + (valid_stc_label != dummy_idx) * (valid_stc_label != 0) + ) + + next_token_idx_dict = {} + for token_idx in valid_token_indices[0]: + next_token_idx_dict[stc_label_np[token_idx]] = token_idx + + outputs = [] + for init_token_indices in init_words: + sub_outputs = [] + for init_token_idx in init_token_indices: + cur_token_indices = [init_token_idx] + for _ in range(max_connections): + if cur_token_indices[-1] in next_token_idx_dict: + if ( + next_token_idx_dict[cur_token_indices[-1]] + not in init_token_indices + ): + cur_token_indices.append( + next_token_idx_dict[cur_token_indices[-1]] + ) + else: + break + else: + break + sub_outputs.append(tuple(cur_token_indices)) + + outputs.append(sub_outputs) + + return outputs + + +def eval_el_spade_batch( + pr_el_labels, + gt_el_labels, + are_box_first_tokens, + dummy_idx, +): + n_batch_gt_rel, n_batch_pr_rel, n_batch_correct_rel = 0, 0, 0 + + bsz = pr_el_labels.shape[0] + for example_idx in range(bsz): + n_gt_rel, n_pr_rel, n_correct_rel = eval_el_spade_example( + pr_el_labels[example_idx], + gt_el_labels[example_idx], + are_box_first_tokens[example_idx], + dummy_idx, + ) + + n_batch_gt_rel += n_gt_rel + n_batch_pr_rel += n_pr_rel + n_batch_correct_rel += n_correct_rel + + return n_batch_gt_rel, n_batch_pr_rel, n_batch_correct_rel + + +def eval_el_spade_example(pr_el_label, gt_el_label, box_first_token_mask, dummy_idx): + gt_relations = parse_relations(gt_el_label, box_first_token_mask, dummy_idx) + pr_relations = parse_relations(pr_el_label, box_first_token_mask, dummy_idx) + + gt_relations = set(gt_relations) + pr_relations = set(pr_relations) + + n_gt_rel = len(gt_relations) + n_pr_rel = len(pr_relations) + n_correct_rel = len(gt_relations & pr_relations) + + return n_gt_rel, n_pr_rel, n_correct_rel + + +def parse_relations(el_label, box_first_token_mask, dummy_idx): + valid_el_labels = el_label * box_first_token_mask + valid_el_labels = valid_el_labels.cpu().numpy() + el_label_np = el_label.cpu().numpy() + + max_token = box_first_token_mask.shape[0] - 1 + + valid_token_indices = np.where( + ((valid_el_labels != dummy_idx) * (valid_el_labels != 0)) ### + ) + + link_map_tuples = [] + for token_idx in valid_token_indices[0]: + link_map_tuples.append((el_label_np[token_idx], token_idx)) + + return set(link_map_tuples) + +def do_eval_epoch_end(step_outputs): + scores = {} + for task in ['ee', 'el', 'el_from_key']: + n_total_gt_classes, n_total_pr_classes, n_total_correct_classes = 0, 0, 0 + + for step_out in step_outputs: + n_total_gt_classes += step_out[task]["n_batch_gt"] + n_total_pr_classes += step_out[task]["n_batch_pr"] + n_total_correct_classes += step_out[task]["n_batch_correct"] + + precision = ( + 0.0 if n_total_pr_classes == 0 else n_total_correct_classes / n_total_pr_classes + ) + recall = ( + 0.0 if n_total_gt_classes == 0 else n_total_correct_classes / n_total_gt_classes + ) + f1 = ( + 0.0 + if recall * precision == 0 + else 2.0 * recall * precision / (recall + precision) + ) + + scores[task] = { + "precision": precision, + "recall": recall, + "f1": f1, + } + + return scores + + +def get_eval_kwargs_spade(dataset_root_path, max_seq_length): + class_names = get_class_names(dataset_root_path) + dummy_idx = max_seq_length + + eval_kwargs = {"class_names": class_names, "dummy_idx": dummy_idx} + + return eval_kwargs + + +def get_eval_kwargs_spade_rel(max_seq_length): + dummy_idx = max_seq_length + + eval_kwargs = {"dummy_idx": dummy_idx} + + return eval_kwargs \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/__init__.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/data_module.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/data_module.py new file mode 100755 index 0000000..1b9a255 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/data_module.py @@ -0,0 +1,135 @@ +import time + +import torch +import pytorch_lightning as pl +from overrides import overrides +from torch.utils.data.dataloader import DataLoader + +from lightning_modules.data_modules.kvu_dataset import KVUDataset, KVUEmbeddingDataset +from lightning_modules.utils import _get_number_samples + +class KVUDataModule(pl.LightningDataModule): + def __init__(self, cfg, tokenizer_layoutxlm, feature_extractor): + super().__init__() + self.cfg = cfg + self.train_loader = None + self.val_loader = None + self.tokenizer_layoutxlm = tokenizer_layoutxlm + self.feature_extractor = feature_extractor + self.collate_fn = None + + self.backbone_type = self.cfg.model.backbone + self.num_samples_per_epoch = _get_number_samples(cfg.dataset_root_path) + + + @overrides + def setup(self, stage=None): + self.train_loader = self._get_train_loader() + self.val_loader = self._get_val_loaders() + + @overrides + def train_dataloader(self): + return self.train_loader + + @overrides + def val_dataloader(self): + return self.val_loader + + def _get_train_loader(self): + start_time = time.time() + + if self.cfg.stage == 1: + dataset = KVUDataset( + self.cfg, + self.tokenizer_layoutxlm, + self.feature_extractor, + mode="train", + ) + elif self.cfg.stage == 2: + # dataset = KVUEmbeddingDataset( + # self.cfg, + # mode="train", + # ) + dataset = KVUDataset( + self.cfg, + self.tokenizer_layoutxlm, + self.feature_extractor, + mode="train", + ) + else: + raise ValueError( + f"Not supported stage: {self.cfg.stage}" + ) + + print('No. training samples:', len(dataset)) + + data_loader = DataLoader( + dataset, + batch_size=self.cfg.train.batch_size, + shuffle=True, + num_workers=self.cfg.train.num_workers, + pin_memory=True, + ) + + elapsed_time = time.time() - start_time + print(f"Elapsed time for loading training data: {elapsed_time}") + + return data_loader + + def _get_val_loaders(self): + + if self.cfg.stage == 1: + dataset = KVUDataset( + self.cfg, + self.tokenizer_layoutxlm, + self.feature_extractor, + mode="val", + ) + elif self.cfg.stage == 2: + # dataset = KVUEmbeddingDataset( + # self.cfg, + # mode="val", + # ) + dataset = KVUDataset( + self.cfg, + self.tokenizer_layoutxlm, + self.feature_extractor, + mode="val", + ) + else: + raise ValueError( + f"Not supported stage: {self.cfg.stage}" + ) + + print('No. validation samples:', len(dataset)) + + data_loader = DataLoader( + dataset, + batch_size=self.cfg.val.batch_size, + shuffle=False, + num_workers=self.cfg.val.num_workers, + pin_memory=True, + drop_last=False, + ) + + return data_loader + + @overrides + def transfer_batch_to_device(self, batch, device, dataloader_idx): + if isinstance(batch, list): + for sub_batch in batch: + for k in sub_batch.keys(): + if isinstance(sub_batch[k], torch.Tensor): + sub_batch[k] = sub_batch[k].to(device) + else: + for k in batch.keys(): + if isinstance(batch[k], torch.Tensor): + batch[k] = batch[k].to(device) + return batch + + + + + + + diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/kvu_dataset.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/kvu_dataset.py new file mode 100755 index 0000000..e15ec64 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/data_modules/kvu_dataset.py @@ -0,0 +1,728 @@ +import os +import json +import omegaconf +import itertools +import copy + +import numpy as np +from PIL import Image + +import torch +from torch.utils.data.dataset import Dataset + +from utils import get_class_names +from lightning_modules.utils import sliding_windows_by_words + +class KVUDataset(Dataset): + def __init__( + self, + cfg, + tokenizer_layoutxlm, + feature_extractor, + mode=None, + ): + super(KVUDataset, self).__init__() + + self.dataset_root_path = cfg.dataset_root_path + if not isinstance(self.dataset_root_path, omegaconf.listconfig.ListConfig): + self.dataset_root_path = [self.dataset_root_path] + + self.backbone_type = cfg.model.backbone + self.max_seq_length = cfg.train.max_seq_length + self.window_size = cfg.train.max_num_words + self.slice_interval = cfg.train.slice_interval + + self.tokenizer_layoutxlm = tokenizer_layoutxlm + self.feature_extractor = feature_extractor + + self.stage = cfg.stage + self.mode = mode + + self.pad_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm._pad_token) + self.cls_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm._cls_token) + self.sep_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm._sep_token) + self.unk_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm._unk_token) + + self.examples = self._load_examples() + + self.class_names = get_class_names(self.dataset_root_path) + self.class_idx_dic = dict( + [(class_name, idx) for idx, class_name in enumerate(self.class_names)] + ) + + def _load_examples(self): + examples = [] + for dataset_dir in self.dataset_root_path: + with open( + os.path.join(dataset_dir, f"preprocessed_files_{self.mode}.txt"), + "r", + encoding="utf-8", + ) as fp: + for line in fp.readlines(): + preprocessed_file = os.path.join(dataset_dir, line.strip()) + examples.append( + json.load(open(preprocessed_file, "r", encoding="utf-8")) + ) + + return examples + + def __len__(self): + return len(self.examples) + + + def __getitem__(self, index): + json_obj = self.examples[index] + + width = json_obj["meta"]["imageSize"]["width"] + height = json_obj["meta"]["imageSize"]["height"] + img_path = json_obj["meta"]["image_path"] + + images = [Image.open(json_obj["meta"]["image_path"]).convert("RGB")] + image_features = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + + word_windows, parse_class_windows, parse_relation_windows = sliding_windows_by_words( + json_obj["words"], + json_obj['parse']['class'], + json_obj['parse']['relations'], + self.window_size, self.slice_interval) + outputs = {} + if self.stage == 1: + if self.mode == 'train': + i = np.random.randint(0, len(word_windows), 1)[0] + outputs['windows'] = self.preprocess(word_windows[i], parse_class_windows[i], parse_relation_windows[i], + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + else: + outputs['windows'] = [] + for i in range(len(word_windows)): + single_window = self.preprocess(word_windows[i], parse_class_windows[i], parse_relation_windows[i], + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + outputs['windows'].append(single_window) + + elif self.stage == 2: + outputs['documents'] = self.preprocess(json_obj["words"], json_obj['parse']['class'], json_obj['parse']['relations'], + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=2048) + + windows = [] + for i in range(len(word_windows)): + _words = word_windows[i] + _parse_class = parse_class_windows[i] + _parse_relation = parse_relation_windows[i] + windows.append( + self.preprocess(_words, _parse_class, _parse_relation, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + self.max_seq_length) + ) + outputs['windows'] = windows + return outputs + + def preprocess(self, words, parse_class, parse_relation, feature_maps, max_seq_length): + input_ids_layoutxlm = np.ones(max_seq_length, dtype=int) * self.pad_token_id_layoutxlm + + attention_mask_layoutxlm = np.zeros(max_seq_length, dtype=int) + + bbox = np.zeros((max_seq_length, 8), dtype=np.float32) + + are_box_first_tokens = np.zeros(max_seq_length, dtype=np.bool_) + + itc_labels = np.zeros(max_seq_length, dtype=int) + stc_labels = np.ones(max_seq_length, dtype=np.int64) * max_seq_length + el_labels = np.ones((max_seq_length,), dtype=int) * max_seq_length + el_labels_from_key = np.ones((max_seq_length,), dtype=int) * max_seq_length + list_layoutxlm_tokens = [] + + list_bbs = [] + box2token_span_map = [] + + + box_to_token_indices = [] + cum_token_idx = 0 + + cls_bbs = [0.0] * 8 + len_overlap_tokens = 0 + len_non_overlap_tokens = 0 + len_valid_tokens = 0 + + for word_idx, word in enumerate(words): + this_box_token_indices = [] + + layoutxlm_tokens = word["layoutxlm_tokens"] + bb = word["boundingBox"] + len_valid_tokens += len(layoutxlm_tokens) + if word_idx < self.slice_interval: + len_non_overlap_tokens += len(layoutxlm_tokens) + # print(word_idx, layoutxlm_tokens, non_overlap_tokens) + + if len(layoutxlm_tokens) == 0: + layoutxlm_tokens.append(self.unk_token_id_layoutxlm) + + if len(list_layoutxlm_tokens) + len(layoutxlm_tokens) > max_seq_length - 2: + break + + box2token_span_map.append( + [len(list_layoutxlm_tokens) + 1, len(list_layoutxlm_tokens) + len(layoutxlm_tokens) + 1] + ) # including st_idx + list_layoutxlm_tokens += layoutxlm_tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], feature_maps['width'])) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], feature_maps['height'])) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(layoutxlm_tokens))] + + for _ in layoutxlm_tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [feature_maps['width'], feature_maps['height']] * 4 + + # For [CLS] and [SEP] + list_layoutxlm_tokens = ( + [self.cls_token_id_layoutxlm] + + list_layoutxlm_tokens[: max_seq_length - 2] + + [self.sep_token_id_layoutxlm] + ) + + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: max_seq_length - 2] + [sep_bbs] + + len_list_layoutxlm_tokens = len(list_layoutxlm_tokens) + input_ids_layoutxlm[:len_list_layoutxlm_tokens] = list_layoutxlm_tokens + attention_mask_layoutxlm[:len_list_layoutxlm_tokens] = 1 + + bbox[:len_list_layoutxlm_tokens, :] = list_bbs + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / feature_maps['width'] + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / feature_maps['height'] + + if self.backbone_type in ("layoutlm", "layoutxlm", "xlm-roberta"): + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + else: + assert False + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < max_seq_length + ] + are_box_first_tokens[st_indices] = True + + # Label for entity extraction + classes_dic = parse_class + for class_name in self.class_names: + if class_name == "others": + continue + if class_name not in classes_dic: + continue + + for word_list in classes_dic[class_name]: + is_first, last_word_idx = True, -1 + for word_idx in word_list: + if word_idx >= len(box_to_token_indices): + break + box2token_list = box_to_token_indices[word_idx] + for converted_word_idx in box2token_list: + if converted_word_idx >= max_seq_length: + break # out of idx + + if is_first: + itc_labels[converted_word_idx] = self.class_idx_dic[ + class_name + ] + is_first, last_word_idx = False, converted_word_idx + else: + stc_labels[converted_word_idx] = last_word_idx + last_word_idx = converted_word_idx + + + # Label for entity linking + relations = parse_relation + for relation in relations: + if relation[0] >= len(box2token_span_map) or relation[1] >= len( + box2token_span_map + ): + continue + if ( + box2token_span_map[relation[0]][0] >= max_seq_length + or box2token_span_map[relation[1]][0] >= max_seq_length + ): + continue + + word_from = box2token_span_map[relation[0]][0] + word_to = box2token_span_map[relation[1]][0] + # el_labels[word_to] = word_from + + + #### 1st relation => ['key, 'value'] + #### 2st relation => ['header', 'key'or'value'] + if itc_labels[word_from] == 2 and itc_labels[word_to] == 3: + el_labels_from_key[word_to] = word_from # pair of (key-value) + if itc_labels[word_from] == 4 and (itc_labels[word_to] in (2, 3)): + el_labels[word_to] = word_from # pair of (header, key) or (header-value) + + assert len_list_layoutxlm_tokens == len_valid_tokens + 2 + len_overlap_tokens = len_valid_tokens - len_non_overlap_tokens + # overlap_tokens = max_seq_length - non_overlap_tokens + ntokens = max_seq_length if max_seq_length == 512 else len_valid_tokens + 2 + + # ntokens = max_seq_length + + input_ids_layoutxlm = torch.from_numpy(input_ids_layoutxlm[:ntokens]) + + attention_mask_layoutxlm = torch.from_numpy(attention_mask_layoutxlm[:ntokens]) + + bbox = torch.from_numpy(bbox[:ntokens]) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens[:ntokens]) + + itc_labels = itc_labels[:ntokens] + stc_labels = stc_labels[:ntokens] + el_labels = el_labels[:ntokens] + el_labels_from_key = el_labels_from_key[:ntokens] + + itc_labels = np.where(itc_labels != max_seq_length, itc_labels, ntokens) + stc_labels = np.where(stc_labels != max_seq_length, stc_labels, ntokens) + el_labels = np.where(el_labels != max_seq_length, el_labels, ntokens) + el_labels_from_key = np.where(el_labels_from_key != max_seq_length, el_labels_from_key, ntokens) + + itc_labels = torch.from_numpy(itc_labels) + stc_labels = torch.from_numpy(stc_labels) + el_labels = torch.from_numpy(el_labels) + el_labels_from_key = torch.from_numpy(el_labels_from_key) + + + return_dict = { + "img_path": feature_maps['img_path'], + "len_overlap_tokens": len_overlap_tokens, + 'len_valid_tokens': len_valid_tokens, + "image": feature_maps['image'], + "input_ids_layoutxlm": input_ids_layoutxlm, + "attention_mask_layoutxlm": attention_mask_layoutxlm, + "bbox": bbox, + "itc_labels": itc_labels, + "are_box_first_tokens": are_box_first_tokens, + "stc_labels": stc_labels, + "el_labels": el_labels, + "el_labels_from_key": el_labels_from_key, + } + return return_dict + + + +class KVUPredefinedDataset(KVUDataset): + def __init__(self, cfg, tokenizer_layoutxlm, feature_extractor, mode=None): + super().__init__(cfg, tokenizer_layoutxlm, feature_extractor, mode) + self.max_windows = cfg.train.max_windows + + def __getitem__(self, index): + json_obj = self.examples[index] + + width = json_obj["meta"]["imageSize"]["width"] + height = json_obj["meta"]["imageSize"]["height"] + img_path = json_obj["meta"]["image_path"] + + images = [Image.open(json_obj["meta"]["image_path"]).convert("RGB")] + image_features = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + + + word_windows, parse_class_windows, parse_relation_windows = sliding_windows_by_words( + json_obj["words"], + json_obj['parse']['class'], + json_obj['parse']['relations'], + self.window_size, self.slice_interval) + + word_windows = word_windows[: self.max_windows] if len(word_windows) >= self.max_windows else word_windows + [[]] * (self.max_windows - len(word_windows)) + parse_class_windows = parse_class_windows[: self.max_windows] if len(parse_class_windows) >= self.max_windows else parse_class_windows + [[]] * (self.max_windows - len(parse_class_windows)) + parse_relation_windows = parse_relation_windows[: self.max_windows] if len(parse_relation_windows) >= self.max_windows else parse_relation_windows + [[]] * (self.max_windows - len(parse_relation_windows)) + + + outputs = {} + # outputs['labels'] = self.preprocess(json_obj["words"], json_obj['parse']['class'], json_obj['parse']['relations'], + # {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + # max_seq_length=self.max_seq_length*self.max_windows) + + outputs['windows'] = [] + for i in range(len(self.max_windows)): + single_window = self.preprocess(word_windows[i], parse_class_windows[i], parse_relation_windows[i], + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + outputs['windows'].append(single_window) + + outputs['windows'] = torch.cat(outputs["windows"], dim=0) + + return outputs + + +class KVUEmbeddingDataset(Dataset): + def __init__( + self, + cfg, + mode=None, + ): + super(KVUEmbeddingDataset, self).__init__() + + self.dataset_root_path = cfg.dataset_root_path + if not isinstance(self.dataset_root_path, omegaconf.listconfig.ListConfig): + self.dataset_root_path = [self.dataset_root_path] + + self.stage = cfg.stage + self.mode = mode + + self.examples = self._load_examples() + + def _load_examples(self): + examples = [] + for dataset_dir in self.dataset_root_path: + with open( + os.path.join(dataset_dir, f"preprocessed_files_{self.mode}.txt"), + "r", + encoding="utf-8", + ) as fp: + for line in fp.readlines(): + preprocessed_file = os.path.join(dataset_dir, line.strip()) + examples.append( + preprocessed_file.replace('preprocessed', 'embedding_matrix').replace('.json', '.npz') + ) + return examples + + def __len__(self): + return len(self.examples) + + def __getitem__(self, index): + input_embeddings = np.load(self.examples[index]) + + return_dict = { + "embeddings": torch.from_numpy(input_embeddings["embeddings"]).type(torch.HalfTensor), + "attention_mask_layoutxlm": torch.from_numpy(input_embeddings["attention_mask_layoutxlm"]), + "are_box_first_tokens": torch.from_numpy(input_embeddings["are_box_first_tokens"]), + "bbox": torch.from_numpy(input_embeddings["bbox"]), + "itc_labels": torch.from_numpy(input_embeddings["itc_labels"]), + "stc_labels": torch.from_numpy(input_embeddings["stc_labels"]), + "el_labels": torch.from_numpy(input_embeddings["el_labels"]), + "el_labels_from_key": torch.from_numpy(input_embeddings["el_labels_from_key"]), + } + return return_dict + + +class DocumentKVUDataset(KVUDataset): + def __init__(self, cfg, tokenizer_layoutxlm, feature_extractor, mode=None): + super().__init__(cfg, tokenizer_layoutxlm, feature_extractor, mode) + self.self.max_window_count = cfg.train.max_window_count + + def __getitem__(self, idx): + json_obj = self.examples[idx] + + width = json_obj["meta"]["imageSize"]["width"] + height = json_obj["meta"]["imageSize"]["height"] + + images = [Image.open(json_obj["meta"]["image_path"]).convert("RGB")] + feature_maps = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + + n_words = len(json_obj['words']) + output_dicts = {'windows': [], 'documents': []} + box_to_token_indices_document = [] + box2token_span_map_document = [] + n_empty_windows = 0 + + for i in range(self.max_window_count): + input_ids = np.ones(self.max_seq_length, dtype=int) * 1 + bbox = np.zeros((self.max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(self.max_seq_length, dtype=int) + + itc_labels = np.zeros(self.max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(self.max_seq_length, dtype=np.bool_) + + # stc_labels stores the index of the previous token. + # A stored index of max_seq_length (512) indicates that + # this token is the initial token of a word box. + stc_labels = np.ones(self.max_seq_length, dtype=np.int64) * self.max_seq_length + el_labels = np.ones((self.max_seq_length,), dtype=int) * self.max_seq_length + el_labels_from_key = np.ones((self.max_seq_length,), dtype=int) * self.max_seq_length + + start_word_idx = i * self.window_size + stop_word_idx = min(n_words, (i+1)*self.window_size) + + if start_word_idx >= stop_word_idx: + n_empty_windows += 1 + output_dicts.append(output_dicts[-1]) + + box_to_token_indices_to_mod = copy.deepcopy(box_to_token_indices) + for i_box in range(len(box_to_token_indices_to_mod)): + for j in range(len(box_to_token_indices_to_mod[i_box])): + box_to_token_indices_to_mod[i_box][j] += i * self.max_seq_length + for element in box_to_token_indices_to_mod: + box_to_token_indices_document.append(element) + + box2token_span_map_to_mod = copy.deepcopy(box2token_span_map) + for i_box in range(len(box2token_span_map_to_mod)): + for j in range(len(box2token_span_map_to_mod[i_box])): + box2token_span_map_to_mod[i_box][j] += i * self.max_seq_length + for element in box2token_span_map_to_mod: + box2token_span_map_document.append(element) + + continue + + list_tokens = [] + list_bbs = [] + box2token_span_map = [] + + box_to_token_indices = [] + cum_token_idx = 0 + + cls_bbs = [0.0] * 8 + + # Parse words + for word_idx, word in enumerate(json_obj["words"][start_word_idx:stop_word_idx]): + this_box_token_indices = [] + tokens = word["tokens"] + bb = word["boundingBox"] + if len(tokens) == 0: + tokens.append(self.unk_token_id) + + if len(list_tokens) + len(tokens) > self.max_seq_length - 2: + break ### be able to apply sliding window here + + box2token_span_map.append( + [len(list_tokens) + 1, len(list_tokens) + len(tokens) + 1] + ) # including st_idx + list_tokens += tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], width)) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], height)) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(tokens))] + + for _ in tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [width, height] * 4 + + # For [CLS] and [SEP] + list_tokens = ( + [self.cls_token_id] + + list_tokens[: self.max_seq_length - 2] + + [self.sep_token_id] + ) + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: self.max_seq_length - 2] + [sep_bbs] + + len_list_tokens = len(list_tokens) + input_ids[:len_list_tokens] = list_tokens + attention_mask[:len_list_tokens] = 1 + + bbox[:len_list_tokens, :] = list_bbs + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / width + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / height + + if self.backbone_type in ('layoutlm', 'layoutxlm', 'xlm-roberta'): + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + else: + assert False + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < self.max_seq_length + ] + are_box_first_tokens[st_indices] = True + + # Parse word groups + classes_dic = json_obj["parse"]["class"] + for class_name in self.class_names: + if class_name == "others": + continue + if class_name not in classes_dic: + continue + + for word_list in classes_dic[class_name]: + word_list = [w for w in word_list if w >= start_word_idx and w < stop_word_idx] + if len(word_list) == 0: + continue # no more word left + word_list = [w - start_word_idx for w in word_list] + + is_first, last_word_idx = True, -1 + for word_idx in word_list: + if word_idx >= len(box_to_token_indices): + break + box2token_list = box_to_token_indices[word_idx] + for converted_word_idx in box2token_list: + if converted_word_idx >= self.max_seq_length: + break # out of idx + + if is_first: + itc_labels[converted_word_idx] = self.class_idx_dic[ + class_name + ] + is_first, last_word_idx = False, converted_word_idx + else: + stc_labels[converted_word_idx] = last_word_idx + last_word_idx = converted_word_idx + + # Parse relation + relations = json_obj["parse"]["relations"] + for relation in relations: + relation = [r for r in relation if r >= start_word_idx and r < stop_word_idx] + if len(relation) != 2: + continue # relation popped due to window inconsistent + relation[0] -= start_word_idx + relation[1] -= start_word_idx + + word_from = box2token_span_map[relation[0]][0] + word_to = box2token_span_map[relation[1]][0] + + #### 1st relation => ['key, 'value'] + #### 2st relation => ['header', 'key'or'value'] + if itc_labels[word_from] == 2 and itc_labels[word_to] == 3: + el_labels_from_key[word_to] = word_from # pair of (key-value) + if itc_labels[word_from] == 4 and (itc_labels[word_to] in (2, 3)): + el_labels[word_to] = word_from # pair of (header, key) or (header-value) + + input_ids = torch.from_numpy(input_ids) + bbox = torch.from_numpy(bbox) + attention_mask = torch.from_numpy(attention_mask) + + itc_labels = torch.from_numpy(itc_labels) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + stc_labels = torch.from_numpy(stc_labels) + el_labels = torch.from_numpy(el_labels) + el_labels_from_key = torch.from_numpy(el_labels_from_key) + + box_to_token_indices_to_mod = copy.deepcopy(box_to_token_indices) + for i_box in range(len(box_to_token_indices_to_mod)): + for j in range(len(box_to_token_indices_to_mod[i_box])): + box_to_token_indices_to_mod[i_box][j] += i * self.max_seq_length + for element in box_to_token_indices_to_mod: + box_to_token_indices_document.append(element) + + box2token_span_map_to_mod = copy.deepcopy(box2token_span_map) + for i_box in range(len(box2token_span_map_to_mod)): + for j in range(len(box2token_span_map_to_mod[i_box])): + box2token_span_map_to_mod[i_box][j] += i * self.max_seq_length + for element in box2token_span_map_to_mod: + box2token_span_map_document.append(element) + + return_dict = { + "image": feature_maps, + "input_ids": input_ids, + "bbox": bbox, + "attention_mask": attention_mask, + "itc_labels": itc_labels, + "are_box_first_tokens": are_box_first_tokens, + "stc_labels": stc_labels, + "el_labels": el_labels, + "el_labels_from_key": el_labels_from_key, + } + + output_dicts["windows"].append(return_dict) + + + # Parse whole document labels + attention_mask = torch.cat([o['attention_mask'] for o in output_dicts]) + are_box_first_tokens = torch.cat([o['are_box_first_tokens'] for o in output_dicts]) + if n_empty_windows > 0: + attention_mask[self.max_seq_length * (self.max_window_count - n_empty_windows):] = torch.from_numpy(np.zeros(self.max_seq_length * n_empty_windows, dtype=int)) + are_box_first_tokens[self.max_seq_length * (self.max_window_count - n_empty_windows):] = torch.from_numpy(np.zeros(self.max_seq_length * n_empty_windows, dtype=np.bool_)) + bbox = torch.cat([o['bbox'] for o in output_dicts]) + + self.max_seq_length_document = self.max_seq_length * self.max_window_count + itc_labels = np.zeros(self.max_seq_length_document, dtype=int) + stc_labels = np.ones(self.max_seq_length_document, dtype=np.int64) * self.max_seq_length_document + el_labels = np.ones((self.max_seq_length_document,), dtype=int) * self.max_seq_length_document + el_labels_from_key = np.ones((self.max_seq_length_document,), dtype=int) * self.max_seq_length_document + + # Parse word groups + classes_dic = json_obj["parse"]["class"] + for class_name in self.class_names: + if class_name == "others": + continue + if class_name not in classes_dic: + continue + + word_lists = classes_dic[class_name] + + for word_list in word_lists: + is_first, last_word_idx = True, -1 + for word_idx in word_list: + if word_idx >= len(box_to_token_indices_document): + break + box2token_list = box_to_token_indices_document[word_idx] + for converted_word_idx in box2token_list: + if converted_word_idx >= self.max_seq_length_document: + break # out of idx + + if is_first: + itc_labels[converted_word_idx] = self.class_idx_dic[ + class_name + ] + is_first, last_word_idx = False, converted_word_idx + else: + stc_labels[converted_word_idx] = last_word_idx + last_word_idx = converted_word_idx + + # Parse relation + relations = json_obj["parse"]["relations"] + + for relation in relations: + if relation[0] >= len(box2token_span_map_document) or relation[1] >= len( + box2token_span_map_document + ): + continue + if ( + box2token_span_map_document[relation[0]][0] >= self.max_seq_length_document + or box2token_span_map_document[relation[1]][0] >= self.max_seq_length_document + ): + continue + + word_from = box2token_span_map_document[relation[0]][0] + word_to = box2token_span_map_document[relation[1]][0] + + if itc_labels[word_from] == 2 and itc_labels[word_to] == 3: + el_labels_from_key[word_to] = word_from # pair of (key-value) + if itc_labels[word_from] == 4 and (itc_labels[word_to] in (2, 3)): + el_labels[word_to] = word_from # pair of (header, key) or (header-value) + + itc_labels = torch.from_numpy(itc_labels) + stc_labels = torch.from_numpy(stc_labels) + el_labels = torch.from_numpy(el_labels) + el_labels_from_key = torch.from_numpy(el_labels_from_key) + + return_dict = { + "img_path": json_obj["meta"]["image_path"], + "attention_mask": attention_mask, + "bbox": bbox, + "itc_labels": itc_labels, + "are_box_first_tokens": are_box_first_tokens, + "stc_labels": stc_labels, + "el_labels": el_labels, + "el_labels_from_key": el_labels_from_key, + "n_empty_windows": n_empty_windows + } + + output_dicts['documents'] = return_dict + return output_dicts \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/schedulers.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/schedulers.py new file mode 100755 index 0000000..b49abc2 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/schedulers.py @@ -0,0 +1,53 @@ +""" +BROS +Copyright 2022-present NAVER Corp. +Apache License v2.0 +""" + +import math + +import numpy as np +from torch.optim.lr_scheduler import LambdaLR + + +def linear_scheduler(optimizer, warmup_steps, training_steps, last_epoch=-1): + """linear_scheduler with warmup from huggingface""" + + def lr_lambda(current_step): + if current_step < warmup_steps: + return float(current_step) / float(max(1, warmup_steps)) + return max( + 0.0, + float(training_steps - current_step) + / float(max(1, training_steps - warmup_steps)), + ) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def cosine_scheduler( + optimizer, warmup_steps, training_steps, cycles=0.5, last_epoch=-1 +): + """Cosine LR scheduler with warmup from huggingface""" + + def lr_lambda(current_step): + if current_step < warmup_steps: + return current_step / max(1, warmup_steps) + progress = current_step - warmup_steps + progress /= max(1, training_steps - warmup_steps) + return max(0.0, 0.5 * (1.0 + math.cos(math.pi * cycles * 2 * progress))) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def multistep_scheduler(optimizer, warmup_steps, milestones, gamma=0.1, last_epoch=-1): + def lr_lambda(current_step): + if current_step < warmup_steps: + # calculate a warmup ratio + return current_step / max(1, warmup_steps) + else: + # calculate a multistep lr scaling ratio + idx = np.searchsorted(milestones, current_step) + return gamma ** idx + + return LambdaLR(optimizer, lr_lambda, last_epoch) diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/utils.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/utils.py new file mode 100755 index 0000000..92cdd5e --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/lightning_modules/utils.py @@ -0,0 +1,218 @@ +import os +import copy +import numpy as np +import torch +import torch.nn as nn +import math + + +def _get_number_samples(dataset_root_path): + n_samples = 0 + for dataset_dir in dataset_root_path: + with open( + os.path.join(dataset_dir, f"preprocessed_files_train.txt"), + "r", + encoding="utf-8", + ) as fp: + for line in fp.readlines(): + n_samples += 1 + return n_samples + + +def sliding_windows(elements: list, window_size: int, slice_interval: int) -> list: + element_windows = [] + + if len(elements) > window_size: + max_step = math.ceil((len(elements) - window_size)/slice_interval) + + for i in range(0, max_step + 1): + # element_windows.append(copy.deepcopy(elements[min(i, len(elements) - window_size): min(i+window_size, len(elements))])) + if (i*slice_interval+window_size) >= len(elements): + _window = copy.deepcopy(elements[i*slice_interval:]) + else: + _window = copy.deepcopy(elements[i*slice_interval: i*slice_interval+window_size]) + element_windows.append(_window) + return element_windows + else: + return [elements] + +def sliding_windows_by_words(lwords: list, parse_class: dict, parse_relation: list, window_size: int, slice_interval: int) -> list: + word_windows = [] + parse_class_windows = [] + parse_relation_windows = [] + + if len(lwords) > window_size: + max_step = math.ceil((len(lwords) - window_size)/slice_interval) + for i in range(0, max_step+1): + # _word_window = copy.deepcopy(lwords[min(i*slice_interval, len(lwords) - window_size): min(i*slice_interval+window_size, len(lwords))]) + if (i*slice_interval+window_size) >= len(lwords): + _word_window = copy.deepcopy(lwords[i*slice_interval:]) + else: + _word_window = copy.deepcopy(lwords[i*slice_interval: i*slice_interval+window_size]) + + if len(_word_window) < 2: + continue + + first_word_id = _word_window[0]['word_id'] + last_word_id = _word_window[-1]['word_id'] + + # assert (last_word_id - first_word_id == window_size - 1) or (first_word_id == 0 and last_word_id == len(lwords) - 1), [v['word_id'] for v in _word_window] #(last_word_id,first_word_id,len(lwords)) + # word list + for _word in _word_window: + _word['word_id'] -= first_word_id + + + # Entity extraction + _class_window = {k: [] for k in list(parse_class.keys())} + for class_name, _parse_class in parse_class.items(): + for group in _parse_class: + tmp = [] + for idw in group: + idw -= first_word_id + if 0 <= idw <= (last_word_id - first_word_id): + tmp.append(idw) + _class_window[class_name].append(tmp) + + # Entity Linking + _relation_window = [] + for pair in parse_relation: + if all([0 <= idw - first_word_id <= (last_word_id - first_word_id) for idw in pair]): + _relation_window.append([idw - first_word_id for idw in pair]) + + word_windows.append(_word_window) + parse_class_windows.append(_class_window) + parse_relation_windows.append(_relation_window) + + return word_windows, parse_class_windows, parse_relation_windows + else: + return [lwords], [parse_class], [parse_relation] + +def merged_token_embeddings(lpatches: list, loverlaps:list, lvalids: list, average: bool) -> torch.tensor: + start_pos = 1 + end_pos = start_pos + lvalids[0] + embedding_tokens = copy.deepcopy(lpatches[0][:, start_pos:end_pos, ...]) + cls_token = copy.deepcopy(lpatches[0][:, :1, ...]) + sep_token = copy.deepcopy(lpatches[0][:, -1:, ...]) + + for i in range(1, len(lpatches)): + start_pos = 1 + end_pos = start_pos + lvalids[i] + + overlap_gap = copy.deepcopy(loverlaps[i-1]) + window = copy.deepcopy(lpatches[i][:, start_pos:end_pos, ...]) + + if overlap_gap != 0: + prev_overlap = copy.deepcopy(embedding_tokens[:, -overlap_gap:, ...]) + curr_overlap = copy.deepcopy(window[:, :overlap_gap, ...]) + assert prev_overlap.shape == curr_overlap.shape, f"{prev_overlap.shape} # {curr_overlap.shape} with overlap: {overlap_gap}" + + if average: + avg_overlap = ( + prev_overlap + curr_overlap + ) / 2. + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], avg_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], curr_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens, window], dim=1 + ) + return torch.cat([cls_token, embedding_tokens, sep_token], dim=1) + + + +def merged_token_embeddings2(lpatches: list, loverlaps:list, lvalids: list, average: bool) -> torch.tensor: + start_pos = 1 + end_pos = start_pos + lvalids[0] + embedding_tokens = lpatches[0][:, start_pos:end_pos, ...] + cls_token = lpatches[0][:, :1, ...] + sep_token = lpatches[0][:, -1:, ...] + + for i in range(1, len(lpatches)): + start_pos = 1 + end_pos = start_pos + lvalids[i] + + overlap_gap = loverlaps[i-1] + window = lpatches[i][:, start_pos:end_pos, ...] + + if overlap_gap != 0: + prev_overlap = embedding_tokens[:, -overlap_gap:, ...] + curr_overlap = window[:, :overlap_gap, ...] + assert prev_overlap.shape == curr_overlap.shape, f"{prev_overlap.shape} # {curr_overlap.shape} with overlap: {overlap_gap}" + + if average: + avg_overlap = ( + prev_overlap + curr_overlap + ) / 2. + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], avg_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], prev_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens, window], dim=1 + ) + return torch.cat([cls_token, embedding_tokens, sep_token], dim=1) + + + +# def merged_token_embeddings(lpatches: list, loverlaps:list, lvalids: list, average: bool) -> torch.tensor: +# # start_pos = 1 +# # end_pos = start_pos + lvalids[0] +# # embedding_tokens = copy.deepcopy(lpatches[0][:, start_pos:end_pos, ...]) +# embedding_tokens = np.zeros((1, 2046, 768)) +# cls_token = copy.deepcopy(lpatches[0][:, :1, ...]) +# sep_token = copy.deepcopy(lpatches[0][:, -1:, ...]) + +# token_apperance_count = np.zeros((2046,)) +# start_idx = 1 +# for loverlap, lvalid in zip(loverlaps, lvalids): +# token_apperance_count[start_idx:start_idx+lvalid] += 1 +# start_idx = start_idx + lvalid - loverlap + +# embedding_matrix_spos = 0 +# for i in range(0, len(lpatches)): +# embedding_matrix_epos = embedding_matrix_spos + int(lvalids[i]) + +# # assert embedding_matrix_epos - embedding_matrix_spos == end_pos - 1, (embedding_matrix_spos, embedding_matrix_epos, lvalid[i], end_pos) + +# overlap_gap = copy.deepcopy(loverlaps[i]).cpu().numpy() +# window = copy.deepcopy(lpatches[i][:, 1:int(lvalids[i])+1, ...]).cpu().numpy() + +# # embedding_tokens[:,embedding_matrix_spos:embedding_matrix_epos:] = window * token_apperance_count[embedding_matrix_spos:embedding_matrix_epos] +# for i in range(len(window)): +# window[:,i,:] *= token_apperance_count[embedding_matrix_spos + i] +# embedding_tokens[: ,int(embedding_matrix_spos): int(embedding_matrix_epos), :] += window +# embedding_matrix_spos -= overlap_gap + + +# embedding_tokens = torch.tensor(embedding_tokens).type(torch.HalfTensor).cuda() + +# # if overlap_gap != 0: +# # prev_overlap = copy.deepcopy(embedding_tokens[:, -overlap_gap:, ...]) +# # curr_overlap = copy.deepcopy(window[:, :overlap_gap, ...]) +# # assert prev_overlap.shape == curr_overlap.shape, f"{prev_overlap.shape} # {curr_overlap.shape} with overlap: {overlap_gap}" + +# # if average: +# # avg_overlap = ( +# # prev_overlap + curr_overlap +# # ) / 2. +# # embedding_tokens = torch.cat( +# # [embedding_tokens[:, :-overlap_gap, ...], avg_overlap, window[:, overlap_gap:, ...]], dim=1 +# # ) +# # else: +# # embedding_tokens = torch.cat( +# # [embedding_tokens[:, :-overlap_gap, ...], curr_overlap, window[:, overlap_gap:, ...]], dim=1 +# # ) +# # else: +# # embedding_tokens = torch.cat( +# # [embedding_tokens, window], dim=1 +# # ) +# return torch.cat([cls_token, embedding_tokens, sep_token], dim=1) \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/__init__.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/__init__.py new file mode 100755 index 0000000..859a3f7 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/__init__.py @@ -0,0 +1,15 @@ + +from model.combined_model import CombinedKVUModel +from model.kvu_model import KVUModel +from model.document_kvu_model import DocumentKVUModel + +def get_model(cfg): + if cfg.stage == 1: + model = CombinedKVUModel(cfg=cfg) + elif cfg.stage == 2: + model = KVUModel(cfg=cfg) + elif cfg.stage == 3: + model = DocumentKVUModel(cfg=cfg) + else: + AssertionError('[ERROR] Trainging stage is wrong') + return model diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/combined_model.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/combined_model.py new file mode 100755 index 0000000..86b3cd9 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/combined_model.py @@ -0,0 +1,224 @@ +import os +import torch +from torch import nn +from transformers import LayoutLMv2Model, LayoutLMv2FeatureExtractor +from transformers import LayoutXLMTokenizer +from transformers import AutoTokenizer, XLMRobertaModel + + +from model.relation_extractor import RelationExtractor +from utils import load_checkpoint + + +class CombinedKVUModel(nn.Module): + def __init__(self, cfg): + super().__init__() + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.finetune_only = cfg.train.finetune_only + + self._get_backbones(self.model_cfg.backbone) + + self._create_head() + + if os.path.exists(self.model_cfg.ckpt_model_file): + self.backbone_layoutxlm = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone_layoutxlm, 'backbone_layoutxlm') + self.itc_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.itc_layer, 'itc_layer') + self.stc_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.stc_layer, 'stc_layer') + self.relation_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.relation_layer, 'relation_layer') + self.relation_layer_from_key = load_checkpoint(self.model_cfg.ckpt_model_file, self.relation_layer_from_key, 'relation_layer_from_key') + + + self.loss_func = nn.CrossEntropyLoss() + + if self.freeze: + for name, param in self.named_parameters(): + if 'backbone' in name: + param.requires_grad = False + if self.finetune_only == 'EE': + for name, param in self.named_parameters(): + if 'itc_layer' not in name and 'stc_layer' not in name: + param.requires_grad = False + if self.finetune_only == 'EL': + for name, param in self.named_parameters(): + if 'relation_layer' not in name or 'relation_layer_from_key' in name: + param.requires_grad = False + if self.finetune_only == 'ELK': + for name, param in self.named_parameters(): + if 'relation_layer_from_key' not in name: + param.requires_grad = False + + def _create_head(self): + self.backbone_hidden_size = 768 + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + self.n_classes = self.model_cfg.n_classes + 1 + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + + + def _get_backbones(self, config_type): + + self.tokenizer_layoutxlm = LayoutXLMTokenizer.from_pretrained('microsoft/layoutxlm-base') + self.feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) + self.backbone_layoutxlm = LayoutLMv2Model.from_pretrained('microsoft/layoutxlm-base') + + @staticmethod + def _init_weight(module): + init_std = 0.02 + if isinstance(module, nn.Linear): + nn.init.normal_(module.weight, 0.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + elif isinstance(module, nn.LayerNorm): + nn.init.normal_(module.weight, 1.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + + def forward(self, batch): + image = batch["image"] + input_ids_layoutxlm = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask_layoutxlm = batch["attention_mask_layoutxlm"] + + backbone_outputs_layoutxlm = self.backbone_layoutxlm( + image=image, input_ids=input_ids_layoutxlm, bbox=bbox, attention_mask=attention_mask_layoutxlm) + + last_hidden_states = backbone_outputs_layoutxlm.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(last_hidden_states, last_hidden_states).squeeze(0) + head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key} + + loss = 0.0 + if any(['labels' in key for key in batch.keys()]): + loss = self._get_loss(head_outputs, batch) + + return head_outputs, loss + + def _get_loss(self, head_outputs, batch): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + itc_loss = self._get_itc_loss(itc_outputs, batch) + stc_loss = self._get_stc_loss(stc_outputs, batch) + el_loss = self._get_el_loss(el_outputs, batch) + el_loss_from_key = self._get_el_loss(el_outputs_from_key, batch, from_key=True) + + loss = itc_loss + stc_loss + el_loss + el_loss_from_key + + return loss + + def _get_itc_loss(self, itc_outputs, batch): + itc_mask = batch["are_box_first_tokens"].view(-1) + + itc_logits = itc_outputs.view(-1, self.model_cfg.n_classes + 1) + itc_logits = itc_logits[itc_mask] + + itc_labels = batch["itc_labels"].view(-1) + itc_labels = itc_labels[itc_mask] + + itc_loss = self.loss_func(itc_logits, itc_labels) + + return itc_loss + + def _get_stc_loss(self, stc_outputs, batch): + inv_attention_mask = 1 - batch["attention_mask_layoutxlm"] + + bsz, max_seq_length = inv_attention_mask.shape + device = inv_attention_mask.device + + invalid_token_mask = torch.cat( + [inv_attention_mask, torch.zeros([bsz, 1]).to(device)], axis=1 + ).bool() + stc_outputs.masked_fill_(invalid_token_mask[:, None, :], -10000.0) + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + stc_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + stc_mask = batch["attention_mask_layoutxlm"].view(-1).bool() + + stc_logits = stc_outputs.view(-1, max_seq_length + 1) + stc_logits = stc_logits[stc_mask] + + stc_labels = batch["stc_labels"].view(-1) + stc_labels = stc_labels[stc_mask] + + stc_loss = self.loss_func(stc_logits, stc_labels) + + return stc_loss + + def _get_el_loss(self, el_outputs, batch, from_key=False): + bsz, max_seq_length = batch["attention_mask_layoutxlm"].shape + device = batch["attention_mask_layoutxlm"].device + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + + box_first_token_mask = torch.cat( + [ + (batch["are_box_first_tokens"] == False), + torch.zeros([bsz, 1], dtype=torch.bool).to(device), + ], + axis=1, + ) + el_outputs.masked_fill_(box_first_token_mask[:, None, :], -10000.0) + el_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + mask = batch["are_box_first_tokens"].view(-1) + + logits = el_outputs.view(-1, max_seq_length + 1) + logits = logits[mask] + + if from_key: + el_labels = batch["el_labels_from_key"] + else: + el_labels = batch["el_labels"] + labels = el_labels.view(-1) + labels = labels[mask] + + loss = self.loss_func(logits, labels) + return loss diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/document_kvu_model.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/document_kvu_model.py new file mode 100755 index 0000000..c97be90 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/document_kvu_model.py @@ -0,0 +1,285 @@ +import torch +from torch import nn +from transformers import LayoutLMConfig, LayoutLMModel, LayoutLMTokenizer, LayoutLMv2FeatureExtractor +from transformers import LayoutLMv2Config, LayoutLMv2Model +from transformers import LayoutXLMTokenizer +from transformers import XLMRobertaConfig, AutoTokenizer, XLMRobertaModel + + +from model.relation_extractor import RelationExtractor +from utils import load_checkpoint + + +class DocumentKVUModel(nn.Module): + def __init__(self, cfg): + super().__init__() + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.train_cfg = cfg.train + + self._get_backbones(self.model_cfg.backbone) + # if 'pth' in self.model_cfg.ckpt_model_file: + # self.backbone = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone, 'backbone') + + self._create_head() + + self.loss_func = nn.CrossEntropyLoss() + + def _create_head(self): + self.backbone_hidden_size = self.backbone_config.hidden_size + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + self.n_classes = self.model_cfg.n_classes + 1 + self.relations = self.model_cfg.n_relations + # self.repr_hiddent_size = self.backbone_hidden_size + self.n_classes + (self.train_cfg.max_seq_length + 1) * 3 + self.repr_hiddent_size = self.backbone_hidden_size + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=self.relations, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=self.relations, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # Classfication Layer for whole document + # (1) Initial token classification + self.itc_layer_document = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.repr_hiddent_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + # (3) Linking token classification + self.relation_layer_document = RelationExtractor( + n_relations=self.relations, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + # (4) Linking token classification + self.relation_layer_from_key_document = RelationExtractor( + n_relations=self.relations, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + self.relation_layer_from_key.apply(self._init_weight) + + self.itc_layer_document.apply(self._init_weight) + self.stc_layer_document.apply(self._init_weight) + self.relation_layer_document.apply(self._init_weight) + self.relation_layer_from_key_document.apply(self._init_weight) + + + def _get_backbones(self, config_type): + configs = { + 'layoutlm': {'config': LayoutLMConfig, 'tokenizer': LayoutLMTokenizer, 'backbone': LayoutLMModel, 'feature_extrator': LayoutLMv2FeatureExtractor}, + 'layoutxlm': {'config': LayoutLMv2Config, 'tokenizer': LayoutXLMTokenizer, 'backbone': LayoutLMv2Model, 'feature_extrator': LayoutLMv2FeatureExtractor}, + 'xlm-roberta': {'config': XLMRobertaConfig, 'tokenizer': AutoTokenizer, 'backbone': XLMRobertaModel, 'feature_extrator': LayoutLMv2FeatureExtractor}, + } + + self.backbone_config = configs[config_type]['config'].from_pretrained(self.model_cfg.pretrained_model_path) + if config_type != 'xlm-roberta': + self.tokenizer = configs[config_type]['tokenizer'].from_pretrained(self.model_cfg.pretrained_model_path) + else: + self.tokenizer = configs[config_type]['tokenizer'].from_pretrained(self.model_cfg.pretrained_model_path, use_fast=False) + self.feature_extractor = configs[config_type]['feature_extrator'](apply_ocr=False) + self.backbone = configs[config_type]['backbone'].from_pretrained(self.model_cfg.pretrained_model_path) + + @staticmethod + def _init_weight(module): + init_std = 0.02 + if isinstance(module, nn.Linear): + nn.init.normal_(module.weight, 0.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + elif isinstance(module, nn.LayerNorm): + nn.init.normal_(module.weight, 1.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + + def forward(self, batches): + head_outputs_list = [] + loss = 0.0 + for batch in batches["windows"]: + image = batch["image"] + input_ids = batch["input_ids"] + bbox = batch["bbox"] + attention_mask = batch["attention_mask"] + + if self.freeze: + for param in self.backbone.parameters(): + param.requires_grad = False + + if self.model_cfg.backbone == 'layoutxlm': + backbone_outputs = self.backbone( + image=image, input_ids=input_ids, bbox=bbox, attention_mask=attention_mask + ) + else: + backbone_outputs = self.backbone(input_ids, attention_mask=attention_mask) + + last_hidden_states = backbone_outputs.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(last_hidden_states, last_hidden_states).squeeze(0) + + window_repr = last_hidden_states.transpose(0, 1).contiguous() + + head_outputs = {"window_repr": window_repr, + "itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + head_outputs_list.append(head_outputs) + + batch = batches["documents"] + + document_repr = torch.cat([w['window_repr'] for w in head_outputs_list], dim=1) + document_repr = document_repr.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer_document(document_repr).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer_document(document_repr, document_repr).squeeze(0) + el_outputs = self.relation_layer_document(document_repr, document_repr).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key_document(document_repr, document_repr).squeeze(0) + + head_outputs = {"itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + return head_outputs, loss + + def _get_loss(self, head_outputs, batch): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + itc_loss = self._get_itc_loss(itc_outputs, batch) + stc_loss = self._get_stc_loss(stc_outputs, batch) + el_loss = self._get_el_loss(el_outputs, batch) + el_loss_from_key = self._get_el_loss(el_outputs_from_key, batch, from_key=True) + + loss = itc_loss + stc_loss + el_loss + el_loss_from_key + + return loss + + def _get_itc_loss(self, itc_outputs, batch): + itc_mask = batch["are_box_first_tokens"].view(-1) + + itc_logits = itc_outputs.view(-1, self.model_cfg.n_classes + 1) + itc_logits = itc_logits[itc_mask] + + itc_labels = batch["itc_labels"].view(-1) + itc_labels = itc_labels[itc_mask] + + itc_loss = self.loss_func(itc_logits, itc_labels) + + return itc_loss + + def _get_stc_loss(self, stc_outputs, batch): + inv_attention_mask = 1 - batch["attention_mask"] + + bsz, max_seq_length = inv_attention_mask.shape + device = inv_attention_mask.device + + invalid_token_mask = torch.cat( + [inv_attention_mask, torch.zeros([bsz, 1]).to(device)], axis=1 + ).bool() + stc_outputs.masked_fill_(invalid_token_mask[:, None, :], -10000.0) + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + stc_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + stc_mask = batch["attention_mask"].view(-1).bool() + + stc_logits = stc_outputs.view(-1, max_seq_length + 1) + stc_logits = stc_logits[stc_mask] + + stc_labels = batch["stc_labels"].view(-1) + stc_labels = stc_labels[stc_mask] + + stc_loss = self.loss_func(stc_logits, stc_labels) + + return stc_loss + + def _get_el_loss(self, el_outputs, batch, from_key=False): + bsz, max_seq_length = batch["attention_mask"].shape + device = batch["attention_mask"].device + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + + box_first_token_mask = torch.cat( + [ + (batch["are_box_first_tokens"] == False), + torch.zeros([bsz, 1], dtype=torch.bool).to(device), + ], + axis=1, + ) + el_outputs.masked_fill_(box_first_token_mask[:, None, :], -10000.0) + el_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + mask = batch["are_box_first_tokens"].view(-1) + + logits = el_outputs.view(-1, max_seq_length + 1) + logits = logits[mask] + + if from_key: + el_labels = batch["el_labels_from_key"] + else: + el_labels = batch["el_labels"] + labels = el_labels.view(-1) + labels = labels[mask] + + loss = self.loss_func(logits, labels) + return loss diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/kvu_model.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/kvu_model.py new file mode 100755 index 0000000..d500370 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/kvu_model.py @@ -0,0 +1,248 @@ +import os +import torch +from torch import nn +from transformers import LayoutLMv2Model, LayoutLMv2FeatureExtractor +from transformers import LayoutXLMTokenizer +from lightning_modules.utils import merged_token_embeddings, merged_token_embeddings2 + + +from model.relation_extractor import RelationExtractor +from utils import load_checkpoint + + +class KVUModel(nn.Module): + def __init__(self, cfg): + super().__init__() + + self.device = 'cuda' + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.finetune_only = cfg.train.finetune_only + + # if cfg.stage == 2: + # self.freeze = True + + self._get_backbones(self.model_cfg.backbone) + self._create_head() + + if (cfg.stage == 2) and (os.path.exists(self.model_cfg.ckpt_model_file)): + self.backbone_layoutxlm = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone_layoutxlm, 'backbone_layoutxlm') + + self._create_head() + self.loss_func = nn.CrossEntropyLoss() + + if self.freeze: + for name, param in self.named_parameters(): + if 'backbone' in name: + param.requires_grad = False + + def _create_head(self): + self.backbone_hidden_size = 768 + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + self.n_classes = self.model_cfg.n_classes + 1 + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + + + def _get_backbones(self, config_type): + self.tokenizer_layoutxlm = LayoutXLMTokenizer.from_pretrained('microsoft/layoutxlm-base') + self.feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) + self.backbone_layoutxlm = LayoutLMv2Model.from_pretrained('microsoft/layoutxlm-base') + + @staticmethod + def _init_weight(module): + init_std = 0.02 + if isinstance(module, nn.Linear): + nn.init.normal_(module.weight, 0.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + elif isinstance(module, nn.LayerNorm): + nn.init.normal_(module.weight, 1.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + + + # def forward(self, inputs): + # token_embeddings = inputs['embeddings'].transpose(0, 1).contiguous().cuda() + # itc_outputs = self.itc_layer(token_embeddings).transpose(0, 1).contiguous() + # stc_outputs = self.stc_layer(token_embeddings, token_embeddings).squeeze(0) + # el_outputs = self.relation_layer(token_embeddings, token_embeddings).squeeze(0) + # el_outputs_from_key = self.relation_layer_from_key(token_embeddings, token_embeddings).squeeze(0) + # head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + # "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key} + + # loss = self._get_loss(head_outputs, inputs) + # return head_outputs, loss + + + # def forward_single_doccument(self, lbatches): + def forward(self, lbatches): + windows = lbatches['windows'] + token_embeddings_windows = [] + lvalids = [] + loverlaps = [] + + for i, batch in enumerate(windows): + batch = {k: v.cuda() for k, v in batch.items() if k not in ('img_path', 'words')} + image = batch["image"] + input_ids_layoutxlm = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask_layoutxlm = batch["attention_mask_layoutxlm"] + + + backbone_outputs_layoutxlm = self.backbone_layoutxlm( + image=image, input_ids=input_ids_layoutxlm, bbox=bbox, attention_mask=attention_mask_layoutxlm) + + + last_hidden_states_layoutxlm = backbone_outputs_layoutxlm.last_hidden_state[:, :512, :] + + lvalids.append(batch['len_valid_tokens']) + loverlaps.append(batch['len_overlap_tokens']) + token_embeddings_windows.append(last_hidden_states_layoutxlm) + + + token_embeddings = merged_token_embeddings2(token_embeddings_windows, loverlaps, lvalids, average=False) + # token_embeddings = merged_token_embeddings(token_embeddings_windows, loverlaps, lvalids, average=True) + + + token_embeddings = token_embeddings.transpose(0, 1).contiguous().cuda() + itc_outputs = self.itc_layer(token_embeddings).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(token_embeddings, token_embeddings).squeeze(0) + el_outputs = self.relation_layer(token_embeddings, token_embeddings).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(token_embeddings, token_embeddings).squeeze(0) + head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key, + 'embedding_tokens': token_embeddings.transpose(0, 1).contiguous().detach().cpu().numpy()} + + + + loss = 0.0 + if any(['labels' in key for key in lbatches.keys()]): + labels = {k: v.cuda() for k, v in lbatches["documents"].items() if k not in ('img_path')} + loss = self._get_loss(head_outputs, labels) + + return head_outputs, loss + + def _get_loss(self, head_outputs, batch): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + itc_loss = self._get_itc_loss(itc_outputs, batch) + stc_loss = self._get_stc_loss(stc_outputs, batch) + el_loss = self._get_el_loss(el_outputs, batch) + el_loss_from_key = self._get_el_loss(el_outputs_from_key, batch, from_key=True) + + loss = itc_loss + stc_loss + el_loss + el_loss_from_key + + return loss + + def _get_itc_loss(self, itc_outputs, batch): + itc_mask = batch["are_box_first_tokens"].view(-1) + + itc_logits = itc_outputs.view(-1, self.model_cfg.n_classes + 1) + itc_logits = itc_logits[itc_mask] + + itc_labels = batch["itc_labels"].view(-1) + itc_labels = itc_labels[itc_mask] + + itc_loss = self.loss_func(itc_logits, itc_labels) + + return itc_loss + + def _get_stc_loss(self, stc_outputs, batch): + inv_attention_mask = 1 - batch["attention_mask_layoutxlm"] + + bsz, max_seq_length = inv_attention_mask.shape + device = inv_attention_mask.device + + invalid_token_mask = torch.cat( + [inv_attention_mask, torch.zeros([bsz, 1]).to(device)], axis=1 + ).bool() + + stc_outputs.masked_fill_(invalid_token_mask[:, None, :], -10000.0) + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + stc_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + stc_mask = batch["attention_mask_layoutxlm"].view(-1).bool() + stc_logits = stc_outputs.view(-1, max_seq_length + 1) + stc_logits = stc_logits[stc_mask] + + stc_labels = batch["stc_labels"].view(-1) + stc_labels = stc_labels[stc_mask] + + stc_loss = self.loss_func(stc_logits, stc_labels) + + return stc_loss + + def _get_el_loss(self, el_outputs, batch, from_key=False): + bsz, max_seq_length = batch["attention_mask_layoutxlm"].shape + + device = batch["attention_mask_layoutxlm"].device + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + + box_first_token_mask = torch.cat( + [ + (batch["are_box_first_tokens"] == False), + torch.zeros([bsz, 1], dtype=torch.bool).to(device), + ], + axis=1, + ) + el_outputs.masked_fill_(box_first_token_mask[:, None, :], -10000.0) + el_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + mask = batch["are_box_first_tokens"].view(-1) + + logits = el_outputs.view(-1, max_seq_length + 1) + logits = logits[mask] + + if from_key: + el_labels = batch["el_labels_from_key"] + else: + el_labels = batch["el_labels"] + labels = el_labels.view(-1) + labels = labels[mask] + + loss = self.loss_func(logits, labels) + return loss diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/relation_extractor.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/relation_extractor.py new file mode 100755 index 0000000..40a169e --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/model/relation_extractor.py @@ -0,0 +1,48 @@ +import torch +from torch import nn + + +class RelationExtractor(nn.Module): + def __init__( + self, + n_relations, + backbone_hidden_size, + head_hidden_size, + head_p_dropout=0.1, + ): + super().__init__() + + self.n_relations = n_relations + self.backbone_hidden_size = backbone_hidden_size + self.head_hidden_size = head_hidden_size + self.head_p_dropout = head_p_dropout + + self.drop = nn.Dropout(head_p_dropout) + self.q_net = nn.Linear( + self.backbone_hidden_size, self.n_relations * self.head_hidden_size + ) + + self.k_net = nn.Linear( + self.backbone_hidden_size, self.n_relations * self.head_hidden_size + ) + + self.dummy_node = nn.Parameter(torch.Tensor(1, self.backbone_hidden_size)) + nn.init.normal_(self.dummy_node) + + def forward(self, h_q, h_k): + h_q = self.q_net(self.drop(h_q)) + + dummy_vec = self.dummy_node.unsqueeze(0).repeat(1, h_k.size(1), 1) + h_k = torch.cat([h_k, dummy_vec], axis=0) + h_k = self.k_net(self.drop(h_k)) + + head_q = h_q.view( + h_q.size(0), h_q.size(1), self.n_relations, self.head_hidden_size + ) + head_k = h_k.view( + h_k.size(0), h_k.size(1), self.n_relations, self.head_hidden_size + ) + + relation_score = torch.einsum("ibnd,jbnd->nbij", (head_q, head_k)) + + return relation_score diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitignore b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitignore new file mode 100755 index 0000000..bf9f45b --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitignore @@ -0,0 +1,133 @@ +externals/sdsv_dewarp +test/ +.vscode/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*/__pycache__/ +*.py[cod] +*$py.class +results/ +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitmodules b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitmodules new file mode 100755 index 0000000..4e60ad1 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/.gitmodules @@ -0,0 +1,6 @@ +[submodule "sdsvtr"] + path = externals/sdsvtr + url = https://github.com/mrlasdt/sdsvtr.git +[submodule "sdsvtd"] + path = externals/sdsvtd + url = https://github.com/mrlasdt/sdsvtd.git diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/README.md b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/README.md new file mode 100755 index 0000000..ca1b349 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/README.md @@ -0,0 +1,47 @@ +# OCR Engine + +OCR Engine is a Python package that combines text detection and recognition models from [mmdet](https://github.com/open-mmlab/mmdetection) and [mmocr](https://github.com/open-mmlab/mmocr) to perform Optical Character Recognition (OCR) on various inputs. The package currently supports three types of input: a single image, a recursive directory, or a csv file. + +## Installation + +To install OCR Engine, clone the repository and install the required packages: + +```bash +git clone git@github.com:mrlasdt/ocr-engine.git +cd ocr-engine +pip install -r requirements.txt + +``` + + +## Usage + +To use OCR Engine, simply run the `ocr_engine.py` script with the desired input type and input path. For example, to perform OCR on a single image: + +```css +python ocr_engine.py --input_type image --input_path /path/to/image.jpg +``` + +To perform OCR on a recursive directory: + +```css +python ocr_engine.py --input_type directory --input_path /path/to/directory/ + +``` + +To perform OCR on a csv file: + + +``` +python ocr_engine.py --input_type csv --input_path /path/to/file.csv +``` + +OCR Engine will automatically detect and recognize text in the input and output the results in a CSV file named `ocr_results.csv`. + +## Contributing + +If you would like to contribute to OCR Engine, please fork the repository and submit a pull request. We welcome contributions of all types, including bug fixes, new features, and documentation improvements. + +## License + +OCR Engine is released under the [MIT License](https://opensource.org/licenses/MIT). See the LICENSE file for more information. diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/__init__.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/__init__.py new file mode 100755 index 0000000..aabc310 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/__init__.py @@ -0,0 +1,11 @@ +# # Define package-level variables +# __version__ = '0.0' + +# Import modules +from .src.ocr import OcrEngine +# from .src.word_formation import words_to_lines +from .src.word_formation import words_to_lines_tesseract as words_to_lines +from .src.utils import ImageReader, read_ocr_result_from_txt +from .src.dto import Word, Line, Page, Document, Box +# Expose package contents +__all__ = ["OcrEngine", "Box", "Word", "Line", "Page", "Document", "words_to_lines", "ImageReader", "read_ocr_result_from_txt"] diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/requirements.txt b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/requirements.txt new file mode 100755 index 0000000..b247e52 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/requirements.txt @@ -0,0 +1,82 @@ +addict==2.4.0 +asttokens==2.2.1 +autopep8==1.6.0 +backcall==0.2.0 +backports.functools-lru-cache==1.6.4 +brotlipy==0.7.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==2.0.4 +click==8.1.3 +colorama==0.4.6 +cryptography==39.0.1 +debugpy==1.5.1 +decorator==5.1.1 +docopt==0.6.2 +entrypoints==0.4 +executing==1.2.0 +flit_core==3.6.0 +idna==3.4 +importlib-metadata==6.0.0 +ipykernel==6.15.0 +ipython==8.11.0 +jedi==0.18.2 +jupyter-client==7.0.6 +jupyter_core==4.12.0 +Markdown==3.4.1 +markdown-it-py==2.2.0 +matplotlib-inline==0.1.6 +mdurl==0.1.2 +mkl-fft==1.3.1 +mkl-random==1.2.2 +mkl-service==2.4.0 +mmcv-full==1.7.1 +model-index==0.1.11 +nest-asyncio==1.5.6 +numpy==1.23.5 +opencv-python==4.7.0.72 +openmim==0.3.6 +ordered-set==4.1.0 +packaging==23.0 +pandas==1.5.3 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.4.0 +pip==22.3.1 +pipdeptree==2.5.2 +prompt-toolkit==3.0.38 +psutil==5.9.0 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pycodestyle==2.10.0 +pycparser==2.21 +Pygments==2.14.0 +pyOpenSSL==23.0.0 +PySocks==1.7.1 +python-dateutil==2.8.2 +pytz==2022.7.1 +PyYAML==6.0 +pyzmq==19.0.2 +requests==2.28.1 +rich==13.3.1 +sdsvtd==0.1.1 +sdsvtr==0.0.5 +setuptools==65.6.3 +Shapely==1.8.4 +six==1.16.0 +stack-data==0.6.2 +tabulate==0.9.0 +toml==0.10.2 +torch==1.13.1 +torchvision==0.14.1 +tornado==6.1 +tqdm==4.65.0 +traitlets==5.9.0 +typing_extensions==4.4.0 +urllib3==1.26.14 +wcwidth==0.2.6 +wheel==0.38.4 +yapf==0.32.0 +yarg==0.1.9 +zipp==3.15.0 diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/run.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/run.py new file mode 100755 index 0000000..5837bf1 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/run.py @@ -0,0 +1,143 @@ +""" +see scripts/run_ocr.sh to run +""" +# from pathlib import Path # add parent path to run debugger +# import sys +# FILE = Path(__file__).absolute() +# sys.path.append(FILE.parents[2].as_posix()) + + +from src.utils import construct_file_path, ImageReader +from src.dto import Line +from src.ocr import OcrEngine +import argparse +import tqdm +import pandas as pd +from pathlib import Path +import json +import os +import numpy as np +from typing import Union, Tuple, List +current_dir = os.getcwd() + + +def get_args(): + parser = argparse.ArgumentParser() + # parser image + parser.add_argument("--image", type=str, required=True, + help="path to input image/directory/csv file") + parser.add_argument("--save_dir", type=str, required=True, + help="path to save directory") + parser.add_argument( + "--base_dir", type=str, required=False, default=current_dir, + help="used when --image and --save_dir are relative paths to a base directory, default to current directory") + parser.add_argument( + "--export_csv", type=str, required=False, default="", + help="used when --image is a directory. If set, a csv file contains image_path, ocr_path and label will be exported to save_dir.") + parser.add_argument( + "--export_img", type=bool, required=False, default=False, help="whether to save the visualize img") + parser.add_argument("--ocr_kwargs", type=str, required=False, default="") + opt = parser.parse_args() + return opt + + +def load_engine(opt) -> OcrEngine: + print("[INFO] Loading engine...") + kw = json.loads(opt.ocr_kwargs) if opt.ocr_kwargs else {} + engine = OcrEngine(**kw) + print("[INFO] Engine loaded") + return engine + + +def convert_relative_path_to_positive_path(tgt_dir: Path, base_dir: Path) -> Path: + return tgt_dir if tgt_dir.is_absolute() else base_dir.joinpath(tgt_dir) + + +def get_paths_from_opt(opt) -> Tuple[Path, Path]: + # BC\ kiem\ tra\ y\ te -> BC kiem tra y te + img_path = opt.image.replace("\\ ", " ").strip() + save_dir = opt.save_dir.replace("\\ ", " ").strip() + base_dir = opt.base_dir.replace("\\ ", " ").strip() + input_image = convert_relative_path_to_positive_path( + Path(img_path), Path(base_dir)) + save_dir = convert_relative_path_to_positive_path( + Path(save_dir), Path(base_dir)) + if not save_dir.exists(): + save_dir.mkdir() + print("[INFO]: Creating folder ", save_dir) + return input_image, save_dir + + +def process_img(img: Union[str, np.ndarray], save_dir_or_path: str, engine: OcrEngine, export_img: bool) -> None: + save_dir_or_path = Path(save_dir_or_path) + if isinstance(img, np.ndarray): + if save_dir_or_path.is_dir(): + raise ValueError( + "numpy array input require a save path, not a save dir") + page = engine(img) + save_path = str(save_dir_or_path.joinpath(Path(img).stem + ".txt") + ) if save_dir_or_path.is_dir() else str(save_dir_or_path) + page.write_to_file('word', save_path) + if export_img: + page.save_img(save_path.replace(".txt", ".jpg"), is_vnese=True, ) + + +def process_dir( + dir_path: str, save_dir: str, engine: OcrEngine, export_img: bool, lskip_dir: List[str] = [], + ddata: dict = {"img_path": list(), + "ocr_path": list(), + "label": list()}) -> None: + dir_path = Path(dir_path) + # save_dir_sub = Path(construct_file_path(save_dir, dir_path, ext="")) + save_dir = Path(save_dir) + save_dir.mkdir(exist_ok=True) + for img_path in (pbar := tqdm.tqdm(dir_path.iterdir())): + pbar.set_description(f"Processing {dir_path}") + if img_path.is_dir() and img_path not in lskip_dir: + save_dir_sub = save_dir.joinpath(img_path.stem) + process_dir(img_path, str(save_dir_sub), engine, ddata) + elif img_path.suffix.lower() in ImageReader.supported_ext: + simg_path = str(img_path) + try: + img = ImageReader.read( + simg_path) if img_path.suffix != ".pdf" else ImageReader.read(simg_path)[0] + save_path = str(Path(save_dir).joinpath( + img_path.stem + ".txt")) + process_img(img, save_path, engine, export_img) + except Exception as e: + print('[ERROR]: ', e, ' at ', simg_path) + continue + ddata["img_path"].append(simg_path) + ddata["ocr_path"].append(save_path) + ddata["label"].append(dir_path.stem) + # ddata.update({"img_path": img_path, "save_path": save_path, "label": dir_path.stem}) + return ddata + + +def process_csv(csv_path: str, engine: OcrEngine) -> None: + df = pd.read_csv(csv_path) + if not 'image_path' in df.columns or not 'ocr_path' in df.columns: + raise AssertionError('Cannot fing image_path in df headers') + for row in df.iterrows(): + process_img(row.image_path, row.ocr_path, engine) + + +if __name__ == "__main__": + opt = get_args() + engine = load_engine(opt) + print("[INFO]: OCR engine settings:", engine.settings) + img, save_dir = get_paths_from_opt(opt) + + lskip_dir = [] + if img.is_dir(): + ddata = process_dir(img, save_dir, engine, opt.export_img) + if opt.export_csv: + pd.DataFrame.from_dict(ddata).to_csv( + Path(save_dir).joinpath(opt.export_csv)) + elif img.suffix in ImageReader.supported_ext: + process_img(str(img), save_dir, engine, opt.export_img) + elif img.suffix == '.csv': + print("[WARNING]: Running with csv file will ignore the save_dir argument. Instead, the ocr_path in the csv would be used") + process_csv(img, engine) + else: + raise NotImplementedError('[ERROR]: Unsupported file {}'.format(img)) diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/scripts/run_ocr.sh b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/scripts/run_ocr.sh new file mode 100755 index 0000000..13d5c26 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/scripts/run_ocr.sh @@ -0,0 +1,23 @@ +#bash scripts/run_ocr.sh -i /mnt/ssd1T/hungbnt/DocumentClassification/data/OCR040_043 -o /mnt/ssd1T/hungbnt/DocumentClassification/results/ocr/OCR040_043 -e out.csv -k "{\"device\":\"cuda:1\"}" -x True +export PYTHONWARNINGS="ignore" + +while getopts i:o:b:e:x:k: flag +do + case "${flag}" in + i) img=${OPTARG};; + o) out_dir=${OPTARG};; + b) base_dir=${OPTARG};; + e) export_csv=${OPTARG};; + x) export_img=${OPTARG};; + k) ocr_kwargs=${OPTARG};; + esac +done +echo "run.py --image=\"$img\" --save_dir \"$out_dir\" --base_dir \"$base_dir\" --export_csv \"$export_csv\" --export_img \"$export_img\" --ocr_kwargs \"$ocr_kwargs\"" + +python run.py \ + --image="$img" \ + --save_dir $out_dir \ + --export_csv $export_csv\ + --export_img $export_img\ + --ocr_kwargs $ocr_kwargs\ + diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/settings.yml b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/settings.yml new file mode 100755 index 0000000..3232828 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/settings.yml @@ -0,0 +1,17 @@ +detector: "/models/Kie_invoice_ap/wild_receipt_finetune_weights_c_lite.pth" +rotator_version: "/models/Kie_invoice_ap/best_bbox_mAP_epoch_30_lite.pth" +recog_max_seq_len: 25 +recognizer: "satrn-lite-general-pretrain-20230106" +device: "cuda:0" +do_extend_bbox: True +margin_bbox: [0, 0.03, 0.02, 0.05] +batch_mode: False +batch_size: 16 +auto_rotate: True +img_size: [] #[1920,1920] #text det default size: 1280x1280 #[] = originla size, TODO: fix the deskew code to resize the image only for detecting the angle, we want to feed the original size image to the text detection pipeline so that the bounding boxes would be mapped back to the original size +deskew: False #True +words_to_lines: { + "gradient": 0.6, + "max_x_dist": 20, + "y_overlap_threshold": 0.5, +} \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/dto.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/dto.py new file mode 100755 index 0000000..c1c644d --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/dto.py @@ -0,0 +1,453 @@ +import numpy as np +from typing import Optional, List +import cv2 +from PIL import Image +from .utils import visualize_bbox_and_label + + +class Box: + def __init__(self, x1, y1, x2, y2, conf=-1., label=""): + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + self.conf = conf + self.label = label + + def __repr__(self) -> str: + return str(self.bbox) + + def __str__(self) -> str: + return str(self.bbox) + + def get(self, return_confidence=False) -> list: + return self.bbox if not return_confidence else self.xyxyc + + def __getitem__(self, key): + return self.bbox[key] + + @property + def width(self): + return self.x2 - self.x1 + + @property + def height(self): + return self.y2 - self.y1 + + @property + def bbox(self) -> list: + return [self.x1, self.y1, self.x2, self.y2] + + @bbox.setter + def bbox(self, bbox_: list): + self.x1, self.y1, self.x2, self.y2 = bbox_ + + @property + def xyxyc(self) -> list: + return [self.x1, self.y1, self.x2, self.y2, self.conf] + + @staticmethod + def normalize_bbox(bbox: list): + return [int(b) for b in bbox] + + def to_int(self): + self.x1, self.y1, self.x2, self.y2 = self.normalize_bbox([self.x1, self.y1, self.x2, self.y2]) + return self + + @staticmethod + def clamp_bbox_by_img_wh(bbox: list, width: int, height: int): + x1, y1, x2, y2 = bbox + x1 = min(max(0, x1), width) + x2 = min(max(0, x2), width) + y1 = min(max(0, y1), height) + y2 = min(max(0, y2), height) + return (x1, y1, x2, y2) + + def clamp_by_img_wh(self, width: int, height: int): + self.x1, self.y1, self.x2, self.y2 = self.clamp_bbox_by_img_wh( + [self.x1, self.y1, self.x2, self.y2], width, height) + return self + + @staticmethod + def extend_bbox(bbox: list, margin: list): # -> Self (python3.11) + margin_l, margin_t, margin_r, margin_b = margin + l, t, r, b = bbox # left, top, right, bottom + t = t - (b - t) * margin_t + b = b + (b - t) * margin_b + l = l - (r - l) * margin_l + r = r + (r - l) * margin_r + return [l, t, r, b] + + def get_extend_bbox(self, margin: list): + extended_bbox = self.extend_bbox(self.bbox, margin) + return Box(*extended_bbox, label=self.label) + + @staticmethod + def bbox_is_valid(bbox: list) -> bool: + l, t, r, b = bbox # left, top, right, bottom + return True if (b - t) * (r - l) > 0 else False + + def is_valid(self) -> bool: + return self.bbox_is_valid(self.bbox) + + @staticmethod + def crop_img_by_bbox(img: np.ndarray, bbox: list) -> np.ndarray: + l, t, r, b = bbox + return img[t:b, l:r] + + def crop_img(self, img: np.ndarray) -> np.ndarray: + return self.crop_img_by_bbox(img, self.bbox) + + +class Word: + def __init__( + self, + image=None, + text="", + conf_cls=-1., + bndbox: Optional[Box] = None, + conf_detect=-1., + kie_label="", + ): + self.type = "word" + self.text = text + self.image = image + self.conf_detect = conf_detect + self.conf_cls = conf_cls + # [left, top,right,bot] coordinate of top-left and bottom-right point + self.boundingbox = bndbox + self.word_id = 0 # id of word + self.word_group_id = 0 # id of word_group which instance belongs to + self.line_id = 0 # id of line which instance belongs to + self.paragraph_id = 0 # id of line which instance belongs to + self.kie_label = kie_label + + @property + def bbox(self): + return self.boundingbox.bbox + + @property + def height(self): + return self.boundingbox.height + + @property + def width(self): + return self.boundingbox.width + + def __repr__(self) -> str: + return self.text + + def __str__(self) -> str: + return self.text + + def invalid_size(self): + return (self.boundingbox[2] - self.boundingbox[0]) * ( + self.boundingbox[3] - self.boundingbox[1] + ) > 0 + + def is_special_word(self): + left, top, right, bottom = self.boundingbox + width, height = right - left, bottom - top + text = self.text + + if text is None: + return True + + # if len(text) > 7: + # return True + if len(text) >= 7: + no_digits = sum(c.isdigit() for c in text) + return no_digits / len(text) >= 0.3 + + return False + + +class Word_group: + def __init__(self, list_words_: List[Word] = list(), + text: str = '', boundingbox: Box = Box(-1, -1, -1, -1), conf_cls: float = -1): + self.type = "word_group" + self.list_words = list_words_ # dict of word instances + self.word_group_id = 0 # word group id + self.line_id = 0 # id of line which instance belongs to + self.paragraph_id = 0 # id of paragraph which instance belongs to + self.text = text + self.boundingbox = boundingbox + self.kie_label = "" + self.conf_cls = conf_cls + + @property + def bbox(self): + return self.boundingbox + + def __repr__(self) -> str: + return self.text + + def __str__(self) -> str: + return self.text + + def add_word(self, word: Word): # add a word instance to the word_group + if word.text != "✪": + for w in self.list_words: + if word.word_id == w.word_id: + print("Word id collision") + return False + word.word_group_id = self.word_group_id # + word.line_id = self.line_id + word.paragraph_id = self.paragraph_id + self.list_words.append(word) + self.text += " " + word.text + if self.boundingbox == [-1, -1, -1, -1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [ + min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3]), + ] + return True + else: + return False + + def update_word_group_id(self, new_word_group_id): + self.word_group_id = new_word_group_id + for i in range(len(self.list_words)): + self.list_words[i].word_group_id = new_word_group_id + + def update_kie_label(self): + list_kie_label = [word.kie_label for word in self.list_words] + dict_kie = dict() + for label in list_kie_label: + if label not in dict_kie: + dict_kie[label] = 1 + else: + dict_kie[label] += 1 + total = len(list(dict_kie.values())) + max_value = max(list(dict_kie.values())) + list_keys = list(dict_kie.keys()) + list_values = list(dict_kie.values()) + self.kie_label = list_keys[list_values.index(max_value)] + + def update_text(self): # update text after changing positions of words in list word + text = "" + for word in self.list_words: + text += " " + word.text + self.text = text + + +class Line: + def __init__(self, list_word_groups: List[Word_group] = [], + text: str = '', boundingbox: Box = Box(-1, -1, -1, -1), conf_cls: float = -1): + self.type = "line" + self.list_word_groups = list_word_groups # list of Word_group instances in the line + self.line_id = 0 # id of line in the paragraph + self.paragraph_id = 0 # id of paragraph which instance belongs to + self.text = text + self.boundingbox = boundingbox + self.conf_cls = conf_cls + + @property + def bbox(self): + return self.boundingbox + + def __repr__(self) -> str: + return self.text + + def __str__(self) -> str: + return self.text + + def add_group(self, word_group: Word_group): # add a word_group instance + if word_group.list_words is not None: + for wg in self.list_word_groups: + if word_group.word_group_id == wg.word_group_id: + print("Word_group id collision") + return False + + self.list_word_groups.append(word_group) + self.text += word_group.text + word_group.paragraph_id = self.paragraph_id + word_group.line_id = self.line_id + + for i in range(len(word_group.list_words)): + word_group.list_words[ + i + ].paragraph_id = self.paragraph_id # set paragraph_id for word + word_group.list_words[i].line_id = self.line_id # set line_id for word + return True + return False + + def update_line_id(self, new_line_id): + self.line_id = new_line_id + for i in range(len(self.list_word_groups)): + self.list_word_groups[i].line_id = new_line_id + for j in range(len(self.list_word_groups[i].list_words)): + self.list_word_groups[i].list_words[j].line_id = new_line_id + + def merge_word(self, word): # word can be a Word instance or a Word_group instance + if word.text != "✪": + if self.boundingbox == [-1, -1, -1, -1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [ + min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3]), + ] + self.list_word_groups.append(word) + self.text += " " + word.text + return True + return False + + def __cal_ratio(self, top1, bottom1, top2, bottom2): + sorted_vals = sorted([top1, bottom1, top2, bottom2]) + intersection = sorted_vals[2] - sorted_vals[1] + min_height = min(bottom1 - top1, bottom2 - top2) + if min_height == 0: + return -1 + ratio = intersection / min_height + return ratio + + def __cal_ratio_height(self, top1, bottom1, top2, bottom2): + + height1, height2 = top1 - bottom1, top2 - bottom2 + ratio_height = float(max(height1, height2)) / float(min(height1, height2)) + return ratio_height + + def in_same_line(self, input_line, thresh=0.7): + # calculate iou in vertical direction + _, top1, _, bottom1 = self.boundingbox + _, top2, _, bottom2 = input_line.boundingbox + + ratio = self.__cal_ratio(top1, bottom1, top2, bottom2) + ratio_height = self.__cal_ratio_height(top1, bottom1, top2, bottom2) + + if ( + (top2 <= top1 <= bottom2) or (top1 <= top2 <= bottom1) + and ratio >= thresh + and (ratio_height < 2) + ): + return True + return False + + +class Paragraph: + def __init__(self, id=0, lines=None): + self.list_lines = lines if lines is not None else [] # list of all lines in the paragraph + self.paragraph_id = id # index of paragraph in the ist of paragraph + self.text = "" + self.boundingbox = [-1, -1, -1, -1] + + @property + def bbox(self): + return self.boundingbox + + def __repr__(self) -> str: + return self.text + + def __str__(self) -> str: + return self.text + + def add_line(self, line: Line): # add a line instance + if line.list_word_groups is not None: + for l in self.list_lines: + if line.line_id == l.line_id: + print("Line id collision") + return False + for i in range(len(line.list_word_groups)): + line.list_word_groups[ + i + ].paragraph_id = ( + self.paragraph_id + ) # set paragraph id for every word group in line + for j in range(len(line.list_word_groups[i].list_words)): + line.list_word_groups[i].list_words[ + j + ].paragraph_id = ( + self.paragraph_id + ) # set paragraph id for every word in word groups + line.paragraph_id = self.paragraph_id # set paragraph id for line + self.list_lines.append(line) # add line to paragraph + self.text += " " + line.text + return True + else: + return False + + def update_paragraph_id( + self, new_paragraph_id + ): # update new paragraph_id for all lines, word_groups, words inside paragraph + self.paragraph_id = new_paragraph_id + for i in range(len(self.list_lines)): + self.list_lines[ + i + ].paragraph_id = new_paragraph_id # set new paragraph_id for line + for j in range(len(self.list_lines[i].list_word_groups)): + self.list_lines[i].list_word_groups[ + j + ].paragraph_id = new_paragraph_id # set new paragraph_id for word_group + for k in range(len(self.list_lines[i].list_word_groups[j].list_words)): + self.list_lines[i].list_word_groups[j].list_words[ + k + ].paragraph_id = new_paragraph_id # set new paragraph id for word + return True + + +class Page: + def __init__(self, llines: List[Line], image: np.ndarray) -> None: + self.__llines = llines + self.__image = image + self.__drawed_image = None + + @property + def llines(self): + return self.__llines + + @property + def image(self): + return self.__image + + @property + def PIL_image(self): + return Image.fromarray(self.__image) + + @property + def drawed_image(self): + if self.__drawed_image: + self.__drawed_image = self + + def visualize_bbox_and_label(self, **kwargs: dict): + if self.__drawed_image is not None: + return self.__drawed_image + bboxes = list() + texts = list() + for line in self.__llines: + for word_group in line.list_word_groups: + for word in word_group.list_words: + bboxes.append([int(float(b)) for b in word.bbox[:]]) + texts.append(word.text) + img = visualize_bbox_and_label(self.__image, bboxes, texts, **kwargs) + self.__drawed_image = img + return self.__drawed_image + + def save_img(self, save_path: str, **kwargs: dict) -> None: + img = self.visualize_bbox_and_label(**kwargs) + cv2.imwrite(save_path, img) + + def write_to_file(self, mode: str, save_path: str) -> None: + f = open(save_path, "w+", encoding="utf-8") + for line in self.__llines: + if mode == 'line': + xmin, ymin, xmax, ymax = line.bbox[:] + f.write("{}\t{}\t{}\t{}\t{}\n".format(xmin, ymin, xmax, ymax, line.text)) + elif mode == "word": + for word_group in line.list_word_groups: + for word in word_group.list_words: + # xmin, ymin, xmax, ymax = word.bbox[:] + xmin, ymin, xmax, ymax = [int(float(b)) for b in word.bbox[:]] + f.write("{}\t{}\t{}\t{}\t{}\n".format(xmin, ymin, xmax, ymax, word.text)) + f.close() + + +class Document: + def __init__(self, lpages: List[Page]) -> None: + self.lpages = lpages diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/ocr.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/ocr.py new file mode 100755 index 0000000..2c30c01 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/ocr.py @@ -0,0 +1,207 @@ +from typing import Union, overload, List, Optional, Tuple +from PIL import Image +import torch +import numpy as np +import yaml +from pathlib import Path +import mmcv +from sdsvtd import StandaloneYOLOXRunner +from sdsvtr import StandaloneSATRNRunner +from .utils import ImageReader, chunks, rotate_bbox, Timer +# from .utils import jdeskew as deskew +# from externals.deskew.sdsv_dewarp import pdeskew as deskew +from .utils import deskew, post_process_recog +from .dto import Word, Line, Page, Document, Box +# from .word_formation import words_to_lines as words_to_lines +# from .word_formation import wo rds_to_lines_mmocr as words_to_lines +from .word_formation import words_to_lines_tesseract as words_to_lines +DEFAULT_SETTING_PATH = str(Path(__file__).parents[1]) + "/settings.yml" + + +class OcrEngine: + def __init__(self, settings_file: str = DEFAULT_SETTING_PATH, **kwargs: dict): + """ Warper of text detection and text recognition + :param settings_file: path to default setting file + :param kwargs: keyword arguments to overwrite the default settings file + """ + + with open(settings_file) as f: + # use safe_load instead load + self.__settings = yaml.safe_load(f) + for k, v in kwargs.items(): # overwrite default settings by keyword arguments + if k not in self.__settings: + raise ValueError("Invalid setting found in OcrEngine: ", k) + self.__settings[k] = v + + if "cuda" in self.__settings["device"]: + if not torch.cuda.is_available(): + print("[WARNING]: CUDA is not available, running with cpu instead") + self.__settings["device"] = "cpu" + self._detector = StandaloneYOLOXRunner( + version=self.__settings["detector"], + device=self.__settings["device"], + auto_rotate=self.__settings["auto_rotate"], + rotator_version=self.__settings["rotator_version"]) + self._recognizer = StandaloneSATRNRunner( + version=self.__settings["recognizer"], + return_confident=True, device=self.__settings["device"], + max_seq_len_overwrite=self.__settings["recog_max_seq_len"] + ) + # extend the bbox to avoid losing accent mark in vietnames, if using ocr for only english, disable it + self._do_extend_bbox = self.__settings["do_extend_bbox"] + # left, top, right, bottom"] + self._margin_bbox = self.__settings["margin_bbox"] + self._batch_mode = self.__settings["batch_mode"] + self._batch_size = self.__settings["batch_size"] + self._deskew = self.__settings["deskew"] + self._img_size = self.__settings["img_size"] + self.__version__ = { + "detector": self.__settings["detector"], + "recognizer": self.__settings["recognizer"], + } + + @property + def version(self): + return self.__version__ + + @property + def settings(self): + return self.__settings + + # @staticmethod + # def xyxyc_to_xyxy_c(xyxyc: np.ndarray) -> Tuple[List[list], list]: + # ''' + # convert sdsvtd yoloX detection output to list of bboxes and list of confidences + # @param xyxyc: array of shape (n, 5) + # ''' + # xyxy = xyxyc[:, :4].tolist() + # confs = xyxyc[:, 4].tolist() + # return xyxy, confs + # -> Tuple[np.ndarray, List[Box]]: + def preprocess(self, img: np.ndarray) -> np.ndarray: + img_ = img.copy() + if self.__settings["img_size"]: + img_ = mmcv.imrescale( + img, tuple(self.__settings["img_size"]), + return_scale=False, interpolation='bilinear', backend='cv2') + if self._deskew: + with Timer("deskew"): + img_, angle = deskew(img_) + # for i, bbox in enumerate(bboxes): + # rotated_bbox = rotate_bbox(bbox[:], angle, img.shape[:2]) + # bboxes[i].bbox = rotated_bbox + return img_ # , bboxes + + def run_detect(self, img: np.ndarray, return_raw: bool = False) -> Tuple[np.ndarray, Union[List[Box], List[list]]]: + ''' + run text detection and return list of xyxyc if return_confidence is True, otherwise return a list of xyxy + ''' + pred_det = self._detector(img) + if self.__settings["auto_rotate"]: + img, pred_det = pred_det + pred_det = pred_det[0] # only image at a time + return (img, pred_det.tolist()) if return_raw else (img, [Box(*xyxyc) for xyxyc in pred_det.tolist()]) + + def run_recog(self, imgs: List[np.ndarray]) -> Union[List[str], List[Tuple[str, float]]]: + if len(imgs) == 0: + return list() + pred_rec = self._recognizer(imgs) + return [(post_process_recog(word), conf) for word, conf in zip(pred_rec[0], pred_rec[1])] + + def read_img(self, img: str) -> np.ndarray: + return ImageReader.read(img) + + def get_cropped_imgs(self, img: np.ndarray, bboxes: List[Union[Box, list]]) -> Tuple[List[np.ndarray], List[bool]]: + """ + img: np image + bboxes: list of xyxy + """ + lcropped_imgs = list() + mask = list() + for bbox in bboxes: + bbox = Box(*bbox) if isinstance(bbox, list) else bbox + bbox = bbox.get_extend_bbox( + self._margin_bbox) if self._do_extend_bbox else bbox + bbox.clamp_by_img_wh(img.shape[1], img.shape[0]) + bbox.to_int() + if not bbox.is_valid(): + mask.append(False) + continue + cropped_img = bbox.crop_img(img) + lcropped_imgs.append(cropped_img) + mask.append(True) + return lcropped_imgs, mask + + def read_page(self, img: np.ndarray, bboxes: List[Union[Box, list]]) -> List[Line]: + if len(bboxes) == 0: # no bbox found + return list() + with Timer("cropped imgs"): + lcropped_imgs, mask = self.get_cropped_imgs(img, bboxes) + with Timer("recog"): + # batch_mode for efficiency + pred_recs = self.run_recog(lcropped_imgs) + with Timer("construct words"): + lwords = list() + for i in range(len(pred_recs)): + if not mask[i]: + continue + text, conf_rec = pred_recs[i][0], pred_recs[i][1] + bbox = Box(*bboxes[i]) if isinstance(bboxes[i], + list) else bboxes[i] + lwords.append(Word( + image=img, text=text, conf_cls=conf_rec, bndbox=bbox, conf_detect=bbox.conf)) + with Timer("words to lines"): + return words_to_lines( + lwords, **self.__settings["words_to_lines"])[0] + + # https://stackoverflow.com/questions/48127642/incompatible-types-in-assignment-on-union + + @overload + def __call__(self, img: Union[str, np.ndarray, Image.Image]) -> Page: ... + + @overload + def __call__( + self, img: List[Union[str, np.ndarray, Image.Image]]) -> Document: ... + + def __call__(self, img): + """ + Accept an image or list of them, return ocr result as a page or document + """ + with Timer("read image"): + img = ImageReader.read(img) + if not self._batch_mode: + if isinstance(img, list): + if len(img) == 1: + img = img[0] # in case input type is a 1 page pdf + else: + raise AssertionError( + "list input can only be used with batch_mode enabled") + img = self.preprocess(img) + with Timer("detect"): + img, bboxes = self.run_detect(img) + with Timer("read_page"): + llines = self.read_page(img, bboxes) + return Page(llines, img) + else: + lpages = [] + # chunks to reduce memory footprint + for imgs in chunks(img, self._batch_size): + # pred_dets = self._detector(imgs) + # TEMP: use list comprehension because sdsvtd do not support batch mode of text detection + img = self.preprocess(img) + img, bboxes = self.run_detect(img) + for img_, bboxes_ in zip(imgs, bboxes): + llines = self.read_page(img, bboxes_) + page = Page(llines, img) + lpages.append(page) + return Document(lpages) + + + +if __name__ == "__main__": + img_path = "/mnt/ssd1T/hungbnt/Cello/data/PH/Sea7/Sea_7_1.jpg" + engine = OcrEngine(device="cuda:0", return_confidence=True) + # https://stackoverflow.com/questions/66435480/overload-following-optional-argument + page = engine(img_path) # type: ignore + print(page.__llines) + diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/utils.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/utils.py new file mode 100755 index 0000000..d66405a --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/utils.py @@ -0,0 +1,346 @@ +from PIL import ImageFont, ImageDraw, Image, ImageOps +# import matplotlib.pyplot as plt +import numpy as np +import cv2 +import os +import time +from typing import Generator, Union, List, overload, Tuple, Callable +import glob +import math +from pathlib import Path +from pdf2image import convert_from_path +from deskew import determine_skew +from jdeskew.estimator import get_angle +from jdeskew.utility import rotate as jrotate + + +def post_process_recog(text: str) -> str: + text = text.replace("✪", " ") + return text + + +class Timer: + def __init__(self, name: str) -> None: + self.name = name + + def __enter__(self): + self.start_time = time.perf_counter() + return self + + def __exit__(self, func: Callable, *args): + self.end_time = time.perf_counter() + self.elapsed_time = self.end_time - self.start_time + print(f"[INFO]: {self.name} took : {self.elapsed_time:.6f} seconds") + + +def rotate( + image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]] +) -> np.ndarray: + old_width, old_height = image.shape[:2] + angle_radian = math.radians(angle) + width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width) + height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height) + image_center = tuple(np.array(image.shape[1::-1]) / 2) + rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) + rot_mat[1, 2] += (width - old_width) / 2 + rot_mat[0, 2] += (height - old_height) / 2 + return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background) + + +# def rotate_bbox(bbox: list, angle: float) -> list: +# # Compute the center point of the bounding box +# cx = bbox[0] + bbox[2] / 2 +# cy = bbox[1] + bbox[3] / 2 + +# # Define the scale factor for the rotated bounding box +# scale = 1.0 # following the deskew and jdeskew function +# angle_radian = math.radians(angle) + +# # Obtain the rotation matrix using cv2.getRotationMatrix2D() +# M = cv2.getRotationMatrix2D((cx, cy), angle_radian, scale) + +# # Apply the rotation matrix to the four corners of the bounding box +# corners = np.array([[bbox[0], bbox[1]], +# [bbox[0] + bbox[2], bbox[1]], +# [bbox[0] + bbox[2], bbox[1] + bbox[3]], +# [bbox[0], bbox[1] + bbox[3]]], dtype=np.float32) +# rotated_corners = cv2.transform(np.array([corners]), M)[0] + +# # Compute the bounding box of the rotated corners +# x = int(np.min(rotated_corners[:, 0])) +# y = int(np.min(rotated_corners[:, 1])) +# w = int(np.max(rotated_corners[:, 0]) - np.min(rotated_corners[:, 0])) +# h = int(np.max(rotated_corners[:, 1]) - np.min(rotated_corners[:, 1])) +# rotated_bbox = [x, y, w, h] + +# return rotated_bbox + +def rotate_bbox(bbox: List[int], angle: float, old_shape: Tuple[int, int]) -> List[int]: + # https://medium.com/@pokomaru/image-and-bounding-box-rotation-using-opencv-python-2def6c39453 + bbox_ = [bbox[0], bbox[1], bbox[2], bbox[1], bbox[2], bbox[3], bbox[0], bbox[3]] + h, w = old_shape + cx, cy = (int(w / 2), int(h / 2)) + + bbox_tuple = [ + (bbox_[0], bbox_[1]), + (bbox_[2], bbox_[3]), + (bbox_[4], bbox_[5]), + (bbox_[6], bbox_[7]), + ] # put x and y coordinates in tuples, we will iterate through the tuples and perform rotation + + rotated_bbox = [] + + for i, coord in enumerate(bbox_tuple): + M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0) + cos, sin = abs(M[0, 0]), abs(M[0, 1]) + newW = int((h * sin) + (w * cos)) + newH = int((h * cos) + (w * sin)) + M[0, 2] += (newW / 2) - cx + M[1, 2] += (newH / 2) - cy + v = [coord[0], coord[1], 1] + adjusted_coord = np.dot(M, v) + rotated_bbox.insert(i, (adjusted_coord[0], adjusted_coord[1])) + result = [int(x) for t in rotated_bbox for x in t] + return [result[i] for i in [0, 1, 2, -1]] # reformat to xyxy + + +def deskew(image: np.ndarray) -> Tuple[np.ndarray, float]: + grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + angle = 0. + try: + angle = determine_skew(grayscale) + except Exception: + pass + rotated = rotate(image, angle, (0, 0, 0)) if angle else image + return rotated, angle + + +def jdeskew(image: np.ndarray) -> Tuple[np.ndarray, float]: + angle = 0. + try: + angle = get_angle(image) + except Exception: + pass + # TODO: change resize = True and scale the bounding box + rotated = jrotate(image, angle, resize=False) if angle else image + return rotated, angle + + +class ImageReader: + """ + accept anything, return numpy array image + """ + supported_ext = [".png", ".jpg", ".jpeg", ".pdf", ".gif"] + + @staticmethod + def validate_img_path(img_path: str) -> None: + if not os.path.exists(img_path): + raise FileNotFoundError(img_path) + if os.path.isdir(img_path): + raise IsADirectoryError(img_path) + if not Path(img_path).suffix.lower() in ImageReader.supported_ext: + raise NotImplementedError("Not supported extension at {}".format(img_path)) + + @overload + @staticmethod + def read(img: Union[str, np.ndarray, Image.Image]) -> np.ndarray: ... + + @overload + @staticmethod + def read(img: List[Union[str, np.ndarray, Image.Image]]) -> List[np.ndarray]: ... + + @overload + @staticmethod + def read(img: str) -> List[np.ndarray]: ... # for pdf or directory + + @staticmethod + def read(img): + if isinstance(img, list): + return ImageReader.from_list(img) + elif isinstance(img, str) and os.path.isdir(img): + return ImageReader.from_dir(img) + elif isinstance(img, str) and img.endswith(".pdf"): + return ImageReader.from_pdf(img) + else: + return ImageReader._read(img) + + @staticmethod + def from_dir(dir_path: str) -> List[np.ndarray]: + if os.path.isdir(dir_path): + image_files = glob.glob(os.path.join(dir_path, "*")) + return ImageReader.from_list(image_files) + else: + raise NotADirectoryError(dir_path) + + @staticmethod + def from_str(img_path: str) -> np.ndarray: + ImageReader.validate_img_path(img_path) + return ImageReader.from_PIL(Image.open(img_path)) + + @staticmethod + def from_np(img_array: np.ndarray) -> np.ndarray: + return img_array + + @staticmethod + def from_PIL(img_pil: Image.Image, transpose=True) -> np.ndarray: + # if img_pil.is_animated: + # raise NotImplementedError("Only static images are supported, animated image found") + if transpose: + img_pil = ImageOps.exif_transpose(img_pil) + if img_pil.mode != "RGB": + img_pil = img_pil.convert("RGB") + + return np.array(img_pil) + + @staticmethod + def from_list(img_list: List[Union[str, np.ndarray, Image.Image]]) -> List[np.ndarray]: + limgs = list() + for img_path in img_list: + try: + if isinstance(img_path, str): + ImageReader.validate_img_path(img_path) + limgs.append(ImageReader._read(img_path)) + except (FileNotFoundError, NotImplementedError, IsADirectoryError) as e: + print("[ERROR]: ", e) + print("[INFO]: Skipping image {}".format(img_path)) + return limgs + + @staticmethod + def from_pdf(pdf_path: str, start_page: int = 0, end_page: int = 0) -> List[np.ndarray]: + pdf_file = convert_from_path(pdf_path) + if end_page is not None: + end_page = min(len(pdf_file), end_page + 1) + limgs = [np.array(pdf_page) for pdf_page in pdf_file[start_page:end_page]] + return limgs + + @staticmethod + def _read(img: Union[str, np.ndarray, Image.Image]) -> np.ndarray: + if isinstance(img, str): + return ImageReader.from_str(img) + elif isinstance(img, Image.Image): + return ImageReader.from_PIL(img) + elif isinstance(img, np.ndarray): + return ImageReader.from_np(img) + else: + raise ValueError("Invalid img argument type: ", type(img)) + + +def get_name(file_path, ext: bool = True): + file_path_ = os.path.basename(file_path) + return file_path_ if ext else os.path.splitext(file_path_)[0] + + +def construct_file_path(dir, file_path, ext=''): + ''' + args: + dir: /path/to/dir + file_path /example_path/to/file.txt + ext = '.json' + return + /path/to/dir/file.json + ''' + return os.path.join( + dir, get_name(file_path, + True)) if ext == '' else os.path.join( + dir, get_name(file_path, + False)) + ext + + +def chunks(lst: list, n: int) -> Generator: + """ + Yield successive n-sized chunks from lst. + https://stackoverflow.com/questions/312443/how-do-i-split-a-list-into-equally-sized-chunks + """ + for i in range(0, len(lst), n): + yield lst[i:i + n] + + +def read_ocr_result_from_txt(file_path: str) -> Tuple[list, list]: + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + boxes, words = [], [] + for line in lines: + if line == "": + continue + x1, y1, x2, y2, text = line.split("\t") + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + + +def get_xyxywh_base_on_format(bbox, format): + if format == "xywh": + x1, y1, w, h = bbox[0], bbox[1], bbox[2], bbox[3] + x2, y2 = x1 + w, y1 + h + elif format == "xyxy": + x1, y1, x2, y2 = bbox + w, h = x2 - x1, y2 - y1 + else: + raise NotImplementedError("Invalid format {}".format(format)) + return (x1, y1, x2, y2, w, h) + + +def get_dynamic_params_for_bbox_of_label(text, x1, y1, w, h, img_h, img_w, font): + font_scale_factor = img_h / (img_w + img_h) + font_scale = w / (w + h) * font_scale_factor # adjust font scale by width height + thickness = int(font_scale_factor) + 1 + (text_width, text_height) = cv2.getTextSize(text, font, fontScale=font_scale, thickness=thickness)[0] + text_offset_x = x1 + text_offset_y = y1 - thickness + box_coords = ((text_offset_x, text_offset_y + 1), (text_offset_x + text_width - 2, text_offset_y - text_height - 2)) + return (font_scale, thickness, text_height, box_coords) + + +def visualize_bbox_and_label( + img, bboxes, texts, bbox_color=(200, 180, 60), + text_color=(0, 0, 0), + format="xyxy", is_vnese=False, draw_text=True): + ori_img_type = type(img) + if is_vnese: + img = Image.fromarray(img) if ori_img_type is np.ndarray else img + draw = ImageDraw.Draw(img) + img_w, img_h = img.size + font_pil_str = "fonts/arial.ttf" + font_cv2 = cv2.FONT_HERSHEY_SIMPLEX + else: + img_h, img_w = img.shape[0], img.shape[1] + font_cv2 = cv2.FONT_HERSHEY_SIMPLEX + for i in range(len(bboxes)): + text = texts[i] # text = "{}: {:.0f}%".format(LABELS[classIDs[i]], confidences[i]*100) + x1, y1, x2, y2, w, h = get_xyxywh_base_on_format(bboxes[i], format) + font_scale, thickness, text_height, box_coords = get_dynamic_params_for_bbox_of_label( + text, x1, y1, w, h, img_h, img_w, font=font_cv2) + if is_vnese: + font_pil = ImageFont.truetype(font_pil_str, size=text_height) # type: ignore + fdraw_text = draw.text # type: ignore + fdraw_bbox = draw.rectangle # type: ignore + # Pil use different coordinate => y = y+thickness = y-thickness + 2*thickness + arg_text = ((box_coords[0][0], box_coords[1][1]), text) + kwarg_text = {"font": font_pil, "fill": text_color, "width": thickness} + arg_rec = ((x1, y1, x2, y2),) + kwarg_rec = {"outline": bbox_color, "width": thickness} + arg_rec_text = ((box_coords[0], box_coords[1]),) + kwarg_rec_text = {"fill": bbox_color, "width": thickness} + else: + # cv2.rectangle(img, box_coords[0], box_coords[1], color, cv2.FILLED) + # cv2.putText(img, text, (text_offset_x, text_offset_y), font, fontScale=font_scale, color=(50, 0,0), thickness=thickness) + # cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness) + fdraw_text = cv2.putText + fdraw_bbox = cv2.rectangle + arg_text = (img, text, box_coords[0]) + kwarg_text = {"fontFace": font_cv2, "fontScale": font_scale, "color": text_color, "thickness": thickness} + arg_rec = (img, (x1, y1), (x2, y2)) + kwarg_rec = {"color": bbox_color, "thickness": thickness} + arg_rec_text = (img, box_coords[0], box_coords[1]) + kwarg_rec_text = {"color": bbox_color, "thickness": cv2.FILLED} + # draw a bounding box rectangle and label on the img + fdraw_bbox(*arg_rec, **kwarg_rec) # type: ignore + if draw_text: + fdraw_bbox(*arg_rec_text, **kwarg_rec_text) # type: ignore + fdraw_text(*arg_text, **kwarg_text) # type: ignore # text have to put in front of rec_text + return np.array(img) if ori_img_type is np.ndarray and is_vnese else img diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/word_formation.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/word_formation.py new file mode 100755 index 0000000..511c783 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/ocr-engine/src/word_formation.py @@ -0,0 +1,673 @@ +from builtins import dict +from .dto import Word, Line, Word_group, Box +import numpy as np +from typing import Optional, List, Tuple, Union +MIN_IOU_HEIGHT = 0.7 +MIN_WIDTH_LINE_RATIO = 0.05 + + +def resize_to_original( + boundingbox, scale +): # resize coordinates to match size of original image + left, top, right, bottom = boundingbox + left *= scale[1] + right *= scale[1] + top *= scale[0] + bottom *= scale[0] + return [left, top, right, bottom] + + +def check_iomin(word: Word, word_group: Word_group): + min_height = min( + word.boundingbox[3] - word.boundingbox[1], + word_group.boundingbox[3] - word_group.boundingbox[1], + ) + intersect = min(word.boundingbox[3], word_group.boundingbox[3]) - max( + word.boundingbox[1], word_group.boundingbox[1] + ) + if intersect / min_height > 0.7: + return True + return False + + +def prepare_line(words): + lines = [] + visited = [False] * len(words) + for id_word, word in enumerate(words): + if word.invalid_size() == 0: + continue + new_line = True + for i in range(len(lines)): + if ( + lines[i].in_same_line(word) and not visited[id_word] + ): # check if word is in the same line with lines[i] + lines[i].merge_word(word) + new_line = False + visited[id_word] = True + + if new_line == True: + new_line = Line() + new_line.merge_word(word) + lines.append(new_line) + + # print(len(lines)) + # sort line from top to bottom according top coordinate + lines.sort(key=lambda x: x.boundingbox[1]) + return lines + + +def __create_word_group(word, word_group_id): + new_word_group_ = Word_group() + new_word_group_.list_words = list() + new_word_group_.word_group_id = word_group_id + new_word_group_.add_word(word) + + return new_word_group_ + + +def __sort_line(line): + line.list_word_groups.sort( + key=lambda x: x.boundingbox[0] + ) # sort word in lines from left to right + + return line + + +def __merge_text_for_line(line): + line.text = "" + for word in line.list_word_groups: + line.text += " " + word.text + + return line + + +def __update_list_word_groups(line, word_group_id, word_id, line_width): + + old_list_word_group = line.list_word_groups + list_word_groups = [] + + inital_word_group = __create_word_group( + old_list_word_group[0], word_group_id) + old_list_word_group[0].word_id = word_id + list_word_groups.append(inital_word_group) + word_group_id += 1 + word_id += 1 + + for word in old_list_word_group[1:]: + check_word_group = True + word.word_id = word_id + word_id += 1 + + if ( + (not list_word_groups[-1].text.endswith(":")) + and ( + (word.boundingbox[0] - list_word_groups[-1].boundingbox[2]) + / line_width + < MIN_WIDTH_LINE_RATIO + ) + and check_iomin(word, list_word_groups[-1]) + ): + list_word_groups[-1].add_word(word) + check_word_group = False + + if check_word_group: + new_word_group = __create_word_group(word, word_group_id) + list_word_groups.append(new_word_group) + word_group_id += 1 + line.list_word_groups = list_word_groups + return line, word_group_id, word_id + + +def construct_word_groups_in_each_line(lines): + line_id = 0 + word_group_id = 0 + word_id = 0 + for i in range(len(lines)): + if len(lines[i].list_word_groups) == 0: + continue + + # left, top ,right, bottom + line_width = lines[i].boundingbox[2] - \ + lines[i].boundingbox[0] # right - left + line_width = 1 # TODO: to remove + lines[i] = __sort_line(lines[i]) + + # update text for lines after sorting + lines[i] = __merge_text_for_line(lines[i]) + + lines[i], word_group_id, word_id = __update_list_word_groups( + lines[i], + word_group_id, + word_id, + line_width) + lines[i].update_line_id(line_id) + line_id += 1 + return lines + + +def words_to_lines(words, check_special_lines=True): # words is list of Word instance + # sort word by top + words.sort(key=lambda x: (x.boundingbox[1], x.boundingbox[0])) + # words.sort(key=lambda x: (sum(x.bbox[:]))) + number_of_word = len(words) + # print(number_of_word) + # sort list words to list lines, which have not contained word_group yet + lines = prepare_line(words) + + # construct word_groups in each line + lines = construct_word_groups_in_each_line(lines) + return lines, number_of_word + + +def is_on_same_line(box_a, box_b, min_y_overlap_ratio=0.8): + """Check if two boxes are on the same line by their y-axis coordinates. + + Two boxes are on the same line if they overlap vertically, and the length + of the overlapping line segment is greater than min_y_overlap_ratio * the + height of either of the boxes. + + Args: + box_a (list), box_b (list): Two bounding boxes to be checked + min_y_overlap_ratio (float): The minimum vertical overlapping ratio + allowed for boxes in the same line + + Returns: + The bool flag indicating if they are on the same line + """ + a_y_min = np.min(box_a[1::2]) + b_y_min = np.min(box_b[1::2]) + a_y_max = np.max(box_a[1::2]) + b_y_max = np.max(box_b[1::2]) + + # Make sure that box a is always the box above another + if a_y_min > b_y_min: + a_y_min, b_y_min = b_y_min, a_y_min + a_y_max, b_y_max = b_y_max, a_y_max + + if b_y_min <= a_y_max: + if min_y_overlap_ratio is not None: + sorted_y = sorted([b_y_min, b_y_max, a_y_max]) + overlap = sorted_y[1] - sorted_y[0] + min_a_overlap = (a_y_max - a_y_min) * min_y_overlap_ratio + min_b_overlap = (b_y_max - b_y_min) * min_y_overlap_ratio + return overlap >= min_a_overlap or \ + overlap >= min_b_overlap + else: + return True + return False + + +def merge_bboxes_to_group(bboxes_group, x_sorted_boxes): + merged_bboxes = [] + for box_group in bboxes_group: + merged_box = {} + merged_box['text'] = ' '.join( + [x_sorted_boxes[idx]['text'] for idx in box_group]) + x_min, y_min = float('inf'), float('inf') + x_max, y_max = float('-inf'), float('-inf') + for idx in box_group: + x_max = max(np.max(x_sorted_boxes[idx]['box'][::2]), x_max) + x_min = min(np.min(x_sorted_boxes[idx]['box'][::2]), x_min) + y_max = max(np.max(x_sorted_boxes[idx]['box'][1::2]), y_max) + y_min = min(np.min(x_sorted_boxes[idx]['box'][1::2]), y_min) + merged_box['box'] = [ + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ] + merged_box['list_words'] = [x_sorted_boxes[idx]['word'] + for idx in box_group] + merged_bboxes.append(merged_box) + return merged_bboxes + + +def stitch_boxes_into_lines(boxes, max_x_dist=10, min_y_overlap_ratio=0.3): + """Stitch fragmented boxes of words into lines. + + Note: part of its logic is inspired by @Johndirr + (https://github.com/faustomorales/keras-ocr/issues/22) + + Args: + boxes (list): List of ocr results to be stitched + max_x_dist (int): The maximum horizontal distance between the closest + edges of neighboring boxes in the same line + min_y_overlap_ratio (float): The minimum vertical overlapping ratio + allowed for any pairs of neighboring boxes in the same line + + Returns: + merged_boxes(List[dict]): List of merged boxes and texts + """ + + if len(boxes) <= 1: + if len(boxes) == 1: + boxes[0]["list_words"] = [boxes[0]["word"]] + return boxes + + # merged_groups = [] + merged_lines = [] + + # sort groups based on the x_min coordinate of boxes + x_sorted_boxes = sorted(boxes, key=lambda x: np.min(x['box'][::2])) + # store indexes of boxes which are already parts of other lines + skip_idxs = set() + + i = 0 + # locate lines of boxes starting from the leftmost one + for i in range(len(x_sorted_boxes)): + if i in skip_idxs: + continue + # the rightmost box in the current line + rightmost_box_idx = i + line = [rightmost_box_idx] + for j in range(i + 1, len(x_sorted_boxes)): + if j in skip_idxs: + continue + if is_on_same_line(x_sorted_boxes[rightmost_box_idx]['box'], + x_sorted_boxes[j]['box'], min_y_overlap_ratio): + line.append(j) + skip_idxs.add(j) + rightmost_box_idx = j + + # split line into lines if the distance between two neighboring + # sub-lines' is greater than max_x_dist + # groups = [] + # line_idx = 0 + # groups.append([line[0]]) + # for k in range(1, len(line)): + # curr_box = x_sorted_boxes[line[k]] + # prev_box = x_sorted_boxes[line[k - 1]] + # dist = np.min(curr_box['box'][::2]) - np.max(prev_box['box'][::2]) + # if dist > max_x_dist: + # line_idx += 1 + # groups.append([]) + # groups[line_idx].append(line[k]) + + # # Get merged boxes + merged_line = merge_bboxes_to_group([line], x_sorted_boxes) + merged_lines.extend(merged_line) + # merged_group = merge_bboxes_to_group(groups,x_sorted_boxes) + # merged_groups.extend(merged_group) + + merged_lines = sorted(merged_lines, key=lambda x: np.min(x['box'][1::2])) + # merged_groups = sorted(merged_groups, key=lambda x: np.min(x['box'][1::2])) + return merged_lines # , merged_groups + +# REFERENCE +# https://vigneshgig.medium.com/bounding-box-sorting-algorithm-for-text-detection-and-object-detection-from-left-to-right-and-top-cf2c523c8a85 +# https://huggingface.co/spaces/tomofi/MMOCR/blame/main/mmocr/utils/box_util.py + + +def words_to_lines_mmocr(words: List[Word], *args) -> Tuple[List[Line], Optional[int]]: + bboxes = [{"box": [w.bbox[0], w.bbox[1], w.bbox[2], w.bbox[1], w.bbox[2], w.bbox[3], w.bbox[0], w.bbox[3]], + "text":w.text, "word":w} for w in words] + merged_lines = stitch_boxes_into_lines(bboxes) + merged_groups = merged_lines # TODO: fix code to return both word group and line + lwords_groups = [Word_group(list_words_=merged_box["list_words"], + text=merged_box["text"], + boundingbox=[merged_box["box"][i] for i in [0, 1, 2, -1]]) + for merged_box in merged_groups] + + llines = [Line(text=word_group.text, list_word_groups=[word_group], boundingbox=word_group.boundingbox) + for word_group in lwords_groups] + + return llines, None # same format with the origin words_to_lines + # lines = [Line() for merged] + + +# def most_overlapping_row(rows, top, bottom, y_shift): +# max_overlap = -1 +# max_overlap_idx = -1 +# for i, row in enumerate(rows): +# row_top, row_bottom = row +# overlap = min(top + y_shift, row_top) - max(bottom + y_shift, row_bottom) +# if overlap > max_overlap: +# max_overlap = overlap +# max_overlap_idx = i +# return max_overlap_idx +def most_overlapping_row(rows, row_words, top, bottom, y_shift, max_row_size, y_overlap_threshold=0.5): + max_overlap = -1 + max_overlap_idx = -1 + overlapping_rows = [] + + for i, row in enumerate(rows): + row_top, row_bottom = row + overlap = min(top - y_shift[i], row_top) - \ + max(bottom - y_shift[i], row_bottom) + + if overlap > max_overlap: + max_overlap = overlap + max_overlap_idx = i + + # if at least overlap 1 pixel and not (overlap too much and overlap too little) + if (row_bottom <= top and row_top >= bottom) and not (top - bottom - max_overlap > max_row_size * y_overlap_threshold) and not (max_overlap < max_row_size * y_overlap_threshold): + overlapping_rows.append(i) + + # Merge overlapping rows if necessary + if len(overlapping_rows) > 1: + merge_top = max(rows[i][0] for i in overlapping_rows) + merge_bottom = min(rows[i][1] for i in overlapping_rows) + + if merge_top - merge_bottom <= max_row_size: + # Merge rows + merged_row = (merge_top, merge_bottom) + merged_words = [] + # Remove other overlapping rows + + for row_idx in overlapping_rows[:0:-1]: # [1,2,3] -> 3,2 + merged_words.extend(row_words[row_idx]) + del rows[row_idx] + del row_words[row_idx] + + rows[overlapping_rows[0]] = merged_row + row_words[overlapping_rows[0]].extend(merged_words[::-1]) + max_overlap_idx = overlapping_rows[0] + + if top - bottom - max_overlap > max_row_size * y_overlap_threshold and max_overlap < max_row_size * y_overlap_threshold: + max_overlap_idx = -1 + return max_overlap_idx + + +def stitch_boxes_into_lines_tesseract(words: list[Word], + gradient: float, y_overlap_threshold: float) -> Tuple[list[list[Word]], + float]: + sorted_words = sorted(words, key=lambda x: x.bbox[0]) + rows = [] + row_words = [] + max_row_size = sorted_words[0].height + running_y_shift = [] + for _i, word in enumerate(sorted_words): + # if word.bbox[1] > 340 and word.bbox[3] < 450: + # print("DEBUG") + # if word.text == "Lực": + # print("DEBUG") + bbox, _text = word.bbox[:], word.text + _x1, y1, _x2, y2 = bbox + top, bottom = y2, y1 + max_row_size = max(max_row_size, top - bottom) + overlap_row_idx = most_overlapping_row( + rows, row_words, top, bottom, running_y_shift, max_row_size, y_overlap_threshold) + + if overlap_row_idx == -1: # No overlapping row found + new_row = (top, bottom) + rows.append(new_row) + row_words.append([word]) + running_y_shift.append(0) + else: # Overlapping row found + row_top, row_bottom = rows[overlap_row_idx] + new_top = max(row_top, top) + new_bottom = min(row_bottom, bottom) + rows[overlap_row_idx] = (new_top, new_bottom) + row_words[overlap_row_idx].append(word) + new_shift = (bottom + top) / 2 - (row_bottom + row_top) / 2 + running_y_shift[overlap_row_idx] = gradient * \ + running_y_shift[overlap_row_idx] + (1 - gradient) * new_shift + + # Sort rows and row_texts based on the top y-coordinate + sorted_rows_data = sorted(zip(rows, row_words), key=lambda x: x[0][0]) + _sorted_rows_idx, sorted_row_words = zip(*sorted_rows_data) + # /_|<- the perpendicular line of the horizontal line and the skew line of the page + page_skew_dist = sum(running_y_shift) / len(running_y_shift) + return sorted_row_words, page_skew_dist + + +def construct_word_groups_tesseract(sorted_row_words: list[list[Word]], + max_x_dist: int, page_skew_dist: float) -> list[list[list[Word]]]: + # approximate page_skew_angle by page_skew_dist + corrected_max_x_dist = max_x_dist * abs(np.cos(page_skew_dist / 180 * 3.14)) + constructed_row_word_groups = [] + for row_words in sorted_row_words: + lword_groups = [] + line_idx = 0 + lword_groups.append([row_words[0]]) + for k in range(1, len(row_words)): + curr_box = row_words[k].bbox[:] + prev_box = row_words[k - 1].bbox[:] + dist = curr_box[0] - prev_box[2] + if dist > corrected_max_x_dist: + line_idx += 1 + lword_groups.append([]) + lword_groups[line_idx].append(row_words[k]) + constructed_row_word_groups.append(lword_groups) + return constructed_row_word_groups + + +def group_bbox_and_text(lwords: list[Word]) -> tuple[Box, tuple[str, float]]: + text = ' '.join([word.text for word in lwords]) + x_min, y_min = float('inf'), float('inf') + x_max, y_max = float('-inf'), float('-inf') + conf_det = 0 + conf_cls = 0 + for word in lwords: + x_max = max(np.max(word.bbox[::2]), x_max) + x_min = min(np.min(word.bbox[::2]), x_min) + y_max = max(np.max(word.bbox[1::2]), y_max) + y_min = min(np.min(word.bbox[1::2]), y_min) + conf_det += word.conf_detect + conf_cls += word.conf_cls + bbox = Box(x_min, y_min, x_max, y_max, conf=conf_det / len(lwords)) + return bbox, (text, conf_cls / len(lwords)) + + +def words_to_lines_tesseract(words: List[Word], + gradient: float, max_x_dist: int, y_overlap_threshold: float) -> Tuple[List[Line], + Optional[int]]: + sorted_row_words, page_skew_dist = stitch_boxes_into_lines_tesseract( + words, gradient, y_overlap_threshold) + constructed_row_word_groups = construct_word_groups_tesseract( + sorted_row_words, max_x_dist, page_skew_dist) + llines = [] + for row in constructed_row_word_groups: + lwords_row = [] + lword_groups = [] + for word_group in row: + bbox_word_group, text_word_group = group_bbox_and_text(word_group) + lwords_row.extend(word_group) + lword_groups.append( + Word_group( + list_words_=word_group, text=text_word_group[0], + conf_cls=text_word_group[1], + boundingbox=bbox_word_group)) + bbox_line, text_line = group_bbox_and_text(lwords_row) + llines.append( + Line( + list_word_groups=lword_groups, text=text_line[0], + boundingbox=bbox_line, conf_cls=text_line[1])) + return llines, None + + +def near(word_group1: Word_group, word_group2: Word_group): + min_height = min( + word_group1.boundingbox[3] - word_group1.boundingbox[1], + word_group2.boundingbox[3] - word_group2.boundingbox[1], + ) + overlap = min(word_group1.boundingbox[3], word_group2.boundingbox[3]) - max( + word_group1.boundingbox[1], word_group2.boundingbox[1] + ) + + if overlap > 0: + return True + if abs(overlap / min_height) < 1.5: + print("near enough", abs(overlap / min_height), overlap, min_height) + return True + return False + + +def calculate_iou_and_near(wg1: Word_group, wg2: Word_group): + min_height = min( + wg1.boundingbox[3] - + wg1.boundingbox[1], wg2.boundingbox[3] - wg2.boundingbox[1] + ) + overlap = min(wg1.boundingbox[3], wg2.boundingbox[3]) - max( + wg1.boundingbox[1], wg2.boundingbox[1] + ) + iou = overlap / min_height + distance = min( + abs(wg1.boundingbox[0] - wg2.boundingbox[2]), + abs(wg1.boundingbox[2] - wg2.boundingbox[0]), + ) + if iou > 0.7 and distance < 0.5 * (wg1.boundingboxp[2] - wg1.boundingbox[0]): + return True + return False + + +def construct_word_groups_to_kie_label(list_word_groups: list): + kie_dict = dict() + for wg in list_word_groups: + if wg.kie_label == "other": + continue + if wg.kie_label not in kie_dict: + kie_dict[wg.kie_label] = [wg] + else: + kie_dict[wg.kie_label].append(wg) + + new_dict = dict() + for key, value in kie_dict.items(): + if len(value) == 1: + new_dict[key] = value + continue + + value.sort(key=lambda x: x.boundingbox[1]) + new_dict[key] = value + return new_dict + + +def invoice_construct_word_groups_to_kie_label(list_word_groups: list): + kie_dict = dict() + + for wg in list_word_groups: + if wg.kie_label == "other": + continue + if wg.kie_label not in kie_dict: + kie_dict[wg.kie_label] = [wg] + else: + kie_dict[wg.kie_label].append(wg) + + return kie_dict + + +def postprocess_total_value(kie_dict): + if "total_in_words_value" not in kie_dict: + return kie_dict + + for k, value in kie_dict.items(): + if k == "total_in_words_value": + continue + l = [] + for v in value: + if v.boundingbox[3] <= kie_dict["total_in_words_value"][0].boundingbox[3]: + l.append(v) + + if len(l) != 0: + kie_dict[k] = l + + return kie_dict + + +def postprocess_tax_code_value(kie_dict): + if "buyer_tax_code_value" in kie_dict or "seller_tax_code_value" not in kie_dict: + return kie_dict + + kie_dict["buyer_tax_code_value"] = [] + for v in kie_dict["seller_tax_code_value"]: + if "buyer_name_key" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_key"][0]) + ): + kie_dict["buyer_tax_code_value"].append(v) + continue + + if "buyer_name_value" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_value"][0]) + ): + kie_dict["buyer_tax_code_value"].append(v) + continue + + if "buyer_address_value" in kie_dict and near( + kie_dict["buyer_address_value"][0], v + ): + kie_dict["buyer_tax_code_value"].append(v) + return kie_dict + + +def postprocess_tax_code_key(kie_dict): + if "buyer_tax_code_key" in kie_dict or "seller_tax_code_key" not in kie_dict: + return kie_dict + kie_dict["buyer_tax_code_key"] = [] + for v in kie_dict["seller_tax_code_key"]: + if "buyer_name_key" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_key"][0]) + ): + kie_dict["buyer_tax_code_key"].append(v) + continue + + if "buyer_name_value" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_value"][0]) + ): + kie_dict["buyer_tax_code_key"].append(v) + continue + + if "buyer_address_value" in kie_dict and near( + kie_dict["buyer_address_value"][0], v + ): + kie_dict["buyer_tax_code_key"].append(v) + + return kie_dict + + +def invoice_postprocess(kie_dict: dict): + # all keys or values which are below total_in_words_value will be thrown away + kie_dict = postprocess_total_value(kie_dict) + kie_dict = postprocess_tax_code_value(kie_dict) + kie_dict = postprocess_tax_code_key(kie_dict) + return kie_dict + + +def throw_overlapping_words(list_words): + new_list = [list_words[0]] + for word in list_words: + overlap = False + area = (word.boundingbox[2] - word.boundingbox[0]) * ( + word.boundingbox[3] - word.boundingbox[1] + ) + for word2 in new_list: + area2 = (word2.boundingbox[2] - word2.boundingbox[0]) * ( + word2.boundingbox[3] - word2.boundingbox[1] + ) + xmin_intersect = max(word.boundingbox[0], word2.boundingbox[0]) + xmax_intersect = min(word.boundingbox[2], word2.boundingbox[2]) + ymin_intersect = max(word.boundingbox[1], word2.boundingbox[1]) + ymax_intersect = min(word.boundingbox[3], word2.boundingbox[3]) + if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: + continue + + area_intersect = (xmax_intersect - xmin_intersect) * ( + ymax_intersect - ymin_intersect + ) + if area_intersect / area > 0.7 or area_intersect / area2 > 0.7: + overlap = True + if overlap == False: + new_list.append(word) + return new_list + + +def check_iou(box1: Word, box2: Box, threshold=0.9): + area1 = (box1.boundingbox[2] - box1.boundingbox[0]) * ( + box1.boundingbox[3] - box1.boundingbox[1] + ) + area2 = (box2.xmax - box2.xmin) * (box2.ymax - box2.ymin) + xmin_intersect = max(box1.boundingbox[0], box2.xmin) + ymin_intersect = max(box1.boundingbox[1], box2.ymin) + xmax_intersect = min(box1.boundingbox[2], box2.xmax) + ymax_intersect = min(box1.boundingbox[3], box2.ymax) + if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: + area_intersect = 0 + else: + area_intersect = (xmax_intersect - xmin_intersect) * ( + ymax_intersect - ymin_intersect + ) + union = area1 + area2 - area_intersect + iou = area_intersect / union + if iou > threshold: + return True + return False diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/predictor.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/predictor.py new file mode 100755 index 0000000..3f4ce36 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/predictor.py @@ -0,0 +1,230 @@ +from omegaconf import OmegaConf +import os +import cv2 +import torch +# from functions import get_colormap, visualize +import sys +sys.path.append('/mnt/ssd1T/tuanlv/02.KeyValueUnderstanding/') # TODO: ??????? + +from lightning_modules.classifier_module import parse_initial_words, parse_subsequent_words, parse_relations +from model import get_model +from utils import load_model_weight + + +class KVUPredictor: + def __init__(self, configs, class_names, dummy_idx, mode=0): + cfg_path = configs['cfg'] + ckpt_path = configs['ckpt'] + + self.class_names = class_names + self.dummy_idx = dummy_idx + self.mode = mode + + print('[INFO] Loading Key-Value Understanding model ...') + self.net, cfg, self.backbone_type = self._load_model(cfg_path, ckpt_path) + print("[INFO] Loaded model") + + if mode == 3: + self.max_window_count = cfg.train.max_window_count + self.window_size = cfg.train.window_size + self.slice_interval = 0 + self.dummy_idx = dummy_idx * self.max_window_count + else: + self.slice_interval = cfg.train.slice_interval + self.window_size = cfg.train.max_num_words + + + self.device = 'cuda' + + def _load_model(self, cfg_path, ckpt_path): + cfg = OmegaConf.load(cfg_path) + cfg.stage = self.mode + backbone_type = cfg.model.backbone + + print('[INFO] Checkpoint:', ckpt_path) + net = get_model(cfg) + load_model_weight(net, ckpt_path) + net.to('cuda') + net.eval() + return net, cfg, backbone_type + + def predict(self, input_sample): + if self.mode == 0: + if len(input_sample['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = self.combined_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 1: + if len(input_sample['documents']['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = self.cat_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 2: + if len(input_sample['windows'][0]['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = [], [], [], [] + for window in input_sample['windows']: + _bbox, _lwords, _pr_class_words, _pr_relations = self.combined_predict(window) + bbox.append(_bbox) + lwords.append(_lwords) + pr_class_words.append(_pr_class_words) + pr_relations.append(_pr_relations) + return bbox, lwords, pr_class_words, pr_relations + + elif self.mode == 3: + if len(input_sample["documents"]['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = self.doc_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + else: + raise ValueError( + f"Not supported mode: {self.mode}" + ) + + def doc_predict(self, input_sample): + lwords = input_sample['documents']['words'] + for idx, window in enumerate(input_sample['windows']): + input_sample['windows'][idx] = {k: v.unsqueeze(0).to(self.device) for k, v in window.items() if k not in ('words', 'n_empty_windows')} + + # input_sample['documents'] = {k: v.unsqueeze(0).to(self.device) for k, v in input_sample['documents'].items() if k not in ('words', 'n_empty_windows')} + with torch.no_grad(): + head_outputs, _ = self.net(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + input_sample = input_sample['documents'] + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['are_box_first_tokens'].squeeze(0) + attention_mask = input_sample['attention_mask'].squeeze(0) + bbox = input_sample['bbox'].squeeze(0) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, self.dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, self.dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, self.dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return bbox, lwords, pr_class_words, pr_relations + + + def combined_predict(self, input_sample): + lwords = input_sample['words'] + input_sample = {k: v.unsqueeze(0) for k, v in input_sample.items() if k not in ('words', 'img_path')} + + input_sample = {k: v.to(self.device) for k, v in input_sample.items()} + + + with torch.no_grad(): + head_outputs, _ = self.net(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + input_sample = {k: v.detach().cpu() for k, v in input_sample.items()} + + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['are_box_first_tokens'].squeeze(0) + attention_mask = input_sample['attention_mask_layoutxlm'].squeeze(0) + bbox = input_sample['bbox'].squeeze(0) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, self.dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, self.dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, self.dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return bbox, lwords, pr_class_words, pr_relations + + def cat_predict(self, input_sample): + lwords = input_sample['documents']['words'] + + inputs = [] + for window in input_sample['windows']: + inputs.append({k: v.unsqueeze(0).cuda() for k, v in window.items() if k not in ('words', 'img_path')}) + input_sample['windows'] = inputs + + with torch.no_grad(): + head_outputs, _ = self.net(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items() if k not in ('embedding_tokens')} + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['documents']['are_box_first_tokens'] + attention_mask = input_sample['documents']['attention_mask_layoutxlm'] + bbox = input_sample['documents']['bbox'] + + dummy_idx = input_sample['documents']['bbox'].shape[0] + + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return bbox, lwords, pr_class_words, pr_relations + + + def get_ground_truth_label(self, ground_truth): + # ground_truth = self.preprocessor.load_ground_truth(json_file) + gt_itc_label = ground_truth['itc_labels'].squeeze(0) # [1, 512] => [512] + gt_stc_label = ground_truth['stc_labels'].squeeze(0) # [1, 512] => [512] + gt_el_label = ground_truth['el_labels'].squeeze(0) + + gt_el_label_from_key = ground_truth['el_labels_from_key'].squeeze(0) + lwords = ground_truth["words"] + + box_first_token_mask = ground_truth['are_box_first_tokens'].squeeze(0) + attention_mask = ground_truth['attention_mask'].squeeze(0) + + bbox = ground_truth['bbox'].squeeze(0) + gt_first_words = parse_initial_words( + gt_itc_label, box_first_token_mask, self.class_names + ) + gt_class_words = parse_subsequent_words( + gt_stc_label, attention_mask, gt_first_words, self.dummy_idx + ) + + gt_relations_from_header = parse_relations(gt_el_label, box_first_token_mask, self.dummy_idx) + gt_relations_from_key = parse_relations(gt_el_label_from_key, box_first_token_mask, self.dummy_idx) + gt_relations = gt_relations_from_header | gt_relations_from_key + + return bbox, lwords, gt_class_words, gt_relations \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/preprocess.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/preprocess.py new file mode 100755 index 0000000..89e3167 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/preprocess.py @@ -0,0 +1,601 @@ +import os +from typing import Any +import numpy as np +import pandas as pd +import imagesize +import itertools +from PIL import Image +import argparse + +import torch + +from utils.utils import read_ocr_result_from_txt, read_json, post_process_basic_ocr +from utils.run_ocr import load_ocr_engine, process_img +from lightning_modules.utils import sliding_windows + + +class KVUProcess: + def __init__(self, tokenizer_layoutxlm, feature_extractor, backbone_type, class_names, slice_interval, window_size, run_ocr, max_seq_length=512, mode=0): + self.tokenizer_layoutxlm = tokenizer_layoutxlm + self.feature_extractor = feature_extractor + + self.max_seq_length = max_seq_length + self.backbone_type = backbone_type + self.class_names = class_names + + self.slice_interval = slice_interval + self.window_size = window_size + self.run_ocr = run_ocr + self.mode = mode + + self.pad_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(tokenizer_layoutxlm._pad_token) + self.cls_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(tokenizer_layoutxlm._cls_token) + self.sep_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(tokenizer_layoutxlm._sep_token) + self.unk_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm._unk_token) + + + self.class_idx_dic = dict( + [(class_name, idx) for idx, class_name in enumerate(self.class_names)] + ) + self.ocr_engine = None + if self.run_ocr == 1: + self.ocr_engine = load_ocr_engine() + + def __call__(self, img_path: str, ocr_path: str) -> list: + if (self.run_ocr == 1) or (not os.path.exists(ocr_path)): + process_img(img_path, "tmp.txt", self.ocr_engine, export_img=False) + ocr_path = "tmp.txt" + lbboxes, lwords = read_ocr_result_from_txt(ocr_path) + lwords = post_process_basic_ocr(lwords) + bbox_windows = sliding_windows(lbboxes, self.window_size, self.slice_interval) + word_windows = sliding_windows(lwords, self.window_size, self.slice_interval) + assert len(bbox_windows) == len(word_windows), f"Shape of lbboxes and lwords after sliding window is not the same {len(bbox_windows)} # {len(word_windows)}" + + width, height = imagesize.get(img_path) + images = [Image.open(img_path).convert("RGB")] + image_features = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + + + if self.mode == 0: + output = self.preprocess(lbboxes, lwords, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + elif self.mode == 1: + output = {} + windows = [] + for i in range(len(bbox_windows)): + _words = word_windows[i] + _bboxes = bbox_windows[i] + windows.append( + self.preprocess( + _bboxes, _words, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + ) + + output['windows'] = windows + elif self.mode == 2: + output = {} + windows = [] + output['doduments'] = self.preprocess(lbboxes, lwords, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=2048) + for i in range(len(bbox_windows)): + _words = word_windows[i] + _bboxes = bbox_windows[i] + windows.append( + self.preprocess( + _bboxes, _words, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + ) + + output['windows'] = windows + else: + raise ValueError( + f"Not supported mode: {self.mode }" + ) + return output + + + def preprocess(self, bounding_boxes, words, feature_maps, max_seq_length): + input_ids_layoutxlm = np.ones(max_seq_length, dtype=int) * self.pad_token_id_layoutxlm + + attention_mask_layoutxlm = np.zeros(max_seq_length, dtype=int) + + bbox = np.zeros((max_seq_length, 8), dtype=np.float32) + + are_box_first_tokens = np.zeros(max_seq_length, dtype=np.bool_) + + list_layoutxlm_tokens = [] + + list_bbs = [] + list_words = [] + lwords = [''] * max_seq_length + + box_to_token_indices = [] + cum_token_idx = 0 + + cls_bbs = [0.0] * 8 + len_overlap_tokens = 0 + len_non_overlap_tokens = 0 + len_valid_tokens = 0 + + for word_idx, (bounding_box, word) in enumerate(zip(bounding_boxes, words)): + bb = [[bounding_box[0], bounding_box[1]], [bounding_box[2], bounding_box[1]], [bounding_box[2], bounding_box[3]], [bounding_box[0], bounding_box[3]]] + layoutxlm_tokens = self.tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm.tokenize(word)) + + this_box_token_indices = [] + + len_valid_tokens += len(layoutxlm_tokens) + if word_idx < self.slice_interval: + len_non_overlap_tokens += len(layoutxlm_tokens) + + if len(layoutxlm_tokens) == 0: + layoutxlm_tokens.append(self.unk_token_id) + + + if len(list_layoutxlm_tokens) + len(layoutxlm_tokens) > max_seq_length - 2: + break + + + list_layoutxlm_tokens += layoutxlm_tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], feature_maps['width'])) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], feature_maps['height'])) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(layoutxlm_tokens))] + texts = [word for _ in range(len(layoutxlm_tokens))] + + for _ in layoutxlm_tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + list_words.extend(texts) ### + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [feature_maps['width'], feature_maps['height']] * 4 + + # For [CLS] and [SEP] + list_layoutxlm_tokens = ( + [self.cls_token_id_layoutxlm] + + list_layoutxlm_tokens[: max_seq_length - 2] + + [self.sep_token_id_layoutxlm] + ) + + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: max_seq_length - 2] + [sep_bbs] + # list_words = ['CLS'] + list_words[: max_seq_length - 2] + ['SEP'] + list_words = [self.tokenizer_layoutxlm._cls_token] + list_words[: max_seq_length - 2] + [self.tokenizer_layoutxlm._sep_token] + + len_list_layoutxlm_tokens = len(list_layoutxlm_tokens) + input_ids_layoutxlm[:len_list_layoutxlm_tokens] = list_layoutxlm_tokens + attention_mask_layoutxlm[:len_list_layoutxlm_tokens] = 1 + + + bbox[:len_list_layoutxlm_tokens, :] = list_bbs + lwords[:len_list_layoutxlm_tokens] = list_words ### + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / feature_maps['width'] + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / feature_maps['height'] + + if self.backbone_type in ("layoutlm", "layoutxlm", "xlm-roberta"): + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + else: + assert False + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < max_seq_length + ] + are_box_first_tokens[st_indices] = True + + assert len_list_layoutxlm_tokens == len_valid_tokens + 2 + len_overlap_tokens = len_valid_tokens - len_non_overlap_tokens + + ntokens = max_seq_length if max_seq_length == 512 else len_valid_tokens + 2 + + input_ids_layoutxlm = input_ids_layoutxlm[:ntokens] + attention_mask_layoutxlm = attention_mask_layoutxlm[:ntokens] + bbox = bbox[:ntokens] + are_box_first_tokens = are_box_first_tokens[:ntokens] + + + input_ids_layoutxlm = torch.from_numpy(input_ids_layoutxlm) + attention_mask_layoutxlm = torch.from_numpy(attention_mask_layoutxlm) + bbox = torch.from_numpy(bbox) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + + len_valid_tokens = torch.tensor(len_valid_tokens) + len_overlap_tokens = torch.tensor(len_overlap_tokens) + return_dict = { + "img_path": feature_maps['img_path'], + "words": list_words, + "len_overlap_tokens": len_overlap_tokens, + 'len_valid_tokens': len_valid_tokens, + "image": feature_maps['image'], + "input_ids_layoutxlm": input_ids_layoutxlm, + "attention_mask_layoutxlm": attention_mask_layoutxlm, + "are_box_first_tokens": are_box_first_tokens, + "bbox": bbox, + } + return return_dict + + def load_ground_truth(self, json_file): + json_obj = read_json(json_file) + width = json_obj["meta"]["imageSize"]["width"] + height = json_obj["meta"]["imageSize"]["height"] + + input_ids = np.ones(self.max_seq_length, dtype=int) * self.pad_token_id_layoutxlm + bbox = np.zeros((self.max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(self.max_seq_length, dtype=int) + + itc_labels = np.zeros(self.max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(self.max_seq_length, dtype=np.bool_) + + # stc_labels stores the index of the previous token. + # A stored index of max_seq_length (512) indicates that + # this token is the initial token of a word box. + stc_labels = np.ones(self.max_seq_length, dtype=np.int64) * self.max_seq_length + el_labels = np.ones(self.max_seq_length, dtype=int) * self.max_seq_length + el_labels_from_key = np.ones(self.max_seq_length, dtype=int) * self.max_seq_length + + + list_tokens = [] + list_bbs = [] + list_words = [] + box2token_span_map = [] + lwords = [''] * self.max_seq_length + + box_to_token_indices = [] + cum_token_idx = 0 + + cls_bbs = [0.0] * 8 + + for word_idx, word in enumerate(json_obj["words"]): + this_box_token_indices = [] + + tokens = word["layoutxlm_tokens"] + bb = word["boundingBox"] + text = word["text"] + + if len(tokens) == 0: + tokens.append(self.unk_token_id) + + if len(list_tokens) + len(tokens) > self.max_seq_length - 2: + break + + box2token_span_map.append( + [len(list_tokens) + 1, len(list_tokens) + len(tokens) + 1] + ) # including st_idx + list_tokens += tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], width)) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], height)) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(tokens))] + texts = [text for _ in range(len(tokens))] + + for _ in tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + list_words.extend(texts) #### + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [width, height] * 4 + + # For [CLS] and [SEP] + list_tokens = ( + [self.cls_token_id_layoutxlm] + + list_tokens[: self.max_seq_length - 2] + + [self.sep_token_id_layoutxlm] + ) + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: self.max_seq_length - 2] + [sep_bbs] + # list_words = ['CLS'] + list_words[: self.max_seq_length - 2] + ['SEP'] ### + list_words = [self.tokenizer_layoutxlm._cls_token] + list_words[: self.max_seq_length - 2] + [self.tokenizer_layoutxlm._sep_token] + + + len_list_tokens = len(list_tokens) + input_ids[:len_list_tokens] = list_tokens + attention_mask[:len_list_tokens] = 1 + + bbox[:len_list_tokens, :] = list_bbs + lwords[:len_list_tokens] = list_words + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / width + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / height + + if self.backbone_type in ("layoutlm", "layoutxlm"): + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + else: + assert False + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < self.max_seq_length + ] + are_box_first_tokens[st_indices] = True + + # Label + classes_dic = json_obj["parse"]["class"] + for class_name in self.class_names: + if class_name == "others": + continue + if class_name not in classes_dic: + continue + + for word_list in classes_dic[class_name]: + is_first, last_word_idx = True, -1 + for word_idx in word_list: + if word_idx >= len(box_to_token_indices): + break + box2token_list = box_to_token_indices[word_idx] + for converted_word_idx in box2token_list: + if converted_word_idx >= self.max_seq_length: + break # out of idx + + if is_first: + itc_labels[converted_word_idx] = self.class_idx_dic[ + class_name + ] + is_first, last_word_idx = False, converted_word_idx + else: + stc_labels[converted_word_idx] = last_word_idx + last_word_idx = converted_word_idx + + # Label + relations = json_obj["parse"]["relations"] + for relation in relations: + if relation[0] >= len(box2token_span_map) or relation[1] >= len( + box2token_span_map + ): + continue + if ( + box2token_span_map[relation[0]][0] >= self.max_seq_length + or box2token_span_map[relation[1]][0] >= self.max_seq_length + ): + continue + + word_from = box2token_span_map[relation[0]][0] + word_to = box2token_span_map[relation[1]][0] + # el_labels[word_to] = word_from + + if el_labels[word_to] != 512 and el_labels_from_key[word_to] != 512: + continue + + # if self.second_relations == 1: + # if itc_labels[word_from] == 4 and (itc_labels[word_to] in (2, 3)): + # el_labels[word_to] = word_from # pair of (header, key) or (header-value) + # else: + #### 1st relation => ['key, 'value'] + #### 2st relation => ['header', 'key'or'value'] + if itc_labels[word_from] == 2 and itc_labels[word_to] == 3: + el_labels_from_key[word_to] = word_from # pair of (key-value) + if itc_labels[word_from] == 4 and (itc_labels[word_to] in (2, 3)): + el_labels[word_to] = word_from # pair of (header, key) or (header-value) + + + + input_ids = torch.from_numpy(input_ids) + bbox = torch.from_numpy(bbox) + attention_mask = torch.from_numpy(attention_mask) + + itc_labels = torch.from_numpy(itc_labels) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + stc_labels = torch.from_numpy(stc_labels) + el_labels = torch.from_numpy(el_labels) + el_labels_from_key = torch.from_numpy(el_labels_from_key) + + return_dict = { + # "image": feature_maps, + "input_ids": input_ids, + "bbox": bbox, + "words": lwords, + "attention_mask": attention_mask, + "itc_labels": itc_labels, + "are_box_first_tokens": are_box_first_tokens, + "stc_labels": stc_labels, + "el_labels": el_labels, + "el_labels_from_key": el_labels_from_key + } + + return return_dict + + +class DocumentKVUProcess(KVUProcess): + def __init__(self, tokenizer_layoutxlm, feature_extractor, backbone_type, class_names, max_window_count, slice_interval, window_size, run_ocr, max_seq_length=512, mode=0): + super().__init__(tokenizer_layoutxlm, feature_extractor, backbone_type, class_names, slice_interval, window_size, run_ocr, max_seq_length, mode) + self.max_window_count = max_window_count + self.pad_token_id = self.pad_token_id_layoutxlm + self.cls_token_id = self.cls_token_id_layoutxlm + self.sep_token_id = self.sep_token_id_layoutxlm + self.unk_token_id = self.unk_token_id_layoutxlm + self.tokenizer = self.tokenizer_layoutxlm + + def __call__(self, img_path: str, ocr_path: str) -> list: + if (self.run_ocr == 1) and (not os.path.exists(ocr_path)): + process_img(img_path, "tmp.txt", self.ocr_engine, export_img=False) + ocr_path = "tmp.txt" + lbboxes, lwords = read_ocr_result_from_txt(ocr_path) + lwords = post_process_basic_ocr(lwords) + + width, height = imagesize.get(img_path) + images = [Image.open(img_path).convert("RGB")] + image_features = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + output = self.preprocess(lbboxes, lwords, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}) + return output + + def preprocess(self, bounding_boxes, words, feature_maps): + n_words = len(words) + output_dicts = {'windows': [], 'documents': []} + n_empty_windows = 0 + + for i in range(self.max_window_count): + input_ids = np.ones(self.max_seq_length, dtype=int) * self.pad_token_id + bbox = np.zeros((self.max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(self.max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(self.max_seq_length, dtype=np.bool_) + + if n_words == 0: + n_empty_windows += 1 + output_dicts['windows'].append({ + "image": feature_maps['image'], + "input_ids": torch.from_numpy(input_ids), + "bbox": torch.from_numpy(bbox), + "words": [], + "attention_mask": torch.from_numpy(attention_mask), + "are_box_first_tokens": torch.from_numpy(are_box_first_tokens), + }) + continue + + start_word_idx = i * self.window_size + stop_word_idx = min(n_words, (i+1)*self.window_size) + + if start_word_idx >= stop_word_idx: + n_empty_windows += 1 + output_dicts['windows'].append(output_dicts['windows'][-1]) + continue + + list_tokens = [] + list_bbs = [] + list_words = [] + lwords = [''] * self.max_seq_length + + box_to_token_indices = [] + cum_token_idx = 0 + + cls_bbs = [0.0] * 8 + + for _, (bounding_box, word) in enumerate(zip(bounding_boxes[start_word_idx:stop_word_idx], words[start_word_idx:stop_word_idx])): + bb = [[bounding_box[0], bounding_box[1]], [bounding_box[2], bounding_box[1]], [bounding_box[2], bounding_box[3]], [bounding_box[0], bounding_box[3]]] + tokens = self.tokenizer.convert_tokens_to_ids(self.tokenizer.tokenize(word)) + + this_box_token_indices = [] + + if len(tokens) == 0: + tokens.append(self.unk_token_id) + + if len(list_tokens) + len(tokens) > self.max_seq_length - 2: + break + + list_tokens += tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], feature_maps['width'])) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], feature_maps['height'])) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(tokens))] + texts = [word for _ in range(len(tokens))] + + for _ in tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + list_words.extend(texts) ### + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [feature_maps['width'], feature_maps['height']] * 4 + + # For [CLS] and [SEP] + list_tokens = ( + [self.cls_token_id] + + list_tokens[: self.max_seq_length - 2] + + [self.sep_token_id] + ) + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: self.max_seq_length - 2] + [sep_bbs] + if len(list_words) < 510: + list_words.extend(['

' for _ in range(510 - len(list_words))]) + list_words = [self.tokenizer._cls_token] + list_words[: self.max_seq_length - 2] + [self.tokenizer._sep_token] + + len_list_tokens = len(list_tokens) + input_ids[:len_list_tokens] = list_tokens + attention_mask[:len_list_tokens] = 1 + + bbox[:len_list_tokens, :] = list_bbs + lwords[:len_list_tokens] = list_words ### + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / feature_maps['width'] + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / feature_maps['height'] + + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < self.max_seq_length + ] + are_box_first_tokens[st_indices] = True + + input_ids = torch.from_numpy(input_ids) + bbox = torch.from_numpy(bbox) + attention_mask = torch.from_numpy(attention_mask) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + + return_dict = { + "image": feature_maps['image'], + "input_ids": input_ids, + "bbox": bbox, + "words": list_words, + "attention_mask": attention_mask, + "are_box_first_tokens": are_box_first_tokens, + } + output_dicts["windows"].append(return_dict) + + attention_mask = torch.cat([o['attention_mask'] for o in output_dicts["windows"]]) + are_box_first_tokens = torch.cat([o['are_box_first_tokens'] for o in output_dicts["windows"]]) + if n_empty_windows > 0: + attention_mask[self.max_seq_length * (self.max_window_count - n_empty_windows):] = torch.from_numpy(np.zeros(self.max_seq_length * n_empty_windows, dtype=int)) + are_box_first_tokens[self.max_seq_length * (self.max_window_count - n_empty_windows):] = torch.from_numpy(np.zeros(self.max_seq_length * n_empty_windows, dtype=np.bool_)) + bbox = torch.cat([o['bbox'] for o in output_dicts["windows"]]) + words = [] + for o in output_dicts['windows']: + words.extend(o['words']) + + return_dict = { + "attention_mask": attention_mask, + "bbox": bbox, + "are_box_first_tokens": are_box_first_tokens, + "n_empty_windows": n_empty_windows, + "words": words + } + output_dicts['documents'] = return_dict + + return output_dicts + + + \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/requirements.txt b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/requirements.txt new file mode 100755 index 0000000..639eb12 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/requirements.txt @@ -0,0 +1,19 @@ +nptyping==1.4.2 +numpy==1.20.3 +pytorch-lightning==1.5.6 +omegaconf +pillow +six +overrides==4.1.2 +transformers==4.11.3 +seqeval==0.0.12 +imagesize +pandas==2.0.1 +xmltodict +dicttoxml + +tensorboard>=2.2.0 + +# code-style +isort==5.9.3 +black==21.9b0 \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/run.sh b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/run.sh new file mode 100755 index 0000000..9f64108 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/run.sh @@ -0,0 +1 @@ +python anyKeyValue.py --img_dir /home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/visualize/test/ --save_dir /home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/visualize/test/ --exp_dir /home/thucpd/thucpd/cope2n-ai/Kie_Invoice_AP/AnyKey_Value/experiments/key_value_understanding-20230608-171900 --export_img 1 --mode 3 --dir_level 0 \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/tmp.txt b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/tmp.txt new file mode 100755 index 0000000..52cb8ee --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/tmp.txt @@ -0,0 +1,106 @@ +1113 773 1220 825 BEST +1243 759 1378 808 DENKI +1410 752 1487 799 (S) +1430 707 1515 748 TAX +1511 745 1598 790 PTE +1542 700 1725 740 TNVOICE +1618 742 1706 783 LTD +1783 725 1920 773 FUNAN +1943 723 2054 767 MALL +1434 797 1576 843 WORTH +1599 785 1760 831 BRIDGE +1784 778 1846 822 RD +1277 846 1632 897 #02-16/#03-1 +1655 832 1795 877 FUNAN +1817 822 1931 869 MALL +1272 897 1518 956 S(179105) +1548 890 1655 943 TEL: +1686 877 1911 928 69046183 +1247 1011 1334 1068 GST +1358 1006 1447 1059 REG +1360 1063 1449 1115 RCB +1473 1003 1575 1055 NO.: +1474 1059 1555 1110 NO. +1595 1042 1868 1096 198202199E +1607 985 1944 1040 M2-0053813-7 +1056 1134 1254 1194 Opening +1276 1127 1391 1181 Hrs: +1425 1112 1647 1170 10:00:00 +1672 1102 1735 1161 AN +1755 1101 1819 1157 to +1846 1090 2067 1147 10:00:00 +2090 1080 2156 1141 PH +1061 1308 1228 1366 Staff: +1258 1300 1378 1357 3296 +1710 1283 1880 1337 Trans: +1936 1266 2192 1322 262152554 +1060 1372 1201 1429 Date: +1260 1358 1494 1419 22-03-23 +1540 1344 1664 1409 9:05 +1712 1339 1856 1407 Slip: +1917 1328 2196 1387 2000130286 +1124 1487 1439 1545 SALESPERSON +1465 1477 1601 1537 CODE. +1633 1471 1752 1530 6043 +1777 1462 2004 1519 HUHAHHAD +2032 1451 2177 1509 RAZIH +1070 1558 1187 1617 Item +1211 1554 1276 1615 No +1439 1542 1585 1601 Price +1750 1530 1841 1597 Qty +1951 1517 2120 1579 Amount +1076 1683 1276 1741 ANDROID +1304 1673 1477 1733 TABLET +1080 1746 1280 1804 2105976 +1509 1729 1705 1784 SAMSUNG +1734 1719 1931 1776 SH-P613 +1964 1709 2101 1768 128GB +1082 1809 1285 1869 SM-P613 +1316 1802 1454 1860 12838 +1429 1859 1600 1919 518.00 +1481 1794 1596 1855 WIFI +1622 1790 1656 1850 G +1797 1845 1824 1904 1 +1993 1832 2165 1892 518.00 +1088 1935 1347 1995 PROMOTION +1091 2000 1294 2062 2105664 +1520 1983 1717 2039 SAMSUNG +1743 1963 2106 2030 F-Sam-Redeen +1439 2111 1557 2173 0.00 +1806 2095 1832 2156 1 +2053 2081 2174 2144 0.00 +1106 2248 1250 2312 Total +1974 2206 2146 2266 518.00 +1107 2312 1204 2377 UOB +1448 2291 1567 2355 CARD +1978 2268 2147 2327 518.00 +1253 2424 1375 2497 GST% +1456 2411 1655 2475 Net.Amt +1818 2393 1912 2460 GST +2023 2387 2192 2445 Amount +1106 2494 1231 2560 GST8 +1486 2472 1661 2537 479.63 +1770 2458 1916 2523 38.37 +2027 2448 2203 2511 518.00 +1553 2601 1699 2666 THANK +1721 2592 1821 2661 YOU +1436 2678 1616 2749 please +1644 2682 1764 2732 come +1790 2660 1942 2729 again +1191 2862 1391 2931 Those +1426 2870 2018 2945 facebook.com +1565 2809 1690 2884 join +1709 2816 1777 2870 us +1799 2811 1868 2865 on +1838 2946 2024 3003 com .89 +1533 3006 2070 3088 ar.com/askbe +1300 3326 1659 3446 That's +1696 3308 1905 3424 not +1937 3289 2131 3408 all! +1450 3511 1633 3573 SCAN +1392 3589 1489 3645 QR +1509 3577 1698 3635 CODE +1321 3656 1370 3714 & +1517 3638 1768 3699 updates +1643 3882 1769 3932 Scan +1789 3868 1859 3926 Me \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/__init__.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/__init__.py new file mode 100755 index 0000000..066cfe3 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/__init__.py @@ -0,0 +1,127 @@ +import os +import torch +from omegaconf import OmegaConf +from omegaconf.dictconfig import DictConfig +from pytorch_lightning.callbacks import ModelCheckpoint +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger +from pytorch_lightning.plugins import DDPPlugin +from utils.ema_callbacks import EMA + + +def _update_config(cfg): + cfg.save_weight_dir = os.path.join(cfg.workspace, "checkpoints") + cfg.tensorboard_dir = os.path.join(cfg.workspace, "tensorboard_logs") + + # set per-gpu batch size + num_devices = torch.cuda.device_count() + print('No. devices:', num_devices) + for mode in ["train", "val"]: + new_batch_size = cfg[mode].batch_size // num_devices + cfg[mode].batch_size = new_batch_size + +def _get_config_from_cli(): + cfg_cli = OmegaConf.from_cli() + cli_keys = list(cfg_cli.keys()) + for cli_key in cli_keys: + if "--" in cli_key: + cfg_cli[cli_key.replace("--", "")] = cfg_cli[cli_key] + del cfg_cli[cli_key] + + return cfg_cli + +def get_callbacks(cfg): + callback_list = [] + checkpoint_callback = ModelCheckpoint(dirpath=cfg.save_weight_dir, + filename='best_model', + save_last=True, + save_top_k=1, + save_weights_only=True, + verbose=True, + monitor='val_f1', mode='max') + checkpoint_callback.FILE_EXTENSION = ".pth" + checkpoint_callback.CHECKPOINT_NAME_LAST = "last_model" + callback_list.append(checkpoint_callback) + if cfg.callbacks.ema.decay != -1: + ema_callback = EMA(decay=0.9999) + callback_list.append(ema_callback) + return callback_list if len(callback_list) > 1 else checkpoint_callback + +def get_plugins(cfg): + plugins = [] + if cfg.train.strategy.type == "ddp": + plugins.append(DDPPlugin()) + + return plugins + +def get_loggers(cfg): + loggers = [] + + loggers.append( + TensorBoardLogger( + cfg.tensorboard_dir, name="", version="", default_hp_metric=False + ) + ) + + return loggers + +def cfg_to_hparams(cfg, hparam_dict, parent_str=""): + for key, val in cfg.items(): + if isinstance(val, DictConfig): + hparam_dict = cfg_to_hparams(val, hparam_dict, parent_str + key + "__") + else: + hparam_dict[parent_str + key] = str(val) + return hparam_dict + +def get_specific_pl_logger(pl_loggers, logger_type): + for pl_logger in pl_loggers: + if isinstance(pl_logger, logger_type): + return pl_logger + return None + +def get_class_names(dataset_root_path): + class_names_file = os.path.join(dataset_root_path[0], "class_names.txt") + class_names = ( + open(class_names_file, "r", encoding="utf-8").read().strip().split("\n") + ) + return class_names + +def create_exp_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + else: + print("DIR already existed.") + print('Experiment dir : {}'.format(save_dir)) + +def create_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + else: + print("DIR already existed.") + print('Save dir : {}'.format(save_dir)) + +def load_checkpoint(ckpt_path, model, key_include): + assert os.path.exists(ckpt_path) == True, f"Ckpt path at {ckpt_path} not exist!" + state_dict = torch.load(ckpt_path, 'cpu')['state_dict'] + for key in list(state_dict.keys()): + if f'.{key_include}.' not in key: + del state_dict[key] + else: + state_dict[key[4:].replace(key_include + '.', "")] = state_dict[key] # remove net.something. + del state_dict[key] + model.load_state_dict(state_dict, strict=True) + print(f"Load checkpoint at {ckpt_path}") + return model + +def load_model_weight(net, pretrained_model_file): + pretrained_model_state_dict = torch.load(pretrained_model_file, map_location="cpu")[ + "state_dict" + ] + new_state_dict = {} + for k, v in pretrained_model_state_dict.items(): + new_k = k + if new_k.startswith("net."): + new_k = new_k[len("net.") :] + new_state_dict[new_k] = v + net.load_state_dict(new_state_dict) + + diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/ema_callbacks.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/ema_callbacks.py new file mode 100755 index 0000000..5f57cf8 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/ema_callbacks.py @@ -0,0 +1,346 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import contextlib +import copy +import os +import threading +from typing import Any, Dict, Iterable + +import pytorch_lightning as pl +import torch +from pytorch_lightning import Callback +from pytorch_lightning.utilities.exceptions import MisconfigurationException +from pytorch_lightning.utilities.distributed import rank_zero_info + + +class EMA(Callback): + """ + Implements Exponential Moving Averaging (EMA). + + When training a model, this callback will maintain moving averages of the trained parameters. + When evaluating, we use the moving averages copy of the trained parameters. + When saving, we save an additional set of parameters with the prefix `ema`. + + Args: + decay: The exponential decay used when calculating the moving average. Has to be between 0-1. + validate_original_weights: Validate the original weights, as apposed to the EMA weights. + every_n_steps: Apply EMA every N steps. + cpu_offload: Offload weights to CPU. + """ + + def __init__( + self, decay: float, validate_original_weights: bool = False, every_n_steps: int = 1, cpu_offload: bool = False, + ): + if not (0 <= decay <= 1): + raise MisconfigurationException("EMA decay value must be between 0 and 1") + self.decay = decay + self.validate_original_weights = validate_original_weights + self.every_n_steps = every_n_steps + self.cpu_offload = cpu_offload + + def on_fit_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + device = pl_module.device if not self.cpu_offload else torch.device('cpu') + trainer.optimizers = [ + EMAOptimizer( + optim, + device=device, + decay=self.decay, + every_n_steps=self.every_n_steps, + current_step=trainer.global_step, + ) + for optim in trainer.optimizers + if not isinstance(optim, EMAOptimizer) + ] + + def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def on_validation_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def on_test_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def on_test_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def _should_validate_ema_weights(self, trainer: "pl.Trainer") -> bool: + return not self.validate_original_weights and self._ema_initialized(trainer) + + def _ema_initialized(self, trainer: "pl.Trainer") -> bool: + return any(isinstance(optimizer, EMAOptimizer) for optimizer in trainer.optimizers) + + def swap_model_weights(self, trainer: "pl.Trainer", saving_ema_model: bool = False): + for optimizer in trainer.optimizers: + assert isinstance(optimizer, EMAOptimizer) + optimizer.switch_main_parameter_weights(saving_ema_model) + + @contextlib.contextmanager + def save_ema_model(self, trainer: "pl.Trainer"): + """ + Saves an EMA copy of the model + EMA optimizer states for resume. + """ + self.swap_model_weights(trainer, saving_ema_model=True) + try: + yield + finally: + self.swap_model_weights(trainer, saving_ema_model=False) + + @contextlib.contextmanager + def save_original_optimizer_state(self, trainer: "pl.Trainer"): + for optimizer in trainer.optimizers: + assert isinstance(optimizer, EMAOptimizer) + optimizer.save_original_optimizer_state = True + try: + yield + finally: + for optimizer in trainer.optimizers: + optimizer.save_original_optimizer_state = False + + def on_load_checkpoint( + self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", checkpoint: Dict[str, Any] + ) -> None: + checkpoint_callback = trainer.checkpoint_callback + + # use the connector as NeMo calls the connector directly in the exp_manager when restoring. + connector = trainer._checkpoint_connector + ckpt_path = connector.resume_checkpoint_path + + if ckpt_path and checkpoint_callback is not None and 'NeMo' in type(checkpoint_callback).__name__: + ext = checkpoint_callback.FILE_EXTENSION + if ckpt_path.endswith(f'-EMA{ext}'): + rank_zero_info( + "loading EMA based weights. " + "The callback will treat the loaded EMA weights as the main weights" + " and create a new EMA copy when training." + ) + return + ema_path = ckpt_path.replace(ext, f'-EMA{ext}') + if os.path.exists(ema_path): + ema_state_dict = torch.load(ema_path, map_location=torch.device('cpu')) + + checkpoint['optimizer_states'] = ema_state_dict['optimizer_states'] + del ema_state_dict + rank_zero_info("EMA state has been restored.") + else: + raise MisconfigurationException( + "Unable to find the associated EMA weights when re-loading, " + f"training will start with new EMA weights. Expected them to be at: {ema_path}", + ) + + +@torch.no_grad() +def ema_update(ema_model_tuple, current_model_tuple, decay): + torch._foreach_mul_(ema_model_tuple, decay) + torch._foreach_add_( + ema_model_tuple, current_model_tuple, alpha=(1.0 - decay), + ) + + +def run_ema_update_cpu(ema_model_tuple, current_model_tuple, decay, pre_sync_stream=None): + if pre_sync_stream is not None: + pre_sync_stream.synchronize() + + ema_update(ema_model_tuple, current_model_tuple, decay) + + +class EMAOptimizer(torch.optim.Optimizer): + r""" + EMAOptimizer is a wrapper for torch.optim.Optimizer that computes + Exponential Moving Average of parameters registered in the optimizer. + + EMA parameters are automatically updated after every step of the optimizer + with the following formula: + + ema_weight = decay * ema_weight + (1 - decay) * training_weight + + To access EMA parameters, use ``swap_ema_weights()`` context manager to + perform a temporary in-place swap of regular parameters with EMA + parameters. + + Notes: + - EMAOptimizer is not compatible with APEX AMP O2. + + Args: + optimizer (torch.optim.Optimizer): optimizer to wrap + device (torch.device): device for EMA parameters + decay (float): decay factor + + Returns: + returns an instance of torch.optim.Optimizer that computes EMA of + parameters + + Example: + model = Model().to(device) + opt = torch.optim.Adam(model.parameters()) + + opt = EMAOptimizer(opt, device, 0.9999) + + for epoch in range(epochs): + training_loop(model, opt) + + regular_eval_accuracy = evaluate(model) + + with opt.swap_ema_weights(): + ema_eval_accuracy = evaluate(model) + """ + + def __init__( + self, + optimizer: torch.optim.Optimizer, + device: torch.device, + decay: float = 0.9999, + every_n_steps: int = 1, + current_step: int = 0, + ): + self.optimizer = optimizer + self.decay = decay + self.device = device + self.current_step = current_step + self.every_n_steps = every_n_steps + self.save_original_optimizer_state = False + + self.first_iteration = True + self.rebuild_ema_params = True + self.stream = None + self.thread = None + + self.ema_params = () + self.in_saving_ema_model_context = False + + def all_parameters(self) -> Iterable[torch.Tensor]: + return (param for group in self.param_groups for param in group['params']) + + def step(self, closure=None, **kwargs): + self.join() + + if self.first_iteration: + if any(p.is_cuda for p in self.all_parameters()): + self.stream = torch.cuda.Stream() + + self.first_iteration = False + + if self.rebuild_ema_params: + opt_params = list(self.all_parameters()) + + self.ema_params += tuple( + copy.deepcopy(param.data.detach()).to(self.device) for param in opt_params[len(self.ema_params) :] + ) + self.rebuild_ema_params = False + + loss = self.optimizer.step(closure) + + if self._should_update_at_step(): + self.update() + self.current_step += 1 + return loss + + def _should_update_at_step(self) -> bool: + return self.current_step % self.every_n_steps == 0 + + @torch.no_grad() + def update(self): + if self.stream is not None: + self.stream.wait_stream(torch.cuda.current_stream()) + + with torch.cuda.stream(self.stream): + current_model_state = tuple( + param.data.to(self.device, non_blocking=True) for param in self.all_parameters() + ) + + if self.device.type == 'cuda': + ema_update(self.ema_params, current_model_state, self.decay) + + if self.device.type == 'cpu': + self.thread = threading.Thread( + target=run_ema_update_cpu, args=(self.ema_params, current_model_state, self.decay, self.stream,), + ) + self.thread.start() + + def swap_tensors(self, tensor1, tensor2): + tmp = torch.empty_like(tensor1) + tmp.copy_(tensor1) + tensor1.copy_(tensor2) + tensor2.copy_(tmp) + + def switch_main_parameter_weights(self, saving_ema_model: bool = False): + self.join() + self.in_saving_ema_model_context = saving_ema_model + for param, ema_param in zip(self.all_parameters(), self.ema_params): + self.swap_tensors(param.data, ema_param) + + @contextlib.contextmanager + def swap_ema_weights(self, enabled: bool = True): + r""" + A context manager to in-place swap regular parameters with EMA + parameters. + It swaps back to the original regular parameters on context manager + exit. + + Args: + enabled (bool): whether the swap should be performed + """ + + if enabled: + self.switch_main_parameter_weights() + try: + yield + finally: + if enabled: + self.switch_main_parameter_weights() + + def __getattr__(self, name): + return getattr(self.optimizer, name) + + def join(self): + if self.stream is not None: + self.stream.synchronize() + + if self.thread is not None: + self.thread.join() + + def state_dict(self): + self.join() + + if self.save_original_optimizer_state: + return self.optimizer.state_dict() + + # if we are in the context of saving an EMA model, the EMA weights are in the modules' actual weights + ema_params = self.ema_params if not self.in_saving_ema_model_context else list(self.all_parameters()) + state_dict = { + 'opt': self.optimizer.state_dict(), + 'ema': ema_params, + 'current_step': self.current_step, + 'decay': self.decay, + 'every_n_steps': self.every_n_steps, + } + return state_dict + + def load_state_dict(self, state_dict): + self.join() + + self.optimizer.load_state_dict(state_dict['opt']) + self.ema_params = tuple(param.to(self.device) for param in copy.deepcopy(state_dict['ema'])) + self.current_step = state_dict['current_step'] + self.decay = state_dict['decay'] + self.every_n_steps = state_dict['every_n_steps'] + self.rebuild_ema_params = False + + def add_param_group(self, param_group): + self.optimizer.add_param_group(param_group) + self.rebuild_ema_params = True \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/functions.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/functions.py new file mode 100755 index 0000000..f998f1a --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/functions.py @@ -0,0 +1,459 @@ +import os +import cv2 +import json +import torch +import glob +import re +import numpy as np +from tqdm import tqdm +from pdf2image import convert_from_path +from dicttoxml import dicttoxml +from word_preprocess import vat_standardizer, get_string, ap_standardizer +from kvu_dictionary import vat_dictionary, ap_dictionary + + + +def create_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + else: + print("DIR already existed.") + print('Save dir : {}'.format(save_dir)) + +def pdf2image(pdf_dir, save_dir): + pdf_files = glob.glob(f'{pdf_dir}/*.pdf') + print('No. pdf files:', len(pdf_files)) + + for file in tqdm(pdf_files): + pages = convert_from_path(file, 500) + for i, page in enumerate(pages): + page.save(os.path.join(save_dir, os.path.basename(file).replace('.pdf', f'_{i}.jpg')), 'JPEG') + print('Done!!!') + +def xyxy2xywh(bbox): + return [ + float(bbox[0]), + float(bbox[1]), + float(bbox[2]) - float(bbox[0]), + float(bbox[3]) - float(bbox[1]), + ] + +def write_to_json(file_path, content): + with open(file_path, mode='w', encoding='utf8') as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, 'r') as f: + return json.load(f) + +def read_xml(file_path): + with open(file_path, 'r') as xml_file: + return xml_file.read() + +def write_to_xml(file_path, content): + with open(file_path, mode="w", encoding='utf8') as f: + f.write(content) + +def write_to_xml_from_dict(file_path, content): + xml = dicttoxml(content) + xml = content + xml_decode = xml.decode() + + with open(file_path, mode="w") as f: + f.write(xml_decode) + + +def load_ocr_result(ocr_path): + with open(ocr_path, 'r') as f: + lines = f.read().splitlines() + + preds = [] + for line in lines: + preds.append(line.split('\t')) + return preds + +def post_process_basic_ocr(lwords: list) -> list: + pp_lwords = [] + for word in lwords: + pp_lwords.append(word.replace("✪", " ")) + return pp_lwords + +def read_ocr_result_from_txt(file_path: str): + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + + boxes, words = [], [] + for line in lines: + if line == "": + continue + word_info = line.split("\t") + if len(word_info) == 6: + x1, y1, x2, y2, text, _ = word_info + elif len(word_info) == 5: + x1, y1, x2, y2, text = word_info + + x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int(float(x2)), int(float(y2)) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + +def get_colormap(): + return { + 'others': (0, 0, 255), # others: red + 'title': (0, 255, 255), # title: yellow + 'key': (255, 0, 0), # key: blue + 'value': (0, 255, 0), # value: green + 'header': (233, 197, 15), # header + 'group': (0, 128, 128), # group + 'relation': (0, 0, 255)# (128, 128, 128), # relation + } + +def visualize(image, bbox, pr_class_words, pr_relations, color_map, labels=['others', 'title', 'key', 'value', 'header'], thickness=1): + exif = image._getexif() + orientation = None + if exif is not None: + orientation = exif.get(0x0112) + + # Convert the PIL image to OpenCV format + image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + + # Rotate the image in OpenCV if necessary + if orientation == 3: + image = cv2.rotate(image, cv2.ROTATE_180) + elif orientation == 6: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + elif orientation == 8: + image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) + else: + image = np.asarray(image) + + if len(image.shape) == 2: + image = np.repeat(image[:, :, np.newaxis], 3, axis=2) + assert len(image.shape) == 3 + + if orientation is not None and orientation == 6: + width, height, _ = image.shape + else: + height, width, _ = image.shape + if len(pr_class_words) > 0: + id2label = {k: labels[k] for k in range(len(labels))} + for lb, groups in enumerate(pr_class_words): + if lb == 0: + continue + for group_id, group in enumerate(groups): + for i, word_id in enumerate(group): + x0, y0, x1, y1 = int(bbox[word_id][0]*width/1000), int(bbox[word_id][1]*height/1000), int(bbox[word_id][2]*width/1000), int(bbox[word_id][3]*height/1000) + cv2.rectangle(image, (x0, y0), (x1, y1), color=color_map[id2label[lb]], thickness=thickness) + + if i == 0: + x_center0, y_center0 = int((x0+x1)/2), int((y0+y1)/2) + else: + x_center1, y_center1 = int((x0+x1)/2), int((y0+y1)/2) + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['group'], thickness=thickness) + x_center0, y_center0 = x_center1, y_center1 + + if len(pr_relations) > 0: + for pair in pr_relations: + xyxy0 = int(bbox[pair[0]][0]*width/1000), int(bbox[pair[0]][1]*height/1000), int(bbox[pair[0]][2]*width/1000), int(bbox[pair[0]][3]*height/1000) + xyxy1 = int(bbox[pair[1]][0]*width/1000), int(bbox[pair[1]][1]*height/1000), int(bbox[pair[1]][2]*width/1000), int(bbox[pair[1]][3]*height/1000) + + x_center0, y_center0 = int((xyxy0[0] + xyxy0[2])/2), int((xyxy0[1] + xyxy0[3])/2) + x_center1, y_center1 = int((xyxy1[0] + xyxy1[2])/2), int((xyxy1[1] + xyxy1[3])/2) + + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['relation'], thickness=thickness) + + return image + + +def get_pairs(json: list, rel_from: str, rel_to: str) -> dict: + outputs = {} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] in (rel_from, rel_to): + is_rel[element['class']]['status'] = 1 + is_rel[element['class']]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + outputs[is_rel[rel_to]['value']['group_id']] = [is_rel[rel_from]['value']['group_id'], is_rel[rel_to]['value']['group_id']] + return outputs + +def get_table_relations(json: list, header_key_pairs: dict, rel_from="key", rel_to="value") -> dict: + list_keys = list(header_key_pairs.keys()) + relations = {k: [] for k in list_keys} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] == rel_from and element['group_id'] in list_keys: + is_rel[rel_from]['status'] = 1 + is_rel[rel_from]['value'] = element + if element['class'] == rel_to: + is_rel[rel_to]['status'] = 1 + is_rel[rel_to]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + relations[is_rel[rel_from]['value']['group_id']].append(is_rel[rel_to]['value']['group_id']) + return relations + +def get_key2values_relations(key_value_pairs: dict): + triple_linkings = {} + for value_group_id, key_value_pair in key_value_pairs.items(): + key_group_id = key_value_pair[0] + if key_group_id not in list(triple_linkings.keys()): + triple_linkings[key_group_id] = [] + triple_linkings[key_group_id].append(value_group_id) + return triple_linkings + + +def merged_token_to_wordgroup(class_words: list, lwords, labels) -> dict: + word_groups = {} + id2class = {i: labels[i] for i in range(len(labels))} + for class_id, lwgroups_in_class in enumerate(class_words): + for ltokens_in_wgroup in lwgroups_in_class: + group_id = ltokens_in_wgroup[0] + ltokens_to_ltexts = [lwords[token] for token in ltokens_in_wgroup] + text_string = get_string(ltokens_to_ltexts) + word_groups[group_id] = { + 'group_id': group_id, + 'text': text_string, + 'class': id2class[class_id], + 'tokens': ltokens_in_wgroup + } + return word_groups + +def verify_linking_id(word_groups: dict, linking_id: int) -> int: + if linking_id not in list(word_groups): + for wg_id, _word_group in word_groups.items(): + if linking_id in _word_group['tokens']: + return wg_id + return linking_id + +def matched_wordgroup_relations(word_groups:dict, lrelations: list) -> list: + outputs = [] + for pair in lrelations: + wg_from = verify_linking_id(word_groups, pair[0]) + wg_to = verify_linking_id(word_groups, pair[1]) + try: + outputs.append([word_groups[wg_from], word_groups[wg_to]]) + except: + print('Not valid pair:', wg_from, wg_to) + return outputs + + +def export_kvu_outputs(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + word_groups = merged_token_to_wordgroup(class_words, lwords, labels) + linking_pairs = matched_wordgroup_relations(word_groups, lrelations) + + header_key = get_pairs(linking_pairs, rel_from='header', rel_to='key') # => {key_group_id: [header_group_id, key_group_id]} + header_value = get_pairs(linking_pairs, rel_from='header', rel_to='value') # => {value_group_id: [header_group_id, value_group_id]} + key_value = get_pairs(linking_pairs, rel_from='key', rel_to='value') # => {value_group_id: [key_group_id, value_group_id]} + + # table_relations = get_table_relations(linking_pairs, header_key) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + key2values_relations = get_key2values_relations(key_value) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + + triplet_pairs = [] + single_pairs = [] + table = [] + # print('key2values_relations', key2values_relations) + for key_group_id, list_value_group_ids in key2values_relations.items(): + if len(list_value_group_ids) == 0: continue + elif len(list_value_group_ids) == 1: + value_group_id = list_value_group_ids[0] + single_pairs.append({word_groups[key_group_id]['text']: { + 'text': word_groups[value_group_id]['text'], + 'id': value_group_id, + 'class': "value" + }}) + else: + item = [] + for value_group_id in list_value_group_ids: + if value_group_id not in header_value.keys(): + header_name_for_value = "non-header" + else: + header_group_id = header_value[value_group_id][0] + header_name_for_value = word_groups[header_group_id]['text'] + item.append({ + 'text': word_groups[value_group_id]['text'], + 'header': header_name_for_value, + 'id': value_group_id, + 'class': 'value' + }) + if key_group_id not in list(header_key.keys()): + triplet_pairs.append({ + word_groups[key_group_id]['text']: item + }) + else: + header_group_id = header_key[key_group_id][0] + header_name_for_key = word_groups[header_group_id]['text'] + item.append({ + 'text': word_groups[key_group_id]['text'], + 'header': header_name_for_key, + 'id': key_group_id, + 'class': 'key' + }) + table.append({key_group_id: item}) + + if len(table) > 0: + table = sorted(table, key=lambda x: list(x.keys())[0]) + table = [v for item in table for k, v in item.items()] + + outputs = {} + outputs['single'] = sorted(single_pairs, key=lambda x: int(float(list(x.values())[0]['id']))) + outputs['triplet'] = triplet_pairs + outputs['table'] = table + + file_path = os.path.join(os.path.dirname(file_path), 'kvu_results', os.path.basename(file_path)) + write_to_json(file_path, outputs) + return outputs + + +def export_kvu_for_VAT_invoice(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + vat_outputs = {} + outputs = export_kvu_outputs(file_path, lwords, class_words, lrelations, labels) + + # List of items in table + table = [] + for single_item in outputs['table']: + item = {k: [] for k in list(vat_dictionary(header=True).keys())} + for cell in single_item: + header_name, score, proceessed_text = vat_standardizer(cell['header'], threshold=0.75, header=True) + if header_name in list(item.keys()): + # item[header_name] = value['text'] + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + table.append(item) + + + # VAT Information + single_pairs = {k: [] for k in list(vat_dictionary(header=False).keys())} + for pair in outputs['single']: + for key_name, value in pair.items(): + key_name, score, proceessed_text = ap_standardizer(key_name, threshold=0.8, header=False) + # print(f"{key} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + # print('='*10, file_path) + # print(vat_info) + # Combine VAT information and table + vat_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if key_name in ("Ngày, tháng, năm lập hóa đơn"): + if len(list_potential_value) == 1: + vat_outputs[key_name] = list_potential_value[0]['content'] + else: + date_time = {'day': 'dd', 'month': 'mm', 'year': 'yyyy'} + for value in list_potential_value: + date_time[value['processed_key_name']] = re.sub("[^0-9]", "", value['content']) + vat_outputs[key_name] = f"{date_time['day']}/{date_time['month']}/{date_time['year']}" + else: + if len(list_potential_value) == 0: continue + if key_name in ("Mã số thuế người bán"): + selected_value = min(list_potential_value, key=lambda x: x['token_id']) # Get first tax code + vat_outputs[key_name] = selected_value['content'].replace(' ', '') + else: + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + vat_outputs[key_name] = selected_value['content'] + + vat_outputs['table'] = table + + write_to_json(file_path, vat_outputs) + + +def export_kvu_for_SDSAP(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + outputs = export_kvu_outputs(file_path, lwords, class_words, lrelations, labels) + # List of items in table + table = [] + for single_item in outputs['table']: + item = {k: [] for k in list(ap_dictionary(header=True).keys())} + for cell in single_item: + header_name, score, proceessed_text = ap_standardizer(cell['header'], threshold=0.8, header=True) + # print(f"{key} ==> {proceessed_text} ==> {header_name} : {score} - {value['text']}") + if header_name in list(item.keys()): + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + table.append(item) + + triplet_pairs = [] + for single_item in outputs['triplet']: + item = {k: [] for k in list(ap_dictionary(header=True).keys())} + is_item_valid = 0 + for key_name, list_value in single_item.items(): + for value in list_value: + if value['header'] == "non-header": + continue + header_name, score, proceessed_text = ap_standardizer(value['header'], threshold=0.8, header=True) + if header_name in list(item.keys()): + is_item_valid = 1 + item[header_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + if is_item_valid == 1: + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + item['productname'] = key_name + # triplet_pairs.append({key_name: new_item}) + triplet_pairs.append(item) + + single_pairs = {k: [] for k in list(ap_dictionary(header=False).keys())} + for pair in outputs['single']: + for key_name, value in pair.items(): + key_name, score, proceessed_text = ap_standardizer(key_name, threshold=0.8, header=False) + # print(f"{key} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + ap_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if len(list_potential_value) == 0: continue + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + ap_outputs[key_name] = selected_value['content'] + + table = table + triplet_pairs + ap_outputs['table'] = table + # ap_outputs['triplet'] = triplet_pairs + + write_to_json(file_path, ap_outputs) \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/kvu_dictionary.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/kvu_dictionary.py new file mode 100755 index 0000000..1248aa0 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/kvu_dictionary.py @@ -0,0 +1,138 @@ + +DKVU2XML = { + "Ký hiệu mẫu hóa đơn": "form_no", + "Ký hiệu hóa đơn": "serial_no", + "Số hóa đơn": "invoice_no", + "Ngày, tháng, năm lập hóa đơn": "issue_date", + "Tên người bán": "seller_name", + "Mã số thuế người bán": "seller_tax_code", + "Thuế suất": "tax_rate", + "Thuế GTGT đủ điều kiện khấu trừ thuế": "VAT_input_amount", + "Mặt hàng": "item", + "Đơn vị tính": "unit", + "Số lượng": "quantity", + "Đơn giá": "unit_price", + "Doanh số mua chưa có thuế": "amount" +} + + +def ap_dictionary(header: bool): + header_dictionary = { + 'productname': ['description', 'paticulars', 'articledescription', 'descriptionofgood', 'itemdescription', 'product', 'productdescription', 'modelname', 'device', 'items', 'itemno'], + 'modelnumber': ['serialno', 'model', 'code', 'mcode', 'simimeiserial', 'serial', 'productcode', 'product', 'imeiccid', 'articles', 'article', 'articlenumber', 'articleidmaterialcode', 'transaction', 'itemcode'], + 'qty': ['quantity', 'invoicequantity'] + } + + key_dictionary = { + 'purchase_date': ['date', 'purchasedate', 'datetime', 'orderdate', 'orderdatetime', 'invoicedate', 'dateredeemed', 'issuedate', 'billingdocdate'], + 'retailername': ['retailer', 'retailername', 'ownedoperatedby'], + 'serial_number': ['serialnumber', 'serialno'], + 'imei_number': ['imeiesim', 'imeislot1', 'imeislot2', 'imei', 'imei1', 'imei2'] + } + + return header_dictionary if header else key_dictionary + + +def vat_dictionary(header: bool): + header_dictionary = { + 'Mặt hàng': ['tenhanghoa,dichvu', 'danhmuc,dichvu', 'dichvusudung', 'sanpham', 'tenquycachhanghoa','description', 'descriptionofgood', 'itemdescription'], + 'Đơn vị tính': ['dvt', 'donvitinh'], + 'Số lượng': ['soluong', 'sl','qty', 'quantity', 'invoicequantity'], + 'Đơn giá': ['dongia'], + 'Doanh số mua chưa có thuế': ['thanhtien', 'thanhtientruocthuegtgt', 'tienchuathue'], + # 'Số sản phẩm': ['serialno', 'model', 'mcode', 'simimeiserial', 'serial', 'sku', 'sn', 'productcode', 'product', 'particulars', 'imeiccid', 'articles', 'article', 'articleidmaterialcode', 'transaction', 'imei', 'articlenumber'] + } + + key_dictionary = { + 'Ký hiệu mẫu hóa đơn': ['mausoformno', 'mauso'], + 'Ký hiệu hóa đơn': ['kyhieuserialno', 'kyhieuserial', 'kyhieu'], + 'Số hóa đơn': ['soinvoiceno', 'invoiceno'], + 'Ngày, tháng, năm lập hóa đơn': [], + 'Tên người bán': ['donvibanseller', 'donvibanhangsalesunit', 'donvibanhangseller', 'kyboisignedby'], + 'Mã số thuế người bán': ['masothuetaxcode', 'maxsothuetaxcodenumber', 'masothue'], + 'Thuế suất': ['thuesuatgtgttaxrate', 'thuesuatgtgt'], + 'Thuế GTGT đủ điều kiện khấu trừ thuế': ['tienthuegtgtvatamount', 'tienthuegtgt'], + # 'Ghi chú': [], + # 'Ngày': ['ngayday', 'ngay', 'day'], + # 'Tháng': ['thangmonth', 'thang', 'month'], + # 'Năm': ['namyear', 'nam', 'year'] + } + + # exact_dictionary = { + # 'Số hóa đơn': ['sono', 'so'], + # 'Mã số thuế người bán': ['mst'], + # 'Tên người bán': ['kyboi'], + # 'Ngày, tháng, năm lập hóa đơn': ['kyngay', 'kyngaydate'] + # } + + return header_dictionary if header else key_dictionary + +def manulife_dictionary(type: str): + key_dict = { + "Document type": ["documenttype", "loaichungtu"], + "Document name": ["documentname", "tenchungtu"], + "Patient Name": ["patientname", "tenbenhnhan"], + "Date of Birth/Year of birth": [ + "dateofbirth", + "yearofbirth", + "ngaythangnamsinh", + "namsinh", + ], + "Age": ["age", "tuoi"], + "Gender": ["gender", "gioitinh"], + "Social insurance card No.": ["socialinsurancecardno", "sothebhyt"], + "Medical service provider name": ["medicalserviceprovidername", "tencosoyte"], + "Department name": ["departmentname", "tenkhoadieutri"], + "Diagnosis description": ["diagnosisdescription", "motachandoan"], + "Diagnosis code": ["diagnosiscode", "machandoan"], + "Admission date": ["admissiondate", "ngaynhapvien"], + "Discharge date": ["dischargedate", "ngayxuatvien"], + "Treatment method": ["treatmentmethod", "phuongphapdieutri"], + "Treatment date": ["treatmentdate", "ngaydieutri", "ngaykham"], + "Follow up treatment date": ["followuptreatmentdate", "ngaytaikham"], + # "Name of precribed medicine": [], + # "Quantity of prescribed medicine": [], + # "Dosage for each medicine": [] + "Medical expense": ["Medical expense", "chiphiyte"], + "Invoice No.": ["invoiceno", "sohoadon"], + } + + title_dict = { + "Chứng từ y tế": [ + "giayravien", + "giaychungnhanphauthuat", + "cachthucphauthuat", + "phauthuat", + "tomtathosobenhan", + "donthuoc", + "toathuoc", + "donbosung", + "ketquaconghuongtu" + "ketqua", + "phieuchidinh", + "phieudangkykham", + "giayhenkhamlai", + "phieukhambenh", + "phieukhambenhvaovien", + "phieuxetnghiem", + "phieuketquaxetnghiem", + "phieuchidinhxetnghiem", + "ketquasieuam", + "phieuchidinhxetnghiem" + ], + "Chứng từ thanh toán": [ + "hoadon", + "hoadongiatrigiatang", + "hoadongiatrigiatangchuyendoituhoadondientu", + "bangkechiphibaohiem", + "bienlaithutien", + "bangkechiphidieutrinoitru" + ], + } + + if type == "key": + return key_dict + elif type == "title": + return title_dict + else: + raise ValueError(f"[ERROR] Dictionary type of {type} is not supported") \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/run_ocr.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/run_ocr.py new file mode 100755 index 0000000..6190b0a --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/run_ocr.py @@ -0,0 +1,30 @@ +import numpy as np +from pathlib import Path +from typing import Union, Tuple, List +import sys, os +cur_dir = os.path.dirname(__file__) +sys.path.append(os.path.join(os.path.dirname(cur_dir), "ocr-engine")) +from src.ocr import OcrEngine + + +def load_ocr_engine() -> OcrEngine: + print("[INFO] Loading engine...") + engine = OcrEngine() + print("[INFO] Engine loaded") + return engine + +def process_img(img: Union[str, np.ndarray], save_dir_or_path: str, engine: OcrEngine, export_img: bool) -> None: + save_dir_or_path = Path(save_dir_or_path) + if isinstance(img, np.ndarray): + if save_dir_or_path.is_dir(): + raise ValueError("numpy array input require a save path, not a save dir") + page = engine(img) + save_path = str(save_dir_or_path.joinpath(Path(img).stem + ".txt") + ) if save_dir_or_path.is_dir() else str(save_dir_or_path) + page.write_to_file('word', save_path) + if export_img: + page.save_img(save_path.replace(".txt", ".jpg"), is_vnese=True, ) + +def read_img(img: Union[str, np.ndarray], engine: OcrEngine): + page = engine(img) + return ' '.join([f.text for f in page.llines]) \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/utils.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/utils.py new file mode 100755 index 0000000..8bd4062 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/utils/utils.py @@ -0,0 +1,808 @@ +import os +import cv2 +import json +import random +import glob +import re +import numpy as np +from tqdm import tqdm +from pdf2image import convert_from_path +from dicttoxml import dicttoxml +from word_preprocess import ( + vat_standardizer, + ap_standardizer, + get_string_with_word2line, + split_key_value_by_colon, + normalize_kvu_output, + normalize_kvu_output_for_manulife, + manulife_standardizer +) +from utils.kvu_dictionary import ( + vat_dictionary, + ap_dictionary, + manulife_dictionary +) + + + +def create_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + # else: + # print("DIR already existed.") + # print('Save dir : {}'.format(save_dir)) + +def convert_pdf2img(pdf_dir, save_dir): + pdf_files = glob.glob(f'{pdf_dir}/*.pdf') + print('No. pdf files:', len(pdf_files)) + print(pdf_files) + + for file in tqdm(pdf_files): + pdf2img(file, save_dir, n_pages=-1, return_fname=False) + # pages = convert_from_path(file, 500) + # for i, page in enumerate(pages): + # page.save(os.path.join(save_dir, os.path.basename(file).replace('.pdf', f'_{i}.jpg')), 'JPEG') + print('Done!!!') + +def pdf2img(pdf_path, save_dir, n_pages=-1, return_fname=False): + file_names = [] + pages = convert_from_path(pdf_path) + if n_pages != -1: + pages = pages[:n_pages] + for i, page in enumerate(pages): + _save_path = os.path.join(save_dir, os.path.basename(pdf_path).replace('.pdf', f'_{i}.jpg')) + page.save(_save_path, 'JPEG') + file_names.append(_save_path) + if return_fname: + return file_names + +def xyxy2xywh(bbox): + return [ + float(bbox[0]), + float(bbox[1]), + float(bbox[2]) - float(bbox[0]), + float(bbox[3]) - float(bbox[1]), + ] + +def write_to_json(file_path, content): + with open(file_path, mode='w', encoding='utf8') as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, 'r') as f: + return json.load(f) + +def read_xml(file_path): + with open(file_path, 'r') as xml_file: + return xml_file.read() + +def write_to_xml(file_path, content): + with open(file_path, mode="w", encoding='utf8') as f: + f.write(content) + +def write_to_xml_from_dict(file_path, content): + xml = dicttoxml(content) + xml = content + xml_decode = xml.decode() + + with open(file_path, mode="w") as f: + f.write(xml_decode) + + +def load_ocr_result(ocr_path): + with open(ocr_path, 'r') as f: + lines = f.read().splitlines() + + preds = [] + for line in lines: + preds.append(line.split('\t')) + return preds + +def post_process_basic_ocr(lwords: list) -> list: + pp_lwords = [] + for word in lwords: + pp_lwords.append(word.replace("✪", " ")) + return pp_lwords + +def read_ocr_result_from_txt(file_path: str): + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + + boxes, words = [], [] + for line in lines: + if line == "": + continue + word_info = line.split("\t") + if len(word_info) == 6: + x1, y1, x2, y2, text, _ = word_info + elif len(word_info) == 5: + x1, y1, x2, y2, text = word_info + + x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int(float(x2)), int(float(y2)) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + +def get_colormap(): + return { + 'others': (0, 0, 255), # others: red + 'title': (0, 255, 255), # title: yellow + 'key': (255, 0, 0), # key: blue + 'value': (0, 255, 0), # value: green + 'header': (233, 197, 15), # header + 'group': (0, 128, 128), # group + 'relation': (0, 0, 255)# (128, 128, 128), # relation + } + +def convert_image(image): + exif = image._getexif() + orientation = None + if exif is not None: + orientation = exif.get(0x0112) + # Convert the PIL image to OpenCV format + image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + # Rotate the image in OpenCV if necessary + if orientation == 3: + image = cv2.rotate(image, cv2.ROTATE_180) + elif orientation == 6: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + elif orientation == 8: + image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) + else: + image = np.asarray(image) + + if len(image.shape) == 2: + image = np.repeat(image[:, :, np.newaxis], 3, axis=2) + assert len(image.shape) == 3 + + return image, orientation + +def visualize(image, bbox, pr_class_words, pr_relations, color_map, labels=['others', 'title', 'key', 'value', 'header'], thickness=1): + image, orientation = convert_image(image) + + # if orientation is not None and orientation == 6: + # width, height, _ = image.shape + # else: + # height, width, _ = image.shape + + if len(pr_class_words) > 0: + id2label = {k: labels[k] for k in range(len(labels))} + for lb, groups in enumerate(pr_class_words): + if lb == 0: + continue + for group_id, group in enumerate(groups): + for i, word_id in enumerate(group): + # x0, y0, x1, y1 = int(bbox[word_id][0]*width/1000), int(bbox[word_id][1]*height/1000), int(bbox[word_id][2]*width/1000), int(bbox[word_id][3]*height/1000) + # x0, y0, x1, y1 = revert_box(bbox[word_id], width, height) + x0, y0, x1, y1 = bbox[word_id] + cv2.rectangle(image, (x0, y0), (x1, y1), color=color_map[id2label[lb]], thickness=thickness) + + if i == 0: + x_center0, y_center0 = int((x0+x1)/2), int((y0+y1)/2) + else: + x_center1, y_center1 = int((x0+x1)/2), int((y0+y1)/2) + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['group'], thickness=thickness) + x_center0, y_center0 = x_center1, y_center1 + + if len(pr_relations) > 0: + for pair in pr_relations: + # xyxy0 = int(bbox[pair[0]][0]*width/1000), int(bbox[pair[0]][1]*height/1000), int(bbox[pair[0]][2]*width/1000), int(bbox[pair[0]][3]*height/1000) + # xyxy1 = int(bbox[pair[1]][0]*width/1000), int(bbox[pair[1]][1]*height/1000), int(bbox[pair[1]][2]*width/1000), int(bbox[pair[1]][3]*height/1000) + # xyxy0 = revert_box(bbox[pair[0]], width, height) + # xyxy1 = revert_box(bbox[pair[1]], width, height) + + xyxy0 = bbox[pair[0]] + xyxy1 = bbox[pair[1]] + + x_center0, y_center0 = int((xyxy0[0] + xyxy0[2])/2), int((xyxy0[1] + xyxy0[3])/2) + x_center1, y_center1 = int((xyxy1[0] + xyxy1[2])/2), int((xyxy1[1] + xyxy1[3])/2) + + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['relation'], thickness=thickness) + + return image + +def revert_box(box, width, height): + return [ + int((box[0] / 1000) * width), + int((box[1] / 1000) * height), + int((box[2] / 1000) * width), + int((box[3] / 1000) * height) + ] + + +def get_wordgroup_bbox(lbbox: list, lword_ids: list) -> list: + points = [lbbox[i] for i in lword_ids] + x_min, y_min = min(points, key=lambda x: x[0])[0], min(points, key=lambda x: x[1])[1] + x_max, y_max = max(points, key=lambda x: x[2])[2], max(points, key=lambda x: x[3])[3] + return [x_min, y_min, x_max, y_max] + + +def get_pairs(json: list, rel_from: str, rel_to: str) -> dict: + outputs = {} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] in (rel_from, rel_to): + is_rel[element['class']]['status'] = 1 + is_rel[element['class']]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + outputs[is_rel[rel_to]['value']['group_id']] = [is_rel[rel_from]['value']['group_id'], is_rel[rel_to]['value']['group_id']] + return outputs + +def get_table_relations(json: list, header_key_pairs: dict, rel_from="key", rel_to="value") -> dict: + list_keys = list(header_key_pairs.keys()) + relations = {k: [] for k in list_keys} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] == rel_from and element['group_id'] in list_keys: + is_rel[rel_from]['status'] = 1 + is_rel[rel_from]['value'] = element + if element['class'] == rel_to: + is_rel[rel_to]['status'] = 1 + is_rel[rel_to]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + relations[is_rel[rel_from]['value']['group_id']].append(is_rel[rel_to]['value']['group_id']) + return relations + +def get_key2values_relations(key_value_pairs: dict): + triple_linkings = {} + for value_group_id, key_value_pair in key_value_pairs.items(): + key_group_id = key_value_pair[0] + if key_group_id not in list(triple_linkings.keys()): + triple_linkings[key_group_id] = [] + triple_linkings[key_group_id].append(value_group_id) + return triple_linkings + + +def merged_token_to_wordgroup(class_words: list, lwords: list, lbboxes: list, labels: list) -> dict: + word_groups = {} + id2class = {i: labels[i] for i in range(len(labels))} + for class_id, lwgroups_in_class in enumerate(class_words): + for ltokens_in_wgroup in lwgroups_in_class: + group_id = ltokens_in_wgroup[0] + ltokens_to_ltexts = [lwords[token] for token in ltokens_in_wgroup] + ltokens_to_lbboxes = [lbboxes[token] for token in ltokens_in_wgroup] + # text_string = get_string(ltokens_to_ltexts) + # text_string= get_string_by_deduplicate_bbox(ltokens_to_ltexts, ltokens_to_lbboxes) + text_string = get_string_with_word2line(ltokens_to_ltexts, ltokens_to_lbboxes) + group_bbox = get_wordgroup_bbox(lbboxes, ltokens_in_wgroup) + word_groups[group_id] = { + 'group_id': group_id, + 'text': text_string, + 'class': id2class[class_id], + 'tokens': ltokens_in_wgroup, + 'bbox': group_bbox + } + return word_groups + +def verify_linking_id(word_groups: dict, linking_id: int) -> int: + if linking_id not in list(word_groups): + for wg_id, _word_group in word_groups.items(): + if linking_id in _word_group['tokens']: + return wg_id + return linking_id + +def matched_wordgroup_relations(word_groups:dict, lrelations: list) -> list: + outputs = [] + for pair in lrelations: + wg_from = verify_linking_id(word_groups, pair[0]) + wg_to = verify_linking_id(word_groups, pair[1]) + try: + outputs.append([word_groups[wg_from], word_groups[wg_to]]) + except: + print('Not valid pair:', wg_from, wg_to) + return outputs + +def get_single_entity(word_groups: dict, lrelations: list) -> list: + single_entity = {'title': [], 'key': [], 'value': [], 'header': []} + list_linked_ids = [] + for pair in lrelations: + list_linked_ids.extend(pair) + + for word_group_id, word_group in word_groups.items(): + if word_group_id not in list_linked_ids: + single_entity[word_group['class']].append(word_group) + return single_entity + + +def export_kvu_outputs(file_path, lwords, lbboxes, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + word_groups = merged_token_to_wordgroup(class_words, lwords, lbboxes, labels) + linking_pairs = matched_wordgroup_relations(word_groups, lrelations) + + header_key = get_pairs(linking_pairs, rel_from='header', rel_to='key') # => {key_group_id: [header_group_id, key_group_id]} + header_value = get_pairs(linking_pairs, rel_from='header', rel_to='value') # => {value_group_id: [header_group_id, value_group_id]} + key_value = get_pairs(linking_pairs, rel_from='key', rel_to='value') # => {value_group_id: [key_group_id, value_group_id]} + single_entity = get_single_entity(word_groups, lrelations) + # table_relations = get_table_relations(linking_pairs, header_key) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + key2values_relations = get_key2values_relations(key_value) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + triplet_pairs = [] + single_pairs = [] + table = [] + # print('key2values_relations', key2values_relations) + for key_group_id, list_value_group_ids in key2values_relations.items(): + if len(list_value_group_ids) == 0: continue + elif (len(list_value_group_ids) == 1) and (list_value_group_ids[0] not in list(header_value.keys())) and (key_group_id not in list(header_key.keys())): + value_group_id = list_value_group_ids[0] + + single_pairs.append({word_groups[key_group_id]['text']: { + 'text': word_groups[value_group_id]['text'], + 'id': value_group_id, + 'class': "value", + 'bbox': word_groups[value_group_id]['bbox'], + 'key_bbox': word_groups[key_group_id]['bbox'] + }}) + else: + item = [] + for value_group_id in list_value_group_ids: + if value_group_id not in header_value.keys(): + header_group_id = -1 # temp + header_name_for_value = "non-header" + else: + header_group_id = header_value[value_group_id][0] + header_name_for_value = word_groups[header_group_id]['text'] + item.append({ + 'text': word_groups[value_group_id]['text'], + 'header': header_name_for_value, + 'id': value_group_id, + "key_id": key_group_id, + "header_id": header_group_id, + 'class': 'value', + 'bbox': word_groups[value_group_id]['bbox'], + 'key_bbox': word_groups[key_group_id]['bbox'], + 'header_bbox': word_groups[header_group_id]['bbox'] if header_group_id != -1 else [0, 0, 0, 0], + }) + if key_group_id not in list(header_key.keys()): + triplet_pairs.append({ + word_groups[key_group_id]['text']: item + }) + else: + header_group_id = header_key[key_group_id][0] + header_name_for_key = word_groups[header_group_id]['text'] + item.append({ + 'text': word_groups[key_group_id]['text'], + 'header': header_name_for_key, + 'id': key_group_id, + "key_id": key_group_id, + "header_id": header_group_id, + 'class': 'key', + 'bbox': word_groups[value_group_id]['bbox'], + 'key_bbox': word_groups[key_group_id]['bbox'], + 'header_bbox': word_groups[header_group_id]['bbox'], + }) + table.append({key_group_id: item}) + + + single_entity_dict = {} + for class_name, single_items in single_entity.items(): + single_entity_dict[class_name] = [] + for single_item in single_items: + single_entity_dict[class_name].append({ + 'text': single_item['text'], + 'id': single_item['group_id'], + 'class': class_name, + 'bbox': single_item['bbox'] + }) + + + + if len(table) > 0: + table = sorted(table, key=lambda x: list(x.keys())[0]) + table = [v for item in table for k, v in item.items()] + + + outputs = {} + outputs['title'] = single_entity_dict['title'] + outputs['key'] = single_entity_dict['key'] + outputs['value'] = single_entity_dict['value'] + outputs['single'] = sorted(single_pairs, key=lambda x: int(float(list(x.values())[0]['id']))) + outputs['triplet'] = triplet_pairs + outputs['table'] = table + + + create_dir(os.path.join(os.path.dirname(file_path), 'kvu_results')) + file_path = os.path.join(os.path.dirname(file_path), 'kvu_results', os.path.basename(file_path)) + write_to_json(file_path, outputs) + return outputs + +def export_kvu_for_all(file_path, lwords, lbboxes, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']) -> dict: + raw_outputs = export_kvu_outputs( + file_path, lwords, lbboxes, class_words, lrelations, labels + ) + outputs = {} + # Title + outputs["title"] = ( + raw_outputs["title"][0]["text"] if len(raw_outputs["title"]) > 0 else None + ) + + # Pairs of key-value + for pair in raw_outputs["single"]: + for key, values in pair.items(): + # outputs[key] = values["text"] + elements = split_key_value_by_colon(key, values["text"]) + outputs[elements[0]] = elements[1] + + # Only key fields + for key in raw_outputs["key"]: + # outputs[key["text"]] = None + elements = split_key_value_by_colon(key["text"], None) + outputs[elements[0]] = elements[1] + + # Triplet data + for triplet in raw_outputs["triplet"]: + for key, list_value in triplet.items(): + outputs[key] = [value["text"] for value in list_value] + + # Table data + table = [] + header_list = {cell['header']: cell['header_bbox'] for row in raw_outputs['table'] for cell in row} + if header_list: + header_list = dict(sorted(header_list.items(), key=lambda x: int(x[1][0]))) + print("Header_list:", header_list.keys()) + + for row in raw_outputs["table"]: + item = {header: None for header in list(header_list.keys())} + for cell in row: + item[cell["header"]] = cell["text"] + table.append(item) + outputs["tables"] = [{"headers": list(header_list.keys()), "data": table}] + else: + outputs["tables"] = [] + outputs = normalize_kvu_output(outputs) + # write_to_json(file_path, outputs) + return outputs + +def export_kvu_for_manulife( + file_path, + lwords, + lbboxes, + class_words, + lrelations, + labels=["others", "title", "key", "value", "header"], +) -> dict: + raw_outputs = export_kvu_outputs( + file_path, lwords, lbboxes, class_words, lrelations, labels + ) + outputs = {} + # Title + title_list = [] + for title in raw_outputs["title"]: + is_match, title_name, score, proceessed_text = manulife_standardizer(title["text"], threshold=0.6, type_dict="title") + title_list.append({ + 'documment_type': title_name if is_match else None, + 'content': title['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': title['id'] + }) + + if len(title_list) > 0: + selected_element = max(title_list, key=lambda x: x['lcs_score']) + outputs["title"] = selected_element['content'].upper() + outputs["class_doc"] = selected_element['documment_type'] + + outputs["Loại chứng từ"] = selected_element['documment_type'] + outputs["Tên chứng từ"] = selected_element['content'] + else: + outputs["title"] = None + outputs["class_doc"] = None + outputs["Loại chứng từ"] = None + outputs["Tên chứng từ"] = None + + # Pairs of key-value + for pair in raw_outputs["single"]: + for key, values in pair.items(): + # outputs[key] = values["text"] + elements = split_key_value_by_colon(key, values["text"]) + outputs[elements[0]] = elements[1] + + # Only key fields + for key in raw_outputs["key"]: + # outputs[key["text"]] = None + elements = split_key_value_by_colon(key["text"], None) + outputs[elements[0]] = elements[1] + + # Triplet data + for triplet in raw_outputs["triplet"]: + for key, list_value in triplet.items(): + outputs[key] = [value["text"] for value in list_value] + + # Table data + table = [] + header_list = {cell['header']: cell['header_bbox'] for row in raw_outputs['table'] for cell in row} + if header_list: + header_list = dict(sorted(header_list.items(), key=lambda x: int(x[1][0]))) + # print("Header_list:", header_list.keys()) + + for row in raw_outputs["table"]: + item = {header: None for header in list(header_list.keys())} + for cell in row: + item[cell["header"]] = cell["text"] + table.append(item) + outputs["tables"] = [{"headers": list(header_list.keys()), "data": table}] + else: + outputs["tables"] = [] + outputs = normalize_kvu_output_for_manulife(outputs) + # write_to_json(file_path, outputs) + return outputs + + +# For FI-VAT project + +def get_vat_table_information(outputs): + table = [] + for single_item in outputs['table']: + headers = [item['header'] for sublist in outputs['table'] for item in sublist if 'header' in item] + item = {k: [] for k in headers} + print(item) + for cell in single_item: + # header_name, score, proceessed_text = vat_standardizer(cell['header'], threshold=0.75, header=True) + # if header_name in list(item.keys()): + # item[header_name] = value['text'] + item[cell['header']].append({ + 'content': cell['text'], + 'processed_key_name': cell['header'], + 'lcs_score': random.uniform(0.75, 1.0), + 'token_id': cell['id'] + }) + + # for header_name, value in item.items(): + # if len(value) == 0: + # if header_name in ("Số lượng", "Doanh số mua chưa có thuế"): + # item[header_name] = '0' + # else: + # item[header_name] = None + # continue + # item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + # item = post_process_for_item(item) + + # if item["Mặt hàng"] == None: + # continue + table.append(item) + print(table) + return table + +def get_vat_information(outputs): + # VAT Information + single_pairs = {k: [] for k in list(vat_dictionary(header=False).keys())} + for pair in outputs['single']: + for raw_key_name, value in pair.items(): + key_name, score, proceessed_text = vat_standardizer(raw_key_name, threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'], + }) + + for triplet in outputs['triplet']: + for key, value_list in triplet.items(): + if len(value_list) == 1: + key_name, score, proceessed_text = vat_standardizer(key, threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value_list[0]['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value_list[0]['id'] + }) + + for pair in value_list: + key_name, score, proceessed_text = vat_standardizer(pair['header'], threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': pair['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'] + }) + + for table_row in outputs['table']: + for pair in table_row: + key_name, score, proceessed_text = vat_standardizer(pair['header'], threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': pair['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'] + }) + + return single_pairs + + +def post_process_vat_information(single_pairs): + vat_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if key_name in ("Ngày, tháng, năm lập hóa đơn"): + if len(list_potential_value) == 1: + vat_outputs[key_name] = list_potential_value[0]['content'] + else: + date_time = {'day': 'dd', 'month': 'mm', 'year': 'yyyy'} + for value in list_potential_value: + date_time[value['processed_key_name']] = re.sub("[^0-9]", "", value['content']) + vat_outputs[key_name] = f"{date_time['day']}/{date_time['month']}/{date_time['year']}" + else: + if len(list_potential_value) == 0: continue + if key_name in ("Mã số thuế người bán"): + selected_value = min(list_potential_value, key=lambda x: x['token_id']) # Get first tax code + # tax_code_raw = selected_value['content'].replace(' ', '') + tax_code_raw = selected_value['content'] + if len(tax_code_raw.replace(' ', '')) not in (10, 13): # to remove the first number dupicated + tax_code_raw = tax_code_raw.split(' ') + tax_code_raw = sorted(tax_code_raw, key=lambda x: len(x), reverse=True)[0] + vat_outputs[key_name] = tax_code_raw.replace(' ', '') + + else: + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + vat_outputs[key_name] = selected_value['content'] + return vat_outputs + + +def export_kvu_for_VAT_invoice(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + vat_outputs = {} + outputs = export_kvu_outputs(file_path, lwords, class_words, lrelations, labels) + + # List of items in table + table = get_vat_table_information(outputs) + # table = outputs["table"] + + for pair in outputs['single']: + for raw_key_name, value in pair.items(): + vat_outputs[raw_key_name] = value['text'] + + # VAT Information + # single_pairs = get_vat_information(outputs) + # vat_outputs = post_process_vat_information(single_pairs) + + # Combine VAT information and table + vat_outputs['table'] = table + + write_to_json(file_path, vat_outputs) + print(vat_outputs) + return vat_outputs + + +# For SBT project + +def get_ap_table_information(outputs): + table = [] + for single_item in outputs['table']: + item = {k: [] for k in list(ap_dictionary(header=True).keys())} + for cell in single_item: + header_name, score, proceessed_text = ap_standardizer(cell['header'], threshold=0.8, header=True) + # print(f"{key} ==> {proceessed_text} ==> {header_name} : {score} - {value['text']}") + if header_name in list(item.keys()): + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + table.append(item) + return table + +def get_ap_triplet_information(outputs): + triplet_pairs = [] + for single_item in outputs['triplet']: + item = {k: [] for k in list(ap_dictionary(header=True).keys())} + is_item_valid = 0 + for key_name, list_value in single_item.items(): + for value in list_value: + if value['header'] == "non-header": + continue + header_name, score, proceessed_text = ap_standardizer(value['header'], threshold=0.8, header=True) + if header_name in list(item.keys()): + is_item_valid = 1 + item[header_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + if is_item_valid == 1: + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + item['productname'] = key_name + # triplet_pairs.append({key_name: new_item}) + triplet_pairs.append(item) + return triplet_pairs + + +def get_ap_information(outputs): + single_pairs = {k: [] for k in list(ap_dictionary(header=False).keys())} + for pair in outputs['single']: + for raw_key_name, value in pair.items(): + key_name, score, proceessed_text = ap_standardizer(raw_key_name, threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + ## Get single_pair if it in a table (Product Information) + is_product_info = False + for table_row in outputs['table']: + pair = {"key": None, 'value': None} + for cell in table_row: + _, _, proceessed_text = ap_standardizer(cell['header'], threshold=0.8, header=False) + if any(txt in proceessed_text for txt in ['product', 'information', 'productinformation']): + is_product_info = True + if cell['class'] in pair: + pair[cell['class']] = cell + + if all(v is not None for k, v in pair.items()) and is_product_info == True: + key_name, score, proceessed_text = ap_standardizer(pair['key']['text'], threshold=0.8, header=False) + # print(f"{pair['key']['text']} ==> {proceessed_text} ==> {key_name} : {score} - {pair['value']['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': pair['value']['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['value']['id'] + }) + ## end_block + + ap_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if len(list_potential_value) == 0: continue + if key_name == "imei_number": + # print('list_potential_value', list_potential_value) + # ap_outputs[key_name] = [v['content'] for v in list_potential_value if v['content'].replace(' ', '').isdigit() and len(v['content'].replace(' ', '')) > 5] + ap_outputs[key_name] = [] + for v in list_potential_value: + imei = v['content'].replace(' ', '') + if imei.isdigit() and len(imei) > 5: # imei is number and have more 5 digits + ap_outputs[key_name].append(imei) + else: + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + ap_outputs[key_name] = selected_value['content'] + + return ap_outputs + +def export_kvu_for_SDSAP(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + outputs = export_kvu_outputs(file_path, lwords, class_words, lrelations, labels) + # List of items in table + table = get_ap_table_information(outputs) + triplet_pairs = get_ap_triplet_information(outputs) + table = table + triplet_pairs + + ap_outputs = get_ap_information(outputs) + + ap_outputs['table'] = table + # ap_outputs['triplet'] = triplet_pairs + + write_to_json(file_path, ap_outputs) + + return ap_outputs \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word2line.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word2line.py new file mode 100755 index 0000000..d8380ef --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word2line.py @@ -0,0 +1,226 @@ +class Word(): + def __init__(self, text="",image=None, conf_detect=0.0, conf_cls=0.0, bndbox = [-1,-1,-1,-1], kie_label =""): + self.type = "word" + self.text =text + self.image = image + self.conf_detect = conf_detect + self.conf_cls = conf_cls + self.boundingbox = bndbox # [left, top,right,bot] coordinate of top-left and bottom-right point + self.word_id = 0 # id of word + self.word_group_id = 0 # id of word_group which instance belongs to + self.line_id = 0 #id of line which instance belongs to + self.paragraph_id = 0 #id of line which instance belongs to + self.kie_label = kie_label + def invalid_size(self): + return (self.boundingbox[2] - self.boundingbox[0]) * (self.boundingbox[3] - self.boundingbox[1]) > 0 + def is_special_word(self): + left, top, right, bottom = self.boundingbox + width, height = right - left, bottom - top + text = self.text + + if text is None: + return True + + # if len(text) > 7: + # return True + if len(text) >= 7: + no_digits = sum(c.isdigit() for c in text) + return no_digits / len(text) >= 0.3 + + return False + +class Word_group(): + def __init__(self): + self.type = "word_group" + self.list_words = [] # dict of word instances + self.word_group_id = 0 # word group id + self.line_id = 0 #id of line which instance belongs to + self.paragraph_id = 0# id of paragraph which instance belongs to + self.text ="" + self.boundingbox = [-1,-1,-1,-1] + self.kie_label ="" + def add_word(self, word:Word): #add a word instance to the word_group + if word.text != "✪": + for w in self.list_words: + if word.word_id == w.word_id: + print("Word id collision") + return False + word.word_group_id = self.word_group_id # + word.line_id = self.line_id + word.paragraph_id = self.paragraph_id + self.list_words.append(word) + self.text += ' '+ word.text + if self.boundingbox == [-1,-1,-1,-1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3])] + return True + else: + return False + + def update_word_group_id(self, new_word_group_id): + self.word_group_id = new_word_group_id + for i in range(len(self.list_words)): + self.list_words[i].word_group_id = new_word_group_id + + def update_kie_label(self): + list_kie_label = [word.kie_label for word in self.list_words] + dict_kie = dict() + for label in list_kie_label: + if label not in dict_kie: + dict_kie[label]=1 + else: + dict_kie[label]+=1 + total = len(list(dict_kie.values())) + max_value = max(list(dict_kie.values())) + list_keys = list(dict_kie.keys()) + list_values = list(dict_kie.values()) + self.kie_label = list_keys[list_values.index(max_value)] + +class Line(): + def __init__(self): + self.type = "line" + self.list_word_groups = [] # list of Word_group instances in the line + self.line_id = 0 #id of line in the paragraph + self.paragraph_id = 0 # id of paragraph which instance belongs to + self.text = "" + self.boundingbox = [-1,-1,-1,-1] + def add_group(self, word_group:Word_group): # add a word_group instance + if word_group.list_words is not None: + for wg in self.list_word_groups: + if word_group.word_group_id == wg.word_group_id: + print("Word_group id collision") + return False + + self.list_word_groups.append(word_group) + self.text += word_group.text + word_group.paragraph_id = self.paragraph_id + word_group.line_id = self.line_id + + for i in range(len(word_group.list_words)): + word_group.list_words[i].paragraph_id = self.paragraph_id #set paragraph_id for word + word_group.list_words[i].line_id = self.line_id #set line_id for word + return True + return False + def update_line_id(self, new_line_id): + self.line_id = new_line_id + for i in range(len(self.list_word_groups)): + self.list_word_groups[i].line_id = new_line_id + for j in range(len(self.list_word_groups[i].list_words)): + self.list_word_groups[i].list_words[j].line_id = new_line_id + + + def merge_word(self, word): # word can be a Word instance or a Word_group instance + if word.text != "✪": + if self.boundingbox == [-1,-1,-1,-1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3])] + self.list_word_groups.append(word) + self.text += ' ' + word.text + return True + return False + + + def in_same_line(self, input_line, thresh=0.7): + # calculate iou in vertical direction + left1, top1, right1, bottom1 = self.boundingbox + left2, top2, right2, bottom2 = input_line.boundingbox + + sorted_vals = sorted([top1, bottom1, top2, bottom2]) + intersection = sorted_vals[2] - sorted_vals[1] + union = sorted_vals[3]-sorted_vals[0] + min_height = min(bottom1-top1, bottom2-top2) + if min_height==0: + return False + ratio = intersection / min_height + height1, height2 = top1-bottom1, top2-bottom2 + ratio_height = float(max(height1, height2))/float(min(height1, height2)) + # height_diff = (float(top1-bottom1))/(float(top2-bottom2)) + + + if (top1 in range(top2, bottom2) or top2 in range(top1, bottom1)) and ratio >= thresh and (ratio_height<2): + return True + return False + +def check_iomin(word:Word, word_group:Word_group): + min_height = min(word.boundingbox[3]-word.boundingbox[1],word_group.boundingbox[3]-word_group.boundingbox[1]) + intersect = min(word.boundingbox[3],word_group.boundingbox[3]) - max(word.boundingbox[1],word_group.boundingbox[1]) + if intersect/min_height > 0.7: + return True + return False + +def words_to_lines(words, check_special_lines=True): #words is list of Word instance + #sort word by top + words.sort(key = lambda x: (x.boundingbox[1], x.boundingbox[0])) + number_of_word = len(words) + #sort list words to list lines, which have not contained word_group yet + lines = [] + for i, word in enumerate(words): + if word.invalid_size()==0: + continue + new_line = True + for i in range(len(lines)): + if lines[i].in_same_line(word): #check if word is in the same line with lines[i] + lines[i].merge_word(word) + new_line = False + + if new_line ==True: + new_line = Line() + new_line.merge_word(word) + lines.append(new_line) + + # print(len(lines)) + #sort line from top to bottom according top coordinate + lines.sort(key = lambda x: x.boundingbox[1]) + + #construct word_groups in each line + line_id = 0 + word_group_id =0 + word_id = 0 + for i in range(len(lines)): + if len(lines[i].list_word_groups)==0: + continue + #left, top ,right, bottom + line_width = lines[i].boundingbox[2] - lines[i].boundingbox[0] # right - left + # print("line_width",line_width) + lines[i].list_word_groups.sort(key = lambda x: x.boundingbox[0]) #sort word in lines from left to right + + #update text for lines after sorting + lines[i].text ="" + for word in lines[i].list_word_groups: + lines[i].text += " "+word.text + + list_word_groups=[] + inital_word_group = Word_group() + inital_word_group.word_group_id= word_group_id + word_group_id +=1 + lines[i].list_word_groups[0].word_id=word_id + inital_word_group.add_word(lines[i].list_word_groups[0]) + word_id+=1 + list_word_groups.append(inital_word_group) + for word in lines[i].list_word_groups[1:]: #iterate through each word object in list_word_groups (has not been construted to word_group yet) + check_word_group= True + #set id for each word in each line + word.word_id = word_id + word_id+=1 + if (not list_word_groups[-1].text.endswith(':')) and ((word.boundingbox[0]-list_word_groups[-1].boundingbox[2])/line_width <0.05) and check_iomin(word, list_word_groups[-1]): + list_word_groups[-1].add_word(word) + check_word_group=False + if check_word_group ==True: + new_word_group = Word_group() + new_word_group.word_group_id= word_group_id + word_group_id +=1 + new_word_group.add_word(word) + list_word_groups.append(new_word_group) + lines[i].list_word_groups = list_word_groups + # set id for lines from top to bottom + lines[i].update_line_id(line_id) + line_id +=1 + return lines, number_of_word \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word_preprocess.py b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word_preprocess.py new file mode 100755 index 0000000..20e6c4f --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/AnyKey_Value/word_preprocess.py @@ -0,0 +1,388 @@ +import nltk +import re +import string +import copy +from utils.kvu_dictionary import vat_dictionary, ap_dictionary, manulife_dictionary, DKVU2XML +from word2line import Word, words_to_lines +nltk.download('words') +words = set(nltk.corpus.words.words()) + +s1 = u'ÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚÝàáâãèéêìíòóôõùúýĂăĐđĨĩŨũƠơƯưẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ' +s0 = u'AAAAEEEIIOOOOUUYaaaaeeeiioooouuyAaDdIiUuOoUuAaAaAaAaAaAaAaAaAaAaAaAaEeEeEeEeEeEeEeEeIiIiOoOoOoOoOoOoOoOoOoOoOoOoUuUuUuUuUuUuUuYyYyYyYy' + +# def clean_text(text): +# return re.sub(r"[^A-Za-z(),!?\'\`]", " ", text) + + +def get_string(lwords: list): + unique_list = [] + for item in lwords: + if item.isdigit() and len(item) == 1: + unique_list.append(item) + elif item not in unique_list: + unique_list.append(item) + return ' '.join(unique_list) + +def remove_english_words(text): + _word = [w.lower() for w in nltk.wordpunct_tokenize(text) if w.lower() not in words] + return ' '.join(_word) + +def remove_punctuation(text): + return text.translate(str.maketrans(" ", " ", string.punctuation)) + +def remove_accents(input_str, s0, s1): + s = '' + # print input_str.encode('utf-8') + for c in input_str: + if c in s1: + s += s0[s1.index(c)] + else: + s += c + return s + +def remove_spaces(text): + return text.replace(' ', '') + +def preprocessing(text: str): + # text = remove_english_words(text) if table else text + text = remove_punctuation(text) + text = remove_accents(text, s0, s1) + text = remove_spaces(text) + return text.lower() + + +def vat_standardize_outputs(vat_outputs: dict) -> dict: + outputs = {} + for key, value in vat_outputs.items(): + if key != "table": + outputs[DKVU2XML[key]] = value + else: + list_items = [] + for item in value: + list_items.append({ + DKVU2XML[item_key]: item_value for item_key, item_value in item.items() + }) + outputs['table'] = list_items + return outputs + + + +def vat_standardizer(text: str, threshold: float, header: bool): + dictionary = vat_dictionary(header) + processed_text = preprocessing(text) + + for candidates in [('ngayday', 'ngaydate', 'ngay', 'day'), ('thangmonth', 'thang', 'month'), ('namyear', 'nam', 'year')]: + if any([processed_text in txt for txt in candidates]): + processed_text = candidates[-1] + return "Ngày, tháng, năm lập hóa đơn", 5, processed_text + + _dictionary = copy.deepcopy(dictionary) + if not header: + exact_dictionary = { + 'Số hóa đơn': ['sono', 'so'], + 'Mã số thuế người bán': ['mst'], + 'Tên người bán': ['kyboi'], + 'Ngày, tháng, năm lập hóa đơn': ['kyngay', 'kyngaydate'] + } + for k, v in exact_dictionary.items(): + _dictionary[k] = dictionary[k] + exact_dictionary[k] + + for k, v in dictionary.items(): + # if k in ("Ngày, tháng, năm lập hóa đơn"): + # continue + # Prioritize match completely + if k in ('Tên người bán') and processed_text == "kyboi": + return k, 8, processed_text + + if any([processed_text == key for key in _dictionary[k]]): + return k, 10, processed_text + + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + if k in ("Ngày, tháng, năm lập hóa đơn"): + continue + + scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score > threshold else text, score, processed_text + +def ap_standardizer(text: str, threshold: float, header: bool): + dictionary = ap_dictionary(header) + processed_text = preprocessing(text) + + # Prioritize match completely + _dictionary = copy.deepcopy(dictionary) + if not header: + _dictionary['serial_number'] = dictionary['serial_number'] + ['sn'] + _dictionary['imei_number'] = dictionary['imei_number'] + ['imel', 'imed', 'ime'] # text recog error + else: + _dictionary['modelnumber'] = dictionary['modelnumber'] + ['sku', 'sn', 'imei'] + _dictionary['qty'] = dictionary['qty'] + ['qty'] + for k, v in dictionary.items(): + if any([processed_text == key for key in _dictionary[k]]): + return k, 10, processed_text + + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score >= threshold else text, score, processed_text + +def manulife_standardizer(text: str, threshold: float, type_dict: str): + dictionary = manulife_dictionary(type=type_dict) + processed_text = preprocessing(text) + + for key, candidates in dictionary.items(): + + if any([txt == processed_text for txt in candidates]): + return True, key, 5 * (1 + len(processed_text)), processed_text + + if any([txt in processed_text for txt in candidates]): + return True, key, 5, processed_text + + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + if len(v) == 0: + continue + scores[k] = max( + [ + longestCommonSubsequence(processed_text, key) / len(key) + for key in dictionary[k] + ] + ) + key, score = max(scores.items(), key=lambda x: x[1]) + return score > threshold, key if score > threshold else text, score, processed_text + + +def convert_format_number(s: str) -> float: + s = s.replace(' ', '').replace('O', '0').replace('o', '0') + if s.endswith(",00") or s.endswith(".00"): + s = s[:-3] + if all([delimiter in s for delimiter in [',', '.']]): + s = s.replace('.', '').split(',') + remain_value = s[1].split('0')[0] + return int(s[0]) + int(remain_value) * 1 / (10**len(remain_value)) + else: + s = s.replace(',', '').replace('.', '') + return int(s) + + +def post_process_for_item(item: dict) -> dict: + check_keys = ['Số lượng', 'Đơn giá', 'Doanh số mua chưa có thuế'] + mis_key = [] + for key in check_keys: + if item[key] in (None, '0'): + mis_key.append(key) + if len(mis_key) == 1: + try: + if mis_key[0] == check_keys[0] and convert_format_number(item[check_keys[1]]) != 0: + item[mis_key[0]] = round(convert_format_number(item[check_keys[2]]) / convert_format_number(item[check_keys[1]])).__str__() + elif mis_key[0] == check_keys[1] and convert_format_number(item[check_keys[0]]) != 0: + item[mis_key[0]] = (convert_format_number(item[check_keys[2]]) / convert_format_number(item[check_keys[0]])).__str__() + elif mis_key[0] == check_keys[2]: + item[mis_key[0]] = (convert_format_number(item[check_keys[0]]) * convert_format_number(item[check_keys[1]])).__str__() + except Exception as e: + print("Cannot post process this item with error:", e) + return item + + +def longestCommonSubsequence(text1: str, text2: str) -> int: + # https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis + dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)] + for i, c in enumerate(text1): + for j, d in enumerate(text2): + dp[i + 1][j + 1] = 1 + \ + dp[i][j] if c == d else max(dp[i][j + 1], dp[i + 1][j]) + return dp[-1][-1] + + +def longest_common_subsequence_with_idx(X, Y): + """ + This implementation uses dynamic programming to calculate the length of the LCS, and uses a path array to keep track of the characters in the LCS. + The longest_common_subsequence function takes two strings as input, and returns a tuple with three values: + the length of the LCS, + the index of the first character of the LCS in the first string, + and the index of the last character of the LCS in the first string. + """ + m, n = len(X), len(Y) + L = [[0 for i in range(n + 1)] for j in range(m + 1)] + + # Following steps build L[m+1][n+1] in bottom up fashion. Note + # that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] + right_idx = 0 + max_lcs = 0 + for i in range(m + 1): + for j in range(n + 1): + if i == 0 or j == 0: + L[i][j] = 0 + elif X[i - 1] == Y[j - 1]: + L[i][j] = L[i - 1][j - 1] + 1 + if L[i][j] > max_lcs: + max_lcs = L[i][j] + right_idx = i + else: + L[i][j] = max(L[i - 1][j], L[i][j - 1]) + + # Create a string variable to store the lcs string + lcs = L[i][j] + # Start from the right-most-bottom-most corner and + # one by one store characters in lcs[] + i = m + j = n + # right_idx = 0 + while i > 0 and j > 0: + # If current character in X[] and Y are same, then + # current character is part of LCS + if X[i - 1] == Y[j - 1]: + + i -= 1 + j -= 1 + # If not same, then find the larger of two and + # go in the direction of larger value + elif L[i - 1][j] > L[i][j - 1]: + # right_idx = i if not right_idx else right_idx #the first change in L should be the right index of the lcs + i -= 1 + else: + j -= 1 + return lcs, i, max(i + lcs, right_idx) + + +def get_string_by_deduplicate_bbox(lwords: list, lbboxes: list): + unique_list = [] + prev_bbox = [-1, -1, -1, -1] + for word, bbox in zip(lwords, lbboxes): + if bbox != prev_bbox: + unique_list.append(word) + prev_bbox = bbox + return ' '.join(unique_list) + +def get_string_with_word2line(lwords: list, lbboxes: list): + list_words = [] + unique_list = [] + list_sorted_words = [] + + prev_bbox = [-1, -1, -1, -1] + for word, bbox in zip(lwords, lbboxes): + if bbox != prev_bbox: + prev_bbox = bbox + list_words.append(Word(image=None, text=word, conf_cls=-1, bndbox=bbox, conf_detect=-1)) + unique_list.append(word) + llines = words_to_lines(list_words)[0] + + for line in llines: + for _word_group in line.list_word_groups: + for _word in _word_group.list_words: + list_sorted_words.append(_word.text) + + string_from_model = ' '.join(unique_list) + string_after_word2line = ' '.join(list_sorted_words) + + if string_from_model != string_after_word2line: + print("[Warning] Word group from model is different with word2line module") + print("Model: ", ' '.join(unique_list)) + print("Word2line: ", ' '.join(list_sorted_words)) + + return string_after_word2line + +def remove_bullet_points_and_punctuation(text): + # Remove bullet points (e.g., • or -) + text = re.sub(r'^\s*[\•\-\*]\s*', '', text, flags=re.MULTILINE) + text = re.sub("^\d+\s*", "", text) + text = text.strip() + # # Remove end-of-sentence punctuation (e.g., ., !, ?) + # text = re.sub(r'[.!?]', '', text) + if len(text) > 0 and text[0] in (',', '.', ':', ';', '?', '!'): + text = text[1:] + if len(text) > 0 and text[-1] in (',', '.', ':', ';', '?', '!'): + text = text[:-1] + return text.strip() + +def split_key_value_by_colon(key: str, value: str) -> list: + text_string = key + " " + value if value is not None else key + elements = text_string.split(':') + if len(elements) > 1: + return elements[0], text_string[len(elements[0]):] + return key, value + + +# def normalize_kvu_output(raw_outputs: dict) -> dict: +# outputs = {} +# for key, values in raw_outputs.items(): +# if key == "table": +# table = [] +# for row in values: +# item = {} +# for k, v in row.items(): +# k = remove_bullet_points_and_punctuation(k) +# if v is not None and len(v) > 0: +# v = remove_bullet_points_and_punctuation(v) +# item[k] = v +# table.append(item) +# outputs[key] = table +# else: +# key = remove_bullet_points_and_punctuation(key) +# if isinstance(values, list): +# values = [remove_bullet_points_and_punctuation(v) for v in values] +# elif values is not None and len(values) > 0: +# values = remove_bullet_points_and_punctuation(values) +# outputs[key] = values +# return outputs +def normalize_kvu_output(raw_outputs: dict) -> dict: + outputs = {} + for key, values in raw_outputs.items(): + if key == "tables" and len(values) > 0: + table_list = [] + for table in values: + headers, data = [], [] + headers = [remove_bullet_points_and_punctuation(header).upper() for header in table['headers']] + for row in table['data']: + item = [] + for k, v in row.items(): + if v is not None and len(v) > 0: + item.append(remove_bullet_points_and_punctuation(v)) + else: + item.append(v) + data.append(item) + table_list.append({"headers": headers, "data": data}) + outputs[key] = table_list + else: + key = remove_bullet_points_and_punctuation(key) + if isinstance(values, list): + values = [remove_bullet_points_and_punctuation(v) for v in values] + elif values is not None and len(values) > 0: + values = remove_bullet_points_and_punctuation(values) + outputs[key] = values + return outputs + +def normalize_kvu_output_for_manulife(raw_outputs: dict) -> dict: + outputs = {} + for key, values in raw_outputs.items(): + if key == "tables" and len(values) > 0: + table_list = [] + for table in values: + headers, data = [], [] + headers = [ + remove_bullet_points_and_punctuation(header).upper() + for header in table["headers"] + ] + for row in table["data"]: + item = [] + for k, v in row.items(): + if v is not None and len(v) > 0: + item.append(remove_bullet_points_and_punctuation(v)) + else: + item.append(v) + data.append(item) + table_list.append({"headers": headers, "data": data}) + outputs[key] = table_list + else: + if key not in ("title", "tables", "class_doc"): + key = remove_bullet_points_and_punctuation(key).capitalize() + if isinstance(values, list): + values = [remove_bullet_points_and_punctuation(v) for v in values] + elif values is not None and len(values) > 0: + values = remove_bullet_points_and_punctuation(values) + outputs[key] = values + return outputs \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/prediction.py b/cope2n-ai-fi/api/Kie_Invoice_AP/prediction.py new file mode 100755 index 0000000..f6d4ad1 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/prediction.py @@ -0,0 +1,58 @@ +from sdsvkie import Predictor +import cv2 +import numpy as np +import urllib + +model = Predictor( + cfg = "/ai-core/models/Kie_invoice_ap/config.yaml", + device = "cuda:0", + weights = "/ai-core/models/Kie_invoice_ap/ep21" + ) + +def predict(image_url): + """ + module predict function + + Args: + image_url (str): image url + + Returns: + example output: + "data": { + "document_type": "invoice", + "fields": [ + { + "label": "Invoice Number", + "value": "INV-12345", + "box": [0, 0, 0, 0], + "confidence": 0.98 + }, + ... + ] + } + dict: output of model + """ + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + out = model(img) + output = out["end2end_results"] + output_dict = { + "document_type": "invoice", + "fields": [] + } + for key in output.keys(): + field = { + "label": key if key != "id" else "Receipt Number", + "value": output[key]['value'] if output[key]['value'] else "", + "box": output[key]['box'], + "confidence": output[key]['conf'] + } + output_dict['fields'].append(field) + print(output_dict) + return output_dict + +if __name__ == "__main__": + image_url = "/mnt/ssd1T/hoanglv/Projects/KIE/sdsvkie/demos/2022_07_25 farewell lunch.jpg" + output = predict(image_url) + print(output) \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/prediction_fi.py b/cope2n-ai-fi/api/Kie_Invoice_AP/prediction_fi.py new file mode 100755 index 0000000..57981f4 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/prediction_fi.py @@ -0,0 +1,77 @@ +import os, sys +cur_dir = os.path.dirname(__file__) +KIE_PATH = os.path.join(os.path.dirname(cur_dir), "sdsvkie") +TD_PATH = os.path.join(os.path.dirname(cur_dir), "sdsvtd") +TR_PATH = os.path.join(os.path.dirname(cur_dir), "sdsvtr") +sys.path.append(KIE_PATH) +sys.path.append(TD_PATH) +sys.path.append(TR_PATH) + +from sdsvkie import Predictor +from .AnyKey_Value.anyKeyValue import load_engine, Predictor_KVU +import cv2 +import numpy as np +import urllib + +model = Predictor( + cfg = "/models/Kie_invoice_ap/06062023/config.yaml", # TODO: Better be scalable + device = "cuda:0", + weights = "/models/Kie_invoice_ap/06062023/best" # TODO: Better be scalable + ) + +class_names = ['others', 'title', 'key', 'value', 'header'] +save_dir = os.path.join(cur_dir, "AnyKey_Value/visualize/test") + +predictor, processor = load_engine(exp_dir="/models/Kie_invoice_fi/key_value_understanding-20230627-164536", + class_names=class_names, + mode=3) + +def predict_fi(page_numb, image_url): + """ + module predict function + + Args: + image_url (str): image url + + Returns: + example output: + "data": { + "document_type": "invoice", + "fields": [ + { + "label": "Invoice Number", + "value": "INV-12345", + "box": [0, 0, 0, 0], + "confidence": 0.98 + }, + ... + ] + } + dict: output of model + """ + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + # img = cv2.imread(image_url) + + # Phan cua LeHoang + out = model(img) + output = out["end2end_results"] + output_kie = { + field_name: field_item['value'] for field_name, field_item in output.items() + } + # print("Hoangggggggggggggggggggggggggggggggggggggggggggggg") + # print(output_kie) + + + #Phan cua Tuan + kvu_result, _ = Predictor_KVU(image_url, save_dir, predictor, processor) + # print("TuanNnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn") + # print(kvu_result) + # if kvu_result['imei_number'] == None and kvu_result['serial_number'] == None: + return kvu_result, output_kie + +if __name__ == "__main__": + image_url = "/mnt/hdd2T/dxtan/TannedCung/OCR/workspace/Kie_Invoice_AP/tmp_image/{image_url}.jpg" + output = predict_fi(0, image_url) + print(output) \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/prediction_sap.py b/cope2n-ai-fi/api/Kie_Invoice_AP/prediction_sap.py new file mode 100755 index 0000000..6fffaaa --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/prediction_sap.py @@ -0,0 +1,146 @@ +from sdsvkie import Predictor +import sys, os +cur_dir = os.path.dirname(__file__) +# sys.path.append("cope2n-ai-fi/Kie_Invoice_AP") # Better be relative +from .AnyKey_Value.anyKeyValue import load_engine, Predictor_KVU +import cv2 +import numpy as np +import urllib +import random + +# model = Predictor( +# cfg = "/cope2n-ai-fi/models/Kie_invoice_ap/config.yaml", +# device = "cuda:0", +# weights = "/cope2n-ai-fi/models/Kie_invoice_ap/best" +# ) + +class_names = ['others', 'title', 'key', 'value', 'header'] +save_dir = os.path.join(cur_dir, "AnyKey_Value/visualize/test") + +predictor, processor = load_engine(exp_dir=os.path.join(cur_dir, "AnyKey_Value/experiments/key_value_understanding-20231003-171748"), + class_names=class_names, + mode=3) + +def predict(page_numb, image_url): + """ + module predict function + + Args: + image_url (str): image url + + Returns: + example output: + "data": { + "document_type": "invoice", + "fields": [ + { + "label": "Invoice Number", + "value": "INV-12345", + "box": [0, 0, 0, 0], + "confidence": 0.98 + }, + ... + ] + } + dict: output of model + """ + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + # img = cv2.imread(image_url) + + # Phan cua LeHoang + # out = model(img) + # output = out["end2end_results"] + + + #Phan cua Tuan + kvu_result = Predictor_KVU(image_url, save_dir, predictor, processor) + output_dict = { + "document_type": "invoice", + "fields": [] + } + for key in kvu_result.keys(): + field = { + "label": key, + "value": kvu_result[key], + "box": [0, 0, 0, 0], + "confidence": random.uniform(0.9, 1.0), + "page": page_numb + } + output_dict['fields'].append(field) + print(output_dict) + return output_dict + + # if kvu_result['imei_number'] == None and kvu_result['serial_number'] == None: + # output_dict = { + # "document_type": "invoice", + # "fields": [] + # } + # for key in output.keys(): + # field = { + # "label": key if key != "id" else "Receipt Number", + # "value": output[key]['value'] if output[key]['value'] else "", + # "box": output[key]['box'], + # "confidence": output[key]['conf'], + # "page": page_numb + # } + # output_dict['fields'].append(field) + # table = kvu_result['table'] + # field_table = { + # "label": "table", + # "value": table, + # "box": [0, 0, 0, 0], + # "confidence": 0.98, + # "page": page_numb + # } + # output_dict['fields'].append(field_table) + # return output_dict + + # else: + # output_dict = { + # "document_type": "KSU", + # "fields": [] + # } + # # for key in output.keys(): + # # field = { + # # "label": key if key != "id" else "Receipt Number", + # # "value": output[key]['value'] if output[key]['value'] else "", + # # "box": output[key]['box'], + # # "confidence": output[key]['conf'], + # # "page": page_numb + # # } + # # output_dict['fields'].append(field) + + # # Serial Number + # serial_number = kvu_result['serial_number'] + # field_serial = { + # "label" : "serial_number", + # "value": serial_number, + # "box": [0, 0, 0, 0], + # "confidence": 0.98, + # "page": page_numb + # } + # output_dict['fields'].append(field_serial) + + # # IMEI Number + # imei_number = kvu_result['imei_number'] + # if imei_number == None: + # return output_dict + # if imei_number != None: + # for i in range(len(imei_number)): + # field_imei = { + # "label": "imei_number_{}".format(i+1), + # "value": imei_number[i], + # "box": [0, 0, 0, 0], + # "confidence": 0.98, + # "page": page_numb + # } + # output_dict['fields'].append(field_imei) + + # return output_dict + +if __name__ == "__main__": + image_url = "/root/thucpd/20230322144639VUzu_16794962527791962785161104697882.jpg" + output = predict(0, image_url) + print(output) \ No newline at end of file diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/tmp.txt b/cope2n-ai-fi/api/Kie_Invoice_AP/tmp.txt new file mode 100755 index 0000000..4a32426 --- /dev/null +++ b/cope2n-ai-fi/api/Kie_Invoice_AP/tmp.txt @@ -0,0 +1,106 @@ +1113 773 1220 825 BEST +1243 759 1378 808 DENKI +1410 752 1487 799 (S) +1430 707 1515 748 TAX +1511 745 1598 790 PTE +1542 700 1725 740 TNVOICE +1618 742 1706 783 LTD +1783 725 1920 773 FUNAN +1943 723 2054 767 MALL +1434 797 1576 843 WORTH +1599 785 1760 831 BRIDGE +1784 778 1846 822 RD +1277 846 1632 897 #02-16/#03-1 +1655 832 1795 877 FUNAN +1817 822 1931 869 MALL +1272 897 1518 956 S(179105) +1548 890 1655 943 TEL: +1686 877 1911 928 69046183 +1247 1011 1334 1068 GST +1358 1006 1447 1059 REG +1360 1063 1449 1115 RCB +1473 1003 1575 1055 NO.: +1474 1059 1555 1110 NO. +1595 1042 1868 1096 198202199E +1607 985 1944 1040 M2-0053813-7 +1056 1134 1254 1194 Opening +1276 1127 1391 1181 Hrs: +1425 1112 1647 1170 10:00:00 +1672 1102 1735 1161 AN +1755 1101 1819 1157 to +1846 1090 2067 1147 10:00:00 +2090 1080 2156 1141 PH +1061 1308 1228 1366 Staff: +1258 1300 1378 1357 3296 +1710 1283 1880 1337 Trans: +1936 1266 2192 1322 262152554 +1060 1372 1201 1429 Date: +1260 1358 1494 1419 22-03-23 +1540 1344 1664 1409 9:05 +1712 1339 1856 1407 Slip: +1917 1328 2196 1387 2000130286 +1124 1487 1439 1545 SALESPERSON +1465 1477 1601 1537 CODE. +1633 1471 1752 1530 6043 +1777 1462 2004 1519 HUHAHHAD +2032 1451 2177 1509 RAZIH +1070 1558 1187 1617 Item +1211 1554 1276 1615 No +1439 1542 1585 1601 Price +1750 1530 1841 1597 Qty +1951 1517 2120 1579 Amount +1076 1683 1276 1741 ANDROID +1304 1673 1477 1733 TABLET +1080 1746 1280 1804 2105976 +1509 1729 1705 1784 SAMSUNG +1734 1719 1931 1776 SH-P613 +1964 1709 2101 1768 128GB +1082 1809 1285 1869 SM-P613 +1316 1802 1454 1860 12838 +1429 1859 1600 1919 518.00 +1481 1794 1596 1855 WIFI +1622 1790 1656 1850 G +1797 1845 1824 1904 1 +1993 1832 2165 1892 518.00 +1088 1935 1347 1995 PROMOTION +1091 2000 1294 2062 2105664 +1520 1983 1717 2039 SAMSUNG +1743 1963 2106 2030 F-Sam-Redeen +1439 2111 1557 2173 0.00 +1806 2095 1832 2156 1 +2053 2081 2174 2144 0.00 +1106 2248 1250 2312 Total +1974 2206 2146 2266 518.00 +1107 2312 1204 2377 UOB +1448 2291 1567 2355 CARD +1978 2268 2147 2327 518.00 +1253 2424 1375 2497 GST% +1456 2411 1655 2475 Net.Amt +1818 2393 1912 2460 GST +2023 2387 2192 2445 Amount +1106 2494 1231 2560 GST8 +1486 2472 1661 2537 479.63 +1770 2458 1916 2523 38.37 +2027 2448 2203 2511 518.00 +1553 2601 1699 2666 THANK +1721 2592 1821 2661 YOU +1436 2678 1616 2749 please +1644 2682 1764 2732 come +1790 2660 1942 2729 again +1191 2862 1391 2931 Those +1426 2870 2018 2945 facebook.com +1565 2809 1690 2884 join +1709 2816 1777 2870 us +1799 2811 1868 2865 on +1838 2946 2024 3003 com .89 +1533 3006 2070 3088 ar.com/askbe +1300 3326 1659 3446 That's +1696 3308 1905 3424 not +1937 3289 2131 3408 all! +1450 3511 1633 3573 SCAN +1392 3589 1489 3645 QR +1509 3577 1698 3635 CODE +1321 3656 1370 3714 & +1517 3638 1768 3699 updates +1643 3882 1769 3932 Scan +1789 3868 1859 3926 Me diff --git a/cope2n-ai-fi/api/Kie_Invoice_AP/tmp_image/{image_url}.jpg b/cope2n-ai-fi/api/Kie_Invoice_AP/tmp_image/{image_url}.jpg new file mode 100755 index 0000000000000000000000000000000000000000..2fa1bfb51b15bb6ebb90835f57262dbff5d7d2df GIT binary patch literal 1176111 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%0DhyAbslJkxhid#EpE^% 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⋘;!X*)aP3#d&;>d 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∓-!PEYFYX1Hxy)43F;!Q3af}m3 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ð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

  • +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%0Aa*!FVAcOH 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 z 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
    $U~hs=Vf+FK~vWg+vxpCdKr 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

    Wn=1`CA#;P)t@{FYP1MXtlw zkGME?3R?jA1^Jf2KOwY--GTO`_DeVAq-4k6oTda3J%E95o})UizIYmLp{rdA;|m49z2Y-%NV}^W!0Z_){eYoD1jMom%q9 z0&(;ZgB0?i1To#7+u0a5=#uv+?{&j4OM#?nO;zUr+J9fpx075YrJdc7X|B6=9K7(0 z@(rr=gxhs=6=voSs;yE+C3jd|_R;ssCm97_bzgehdJ7n9*JcCp2u>kjhIvYwu5h3- z6*vRohhZwGj*#kb4XAD^Ff+srDmuR`Fq^hQsW~z%g2vCG5hV)+lF0i0UY3=1TUxJT zf6*`q6uITDVpGE^C=ooVPE)aObar$Yb9Om$iN>$!+c%rYL?#@r0J|F)+Oz-4Wu7+B z3)GRoW|3H)pP6%0Z_@bI*i}Fl%hj(N##15fOLgX-TXKiT{YSyLx^EC4Qhu0PmE-x+ zj659P5_S@!#pM8qV?0F$BZ5Q)zLtCV?g)^d*Li&Px*pt37{~>D0F!SPX!7O|@O8e$ zwc2A!0KGcEVEn+AAal+EIi1|FBT>#O>=P{{RwBB+5{);mp3!e`u;^ZgKZila!`Ja^ zz|v?(cvq@7KPz&JD>i=b{Rv0eG^p8|Okl!lAoyi%5zk~4q> zyI5b{N7Qu+k-pEmLynvCK+;0_?qq`{bxa^ZP~(^hYf1A@aBdce=pXTn!>NOy*lmIg zoV<3z?JiG|`i#KsdO{AAk!cb@>|lW@`R?@P)WFyiyN2bb0^x8?xhVl+_iXI!l!j#_ zbkGYd)5+_-hfItb(2@7N89dy$khTAE7BzJcDA0xr{{k^-nZ^3nyR8Ll&&}B^QV1ms z_*z$vDUBaXpEV1c*MPM{ya}C6BVD;bIidyzu=;uqK=hqM*1G~@A*ped4n`TmIZL71 z6STn&9Jt>kRx{Y%dXSuASahw3!eRCSqJ)4uu&OVO*S*}d`tOECLIW=offH6u{CHhx z5$TOt8o|Je-(=bzE8dNF;N2NBf$Y#YgrPGB$SGcR=y*`$Y~4lWwBM3!G#&~_Ylw76 z1l2Bbi?F0WtNtJx0$SughA(K-?gX?BZp9mOT*-^vRJj|r?uX0CTy`@FRXjjC#nkTaL?gXa zdg84q7`d?$TwZ|6&6o|fH?2M4gu;d|Dt^L3Iw{SWJsR;vJ5T%ev{|6?eW_9_j zgU7bEMKm**R0Q12FH_^L*%05~-__nki(xgishjG2y))jz~ zZ~+^?Husn6A~G%Q*{0QgS$p?`-c4Au<mXyP_p>z45h| zr_KlX@&NbP{7kAfUQ_w)Vvc|^P#Sd>-niy-e7gp65{9Mn^roPU_wRwJHT9fZTJY)c z6XJQ&3S$bKW!FoFT~R}UK;Xe>z_;>pk-I@HC6oM7r{+kUmB{@;+t*6FO*tT>o4-$0 zq@l<4n|G#2J)3Xf&8d46UI5o`G@qBmhbrgRUf(<}2_#H{w$4DKUTAeklRKHN?{l6AqsxmYUio*1{S2cFo(DLqk4W6X zUVUF{jD5v(nJi-&(*bC4WKN&C#`$Zoi9bsk2fONQ805MGk@%-LOz7omaH*c^D$Eqc zH-}3yD=`lO%8?nQ11pd>65WlYoWXMNcTkgVk|7)yTBks3iGgfkX2v!Q6da+ zB8r-9=IoAt}ZJsk`oTr3&LC64%g2AJGY;TP|`g!7nD zdE#saC%u}(nO#z=qFceO>`SPuJQs0#IT%w%$D@VVv5C^Cfo`ao~SHuUE$`M)z}-+XJH$Tg1u6zRTqebj(9{mk!&5PFLY&|GE6 zcnS;prNK|`I6p3{_IaZ6dz8Rf)kOCSC+^D06iqacr93AaVXnwNK#+EjwO?2jha9}I zs!Gm5ve#HVY@cOwak+@ro#Y*?EHZFy@lLN7SPAhF`HNB3+R34`JnxZ8d!5}tMSB$R zXtgvvucp73I*~o;&~JafAU64NoCBo^Z?HZ?;)cyGc?|`mJgra_q{)3NGt;Ej6XZO; zXU<6V1%tGzrA!NqGj;hF*T3F0##0Ud6N~Z6YL3nM;&HbqU9p)yO`^WPRSqEL^z#bh z4u)*qWOiD8nsth1J@K^&iDZ({4J5W2gcHJr5whOR(=OwupA9CDJy-0P!ZbL&HMFzC zxFAJQX<(LaDf6gm<&mdU&8^5F1u&CuanzOYIz*bq#cvofV;{|adpG*OpB7n$wYDssCc13I|k z16S}Ws#0A+c}(fjd|b)wnqn^r-Ca4s)4MlJE*GbCKkl*{PMdoh+&_IVmmER=mAs5K zLnC!F-gaEsOSRpXxP(8k?;E%m^ibW|fAF{Q_t7kfDB>@}=1DdCBUm%QyT5_avypJP zJyFf|UK18X?y|*~=JfC5KC0Xvciu?JxvdvL;KkS#B>BPZclH9+uWZx$;HR2VrHC4nL+}gST~g!4>x9Yvy)M<;--i#e#^82+1#)NycS+ zafAjk@mZepkHkBmxaXBmeGM%g^dL8@5(Y;HX@M+SA2ZhR8_~ zzQDXY!4;G+Jdcq?<{SruCCF2`T4-TYsouI517U9Z6%E% zi`jZ@yid^9XK}4~3kZP;x_U0L{WAr&eo|DP>8-TwH zYP#PiJO`>rgyyRLzp)_tY zzX2-c#Ph;=tdGL*MG`v~taPv?xPo8Sk^{Sky0`rE*&1>Va8WjBDvM$XPkyne=pjrJ zWIM2VVAQLS8TfwEWqBq9$!ch#y^UF0NLG9Mil?qD*}v{@5``gr+DLdTTma*^Mc9(D zteqTpe#=yc@azB%r0Hr&nBD_#BYa{wD*uA~d)7+H7J50wU9qvZWlFrtmN#Uu^7A86<7_3 z)qJrbnK%% zW5%9)k&(hUy?8UyGr0vzib^G~a*Xlx?Vs8Cz?k!K5WmBJgQhbGMZJ_$cl^EOL2b-Qz*>@B)HjL72d@vh?9)s#~-r%S|UkqsE`+u1_rA>dQ=$y;Gsf%mfIf~v^A1kIRCy`O$?JVq^CVjU z`Q=;_Mk9StL~r%TI&J*>$T6&EoiSj~lQPuyuj)dED8nq}^CXxgefBrYuItH}HpAT)#;L&xqWGo4 zK{yuBj>t8_xu&hW3^x*6O|F7;4B=^jk9hqN7~nip?h+YAH1N>OYoH**c6*WP=?t3X zs{P~czKw$v#1=C77u!vpBb8lG1m9Rl>><0%S44eiXDy&Di+q|5MYFTeOcqZCJxjFR z6XiiZnGHfdula@`vCsQh@)A_wRG=4DG>xT>ob-N4yagKTY&GJ<$6?T-Y*#p=wycGI zGo7r-7;o1;hw*J+$H`cG8cm;y2qmbzW z=J!0mEAFBGxYH5yN`H9&w%K6lkT(GM`%m(6(U{14SdcX|)aN6NgWO3Z0ZwXxHitW* z!sDjLQ(PUc!5}G!Xec!IrR9+aH<4;*y%0nW>sG)3$BDz(gjMluTP;tzSJg?5!>dgl zft1isg7%gQYRVtjKFVdOjq3>zR01%k2YZLdxzlp0`(3d9l*&Os1xY&i&_WOo1o9C= zTSX<48!q-%SDt&S8&(S<{%AxRV(YIi$kF6$QtFD+S$_4JVEB|N3(g42K>7jM=-E5w5534z22rD^;pM` zdPH`$2JTYZq2vCoWu1nOVO|z>3rNon0eaBuF1u|zzx`1jW1mJ2#w*AL;ch6NZ5104L%e7Ic1jO>O1Z;_=ShU4%- z=?q>$E?|$-4j*a#Or2+ABfpfTTbiTjgLTdU6BBRD6J)D2$0|XbFa`DuMxCRwjDI$? zyZW}qztk|W$?vJi|L7-TuEwR0;eucQEdU$-b^G`OqByv9pmh8GQh|MNR(Xh{7#a) zvM>lni+z{n1(T?a5#X0lpV?yQMbU?FW-fLBMLKxSOyIVdfW{W9eT<{_SLkqzhcF4< z0G>1;ES>dDbN%o&Yy?qdLDLE}`U*6s2zxwI)fObGaT~lFSN*yraG%q_VZTRCFAmqi zk3lg6x!$5Oin(e|ad*Lw0{A|IWjqSB4rSE&=;|*y{&k0^p93-hP*!pdVa(Lo)m>IO z*h?WgNq68vNumNmm9kIvQO@bzTMQSif8r%ejNjRVdR_wR;RJ09ugtC5)JH*Qp;RQ^ zB!tE(Kzt9s+wG`xD&RUHY)}!$s_~)p=Z!r&EX>M z$zEPnVh@m9zmpCd7Z)s-PGlCZ`EofVRD*wj%;lh|KGA-D`xAM`JDr#tb<~n4g4w~~ z2d&W(oy`3@0fwU8R*s(t25b4<)b3gY11jZ(>P$-uh%P?#3R}bFIpDkg)i{04uo^>a z*$n3n2tgtk!5Hey$fZ(FTQ-F`IFJR#sUHFn7FHzGA1oshH?;DrhMP|?9~MvdKuN-A zLe^v=jL7rW9_VcNHsqITeZy7Z5ffEeK z`&IYfmuh_eL+YBdSdXi@F4M&%rkQsp?Rq>9Tld6%+X3wKK)q$p|rr4}ur67Gp+ zey1Za!Bb34z{m{z@r!vgcp6dVBC8yRb(^Achg3ygfN-Q_4@GT9ruo< z=&&!eEDtI0M@-2sByw=K7BFtMg|ymC`z0-1iyT=Q7=tH3^MJ&cua_6h>#A2xYy{8l zlY{beuqvo7RLp20eeK2w+t4{xtP7c4(t~{n4BJ9Rsa@4+ZCnGh4rT^G%qZ}HABTxH z1$rm5hHCxndCr5PA$;gP9l}0?r@#CSH}OV0NHxENt}OHGIM$rM>{tJU+A{@`lWsFD zw)r#2il<7T2eKD|b|91(XI)WWgOw27X|rq+hs3O#zA}~adcVGke~k~!Nsck@a4r@; z%A(2)4eP*pc)|P!;9azr{mRSfqzUP4F)w|ug@GZO4b3j67564Ls0LmcGEPwi zN_3r`EYRDydS?2h%<TI~(VPj#} z0X{0oJ2CSQ&H6Yq z5Lp4<5JcZ9d+oyUQOkWN5Y_HA!5NH zwPh%I-w@|g-<|PNd6o&oTeFjCZhJ&ZtdDwHUyOGLCjWpd$#PfJBziLqgFB@=1w5zE zq;z~%Z_P<;ZtSh6*ya~_bNa5rb(uyhDj1*0N*~=TtL;8Z{?bPrl-G%g-PSiNMM_#f zI3@O@-EOtg%UO;Zf)iU6ks?xcoB}`|LS5jTjG4zTb5wcRD&vyZZL2W zo9qz`x!@eg$HSTP+Z;DLT}jSfXXNTu0yVVV8#MbpwKCuf&r<)2e->N%&#G<0M5iBI zGyH7qn>Pv14rzrtE8+J?qwFYN-w(&4B>Z%=(z$%qIW~ zY@S;Xp$m$R@JNt&93g$BG53_iVT9|@$VeMlN0XOX*q*)Yxi%_7UJnZGOA zcoWkuHFaQNDTzO22PF2jF*s#rbiSz5S#QJ_H&ihau*KPj!>KQFsZeo0{KX z)@XY(OXY?rCIR*1>VCTBTftWV^_+iPSs_`rgFN!bs6B?HdSNbJAw&2N_Eqd-u_|HG z?4tO$Y2NLzChgCS_UDle4ySpJa6}%dstxTs^2O{`mTe&dJ!POW1tRtZpYx>vVC@g= z)qlTyxwupGWPvm+FrXfaeRv%0`LygDiwtlBgFaact(JB+^nKNtqSi7XflL&Xv=i!O z$%ZMzofgDAkh!|xLC9-8iMj&K9eXl385bmeB5+cEI9~ewwn}jz=IrUo7c1#AlMr77XEyV z-kj1Kwek?rOl#9Vo}<#SrDd0CYL02{HL|PLnfj|um~ z_wzc>0Q2BDY3yDzmn%CXa&Djlrjv&_u>^=f0vgL{k=hp}#~gaA?9Bv;$mWuiKFl0WM3A$^ z?;j2eD-M}9vi-^@dLbGC<}Z?0Dyr|#&k&oqkY6sgS;HPW8qJ+@{w9R${Gv|S@aBXk z4}wJ$= z#%Xbo7yTTI2s)8-$kF^Hqn)FLw{Kp1HlYDL_xB)z=gGEwuV{yt?)LPQz6y9*WXCS2 z-a=0&O+o|=>D>)a>fd=N2kcsg@|_Nl=i! zjWY1d;Femn+bE0X?N(6Un11cg)GD_=N|00MSy{&(KYu89DA^P49Yjx24+=+hT8c&g zbORs$@O|0q{E2RD@3uOfv;t58=R|Gtoby}CCdWKIp|KGdIjZHN6rCs0B_&S8qy6db z?KQLKRBO*%AE0oGv(zuG|Li|j;@L~vY}OFN!ZuM)GefCDU_<##!&%o@>V@%9ask>O z)uS&fmUU7+{4DIEty!5i)|hznc{Nv$;y5LMWSrC1R+nM3B4?n@(^J`bggMbb!66=C z6?{2d%ifNq}8y-v0j`0tOXdr*nD zw>hB~f6<4jQ#;S$E4*{}bUQ@tvdXc_(a)6U=^Jr(k3=|?P>`>V-^AH3uX-`XR($8t zpCcdsNEDE3~Jb`QU?XzeRdn{YVIz-LZnPF<*A8{M`K2*SA7U zhQd|Y5XeK6Q&>P8Q5r~_Ki8(RJ0sM(?ng!J4IQI-cCGGuarD*-wRIeJL%f)7QMwK}!LJ zh&O=33?F>&jc*0-MqBL~`k`WNqr?xIs373XSc!PM1-q7a0XLiInaGfRisS=cxsEnF5Y$0DqK!e#SzugcP*i~oNJT_Fm23GT^mMYN9n7MOJB+)jZO_qV~LG0LmFF;7Iuxl>Hv$d8cZQAj~Xkpm0ye7r!l zK>otc*ovKuM4|WZUoi>YJ(w;3AEvkO61Q?Q@0DF6-L!&fssPHS zIAqtIudvzYWcK&Zt6i9kY4-RuO($L>rWP2yv$ZVq>O>5?{m}DapfQwifapT@j(H`R zy~N|?o?cHIK#7PyDS@#=g9UDFLy1ctX19Gn0c^)9va}AdE4tw$4>^@JducVis_!VJ zr5}9f-&;Yvp6`y~9R)=7fD*4D5P;^pxsyY06P1`>WC+UZ5YH`X6?F%iYX}b<$W1we zHARg4_L>}raJgTfP%C~*Ad8`<{VLh4$Pgkx@)75nI8Y7!udY85xQgq4&5{VfTWETB zC^mBY9y7jg`kw3#&jY;9BikyeiG%VPBU$-y5_qJ)ln3Lq&Pj|Gzj z%l1_=FWt+{w%1{KiKYNf2g0MJuK!$N+>f!tZsOUFb~rJ2#3}*Vcbxhw?^2-X)Lu*7v0Ge4fBvkgx_ zR@~gO1nQZd;CoHU0J%a8$oF9``gBX-~LikZ2RLts;X-9hJwWDd}}hLpTv%>ZP_{ZPMFa+Dg+&$ zKb$QGxZgnss58k5bv*B=-Z^2B6T7?i+$KV-YVRWT2Y(A+-kaZIUI-IyL4%k?cMup)zkb*g!KMS5vSu#T!JbrynHp|h36^blan;r zzNVeHYFXWOixRq|(Dwr(IpU~f^{(cfa8w|aT{<%^XQn6RqwC-HE=ky6e zyqbyh6jya$sPF7uYy-O|j0y8Q5KDf66yD`WkE-}A+AdW{FOd5} zCU5|B6dp~^zH*dV!uIsE+rm!>+z7rt-kYroMvb!|H-KzQcgU_}B$JuX{`)uBK8xGi zcfd)J5$V4VC49!VRSV`yNYz{VDqrIi@NtFucGk8Iz&l%-Erutu6PyH z-BQ5{`pNDYN`B4(&YgCc9!seel9}oaJ*ztj8qIQIF&i zYf{zN(QE|#{x)vJQqiHZATMpUz~Y2ej(n8%ipUr&@UT`RCFHqMP4RQn!z}R;-?pr~ zR+mg3#QAW>1Lh=68LrYNkRfm3tIg$KCGI_cAcg^j9p2~m;LlULN&k;zi9@bOh}FWA zq1V@ub(dU~G*0!kC7`KhMm~KROYh$#J9l)VFZ%gSaqPep%r4kK0(7EEf88Ze&J6l&>E@pG;Er?9NGQBRVLAUI+UnzGd6jVXWO58ZagMA6}Brb15D zcN4RyUz?oLBb%~=tZ#JM42W~=Lr)mjD-9e9;7_Rd`T@3T3+K0YSxMC+D`XG*4|E`o z@x%S}bU!OE^EPdfJFUu%tI-?%<}Ns0c-DQsS$lk`85f2Lo;>(Us{58aTj@0U%6xCK zQ;e!Y%JjQLhEzbRLU)tVj~!s0>mYxBKpp(_B$^CVNlFPhFWd|}v0%J+4^vYtvlz@B z9%R|G%7xLLAqvGS$DF#ao71a=R zUu{@&Dr>VDZNL4V>ul1aq5GH5K--?o+Vl@<;Oo9UzdHu#XG9Gu>YpDqNZ`L(R-@iG z_iV(&A-{jjvl#7cBt3{zPA4CK^xeCLJ4KD-rgqF@0?W*#ZJe*&o@%h>=QHlSJ}=+% zB5!~>92~7N*IphOcy6`rz_H@b>slXX_r*DJ<%(PqDq{Hwmmlanl#8iB=pu&z(Z9>kT(6TF+mjqO~m z5M_rsp+pr)R_9jq4Y|Y(FSbLHi1&i!U~z{33^Si|b$L~ex8+l+QM9fikB%Tz z4(~Ai<8WtioL{rY7HBi}gFZQ+lUA{i>=^?KXo;U>EGFzL#g&`KTtEmyU!hgovVHap zT}r6F-4hcEAuC-aL~ zjBD5|N_~h&DY_Rj3yTh(HR4s3_S<4go&oxM;qMUSCgsE%Xkc9MQ13XwF2)fGJPno?VsD{N-mNZ7p+vC7Vj4;`pR?sLMb{l9qzBK}29i2IZG=)3$ zv^-`>`mR&D-BQ=cO;m3W+J3O>)Tc}WQSzjj$?A^P9io?nKkq7WtFE^K1=&=Q+EtZZ z(fnzB?o{#>N6v5uSLtu!8o`aH&JR^$K6&$*>_7wY{FA$-F=MfX6$>a!hF^0&V70ns z<0bL^@@qi#Hb!!%_St(Gy&Rvk^3~oEs+sMH4GbbOJE8M?*YD?_Q_0yy{~d4Gfg*gnQ*(JlPJiv15`HP@QZTYqpCGte z0#!TX6d}EoWmRGM6t!DQj=-Ogx5osUtL>p$)8tzUz1RXC?iUqg^}+q$eyRUs5Nl3) zu>2!~rXog3ojSQR0~+ndY?aXylr0D30mZltdeHdCfJFn8qu|qlVyrh$F!S0JSEa5? z*qtMu+8;5DI?SYNJR<&M<>@9Wc~;PXI5fy0Mg1lNp58bg?4>vF9-iRPM?@uTBJd@S zoj7HIm20vdm`(EROw<0Tw80bND|>fth{E73oCf4TPW@2_nzte!y;yh^!A_obl)Qoy z3FM=MnUA{tVKr@ZeH>%**P}SvUs20Es}>g*;(>m+H5Y&3<02hI15?g5-ck&I^bT5s zK3ktC=t#Jaz1glU@1(6AkS>mxSV|$r2on#sfsRqD&6&M~$De55L_ImAa}&d)GkqjF1MJ4>kt2V{%JxFX_*nZQYbd6!)ZZ|XgKihe%pqCLygQ5gL4iA z+Jb2bXW|w$IGVbO0I3f9eVVueG=Ydy`AW8I3SXEGld}(Y$2fZjq?VZXgqOn;xQr)J zS|=cZIN+4k;%BjTUJ}>m9`}Lh+=yhDYJSuey73}@?>cg}FJSvkP-EQsaz(KC50HZx zR^LmOCPsWYc5H$g_AQa9H~{7I91NU+xP0)sEUwdCG%DSH=`lZ`**pJSm0KF#pz{1e2N4AZivNE@Mn5p47>DCF5kcA%vY4R)RGJCcz+J`UYjG*{%|EB+I~X#iL=aj@YQEL( zBTrw(zmF*&_4I9ifea?31U$Jaj?MGMZG#U4Z^6DA&^lZn@NeNixI~GIBSJEsLfQPT z8a2=IBZ#Iz2GY~n;G(Fswug(Ztp9LjSS>2b%lVYyL&dr|>(Qwlfe{sV{9U*4M1M>i zPS&9XhVkD%U6jUd5$@#a$>HYS9uMmfZ=-MIzM=pN;IB^oErqwaf6Y2^)eEL= z0sSlcEEtI|4*0vaueoY_h?YnVGz2P}N84CE9rW;)G69OpRqiO$=cN0cdsCb2crMV< zK@lIGXZcAJB^OP?ys#LuGee-hVOe9Ti3wq2Zte*(sd^5^OJ@gMg)sId8ubc z%m3S?2hx>(prM}tCq7RUZ2mT)Wu`gM=|9gWK55?K-Nd|i$2*n`dDZ#9;iel>&-YfY z2kV0B&)Yb|2EgFaC)fsmhodd4Ts5DNPGHq=BxVe%rdJ4F7tAq;P4h7tB+1<(M?^7_ zW{3j6zxz1h?5&MEKB-TSao7kXBpd?bWSZJX9-CqN9$3%}q7ill4g|AToclpj8%Zxo z3RxRt?~c+Z6$ro-H#FIqrFZ9rI>m*yVpuN_lQRPp(mj0O_5NLNb4*)Web2EwE69un z%6K-QjOewdA)m(|245c@WD8$F^q3VWkdnUHhtR=Kn$Li zDe3+gLWnV(Jj#y>xRk+Vac+F_;OJusxP>3Qj_%(XCXnd#qR|`(bjV{x#Ik(7ed2*i z1Fb1MjtG!c8HesxkSeatS1%x@s#ap3$)=mn{;cbhIC$gxZr~iPojEGj_eY-9bC~m# z`ARu+C_^jfGAB4>8R9cLU)sO7Y?NWTcE2aF`)QK{qYsrpBq%&2S8GoSohuW|$Bcjt=b{c%EKdtQ|@H zrh$0VJ}_#^$Mm3>`SeAewsQ6>Weemb5UTO;k@sMjjk86XPtL3g{4uemrCIVo+uIpT zDS!{}t9$*cRqcA2IFR1TD9B&k91<<^ye%L^#c*q9)IhT zWfIDq>z{8cm(r%=D*BY))iIJW;|^}AfXY0bK%x?G~o zqmQCG6;K}fYwZ5OS z!nIf7wPYLdzbT-HY&uZZWp*~bZ)$4l+138RV$t~L^`|;=kQPcM$qdx}26EXj4jDy_ zEEBzidp!B)2da&&j%@#e0CL#ik?FfYes*?{_)wra3);6he^)tSiwsi?9DnP2`_1sf z!+u>(gN>I`G$-{VgUS=$+fKDBdS~eMD0<0%f#!X;0dnl za)x5>|Gs~2(G%uOPr_;d?+4opY+-y-QXZ*%xtZ&Y?aZ0^_`*;m&dm2@so=zrEo_BViJhKwEVV?a%*xk9 z_`S{TjvYygOzyW~P#3U){ffxFqjGz>b!%P(5tO<(ihQ@0v^{7Myyl>Xh8(b1N7Kwr z)WmMby*lR|@$776^9=}ShX#WBvi!G_GjSA$wG)$$N9K+R9*!T=UxUOYM#Zx76r0yH zQl-~m!FMmF7;C&Z=7{FTuD;=yMLkWHo?RfqSU}}48L!SZmC~#A;%a(BdcG`;V|TLp z-%L~jD&wdnQ$iSq z6+fSx#yj^EdE1U}j5ob75M{xAuQ)vofc>3%sw)wzM1knugLlJhlh|u%EB&`so2y0@ zKjU~+6U~i@sWe2`@~LWmcOJS(mHg7@J~M)xF!7%IwAU}d1dTyt4>8c=?@ zcjw&VakGg>-S3;$E!iO@&ZzU|U^iMA*!8?@IWd-B2)H$Ql!k!0j~w2UNVO14+KILu z3?;AxY9Ut8KS0%>i)x9v+N-M8EX|PzwG{n2nwi>PwL`=+ak&QY9NEhjPOFu^#z+w~ zcC8{ozG}_5mJbzfiGhY3N^39?uVH0-ikZ=cm$*edQEAkk#3#|<4wLcwhFl_;!URhr zwd5j;m{(cO4w8)3vChnP&b)tv9SOL9G z``f@LR1%%}%(kSO?|rY(RgYrr!NC0^9mNOdX7{j_3fD!6^eEqCt$wq( zHi>iJ2bPx#`6!xhiAlJ3b@$t63tbO11VU21utF|cSW7YUDJHeKfOte`vkdq>Mjwg7 z`|9-M7RYI*Fy0*HsPA(~cE!vCh1eEnX`SX75dC}2>|I+?Zk-8|ZeS*g+#S1$`&(K5x_8rrdX5QUWB0S_ArBK3g};tYxBYBr3`t@sFU zrr0x@UL3Poct4N{{k^g9t$Ay9#7=uZSHGzMVGbiApcyRe*S+KEw?-%I7`KLXgZTl5k*IQpb6EbY%=@od>#DByv+IClwQj2>b1-m(jK>C_eX)Pb zBg83+nREV#nE6u>v4HSppOeg+vL_UKG&?g|t1-%8yA3`Tlp+|EZ!zNcLm_?aOb-`k zqifc9Y}C5O+`dS6W+OQS8aMK9SB6O^Q|+AcqyCQi<40h|_xJg21GYj9)*8xYFQm3E zi(T_xY>bIy(!IQ4fcO_am>_!KvXm1!%#oK24U$EYMedn0Hu(FGwj0JOwo zzD-v{UMrR{dYH&hab97|NfH zj=dDU0XK;{|MQI>z0LXC3gFTe$!Dx~<5)?!qEAf4o!r$1%|a9 z|7%t^8)>aiBJxJhgTC@-@$#J5yAL-M3Acw|nl_678E%NqKs#ZB2V&}iFq_}moop{f zj?_G^VnvEN&-{rR{uBcl0z_q)71cgyVNPD>@WL&p(B5NCNgRRDg*(JL4j1H~^z4^$ z_8)v^DP6m9BQJN*K~G&OQ(x7beR=Rp+bj+OPLR3#H!OLk_LW8oj6!-J!DBAR&Q1G@4o1V=w~W<5NO5)yo2w_ zspE0eYSrrDWKPHpbwC5UUl58MXqumS5~kvA*Mmk2wRCU$#3Q*?aA3#8JHr zv)qGq+Sp}3?~!+Kh*!3-)K<747!gWMeXu0@;$4Bx_sKNsWbVB-YMsMbuKiM2(7=mr~Il-!Ged1-^qr7I8;y{+6KR(}rhwt+qUL z=qJ@xw;D8Q{>*^x6Vb;++}C&}b$xz8qS`QY^=KjQg(uOnNi@)czi3WRz0xIK_-0pR zp5>Gn?DYa3KP^ACJ&-co*~ocr5Rir9$1c7Z4A zTGN$td7Crj8J;ffsWe# z8Z;H^dPV@n__&cYVd|)~{L3Yk*tQJ#bktq`?j^Ono>*i=wLo^;5j`rECy}qc=^w_2 zs2UF8L0rdF=k?_Bb5GpBcCRZuEgJHPrc)YI86upz)S`Ua_o~qisHfnn2c-(Q`Bi-k1+u$oqu#;uk7=Zpfa{!JLkId6~4=$8V zP2t*~p^fS$nOcdc#G{2_mQVctraU4(+}O)Wf=Pky_Bsp-g@wF}0;}pYFE)FRG3KCw z!l~oKqDFTq&Ec490a=mMC^=T_@yk74r_*zM*DZh}x$bpcHF=H;Y0{QX6un`y&n$1z zZbQIiv)sE4?(!=ip<%0-lrXx~i8@7i#*zFgAlFeg2zzpk$Wj1nW%K7eLe z+{79_pIq0TXbPxl2aAi;2D}p>VlD-sYh-G-m11=%fnREyf>`70m%4e1t>QP-{RVd6 zOZ)>EC73dq6HiLDhr^9V=-m49SK)HCxPWCU(THFHwj8EDLs!_y`Qqcf)#_EuyRCL$ zLMHcCnn-lG!avKhO4GEFvT5?mGlC3LtOVUp)}f z?+(NDCr)!#U7EIdh~*Nhw73sZK8eZ@mc%iu$=mBOa3@g_^InlyeaI|Lhk_0Oso%%* z^)@ZxbJM+)Cx}|l|M&dL?5eWYyI0pS9LDJ#!dC3jl*f+}O5=^{Ff16E>{uCi+~B;_ zyq;)qr?{-9*bC>(f<5A|KaH}BbQ@4FoZMGQ{(+ig0wysyjl9M8{dltz&7Y%4ou_eb z@({$sH#XU2bn+f`&R@Oqa147&!Vw}>)WJQ+s>;$@E3*A%4>7GgxF7TBBxRuGfwz;^ zwlef9U(KO&Roov}-5PVlHh+T54$aFrVJ%|fo>%$}>oHq<^=!R+c9#TG1!$3=z>N~! zQx=W#KP+tJS}ljJGw6wO3(@$rIr=7s1oO4=lf+!s*mhgKw59w&f&BoF3I4geOT@E> zdv8oa^`0qZ%ts&m2VoEvdPa(cT4>EkQ3pLX5Iq^XGWM!vY+G6)#`p>;F<4Zw&o*bC z?;jiKTvKB9*#TvQ!}VVHH+3L>gR@m;`+RDU`w9PG448sLbUeC5?NTTYX7L6Wzfl1o z<=oz5c{%j}Td1uY0Buo^Jln)8ukbfc*goiL{yBPAQRtgdViwHHAMp3E)Fd!{qx+di z!Q(K=GiiP;Tk1I|6dB5%Ox)1*cA(FGD)+BhA!{bD)Yh5(r@bcpM7pgyOVp^83`1_h zyUsV=Yv^jn!+))fZGMZ7`3i^Eu+-OB6`z|Lw#{Zed!mX zLaS?7{U*o3*J0Qyvt0s}<*#K~YqqnGId(#v5V8mp z!e8-Qo%ENo6oTK%!vKJNvKF^NPk#bm}@!A|%U2*(E6xQyRrR&Bk?@0BHdjK#>l z?bp7r>J~xFK4HtSzmJchL@co3wo;b5?(lretqylvEBo!qKSr*>?rC%O4Hg`GP#LBA zDsn>%yOVy8)kjG1zgrL+{2awj%bJp=r! zm-sWM#SEqBiJLL|`yt#QBQH6RZX~OlkL3U68Kf3ZQA)zpPx_BMXB^p>oTiOw?p|m} zdsWH2PA2kZSj?F;7@#+*1uDzN?} zrWorg(xS;O`1@>rq{jJH+d-wiO(7fj$Y3yP1I8k%rydtl@V)#3#w!!mKVdZMRpR+q zT=0W&WDB!??N*P^iT4(?2!AzRtG3C-2;Nf?Hu++(>#VzpE8_n@*)o+P2gKl>7}4cG-7G*bbp1%ntfge-IWFQ{^s$~K4t`5X@8y~p9^=bU1eB9-#d?(RjoymDYHBbAI>HiL4ai0d~ZtX zZJp#PF4^Oa!2?IOd$9VHsAr+BZ;N8gz)JwT48@$i<<3Oc-$jT3|5erhHfa?wG8I?4 z7@c+f)-?Gfdzcs60NgOTZrIMal~Sv zNY44uvT49oT%Dc=3Wa|D1%9Vb7ZdGwLH=GQ36$L#Wj5L9V)1xRSSA_v_xE$86Kv(Q zZfW$d_t1V=>Y#!n725P= zI!&x1pS&w(vc0LzJ&(^|oTQy`=aJ{21Z2+uJm2IbG!|{4u^z-Mbg8PHX8#eniOX!_ zQEEB}T@8^@ZAY~%4cN@fod1<(4!$4_!*&)lS^UH(?C`g^ew@qf_Mx~y0H$;AH70Q% zGu~CY$R1`Zj_)R*xp8M|ldu(-NGcmnFR3{)8Twm$L@}YG`ABH3*w@bR`fYT!1+PQl zlpe9sZtF*F^A|+DMS^6Fd9I?mXSy;+e1*AUB(U%m!#--HlQvfOYM>32?gu!M zn>j;vUC#wuyz2#dAS(RA3&Opk>WFuW4s)1`tPiPw+L9hSa< z$4Plm7J+gzHVpC+7-|gn)nVxN(%IM}^V=VH?qn)A`uy7SeNo!CDKy|tH6On>aM{9{ zC%-tF33Y+6EQ`@yC@iu;E&|uVP%IqZn3K!qO+WJF7h&MF&XB`>VL#)Kvjl@?XHCwj z7A`?os0Er;hXEY`rW>Fu?VL9hRd_4FzohuHN?!sX{F$~p#ulypN$6x>=7>-b6-DeK zD*R2mYeoVz`?%qA&*$a|Z%5R@i=zrgK^`oe9_a6aj<7WBi+|f^w*EDX!DMyrmvI z*~7q(%LH$4fWIa|P$~xC5O^EGpDEj%+~X0DFCj-quCgd}9UooXcy>pAWbRmUan^z_I@=b4@Hn7ewy^liZC=o2bjSYJ%YH}8jgnUwb zO{-tkkD*9~P-CX3JUaBj8HY7{N0{?#>0Wn(dO=|a560H`P7I6G5EdL_^j$73Xb#lPLASx)vnQD(~p6qmfPkwf$IWLk||PkC*4y zZ%Hdc5jFBHyE7b_`Z4-sGjN7@w->@gU9Uo&)Tg7Yl7srZ!ZXP>LX2!4z_`;H{t^o# zZ2?_#J|OL`kbEJ_5Xz`+b)Z6_(*5#a8|g|fdcoiW)3S?qF$Uf%Jj0I|XhEDw5n*#a z1i*8Qd~!cOx&vr#4V&EE-yBiU?J(CmUAe-%*~NT?UaC*8W2bJqb|%tUY6(&Xt_VL~ zR5svn$G^DKi8%yGe8*bdx~?qMrh6~Aw+ChS2UZ6ftRcdMXVL8{Qr)SW{(>9PEW`J= zq+>R*D>e!A#}*H$8D5h+nt!c`9Lz)QqT7lbfnl0e6ZL$BxRLDV)31g9F8l zcQ!OcR4xGPNjjlqj}K#BEQnwSZKVeVA^I{odwl(eA+dIG6Pv!Kb2t|dsZE|& zpp%548tXeaRudE59Hp^ue)P1I{Rgf=pEd`tMA3!uFSwrOth#Q8$nV}DY;Au~oNRo; zUvyNx-aOScIap5vID?uMdy{^#b5;v3$)1)|_WUA$(NHy@naAUP$P?o!vTtg7k9ZLW5NX>kc64Df-~34&+9sK~a5Uv0}a8+7|j`&cMGq1(4Q9PG23+4%3j3dyKurYR%vScB}7r%teN# zl((M}SOeHY0haZd^HQFc4zj)XvyiOb=l_W@)@~8NO#Yg+S@#pE*?oPlCe4Z=!*iZh zS`tm)`VnreMrE%9L(ytn{+S|(cZh4D|M?4x(nATYwOe}GRBzU=e?K1EkrKPUS2HKR z+j6aHC$7^A77t*qc9+iab37)T-}=|tC7OhAi*_Do6y_PWtGdC+iugwKKuyHMN+8k$ zAW7QfBqn^%3O+GcW%@J`Vl#J;pT&ZMh|F0C@w)?L^7y)nYiw;KZMpxF^`arxtATj= zU$dwmIFMsauCV<#vpATxt{RGV2d&lDDM_PP_`P8F&>pWp8yif*UHrM9q=s%Lf*?JO z&-T>QOeIo!9@-z$@#lhLfTsw>UcE7Synz%uS)$!eumw~K3<$4A+3O2P>d%B zD(1cJ`egd9!(X!&R&>|7{`tb{#q!@AS$QzB^cNsXK({}953SPS!uPLi!{bWDGe3`(V%z~|jd=bE(MO6?hG?sR%#YqBfM>WB+nRy!g= zsz;#)vL`~~HMffPV-mC7BuBEwzdf)|kZjN{W%gB2KL7?h+w6h8_!4^oMC;deAHUew z%91D7+S`eG0fS%^5>@co;(zj!!bKSi_ww?>G;SSs{Uqrp>ccA7;CyAtgLv)c{P64! zoAvvN>RU{e5$Kd&$>ZGrsN#PN8uVKCV#}?&QBSF|HU@s@bcTJa2=e6&NJ3)221A01 zHh{V4F3TOKlb?p}Js5M1g*v{w1WiKO z2P74*nA4^h{m%-*SEeB{D%XhKbUIM60MuFfoJWL*#`;$thfJd4hnvKm1f~&HpBj#5 z`nr}1-d2?p8@}&RNKKrVKzxAU#qZaq!a8U|F=1M$zJr>-TS4A-k z>_?^cJG~nMx589;BJdYe)*-{G^uwTfQ|1ZNCL!jN)*s+~ZojbehOX!Y8omG>+DY&o zKYfY#Zs5nartZ@6>Mi`Y!S9QD4G=6+{&qC!c-d=SdQA1faq^nFgIQQ{<_s8~*cnS7 zd_g{_gkz%iFSv7wibD$8f&6ioDhJNe5?6y z%EcTLBpF!%i-F&lZIeCzo^@}&G-&!HzEZs&dAP0_Ksfr4{7m7aEjVF8GO1xMi4yP| zar|L>Id_x0Z7a6y^(59kK5J8WHA1F6L)rkyt?%6r=(Rj?&K=}uvaJhpK z#P(~kN==sbJ=Kp`W}bxNe*1=^GJz(-8c;limBxIj|3YqJB3ZKMcS(oxT~?xWe}uXQ zkph>hNn3Kf$)zKbn>P+S;$LXTn|FIObrB*gg>fSswG6?hy=@#>P&y{+98S99DsW+Wo!mo~If zVJk7QOf=a;wb3cAxyMyYn>&wuM-DuTqBJY0xB!OKxn>CXl)*#)BI(vn_2aVvS8Hi) z8PF2x5Gbdx1y$lXk}3T_G&&07P?*h{+mrZj1M{R@`qEPl z1(LQ(IkO0Xh}%tELO%Ke1@bugcbU6B;~OYMPbYzB`>Q|NpYp!LH4VET=-;Ej(C$~i zIkMRyC)rYa_nWa^>h+%!5-R1XZ8}>OJD%F3KKj(*wUi6#A@RnK0>dOm6yG`zt|41s zUd{MHX4C7Yacc0spCFcQ$B-^F_~cyRK>4FqTf(-z=!Y(1bdt?ebgg0R5d|>=S89&t zMJ^%96%^AbN*=SC471j;p|;;`wg}7~*27smG4w+cM++^I!90CGQno znPdO4Yj?)Qei(BT7ruIL&alEk^&Yq~$Oq=iGyeA!+j{ja#xJ*=4cEs0+jJ9^sqh`{ zqKyJ;l8y6GMAsOkuGN!Ix4TxP?A^=^JuzS{XMc*)>`msH<<<`OLWs2R7 z3C}Q*Fp9KTmq@mQk49AI?QawvBQBivLF1N&Rlin<_xehB2^Y~h7+;I?lIam6mRBk4 z7JNQvF2QMH)Tm8=+mD@rE*wniu)}}Xi#(*wio~jXh=mh9Z7djfDb4Wcc!>PnsziM| z0#PRq=X?tx9VStWi`O?(TF~SFbgsu6t+i^18;cW&62dv9G-0} zIFX{UPbtpG^&a$06Gum5mU7yo9&@9UK7K}xZxwG_DI8`SHn)(x2)P@GGNCc&e}5Um zb#F91=(;zN71R9e(T>AS8AZsGk3;P4304=T(XceoUVdBQvruzJGFA|9IF@unC0Q31 zOARgT)L&}Wx8!co{59*R=1!kRAfYT81>aHFTddU3>dC3DuRa8hrKe05n`Vw=ew*^X zH6OR{?8fwrp}Vh$V`7Va*afg@s7fw6^;q(ig`#yp-?n`%UWb}+PxF7D_6Z^2k z@)&h<(o0&qCvHK*dIBoJsEPuwNxj!%&cnn{qw|&9+#ZgbRke3+#%vk{5CF3fJ5$)S z`08?4;wFZvQHaS(<5MXvMk!yQfOsT|8U;>yjvZmV?7@6b@xnf#3oz-5?i9jT&sRCKl0`^o^qi^Aa^1Ve{6ADy`NgV>|%G3Kcz z**w{Fvoz*4HOlt?>z`W@{3~0Qz2o&;%$ogvd5fS7(4}%Z@njXN(1t8d&#n7MbPR^- zQoZ_fey~}-MtgfZU3hkt%bSQ_{v;a7Wpe!CA20J5s&b^>=lqhb{IS~Z%=0&!CV2sUvKvWsDq6SXWbli&lbeV6 zvj|L&*|jLEmy^mYE{^fKt7Pi99rs#pahS!vBV`MXg8SJ(9T6>+1D&hfZkY$D=!>EAA@4NB1hF#f5-%7)FIN zb&{)Ci_boK=py<^wybdA*|4L?_Z#pCQhm0@`{)n-HnjK!<={uZ42M#@aJm6yqN314>uc#Qoz z0J2kw8Wqw!fa`e?C1QNdDRAmfzxoO0q%HIjkei{f4(62e##sCn4&}MiZ?@HF~ z$9MG6WdruZqL(xu2c`JWi=yKWZNrrwrUP>NPtrRCWRt%mN2U0Fird%c%ItRN@Rpx# zI2=P$d@Z$wf70rmhjG=RB~GP?+1lJF@`FR_*~p%g%_Bv;7IH(OQuI23dH0(a zi(yfly2Mj>=Nq;T!=~TF@3QS4(3n+^)XLpa&`A!WL*KoEM%rY3sx4(6EzOH~#k_o+ zXo=__g>|dR*8YkQXRF_{?x!n`iWe$R0#P(8+K5*=N6lF0kAQh3R zMWu)s71@`Wl9~HH_qor0ekbnhq!m?aFKYh%!#XwMzSU}7(6{^FriC0!qq;ms5#Dly z$q#4f2+T0Ft0VkrwO8WTE$5)o&kfCpDdf&1LDl|fGC3GRah9#fe8==L6#;?@RH-)# zz7iS8F>y=qYgFnqdX=+6%75dqsLi&)q$g^8U)YkOG_(PhtNc(;&bh0cv*X;<0!~fB z^7jHCykM72nln$JLHPqeS&w!mQnNpXZHpXT*iK{*agJL%@W?%l2yV4aem>l-NVa0S zr{?{o7ep#S2fVa;BLb?4>C|RH`!QNecc~BGW62;8+Y82)EtkX0xgox^yCT7q;IWN^ zL!Jfb?kN-s3hENa*L&Zo()CByoU`NGXjp2WtV;GB%dO4{L5p7LVW(&Pn4*7?33PNB zN7f8MpUi(~7F|ABCJl&^_jb4*0|!IZIY|4aw(Q9yu(j<_aZ+`l{MNZ?j~nQx8b~zO zK>&ov;%i=YT$Tw5 z25BckWF}iK{gF|@BWHjbpoUSU9pdcunR=CS`VDi)8UixuUcsU7L9HVvYvC7()E|F& z0Y#~;#iD7S=y_t;J~-c?pPNQR__kN_m7rHWqzJhM0n2%vRZ#GGASyJfj!2OdQHj&Xzeo$ zl3a6M0ZN2>@Zo#~XpX=6<3HK_^8W~c-S$yo3KP+ITOjWA{BVDkeXpW6a8u>~;$cUa zR8Ls^RW@ae{G5oLcL8(YJ0jOxRg*oj96n1$OO_XD%ECJxItrA=Cu2BsW&5J6+7QgP zvc;F?1cP5!q6$-};3g|zZu#2**<>P!?! z=R%oYAvlyv|DDL@Ec$K`FK0jtDU3C2$%0n{W3)4*bZ+xMC2-KCMgcSQc~WoyWls@F z2QTx`;ZmguMv+RBV__v}@_%W4vE&TuI3*pvEkba0at?OXVt81}b%;P#z1BClV|U`4 zn$7sL9;h=?W!#uf6V$fNl7GU+W?LEa+rbt?WTpV_hKj(+%;0>r2i}kqkyauLaw+Sz zK7)ur1Uoo%#RCErR!f_ zG~#+)*M|Y(b+O(SKvD+73t|SK3iwy@&b|9->N4&DlD&8ALL2|{E+};c{%|Mau_Psx zQ^TWh%q_a_V|iGrK|!Sei%e67yU%O=c(h` z{@r}NfEo5z?DK;?Y~<9rM3&w0f`W+FwMqGT5w`!lGsvvG-raB56SJoFArzjEutpSZ z@8d8dqsQCBi$&xmWY&bJEm#seeckGZu6WbZ&AFI`ne#h)eJ-JtRrM>dhwWvHkiq_V4BoV)90lT6%qQWhYNo6%^acn-lbcdMj;URv9k`t>)qO-Z0fvB}-DAl%S^CgJ4ZuaD}U(H3)KXOpIcB! z1x!kQ;;?8j82&skDqJzBGql5R74Im~&OVrGYu2w0;{BRc`T}0TK3G&z3ZU38zBmZj zD#RVO-HKSxL!1jA6$~hvyyy`(RUwL!AFKq~nkl|vniNCb1SR9N8W2RcT!yBGgF6{> ze374qd|-sN>qEh+fF0V8a&DsUYf=^x#?91{pk3{MJx@+)+w&|*|G3`Z^INA)O;#8= zNA~r`woZ(Ne28;pKTVjc%^VlUF_`sl$q-b6;vR9XL;n%?%nMdUP9^xOn64Xc!*jIs z54LZ;cT_O7g;BOpHSo8+T$#>S1sD&fPmM8tz&vX1lIyCe_>738A}RIC38cSOFP2b5 z@CUbgpff=@uO@dEd=dVyDIL>&#sS%_GrDrj1odVu(6}xPq{R22Dn1nW`LvLA)FnC1 z&wtJ&q9K;Tz{bZ@BBwlA)8Z{cjzk(>ilH1cQ;YYNvE(D8U$6t646}V~;}lT;T8FGW z-Sd;am-WQrKf{HCKQg40zB*RxV)-?t$(5b3ITqA66kB|Vqp9SeDO>y?~ zv5hsP^bqAJ^6mzDI%zD86)OBy%TVLr`J`O@y)T{`YQ2sSOll0;(=~jN8PA?y)ENC- zt|z`ewCc_P%-HgR^d79mV?WLzlFVuCJCue42W@HbdVSG7&;YIU>7wI{Qg;x~(K8;% z(g61mi#MK&u@LiT&LtPzZ2wthWh&v3q_>&o;PyD zKYM{w|E1K2%m-ISRgFypAQL6yV~yx4d;N;nX2u&U4`1MKlBHZuY$jVEA=4RYf5VFv zu5ZFH)TTF5T2}NO^qXyhauz zeN%gxdM%vn85CXJXlZw@vRC-N$L%nrhI6=Ywp+mD;$$M1Wkt6hKDfSJkSL!oV zD7B{E84ea*sl&lNwjtmTZS0C<8TlU19?J~9c|AgZv-Jf%he#9(43 zC{a9}p{_CYA>D2)T)>wwNRQfgQNjC=pl@m$$QlL|slJX?@WPfssaKoNLE&8y@lP(+ z4e7{xzY3%!927qymRZvycc2?=~5ynlPwl#elIR{z*)Vb5w zF_7gklZQ!&*ey6#I7T0`=&k_)T&-li5#srezjpOPl@@<|S=F&=h}Kr)qS9NzWjyKr zt-0~sY_hyi+yWzx;%n+r6&j|AP@ZuiPW=Yb`M4?0v@&dx(TsW&5TaNsh8LIs<5fcD zuwX0O?Ft9^6TN;I4wU{zjOV9Ys}qU2!Uecp4(8P*Y$+}Gq%@ds*HIpNVL9A!cjXaV zrs-qH6)m3aJtal?-_%@Z?Q&UTs1(8hZ1QwkEk*GA)X?b)E?5Aj*)dy3g-36>w<6o# zk9)(is#Qwz3Ctk1%2)afxAKQP((G9#8|})FK^B10LKu?Y##qMo!D^5SJ$s4a8~$be zC5=&$k{SyaVUdh-%cuTDe!i~EcA0^@-^aVuWB2X=C%WDAhtPy%E0I%(mTtl=K#lT) zp@zZ8r4scYW%MFL1sP&;ukbxiSNXJ|F*O&J+~)j)d3UF>P`Dr9gfwPLU{%_-H7|MO zQpQ2>%aptc2o@;)Ep8&G6<@}Ch*-hWca@GZ-}_bzIVa?#VUFF*v(aV%Px)4i8oF!i@cT0>oT}4mD2iz z?ylm#Z)$7%gRKrK4=9@y@u-h+2=DtFFQ&1#!#2}vXw5#vKWAW2S~Rc&gk}<)68}?? zC-m{4Ef*KV&VR<1=57~5F@)}Y>DOjG7qDO`M} z$Ssk*6Z1+wxh(&xm4nXRkXg6*)`_RT^vhk|8Jv5^JpD!C|HdjX#A_hP`C!-USc61`UO1b5L{hA2&Yw5vo;%qqlmt%x99Ge1AGS8>55C{c zK;?z25KK#~FB$`M&7Af?U!@J({NSu}!r7y{QR;BjU)P-WZHoS*yt)aGh*F=KR&g$t zW&x(?Dp?ja6qUN(qE=toL)>SJ+Tk3a07I2_JVw0+pKynmH-NLFdz3r#R34p^60mpp z(*WEsE%A}i+ZK%s*l(k@o|mw*%E`hd0&UMg-gIz!pQLFxFNjks)&$F;UUiBiIcyLl z(+F2FaeT(n^2wm2gNgn&UwOU7;M>XZX@(IKwKniU~8ykDCD8Y#-@wtG~ z-eEJm&-YL!UH(!yDOXxA=d|5MYbtPy8RR?jQK*X9v;@s7PR@Kl)*+f@sC*9%RRkW&IbtDy-ZW~E z4TAF&|C)=%LiBkA^;**%yJ_YHBECnM!|f(_g?Ow7Um{braj?b141rrK6$eVZq#h27 z&}ZYxMJmJ^=v;kHmh+_U4zpZ4>jaB@vplLTaB>$Ek3z$Z5%CWI44+kku&S;6E@^SvB9tyc=)sTTVdY>>~&empJKcS)?nM6NJOEX*)N!l3XJM^ zUJi5K%<2KWecIb?oCA?=se5s-+k0>wFNkTxk-mqj+|QUPbmK_J<*k?Jv6Q=U%VWFG ziRaMvA=Kd#I`P2jqKsZ{nHp83En}4}(@%eb*_7A%-uC<&rxka>WuqYWXz;75nBLCt zS+Z^rl~;Nu5|y5Id(2m@XD}KTo6ORCbMR(C0hBaRZ^_Y21lPH=*nlccekQy)FRugo z(O}%?pgUFiMksY_@jE@rFVah?)_Z2{nM_f66)CyX{oSl6-g4WD3>OLQ4ec_yypgwC z+7bWslm+J(*|TXzb?{kq-q}YsDed7U^47X<&2G3rGeL-jRtFYd zm0PRT0dT~JwV-szNq2}XxTa4=Bd2(+^O-Q84A7@<+JS*v*~#rT7msj+vQ)3d!?|s8+wd}^BSHiNq2YhCuy({Tl7tB^H>=P zu}P1$oZ6ZC^Xpc<6lOgA?Di3@^I^9?6*`Pw@61NrL)q z+BmYz7GLa0YS^Zp$_wJl*W5O_l%H@iY%yR+jDwI*|4&7+b3idK$*ZVo%KVcH2?0yY zQU2f7CgKuE^FcX?gd{J_@Bh8%0#6#xY2!_t{w^>ERRSaG8vTDV{??zXT9N;4{1g>i z(}e4+4912|)D>CuR9f6Ose_&W^VDGmIY*`Oy#aV%ZoT}@uSMcX7#+!PoFSX>{bj!Y z->$&N##|YBQ^az5DG%SKE5wu1E^HKvz?!rQx=+S=o|*&59{@g$ecrqeY@@4_n=M1D-n(6<^8A zd!|$4Pa6_TJ7-t?Wo;rxX+!Iox3GShC3*{$+K=kso+u^$hc0gDRN+(KinFwq#_qj= zTSjN}Ro>?Y1+cAYZR?z33%?|te0R|-Ii})b_qxIPCoAn%$$Dvj!+mwy-_`i`)B%Ga z|CT?6@sbk->!J`kCQY9fy{2O8?iD^P$qJqDu=ACld=x3MxDHm5h=ED@Uv*j=XRwy) z{9(PLqPG8FNJJCKPW+}O*mY7C;6%Ipodh@TWkp*k@1x&6uvZR5tgthA|KYaJ^mHca z(%corbQS0Xgai4Fs7r7#Awv^dng@bvqEj*hjd!YD<}4YEk=1C0AdStO0bwGHu|P#n zQSr)0F>hL3_19&53?A0DUwW103fB0K;8)YFokEV%RG?>gHG9@4ltqamf0ofoLp0pO zG}7rt+uE3VD+iqm!pR_Vlpf&*iXty}(2_QE9u7&5TN657N&cLlKVI}UkvgZy25N4! zrx&K-KVj>n#^l^r$+!>Z0FRrtAC#YK3*eW|8e97%4ulIMaY36$%_6$|cpq`wy=&vI z#;LRSbev;++3Ry}d*c5VgwvI4(GolF!toI+Ru_f#K}nC+eIr`%J@)c?F1M7IIf`o? z(}?A{U^lub>wU>u1BNt>ZDSY{f^@$TCemKO<+KJ}%8sxu)y0*m#F#PV`A5w;H*{=m zdb2CAH!zdEMg}I;&n^?22_D=i>vyV`?0%8^GdFwpIi%1Sc?Lq@4g*epW&4;&G4*{% z*eWSszKAF;^5`-}UVARX3Tv5XT|YByjy}f_$2vg+ODl7i$!$W=NHZ&!gGd}5_7951 z7xI9T=2wqVA`_?)_O9&A^~_0&$~jCQC5vrw?a!UQP%DwMVaMNS{e^kxDp$_ROsrP6 zN}eL~DP_#%d?Z&&Puzml{B+XtS}{L7r)DK3OL*p_rl{Q>2(*=>8unaWgF7JR?iETMLv;QD7VI3jF zgT1_Gi%mawL`wp?agd3|G#^yK7E;H4CBMVfeAn8=kkz@)lRAayH+FSYcGH3PgDJ5p zG|j$f9aOp#!HlS_&jBYWJiIFkhLE+_m~#GR&&2MhaOycLo2*QwEOM@Gcg>mc(yVSr zomOg?Om&>Qf}WBhRKF4zyOg2$BFqb-a$2do0l3B+gW6^EP|1ZfY9-FC8bnkHO|8*` zF=Lwk-C=jh=$?yD0TyZjfXn*_qBNdN2_sU!J|-GC8Q_Ch;=u#R8QygvW+1JE64$R_ z8(hE?-_&et|3`@>K#A?P*^F@o2l--twG?E-TN3%gIqU|j0^aU?EN=0S*}(R%I~FxC zW_}Rk*RE1d3;U*~IIz#G-Z%K7imp#fj&>x}fkjGyP0^qA+3S6;yga!@*1GIVF#;w|XlXlbBD>Nx@HHMaZ8tHTJ1GsCyK3GZy=H zEX*%#ob=PO`=OwLW#Z>k5PWiV{!DJpyESh+Hr~cu+(`M>?kzeCj+I28CCvA!H!}cKdHLJXPhaTGPww|Ifvd0j!NiGHfYFx``>?B#Lba1Wg zvFVX%`gEWrKOvn-i)~g=rJuGSh2d2Ib23&X=m+D*EkGzak-FwaH?M8Hv**_PbBYt| zIJzCjskpuiDxGp9Qc07OH@c8;ajy8yL(liT$vmD0-+-Eue@I#vE*#c{Dy7{A-u&5@ z>#E|2Ga@*5;E2%hBfv!`UJ}nTj&CZJqc9w^>M2@gC6?}=z4Bg((zXAN#=q>euCZj| zi_`R332h0d)>?;nWX8BE)G6|nXa9^UhH5106_ zPwf}|{a<>E+c$-oxXxD(>UoxaAQ#f4+J9vxz4MYCqhLP+|Nkzp8>>7-q)dd;BoW4< zX{Huq;Axpq${HP>`pcl8hKwH<<7ZhkqswPxrm?`E7WzJMYh zjyRq1zK69f4ZF2@`0`<;zi&RHH3rFJOdp53fUoF9ZQCcng~dpIYO^{|>0N1Z z8(OR*AXLN1CJ#}^-yT@9@aE37^60R%+OM3={jgo`FyF^pKLKGz9!a3L^u{ph3b=As zPb)G3?|vGv`wI^_Bhm(v6$?d=<43{ig2F~d7Gu?-8BvzpJyv`dv*6tkhx#XNyWtmC zf>0)N8%$S}7soKRR1ej2`oJ9$8QBXli#B;tf^pU=MxM!)?_&;az)Q|$kiRq$PuGlFe^#DgoLHxV z$x?wGGqV2ie-!X%JQ3oa?&@sY6<*HAe)btQ>;&_&u=4sOqxDg5@3z_A#c@E}Jbke*=-o11?ElULf}Ut7tv7-91i-n~sj?fXk9pS6CV-0zdsaha~IpXe2uUZiF= zk-}d+J`99=9MR-6IyZMI?_ajwR_jZeC+-VF$=*n3W#QsXJhs`^@PHH40d_ z5TPdKDz$(uR|D)1k2bYm6;k~0N!Zygf=M`TR->&Q@;`b8=e+=V%5OpMy=%L)jxGF+ zxvg=NJTem-O#$Wu2S3Ll;bu3veEEUc{jWrAGc!4Z@KPKo_L&lBPsuiyu4TmLr#eI!ogGPgq>~t);&zV zWw*QzywW4RXxewELyKS+@gF^9;vKkwt&bY05{6Ar5u(rOVyD=>v-jo^oByYj!O z-7Nm}NJ*Q%4@f;u;e6O&K4Zy6y@TSn1pgu}yA18Q3F1)a*T`H`e6d5B2g zl!dHlP$8FTW?y`Y;02adMmDN&S$rce%4iQ;AG-bQ5iIvx6q5xMsoj zU+{Gaw#q|yW5**~4LJ4~3(iv}h}XI+Ql0|s2s>v`KzTK)I(w>%V5=wDAzQNLpdXo^ z&-4PW(D?9Qa5fRbqgLD??$BR*G}oUhE^S>kW=#bKC|~KpMdVMKy0Z&`guY-lc$m%r zg3BgcHZ2#orn<+hueI0Yp)|^J&=^G}lhP5q|%*Nce!^$`~KQ@5Pgv1x@C( zY!NaX+;#tqsj4;vqLVxo*JN{6WecOG#q#2&t^lDwMHO5m0>;=+@2uVTtC^?$Fn3Uc z4B_2|-Ie48^~Fv%R$hQsXDe8hhfOD%h0Ovx~j`2*^_OSQj zc3XFbK{e+1>ZcprG3sqpHZLPw{@A~star~57K~Mb7jBjA7^P|{9B$jTe*Tv+vumO;o?UOAvF`FePK7jaV&KRxO2Fsmoo)fjsf9q3O+^{?X{Q^?qN54Q+u#otdMgK zsaU%n`*l#{jdNRRUl0=(+Svx}=1d*IE^g)MqFsi!dlZ`;luuJB_;Q=h!L`pd zxx*S=^i6|P{yd1Ec`(vvtzEsMB;w9P@!8;u&3Jqcy#E;6cwQD5aGFcI=p7BWKnVVW zc?UAraJFGd?*Rh1<09voLqhMP+{G2xg6{rZ;QdX*RvSNVIs-O@%#-K z$6V|DSS&p)E{(5t1uYPEQ+66!49DV(2*-aH-h5Ddg&2rl_f2hUYL1GdK@48^C0En{ z6f>tb!Bn}6Kh3i|WhTb9giE3mV!!N=Da)xRVJNktVLI_kaP61idbmRmM6EP9Hn$cP z(G%3epn%t3W=PIDV1Y09kyd$qq03Uspa|p={}b16D#yWRR}U^3RuP*v0gvwM%)3wC zh7tf6Mlm&={!OjSTsXJv;0|>a`FcSxoFeS+%q|Pu_$9=MB31}rruw(ne}AJ#DKJVE zu9qEQ*NG8bZP2MX1|h&TkyE+uKnV{CwfQ(z`Z3|>82a>}9_OjrS>@e{!#l@7>9Dc>E{$|3rijp=i1ju8U@^O>jBxaHcq&YtR~+j zy)%!7)lXhSCA>IY5PPE6lGJvaWVVNF=RtdmQJ8U@mkmQux18_X^$@DN_ySWbl#$WH zNgQU4?jJb(XB>^zN6mo?S0-v3CueI)5xEx;cTp6{(Z+6Nw0HDhLVcC1+r@=3?(IYz z+Sx9=OkE-L5o>^P?u3ZTycy@v67SBJv2raL>kis;%by|L zci#ue6#ktU*Zm%8-aihm*kHv&#rg86XAltscX9-YWW)$xVP zgR)<|3>9~!-dHuxej)=+f(x| zaQsw9-a@|H@Y&8vACaBroVI4*9Opw|Ou+A&Oav%yBGOXaM$=+E-lk88RRuM?WE#?e zL>pHtN*`WAHmTUzQSNe;gralSjn%4Yxx}QMKRmaZQ>807tV>;3WM^{JKE}h8Azr=yv<(>w>aDCi1BV-X;l0(hfmwCRg8LKRd5z7^f z`;=;4j)9_O6>P9sjmKh@wuQg2uZ6+J{UU8U?mHXn1#aJacF_4>+)eVMz`jtO_4 zeEQ|YgRl4?ReC0N$ier4<~hdmO!BF~7TXNQ;Wa250iphr9@JX)JblPeQtUq-D-JuE z3)ucyO(OvNm+42;wR zUi(3qSRya*iXBf&(yLbp2aAUH2!F?O$w^aVVV`-XLX-qDE1&UV@%H$+&^ZG4Beodt?z5f){40iEhm@F|FO_(Ve&&pl)&nD{aF<9w zX1atqYEs(6ov2c#)-Zys8m5vu$Upn6e}rE)5r0d@fWhaA?EoM(`WOqdNG0bedIHr; znmQN+$DR>9qMuXseE0ebY+YY?<|taS?Y|iKvSj|JqIYN$T2}QKG*$puE6^lQ za$frA+;!(R^x3{v5%m@I#ezcRPI6WO6d)AFP%(>Oa?X^dxb08r#U}EnrjA!_jqxwh z+xy0-o2<1y!E?lD^W3o|k}I#L4%qP~AB|i`J3EA!;WOvVW}ey+`xWN{AAD~5?BVjM zZj|BfIW->nkUGyEmWIL7ZcTs@F6ED~-wB%>k#ACG31K zPEi<~>!S`@x2->w%)=K@$vL8w&mpf!?HUo0Tg1;_oBXzXc%ke8M50)j;p0U`QnoQL zKfmxF*uy*&&c*xA4^jY@o*t@U9RK~d0beD&La`)xC)-Z<`?~sP3fRb8k|*Hb5q4%% zrF(eIqw*ufok_|K;T)?UR!0FwQas;U=*8?ruwNsV!s&rkN!twCag>y;Z~s7 z%Dong;Dx_)sAA4wb>u<*z`iAwu{lj+EkIs~f`TZM;MB=Q{DI%=i9_g~!3wZadrL=L zA4M479GScJ(ck~S|LmY0 zJV0%y?wMB(tuAIOV+p@aJZMYto~25S;d+SrL*>Gg1T1Ba0oa}(i%VW*GgWl@{Q|Ob zwy^sH!&0~pc(5rW4C2GMc94AvyDY8zqy7S7@qV#``t;LrVM`LJ0g~yb--uj1&_|jsKPRPKdw!9g z31DOXFg>e^U&qDPYqfgD1JCryD%*y;h+*DVekFo!xiTz;NB?G!-&Y;(aL>kNpJST6 zS4rAFUY_;8h}01h=J_RnBlrfLgPatFS?iprD*E6ChF*^ZF)vp{l+{yMZ%}Rga#?~8q(P*ZjLR7YBP}@$~@xX@%5uj^9!S> zFdcc}kLy~<1BsWUQ9Ii}+87<2(xE_nv!N;agpqh4`t*6?PsnSBc9Y^D@zA8ynSK@h zVbbk5cAM{@EB8xzsC>tipqK%qahT8{I71cDe)E*bwh}`uU}>Op$E746xv!r$+zptl zJZ!2wfR-TE+GJx4`DH}{o93CT#bO3s*ZKXqnK&W zuyI>Fi;oOUc|R?rrd$sDi(;1G{!Q(IOHVJ@#pRSvTc$YUYS^PAk#is+(M)*-F3LX@VMjCfrqxylw_B1!oQnA;nY#3hea)Y zol|90?4WpFjvOmrE+TRRm|^h~ylb4bMN4Q9*`xQHJ*1 zD^7Z%kE5;2Nqh7)Pr0^9IDK>Q5HD{E`RT&Pb}r^-Jr4@PV)ydn(G#q;5wUzqW?M3P zT`rtAiE5Vk$ehQP{MqoR7i7ZhiagX`;Zld>b&gwW^uI^GCI^7zIe27Kp+t8X77P)V zw)+!i7s`%?AkIPM#W#PPNz{du;H&DAnauNzGgV1X(5u5f%47HnB1*8-UPkRAg>vJQ zpKRyt2ejUpy)J#R5MJDB>xi**V!P*jQ#(Gouw0UHf;C~fdjx=+BEiF7mb67N640=5 zvHaFy!Y6Kt?H{qE;_(;*!((ZS_T{6nY*M>OL+h}`; zP<|2!mB@=U#nP1csjTuNT4@c*8E>Z>oaFbXQ~(lZTm#ubBvsezit8BA42S2T4}8gRhym_yzbrf+MFnB@C&n zT`nIp?5P~yrF2ydkUwgbNj#^0CY}}yygBIKXFmOzd0JGPrW(&i+s<*9jkdj6zC)?Q zK4iQ|CRMs_)qpPLgD!n&B%D{5I3iLyjYR3sBJLX$NDt@pPJ}zZ0{6nhvrWRl)-{#V z+LZDiF+r=&?e%w``9c^Y_ex-@y~X{@#6v@K!J=Dx0-i7%^b}4(cPboR&_&iyh^(tT zY0^Z$S~sa<7#fC*c-I>@{j$k^(Y%QR+^V8*dG){C&}FC3Ud~s2o((q3`uqDiU^?aH zzo9lKNz6rMKuOLoxmlqGKh92xB~C}*OH=MOVsylQ^)yvl%|*qrUxRmkY@*7plm>}! zSj~9~IoXEC^%v)y{-*YE5+;<~%T>^2SIGG@iHscT99Z-+_B!RJ=4MPEMsBwHbtD1O zGjs|um?V^$92A8k`F(GWFphle)Uiy7q{KdR4+JppXA*~`ck-w-#VGN z;In1iVZ-1xvA*52pQR9@>E@zV@(OOT_U^9ymx}`vj&G!H@tnk=dK}TV+6p8z;v~xS zmBX7Vncb8v)GQkaf;^28+F02Gn=T}4<8*J~woRP66>HxTGF}GdbQ>BNHHhZ}X@zB~RfOFD%X*1T=RsN~N^Gd4M`xv9D zdwLd6M)&pg7?x_PlXD$FYvNcR1esdHyHdit=tHm4imFAHzTS~TLVzVVCf!^n+@$ob zglHKmSgOAe3BT*_bWSTHJLGvRGF+n6`>JcL-guOF--=EBByJWNYdO{euuH;?qFAJcd-zfk;H%wcq`7 z{?)FgM^PV)?UB#+OrT}8JF&jOTUbc&Ia;2lN_7|E8)R0ow--+tD1E}y$MaYit@7px zB7FinRIAGw*^PIJG@@9(E|-$0%CdMf9u*3nT;#T?OHQPjb6dU?V$80BdDdrwI2mdYjrmLHglW@YbCN^#%eyrJQ6Q3K{Pxt%tiQSzza1=R`vcp>twL#Dw=Eirg#zKMc{!N-&Aq{5IlmKKJlcE+}6Q2f-OMDBd4e;7FZL-h%&E8> z^%J@QmD-8h?slaP@w>!YJEV7CFN?ilWrzAGuIC`?I3Fke zogf@tmAWlfyli{0H%@;|+tsD5C~Xa1C2t&CXyv)vcGMVVW6tfDqVhT-PO!?2tV3sz zW&yDWq}9FtmPK1y&9*RnmB4eO)qIf#Z^8jxn!SUu zi0ul8mrK?%s0j0+m)x1SapB?0D!N8Z&p?pyx;N3FDmAx^`=sYyh?<}VOCK`4uU|!9 zQQuLYnD;PpV_(OMx!_N-aJ}S!DCHlY<}Jw%E7?Rk}@RRLMgK@&>z@umDBm++c zl3t}_A01mWzgX$PsY3F}Or<&b$$Desq$$mokqxR!V)y#EHizNq1N?dr@XGq0Xg zXYDDmz18l=Hq-=nr|ws(|2b(E@*5xgAk2h|Ye54X{K9{*)j7TMv!P9~luX%RB~gmE z2o8b13B}Qp-vvf&O=N-f3VgQGJmHoFekcK8m&g5%MP#Lh3ERvro`USSGFE3}!b`p$ z@$hqeH?Pv$Ko!_t_O_es$(S!#01pDwq{PR19a+y?o?-n1n{{N7{D|{X@7hT}i<@AR zSt^Bc|L*S8%Drf82QXCTv<$$g8BDe!6h~8F+%17_#0E*;wAWAEb@Ft zyZy|=4%SwDCFHz$kSqlAp!1VhVeDx|D2U+)M~tU_ckHoQW4WxajNkF#P~KJ=7!Eq@ zrBZtdB%)?iwzfN4hFrBiUT?qNSn3FxBlK!OrTjJg_cg|Hx>9zoT%tS@T%Da0!TKEN zG*vlVnHJD3ACPfxT5Eqj-`+2T@q9kCw)I^=3ng^2>B9RHEUlo>K&$c%xtv!uZYI^0QF5YFq&mY+{&h{`Z+^sTOsv)QllVXIc4jLif z@uUZ+SHQN&OYGv^VziybIBwo#eGC*BxhC^J!XXLE5vo9+{IHD6C4V>-=a-lMsCD@b zuTMwESCw5AvIQmy!suJ zNo@~d+)i|ENTjV)jE5J|eZ6fz-EDecbj*mjPA%GTHNCOuy!_H{)>ioW^B>tZSwP(a z=1%?xE@nW(b!K#lD_E^mIyOTV+2zN*VLJJ*_nqYhiDS=?#Rzu^eQaKJS5_XaC)ex6 zl1Y|&mKQ>zEz_04r`w-t^-3o_NCr_bALG#RE#k5lbb5G#7R-DXFK`J%Oa4IpSlakD zne`2&F+QVsQ9xgLjh+zV!`2I-7{s&Y5mTpCefXNp!@a1>m=R{Jb%%~?$}~CW;bLql za(T{aL8Z<@ef+HKZ>57UtS6?;7VOvqfw{@H?;LqKp!eogjq|M8#pfJfRhmW1^SmM-TA||-CU^i zLaDi~lcp}e0hF{YS+M`>?;T#L&KaVH6_yS%#!Qg7y7HMI7X7c|2}!{N_Nf*0LuCLvO!F;cX(yMen;Hgat# z_egz+%$-t+FWi8(yrK}BmW{rtztGB^>Yh&c;E4F{LZ$<$pQZ;sLe-}Fl=@iGhcWfh zKE4j|5@m|jp(7wWFH@FOu^7PQPtSUIDyVwBr3>tZB2Wiz+Krq+F6)Axz|D8_CBZeP zsCgJtYUw}H6=5{)wPQ<5&mq2V4kv~E?}x3^P1-VHWh|Hq%LWomvg-^51z~fnD}5<$ zs@TRcp+4#j7&uM1HUpaHmY!8E^R-@8ySmYAY0<0d;&YP@;exBR-*Fw4%r!3 zfr0&VNTHvc-U|lI(+C8H`A&N)DflkgXc^KnzEglDCfcV4gjP^r4D1KhW$Ml4ZW8Rk za)H7mA;9gVb&Q+V%b}9Pn5AX!BNq?tMU!@^dNg2K=M3PW#n=rQs1;Wt77V~z?R>74 zh1u-~U=uf7IF!04nf8}%F7kWn&TSJ@|A}S;`e##jD#%oFF4SHDN9B?{DPvDU>-P1H zqr)3o`=unr5y<<_hLxijhgF_2i{ARGG^l6vS`keSSb>sZ%kinaOnAR3^Wo97$@}r; z&dLSINzb0;PbWwB#4g;kDQV0c^Hr{d({ctGp32B=x;hFCC|BYP3iBgY##Y6b$6i8J zpksc_RY3V8u!TQ>lg@J*NL?hCQpOyn+Y94pmeg%E5v~AaT{{T?IlgmW#ug}N$d=4d zo@H+~0Ld~}T+W+5BAKR#3*0%tgNtW@AFb2&#sLyW_j4rO6hYewQ9MOKEr~adtdpH)etB}hx_W7lT;;` zJoyh&htrio0ZjQRnM)|S;$+;zdxhX{+G^j<@kJZTM?G&S{|HzGvUn+H=3t!RO|$Eh z#sN7k$H$6B70ezN$bo82$q!&@5(>7Ce+AzbEUS=Dk24xq-Vr&+6Mo^N%t=SNFL6__ zj;t2`ROG8(CT`vAA>+y`xU}0kaLLvS3!ghJbEldsOS?z0Elm!YAFYkpN9%@%R_2GA z+E)J!e(IxfZ+=tok=+%JJtbD?4#tO3d~u~Gku`zdLbR+kIOPfRWBLv?UcBK|B-0d+ zJbDpX^E|uDmWy;gVMy)yt*lbg2pg5L4mM6~cXW-|ni4l&lN(zT^KkEW3&$Px^0ZSN z=!#Gl^`?J5rL(1bYHDNZr=}Wl9&H%NHE&e8$X@AqGS0Tm2cU4eKGf@<+|(s7z;<-f z+ZK~^&6R5h0L3q3YS=ve>$1c!k6Y?H>r=pL&WzG?2<}a&S5TT}pZPh)$_A;ATV0$T zNnGgUm@8c12FxXL(UYQ^j0IM{(iz=T5xZZqw~|Q*5>CHUrZK6c2HX~G*W?Y`lhx*Xw&#!piXY(g!je5 zF^Rb?)fLEmAKoD_xX*WugNZm?{Ytp`eN2%JIoq37!M%dy<(F|=j*-mxQH`&vf!pHG zyBLRIWl(#c9HcuGE!pn)rjYu#)s8eK&x@$}{QMyqsY~SYAXXe?mg#FN zwQKUrYJ$d&*t*8<2Yc*kUwG~X`l9nLQ=$US%7?;y_0qj(d10ifh*Zad3TlutM$S|j z3dZgtGfFsbA$#$EzMhL^tDoOx>gRXb*X#+tHNETFU{&3&ryf5OJ(AyMvS%yrf(Bjm zFBgC{64goH%1C75RcV7`XH=SBBJNE5XI{MfzzUrNBtOAtdlFsap=3moWE{dJ zil&#C&oi``LgZtg`>r5<%tM>oIiIMSN9k_X+MZmZ=F=9AE|JZ`vWTagW%khf5B+*P z;)6>$I^Ws=KLgT6K-QAK*5Oo{g|n3V2A%a8gu4$N0$xwy>!$3&G0LKoU@waB#p?3O zkGOFscS%5gg|)hD^_K(h%};vAw2@>Zr=he9gwAUhh5nxRnHQ=5hpzVwYa(s`K;5;l zScoWyf)EvzCPuL!MP^+V5fM==NRg^-CcSIG0`aCs&K(vd zO~W2u`b}sldoejMzJt`s;y-nOu)~cjZ2WERT!^zsL%$Ln>imMzfXX*ZUyI36I#IRO z5fGQyRQRpa7+-GIXD=`mB{lUjYbc$m)u#T@JED<-usEBN@th}5c0Ai4y!d0~s{=tb zGw%Ppi_9EzHL34ctAQ@?Oq4wAPY}y_==k{4xOdE=v8^-t^d?m4SI}(sVW2BMV%idI zS+0c{QH2(MW4|aC`1R>s@(pwF$I_?#e)EACOHU8d=_vUqyFV4SkQzzWAnH%aiz-Mu;2g5>C+N1h@aW-Wva4d zJV0z*Mc<4?^GVU_52G_j+?I)5)m^X5ZoIXbxunq|F2n8nxW}8f zt&$R6cMl#|pmJWMuNSd3D(`Up*UFAN^XGhPE)YFh-5jub@~M7c^~C9^%*RmiblmGv z>-(rF}@0P%hRi{$0gkMr(3WhjN3Jih6?XGXSsi z8s9zi)7oC21V}%s%sKk!l`Optz+BF&dmA8MBTvmfz12dW_{nL^J1~MSVQ61T7)V?+0nzyT7?!KBOrb#^yP90J`qG`2C z;qjBGn<>Ag0_2~=;X&z(*k|HgG-X2Q~orBd3Kc_dwa@bNu5N z(vuQ*i|vliLE9H>F6_*iI%1JgOsQ)tw)aUe@0%`M_Vczo$B%rE>f4-B14SI>#oE zr;r;Kw;6|jov-rW&*Eg6J)fF_=6~4&md*Tp?3Ousbc5)N+XYsG4ZE38ovD-VhR)wV zEY`WqkS7eQosWDO$fU;}mR~X7bVa%RfB6L~qF_<@J^t?n3Xf9f)*rqzqWgX-eV%=j zn8mg4avjM>%c&L$0A!pmgPCASvPA&mNT<=e)c%w&d=VqhlUZ;Ky(3IU|Da#9B#QRO z6XK`gr3SJ-r)2BXqxE6Bk3&dlDk>|r?wtxbd}q|=HClq0I?s9agTJR4QaSDx_{{BD7)Gb z-7oBJa>|xBE28WwTVvqwpW;Pie6@XsPg3HTMyAFh#wP1v=@&=dVAiZ-1zGgx!O{ic zQpFMGWUEix65Ks`Ij$}CCw|Eeb|(oy>I4shPv96Y+h;|jvlAda>WZ9~mC_W<$&*lF z;6}zI_Gy9c=P}2AQy0 z6LR;DevX2lC&@xW)$n)7Sk`-Pn+PD3+(lnw^Q8(Oc2C7WFFT&57B6{*zlESq(A4M< z>Mv8OL1%my7tVrLreIS046R(!)z#5nyNTST96X>*sQ zH;JNy0kfwf2`6I5IXQhDk63BiQem{!HO@FAZ=OMOf5xe$2k5mHN5oTw*HA>%SqgK z`x$y5n2?@`NX+|76viZr!LoCqK2Bhcq|Q1HGI5n$F0s>6v=E)Ws|18xz<{NKdy}oq;^87wH|6o1IJl?qWi_ z5AoH6f??#V)=3v~Q^qz+ssdcKN`05S?CPP``d8#p!vhx`zlH?XYy6({*?oP`XcDvw zD3MzDFX2kTL3e9{adu(w;pRp{*B-O}1}#g8gkn~Ye^*;g_xi{_x17*`RA(n*q)`t# z_;Tp6G=2D<0ZxK{J%fxnWO4!XrdpfviZ@HzB>tdgH!C}GQIQYn)~2;$|Jdy&gR;T6 z{u!wCm!6^-m15;m2&BhT*aoBvmo%TCI4dXv;=d5@v;pW?IheVY;;(ASMXqxwufvXxE z!cg&3^o-8}jci~0Lg;If2|@>PO&xKcjrf2C2_y9k_mwqoNEYN1K;blC_v^4+?J|Mo74c7|K6P!&*x>{avXx zn}vo_3&|oIa)+=NfruEk9_%9iuJAD<7)H<{xVH_h)lG|HuYvPOHbwBYP|SA3Ujbtr zV@Hoc)U%N@FvA;ft9za(T$Lzd$nA}Bd=(ZT-Ar%nq()k-^13)&G(BrhJugR$=+c+% zlKpM&*ay|?zvO6T_h=p;l>XLMd2wdPj1c6}F1s{0dUMihiD^zjx}mY`z!Aq`Ca!Gh zD6(SW%ME^GRvWiAI#oj=cKJ3#d%QHyx?cxcI<4z*8z8!MuUhEZVabArfovF>iWg?W zJ@>{aypy2CH%bVVye723f}@r)Xy^8!kGqhl0=rP6m<0u>D&hkV>-)R-k1StkBsQv3^$RH=%JqE3%(L{WH~h;$@J zK(gFjO4%SzxYbe&)&^y}XXW{n(}5 zB}s;?szvRSO}-Ss+SNAw8i*GEe9J;llnBftxl5)~2kDzR)N z&zs#5a^AuFWi1JRIi5YyMJQRwBI(-rW?2SRl3Fhi{$Y-=t`6G5+Xj z80@*i?;XR3hAb4yuA%yUV{~fDH(&w%aZtx>eA#8Gq+6CaDGR7UR&Vwz(MjvJm1XdU zp0oQ#vf;RC?DJ?iyJ&t}ByJ)ytGeSuZQn`v=>Hma3*yBkJ|U~wFOO&O{RJ*)k!2<^ zOEyi}mzGnf`a9i`=qCTs02ob(Bt#UbwlxT?n7-rP8gl82g@3YIGNeO_9f7qLg1TX{ z`Kw!KCjHiF*m1qJ1>(lAd)UnzwjeLE5i4-*X7drA!^BOUk~$ z2d+YvYasy7t{;@7$M%00w%=t9}gUP_Vz`V`#1#Ne$kMc;_GE;&}(N4 z6$vX#%r>)9Ap$$YJj?l>CKJLaL-5(cua)Tpx$xOlniR=KjLdiOTV6q|jxb2Bv!F~J z8v-u(g`#S<_~2--Bnx)j@#Wnv%cw>d|8B+~`bgo^t1C(Q19 z-W%;u5GnJIS6!O5H+ctB#0ZexM|vKji&3~5ww`>WObNR@yL+Q;shMEu$ck{nxlKd5 z8$eqNxtI=R?6tVIMr%D5ua$Qp3*XYZcE#TiNwoZXaFUh4?qMJzmr{~M2czn|BQqq% zZ96ZM$Ctq5;A(j!b8HCh$?jwTB5HD<^$m+vxWVf}5j{mzeB!R%MInr!keJZ=CK*gRR?%&-c7`$eGziLYMfZ${WK6+X0GxZ`lUAAv5$(c&X?L@42Vj2MnG1& zVSUK4C$}~NoI=|eOOWuK_jAGHHQ#V6(QyS9e)Nq8{RmqAnf$^r2H*31h*?MuKcD-@ z^WM8Fr?i2Op116Cs2Ih$lcpE;x3BznPAv8D)E~*xdAKH=Y9v~|twc>UXjw+oMHw1V z^6D7!^UQ##0ipO4f-kS#Blbwh5^G5J?>@)<2*x1w4XcieX)|#(ZA%$6i1?5d=!j+Z zXvl6Dfo+&X?ygm(C(z4zqGP0$^zJv@=pDACYsF<`(ee0nh)gUm{S7GQ#Q0HS1J|I_ zZf9PQ-1^HJp7elqIYQ4Pu_11Z} z1Ca4*140oqtAwiF&Kz9TqK_l8_w&oB?(^U5`vKH?dZEM`U!5jiRL)yoD7;Rnida!+ zUf(q&3oHVWsgDKe4GD$zSAdRC3q79PcDr^WsIu3XDXr7@&1On{sZ4Snw}wQ9gqg^Z zvIOg^J_5t7S<7rUJZQ2cp0W2JJ)J1_tHpYHW%qWB9GY!y*x#D5=}JNX%GEo=iWMRr z=uMvJ02$Li4#PcH&J=ZPB)Ultm`27C(W=i2j%Jbn+Rf}jERmjXep-s`Q`E_!4GFeO z>_4#}7T43p)e3q>jzpBrNny`sl1{ROr=bqYF&!-jPA+x?5w(*3Aq1_kkJQ@Qbg9(6 zDu6yAD2RdN-Jb8va+^6^M&$Xt8;%2RPFh9&Sm5-;)(R6F-4Xp@gDIut?joZ8M$24>SHGb(I&d{2_PRc)4yg{U+Q% z;hn~)P|I_S`o`j{L#-TVp%eAzc2fIRfH<7@Ag%_{#7+FtvDE4!+VEDg!St~k`UahD zgG_-rBNh(5g9^i|*JFi!(b1zCzV{6Sxtl+G`+h^a)VY?)0J@&LC+)<{VdhG!)s6C_ znuQ*`kJz0aM!38(?C>+J?|7n}^f-e^7f#dtXdLN5Uv+>j`XelWL#UF)lsDqYe>RRo z6>FBS^X05Zf`HjIU{G!yq86%+ZSxd4x{6me`3WaAsFTp0BYlREdq{9)PZn5rQRX(H zT7Kf~OO#t$+r_JmW=s!4%ykocG$)iTn2}o1?CJrsz7vT>Jk2^b`)@7?b@5Jb0+ToK zd&x5#cJkUK;!7egpIl8^7L?7AD_Dv~P^43Sf|=QCG<1_gQ-=D{H#hE@Q!44}+jcN8 z-Akp@Hz!Tb`)?2r-rw<*H(onkD5HRxrQ;+DL|b&k#zZ`Jnf*^PS2ZY`P#@^)BgdK> zhCYROwN#Mr40iq|b|{hkku}7T_{bh@XLs87vdht1oz^!BHP3pq)YN>UVD8&Cfaer&qU^7X4i*^4p*uAgy&7~cx{8*3RIizr)# z%?FyYlszmnIT@5Maf8cUZSM+8wQUm$3IqYiBV{X`?|*#m<3_F}e^9$0O7Rb2!f#|i zwb=|b@|dTDda=5+%P%8^C1gMGxo}cP0eNXP7@WW&4q$FLOQ9_ED-hf$l=af-a7~tKRVKh#SgjJ~qRJksS0n*d4f)i;cJ@fmJUnikUYUK>#OUj$!h| z)}E56PV#bFqDT(?^YGXRQ{?@njn~y}K7sQiWE#kx?K9qLf3V;+c|4Fn!YSTFB z<#(oa$NTdz{LT&J{DN7Wo+taKZ2JgPaY1A@cGq5c=CCYxCE9Y>;n}m8J0oP#rSFUj zo1=TeE@}o$qvyf=$cKpzmmrOeE*uI#%N5OS5D8g`v8Prw7|cw-=MKsud!Y2gtembdmSk%d zB~tLBaT-tG35AJ6)OuvBFR3K`g}<>bZ8qO_ZB%0Sqx_h*0=V>nIQKM8dIa0n!Wjjy?u< zyxVY);FTa$ZTMg?Z1s&q=1cUZg~HH7y@bIk%Mas^mtoqI$MkA*@ zfj31ym)fKItYZv6;hHY9vO<|bK{1J#S6 zI5qQ`!12NOFs}z8nHz%nJP$hpL)X9RR8Z&8KZnE@_RAiyyVSd1Z>J7J&Z^ZJ7h?X> zeLlRosE-Vh+-NLd2@HQ5nUL=Fg1LPF2|CPxY~?a-=q!Rz&E~8HfNwptBKLg%-?l2M zm@?Gn*%kvR(Ni&Q#H@I8uWWf@HT4YfnW&Ss#Shfysp>1_2Xemzq33y+w?DQ)?H1xy zD^v)Ju%Q`KQZjzb59@>GrUjwPfp4}p;8OC;C`|5ZqwaK;RbjFo=2b{XP{+u*xCx|O zR1+h5>pftc8yKbMU$z10vvScm`WHwSI8FRQpQiq7t?irUG=?C+31)a1*Ntbbd(O=V zCLhVU%=@*$oV+`xz`@6;a?=G{3jQ*x2oU(oH0q~>*jiyMjCw7KwIAzZnfk-PF;L(N zMnS*;eGG;v{`X53Z@CMc$RQ!JKx#0a1h_#;$S)*0>67o9jF{~ zgV!9pec1UdL0u0T_0U9=UaS_`-(12KkjDT^T%}O9h^TFAP2$ubF-fJj%REZk7zH!y z{=TA6X{x2in?6QO9AVf}>`ssHX3(P-sVcqf|0iZh3aj1KL48r6LEwhK-l<8 zfN!qlD3@0_LHHKQ=wTxeN;Tlis4}!Nze}hOuxPak?y8xO6@-9@wxhgC!{*FU0G=V z^THg&!|vh%YH8+7LnTpXVJ^!i%xTK6Fh|fCgP4uGNXS1K*_qd?86f*3OEn0((MQvR zvgKQO3;#V&yk62~oS=R_Z^WS!WFwIN5ORhKO$34+qC|-|D1CSPNaST1D=VnCb=CRj z-wPKZ`g$;eAqXb?B=)KaZb`QiT}cD4!CFe8Rwqq1a)L9$Vo#K0c_b7_3iiqP0%jhU zvEyVYv8Xne8dM{#jg#&l>0FsRI&FEZ=tWCGXw1$5veBw1db*PFQSrZXB%tiBU4(jD z1*jjPKxhrhA{DRzf$8-lR-r8KolN=M*N26ZjyI8>V%{$~^c5~0Q1uL5n!*xq!+(m2 z55%X$X-5aE>T^BM%-;ESHqjzKBQN~JS#&a5{wJ}{%k8I^;{|$wGf$agOqhl0+}34; zeq6?pB?GfGt!3?m^?#+;bR(h|UNquIiJd*0@NkoO@};>m4a9Q&o%PhurTPcao?Etz zS!0k9>HEhjZHm|&C?h)XY<0n17z1}lc7Tc*3dd6WNS%?A?XQYNis0%nB3JutsRTlu;q0XrbGm~3ty?xknVIEmr0DBrw*jXXl3&-Cf<<@DqH)hfsO;Pv0e8;IS!z$gDh}(`1PGurJ_t`%YhIo)H z9S2&TuZ&51&@obRrNmFc=4~WQZo{Y29}Ry$Ps@OJtx5p5!jO4w%>3Uu-L_fIZKF`G zuxK$^ROhA*BahhF-hOz+HG5o}SeTPe#j=ToC?Acn_yvPnG#A^3bJ5cM!h;Pkn+>@xz(-re`qn$j_)|!XvOURKtV?m{OJ85=f_cDn23zr_xaRIp`FHNd< zZYN12K@l?VlSL=q)&g9fL1HRm2xJuy=dkMSeDZ9uqs!Sb=>t$85|57uxzrL=4 zs7vqcbgGqPXS4KQt9Uot@+^Ww?Z26jFyr7i^UAQLQA8WwC_VvRf7b4)$k&=5T z^GQ?Cbsjy`*boIci$mRBaRtNa;r zD|w5{7ST*vr)pCa9HIst#tS-VnOPaq1}02b;Gp@;N)+2T9XZAAw6~3R5S6&87Ea`B z1OuV$7)>m)uVNQC3ZkVJqw0=hwR{)y4eAoX=|t&HFR+RmbDw}5%X8czJxMdiilrey z7KSvF9$`3LqoudieDxW|iN;HdO#IRkO7bOt$}-tKnF#E-Cqy@X$mA(OQJ(MOyvpp8 zmD0>7>CYT=I*#d~K=%dpw z|9s2V6LqtsMech)qQ)pJHBiqhO1cW^> zUJv^JjQfG?d5Z6}P+{L%-+E4Je``Ao+y;QjEH4XkBTbKQKqYytcw%5{pmMEsEv|g^ zD$d^DnPyeH-0QdY>)iP5_A^%Oa`x<~9E{Q4c}E!P!FZV=3m|_iLmH6&6}$x#Ad)Rs zLXrFEUY3pQ4e-{VGN#3R#%kI^%tI&Y618B{xmL=ICsaxuG(N!=*G}%fT^-8617sws zn0l>5v(InqBuh2u;zvpM!53dGtLZRKL2{W4yM?RyR)kRB`L4}y^2rY_PNG@F3$--h zfdsTOC8B;#B!%tHUWQhjW`60kBz@0rLVB1yaF=QzRj@i2fw^Ht8*aW7!%R%a2z<91 zUeTLtR$^+t;#so=0}BtA{z=;gMyh`=SGHLs3uZ8S#VXxuvh2i)bZRkLvG?Jx;gAY> zpZz3|bqKpg!i~w5maz(zj1`{H+Fycz_L(aPth4Q-aAZ0T?XA=3Qo}QMY%f{z!cYG*YKtwvCXgpoWTO zB07FmCx*~=b+Ph1WzIDff?L6#7C>TZ-Z0d`^gKWd)3c;DM3w2Knk@dPz*b;-nfa7F zWpSQeI^oO4%;1yrK|ozgz)v?!`ZGe#rShqft;Kn+Bgf-9!;Qb&4BWawlSvJTE^+~PY>_C52iVLqsYy_3m3m|( z(ohgawTY~Y8qseIY~7f2{dmPu@s?H&0vCKEI=}&-FP&awAy~47n+tE2j0_+>1Iac| zM7)aN0far#%kq;Ylr5m4ve&4O6ouxu=b}yBnho2%0(xXhqN|&ZLFlc#oTpd@=!aLz z7K+ev?ltw|o1XQB1Ol8;$J>^4W$b?1$iwxRQ?H%zAyHY3YT>}0ohj0er%`g8 zp9jpm2kZjRN(cmO%#$9%e+6uMcXDKjOYo*cV3nP#g;=NO!T5dNA8WGCB<`hW&_8Pv z6ei~sI`kAsX=K|I#}`&jBGxKd(!fO74*Wt&FiE`P-EFkJ@QGu7hJ8hRG7ZlbH zxgX2^?>9r0!>)rXp$=gVl=EciK2lmFi62M!zXi!hIX;@tC8BnWbMU3UpTW&Ce)S1~ zTBPgXNPh%e?ZPQkFyBL#Dxy~JbbGS|!pQIUmK}jpT~cnCG3H^uz?DOa684d&C2#Z~ zOwgZ&9}Rn#f~Y&|<_2W`4_4v-;1&K~|I5dLsvJ6Jtq@{^wl)_W@;bid7ASA3gFNJV zH)?Se<2xzMPzCB_R1SH;*w7!NLr+_&+zqRod^GRxC6Tc$OH>Zc?Vm&1rT#mgs~KZ= z*qFCwvA8kz(J3sW>W$8|Kn?CPEZHI4zG=+x?5h&?o8;Qq?(H+*?tIhv<*&tegXTVr zA7~5m|5*EOsLR zAQ6Z5rVw4an(h(0iPLv&V&~*OQ_0lddaq9}z=n8e|GI1C>EY9=&Uphw;vF5_m^Tj@b@_W4n^Uv=sxwlcTj1~1bVn>$ug*u+nt z@`7DDA3zWoQ^-OqLy77z@cT%Sp2Q%Gs55iW5s0NL7WYYVuq(GOi(?>YtI1j|gGfma zoclcX*^|eSh|5Mx{A*1AiZV3*y}<2dE%B^mkv$xIA8ik&DlYyVvzDHt%|n-@m#vd( zesfrOsG`3A&2{;Tb5E4Ko!J1z!{|E;uv8^~|$@1ZY$6^ab_AvnKuVsL>i76*A8|g2!wUITZ zkrDia!4u7!`<1Qwtqv~?3P}nzfoBxWH+OTi8C68($lfmyekuD*F0atD66Ae=d{Ca| z(>TejEP5+fa9T7f7RLY&YqC<%9>6|1L77YMcxd<*GwWk06i49R8Mzw^;yH<%9>!0v zDUPn?blj8n96u7Fl$?KC4rsj~dU;}V=Q1g5n$~{Q9~?{~mdoc7D-x*q<@+EH2c@cL zLkDgG`dB`lCDt)(#8~}uitmOgNlPqcDnoTfD7GgSDf9`|aIaPi1^Roc;_|Abh->Fe zQ@*?;>Ff68=pCs%+DQ=tgt#Ru@zV7lINGh_^lo;Zr;Ui z%qaHIXc@fZ2@V&>m+v1<6*=)b{mYR>eX?xaOuEhaB#k2V?BL%UnJX}=mPh&p{_W9{ zk5GB=2d?xItLzC{(P`2@cV1a~f@ZREjOHOHFW*W~sQ!?sx%%%M55{V5^yf0opkSI!-&e1h z%SnMoq|MrH@&@fqj2h%Rx!?x-9ojj~pPzn50(i}PGKa^B5#6WRCun9F+>Zp1^E|Fv zqIFQM{1C$WMAH&z0xP5VGWgsZOm%#1uP-JjYiu=DxRt~5h|pSI=f(f~4q3ST;r$C= zq9%?1bbtSS7i)1_VTz8cEVpdsXo%*>7ng3Dk=XTbTeTp9$LA4+p}xy*E14PwV;>J^ zto*Er)}=a&)5iF^VW|Udf7- z_jZnvL3Pcq3hU36== zweFb=eLwnmY`8w&$vT-Y`<1BYY0lx|TZ*d=hC)k7(0TN`+uuY3S##SsQHd_)$T;RZ zKnaA1SzT|5Ma!io1gFUrTeswhG_+b=sdMM4^Q8&j0bT@B%)?h8k`xHz-?%Drkpwpe z@<;$`5n;HO)LnaWOW@Pjr4LZ@#Fi87rX_bOIS}~RCQOh$u`qqnC(**dG!Ex3LC3qV zN`I$auF;TaU^mh4#sK5((pFAg&hCLwMM2^_#x{Oi4YMO;@OSa=#dlnsPBJk`0^62Y zfhA2;vjP4_)hD6*!0f|h)v5-RYvi7l}7tBOgS42O~UbZxw_Zo6|J`Y{Nt(%XpqYYF+us ztn(f^?mw}`MRE-rQ)9GW>DA%q27wQfFqN1?Z=o(NSsz%-(gd6f^ZEj*!1rL2#*wn- zNyW8=36k#yy1(D@%d%Yy4U3%jjM&obK^A5Jy=jBhRFOUN=0kOn^$kLmlav0^ysI`V z11?IP{9KydXI)iFIL&0QQ3g49=HEH@ zi_}XOAKbV-MVCMZ!VDJOU+1X3>d=LQnF1w0kZLcHcpEq0FZ_@#PongGVStT0$bL7> zz%3?v=Pj>ms>pL3vZ9CL%frW*qLZYXi$JQ-$SUzqm3)#@Czt(pEJA4)%M{C(?g0=E zyBU%Z85@XwfG|JElvs#de{DXsXSxMd$o^xl(O zOZ0S2Uu0rxEqrcdWei8HH95waJYN{4;*{cPF`Kn{R%Qs^+;$ zuJ1_o4bnexk~vzjTWs(Jt%w&}Lo0PAQp79&322afi;8*B?7)~C=|@koyWaAdf>h~l z7u>yZep*3MgXf7t4jz((O%Myg@tr?ejM-%IFk{JK(RTX!lE$Zh`#4|EG|zn2vI!|D zHUKG{IHNrECI3HlU@;me>BMx!DV^l1C&*Ydb`Sk-nu{TlEX-QGoU@c>QlBo?e3%z- zdPjeW#ahu$R$AmWwObp4x@TM0C!URax9fvOc{LpzrE;+CdwLONnj`l5v?x__R}{fq z9MbabxFo6QLF&gSZ&NGkQ;RL^{6q;C^Vq&{p*=L}$0*OuH15`_UKp|L$lb#UT$TsD z26Mho-#nt6@Zc-0l;rWxz!n=pE87D7Ba;Tyd*vN6>${jeyOX`Af5>2>Pa=u>h24`w z&abW-lzAt-dcpab*gC5WkYG^KY2%c114?+8?*DC0Kc`UBDJ|aatU;qObLL(887%1Gm6tM;b zz}!~B`lW!o%GUgM&QOiTIyj$Ex!_`X>=}=>GhP|D>N!2%MrcJx1_Hy*MLNqxjyPTR zxXdE%j&w223eJ=pfy`X^6%cgHl(}ORu^J-L5b+>xKav3?7=*8o-IAAeDmqqub(F1x zU{4btN@Wdc<&bsy2c9qms$rhf5o|60t2TeIa>n}mr6qrmC$oI*y=@ckH$r{yxx*a( zz)aXZ5b_q;@Br5_(Q5_C3M|69s1OV>mnM6Gg801>avFg_5oSNu9<}Zx)yT9ZCu9!` zNw=UC-ct2L{dUyY;Q>scH)bH6Csfs6NwE?bcsu5 zwln?n(_qOs;~c8S&G z-|@DOc>}TpKz^MHHRNAxR~I?gJ$m&H9`E86)om%e(ArEOrA1pRi__#%Jb)*v=V`Hr zstIir2!@QmN+s0QF2|wIqM|P8HG>KxPizkQVs4x!r_)Q+PVK%7wB5Z|2Oj8$op1#7_Z5M33C$AU z!T#~sY%|o$9@DqSRSYEa#OxcmwbW+Fd1DTr4U;}UH8{aGJU=$;38iOX(?Mh9@+QJ` z*tCT$(Ik>3JhQsqwZ|+@(0d6SRGTrz?nOi_k3kZD}IMq-- zIY)sG?Al+t9jvO=ZLBdp@ly1QznoE@F^km%ED=kx2D1$it8r$ZqF#wH+YUy{Jemt< zq`|QHim>_j@qMzS4Cp#0xj*rKxcEhLkf`d#%*(G<$Lu?rp^Ht<*B{e8eq-YQv!HUW zNM48&Z>(*im|u|U(tfSqRJg?dVB@zIe_@G**ts?|Y{`RlRz)ov{$?ecqa%GZCEu@) zk11cX(Kea8V`fllj+R@);BiQJPzuEJr>jYhkv_H(}j)SO*<0E{iNIM{pe zYbyEX^Sq;}>W334n^}{gH^8!vMwE=6cy90(@EBn5m~`UJ6Ul>cSc21LzsD^2vs1t{^gPeb*Y zBAYPV53*AFDxz=tLeMjcOhXktR!d}t8PNR2R*{-kDvqlf{}+gm2P6JJeh#0s`^HI{ z;{hd^1uyCb8W5kwA9OQ^CS;jE@7H}2{u?sm`@8Jfe%(3$*GrN)C~$B<75KlBlbND` z!w<=rP9djbaGc$HK@9}33-ul<^3NHB2;qE1(QAvd7i}T03dD0DdkQ6#yk0&O5{mp_ za|PM)d_U?PXfm70h$I&3^n|X|k$uazY9n4d`cZlwoF|^F5gzZ8h7TXw)16`e#vsX~ z!Q59uC=e_2dtY&>ks-j(A6EJLN@B4=KDC3>lb$CItu|J+_DemXcy3|?$EQlh=HjRA zt5K;=2#5H)j%tXOSCf}Nf@-ei#Kitp6FWw+^H*OGE(Rrj&GJc)&nawp$pE?3|P{41Q*BImJ1F))yMFdo9zT*}T;ZQ_>(c-;M1!*im( zlRM@jJ&NU?v^`W*0vHm_e>|3rIaI?Yq{%ZmzNJJXwnyy1A)KwYQ(vNl!IXimn*oN;~svE!4G5Qy@h!F*zVc6zf=E@?4s)vRpFu&R<@E_!o zD*echc7qS}W~A_nK-L2ecC;(;uAx_~(~&Ov{1CkacoWWT-#4=SPxn&hdAyb?@)P}clo)3pv~K}eRcb>I}|*`4kF^09Dawm zRf}7T_JAl#v}3t9y<3s_D@+H=u13yZ2%;|-CNA$pD{4f05m8!`Z|5W~y9<)?PgMVy z!-;*6<|HC>+22l*#I}>0njxg8Zz6SE_oW{(0ba;-PHcz&9{oNHK<8IcXcUs=2Vf9MYY+hbQ zo6RR_y;6k(mx_y_(^cf&13-z(KyH6iS_a1PYKRonVNCfN5g}B}A`zk%_Orz?@;`p8 zewz4zimWId@+shjYSq;6Zi7}qt~OFukU;%vOZD`#Nv#=~F(x$Bh=pgAp!z3=O}=dk zMz}_9G2GTFsXNUL^MLz|qLXEbxS|rXcei@mk)}prZ_2j=>t02fxPMWK+IJqYAMs6A zzH@HnzJQ|-$Z5_OA>nrRlfk!L&&^G~@yD|iX7-iK^4L9b=*vMtbZ;2as}MDrouXZp zM8$(gLw@geEgawhK5;lRpZpiGp#wjPCHJ00IU#A+U%yPQGJfb8Pu&Z2QXzX4Lh8tX zK#69KCCuHrC&W%bmRCNn#=4S)n;G-)r@6?pGwh`QW+}#v?C_KPm>Ycr@~y?!%Czw< zuP__bcS8bWH>=~iX=pJ0s9U_agf03*IHsdi?4Rp(Iix%Jg6|c{Kj#XY(cpnF-(gsU zT&yNvf{mLwLYC=i8d(|6PB^=h5s6@zDeM^=>lUySx67A0hjz5MNdH@ZkbM)7nUQvY zN{i_gLw%)piFLM*vNKFh49*HpK^A_etHarvBA(<9;~qwIiRPnG^&7!DXthpsvR_4>9&mM zemmj6JyGmeH%XmYR%1F`azUz$miw2PIDNrFd$|Hd{^`^&mHk{#|4duI0hIUF$)Mqc z{%7;&SaM?jdzdWBfc8Dz%~4 zcmKrXL;k#j#E-~JbJbn4n5Rks1MA}kTBtF;RJ$A%#Z~v3k=v)s?>xQ2^xDZ3y%L(g zCb98-*;7HM7$x38g!uUiqogZ8%=*OWN8JUFF^G>$Amyxi^hW9&oUCxQOqI@IFDi&J zB~Kmd#E+JO*|#BxB>e#T5*aA7Q>Yfb zx_j+gG5H;@L*v~WTHA#)CtUhAK61y|c?RDJ zhjBWrqUQO#c-Wy&H!Ti3S8h4voA4lvN1f1Li5+5Y_x|BZeuSEdIYE8BjukB;aO({0 zc7~nfR=rul^7DV@wU*eEqcUVy>8m-LF3w!*q}&dPjAv#@HiRwjAzf!`TBy>@ znS5%cusxse7Weq86)?J|*zwS9n<=nS48yD*nk-<(Ps)7lbDDg*Yk(|b`gD<*OK^-_ z?nwYM^5EB9T2Iu28xl;@ziYE`!j6X<)>+q0AR-%QEp2%HJS9pfDD+=Ol#>^;ayJ=T z)Oi{XR)5`tzbZ9pFc3ahdX%(fx7~*5r%cfgPbth3d?E|^ulL2QF_~0{womrYR4ro} zO#@z6|wEvdTBGJ{Ywc#eG#Rrawtg0+*BXe7_N!tY#r>=ItpXvHvG;f!Nb z``p*5bDJBco;kArBk!dDk#~xz0=~?$qHMMJo|`%7qH8E^d(v!|%=RRHrupn#Ibhc0 zOM5AJDdh4M5Xy|K+%=i1&{6%As#-Wt%P~WJL)cGON4yqplVALQ#J6R(jx;kN-<-%x z{_RHgOYPUcVdKl0J(J^h#G7D3-$&~#_%V0eGH5F>1939MBbVb_bAb{Unv?nVq#)b! zH)Szt!*oq?PEF?b={D!Hm9k4Q#rN*WqV}|Wn6N&eK5L`+;nRJsdml8nUBf14yk!@+ z#H1eFDnIoVMoW+1(B%lh&8na|@d5b*u20^L zP&0V+pBLi=Qr3>M>erMqt?vby9+@bUW8JK#wvGP@_!9!*a z**W9?NC9zujQ0GH8NH*_cJ|4}?`6C+K!lw|SFX%^6m;Ns448%gFjf99^Ic}Lsu*FVJFM+nON@R4U&)*_OC^oVB8 z;9JB?)o^`Ar8ssw4|8@bqxQ>gxpJPP7l{udqA8 zAHbT}aD`1O)1rmG#=e3mB!e5rxQ1^_4WgDOmBpOS2=c6+;1#@d`|5o${H3kN>4i|? zyQb&0h5vc=Sw+PUwckRHtPfO3s z+=Jc(xc!eVzA>&M`H4{0(DBB5vxO1?2>pS<-$pNysttI?6GT;RTrc&nG6X*zvkps< zE~Tw&n@1hln<_Sv96cjv@q3F?F=zols2vLO_i25XVkxfrF zx!tyBEX0*PdOor$n!5?~>#{h0TLtAdTd~ly%|Zj#RS1m!U@_IgtO~9ATmF98IYTL1(7qJba&KCu-{}j@e<|Mst!X;pFCAFtl7Qpkn>+Hl)EPju7%* zT-V2bVRH&vE}8T2*Jx{%#dc_aZ{%+@&1jBRV?zQhG6gHMVdg17x`14KndK}1DB5wc5>qLjjjiYO>4MMOYEY>F;qD+nkk zDN#V7Bq|~*LZlNxNTiK|7y;>#&?9|96L!eXt~aAq=ly5g^X|RljZv5sBs)86&9&BS zzWIGVsvpiNILDpm)hEv>b^JO?>srL?Sh;W0WT0R;vAdbofItM2aG4j*nU}7~e=@cUTW`Gg|%z;^VDC>aO^0^EZ6i+EMGDtu`~N)&2S=Q z{Me&pF)%S15Wo{8YJ_=h{*<&vmc-CJJHonFF93!ncZ0@@VrI@4^mQASB=m#6Wy2v` zd{N$Fzl2<(sWOn%P1-53mH%_#NXxE9aJi?`y5^G|rCe+Q`d%oz*v1>W(P2#<{2Bn0 zR3vSNN!{Q@QEas)cU{iw{awc7m^KZ@@Oa$Wotv$DESXm)kHUVZU^gCn*Ic37O8(C- z-bwXeWfk~U=K*#%6khBlWJ)We63Kd<uZJ{ zjgCw8RBo+`>F>JQ-M9h#<`17QQ_`wd4z3%mGQB)rx?2?8fAH?Pf{``EZmQ<>2PWzg z?J|y*)heO|U0j^7yq$aP`nDnQ$fA?^z#8W!)xZP?MTbMAuEeJpMC641;=JbTlVgFL z`puHyQPmO7HdzAQu7epKD^N3AD&2*#3v=QUBAlN(tqn`=+ww%%^x^XApP?7lM-KUm*F#VyFeB4lc@cFWYy3JKS-%=aCrOaEBTsfpITbdj?sNvC=|}YWOa6HOgXNY}^mYK_DHz zSe_1#R*g+htj@Xqb%5jV3;mni+=%@r#u;A>c1G2t_5fR=6RL2WAWd zbrx+A9YA7NZIvBRh9H!ovo?Ym61xumCR1A@?m_! znwyEA?|*g55@a&VZl8ywUk<4xWBJPTM9j5@C{%TIl-HR=Ev;+yQ6Q$m#l z)tU)wZ1*TnDMPw!&)NlyvsZpkIOxsa`>ps>L?^CcBS7mC20F~8Ob=3}mXBhvly2q2jp2yhF7a^;7$*@?DhbG=ey zV}}*t{0f3!n{PtA;M|6pAMBGJr0=O#41;~+COXe3MVoi~8NMw49*OU;jUAT{nTVP5 zAhf>qtGm52T{rtr>mHSK%=yLcYqHsI!>d&`88S!QOQA;Gb>Ss<;Q^cc-S)&R!YUSb zkUi!)Q?YewnaTK1vRjPl$NC*g@=6X=LSkm8bnD@COHetI*$5-An;Esqkm4FbG<2!K z^C?Vyf_DeURhEGMPK%&r)ZJbiAw)WKiXJE~bs5v~r6{%DcR?mT51guh3R~R_=>?V~ zmlmc&^g;Ck>K6mL4QF+Xo`1Q;9U2*3wQYl$*1hXoF3z<0!cn0c)^99(oGd|A&PUxphjwMa9eZBSaFw$^58uRdYdbD)r z9${x`SVgGj74#`aSB!RUANV@y6cls2w<|I5oYw|7PA_{3g5hgihsd}C2rC>xH z&AP77`P-knnR>Fq=*r$t={Y%STRv##H${J@FP|fvrqHWn9`0M;Mk{Lp*6^f}+w(){JiS{wXI&|JxM{)zg^oyCn~viQq{hj_(sS866*%hL(<^Kx)(G!9M~I$x-){5~e7nCaI417TU*NX_RFj`pS7g&m1 zVT}N)uEWff>c6M4D&T4>We#CKn5do?w^y#M7F%OmQ@qck-O#haq6uvcu#F1^gL=~6 zxPt^`9zUgY_5;ih=-in1sfHn7KHdqT*=YCX-oiQd=e6Iag+$@ z&-j+i?CWPk=Tonl+~$$njPmWugbG9N`)#>y}{>KM!A?m4?d7R?aWYOMsYuXa8Yq2c^()<@;*F^Avi5h9&gR2Tzg}rr=j}w*% zyJPMe++1alB}-`CmR}h>?0VX}*{Ht!TF%I;pO?1UhotIu_71r$ag4oR;Zk3_tp20B zZtAX2W}j&DWYooBZPX!efLWZ)!TN;hI=rcN;ps7iCH{JbUCe4`Y|enNxcxJ93unc` z7Lm_BrT1od!6tw2U#hpaYyuqWwBU!9Yd(6j9RwSqUV;`V5Ef7$5M)+f3G>(NYXl5Zo`q%RVo5tpgtmeP(C zed}Xyt#2KG#OT_br#$afj+W=v7>i?VdHI%JivvIHX5_MuD;Dc$bPmUDDmmqS`-{E> zuOnA-H`m`Sxr?M3ezE63ci(WIM9WZT&{Fuz?$NF~ina<=ojY}Ipc3cZ=&Q+h&5V<^ zgf{6iR?$Htj?`x@YLxblC6Z)gl5o_i%%Npe`eBMNt`kQ|`EdS+uE3x~W1&#O#?74| z_0)Z9Co-rnT-!-dE>o|M1~Lq>cqsN84ZT4}SRZY}2nk*~U-M=}^#y53dc*sn12n(R z^-?9Av~PB{XuZ7vlzX`nXKV0atb{wT2^Lh-{qpxBlPjGnzQ1>g{4(HfpOjvZ7CNWL zOZNo!M)0JI@Wo7-Q?^JO2*XWoP>C)#sI8ZvaMXS?5v>TM{BswB|J;Sf2=ZIa)UctX zdy+ZOPyzW-nqLuJ3tK9L$$Ai(?9p~4Tcbbk3g=65otZ_zi(@SFR)16=VA(LXxOE{^! z!mu3Tow}Smw45ybxoxHFW?FlwpyH-4BxRuVUzY{0gv&wfne>4vV{5jYUa-e>SNOWr z|Aw*u-{J57*S`VgY1*EhZKuOY+A7mKX*uouL0gd{GEU8A%2vj4&5*WU=1ls*dWMRx zMF~Ttn(b1xHFstl>#o`Ru^ayN-@I^@8LWoPj#s=H?ZcUWPkW+V9}TSYTn_}Icj{o} zvnKkn^w+g%mpx8+CA%%Yhkw|&V^j#1{T}Yf9Q1Rp0_1N`>4xkkv@n;ofU&lpre$_ix~HMfzOGe0!Yc1h8^f`N)*pR$Wr?!r56!+f{+^07=#2 zA(klKQ4gX-8|$s|h(hTa7M>lh`z?g#CQMK@85D5n{HOS?ddeoi>ZS4`H?GNzqFuLT zOnFTW(2k&mH4f&H%uY1S|7&zCbbIg#3Iqx>Gt%ub>dbfV!}y20hIBaa)`f65x;)-uEn1`o;l&;)2XIS5DCEMKDK^GKR! z>JVe4wP#loW1uqpsK?Y{VOf%5dV90YVY0+8E1cR*t6&c$J>_?>vGs!M8|d2=;=R>N z^k{3B$mwHJ*?rCJ8a4_=Bc#FrB13-_c?Ti0-?;j(9kRY|Q0IW+{1|<)>`(fMPN{c9 z#BnW}&SbXaD$rQe_5VRJ#vX|*N=;L5#+t+)X1G2u3WSvMEbixQPgi2@L?>x5`!>WI zQMT$y3}5i5(+>Jk^syTdp%^3S(0EyS=>#{(?K)cUS;FizDYYKC0=t5J1%HX}VbWl8 z0BMQeMLz^+@bcK_VTf-b?qpSHYxvN{4V&7Rq7i2v%2Z4AbCVM;#OO+B#5|=$frmhN zki7Veza2ArHD^-=p(ORYNsG>9=m=?%9Yt#H7>9|&5F>68HT%~#Q4L6N{Mq8zh-5tu2zCp%_# zZqgRkV4}$57$$--^AWO`I2w5I^4jN}UR#zt1;stP2?boE*3T=E@tII3%DK`7u@0n> zB-m3N4MsdVpx^Ri4M=Yt@Cp-pum+I!)#2OdBVgp`(8B6CGenq8SAWsaT%peso2&GP zvxQ7K(E_U8gH=8x_^oT+aHYHd#5ags?mn3+4QTI|{lpxO!_(ld=lcEu?ifNM7C5&4 zEJ_kw&3Exu3#2r|YYr^FVS4yz_})d<67+C@9rx>*W_UuETjQ}zsq5hS&g*7lWZc@r zhnSnWU=dL)SlAYP9a>73hrdGGUXHy)Uh!|~&al9c6z6T7RpM2^%0dQtIg2nz*ro8qU}f7UEX_uU0-qDy zuL3M_DjsIom5STSs1;rfq#}+kxSws0{WX~-jl-AqZXGO%>NmYB7(l8#rv4DhbpqNR z%oY?8YSkSX8QBpPp-UdO%_F>#Ho@_LT|xk}DHjWUBcaAX`E;Z`4u~`$r<5{f8KrGVU%~ zlN~E&-178T$a)~H0M)?TngRTHHPVPeHXU5JSr1_>d`Qi#cm8XiQS>d^-=d18oAafI zxOp+!N2>*bEibA@Nu>}`UV^?a#`Zoa$@7`=zS)iZ(!`HdxW%(p5blvB(UQ68Izx$o zd+x&YJwY|jlQ(=%V*mQ|Zy!8$k{Cc3@GB_O&DbfsnXe$#!)6?-&9`-J`|4bgFLJ#j zeeMGBr<3P*O25>K7su$+v8&=6Pd0;#(()4mWPEQoYgNQrm@?|hxVQ?yi*kfMZIfjY zfIZD}9rV*mes)HC$aGW4A9-wg|r4a?$=UV$En@y5+(^Z??xG*erIYAT5kx+PqTDOL|TB+iR=k zEQI*-#15N-9X>ZXs~DerIICkSs3Nb>?kI8wTA?dDje#3EvsM;OD9|Bx4zX7#eqeG~ zx*uwXoRnvclBloN{T`z_#KK@9BR4>L$|FJh3i_xC^Qst25vPdLfR|vp)PSz@g`$mB z911tkC6DWELH#Zp;G=2&%%;O#mNic0vD+~{sT)yFC<@uQs(^~=OU;i+{+OJac$x4U zZM2+b%)Kyi+}Kk`oQGK_ePRT7g>ZDjsn7C-LLz#Uj4zHA8|GrqMG++kY=}*I7NZk1 zB`0g0kgWr604Q4z6oU<|h6O>B;zrEbDF1z*4q&{uo#87GYqiKgGX|2a_2-EJ{C=NH z_(U!j2;>uL*S-6Riy&UP$-q%oDm7`4AaDPkwpWh#dw568=5w2yZR7yDAU5OEWL@v1 zcVNoj(=4rzZ98}$T_T(LTo=sdz+q>wpx|BWZ+zNmPO=zrG7-%)BU+FJU`lb`9q$m~ zj^c^Ii{3n`j-;L`PpjSO!eA|(>MZ5THB|Zw!Gyqxr`<6X8ceeBEpn?6F@aB8N}+G5 zMotD=bSMPplO7-S4bnfk9?fF#q)nrI%;@A2B%QOCITyr{F-CHb-;r)s`?1Lqwkn8(%yegLg6!XdnxtH# z&wif=KbYCyCYm$d6UDZok}0^eUm?A7Zs5ZxxN$BvPOp=`pt;C&PBAOHehAR* zoy;b~694F&t684Tgt`7okQ-Lv_NRzR-^nX8cfX55gHb8mvYZ7JXpk4-LUnm7d0CYx|N8nocjbKgTkZ4%p*h*IcH%ZW)*nx zNn_#`MpNz2mVnp8J zX}q2ra8W%8T(g0c8lGgkZVXh(Pp?SZ`| zpeWtZW&;ft;cM-%~6S90PT(T1Mf4wGa*$3XN2=h+Cc2$mgWZEBK;j!v7cmL zY%TTSXeSfPv6M3KlMVy~kmt$bjtm;Cjh&#)+;@p;Ql3!vEB$uOq#b;P*(La)_);X}T&}Rc!+Uky z?96)#tR6q_J(VZW!lKsQ0wI+@i;vEXI>s1~doUb3`2C9nQf(F~m@pIB2{$(i`fdHR zJR#oMU7bGFt2d`}i_!$U*9y>uq66r1wD8o{H#%=34&=NN*`;O~v&Q7ycgkNYX4dgZ zEHc%BIE#4dD-et1h-<;rW#-7nd{6Z|Qr47*Z74PBgdN*s#vm2jUc4{6(K28x;l1Fh7e9k`_}_KOaOp2~8r8I)>n?x+Ls3r0uEcaCFjk14hv7s-zuS?XYz#F{;(cOF6pu4&h}^ zF2Ov}COvNT0T@y%x~+Ta8(A6of@ZSNtAzDU33|M9F87jkWL_g^U43s3u2$U6ybiiT z6jDa*B}A_h3-IWwRF_>~f2KD6;Sp#;%u&}IpR&Kd+DSEba#Q7zl$L}%Ry}QAt zzlo60Co%z^F-EsKp^E-yisAuQL>poEbj!%X`IBgrISlryZ)iB};aJIE{`MuPkmkE0 z9!ldK2TkxyBXO@4FI%o0H>)|c2*q>+5i2;G0T~1#S$1hCt-!1W$TJ0C6s9VFit07;|&Oxd0_>yY6o; z2G?uWDh#FM2C5wJ6XcG1h>Houd0#j@*fiQjrxv|i9n*g%F;w2P(02X&4Q^Yamcm$X z6m6W}(((4pU#=57f$CC{I{Onporm78QgD?(O3rYvmz1jat!D6tT7eQ{t)Id(E7q7K#I ztjAXfGvWKDV=G_!z8>xWbfk^oZ^FZkbbk;r&fQf# zPTmV56)^kw`a9w@%iK?li(pH?ujIswIkLZcgXEs39Y33K!|^hCFFp=CJ=bm7BiReo zwcv1TXGM^~63ctsp;X2-x1k;jvgXOuf#!z9xR~BMgy7>rpm|QoA#Sv0C?W&p4ihP- zY&{n3R~Y6|ll%KRMfSvpRv4ov2kKw$ZaezR>Ar4e_deS2Aua$rfUg# zPYE(4kvkh_{d|z7`~I=4s8zk0~FMT0yT2ZS<3a206p5$;Y*2)8z9_=DPWY z&N7DC$3qEcDcK({o!K(r{%%m^d?;r>wp0|sBP$}OsEzf-G)21d(}5;#IBxW$&p4*( zQ|`tGXG6Bm^>Wy=CbF)Z_u-T?FSc&y`_e$iO9N|CZUTmYuoF!duCWka6mNwr*Abxm zP))HWb-G&X=B|Cq~}85g`k>7d7;^mhqUjIz2SaqrYwh8 zx|s_R(3%szTyePUIfGof07?spF!r}SPe$8KxJyQ~$4IXMMkp{QJK(UH1;NY}8gO!Nk=4F=6jGsY)G5vaP4r;HE>>@9=}g{Fc+>J=?5A$n_~ zD)8BvysfrQ$Kf=kZPdlvWLc>D2&NmF-l=cV88*mvR^iNm1iRQR>)r|3Q0*~YD1gf` zsrSBol(X%{+5%vo`%#M1Eo5(|U@W)OjgU!u15~nSZ!1NXQ|kk{K4D8|O~+Tc<*wHo z9rzi(>sLlInM}88V_(%;j%hwW!&t%n@)(@dJGN3Xp22PmG#>64D;BjJV+>oIqZcyW zpC5XXdPZJT+VZT^tuE?7lByg@)gdNln}pYh_lu}QYdUBl;?KviUCkwE)3Svzd9I;u zS@H1|ZqC-BITjheWUfZ;&Jj#*>ph8py4d33_0nzlw!Rud4Q>pPSg=>rsUq$5w53m0 zcc)QwV83ngaA2oCjBH(Z*ly;ZZ_4~Tve+8t%ik2zZ7%nD8FE!8&YIroqA8D5x4PaT zlZloqFcPo7m&nq;+Yi!?$Z}CI1}Kp=e7A*wB{3Tm5eKt|0>q(fsuYooa>--cDR0Bj z(P0?UgzZq_o*b%xlysx87nWD)_aR%)6|#sH?zksQ5f#cailb2lcZL>0!m}=Z>u0~y zth)6udC?fOwgvi-0HxOnPw%&Lw6R7h4cqm;pRyLnM_-ps2Va3r+0?olIldVD{^iJ7 z(vm(nL=%?E|4ayW&QOw&t;HNLhoDg0VS1wVTcN=9+N#{Lmv={DL7Y$o20Q!iXFOk9X*+P{5P}pf`$}ux<69hhC(Ip zAA1QSlt-GG#H|!)E!B*GdhmgRXuFy0%_RCE`@1yFk2NS(*13Cs^_Tb#LgxJMf+IT? zqx?4jD)#Ngb>P_0ku9gmTNl*fRDbf6%jBM;{kKJ1*`t#@u&1X8?M_3MsjXt+5*ky& z9Jq`0wg4>93^|6CN>BQF{C3A)M0!UKsWVPz#s#n=>cfLYrfY0mMO1O^7tbi^Z?Xhp zY0J)avM*J*WBw{?#cuYm*nOd(@b78r`N{!pBu%%V$$i|2inl?Wg+gW-=>}K$8w_tE zypf^r8)akNP76PWv*)=_-Z^bO7vc!Fd07tCH{l*u`L>vE6Ew=r+pzCF#f125YEtSo z6IzF}bnaCy%FH%lpau z$}Ha;o^``U!jL{Ech3w7chShPU&FM4^0a~csUWo?y83P1<0}hD|M3Aot_5%V>@9zzT#?Ii8eI71ba?Vp#=ozf zE&mm1C$KevY#7`F+%a5B{xLuQqvQ4CUH|h-j?Vh=x`$W%ufY8*Ann~`{?Cof-W&Vz1-t(Wj1ke18q!zEQvH#{4VWe?I2Q(Tf#rOp!Wa z$-Cosiqr>lgymD8I!GJOBw?v^3lgqWA2{>HiQf}FS5GB1=YAbLtUY~+K@aNwm3fP? zlF>v<_kz6XTB;Zr*zKOmG7J{V;xtKw)8p83b|j2iE2|-_Ov_02+zUB#+;MkqhVX01 zU+Hd{Ao3DuX%J6)yo`-2hw~go>qIV-Ao|+=bM9C^e=QOo`*?#4c{R|CPj?0K=ncX_ zh6)xaU4d_|zJyr~Kg`6Q`v;GI?(hDEy^mIa-v1!Ge-t$!W^zB`bQu@<9m)_y2@S|{ zp_oy9X}cF)-QdQmFQ;RcpqD**Dh?xU1*|1z3#DGZW9{JTa<7H0;oG`%6QWZHtxz>5WreJk=66 z-drOaNBYw;d+du5;V%YCeKg^322F+?$?r%_ThWt#uNj^RsXo#-Y23Q{fjwBT)XN!M zs8Dz8GI+KjnE`TdX>=r#mWh_Io6I|7JIRA&A(I*vBj{xsq}d7~(+X5B z*#z-IVS8_Y#ACvQ|Fk!bnaje zZWkuoU6N+D;0FuF6gp*LdsW=m3%*M0B~0m-x2Ws)Ry2kM1BQME{U8h(9OyyG{bZ%_ zR7Ic&ck7_Ou6!mz@?y(lIN&`5O`wHaa9fDU6pK5lMGp)=)tc!iD;YgO23#UJN!Vs# zZ|BH6^X1a{?FNJPLiX<0<8WL4$fUyYK;A9XGvJxcUqESZP<%uCj_j;!N972(l>Iu^ zD*d!L;|W?1GMJ0{{J19v-#g-Z%>E%vl?vLvn?`fA?5H?`W`>0&L6q1d$^M24J;Mf7 z)9@Y8IL?G+scdGlCYm2{LaO?giT6obMt!XV!1W^7AWjj?GabQ|*==9zo~v*!8CpZf zfXtPRUm2*oL|V*fAePr?S6xCH9ow^`pJ*>CRw#9Bffo3N%NLXFQnxcE2$zEu9tR1U z+|;mQ>2A0uGmbX2C&+CGdcuJOB*&LbEPj?v9`e#5%vOzgJcGgF`xcS0b=0AqZQN^U zW<9dQvL=gNhiF<#?dbc2L%a|G;IG0rf60>G%g=rM76uDvm}*`?=Ao|K8-iKc zbB$x`%!bV(>IOKi^06TJaxB#ZGv`Ye`WgldM#rG*0kOpnJ{$sWk2p&v1{k*-kY6Iec3ECW#N4B zBjK#j#7AH%(za(?<3|>|Po@5~j8_~{5r1#tkgRg>`Y7+^?x?}2&fh7==mx2mtHcM3 z%NNIPOyRPeb4Y{zCtUU&?5<@bchnE9l>Wr{3~*k4(>Vcj(?D&lrGLsP_GRE}{{F1c zqh<{$tM{Lv;nYK?Th zT#OXymGOI1>zTee#9SktNF(hO-v^XA$6G*h;xM+^cP^ft_wAO(4(!Eu5ZtzIjPVAh zx=YY?-kF$}Eix2yMXy=H3TUKs3Am^ccbC@WwCc+V5cIC&fFlrmi?SWW+({9|=z@Gb z^U9pCk#PZb|9BrLzR;#TXpguHfgH1GN84~z90{WEaPpk-9Y-M6jEomv7+yK$aoL$i z%k&xAAu^zv(wLkL`0CmrKAus4ZTB%4tLskf=5ffnvC^?gK31!^na><$9r9bc1u^=0zq$`l2F3vXmpCi9uswZqif;O3Ao_mFzhYJO zrhpZMo&J`u3(mhh)%9!!C2MLlNn{whZ}coU!n@{yf`t77SZIab_)%DD2hgcF*&;R{ z4a;-{>zq|6TCk|$gGv;pBlC2lUNns-KRojj{_wo%MJGei;&m>v9L}uz6l;<`Z>v z@2^Tu%_^6S*KHTD44WUk+NYEz>8?n`?|;1#y{&#SsbaWdlmh2%&>-9-LO%$?k4mpa z%;M{2l^2WAe5npT5zg6xRZ36$nv${Ao~%K(nR+_@b9ShnUZtWQ(pul&2(4QMt317$ z4z}k`(O1hO->>IQS%c=H;3Fafe9a`W2{ZJ5cAw6FZ7p;dghQsS|MlZ%#s1$xo`Kd2 z?d=cCEdan8g&aY8U8)r7opAu*oGUuGE4SvH(LFjp!?0OzlobBZS&`)Nu!D6(~0f?T-i?@0p z!LQtNOxMPuL`b(=6B6BRNz3GB^aQ8+I+xKhTZ21NZn%8;9n*(nx#BXB-7WPBlOVm- zIX$u)oDCJ1Vl#bVKfkZFSqEP?z(GcHj$4ILX4B8q5GLOQYLEjxCTx9`M+(ih(dF3) zdxLsww&f?39o1}dl>Rm5LVhIqFx8Y|^XXu;rH~^lm{-{L%6D(Zix;x{K11IE?s6}1 zrpY*`S%g-+6rf9QlJC|FE%<}5N0Uo3!p1WdPSYu@E;`BF#g^`}NtdYRaYxH|l_C)3D@^%;YI*2v_3iC|#BotMhh}J+_%Bzmqa#E|5D8ZK&`V+B6_$ zVLHO#;z>c~F?FS*1LXYpW0_$eM^gZ+q9d{w2SbAS#Lz`+*`t@|PQG~?(IP#uG3|~O|o+S(ch`R*%=46ya~_~N%#GVr>cZ;{%0#eYPb?Q9G9wJhia1d~tf-vd-}hz59xi_&Sn z#p3-~PTNA7jVO8Q&|!aSLpPM^#~=;DNG1af)pHQGRK;M|5B@;~l}lB^?v!vg0ku31 z&hE@wQOHCK(tnP<5!YYIgzlX$k3!QDGMKI0k7|xb*_ptQYcNJ3!TZ4}ZF|7kB2-LI zW7NVnPv0K+xTQbn$augGc-4la7cjYeY;}DEP8Ast%Y)m}LJiV3tj#W{+xqQUL>NGOPy3)!8Kxv2vTa)gy^;%O=g_l5Goara(a&4QSlEh^x;e_bxF#MZYr z8dd0dyidCva0IQh?6&_#2^ML0s9(E0{{gi~Al%2+XlOak8h?Ivf;7L(WeVb^rfeNp zfTHpq^`Ks{B6efL1@vXOjcf5tw6B%j&>>6uk_gMskSLG-5rg3mz{DTZeL;Nc+Htxe z(17_A<#Cd{lZv|s7Rgz?rO(k?`)~Gx1u$0afc28k_h!ZiVpIC`iCaLtjEt5a^~m{* zQ#+6a{ZSqaEx4YCZ&CTM*F>hnSL~QFY#Q$U>f$i0-;^j4K);tyJC|^*GBbh*9_*<` zY6`eHAR=+?jve$C9^`>5VnqeTMZH7`KVLI0Oe{7NXV*_9HNw2asi;PX%i zJ?dU@C`BP=i4P#zor@?^UIL&k$s1B^Zq!-#TG+q5VVq@2=in9e64UbWnykv z5~G=>Ff@VAF4D)T?GfA2Of#0mLr!Yd!Jo-!P#4UAb~1;sLGw|zSpDmB3|gk(ro>Mc0x&Zc1}k9 z1~1psYE6GdvA!1TD3wXJv7hRHICRUnbKVwnUA?kH!t|R7`PKcbb7gO?>u&qi0t%mV z-wer`uwG*RZC4jR=`<{(r;|8pQODtJZ?4Iv!@$h?GzQz_Rl02#%3sD?h@bM?#%gB; z(S+9ARpjQdc7g2XZTkrqPpA~$hDxEHWG7gUv+OOg{9=1c2dNeHvZU314a0+BK+|?* z!Qxr6sWfig5|2(?u$JbYbHc+9`K zyYJ*?2W%~8pRnsMwdxA^tnf}C^fuFS#g*%iED99)^vuKe!) z`foE{LdBK&C8s;2<0^4mQmhg#zGyb1?&wzDH$r?9pRwWE;c|<36^4!HXEUw zQ_EeHEi>jFtI0O09(hSeG)yiYJzg9@iW4`Tx-fIhvy&gwmvFzQpnO~T3d{GrV+(3( z_YKb(;H&h^Pp~=Rl>OVTNp8jH=I7TRcwIl=XUlJT@`S;fpVJ)ZU@IhoX^W%2yR`{#yZGMy1sDaojS2e6#3lCBdJeJTGNuFb7fkZ zx>!mUg|O5No7;!eVG0cGd5F-PKctKb--?e^QI_{z^s_3B%QCL1fc<0OjOC%?gK($; z%5mRRwTD3nH;(owI+18(<*>^Z>Ky}ooAQ|0Mc5kOwqyPW`u%LLuN5!g%+PlU&qFXB z60Rby=tO>iNBM^7l0jh5)utrR@JcyEa6U30S8%_&qqNgjxr{nAtM>|WiXYsRT*|$!URCOK zf;0>sSVBhqJFV3+Hn)vU7-M?gQWxCtl`uOtq;TwaVddA@sY9>HV|CeRTQ_+je&`D^ z!xt@aQjuQD(~5m*>CLuuBS9(F2U{+6Au3vRWmmIg{;0;8+iu>z2rxhU#jL|d^Vt)2 zm3!mFK4$ZhE6M4TxVnP4K=vGWbZr{}3jV{C6_G`w>+n4(4(<$EBihG~K2F9}nu+<_ zr8|id!Qw%J3=|8r#+U4#@YV`Abp&}aM_pvyegb@XdRzPeA7)Jr5Y6_*QH{P%5HQD> z8`C5%1|w&{(M!l0y5L4pZCOsr>T{;aCLs}FEq!sUO8h!~-h<=AfrX-!PBzbx)X>WB z%qebvvT&dNyd6V(tB1*7t?y-=WW(Vcw3+eMi6YC(A}`q~n(=Pg+8bU;{Z7Y>Anx@N%^ z$>HdD*nu~HgKoKcj(R`GZ?vQSaU|$UrZQp~^$gF(an( z*S;u;{Q>4gg53$?BTeYd9h%jF%x|UZiMWM(i~ydzs6q@tp-<(uJ+SZC=uONJ>sJk4 zf}(Mu*w{2M-^nXvmF&xAv_==thc9Q1+3%GWP#hI%lKq!H8gFbb|fU# zt<*ClR>&ydXw(4L@M=Gx!OGImLx9jT_8CrY#J*y<$ z*B{S@4f_?$jHCG3XVjCAIU`NtGM;08!~v;M!lMO9L3kduaik;pDxr#7iL8ncYz?=t z@$%csg9l=4oJ&9;E5bvc9`dx0t2vHh`n=E~Vw?Nx%~+Up?|K6yvwakAs`J<-iyIDX zgt3T#5qnD1qv_L@QLj*x`s$jFi$(|YspiAzsHQKt=Rq-Op%d!Hy{5)kCHsprLl#DT z1>5h9*y}D~ld%UtXwbm;Eg9R+sTktU#z`|Lt9TlDcR`LF1d>>b3L(a$wV`Y0)Au|q z?X^@nLheu5-l3kEag|bZTe3$ACwU=6G&vh@8|MmX9bSdc&GvMXcMLXd6&6byl6qxl z=nlD8CyI0vSakIT!7q9s`hP}hCm2AfyR--q~<+d z90e6X>%nw5?W%|zVzjVZQO6^os4V8C5vn)gD&B2ZqI%(t;sO=xCgcI_Y;ZQ zby@$#7yS4p(_xGGnQ8y^2rz>G#3cM5o=>0myg!lUeoT%WI5J~K^-a(LdHB=!f9HTR zo4UttWZGTlYL?=vh`0qG&i?~{*!nMqU>?9FzM~RsulD^wCqUg^*}Zp%Rk!tbv;zeK zy`aZOTbvjrjBQfV6o0TO2`n-x9AgwyW=PK=T}lot;lVk{NIJqai(qbmegv&rmi#4O zdTY5S(CIB7w-c3V(P+w|9LlZd|8{F>r=PG?v6~ie_1l8L^*_&e_|t!U?H}+9Ib>o6 z;Mo7mI8-GgZSjH~#QghdI~Athyww2b%lUH z*|b9Iah&rm3zems3r*$V6F_(T148+Z5s^>hJ2_6|AAHC^NRNMq{oh9bhrk5V-RN%o=I;#neH31NS~ke%t#$;LLS-7_LJD3{)u zV^+98%f&5>SRbm5xweJj$_#Qk(FB>CG2}jY(%&*^wN5ZWT1tOUtJfu{P+0cAOBObg zNh92Tb2X2tq8g-&tROodnYa1volt#fsw=znz4aMxMUF)IpMI1i#1}vKPiq|hTN-6+ zX=(5v64|?Ot_joP7tuHOd34)ZDodlPY$ZhPn%P$^RW+yWv9$X5889C56t56i;S3Wi zU!sL(W;@OE1#d%ryZ!>{6C9vRL8}`h1kD457q6Q&K#queQJ+x{MAc5(wUFoRf5Y({net{olTGeanB%1dQag zsM}MFt$d^@F>Z9h#h=rdpCI7X{|yoOQ)cmD<$vr{WrVn)ql&@A8y@WeSLo=NPd|gH zDaaJ7?7zjt{9FU|=`^LfZ1v$!{*DPYQhGEB4c5GmKYN8Wl|gy)N$My|6+^!dpVLmtP3=aQ5AViNq< zO1iV6)_r~}nrylF*=OX?Y1t!+v3_0|>s3z4$z73(Iqeq^gQ1JZx1Q|178F5K?vFj! zn>tTV>B>!IDRsy|yOV{jhUNpLZJd^wz!d6;BSd!@YM@EmE>)@WV7(nX8UBXaJflo& z8cazl4PDD z5gL1&^dz44Ikw7HL!n2hp;StlQpr*sZIUorw9+(9iZaf}*#=*-8#6sxM*|%&9cRtDeQ~EM;fU0{cpS1^5hQ%SGv_=z$ zV23iistXVHp@l{ThWH1%dY~WqAlv#z4>onnWH;Lb5bBDm-~*@>Y+f;s9{2}tl}OCO zzbZDmIWToA0XWFqIF`G9Kh~MZNfpTY#CxTFGpHhD+G8%|SN_ZwO!}Wrc>zxN45_dr zZWCmd+<1l->I1fH>NQgV)5fx>6W}uQ8ON`p6)>?GAM}yj-x($CLa7zEQAK`>e8XAd z%Y}j|`SIUybZdA?|<`7uUJNHXchs$~HQbi&Cxx+=%lL=}o4{rG?svdIRv%>RvD?_DQ zR<84dVinL9Uk*39E_s-gvUBb8k!1PF`XT8C&s0&e4cL_Oe)N5~qc*?Wj-~7D3QRk!Hnt^ykDX$7{?m~ISHV16E;&t21mj{#pvS|e?u>)P+6bXC za4dhH*VvMHp4Gy1tx2?1v_YUtfG>9d-@P>uVs=@Aw~VRQB7^ssSO9g1Ne^GRg%c$c zza8aXGF{d}Q^zA375g*lTJQB&eWB!a)ayFqRsiFc+AyYb8Zh-ie*M&Ujh&_Uw1ykc zFU#JT*dL$6uUQF6*JMRF=^46rdX*z$*%fqU-J+Bz4-NSnx*j|14N%*)H zn(~BqSUxc;^sQE#20=8CWiX4uMX`hP_kQ2T&7e#WC8$5!WvnHjrn);YZF&w zjO;tFh?NeDoQrwECTl90^Kt#%m8aVxKd$cjGU8RYMn;!!CS#Z@d#1Z%8U<>;=)OFt z!SVe&uTMAaF4}xrjh2GL!cRB{9DU23_kvr(Ru#G;Q~23DwR$&OtAy|0!Qv|;In_YZ z4N$W~q9VUS-alt8p%uT5y;e2&MABgr#*6CkYJ5Xl%Xg45MzSPz&z6FV-a0pZx|+{C zfJmDpJS_2tqosV`?W9tV@m>!sX8Ml1p5h>n*!iyJ$8zsoBj)?ZpoJ?741?$D_>}aK zraixXj|SO|LQ8<0%9V(8YU5h>)&AI$g&11J1BrMl;+hKzN14?y|!UFCU z!vJr52g;~LGJ1VBqVZ|viRJr@q)v8?~(>t5GAFP{ICx>;cKX~~xvb_d8 ziTO4%^sNcjo26gq&d?Zn-A+^}n9$m9fqM9+)I$}mhZ)6Z zo8YRsWz-4)XmA{sFkoxf$aW`^IjxK^_9V||ro0t4K77}zUt=~dXQ@y%m~=NIR0xvi z%3`sgD0ad30Uwgr8pcEnd#)|ryT;X3HK>q`yxyZ4R3?Ua7x=hlVz~b_@nS3Yiql3i zE+;^8wg-zQqW=)`F@(6_UgdaG3#dhF0!}H`>nnn+AE@#?dx7i89#@@a+ zc1P?`PTS8=MNX&wpf?y%AN7XmcM+*0tLYCYvW{<&{w%harpsemxh4kp^9)_5Jhz@{ zKWYy|_(3KW(Ab<}t*Pmt4-VwqD2_JWH~Ri{2y1PvV|Na#(V9}_rMYpRx$)*PryV+6 zu6gfNREeXPu0NXgG}>9B>_c3c-8J}$AxN zX+Kr`{K{k!%U>^IFh0hVIC{(Qa?8wj#^_1LORP7|q2KzasRK0$kwq3#`s9s4M)1~- z4#qT>HCAMDvd#G0<(kZ)FKD3`b4fj%&0Qj_Pnz{uFrQBV%%dS*-XP!l3wD|g_$gNc zdw5f*FNoTT*aUK5zb|*UCCZOlzz0qtI3(C~P=q>1hb!XFB7CL7P|gf|k=&r+nGrVa zprf|iPbS=^jiBU=p}?;zCYo4%+7oI0zVc*!Gh0;ga}xt>sos&NoX@Abi1+F;FGeg*^4c9uOCMDd4ym2QJpxqtd~lQyME)7 zP6R~4pFo;q9`CN#e9djyd!1YcBl{Dy|4$#+2f09>S-(34YYu6=@LCs4Kk~_cjr7~{n>pc& zEtsh_ai=-_&4;HeP1c$}HMiZ_%Q=S(G!4NZ@gM(tl=>i7n3Ucu6?qtzk|4Q9g$|@e z2Fumr{g)#-#oaCgM~Bi+mz@J!x6>R@xG+cNS5Nr2boMdj(27IMBrZJW&UCIkx zTxA=JHy6ts_nnFY`fcAaP;A?wKO8(iP0+Z=aXG^b>3m3ZtDZMl>N_B&I|ini#jmop zN3%W9&Yuj$DBJ~K2;FsAt_YQ=X7HL5)Ecmzp%S^UZ4Z)j8tu$@oMJVonUi#|an1Hh z`>83%koH!_he(A}1;t7DH3qB6kNvAN$+76yn1I0qRkHiCMBZfB5gzNxYm_4c1N2T- zgLOv0&XZ~S!%vFjo>i%+$9}&8OWtg-wMMcN_GLr`|1;U_&Ks4Sq(hBsHvhStD{l`$ z#T|%Z#CknHsg!!%y=LfMRMxY}=Wf*4Gb8~tNd23oDP<1lwF;Wlz& z{$QBLe>czzJT7uj6zFBrwiaQ5)WDhy%W+K+_p%8lb0TnYthqI&f&tb|I5YxQE zFGdIFX9Mi)Kk$8o=UiW>I*OUMNscA8?(8n7s@s{bf@q!F+Xs=@ICaxGh58eR=DV7?OQ%yXniDfXr$BkX{ zV&K7ZXV~T=wYmjN>1lco;gDWFej`{fQ1SH&%fI@$%;0{MaL%@#kVJlY6#Tc8%6f!KRsN zjCTqIfW`#YNVn(lR5@=pbAEH^Y@Ll&di|qUhvo=|5*ohJ%@!v$P?ZVJitVrXxFbqK zf1IsCr6)Dt!7WQou4hk|XE;5ZRw!YJLa#04wVny>jMeJ-0MS>*NSk{b4PU^2=Q-9~ zvD%kMO{dRtec>=2FX}c||K0{LU|VC^CysUQ?ELYrlCs$_<-w7|TLFu|AO(U#999r9T=Umf?EJTs$pTIz%jJlo9BZ=#FBafVn^i~{axL*zs7{A(HurBBwfRsrad^H2-JRNOle|;d?o({$HY5RMQhNo6~g%i z8jf@!vAq(knU^=&V#~}Yi2IHn9QbR#KmGHoNz6@w^t@fgGWxc%WceLb%!1}3Y0M|6 zlz!ah6@eY-^nv-ljjUti_kkD9Wx^JsYGgGG zhN|ym$dY>v(4_&6hd-C-YrFpK9sBNOIb}x7%W6fhj_Mw#ifqv^FjLjFr3T8u++66m zA(wEG8<{s&{q#BKFxR+@d-<>8&QCI9bB)nhsL+^(BW{cfb>4o{K&5t}Tb`EM*gh!* z5}2t0r-kt(TH;gqh}3QBNjz2rGQ!IRd&kAu$VBW%veL$xSc!%)PTH#PP5NBn(#5U4 z0K4Fs4k+uEn;L!tf%Z~4(#z1qzoNLS-~w;Z!4zT*mwmB!ysWhOgWS!HW{Bf@F01zC zQp6RzV2GyRmxhOG=}SrL2a1UdNkWzkVrm3 zo!{&A>*J-OmjW^bj3ljavHS@c+uPp11TPci{Tfs6q$Hagx6#f_6Sk*|a>#)cP~U=v z=5g0JYw>gFH%Epn-m{S}KYxk6$nz$LF6<%`>3C)DQx&ttm4BnzF2DT}BkvP^ ziz!WqP-8K5={B&;3U&_G+hoiNV`>9mJw|~WQknkFiIq-gQ8&*9`NCgg^dgJRsa7Cf z7+8`aFUc6>$7^3UoOs!yvJYlqTg~zft6$LjY%}xSKa0pz#MxEC`A&PsSO14gywJ*H zi}$vZnb@hN+jz3TJR`-kmtW)IJ!dV?t>-k~@7}A#C@ps9d}Ok~6jS3DQ*81Is7Ea> zq<6`|Jdf`Z@rk)WQm1#x$2gh0hPn+ZAYOW!)sWQRBmqA~i!f^!lb%XW-G#hBdofLR zxYInsXhU~U*yhnv!>>>HOQYnKWYof__lUNs;e)@wD*KP3=FN~F?FSO$PMld~WH4+8 zEvF9;gO1&XN!_zhoS;4#k0rQa1zM5xq1m8H9>6Jxac_cpUHvJ1K!mN9Q`Ntv)y1Lf z&iu;Syd<;@%B=msvGk`)W7sXAbc3Ip5*&ieCmf?_l zH^g7WlMBf^Y3&NLvNE3fJ1W!;#a!#W+hEPqGdKLsYYEUP%QD+P8U#djDn?^)N-ePrz0 zgmD}BD=*7S$mlbo2w#p79&q9?+Io_Xf8CFIU=4QeSVpl-Q0#OH{hpPC>M=G}YIt;_u9{)|4uNJ-~?A)^60HahPL!vJ7Trqg3 z`*xMN0_Btq9P*n+ePL<|{$&ueu+8#8flGYey#e`m^6t4GReqGIuk-XlaOv{w4`XMb zOoG;&z>xyP*9~-6#)ESKTEPMSry#qbb#4bBqAl>F501T;0S>_JzXY^iH3-ECkOB3f zXHXAJUE$9ud)?Q2Tg;o>h0^nkywRY9idp$-CJ+1u+~IXD9pW@&&-qu7qV<8r3bEEwy9Uhq&a)6x)40 zo%v0!0a_O&U2y{zzT4m{Uj11oUUpedP>5C88-A%={{+QjQ zcevMLW}GFlE;P7|EV0me_b|ZU`Hij93>9J~Dqs5M*O)yDa~$FiP3;zt_=m|CMg2)t zup8;ctt?i~f3|2JJC-x=9~#bBDS8QkyaHA7QxM_@^wlG;9=UFA3{}Z{OEyBb4PTJI zSl&#=Kz}XSYY9%2Sp86D>DB|UPJf+iqt z1hCZe`!4nASaa)!HmW)*l5MU$vfoKw-r+@;=!yEipD!&9gvFjPkp#+}R)Zym)eF%_agN>UAL{KzC`Psn9mu_I_I`{x4-k38VAAaGG+dT)|aD-?J0__ zE?&vUPNfO(0F;;^tWW?G)P-h33Qkvm)+PBD=H2)L53uGhWub*g-?iE7?Bvik)b+;= zYO+8v<`d>A(SF3~d3FkDq#^0zS=qAJ9fa+62oB0M6UZ6zyHbK@m3mNs8|e3Hp1S`M zhg>XiwO8W5B+|18EC-o5!H>M)v1p)!e7G)S`9{d#f$S07vO=b?xs7&3OkWBDwW*rP zi!bm}&V*0B&Algd1u+^aC0u(};Hd*>;X$N@H^xh`)0e{@?PP*Q47-(Q4YL7(+K3!M zM>lxTDXQjgp40)UA7_jF5N5Bs#YuWvFUSDFG5B>Bgy76B%hi9YHy8EA1F8Lh)ruW) zIT10Ne01((Y2D0#xuLMC>kPkDveyEL>>=P@-tgbpnOyju%jkB4)2;oW!Sutv_thUE z2zLq4qT!r-$mq%OcgWz~g`l}>5^JPe zlHi1`e>Z$T;Tb#`&`?SzoZhz$1gI@@ff>{Qv!LzRJN8mBd8pQ|BRDv92*eNb1wR67 zc`;o;BWCoy3sDtw*Q5Q7NuiV)NUZT>Gr>5>ATDTi&dBV#SzN0l5mhdJM zW5xgE1RXz+)!!>Ijjjrtd@J4CQ2 z+8%m3vYNbfn*WG=ZmMD?2aECAAp14sGUt%aUDbZI8iJl#`;5;@m1(UnKi~R)IS2pG znYm>b{^gWqNrYnWZvf>n-1oyNfT_0`T92KPEVlmlhWywOi2SMOeiwj>7~fXg5&H4h zm|Hgwn>e&2!<|pD(ePV(V`tH?F#yFerY3I-8I^*TCGiyHC;817zHx?aKFEMe=iO(o zmorZ|nB3Xw`1_o4`HxZiP6@El2K`V8;+97auC23~5t5=<2rhr2Uyikq@tzDbx1GfUQ{oEyQkUwy{@!|VTH=gV%y<`7KGh}cB+A+%m zV65C%c{`pHT-1lrMeesMbXDX_@KBzI$yK!FMKcoZxrkP8-b$y&)(LsiupiiHZIVkw|uZ_hGJQbykyotxi*~~Ke0vW-RciP9pl->ur#L=1bdZf&f zs)s2w)Y1pqHOY}A^tdZJAcnsZ9+<`a4Ae|Q3)vL2dzfQ`^{R+H8Pv+m^RBCBLJ0-Ye#J~-FBu3L&z;3zptMx|0T; zn(v0@_c%?XH=oae>QHgIS5c@ts}KO3Gv!+htNTQ;f_cq4Rwcvr35;G6>VoG@QOQ+q>mO~tYZ!kD}>4MOUc5^njMLSi+(@t=F z0L{-1!Oo0_K8`lE z%%8tDLF=?0VbeTHElT~xQSwgU>Y#A07eMrU9d&Kv+72eb>bY?CV%a@lu|EOXVL)6w zHOpZK{H1(_ft~b5A*B}E?l(YvpfurKRMC;;|NO3&thszsq?_CIY zkfCyo)qd!!mr?46^IALYHgtcmPyPJ#?+#=WCdxosvLi=ePZt02qq39Sg*6VHWi*09 zSVej}nZ&R3pvzrt+gczWiu;7*_lJE!&l zL>F3K>=6cCqBj^61KU=!b0(9E`^|p9k9=qxoW&R_1fMHdT%Ppc#=H#l6m5#)*OaImw$a;!MRG z%~hrvO!&mx8&Qs(@>FRm1KDi#de*3(95lufUOOX&=w{|R!5D1`ZRkg8V1Q4}RvsoX z4V0CSnQCRk(AvouL;(UQH81&r1wcMODS975|C}0wLHh3rJRL3F+)fQB1RsT(r7&DN z{_0`3&NNwHAu@>>b$;rS9L@btsQIvu53++tEpjgts#3;V$9?x*t7>Zxy)zhWDrXM* z={M($esyv>_+viS3TPa6CP=^wDl2$NFmvoR42Rz(u%hcUP`#XC>?M2chu*{*>y-uo*^U;J@$1k)F|&; z$q5bG0V1^BAY#spC1kgV{Uob-Ezy5L#b(`p16 z@`1`Kxlf}-9Q=;TJH%+jQKjM+RpVi&Fo_})%3chR3XR; z5ljj%H16xbJ-xLL#{e5&>Tb5s6_=n>C72{EsIQIZ&&r}ffY_<(Nq(^50HCU~@m1{) z3IcUDNXPRAKkRoydg<_qh=j|Kd8%~sYzVg1G_ub!BIt+wu6g|)1nDKfN|O@#ut$u@DGAU%xk$z{i{3ECeLJDFhZ3N_%QT-joFNc zN&7dz8A?vw*>mYhS9enS#oE^8g_l3!*~V09*)RfTjLll9lbIIR(N*FO>zQTJE`&q4pLv6)q(cUyY7yvw82@#kA6*vxd-@*dYAFWH*-E?Qb#Of`vNl4 zR8P)TjH~$@&7u>t|5gng0l|!^m+KQUs*I0;?og_abaOI>Y)ty)llO_hwt9p-&dSlBNH`mD)f2$RSm4_wA7pmETWXjqcvq zhj|`02ALjb>zZ?hUf&5uzB+N7nitmuA7*$2$zZjrk3+4%-~ZSF2Sr?yvacb>`f!6K z2`l#Or@qT8E$I*eiP=?+9z*fvWdz1)ej&tH^7CJy4=S^Wj;Bpu4dr0Uc_Pa1^W+8F z=(+9LcBsI!w{anP87P-p?MT~1O(tcx5 zItcpU>4bjx^&-reQkB7bZVKrWD2Flh;<1MeS zBM821-8ZFEG42(#@119_+-ttT(w0QZ0`RtH1u2@bALob5ZuhbJ%hGj()1Aw=;WLz5 z&;ueO5GL?ax+xkpdDy7Peu#@Nfr!*p*FXskV0_a{_4)NZgX1db-@_w6=wt_+wjAI# zR9SX{3%mg=(XSslw4dvn&oXOLb=AVK_K$2K3 z9+)`pwquZ|X1&wr+aol z13!zh#=&(tXyx1hU=^4_JSg6-upzjcX@8TTnn25XTqB zP>!9^onR0y7M4|10Pwhura1q~IYS2_R1GDb^gBN7nZxU(KTir=g0$SC&pAl?NEBl6 zV00oxiZ5*@_P)7}-~z$bxD$tIO$I-?i<{vms#b=5-n4iC;0vV|g7DFg_b&qzig7Hc z1AUgV5E`xtJfB@nm3k@{Ue-V#taU|k3Fp!ztG3n2e#lOKdMon4>BH@j0WkCDP{xhh zI@tJ9dN@(sWppT%nk1A3V`7DTNY;kQflNppK-Gs3a1yThUPu;%137*rS@eP=LosYW2+X+XP~oF(pS-yMPLP7BMz zAKUCw`e<~V`fNobj5|wM&9GbBIhRLPT1M0a9iKsndQ<4Jqn^YxMXS-B=PX1rA|62{ z{V%|;ScKg)GBfNoq`qSsrUJG^OCkQC-FR7Rw02L*b@a5W9TTj|&^5lP$32Y+W0exq z(mHC1y;&!rkzC^Qyc{AM;CI1@7Tgh4hwn^Fc%A>h=@m| z+cvqLigU%>`>r~F9;|7D0|G_!b;ka6OpPyC%eB|r!)SLG&ee(1o|?xI1zU!8b+{ZqS)@8}APrZ~0w#cxl2Y zcbXMtG|Yfa3qoU+Tf!Hd0jD3A=HZ5RyJ(GA>rvQUuRrp{7JzV$iQZ>Iu|&)jy&AP< z6g^$e_V;tH4SOIq+L?GHn{}~_nD+GTw2}o`WFoa-5;=4q?((7dLZ3^LGekT2q+sC) zDxbs8f0F25^abDNi7)IdzVTj$Dy#yxMfRkXA25M2)I>U&v`AE869|&)g<}cm3%9x- z6J|PP9H8^Qz%pQ%%EBB4jaVlv;WDV=SyRDKn@9#~SD$C)@*2%sb>oChYa!+T%3a;a z>zg0Xzs+jgw`@;xJ$@DjNv{Z{Td)f=EXv;T>7cF;AB%^bp{X!3&wvA#Cj7#hRjjCIw||p@ zyJECb;yxihaM@F3ugIzI$LNdDn{*9Cnt1O90w=t^UCu|fxx^~Zs|AP5xp>M6{u|@T zW5->z%znnodK{?t7%fymozvHE9FNT{SYXpJG+;Ec6vksH)|=}r1gsV>kmEqtf}VPp z$y+`zmFL{p+x0hdnNmv7sZS2ks#T|Ot~lP3$cbX#%UTB*7=qr4glneUYt5U;J~qy? zX8*mH18iQK(aQ~f2+bx_A1bXo4+UY@24RaXHy&YLu2?Wq zfD%inwXj(EL4dhAlGrljeIWNRnIS*-hCaz0Hm?SG^HJ1iXqQ91{+Q#1wge-Y)!mEa zhmV)q-5i)(*IpX*bY1n9dDas$@9=f~?zXTeV=mmr&epnO7LBl#cJ_X6nUrQ~74O*n zx5oFtJq2M^23L>YTMg;Mi#MRWIq6b7(0<28ePF`?;`8-66P)JDx;nT^Vw7vHRk7wl zx6`r#6;#sYRw9&c<{!Ld$FZJyj@x+sAA9<1e0b8$ zSbcPVkK~hIYo$a7(+ipae$hC&7}S69I>H2g=YSRQVP`@*afHk=-E>keafb}ElkU^t zup){#hp~t|V4}C-h|EXnl|cfyR)wy-9sJs;kvM%#Vw#WK;4&H+)cVsOEwxwe5 zW7K!6M885orDT+a<< z<9{=-5i9dTF+&Cm`Uks%B|m|^`8}j5ODVy$sdxT2WB&1tgy>=aS2fstoxL{B=t^0x zOgGLGN>J3no+)}uzEZx_|9rLIAkK{c3^GImnAy=61|wp~AJ?i#C;r7%c%KX-tf1cc z+~XaK*-`ru9jrSLkpc4Wpj<=2YMi&Vwv6lzdvvKn_8$mhYZWgN;}(>m$cz$xkA=f#GQ)-WGS zn7_6mceZetqc^JKthC9dB1d~xZNHWddj9bFYi>(nKEjL=y6ZUeT0!G$(59`9@|T2~ z;;hx2NXVcOz5O=!*O&-v;G2rqds=8s571BVe*^aZ`%&}EJBRb16HqgA!WOjRJ%zOS zav+N%Br(yk0)4`%#->OfhM4tNFp8dj+iu1pcK?uD!9T7V#J#?DlLk@077`y&yZX`q0%kv zdP|!qfyqR^ew|%Jno`;(*X8^5mSd-Y9p1QX8mIMVgIi{;5A4Ut4J=CF>mkiNaD$ z`ZOER>bw-+sGdC$`Mee=DzWZ+poh$%2)RM&N#$AOHBsyghvzk>8$~f;D`o_Er<&`Y z)vc>LC?n&pOmiTQ_B1v9u}mAC{5j3-OVslFbFSMZtlAssO(Uia%P05eU2ahm67#M1 z4$%R9iI`Ds7kR#{SRv|Z@3yOJ#kZH9l z@Kw$=FKajqDVzjHQPH2H$h#`VJ&HL4kmwyRqbxIH_A*7@;dBP)Z#=k9pFcxDdI@aODZ_=f>lSzsP{-s=cIO6xmE?r9jd z)3M4EUj#=)1vX|YlJRxbN6@rLyC~#y!JGc=UT=8m$gxpd=Wkr+8Dfanh0vwDeKE1H zr6vQS8)mzAMnhv3F=LoIOwIXLrr?jNTC>>__RgcB<2ELLR0NyYCTI2^<`Eh^`JC!> zX7O=p#}4tJ;zSNYY)p`=eb~jB$urDUoXD&D8b8fnYs&odgzKKjpNH=X?wr9|Ak9p# zEkS}z0(c7E$;Zm~hMKfy&suj_7H+^L@@od&iQ8>2J-(p&$@es-jk+*2h@G;i%bc0S zOzFL3XOZ^bD_4PyGR?QYXZO911M4d0^KQH)xti^j!#qqOlM5ccK*LwH45a(_=C`+e{qs!B3g7a zr$0Utk;(L&z=V^*ccT|LJuT;>NW$m`TdB*Ks938&8_;DMYmgDEL%Ty2&V;8-pL>R> z>B5^@FzpZ?QX`4k5h`9O`Uj->UH1vs0(K%wXrI--ldGDderrBEsB6TfXXs?M-{4-l z*R&Yuw=5DZoCiDTIUw;|Ueik-ROtC5#0sPdrNR=R;IsA(A!TQW78Q~&Aue>&754!r z$zA!LdW_U#Nc8U^)&y-=0#bpGeZOM{S~wePiOsTF$ZMGx!-%Q-l%hgtcvGq$j;SHN zL&;V=NPag0p$0@kM%VL|MCwx(K(HiM(3_&@{T_(Y8tDdWBxN*i@ko>sIv2HlO8y2< z>%0Zqb3W{)*h-7GgB*d}c0u=|Xvc6mzL-tS=%)|S$EAnX{&XBvg+gXc8K5txe3iDL z-w)K#D3QSvR-+L;UuP4jV{msa1(8oBm^3jR>l@mi1^+kUy#3p5 z2kUK*YoKtX$0%s~oYn6aC%ZY88y}B&Kk^S+xDY$C)B}=l$7Bmvc@BLYk4Pp6)iEDe-0v3>8zZ4f2k?wkVB&Kh z3;^}RN)l~G3z@yoY(CPnY9p~O_O9t;$`{Mxv)-sJ?!eKONP5OzLq91;;AlgKbjf;} zgbk6G5dSLE52GxIC!W?x%6@r(zA=}W&X-Ttc!^|vz9t~ofOAr4fE>BDk|Xd^toJ>` z@^)r#JI&s?o{?y|nAS{RZv)i00i*kWntz#^>r;&8_b8(IC*f1DyDs^|!Sol-r)Wky zd$+9x(9{GiPDYO)qsBHgil|{rySViRRG2c?A2S*Zp84whw-wx2*o$Amy1EMCmJBU8 z1n*2UI@U+2`YEP36}KJYeCEuHKv?jTWvzj-;e>iqKOm^$~Qa%T_4@ zQpk%7ul|Fa>{A0)-EBX3%JRvR&SYqC87AsXn^RX)Q1yaciy{-KV$QaZ0h~#s@)ql?hmS!%i zVWM!k&RsF#Kt*Ss`Y7OMam4aA%AL0zf;UbmrmU+bzIg5~Mz6 zm?z^h=YjiRvI4%0Ml(v*9H-`H74?szkLn7Peyrxv6TV<>UHXet+`Jb;Ins{>v-`o} zc;evz6TPjN)QCPIqw`k5YvhaH565Q%PZo>*Z6K}a1V&%)%lR!7b>PVTFY;s z4Y@^?<=yKVhqX9`-7e6g6xc#2%oN7(aA%$xOyxzYBPb2*Sd&2);~^OrXxw29LEFufy5M&3?RHdJ!hUW^&Z`68Ldu%89XWl4`Vn$x|+ zt7G>Hdc9W>IUI&rU!5br!o94ZW(@K59 zj=xwPZE*AdxFUiE0H0>><+u!ZZJrMI;{SIw2jv}kV`BaRSI;0=@SeIO(XBgV0W%*s zv?dc8i{?#2TOJ8%z~T1#O^A1=gM;KrwXRbRdK*O-~;p2lqtyO)P%bv;0 z?ChQJ$RgmGh0n?tP$0+hXByI#?gllpR)Ho!r+1z8Ewd*vQ!9_wn|{0#*)inoWp1+! z0-e=Te16g&9Xq_lDV^|rTJwPWdh`{0vCB7* zb}Kn*5omjIF^x2H`8t$87}^^--@AR(p{J_cnko{Cn1;N4-MA&6y$`Eq3VEN1b-gW6_&sNdC*wZcu z=VtS`*yMBQb;tQj8ifZge+-#%XsUX7*2g2$he;?hQbTxDV+{)PPK_MG1CcYw4;R?BhqVr1*aQ*nvky! zFu7@jUECPy?nmNz7j?^%8_F4xnnZTuN%XIexM2$oYIv~gHRS|VhXVBdZ; zRACax&cN%Wp_VdYwbjh1eO^{RQ-u8=9J@;Fa&(=~J=M9fW$v8R%y>lyrnyO4%sPG>W`E^_jOjzt#2adOWZOU zt5l7CrP+{-BOF?q^7m}RY9zXKM(RuU#?3ph^4Oir$Ai2>CiV+M-);AD%3JlQ>B4qM zIr38pUj-BO5Qa3JomDU&PXj+G_Tj1Q1QYuXac@=S@Q-|(C-#qSo?(7Q)cTrg*mJ2$ z9jLTdY9PGQJeB0-If&;8$DbmsBv>x*oLBro>IYzm*TVO;bRVUF>; zajEKaIHk>IGcHoe3ZG&ygsbxnw`RTiik(KU|E;2Mzr%i`L*}!- zu$4Th!tZ6sKoYC`y8+-mvssd{1d@T3coN$9aUj}IkIbelVo9=J)lkD|-T2Iz37Z0D z!Q%g8KJUNk%9+E>84Xxc>gI7J^9F)2t?)-g@k|7*ycuW#f>#iNj(9p1$`zh_M!x7x zKbJcCiSfHS9lh?aUtEq_5^e{>sDLGIn^xgmSt@nvAEIoxPK}a2KQ> zybu=ez4b>K0Mmz#OY+P!h?&ZqRnp=AfA{TlOWM;0*aEB9^B;`W=GrfxUQ15MWy zUa)`p^asZ)KGDfM`JbCxuCFZ!9e8r;jmB9EM$>|y8%FD*BQskAc3O3pZ?CR4btq)N zfGm|qfg8wYfL7{6&~mHKeLt?S{vgxcb9jX9cHL3&tj{+5L$md@%?)&aB9G)Ni{UjH zP?N{Ch8=Ge2Ag_$i?lZtN5!#&&xwrhuB^Lzc#fL~7Wg6G#;kZyvFp{nKGr3UY5B36 zAG{pL(i}&`;F`Ff{PL*}dOc0;DI9KjO~b9P^U4vonHIQx$99 z`>g!hpZ<1doL-V{s^=#y>MqBl**R}j`n+~m(X$^nw>FRQIPv~R=!nBDs^pdDrKpj) zi1az(S_5|brY-a(tuG3KGr=oPw3o zYbv0T8*NTwpry>XxNidWiMd*~)z^vAHc{AD<(N&M#H`l4oBzW?aVBBPgHmrufcEZ+ zj>TMsVLE4vAnS&H<|iUrAT2-#Wlr{DenV=X{EjTS44vBLWL6u9JZTEtlI?ErX-VG) zaAb=NAKeVv!2gWaBX)Q$2#E=sUXl;M1oCX0SNgmn5$;0iXH)f2!=QyLmJgnsqGvR# zf*$N6>GrtJ4=MV3Gw1L)l)1Fr%)-oNMD* z%qDW5HMJxcRnRUY>gxodj;G!Pq*x>uwv&9@n2+}yKafdqNz=W>vXDJ3kR@|UlpnNN z`(f+J#rel4XCJl>m;=QSXS}of6MGKzDxBBxveB}+tpktXdfLJdv|*!v0rek)5WkK~ znSH%0|CK1v*J8d&P0rGDT1o6n)G4e>9cf4U2rOFJqKZV{b?+Fqd9X1uFnLmko);}Kq(rTibfXx3$_1l1TL zSkP0Ti72AdS(k+nX}qpuEK|8~o_&S0NE)8-2_bS;bF90b@aN;Gx&2}Ob;NYxpZMJ% zND!G`DC4-N1e&`--r@S;fx~Fp|03^Aqnb#=by2(3rXAx1H~?uyMFmVNAc{z}q9P(9 z2qGZFp%D==BGODD)s85LX+c3ji3*5}ky#Ol5C# zZi;?~-W2B6=T3&oJh6+W-VxIC6Fg*WdJ@m`!8PHGRO?$xqze72H-<%7DnnkPc+YtR z{P=lfb!%UU$NT1kG+G`mN{kZNk*r!_e8KuXqZ#pJS{axe~$+IQTe<1;WPr5KW1h4KB~@OCUp)hea8vFt`y0x0S7{Mc(ur$WVo zm{t-GnF|I9{h7Nl%Vbs^vb5-T?CNf^eiRkWs=XA`;`uB)B%h6~t`NHcR|s~sOVEtU zqNS^&qiz{%8RGjl_EHO@m)AwJN6g@S>ruI;|K*+yR0<%8n)~<>#b9DcEcu{+@VQlJ z)Du?z8WqlBp6B*=wSlyjHqpJi?+~BWs48tAGP#@k_c+YkFXVN;K=Zh_5ZC@9&9g45 z66EUB{Ho`{{yi2xI(C44qz*^pBxWkVh>eo72BnOOk->n|_w~cE^PaoV%}2j7=Et%2 z3H!iZ9EW_;Y%sf=05+BN`6_ovB!LL2-RZ~s*v(slc$#kvi$}96__VF_!4J5e=g%7< zORIdS*{FDCC%xs3D6OxQLLGS7=ER@Baun@}kGYBG=BH!d`qE!z$ zK}H8Og~K=$?;L#vmQp^|Hxwt|)lmUBAAQ$*APWtAyi!!)xFb!AB2v7Z-=}&E|I>32 zQ>PW_V0B%8rF7Hg<7N_@huIu>-k9ub8A9PA< zZQ=g49fs7iBb1h6KGOs1Rhq%vmqnfQJmWL8;P%|(Qt{D~qF?Qp$sGgu_1o80I$uoj zoSaP{%P}fB`>%6p$azXjNB8@x3fUy-Gri<(sej_@4TbFPdie$U4CJJbsrSX0&h6Ye1!ls7)t46lpibq zrMa;wZ-n>$u^$EAMO!8w490S%{6e?`&m9R?lOey3PY1{6gPBB2T*?|2>q`10nAr;o zQPl(}%+}zK&d-sr+w;QDyBJY2@aL{X+?MwYnK-|Ud<~6ebn#PZoU8zQ)2;5-WyS`# z^O?~(n>~rZFWKnWe=%h?b2#c7{R=G1{iL$fx(fI?2rS8L$YpnHb1-*$Fp9YwP)O%G zp2&s#Odq?*n-8hTTF`wX-wK(fdW{k6os;%jP1>jqtA_-?)+NO=4yHM{BgTGAW8#%w z1*~`wo_fbHy!!rdKboxRI9{&woJd~b|Myq2-i;bi*tn<+!Ufc4|n zD^L6MNy-syF8WTcN-nT!i$F#$3Zc|h3yn8#o?CPZ+fGjZQYAl#Z=*oUG(F!a=22Xt@gt<_fDE1UVXD9DkT+Q=H zOC_t?(<5Dx>yp1VBv}W3P9fXrUtAAwDWzRm_+xa08qa^r$y)$R_|aKX1Ld|i35;M~ z?j%s;UDkg(P_##g6MNJz(IrQr)%gwi z&S)mDgnrerDm8X%!|4{tL%Fh>2KPv3b7^n>{c(gRdQPhM z)xk*SQ}D3z?_twrUFB9{ElH;w1TJ2oj@ns=$i@I6zQk0fkF9~8kPVF1!%ju7n&#Rs zj}7)UjPX#7p(mmeOKzJcv{NFdovB?31)XQ_mVNQ~9eOs_rx@i7RET;|*#O*l|F+mw zaT9UMkT&Cv7_Bz}W^HFHv9<(W_`HADU@8GI>$Ced9d=cix+U$T}Y=Py3fp!mLgxxz1|a zYo$#tXNVeu5RuJ~?Kb&|cqL?G?pDD^fxpxI1rT-fHt;-YOGqIqv7*iKqsYR8W#J7u zez+NVH`To`4&)U%`ML%qaMksS!lPe|tz9u+ykX#*&6U)kT-!z7Pb z)u^K()w6Oa9mgvzZBvGZzY&%AO|r2}*vTe;=Dj2S_|b;bA)sdS_-q(_hK`pPrzLLU z49XOQb%8*XvpjT|bg`$w7SASL7ii0sS%N%7CH-{Bz_+8oCwT_#;S%~>0e40V*~H6r z8w%^9w2jDR5shPEhb^L=9#hRh?$ekrk`AZX;m3FiY5R?ADmDBD80QMf8*8Ng%xYV> z_b97|Gp{-MQJ+W`4jN6{O?F)REl=tu^O4ZXtQMR8eAY!%lFmu`T;Ue6J{U5^NNG{0oQJC$zGs7<@Sx2T>~^(M9wL+3FeMwofME^Qb+KD^wN z26e-F!k)c<(u$n3(w}%J|Jkog&UhGVa=#J~fO}7(H_+{zmuQQK$YQD>Jsf;B@XA~j zO^oVW>>ba&N#4gFviPhZj@Yh~hmbd3q)fdBxz^CA19Y)!8lj>cx8il=?8kQ#_ssbe zv@Tum)qhXv|L=dk3&o6S+5Zi|S%DtT)PgjEl4*jX&N?XFS(FiOj`-B_Kp;EWdPf~d zA)V4!iw5=%xE%Y`^vw-d%^ZCX0_KNh3dI`qsbpLm{-)az#Fl{~r*mdln-LjgUUho3 zmhR!HIM^FD9FalJSc<{^YQ~nv%}Pg;#ok$tfdE>(ul|3(-v7xG{{QFiU6~+u^PpZq zUx;yKf4?LiBb{L#i=^Tq(l(Q+Q~O&s8#(!LnA?o4BfZkq)(>|(MCNA7 z9^JT?^flTj(7-^ znC_wc5eqlnZ(*G+=e|#CEQ77-%%o>b+(Fza&3PpOef(wJ;iLR$&$QtP9nwAqRE8sv z|KWh6;raZmkm>J?p zU18ZN=iYEp1r)dZJ8h+P7KTyl)Cc>WXO6-b^RX>vzKTpiVwy&gm!d!26GTX&bcKHpaP`$L*aR)|K&nt6@y`vnA zq%PY_MVW%=PJ)M=U5a)lO(8=W-OGkNv$vuCUs}=1qbXmxz+@pVe#&|uJ3B){E zb+J_rp37F>Vp@DJ1ncWI#4XUyLKh0>-JPR7b*@Cc0Yp9$7w6r6!{qFRk`xiUwCbXK zx1_MX#$5KsynxqHXKr+}I&Qn5b=U!RI-0o{)AW*?3*_egnH3(C7=O{t6GXi=D%dcA zv?P+VwbGex@IAlJ%(K|S6VHxeS9jLAlFgt*?3UP+IjC?XJE6Z?xJZBLaE^ejvAsU~ z%?qYrXwqkrndH%8GM$7j7Plcc38OrA8nzvoQg4jWi7tz8igm*?lf8SB)j0Vkvx&QC zXF5mWU+W!~YVo<(VyER4Hhkk6zw%*Z0PI%tk>L`ULE_*`v}H$$SH^~NAXf@Vi%f9i zy6o|s`;sr`b~M4`{L(YPQj002FV|0-O=)k%A|~|q03KgKYcEJ?j(yZ}1YeTXd2Gt= zHtSga{nQBmmymXMEK829zixGS41HT$rX^01Y8wMUOr8X0iVaH*dSpU19n_7EX-%+M z8(lt|EvPN<>^5Jyc_M;bs6^BZfpBu+ch^T~)bS4vlle(qJr%(?9^)9*k zhT<=I`Jh}k`K}v6L~H8@i!Kzb<`qMADyx4}{_%t#I(RIi4(+_$F}tV{ajWu{-Im_D zuPW;c8}DbFuAfB=->2^ejHld<*X2}P?Ul4j%x?9W^RTEYu9Y(B;d2bZ7yjkB0dfIN z60&>$^h)`XjLIGj(vt~ync0g4CqQ&JTyUAy+;1h2uwnG!lJD6<0`C$e<1P3Tu$6YO)q{adJLc z@?oHLRP5z{lHP9Lpg}2=_=Xj!_D9^*_uk&AWuRIG~eIC3BU;)WO=x*3UxF zgn=#@K->y3)|G1{Aaey@CzZ(N*bPxfYC#ho>F~ufqsZ6xPtf9`1^rWQ<(#lBMlL2* zDuuL&9<<5v3&_ChrU)=z#kB?t1vb$>9pIBz!$FFMAkr@>Na$% zQ3HQ>Sb9vN!=#I6pjkPiZH258qeuiI`jGFb z9pb1`%EFd1BB5&;FRu(^|S~&}L+;`E=H(NhJ**PdS{Fb3MSV8qIYBIuwYbfnyTe3tEmXyAf4x zq>k61=M-lqnOne|E}aWx*HVc^*XDF2m$e9s?HetW5G*=ce4=PB+3JW4N!_GC7=@L| z+A8JmcwMn|JReUFW!B%UGSyLfJFD7Wb;c}8F=sK_&j=per!64c(TE+!$y8q5r0b+! z8}pZVRvTxrKk(9diV#9qI*(=Umr}u_7Ayo&jmGYtf?hMK+@e%fhMo@ALzBT*$(d^E zbTA}ag^OVvHU>NVeVPSxCw&^LGA|Ye)l<6G+$QPxRkSZ`k5dv)QZcfp_rgIQ56{5w zj`GSGt$E}eZP3+HEDEwn6BXhkGP!A#O^8F$d&)zP9@a!v2ya)>YCtGr8i?rF+FrKY z`wVMI_*Bs8YMPd%Sh>N6CvgZ`!H?z*E$$8t^WB~M+Cp8!VU-?QxdG$LR$@~5Hb{L* zcI(oP5OK!aJkwe#lbnOt2OFLe4Gm3r<_jA;clp;5JEMKg1&G9&+G;8@tOrXtpVa_J z#&^p4t0 zmgw>KX?-nY!KLiB#BNmVEq8QbfhV6yx!F~#Pb{Zm96yeR+kF>%`7Rvx`O0nJt1HrJ z_Nyp&+4@?_`}=t`O{|?1Je52OjeJ|;Dx#@44ZRgRUuwGv1$0fIR6bY7n)op4$_35S znck{#E1BdM>Az~)ZL=ml`p=sdD*o|_XA5qGJl@zF^*2l3kI_9WR+jj7C0<`6bW6r* z(qnPtLryM+h79uDwwbk?mL%=|`?9*Tug00dhO!8<|Em@YM`P)c=a`)0s zK$Gc{2`Gwwil20UE5zERz2(fwi}c`EYE@9k zo{288RnK&2j__XkFc$D(P6b+qJ)9Hz${Dj!#V~VclFK@t^iGuS#PtGyH{B}e(i?Vt z8AV1n%>%AI2dOHqLXkB{pi0&fQCvIVq9Q3u@K_ty<(}FTvQjPt7d$Qf5-;LZ&-ZCQ z;-JytWxTU!tEy}dadVt~hnjo8JmLigYHeq3wAr@;VY_`ORG=V=o2g3VkiSCq1$%`_ zwXU2uu-ekc4^U4*5@QMvq?SF$`U@&M&1( zL;juG;*BR{UZQt##p9`cM)$~RGB?;fXyI&YlU3!?x(?3qCbUC4e`4kLiu_PeJGYj*>J1qEt^LwAiB6hV#E9M zu2zknI}IGa_sLS;$ev+zC?2Z=qC)5Wj{|kI$Du>18J$$Mz^#dWh+7B1;`&g>t%rEy z%WJep>ExUmNi^ERhXNKVKZnh+kZ%gM4wWDLwo+Kdt>v}|mAVBL2dI-TWb~=9Gxw`t z^A0G`Gi7N+IMYl*WWPxF`875ERh^1;?Xe?1sTrGiBMRMkc3hKOO70tCsjzA^DAK;6 zrUV=f$LDQyo+HkxWKX$9PkIAMx(7pUL)OAT>-n%F)XmkN2c5O}yBL#h9$*i-)zqtq z#D=2G+(BG7-YK6+56VYWoIR+``RBmZuUwZ9^?}>Uz_b#Fr17ke!px{^TQcx6P&Z2du8y)8lV96$+x`MsJ&|Ri}&;e~+cB*@AdX_Bu?IIbvZ2VB>W!LcICjK7MQF8vOXg}10b6H}A zOd+y+^w&^N%8@Tr3cx`TU1j%-pl3N(yzjC%ocav_Yet`?zN``+-XEDUHQxEIOJhOp z-_(&F6?C|n`#Tosu>{RiExkJ%v}35Zi2iC5B*XHFfZ)sEVgLHq13gHDTQ@ym8oNS5 z_zM;#!0XUiXut1G?Eqc*2BC;Su;%Utv?0b3AAF)RD^qus<^ps zk6g{K>!=ZX{xP?aB-B$dxhfZt+2E?*rzLHPu|pUwM}#t(bf~uSGTiawBi53`b@L7i zKLR7(pW@5GSi+PBG~Uek^?{{ased$f>RwJU2x>X9)PSKxerz@5>*|~$7lj3AyPM%- z=@sNW`N&G*@+F>dvgmw*t`~4ZBK&0#X0^Nl9jmIFy1GbXkyJNrsKyjxfaFtXn#MV? z&Ep!G){wL4b%DBkJrj|Pm)-lT*7S*E?^532ebxo$X{kM~iAv~?Df>SSv8rT*VPyfh z6-Az`2=ZNrerG)P~rIM}RhgGJM8vQem09+LFjcF~dY?yG(3mrT-I{-TaF(%WsG6Hg!Y*Cif) ztbtH&8AFr`fSIp+el>XiB?B{*^o|ne;wJ1z%IHo2SaF!%Oe-ZKmlaAwKnDH>@?ubF*?mX!FB)VY`*iD@9)lgdQEkhw=`w= z;HU*)e3+!jHv?*6kur{cDoD|$Ev!W!=nYpOgU=CcG5iAPoL76r;^Qs0-p`& zx!1O9khb?n?Qyl0Tq{Zi`^uDk%lB#i;MA?Q*-0aE($Ai4SbOLGCt^Ag?dHS{qp#p< z%ER&98UL_Mmepvtu3XL4!aUwWE52!;-$d7!%tsO3x?rh$QeeVhPswOSr5#%_AwCyf z9q>Td4ITn`km`bVu7Hhgh_6HWKCJ2scQ}&p3uc55_fsI}4V{BZj;wg6FKly?F*xr( zM);LhJd&3kJ>GZ4}%X+b#s& zkuU~#$g|+xlhIXC?};9!JA}%aA6=!4(z)!(8?B_Q2u{q^zfUTWv+}y9%Y-`A4cpjh zOo)bldC8yKI?UF};7oX(kriD~kRnW8^j9VN<@-!dL>$#Hktzc=(E1mC&w975zEe_b z?z+%tE8BqKAk~NvRW42*j7}@e$qqVwuW6=y|5?MS;i18S2g3PiA9@B< z0nNn|GR#-66j3m55lt-ePxrf6-e#HAc#Z^vy9i@)Hw=;iP2P_jv$owseO?1Fo7#PV zKV?`D55^e5%T;f7M~;?y`~TCr$8f4Sc^F@4LIo9CyaspBADz5AX?P4^8Zj$!cG?rH zqFk6^_LdhCyFqSQ#gm=0uF`)AyIl`PUt{11BKay@y0dRGqikN&)bLb~H8hO;eHek3 zj!ri8BInSr@g%l$wzKolxPj>LhI@_wD5_jP5TCP|ryE!+(P9f@^JF5QPP3?gmYel` z8ZR3y-oR2GE&B)E>vUw++S`uLB%i3O%m@vT@$qGvJD|h{}`1xN{kuy;) zArn=R)Ja#JoEoums>Vm+Px$9@ikOXQCSDzP8rJ(5BCr_c4L5*}Ju58r^6mtL9b`_o z(^Xf`7iIou)=L*}JrY;YrtHRmh+27N=xuzwMm&=KWXg$ws)qd*!g)pXi=z8$u+u1{B zs1oaT(_^(tbP7=%H{I-Uh_3GdM#tOr|9NO4 z3lVn-hI%;$Lpf zjWz#IxHbP*e4GFI85O(&rhXlmCa9)e6O-wh8mJMz6gCoRDRz?wV3cQ|se)|o;U>i^YIK2Z^ z-g-YqfexKjRPg@K{`*f}-v86zQ+)sb`n&&kfB%1TkN@v~$NmizB^{8@$cF+enU+Kr z27?x6qTt}1n04QD(#hrS3j}iuyN#H;3NuGT9rOr3p7IVRRG(QjXNh*Y+j&Vw_f{(? zb#~s!?~Ez_AgrR+4_rI;5>y6^0*y{UK)RSAiy$te9u}O|3E9F=35{dhNB*qGwU!7c zXTNENu|M95u^>J9;xalU-$GH*Z|n({?eEYcz^$cagJUE3fC^70RmNfa9hkDZR9rB; zL^(?TjfQW7n|C+0rD-RT33*1rEfy6UmhU}-O8)k<+8HA@Vq6o&Oyq$wsB;_vsP#mA zky{raZM7=xC0>JTPaFY8MiaIAXN5-4wurY9TlQvz634!p+*!}6IR*X~X!xS*YPCa- zCH#!Z*WCa`ZYb7EVnPb-x|4|BoL6p*o4?7DBN4isDSfz&fWGC8u$dw=l2V7<h0?cYitN&EJM+M?u8%6q6LSh5R*@g65@$4woFhH` zP?92hO59r`$qhF6eB}4R+{lX+RM@&>DTCxoV&1ZMQ`$`BPHodp*l9O(vh2?4IJ7MkZKL0zx{* zP#X@eq9;2k6^PyBzhY4kz-%?Ha=m7MMB&j`(Kv&2eFcG~0V(_kb1Fe+7uZQw_3>G% zlD%vnl?;Q=Cg<`-${>nM`9{O+0gqwfA1QJ(eCyJs#h3+JK_2OU$Vca_?Y=7=YTTDmMavzlFk zqN!s@$E4jjVE99e<=qotla^!jMErmRX}u&|ex{LinmteWZSX6K9YJ%O2sJvN0j=Lc z{7n*~=uSh&F4E_tk~+GgIW6l;BJ>|db(1^Cbr2kfhHA4feSB&dN8(IxY1#`hw)ZDe z`SYN!hHp#i_=;Dk4`fS#m|Tq9K(e^yoEEywPRhGuvR*jQVT4`ouG&7)7=1~D(c(Ii z@>Pf(;pL9L?|m=diyP30Va!&s3bPrekXp`rvxtM;^Sr%e*@)Yl14Ff?Ii zz*gXJYOgH*N`+Y4m3E7z52ZPzWw11IvtOP~h=m0&gB+lBG`iu+-lAc#skB@6Swb)8 z+@MeMkqarprDx_(N2_S}d-yig*b%JvPT zlfHImwO#xG#0N(3pxM5+3l0a01|M&5;#@bOxL+FNje}4;_xt;J zlX5y)9Y7WD0NW^$PL$}{ES(U-^wK;x9caIzW9B}3(K6aq>Mup>Fzv325ON*!Fn0MF zc12#geC@;MZ52_@(v~MXk07yyRkL_-$(o$rZAVuocq5x4c;e^V0CG*j>qR zmvZ5N-$Yr~$7r-f%o4xyD2>rdbYdj1aw9g0$q7QnO-T-y9bNcWlVdjQzx(1U0GC!#xIDp2m8cOU&9NFf)k)WAw?ac1OZ{x<*JFRnvUmZh z_*5J7Q)+hOeOICSEV#bBFE}-42fY~Fq127%j|+7laeg&6U4L_GndZxp!PyTEoaN+P zQ!k`w$+xRBfF2UjVJaEosui+8NffRJJ&@|M$H+u(>-jhxlRtAapguOmlUWdVpcTI* zQML;tjuL832fb=_M-;UM-bh+V5f`NPGX`?AtFM%UZ|=|n0rR0s+BnmBUymsw(i={Y z{lwFgWd6Y;$gAu3x_IGquSrYZb#8wb|DX!6l(Z*!`P>=-=1IMbj7`Jt!ky@_f^?8I zSkOXP&~PdDKNb{3b7lAPH07b>hca|^QFrff+oOK$YdmqBkBlFAX z&#lQ8^yfJ0>rW=D%aMLgF?|sznfyqrwo{!Rcm*F_#FJF|#eJW4zdq2I0ejCGif$s+ zin>Qm$s|}HXfjwV*d_z$$_p(=lu*%%Ph1x3;5f*;*gcD4FRtFWeahv*v#Rl@N ztR_w-178vY-2g@N6fI@f-t1~PKJsl0gIecTtuUS&dl44bb$_^w_Y00n=~o#BmMRqo z3fQJEYhBGnIpG6GFRL~hB3lik)Shpq=tr=sQT3h+bXgB}!`4kpuF6O87pVywE^-XB zt!sS0rZsCH=68MmbHGf*W_1Mj2`bQ=V27Zu<@}%(5j|uw<1BqXPD!>@#n(8dKPB@% zaCgIwM*H&7T1$STZ-CT9=UvWE@MpX*i^?{jT&7AA<3ssVvO=BM-y3om zlRZ$8@)!>#mkGKbZbfn@V6thUqOa!uk_@sMQP-67@+dz(gG}Ll<%E8)!zguRwm)lm zG`SjnLbjQ#g~((xpwc%lmQ}H3MZ`Y^kYSj$mJMO!%CNQT(VqK%0nJbu(iTI`wi9{K zS}KY;S=Di!VcLfaLgl`84Q4nvYmqqdemwXVq$R8Sw&^VS1>CgU z2tDa{2K}}wT(+grizmRZ%8_|sXhF?Jpm+OtN>G>+%q6R-Y9{lUt0e`S_Aug^ze^o0 zzHr9Fi9G&i^7$LV-?uffGHLGuFGv#uW6Jkw7prd_Va}8rd1x%g;u3Lt7$c*r`c+!j zW;=?wP=Z7~s{GqaCOX=$^7lKCqSeEYn*62Jp)lVo3t1;f@r<*f}yPU5c z1N$ABSkk&yY0$FTX-)TCEah97;N)Dni572XQdEaVyJ;=e6XlLU@gr?tr*b3Y!uFr!{kvcD% z(eiR&{?XstpZ@>NgFsK8wh^~{ZjS^2h9q3*W)*!ra9LjWGU0=$lUl?$?B;D`hhg9 ziFn1Zbn`h}U1B)YlUA8NIK_wxoXg9X>v~fq`(B}EsV#c4T?H6b>Mc9h*j2B;V%vGo zHNhXgPumZkiro~F&nE+Egls*t9g%~j5uVJF<)nE~1|&rlT}?3UlqmZo>BEH?Z=Xy4cGHVDP$+uF^aZ-Baqj zJozhqp-$_;TTwh)QVo~NH1hAEp zGv78OE%V+lbKb7)YnWy{&#kJ8b^~#%tjFe*@FaWHO(9i(<>hClYs00R8=2G9poCt; ztf6#NDjfQ8mzGqq=7P4*Pj-GfPhI1W;G3w$CR2T2&v$4jENU1H-=tK`m|DtG7~ftg zI2~H`TRVGXJrij0@3L)ybhV;kOzRG4phGwv{0FXR?`w&v3?*VW;=7{L67QQN=ap=| z7F6boNGvg9k7ulpQ^;lVH+&_hST%Ysa+0i$?OJavobqtCCTi8KZ98aahB!fmo6b~fGS(Gwr}1ua z4J_9GcBOjJ)_=yTSPZ~}GMR@KoANq2$inlNtd`&l>wRcSSnTM>7%P~VR)W0dKK9TK z$tm(GZxJdPRrpd7{ix`s$((NjSzqZRC~E!r8S-;O_m=hc)gbLzM#=XJ)$%}>*r zI_Z3EU(Y7S$e$;Q$G9K3m`>hMmHZThB#R5E=}c{c6T6-9OI7cz=a2lO=!M)*H0&h( z3NogK-3N%b#8PO0i6fVWcCv|I$Tg^_U*X~CxYJ&6b7*hKS^FdDjW%$rTk=)7HTF=t zLSzRgMwO+~;b&%_ab@eqRPFPf<=7xi@`zgF8Hj$5R45f{u)qYHX~}F+Z@S*J0(J z@TsoD%h$(x!>NspZP@Q;bZOw?{$sVy1x*hxM9#lHt9(&ZVNY+tM_${XRLmw)6IDzj zFNBQs$H(`pZ>@qf%F_Q{|^Sj=H zQC-QI>r+A8SYL8);JB!AjTFg_Iw{)IRBRS$jU_brQ-4Q`m7&yV&#H3yIZAd8>y{Yx z9n8vk6WOKCs3sbM9s>(I_d1E-Ya7T#Sf$mS-b%adk>{fxk<@IhieoSl|6{IR7>rCZ zAXJ#AB(3|8>3c;EzcPTG+{I;`0TkblC!+RqM!*V)X zpM>gW^k(}m1cv=LDQ;(low2%ujDdk#$dmYr1f)~VValx}oEHEpoUD$3=Biof!>`oE zIl~M6eBUlFO6wH=Vrga@^5O9F)?NOQ44onBVLX*uYa0W3(nzfdl?8*ubumCi>Zb5po3# zn898{o{kc`%6ELFW)Ht*FJLZjIA*4i;kx>QLfgde7=-@3rXjGSGuH-#zeLVze~Pb> z{}oJR&~V#NRq`ZHkMRYp8dQA><b#4mT-t~8v`*PzE`Ss$6#c2lTSkfbITIWzS3ykbZJ`mmC{|D z?2Bx226r-Y3E!M>3taFQ@gVI)UC}0{XWdizKYmjsTZ^vp9oGap1y0%DA}a$J zcb-dmr7%l^4T@s9A}y)?ZSlCep?TBBa|y4HF5X?IQ@JhU^{bnttd&0L{^-D9eYxLeTw~*y8^qr zR?y}v%Aj*d-M&yZpu?acG6y5B{5c?SKoiMk3+ZSvU71|qXT{GK$P9xGsy2@BOI_B! zs;!A0>9&mAwrJnE^Ld>P=Jk~EtK9Rc%7xE)VA9w6bb5*(gA+%OFfjnb^$r@J`SShv z!Y0I3c#V@q3r#=2F&$6JS$1i--TotBE57Hsb1|?CrOt=^x@GoK%W_WF_N}SUL#hRY zZ4qOtm2gMZ$kuQBAX+_%o!cA8v@ZntQP(3E)7mV^For0PREK#i>&B4l=r_e#hGB`* zu}@lJdz}7V-mR2|ZVtCuTs)C1N8zAFNWSEADyzn9N;T(E1UB0zIn% zh+(L0rdBW`O}}i^bk$gQZiZRl7A|F9%J_XeP=1~>nac<^BL{k)0$8}w;&SL^Db@bM zmv6?6Y{wvLlm}yR;cVBNLC=f}pMDkbg1!g2m zgNI&k1YS?vy|yPq`b)N#TO#OWoo8uKJfq?ZJKAJbbzcBB$>*Aj;mbQK^>>56T<=BI zn`~;TsNm(zui{Cn=<`XABv%0yYr}?l=LMZ)6*|8NJ{nd4qgtNGQ2YE@fgk%2-e#rz zUtsZ(?Xnk*h$pRWdMCn1n)CR)%v_%53%`qJ;_7qeyh3XdMtDBGP3?3g(EqW#g~(~B z5v_GelC0<~O4E4QUFCK#vg_NojU%7~byB`=1zP(vw;a5WGnpdhyoPTevd_~Be7v~*Max5$ zp`s0B?I^3#hc@ihW8XlPI8w2-)G*ZJwLUEjQCcOqWxR>@Ko>F^MQNesfJ9(ALCw-q zj41m2fYV(Ay9v1RT>?DDiqFNjgVMm`*!|0iLLk~|$G>>HDMFys9_8D8~<)ssw zQ_o&bwq?BUOB||{bnXk}g#NfyQUx1wYpakit)x2oFZ%B8m*YJ$g^t%JtD^pW{=PeJ z^CmVjXKVYxB(b6;t!O2ckmtK-2~Bv6&`PNzbgwNg4N6B9+6M54(yrba#V z41e{Z>6ay$%3DrCk%RT5vYXp><4KnXk1x?7H?)+%Bh&s5v)cUwL3Rg>ULB7 z@njvfAUCwOD4V$4tf=H#q}$T?gxbP8ysrxnKb1C#^Nc;&EtR4YmBZmel^hDFK*3N} z=R6M)GSt^ezh1N^T&cdmP~nzPp0>j{TPVw=M14Q?eAFq z^CuvjG6H(}!0@B7R||6+b(1gV&!Fbm|2^d0Q=#YiabAR^FI1 zx(3M*4D&oF(*;1^Y->G9mo@Uz=rHb`l!!K){5y2{>O`2){{ipm{}SJDdN&oLsTURs z@x{IszgKznT-%Zel!Z;5GZ;dFz?r>lw@s*xeQd1ZC&xrjzw7 zChMLMX$>$T_UR*g>;pC|fP62rA{nSD|Jb(yP*?*gzY835Pa^WSg47SAGD-C85$-6L@w6!(1@ z*#WFumjeN6sX7rT1Q4u!8Ft)ZLRRo1;I_^}V1a}&h6 zEIrxH7yyX(zgqi5z8@sato1M%K>3gu$vw$kR9-_`t}vT4ptSu4(NB#{5SNUpn5@m& zm3}KTHrsW_*|@U{%pM6;2_muL!qQP=w>JIBvvC{OKY6jVm8REzpa>DNP z%nutL)~5zo1$kBw3L!ePQ&GQt_How8k835$!q_-rHVLHfu8>ZX){it$+!r<4=abn_ zlD1Y1+OBEHmV3$D-USmrksOtblfsb+#`kGxa-nE_EzR0C=4fpfwTPETm9?_Pq0~w6 z{Gc1D-r;uM>mW+sAFcw^$p%CmhLEWE`?TS&FIG#}-dJ5Bs{;2nx8C8_{<}%pXvE$c zTO?1PJvF-wu6)MXcmKWP1Gz%khqIx!2Kwt#yVLtOQP&N4xsh*dFZxC8zUH0QMoDdO zc~{x0X)%i!_`NV@ER~DxSAgo7u8AUNmYKs5_6hZs5_^$wYH1oG*{}hsQ}}kpGPvZ-J$6IQ zfsR2{mI}#M9EibXxK}46sOLAH_*f0BV8_;sAfgjz;0?aKjdokLG{I&h6_evjk&bjnG*mdxbIphrb~%186p5@eQ-CgxIba!IQIA%+WBF_HtjjBHrp+q zg*|)qy2u1qyGDMW*7f`rwaF$V*-~za{}fF4mhYH`e%=pXrNrJ9DmIVb0{+qrf;X#%K3znB_pLHUX-Ctl z6O=bIS{a>lDYwW?7z>f#Dl=PiTy~HB$Fl-}QHYWS-1Cwq@`K5)ySAvL5y|2ZD!ln< zXRQ$zsP0p3c5Db41)tvoX(?j7kyh{#%rF#pKTV_}Zd?C~VTX~+*gJSRU2!e8l+@ZC z8~)z;#r}Z{6gIM4!_RZN**8#$rz%#TBDW)}FXA=K3zFWNrsa=(OsdoPgG<>pp_zmEXR?dl)EWJ!4B zyi?CcW!1#}p{E|9Ja&i5Eb6fVDH^6x9Y=v>JBEy`ptXg{Eb3iHba-bNvn$6Mx?b=^ zZ;(GZ9ccM3KaUACkX-9kGc=&oxL$(~XE-1%xUnFe)&_oIbQ6qF%K0pp?E`mhsgloV zrahrXyTOyVscTd}r5}ABAXe_mj}ni|SDulcmcxw57gR>5tDt#JL;gv8H3|7CR)y%Z zGCX|q@DFzS9dRef@fDPbly^R~wnUjZP!P#&iKhj+OITB-cJrddqjL3uBbhEOtePU# z$gNY|ws)ekUTAbY9ICg!?F@JX8N~f$7lC{icE(H#l6AKpoqhow9TP+Lv?+Egjqf(- zZ)u(UeyU8fJyS2_C#AbC>LAWD7ZYK+&MIgtm48a}ML_~cc?em~8@oV4?Em(|j<7#~ zL%!`w#JjUwsE*P(0+>!6a(&(!ln@CwAN>Zso6m`{Hv`K2T$x@9r6Uk`bXr_Ie>P*XYrkQiD=bi8Gci*r3 z`R}>!*YnTwdR{N<>74WVocHIwug;{lsazGUEdF&bGEqYW{w^IATzj5p4Eq&`9d3~E z(on3PoM~gEP<4^g?$8iYV;O`AmRi1^{NO=DyFk!UW>ATcl{q8b03)N$$$hwhf-}R6 zxRVrB7631R297f;#coQuNq$QIeRgZ06OpxC~1G zRW_=6#%B%`V{1pqm5HbUAyyep1?B(L%Up$NsC3#-;4Z~`d%#LcAI%SBbW%JIOzghe zYr*V5M5*ZOqnS^X2GpN*gQS3N{KwHhU(h3f&oJ4Ann{6Wcwg zBQLt)lQUR_Z_s{_s4&N1tJ$Z2e0TflB?&7QRsWP38~Wg!$?pD$Lv0p0Vp4XZn^5sfcE2k`&M z`j~~L;6u*1`Q(9DQV+w;M%*-Jy}CjzNV+tdmQ4wlYBQ=*LAcNGA_6uH#T5zLbJ$OI zdhcQXS|RqoR$zb=${+Y(q1jd8mH{gDg3{yq@jWoaPI(7dx>#i2&TGU!7jBe)_ugzH z)>F#1to`tx?>FG!>uKWI(%g)o_Ix(L|K!U&A$9_7SVSEINtVW2vi7LnM(N#&ET4(W zd_VWjcRbN+;p>woXi?uAPzemn5YcPi>oC!MD9^3r!?+cvld2cwaWv(SC7M2C%eE`0#5j|TEvSP)K_N&2Tqh9 zn>>M+-{E0IDqF~B^fCI(XqNacgUyh9drUsH5v&0G9hARZt`x7) z{yrgoZKBD#`c$c!<0I1re(zs73D&XjKXIWtIkkMBkzz3nLl{$Sp(|)mDJ3zmtd&r| z!0DZ^o_^(%GgCCym1ne>ml&rM$R5dRTH{6BZ z{ykpP$1Vn%fnP-a+)2-u{KZLo^f)C6)^mokZiCaGtpDJ*y&DD%A$gtte#XgS#k`WH z$PGEG^z>-?`LO-$)Pzr9%9nTwn|S_Cor^lF8sij|7Bvimi>xg#7{;r zxSGIV{6(xnjnE|?1?x__)XTTozXRyUOVxw8B@P_Lw8+pd^cy*{qkC%#^Vn)UE)76p zfms!#Nk9vuAi+G@*>Ie%$B{6$+mo>k`{)x_8O0B=|X^5AStN<4Jo|NQ z;dR$p#F=!2eQ5%xSq0lJT5DVRmAx1>IYAK>l)U|rccJ} z(Iw2iyf0+F3K$(X^Oj=Og~pb}?CGxle5=Zt1sDx&2N`)*dOx!tA250!r#1s z)^fu%NA-J1@XZ<3UUs}&Gq}vf0>BJi(uHdk(g!(E8Z5`j3C`n(lhU#kCVz_@Du zc~D7o!AP@w^Zv>8|DDpn`qnKTaevmn3y;v$SEj33y}=K*8&S#!z;n@ zU;koS`@yGn6M@-7I4)TO=8pdf*Ea~}2yOc9n6CCgcUQw(44-pqBz|S&cSo0IhI0nk zhJd&Q7S#XwPo{ecTIh*7Yi;h+U)ZK)dt!BO*yjBYX}EzveEGsE9_MIXd-w*PWKATz z=~RVTj*j?>E3jd+-hZbsL(D$A{OuQu4BcX=PC~wl2s>9b>6y;S;nK*DRW-IKc50|$ z5Zkxm8n#kw=<4csWWjrK#aIf}!|!1|=@$>XnSqF}b4G@!#ISywJA%{H#V2m8seLDz zGP(c9M<&xY$H~&@ogU{T>ja9C%7nzR3+BENV+}ISB_i^>=3N!c=HPLU+RTi#I?tN) z+m<2t2A=qux3d)AU;?7{O(5T{v+_qljz38Y;)aT3(oYEJ;bqm2mNdT*c>2uq=2Mekb@s+2oy)*&

    Fu&GIZ@c6e0mv6_PbYNGNYTj)RTR%{@3{!r~5Dc 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

    krxJ;mH&+uR5_*o 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 z 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

    @M~b=i>O5W?_Ap0Nc>M3 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 SV A&H86u?ANL6fc{TtGH$Q{ literal 0 HcmV?d00001 diff --git a/cope2n-ai-fi/api/OCRBase/prediction.py b/cope2n-ai-fi/api/OCRBase/prediction.py new file mode 100755 index 0000000..f986686 --- /dev/null +++ b/cope2n-ai-fi/api/OCRBase/prediction.py @@ -0,0 +1,155 @@ +from OCRBase.text_recognition import ocr_predict +import cv2 +from shapely.geometry import Polygon +import urllib +import numpy as np + +def check_percent_overlap_bbox(boxA, boxB): + """check percent box A in boxB + + Args: + boxA (_type_): _description_ + boxB (_type_): _description_ + + Returns: + Float: percent overlap bbox + """ + # determine the (x, y)-coordinates of the intersection rectangle + box_shape_1 = [ + [boxA[0], boxA[1]], + [boxA[2], boxA[1]], + [boxA[2], boxA[3]], + [boxA[0], boxA[3]], + ] + + # Give dimensions of shape 2 + box_shape_2 = [ + [boxB[0], boxB[1]], + [boxB[2], boxB[1]], + [boxB[2], boxB[3]], + [boxB[0], boxB[3]], + ] + # Draw polygon 1 from shape 1 + # dimensions + polygon_1 = Polygon(box_shape_1) + + # Draw polygon 2 from shape 2 + # dimensions + polygon_2 = Polygon(box_shape_2) + + # Calculate the intersection of + # bounding boxes + intersect = polygon_1.intersection(polygon_2).area / polygon_1.area + + return intersect + + +def check_box_in_box(boxA, boxB): + """check boxA in boxB + + Args: + boxA (_type_): _description_ + boxB (_type_): _description_ + + Returns: + Boolean: True if boxA in boxB + """ + if ( + boxA[0] >= boxB[0] + and boxA[1] >= boxB[1] + and boxA[2] <= boxB[2] + and boxA[3] <= boxB[3] + ): + return True + else: + return False + + +def word_to_line_image_origin(list_words, bbox): + """use for predict image with bbox selected + + Args: + list_words (_type_): _description_ + bbox (_type_): _description_ + + Returns: + _type_: _description_ + """ + texts, boundingboxes = [], [] + for line in list_words: + if line.text == "": + continue + else: + # convert to bbox image original + boundingbox = line.boundingbox + boundingbox = list(boundingbox) + boundingbox[0] = boundingbox[0] + bbox[0] + boundingbox[1] = boundingbox[1] + bbox[1] + boundingbox[2] = boundingbox[2] + bbox[0] + boundingbox[3] = boundingbox[3] + bbox[1] + texts.append(line.text) + boundingboxes.append(boundingbox) + return texts, boundingboxes + + +def word_to_line(list_words): + """use for predict full image + + Args: + list_words (_type_): _description_ + """ + texts, boundingboxes = [], [] + for line in list_words: + print(line.text) + if line.text == "": + continue + else: + boundingbox = line.boundingbox + boundingbox = list(boundingbox) + texts.append(line.text) + boundingboxes.append(boundingbox) + return texts, boundingboxes + + +def predict(page_numb, image_url): + """predict text from image + + Args: + image_path (String): path image to predict + list_id (List): List id of bbox selected + list_bbox (List): List bbox selected + + Returns: + Dict: Dict result of prediction + """ + + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + image = cv2.imdecode(arr, -1) + list_lines = ocr_predict(image) + texts, boundingboxes = word_to_line(list_lines) + result = {} + texts_replace = [] + for text in texts: + if "✪" in text: + text = text.replace("✪", " ") + texts_replace.append(text) + else: + texts_replace.append(text) + result["texts"] = texts_replace + result["boundingboxes"] = boundingboxes + + output_dict = { + "document_type": "ocr-base", + "fields": [] + } + field = { + "label": "Text", + "value": result["texts"], + "box": result["boundingboxes"], + "confidence": 0.98, + "page": page_numb + } + output_dict['fields'].append(field) + + return output_dict diff --git a/cope2n-ai-fi/api/OCRBase/text_detection.py b/cope2n-ai-fi/api/OCRBase/text_detection.py new file mode 100755 index 0000000..010f7a8 --- /dev/null +++ b/cope2n-ai-fi/api/OCRBase/text_detection.py @@ -0,0 +1,22 @@ +# Copyright (c) OpenMMLab. All rights reserved. +from distutils.command.config import config + +from mmdet.apis import inference_detector, init_detector +from PIL import Image +from io import BytesIO + +config = "model/yolox_s_8x8_300e_cocotext_1280.py" +checkpoint = "model/best_bbox_mAP_epoch_294.pth" +device = "cpu" + + +def read_imagefile(file) -> Image.Image: + image = Image.open(BytesIO(file)) + return image + + +def detection_predict(image): + model = init_detector(config, checkpoint, device=device) + # test a single image + result = inference_detector(model, image) + return result diff --git a/cope2n-ai-fi/api/OCRBase/text_recognition.py b/cope2n-ai-fi/api/OCRBase/text_recognition.py new file mode 100755 index 0000000..d431792 --- /dev/null +++ b/cope2n-ai-fi/api/OCRBase/text_recognition.py @@ -0,0 +1,39 @@ +from common.utils.ocr_yolox import OcrEngineForYoloX_Invoice +from common.utils.word_formation import Word, words_to_lines + + +det_ckpt = "/models/sdsvtd/hub/wild_receipt_finetune_weights_c_lite.pth" +cls_ckpt = "satrn-lite-general-pretrain-20230106" + +engine = OcrEngineForYoloX_Invoice(det_ckpt, cls_ckpt) + + +def ocr_predict(img): + """Predict text from image + + Args: + image_path (str): _description_ + + Returns: + list: list of words + """ + try: + lbboxes, lwords = engine.run_image(img) + lWords = [Word(text=word, bndbox=bbox) for word, bbox in zip(lwords, lbboxes)] + list_lines, _ = words_to_lines(lWords) + return list_lines + # return lbboxes, lwords + except AssertionError as e: + print(e) + list_lines = [] + return list_lines + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--image", type=str, required=True) + args = parser.parse_args() + + list_lines = ocr_predict(args.image) diff --git a/cope2n-ai-fi/api/manulife/predict_manulife.py b/cope2n-ai-fi/api/manulife/predict_manulife.py new file mode 100644 index 0000000..6c6800e --- /dev/null +++ b/cope2n-ai-fi/api/manulife/predict_manulife.py @@ -0,0 +1,98 @@ +import cv2 +import urllib +import random +import numpy as np +from pathlib import Path +import sys, os +cur_dir = str(Path(__file__).parents[2]) +sys.path.append(cur_dir) +from modules.sdsvkvu import load_engine, process_img +from modules.ocr_engine import OcrEngine +from configs.manulife import device, ocr_cfg, kvu_cfg + +def load_ocr_engine(opt) -> OcrEngine: + print("[INFO] Loading engine...") + engine = OcrEngine(**opt) + print("[INFO] Engine loaded") + return engine + + +print("OCR engine configfs: \n", ocr_cfg) +print("KVU configfs: \n", kvu_cfg) + +ocr_engine = load_ocr_engine(ocr_cfg) +kvu_cfg['ocr_engine'] = ocr_engine +option = kvu_cfg['option'] +kvu_cfg.pop("option") # pop option +manulife_engine = load_engine(kvu_cfg) + + +def manulife_predict(image_url, engine) -> None: + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + + save_dir = "./tmp_results" + # image_path = os.path.join(save_dir, f"{image_url}.jpg") + image_path = os.path.join(save_dir, "abc.jpg") + cv2.imwrite(image_path, img) + + outputs = process_img(img_path=image_path, + save_dir=save_dir, + engine=engine, + export_all=False, + option=option) + return outputs + + +def predict(page_numb, image_url): + """ + module predict function + + Args: + image_url (str): image url + + Returns: + example output: + "data": { + "document_type": "invoice", + "fields": [ + { + "label": "Invoice Number", + "value": "INV-12345", + "box": [0, 0, 0, 0], + "confidence": 0.98 + }, + ... + ] + } + dict: output of model + """ + kvu_result = manulife_predict(image_url, engine=manulife_engine) + output_dict = { + "document_type": kvu_result['title'] if kvu_result['title'] is not None else "unknown", + "document_class": kvu_result['class_doc'] if kvu_result['class_doc'] is not None else "unknown", + "page_number": page_numb, + "fields": [] + } + for key in kvu_result.keys(): + if key in ("title", "class_doc"): + continue + field = { + "label": key, + "value": kvu_result[key], + "box": [0, 0, 0, 0], + "confidence": random.uniform(0.9, 1.0), + "page": page_numb + } + output_dict['fields'].append(field) + print(output_dict) + return output_dict + + + + +if __name__ == "__main__": + image_url = "/root/thucpd/20230322144639VUzu_16794962527791962785161104697882.jpg" + output = predict(0, image_url) + print(output) \ No newline at end of file diff --git a/cope2n-ai-fi/api/sdsap_sbt/prediction_sbt.py b/cope2n-ai-fi/api/sdsap_sbt/prediction_sbt.py new file mode 100755 index 0000000..13f2b85 --- /dev/null +++ b/cope2n-ai-fi/api/sdsap_sbt/prediction_sbt.py @@ -0,0 +1,94 @@ +import cv2 +import urllib +import random +import numpy as np +from pathlib import Path +import sys, os +cur_dir = str(Path(__file__).parents[2]) +sys.path.append(cur_dir) +from modules.sdsvkvu import load_engine, process_img +from modules.ocr_engine import OcrEngine +from configs.sdsap_sbt import device, ocr_cfg, kvu_cfg + + +def load_ocr_engine(opt) -> OcrEngine: + print("[INFO] Loading engine...") + engine = OcrEngine(**opt) + print("[INFO] Engine loaded") + return engine + + +print("OCR engine configfs: \n", ocr_cfg) +print("KVU configfs: \n", kvu_cfg) + +ocr_engine = load_ocr_engine(ocr_cfg) +kvu_cfg['ocr_engine'] = ocr_engine +option = kvu_cfg['option'] +kvu_cfg.pop("option") # pop option +sbt_engine = load_engine(kvu_cfg) + + +def sbt_predict(image_url, engine) -> None: + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + + save_dir = "./tmp_results" + # image_path = os.path.join(save_dir, f"{image_url}.jpg") + image_path = os.path.join(save_dir, "abc.jpg") + cv2.imwrite(image_path, img) + + outputs = process_img(img_path=image_path, + save_dir=save_dir, + engine=engine, + export_all=False, + option=option) + return outputs + +def predict(page_numb, image_url): + """ + module predict function + + Args: + image_url (str): image url + + Returns: + example output: + "data": { + "document_type": "invoice", + "fields": [ + { + "label": "Invoice Number", + "value": "INV-12345", + "box": [0, 0, 0, 0], + "confidence": 0.98 + }, + ... + ] + } + dict: output of model + """ + + sbt_result = sbt_predict(image_url, engine=sbt_engine) + output_dict = { + "document_type": "invoice", + "document_class": " ", + "page_number": page_numb, + "fields": [] + } + for key in sbt_result.keys(): + field = { + "label": key, + "value": sbt_result[key], + "box": [0, 0, 0, 0], + "confidence": random.uniform(0.9, 1.0), + "page": page_numb + } + output_dict['fields'].append(field) + return output_dict + + +if __name__ == "__main__": + image_url = "/root/thucpd/20230322144639VUzu_16794962527791962785161104697882.jpg" + output = predict(0, image_url) + print(output) \ No newline at end of file diff --git a/cope2n-ai-fi/celery_worker/__init__.py b/cope2n-ai-fi/celery_worker/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/cope2n-ai-fi/celery_worker/client_connector.py b/cope2n-ai-fi/celery_worker/client_connector.py new file mode 100755 index 0000000..49191e7 --- /dev/null +++ b/cope2n-ai-fi/celery_worker/client_connector.py @@ -0,0 +1,81 @@ +from celery import Celery +import base64 +import environ +env = environ.Env( + DEBUG=(bool, True) +) + +class CeleryConnector: + task_routes = { + "process_id_result": {"queue": "id_card_rs"}, + "process_driver_license_result": {"queue": "driver_license_rs"}, + "process_invoice_result": {"queue": "invoice_rs"}, + "process_ocr_with_box_result": {"queue": "ocr_with_box_rs"}, + "process_template_matching_result": {"queue": "template_matching_rs"}, + # mock task + "process_id": {"queue": "id_card"}, + "process_driver_license": {"queue": "driver_license"}, + "process_invoice": {"queue": "invoice"}, + "process_ocr_with_box": {"queue": "ocr_with_box"}, + "process_template_matching": {"queue": "template_matching"}, + } + app = Celery( + "postman", + broker=env.str("CELERY_BROKER", "amqp://test:test@rabbitmq:5672"), + # backend="rpc://", + ) + + def process_id_result(self, args): + return self.send_task("process_id_result", args) + + def process_driver_license_result(self, args): + return self.send_task("process_driver_license_result", args) + + def process_invoice_result(self, args): + return self.send_task("process_invoice_result", args) + + def process_ocr_with_box_result(self, args): + return self.send_task("process_ocr_with_box_result", args) + + def process_template_matching_result(self, args): + return self.send_task("process_template_matching_result", args) + + def process_id(self, args): + return self.send_task("process_id", args) + + def process_driver_license(self, args): + return self.send_task("process_driver_license", args) + + def process_invoice(self, args): + return self.send_task("process_invoice", args) + + def process_ocr_with_box(self, args): + return self.send_task("process_ocr_with_box", args) + + def process_template_matching(self, args): + return self.send_task("process_template_matching", args) + + def send_task(self, name=None, args=None): + if name not in self.task_routes or "queue" not in self.task_routes[name]: + return self.app.send_task(name, args) + + return self.app.send_task(name, args, queue=self.task_routes[name]["queue"]) + + +def main(): + rq_id = 345 + file_names = "abc.jpg" + list_data = [] + + with open("/home/sds/thucpd/aicr-2022/abc.jpg", "rb") as fs: + encoded_string = base64.b64encode(fs.read()).decode("utf-8") + list_data.append(encoded_string) + + c_connector = CeleryConnector() + a = c_connector.process_id(args=(rq_id, list_data, file_names)) + + print(a) + + +if __name__ == "__main__": + main() diff --git a/cope2n-ai-fi/celery_worker/client_connector_fi.py b/cope2n-ai-fi/celery_worker/client_connector_fi.py new file mode 100755 index 0000000..cd5f3ba --- /dev/null +++ b/cope2n-ai-fi/celery_worker/client_connector_fi.py @@ -0,0 +1,56 @@ +from celery import Celery +import environ +env = environ.Env( + DEBUG=(bool, True) +) + +class CeleryConnector: + task_routes = { + 'process_fi_invoice_result': {'queue': 'invoice_fi_rs'}, + 'process_sap_invoice_result': {'queue': 'invoice_sap_rs'}, + 'process_manulife_invoice_result': {'queue': 'invoice_manulife_rs'}, + 'process_sbt_invoice_result': {'queue': 'invoice_sbt_rs'}, + # mock task + 'process_fi_invoice': {'queue': "invoice_fi"}, + 'process_sap_invoice': {'queue': "invoice_sap"}, + 'process_manulife_invoice': {'queue': "invoice_manulife"}, + 'process_sbt_invoice': {'queue': "invoice_sbt"}, + } + app = Celery( + "postman", + broker= env.str("CELERY_BROKER", "amqp://test:test@rabbitmq:5672"), + ) + + # mock task for FI + def process_fi_invoice_result(self, args): + return self.send_task("process_fi_invoice_result", args) + + def process_fi_invoice(self, args): + return self.send_task("process_fi_invoice", args) + + # mock task for SAP + def process_sap_invoice_result(self, args): + return self.send_task("process_sap_invoice_result", args) + + def process_sap_invoice(self, args): + return self.send_task("process_sap_invoice", args) + + # mock task for manulife + def process_manulife_invoice_result(self, args): + return self.send_task("process_manulife_invoice_result", args) + + def process_manulife_invoice(self, args): + return self.send_task("process_manulife_invoice", args) + + # mock task for manulife + def process_sbt_invoice_result(self, args): + return self.send_task("process_sbt_invoice_result", args) + + def process_sbt_invoice(self, args): + return self.send_task("process_sbt_invoice", args) + + def send_task(self, name=None, args=None): + if name not in self.task_routes or "queue" not in self.task_routes[name]: + return self.app.send_task(name, args) + + return self.app.send_task(name, args, queue=self.task_routes[name]["queue"]) \ No newline at end of file diff --git a/cope2n-ai-fi/celery_worker/mock_process_tasks.py b/cope2n-ai-fi/celery_worker/mock_process_tasks.py new file mode 100755 index 0000000..4aa97cf --- /dev/null +++ b/cope2n-ai-fi/celery_worker/mock_process_tasks.py @@ -0,0 +1,220 @@ +from celery_worker.worker import app +import numpy as np +import cv2 + +@app.task(name="process_id") +def process_id(rq_id, sub_id, folder_name, list_url, user_id): + from common.serve_model import predict + from celery_worker.client_connector import CeleryConnector + + c_connector = CeleryConnector() + try: + result = predict(rq_id, sub_id, folder_name, list_url, user_id, infer_name="id_card") + print(result) + result = { + "status": 200, + "content": result, + "message": "Success", + } + c_connector.process_id_result((rq_id, result)) + return {"rq_id": rq_id} + # if image_croped is not None: + # if result["data"] == []: + # result = { + # "status": 404, + # "content": {}, + # } + # c_connector.process_id_result((rq_id, result, None)) + # return {"rq_id": rq_id} + # else: + # result = { + # "status": 200, + # "content": result, + # "message": "Success", + # } + # c_connector.process_id_result((rq_id, result)) + # return {"rq_id": rq_id} + # elif image_croped is None: + # result = { + # "status": 404, + # "content": {}, + # } + # c_connector.process_id_result((rq_id, result, None)) + # return {"rq_id": rq_id} + + except Exception as e: + print(e) + result = { + "status": 404, + "content": {}, + } + c_connector.process_id_result((rq_id, result, None)) + return {"rq_id": rq_id} + + +@app.task(name="process_driver_license") +def process_driver_license(rq_id, sub_id, folder_name, list_url, user_id): + from common.serve_model import predict + from celery_worker.client_connector import CeleryConnector + + c_connector = CeleryConnector() + try: + result = predict(rq_id, sub_id, folder_name, list_url, user_id, infer_name="driving_license") + result = { + "status": 200, + "content": result, + "message": "Success", + } + c_connector.process_driver_license_result((rq_id, result)) + return {"rq_id": rq_id} + # result, image_croped = predict(str(url), "driving_license") + # if image_croped is not None: + # if result["data"] == []: + # result = { + # "status": 404, + # "content": {}, + # } + # c_connector.process_driver_license_result((rq_id, result, None)) + # return {"rq_id": rq_id} + # else: + # result = { + # "status": 200, + # "content": result, + # "message": "Success", + # } + # path_image_croped = "/app/media/users/{}/subscriptions/{}/requests/{}/{}/image_croped.jpg".format(user_id,sub_id,folder_name,rq_id) + # cv2.imwrite("/users/{}/subscriptions/{}/requests/{}/{}/image_croped.jpg".format(user_id,sub_id,folder_name,rq_id), image_croped) + # c_connector.process_driver_license_result((rq_id, result, path_image_croped)) + # return {"rq_id": rq_id} + # elif image_croped is None: + # result = { + # "status": 404, + # "content": {}, + # } + # c_connector.process_driver_license_result((rq_id, result, None)) + # return {"rq_id": rq_id} + except Exception as e: + print(e) + result = { + "status": 404, + "content": {}, + } + c_connector.process_driver_license_result((rq_id, result, None)) + return {"rq_id": rq_id} + + +@app.task(name="process_template_matching") +def process_template_matching(rq_id, sub_id, folder_name, url, tmp_json, user_id): + from TemplateMatching.src.ocr_master import Extractor + from celery_worker.client_connector import CeleryConnector + import urllib + + c_connector = CeleryConnector() + extractor = Extractor() + try: + req = urllib.request.urlopen(url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + imgs = [img] + image_aliged = extractor.image_alige(imgs, tmp_json) + if image_aliged is None: + result = { + "status": 401, + "content": "Image is not match with template", + } + c_connector.process_template_matching_result( + (rq_id, result, None) + ) + return {"rq_id": rq_id} + else: + output = extractor.extract_information( + image_aliged, tmp_json + ) + path_image_croped = "/app/media/users/{}/subscriptions/{}/requests/{}/{}/image_croped.jpg".format(user_id,sub_id,folder_name,rq_id) + cv2.imwrite("/users/{}/subscriptions/{}/requests/{}/{}/image_croped.jpg".format(user_id,sub_id,folder_name,rq_id), image_aliged) + if output == {}: + result = {"status": 404, "content": {}} + c_connector.process_template_matching_result((rq_id, result, None)) + return {"rq_id": rq_id} + else: + result = { + "document_type": "template_matching", + "fields": [] + } + print(output) + for field in tmp_json["fields"]: + print(field["label"]) + field_value = { + "label": field["label"], + "value": output[field["label"]], + "box": [float(num) for num in field["box"]], + "confidence": 0.98 #TODO confidence + } + result["fields"].append(field_value) + + print(result) + result = {"status": 200, "content": result} + c_connector.process_template_matching_result( + (rq_id, result, path_image_croped) + ) + return {"rq_id": rq_id} + except Exception as e: + print(e) + result = {"status": 404, "content": {}} + c_connector.process_template_matching_result((rq_id, result, None)) + return {"rq_id": rq_id} + + +# @app.task(name="process_invoice") +# def process_invoice(rq_id, url): +# from celery_worker.client_connector import CeleryConnector +# from Kie_Hoanglv.prediction import predict + +# c_connector = CeleryConnector() +# try: +# print(url) +# result = predict(str(url)) +# hoadon = {"status": 200, "content": result, "message": "Success"} +# c_connector.process_invoice_result((rq_id, hoadon)) +# return {"rq_id": rq_id} + +# except Exception as e: +# print(e) +# hoadon = {"status": 404, "content": {}} +# c_connector.process_invoice_result((rq_id, hoadon)) +# return {"rq_id": rq_id} + +@app.task(name="process_invoice") +def process_invoice(rq_id, list_url): + from celery_worker.client_connector import CeleryConnector + from common.process_pdf import compile_output + + c_connector = CeleryConnector() + try: + result = compile_output(list_url) + hoadon = {"status": 200, "content": result, "message": "Success"} + c_connector.process_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + except Exception as e: + print(e) + hoadon = {"status": 404, "content": {}} + c_connector.process_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + + +@app.task(name="process_ocr_with_box") +def process_ocr_with_box(rq_id, list_url): + from celery_worker.client_connector import CeleryConnector + from common.process_pdf import compile_output_ocr_base + + c_connector = CeleryConnector() + try: + result = compile_output_ocr_base(list_url) + result = {"status": 200, "content": result, "message": "Success"} + c_connector.process_ocr_with_box_result((rq_id, result)) + return {"rq_id": rq_id} + except Exception as e: + print(e) + result = {"status": 404, "content": {}} + c_connector.process_ocr_with_box_result((rq_id, result)) + return {"rq_id": rq_id} \ No newline at end of file diff --git a/cope2n-ai-fi/celery_worker/mock_process_tasks_fi.py b/cope2n-ai-fi/celery_worker/mock_process_tasks_fi.py new file mode 100755 index 0000000..00bec4a --- /dev/null +++ b/cope2n-ai-fi/celery_worker/mock_process_tasks_fi.py @@ -0,0 +1,74 @@ +from celery_worker.worker_fi import app + +@app.task(name="process_fi_invoice") +def process_invoice(rq_id, list_url): + from celery_worker.client_connector_fi import CeleryConnector + from common.process_pdf import compile_output_fi + + c_connector = CeleryConnector() + try: + result = compile_output_fi(list_url) + hoadon = {"status": 200, "content": result, "message": "Success"} + print(hoadon) + c_connector.process_fi_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + except Exception as e: + print(e) + hoadon = {"status": 404, "content": {}} + c_connector.process_fi_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + + +@app.task(name="process_sap_invoice") +def process_sap_invoice(rq_id, list_url): + from celery_worker.client_connector_fi import CeleryConnector + from common.process_pdf import compile_output + + print(list_url) + c_connector = CeleryConnector() + try: + result = compile_output(list_url) + hoadon = {"status": 200, "content": result, "message": "Success"} + c_connector.process_sap_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + except Exception as e: + print(e) + hoadon = {"status": 404, "content": {}} + c_connector.process_sap_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + +@app.task(name="process_manulife_invoice") +def process_manulife_invoice(rq_id, list_url): + from celery_worker.client_connector_fi import CeleryConnector + from common.process_pdf import compile_output_manulife + # TODO: simply returning 200 and 404 doesn't make any sense + c_connector = CeleryConnector() + try: + result = compile_output_manulife(list_url) + hoadon = {"status": 200, "content": result, "message": "Success"} + print(hoadon) + c_connector.process_manulife_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + except Exception as e: + print(e) + hoadon = {"status": 404, "content": {}} + c_connector.process_manulife_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + +@app.task(name="process_sbt_invoice") +def process_sbt_invoice(rq_id, list_url): + from celery_worker.client_connector_fi import CeleryConnector + from common.process_pdf import compile_output_sbt + # TODO: simply returning 200 and 404 doesn't make any sense + c_connector = CeleryConnector() + try: + result = compile_output_sbt(list_url) + hoadon = {"status": 200, "content": result, "message": "Success"} + print(hoadon) + c_connector.process_sbt_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} + except Exception as e: + print(e) + hoadon = {"status": 404, "content": {}} + c_connector.process_sbt_invoice_result((rq_id, hoadon)) + return {"rq_id": rq_id} \ No newline at end of file diff --git a/cope2n-ai-fi/celery_worker/worker.py b/cope2n-ai-fi/celery_worker/worker.py new file mode 100755 index 0000000..68b9d89 --- /dev/null +++ b/cope2n-ai-fi/celery_worker/worker.py @@ -0,0 +1,40 @@ +from celery import Celery +from kombu import Queue, Exchange +import environ +env = environ.Env( + DEBUG=(bool, True) +) + +app: Celery = Celery( + "postman", + broker= env.str("CELERY_BROKER", "amqp://test:test@rabbitmq:5672"), + # backend="rpc://", + include=[ + "celery_worker.mock_process_tasks", + ], +) +task_exchange = Exchange("default", type="direct") +task_create_missing_queues = False +app.conf.update( + { + "result_expires": 3600, + "task_queues": [ + Queue("id_card"), + Queue("driver_license"), + Queue("invoice"), + Queue("ocr_with_box"), + Queue("template_matching"), + ], + "task_routes": { + "process_id": {"queue": "id_card"}, + "process_driver_license": {"queue": "driver_license"}, + "process_invoice": {"queue": "invoice"}, + "process_ocr_with_box": {"queue": "ocr_with_box"}, + "process_template_matching": {"queue": "template_matching"}, + }, + } +) + +if __name__ == "__main__": + argv = ["celery_worker.worker", "--loglevel=INFO", "--pool=solo"] # Window opts + app.worker_main(argv) \ No newline at end of file diff --git a/cope2n-ai-fi/celery_worker/worker_fi.py b/cope2n-ai-fi/celery_worker/worker_fi.py new file mode 100755 index 0000000..54c49ea --- /dev/null +++ b/cope2n-ai-fi/celery_worker/worker_fi.py @@ -0,0 +1,37 @@ +from celery import Celery +from kombu import Queue, Exchange +import environ +env = environ.Env( + DEBUG=(bool, True) +) + +app: Celery = Celery( + "postman", + broker= env.str("CELERY_BROKER", "amqp://test:test@rabbitmq:5672"), + include=[ + "celery_worker.mock_process_tasks_fi", + ], +) +task_exchange = Exchange("default", type="direct") +task_create_missing_queues = False +app.conf.update( + { + "result_expires": 3600, + "task_queues": [ + Queue("invoice_fi"), + Queue("invoice_sap"), + Queue("invoice_manulife"), + Queue("invoice_sbt"), + ], + "task_routes": { + 'process_fi_invoice': {'queue': "invoice_fi"}, + 'process_fi_invoice_result': {'queue': 'invoice_fi_rs'}, + 'process_sap_invoice': {'queue': "invoice_sap"}, + 'process_sap_invoice_result': {'queue': 'invoice_sap_rs'}, + 'process_manulife_invoice': {'queue': 'invoice_manulife'}, + 'process_manulife_invoice_result': {'queue': 'invoice_manulife_rs'}, + 'process_sbt_invoice': {'queue': 'invoice_sbt'}, + 'process_sbt_invoice_result': {'queue': 'invoice_sbt_rs'}, + }, + } +) \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/anyKeyValue.py b/cope2n-ai-fi/common/AnyKey_Value/anyKeyValue.py new file mode 100755 index 0000000..1157c7c --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/anyKeyValue.py @@ -0,0 +1,101 @@ +import os +import glob +import cv2 +import argparse +from tqdm import tqdm +from datetime import datetime +# from omegaconf import OmegaConf +import sys +sys.path.append('/home/thucpd/thucpd/git/PV2-2023/common/AnyKey_Value') # TODO: ???? +from predictor import KVUPredictor +from preprocess import KVUProcess, DocumentKVUProcess +from utils.utils import create_dir, visualize, get_colormap, export_kvu_for_VAT_invoice, export_kvu_outputs + + +def get_args(): + args = argparse.ArgumentParser(description='Main file') + args.add_argument('--img_dir', default='/home/ai-core/Kie_Invoice_AP/AnyKey_Value/visualize/test/', type=str, + help='Input image directory') + args.add_argument('--save_dir', default='/home/ai-core/Kie_Invoice_AP/AnyKey_Value/visualize/test/', type=str, + help='Save directory') + args.add_argument('--exp_dir', default='/home/thucpd/thucpd/PV2-2023/common/AnyKey_Value/experiments/key_value_understanding-20230608-171900', type=str, + help='Checkpoint and config of model') + args.add_argument('--export_img', default=0, type=int, + help='Save visualize on image') + args.add_argument('--mode', default=3, type=int, + help="0:'normal' - 1:'full_tokens' - 2:'sliding' - 3: 'document'") + args.add_argument('--dir_level', default=0, type=int, + help='Number of subfolders contains image') + + return args.parse_args() + + +def load_engine(exp_dir: str, class_names: list, mode: int) -> KVUPredictor: + configs = { + 'cfg': glob.glob(f'{exp_dir}/*.yaml')[0], + 'ckpt': f'{exp_dir}/checkpoints/best_model.pth' + } + dummy_idx = 512 + predictor = KVUPredictor(configs, class_names, dummy_idx, mode) + + # processor = KVUProcess(predictor.net.tokenizer_layoutxlm, + # predictor.net.feature_extractor, predictor.backbone_type, class_names, + # predictor.slice_interval, predictor.window_size, run_ocr=1, mode=mode) + + processor = DocumentKVUProcess(predictor.net.tokenizer, predictor.net.feature_extractor, + predictor.backbone_type, class_names, + predictor.max_window_count, predictor.slice_interval, predictor.window_size, + run_ocr=1, mode=mode) + return predictor, processor + + +def predict_image(img_path: str, save_dir: str, predictor: KVUPredictor, processor) -> None: + fname = os.path.basename(img_path) + img_ext = os.path.splitext(img_path)[1] + output_ext = ".json" + inputs = processor(img_path, ocr_path='') + + bbox, lwords, pr_class_words, pr_relations = predictor.predict(inputs) + + slide_window = False if len(bbox) == 1 else True + + if len(bbox) == 0: + vat_outputs = export_kvu_for_VAT_invoice(os.path.join(save_dir, fname.replace(img_ext, output_ext)), lwords, pr_class_words, pr_relations, predictor.class_names) + else: + for i in range(len(bbox)): + if not slide_window: + save_path = os.path.join(save_dir, 'kvu_results') + create_dir(save_path) + # export_kvu_for_SDSAP(os.path.join(save_dir, fname.replace(img_ext, output_ext)), lwords[i], pr_class_words[i], pr_relations[i], predictor.class_names) + vat_outputs = export_kvu_for_VAT_invoice(os.path.join(save_dir, fname.replace(img_ext, output_ext)), lwords[i], pr_class_words[i], pr_relations[i], predictor.class_names) + + return vat_outputs + + +def Predictor_KVU(img: str, save_dir: str, predictor: KVUPredictor, processor) -> None: + + # req = urllib.request.urlopen(image_url) + # arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + # img = cv2.imdecode(arr, -1) + curr_datetime = datetime.now().strftime('%Y-%m-%d %H-%M-%S') + image_path = "/home/thucpd/thucpd/PV2-2023/tmp_image/{}.jpg".format(curr_datetime) + cv2.imwrite(image_path, img) + vat_outputs = predict_image(image_path, save_dir, predictor, processor) + return vat_outputs + + +if __name__ == "__main__": + args = get_args() + class_names = ['others', 'title', 'key', 'value', 'header'] + predict_mode = { + 'normal': 0, + 'full_tokens': 1, + 'sliding': 2, + 'document': 3 + } + predictor, processor = load_engine(args.exp_dir, class_names, args.mode) + create_dir(args.save_dir) + image_path = "/mnt/ssd1T/tuanlv/PV2-2023/common/AnyKey_Value/visualize/test1/RedInvoice_WaterPurfier_Feb_PVI_829_0.jpg" + save_dir = "/mnt/ssd1T/tuanlv/PV2-2023/common/AnyKey_Value/visualize/test1" + vat_outputs = predict_image(image_path, save_dir, predictor, processor) + print('[INFO] Done') diff --git a/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/__init__.py b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier.py b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier.py new file mode 100755 index 0000000..83b4769 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier.py @@ -0,0 +1,133 @@ +import time + +import torch +import torch.utils.data +from overrides import overrides +from pytorch_lightning import LightningModule +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger +from pytorch_lightning.utilities.distributed import rank_zero_only +from torch.optim import SGD, Adam, AdamW +from torch.optim.lr_scheduler import LambdaLR + +from lightning_modules.schedulers import ( + cosine_scheduler, + linear_scheduler, + multistep_scheduler, +) +from model import get_model +from utils import cfg_to_hparams, get_specific_pl_logger + + +class ClassifierModule(LightningModule): + def __init__(self, cfg): + super().__init__() + self.cfg = cfg + self.net = get_model(self.cfg) + self.ignore_index = -100 + + self.time_tracker = None + + self.optimizer_types = { + "sgd": SGD, + "adam": Adam, + "adamw": AdamW, + } + + @overrides + def setup(self, stage): + self.time_tracker = time.time() + + @overrides + def configure_optimizers(self): + optimizer = self._get_optimizer() + scheduler = self._get_lr_scheduler(optimizer) + scheduler = { + "scheduler": scheduler, + "name": "learning_rate", + "interval": "step", + } + return [optimizer], [scheduler] + + def _get_lr_scheduler(self, optimizer): + cfg_train = self.cfg.train + lr_schedule_method = cfg_train.optimizer.lr_schedule.method + lr_schedule_params = cfg_train.optimizer.lr_schedule.params + + if lr_schedule_method is None: + scheduler = LambdaLR(optimizer, lr_lambda=lambda _: 1) + elif lr_schedule_method == "step": + scheduler = multistep_scheduler(optimizer, **lr_schedule_params) + elif lr_schedule_method == "cosine": + total_samples = cfg_train.max_epochs * cfg_train.num_samples_per_epoch + total_batch_size = cfg_train.batch_size * self.trainer.world_size + max_iter = total_samples / total_batch_size + scheduler = cosine_scheduler( + optimizer, training_steps=max_iter, **lr_schedule_params + ) + elif lr_schedule_method == "linear": + total_samples = cfg_train.max_epochs * cfg_train.num_samples_per_epoch + total_batch_size = cfg_train.batch_size * self.trainer.world_size + max_iter = total_samples / total_batch_size + scheduler = linear_scheduler( + optimizer, training_steps=max_iter, **lr_schedule_params + ) + else: + raise ValueError(f"Unknown lr_schedule_method={lr_schedule_method}") + + return scheduler + + def _get_optimizer(self): + opt_cfg = self.cfg.train.optimizer + method = opt_cfg.method.lower() + + if method not in self.optimizer_types: + raise ValueError(f"Unknown optimizer method={method}") + + kwargs = dict(opt_cfg.params) + kwargs["params"] = self.net.parameters() + optimizer = self.optimizer_types[method](**kwargs) + + return optimizer + + @rank_zero_only + @overrides + def on_fit_end(self): + hparam_dict = cfg_to_hparams(self.cfg, {}) + metric_dict = {"metric/dummy": 0} + + tb_logger = get_specific_pl_logger(self.logger, TensorBoardLogger) + + if tb_logger: + tb_logger.log_hyperparams(hparam_dict, metric_dict) + + @overrides + def training_epoch_end(self, training_step_outputs): + avg_loss = torch.tensor(0.0).to(self.device) + for step_out in training_step_outputs: + avg_loss += step_out["loss"] + + log_dict = {"train_loss": avg_loss} + self._log_shell(log_dict, prefix="train ") + + def _log_shell(self, log_info, prefix=""): + log_info_shell = {} + for k, v in log_info.items(): + new_v = v + if type(new_v) is torch.Tensor: + new_v = new_v.item() + log_info_shell[k] = new_v + + out_str = prefix.upper() + if prefix.upper().strip() in ["TRAIN", "VAL"]: + out_str += f"[epoch: {self.current_epoch}/{self.cfg.train.max_epochs}]" + + if self.training: + lr = self.trainer._lightning_optimizers[0].param_groups[0]["lr"] + log_info_shell["lr"] = lr + + for key, value in log_info_shell.items(): + out_str += f" || {key}: {round(value, 5)}" + out_str += f" || time: {round(time.time() - self.time_tracker, 1)}" + out_str += " secs." + # self.print(out_str) + self.time_tracker = time.time() diff --git a/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier_module.py b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier_module.py new file mode 100755 index 0000000..5786cc5 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/classifier_module.py @@ -0,0 +1,390 @@ +import numpy as np +import torch +import torch.utils.data +from overrides import overrides + +from lightning_modules.classifier import ClassifierModule +from utils import get_class_names + + +class KVUClassifierModule(ClassifierModule): + def __init__(self, cfg): + super().__init__(cfg) + + class_names = get_class_names(self.cfg.dataset_root_path) + + self.window_size = cfg.train.max_num_words + self.slice_interval = cfg.train.slice_interval + self.eval_kwargs = { + "class_names": class_names, + "dummy_idx": self.cfg.train.max_seq_length, # update dummy_idx in next step + } + self.stage = cfg.stage + + @overrides + def training_step(self, batch, batch_idx, *args): + if self.stage == 1: + _, loss = self.net(batch['windows']) + elif self.stage == 2: + _, loss = self.net(batch) + else: + raise ValueError( + f"Not supported stage: {self.stage}" + ) + + log_dict_input = {"train_loss": loss} + self.log_dict(log_dict_input, sync_dist=True) + return loss + + @torch.no_grad() + @overrides + def validation_step(self, batch, batch_idx, *args): + if self.stage == 1: + step_out_total = { + "loss": 0, + "ee":{ + "n_batch_gt": 0, + "n_batch_pr": 0, + "n_batch_correct": 0, + }, + "el":{ + "n_batch_gt": 0, + "n_batch_pr": 0, + "n_batch_correct": 0, + }, + "el_from_key":{ + "n_batch_gt": 0, + "n_batch_pr": 0, + "n_batch_correct": 0, + }} + for window in batch['windows']: + head_outputs, loss = self.net(window) + step_out = do_eval_step(window, head_outputs, loss, self.eval_kwargs) + for key in step_out_total: + if key == 'loss': + step_out_total[key] += step_out[key] + else: + for subkey in step_out_total[key]: + step_out_total[key][subkey] += step_out[key][subkey] + return step_out_total + + elif self.stage == 2: + head_outputs, loss = self.net(batch) + # self.eval_kwargs['dummy_idx'] = batch['itc_labels'].shape[1] + # step_out = do_eval_step(batch, head_outputs, loss, self.eval_kwargs) + self.eval_kwargs['dummy_idx'] = batch['documents']['itc_labels'].shape[1] + step_out = do_eval_step(batch['documents'], head_outputs, loss, self.eval_kwargs) + return step_out + + @torch.no_grad() + @overrides + def validation_epoch_end(self, validation_step_outputs): + scores = do_eval_epoch_end(validation_step_outputs) + self.print( + f"[EE] Precision: {scores['ee']['precision']:.4f}, Recall: {scores['ee']['recall']:.4f}, F1-score: {scores['ee']['f1']:.4f}" + ) + self.print( + f"[EL] Precision: {scores['el']['precision']:.4f}, Recall: {scores['el']['recall']:.4f}, F1-score: {scores['el']['f1']:.4f}" + ) + self.print( + f"[ELK] Precision: {scores['el_from_key']['precision']:.4f}, Recall: {scores['el_from_key']['recall']:.4f}, F1-score: {scores['el_from_key']['f1']:.4f}" + ) + self.log('val_f1', (scores['ee']['f1'] + scores['el']['f1'] + scores['el_from_key']['f1']) / 3.) + tensorboard_logs = {'val_precision_ee': scores['ee']['precision'], 'val_recall_ee': scores['ee']['recall'], 'val_f1_ee': scores['ee']['f1'], + 'val_precision_el': scores['el']['precision'], 'val_recall_el': scores['el']['recall'], 'val_f1_el': scores['el']['f1'], + 'val_precision_el_from_key': scores['el_from_key']['precision'], 'val_recall_el_from_key': scores['el_from_key']['recall'], \ + 'val_f1_el_from_key': scores['el_from_key']['f1'],} + return {'log': tensorboard_logs} + + +def do_eval_step(batch, head_outputs, loss, eval_kwargs): + class_names = eval_kwargs["class_names"] + dummy_idx = eval_kwargs["dummy_idx"] + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_labels = torch.argmax(itc_outputs, -1) + pr_stc_labels = torch.argmax(stc_outputs, -1) + pr_el_labels = torch.argmax(el_outputs, -1) + pr_el_labels_from_key = torch.argmax(el_outputs_from_key, -1) + + ( + n_batch_gt_classes, + n_batch_pr_classes, + n_batch_correct_classes, + ) = eval_ee_spade_batch( + pr_itc_labels, + batch["itc_labels"], + batch["are_box_first_tokens"], + pr_stc_labels, + batch["stc_labels"], + batch["attention_mask_layoutxlm"], + class_names, + dummy_idx, + ) + + n_batch_gt_rel, n_batch_pr_rel, n_batch_correct_rel = eval_el_spade_batch( + pr_el_labels, + batch["el_labels"], + batch["are_box_first_tokens"], + dummy_idx, + ) + + n_batch_gt_rel_from_key, n_batch_pr_rel_from_key, n_batch_correct_rel_from_key = eval_el_spade_batch( + pr_el_labels_from_key, + batch["el_labels_from_key"], + batch["are_box_first_tokens"], + dummy_idx, + ) + + step_out = { + "loss": loss, + "ee":{ + "n_batch_gt": n_batch_gt_classes, + "n_batch_pr": n_batch_pr_classes, + "n_batch_correct": n_batch_correct_classes, + }, + "el":{ + "n_batch_gt": n_batch_gt_rel, + "n_batch_pr": n_batch_pr_rel, + "n_batch_correct": n_batch_correct_rel, + }, + "el_from_key":{ + "n_batch_gt": n_batch_gt_rel_from_key, + "n_batch_pr": n_batch_pr_rel_from_key, + "n_batch_correct": n_batch_correct_rel_from_key, + } + + } + + return step_out + + +def eval_ee_spade_batch( + pr_itc_labels, + gt_itc_labels, + are_box_first_tokens, + pr_stc_labels, + gt_stc_labels, + attention_mask, + class_names, + dummy_idx, +): + n_batch_gt_classes, n_batch_pr_classes, n_batch_correct_classes = 0, 0, 0 + + bsz = pr_itc_labels.shape[0] + for example_idx in range(bsz): + n_gt_classes, n_pr_classes, n_correct_classes = eval_ee_spade_example( + pr_itc_labels[example_idx], + gt_itc_labels[example_idx], + are_box_first_tokens[example_idx], + pr_stc_labels[example_idx], + gt_stc_labels[example_idx], + attention_mask[example_idx], + class_names, + dummy_idx, + ) + + n_batch_gt_classes += n_gt_classes + n_batch_pr_classes += n_pr_classes + n_batch_correct_classes += n_correct_classes + + return ( + n_batch_gt_classes, + n_batch_pr_classes, + n_batch_correct_classes, + ) + + +def eval_ee_spade_example( + pr_itc_label, + gt_itc_label, + box_first_token_mask, + pr_stc_label, + gt_stc_label, + attention_mask, + class_names, + dummy_idx, +): + gt_first_words = parse_initial_words( + gt_itc_label, box_first_token_mask, class_names + ) + gt_class_words = parse_subsequent_words( + gt_stc_label, attention_mask, gt_first_words, dummy_idx + ) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, dummy_idx + ) + + n_gt_classes, n_pr_classes, n_correct_classes = 0, 0, 0 + for class_idx in range(len(class_names)): + # Evaluate by ID + gt_parse = set(gt_class_words[class_idx]) + pr_parse = set(pr_class_words[class_idx]) + + n_gt_classes += len(gt_parse) + n_pr_classes += len(pr_parse) + n_correct_classes += len(gt_parse & pr_parse) + + return n_gt_classes, n_pr_classes, n_correct_classes + + +def parse_initial_words(itc_label, box_first_token_mask, class_names): + itc_label_np = itc_label.cpu().numpy() + box_first_token_mask_np = box_first_token_mask.cpu().numpy() + + outputs = [[] for _ in range(len(class_names))] + + for token_idx, label in enumerate(itc_label_np): + if box_first_token_mask_np[token_idx] and label != 0: + outputs[label].append(token_idx) + + return outputs + + +def parse_subsequent_words(stc_label, attention_mask, init_words, dummy_idx): + max_connections = 50 + + valid_stc_label = stc_label * attention_mask.bool() + valid_stc_label = valid_stc_label.cpu().numpy() + stc_label_np = stc_label.cpu().numpy() + + valid_token_indices = np.where( + (valid_stc_label != dummy_idx) * (valid_stc_label != 0) + ) + + next_token_idx_dict = {} + for token_idx in valid_token_indices[0]: + next_token_idx_dict[stc_label_np[token_idx]] = token_idx + + outputs = [] + for init_token_indices in init_words: + sub_outputs = [] + for init_token_idx in init_token_indices: + cur_token_indices = [init_token_idx] + for _ in range(max_connections): + if cur_token_indices[-1] in next_token_idx_dict: + if ( + next_token_idx_dict[cur_token_indices[-1]] + not in init_token_indices + ): + cur_token_indices.append( + next_token_idx_dict[cur_token_indices[-1]] + ) + else: + break + else: + break + sub_outputs.append(tuple(cur_token_indices)) + + outputs.append(sub_outputs) + + return outputs + + +def eval_el_spade_batch( + pr_el_labels, + gt_el_labels, + are_box_first_tokens, + dummy_idx, +): + n_batch_gt_rel, n_batch_pr_rel, n_batch_correct_rel = 0, 0, 0 + + bsz = pr_el_labels.shape[0] + for example_idx in range(bsz): + n_gt_rel, n_pr_rel, n_correct_rel = eval_el_spade_example( + pr_el_labels[example_idx], + gt_el_labels[example_idx], + are_box_first_tokens[example_idx], + dummy_idx, + ) + + n_batch_gt_rel += n_gt_rel + n_batch_pr_rel += n_pr_rel + n_batch_correct_rel += n_correct_rel + + return n_batch_gt_rel, n_batch_pr_rel, n_batch_correct_rel + + +def eval_el_spade_example(pr_el_label, gt_el_label, box_first_token_mask, dummy_idx): + gt_relations = parse_relations(gt_el_label, box_first_token_mask, dummy_idx) + pr_relations = parse_relations(pr_el_label, box_first_token_mask, dummy_idx) + + gt_relations = set(gt_relations) + pr_relations = set(pr_relations) + + n_gt_rel = len(gt_relations) + n_pr_rel = len(pr_relations) + n_correct_rel = len(gt_relations & pr_relations) + + return n_gt_rel, n_pr_rel, n_correct_rel + + +def parse_relations(el_label, box_first_token_mask, dummy_idx): + valid_el_labels = el_label * box_first_token_mask + valid_el_labels = valid_el_labels.cpu().numpy() + el_label_np = el_label.cpu().numpy() + + max_token = box_first_token_mask.shape[0] - 1 + + valid_token_indices = np.where( + ((valid_el_labels != dummy_idx) * (valid_el_labels != 0)) ### + ) + + link_map_tuples = [] + for token_idx in valid_token_indices[0]: + link_map_tuples.append((el_label_np[token_idx], token_idx)) + + return set(link_map_tuples) + +def do_eval_epoch_end(step_outputs): + scores = {} + for task in ['ee', 'el', 'el_from_key']: + n_total_gt_classes, n_total_pr_classes, n_total_correct_classes = 0, 0, 0 + + for step_out in step_outputs: + n_total_gt_classes += step_out[task]["n_batch_gt"] + n_total_pr_classes += step_out[task]["n_batch_pr"] + n_total_correct_classes += step_out[task]["n_batch_correct"] + + precision = ( + 0.0 if n_total_pr_classes == 0 else n_total_correct_classes / n_total_pr_classes + ) + recall = ( + 0.0 if n_total_gt_classes == 0 else n_total_correct_classes / n_total_gt_classes + ) + f1 = ( + 0.0 + if recall * precision == 0 + else 2.0 * recall * precision / (recall + precision) + ) + + scores[task] = { + "precision": precision, + "recall": recall, + "f1": f1, + } + + return scores + + +def get_eval_kwargs_spade(dataset_root_path, max_seq_length): + class_names = get_class_names(dataset_root_path) + dummy_idx = max_seq_length + + eval_kwargs = {"class_names": class_names, "dummy_idx": dummy_idx} + + return eval_kwargs + + +def get_eval_kwargs_spade_rel(max_seq_length): + dummy_idx = max_seq_length + + eval_kwargs = {"dummy_idx": dummy_idx} + + return eval_kwargs \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/schedulers.py b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/schedulers.py new file mode 100755 index 0000000..b49abc2 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/schedulers.py @@ -0,0 +1,53 @@ +""" +BROS +Copyright 2022-present NAVER Corp. +Apache License v2.0 +""" + +import math + +import numpy as np +from torch.optim.lr_scheduler import LambdaLR + + +def linear_scheduler(optimizer, warmup_steps, training_steps, last_epoch=-1): + """linear_scheduler with warmup from huggingface""" + + def lr_lambda(current_step): + if current_step < warmup_steps: + return float(current_step) / float(max(1, warmup_steps)) + return max( + 0.0, + float(training_steps - current_step) + / float(max(1, training_steps - warmup_steps)), + ) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def cosine_scheduler( + optimizer, warmup_steps, training_steps, cycles=0.5, last_epoch=-1 +): + """Cosine LR scheduler with warmup from huggingface""" + + def lr_lambda(current_step): + if current_step < warmup_steps: + return current_step / max(1, warmup_steps) + progress = current_step - warmup_steps + progress /= max(1, training_steps - warmup_steps) + return max(0.0, 0.5 * (1.0 + math.cos(math.pi * cycles * 2 * progress))) + + return LambdaLR(optimizer, lr_lambda, last_epoch) + + +def multistep_scheduler(optimizer, warmup_steps, milestones, gamma=0.1, last_epoch=-1): + def lr_lambda(current_step): + if current_step < warmup_steps: + # calculate a warmup ratio + return current_step / max(1, warmup_steps) + else: + # calculate a multistep lr scaling ratio + idx = np.searchsorted(milestones, current_step) + return gamma ** idx + + return LambdaLR(optimizer, lr_lambda, last_epoch) diff --git a/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/utils.py b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/utils.py new file mode 100755 index 0000000..b9ca682 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/lightning_modules/utils.py @@ -0,0 +1,161 @@ +import os +import copy +import numpy as np +import torch +import torch.nn as nn +import math + + + +def sliding_windows(elements: list, window_size: int, slice_interval: int) -> list: + element_windows = [] + + if len(elements) > window_size: + max_step = math.ceil((len(elements) - window_size)/slice_interval) + + for i in range(0, max_step + 1): + # element_windows.append(copy.deepcopy(elements[min(i, len(elements) - window_size): min(i+window_size, len(elements))])) + if (i*slice_interval+window_size) >= len(elements): + _window = copy.deepcopy(elements[i*slice_interval:]) + else: + _window = copy.deepcopy(elements[i*slice_interval: i*slice_interval+window_size]) + element_windows.append(_window) + return element_windows + else: + return [elements] + +def sliding_windows_by_words(lwords: list, parse_class: dict, parse_relation: list, window_size: int, slice_interval: int) -> list: + word_windows = [] + parse_class_windows = [] + parse_relation_windows = [] + + if len(lwords) > window_size: + max_step = math.ceil((len(lwords) - window_size)/slice_interval) + for i in range(0, max_step+1): + # _word_window = copy.deepcopy(lwords[min(i*slice_interval, len(lwords) - window_size): min(i*slice_interval+window_size, len(lwords))]) + if (i*slice_interval+window_size) >= len(lwords): + _word_window = copy.deepcopy(lwords[i*slice_interval:]) + else: + _word_window = copy.deepcopy(lwords[i*slice_interval: i*slice_interval+window_size]) + + if len(_word_window) < 2: + continue + + first_word_id = _word_window[0]['word_id'] + last_word_id = _word_window[-1]['word_id'] + + # assert (last_word_id - first_word_id == window_size - 1) or (first_word_id == 0 and last_word_id == len(lwords) - 1), [v['word_id'] for v in _word_window] #(last_word_id,first_word_id,len(lwords)) + # word list + for _word in _word_window: + _word['word_id'] -= first_word_id + + + # Entity extraction + _class_window = entity_extraction_by_words(parse_class, first_word_id, last_word_id) + + # Entity Linking + _relation_window = entity_extraction_by_words(parse_class, first_word_id, last_word_id) + + word_windows.append(_word_window) + parse_class_windows.append(_class_window) + parse_relation_windows.append(_relation_window) + + return word_windows, parse_class_windows, parse_relation_windows + else: + return [lwords], [parse_class], [parse_relation] + + +def entity_extraction_by_words(parse_class, first_word_id, last_word_id): + _class_window = {k: [] for k in list(parse_class.keys())} + for class_name, _parse_class in parse_class.items(): + for group in _parse_class: + tmp = [] + for idw in group: + idw -= first_word_id + if 0 <= idw <= (last_word_id - first_word_id): + tmp.append(idw) + _class_window[class_name].append(tmp) + return _class_window + +def entity_linking_by_words(parse_relation, first_word_id, last_word_id): + _relation_window = [] + for pair in parse_relation: + if all([0 <= idw - first_word_id <= (last_word_id - first_word_id) for idw in pair]): + _relation_window.append([idw - first_word_id for idw in pair]) + return _relation_window + + +def merged_token_embeddings(lpatches: list, loverlaps:list, lvalids: list, average: bool) -> torch.tensor: + start_pos = 1 + end_pos = start_pos + lvalids[0] + embedding_tokens = copy.deepcopy(lpatches[0][:, start_pos:end_pos, ...]) + cls_token = copy.deepcopy(lpatches[0][:, :1, ...]) + sep_token = copy.deepcopy(lpatches[0][:, -1:, ...]) + + for i in range(1, len(lpatches)): + start_pos = 1 + end_pos = start_pos + lvalids[i] + + overlap_gap = copy.deepcopy(loverlaps[i-1]) + window = copy.deepcopy(lpatches[i][:, start_pos:end_pos, ...]) + + if overlap_gap != 0: + prev_overlap = copy.deepcopy(embedding_tokens[:, -overlap_gap:, ...]) + curr_overlap = copy.deepcopy(window[:, :overlap_gap, ...]) + assert prev_overlap.shape == curr_overlap.shape, f"{prev_overlap.shape} # {curr_overlap.shape} with overlap: {overlap_gap}" + + if average: + avg_overlap = ( + prev_overlap + curr_overlap + ) / 2. + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], avg_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], curr_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens, window], dim=1 + ) + return torch.cat([cls_token, embedding_tokens, sep_token], dim=1) + + + +def merged_token_embeddings2(lpatches: list, loverlaps:list, lvalids: list, average: bool) -> torch.tensor: + start_pos = 1 + end_pos = start_pos + lvalids[0] + embedding_tokens = lpatches[0][:, start_pos:end_pos, ...] + cls_token = lpatches[0][:, :1, ...] + sep_token = lpatches[0][:, -1:, ...] + + for i in range(1, len(lpatches)): + start_pos = 1 + end_pos = start_pos + lvalids[i] + + overlap_gap = loverlaps[i-1] + window = lpatches[i][:, start_pos:end_pos, ...] + + if overlap_gap != 0: + prev_overlap = embedding_tokens[:, -overlap_gap:, ...] + curr_overlap = window[:, :overlap_gap, ...] + assert prev_overlap.shape == curr_overlap.shape, f"{prev_overlap.shape} # {curr_overlap.shape} with overlap: {overlap_gap}" + + if average: + avg_overlap = ( + prev_overlap + curr_overlap + ) / 2. + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], avg_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], prev_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens, window], dim=1 + ) + return torch.cat([cls_token, embedding_tokens, sep_token], dim=1) + diff --git a/cope2n-ai-fi/common/AnyKey_Value/model/__init__.py b/cope2n-ai-fi/common/AnyKey_Value/model/__init__.py new file mode 100755 index 0000000..1ef6c85 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/model/__init__.py @@ -0,0 +1,15 @@ + +from model.combined_model import CombinedKVUModel +from model.kvu_model import KVUModel +from model.document_kvu_model import DocumentKVUModel + +def get_model(cfg): + if cfg.stage == 1: + model = CombinedKVUModel(cfg=cfg) + elif cfg.stage == 2: + model = KVUModel(cfg=cfg) + elif cfg.stage == 3: + model = DocumentKVUModel(cfg=cfg) + else: + raise Exception('[ERROR] Trainging stage is wrong') + return model diff --git a/cope2n-ai-fi/common/AnyKey_Value/model/combined_model.py b/cope2n-ai-fi/common/AnyKey_Value/model/combined_model.py new file mode 100755 index 0000000..db87e49 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/model/combined_model.py @@ -0,0 +1,76 @@ +import os +import torch +from torch import nn +from transformers import LayoutLMv2Model, LayoutLMv2FeatureExtractor +from transformers import LayoutXLMTokenizer +from transformers import AutoTokenizer, XLMRobertaModel + + +from model.relation_extractor import RelationExtractor +from model.kvu_model import KVUModel +from utils import load_checkpoint + + +class CombinedKVUModel(KVUModel): + def __init__(self, cfg): + super().__init__(cfg) + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.finetune_only = cfg.train.finetune_only + + self._get_backbones(self.model_cfg.backbone) + self._create_head() + + if os.path.exists(self.model_cfg.ckpt_model_file): + self.backbone_layoutxlm = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone_layoutxlm, 'backbone_layoutxlm') + self.itc_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.itc_layer, 'itc_layer') + self.stc_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.stc_layer, 'stc_layer') + self.relation_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.relation_layer, 'relation_layer') + self.relation_layer_from_key = load_checkpoint(self.model_cfg.ckpt_model_file, self.relation_layer_from_key, 'relation_layer_from_key') + + self.loss_func = nn.CrossEntropyLoss() + + if self.freeze: + for name, param in self.named_parameters(): + if 'backbone' in name: + param.requires_grad = False + if self.finetune_only == 'EE': + for name, param in self.named_parameters(): + if 'itc_layer' not in name and 'stc_layer' not in name: + param.requires_grad = False + if self.finetune_only == 'EL': + for name, param in self.named_parameters(): + if 'relation_layer' not in name or 'relation_layer_from_key' in name: + param.requires_grad = False + if self.finetune_only == 'ELK': + for name, param in self.named_parameters(): + if 'relation_layer_from_key' not in name: + param.requires_grad = False + + + def forward(self, batch): + image = batch["image"] + input_ids_layoutxlm = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask_layoutxlm = batch["attention_mask_layoutxlm"] + + backbone_outputs_layoutxlm = self.backbone_layoutxlm( + image=image, input_ids=input_ids_layoutxlm, bbox=bbox, attention_mask=attention_mask_layoutxlm) + + last_hidden_states = backbone_outputs_layoutxlm.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(last_hidden_states, last_hidden_states).squeeze(0) + head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key} + + loss = 0.0 + if any(['labels' in key for key in batch.keys()]): + loss = self._get_loss(head_outputs, batch) + + return head_outputs, loss + diff --git a/cope2n-ai-fi/common/AnyKey_Value/model/document_kvu_model.py b/cope2n-ai-fi/common/AnyKey_Value/model/document_kvu_model.py new file mode 100755 index 0000000..0e04ed3 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/model/document_kvu_model.py @@ -0,0 +1,185 @@ +import torch +from torch import nn +from transformers import LayoutLMConfig, LayoutLMModel, LayoutLMTokenizer, LayoutLMv2FeatureExtractor +from transformers import LayoutLMv2Config, LayoutLMv2Model +from transformers import LayoutXLMTokenizer +from transformers import XLMRobertaConfig, AutoTokenizer, XLMRobertaModel + + +from model.relation_extractor import RelationExtractor +from model.kvu_model import KVUModel +from utils import load_checkpoint + + +class DocumentKVUModel(KVUModel): + def __init__(self, cfg): + super().__init__(cfg) + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.train_cfg = cfg.train + + self._get_backbones(self.model_cfg.backbone) + # if 'pth' in self.model_cfg.ckpt_model_file: + # self.backbone = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone) + + self._create_head() + + self.loss_func = nn.CrossEntropyLoss() + + def _create_head(self): + self.backbone_hidden_size = self.backbone_config.hidden_size + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + self.n_classes = self.model_cfg.n_classes + 1 + self.repr_hiddent_size = self.backbone_hidden_size + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # Classfication Layer for whole document + # (1) Initial token classification + self.itc_layer_document = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.repr_hiddent_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + # (3) Linking token classification + self.relation_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + # (4) Linking token classification + self.relation_layer_from_key_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + self.relation_layer_from_key.apply(self._init_weight) + + self.itc_layer_document.apply(self._init_weight) + self.stc_layer_document.apply(self._init_weight) + self.relation_layer_document.apply(self._init_weight) + self.relation_layer_from_key_document.apply(self._init_weight) + + + def _get_backbones(self, config_type): + configs = { + 'layoutlm': {'config': LayoutLMConfig, 'tokenizer': LayoutLMTokenizer, 'backbone': LayoutLMModel, 'feature_extrator': LayoutLMv2FeatureExtractor}, + 'layoutxlm': {'config': LayoutLMv2Config, 'tokenizer': LayoutXLMTokenizer, 'backbone': LayoutLMv2Model, 'feature_extrator': LayoutLMv2FeatureExtractor}, + 'xlm-roberta': {'config': XLMRobertaConfig, 'tokenizer': AutoTokenizer, 'backbone': XLMRobertaModel, 'feature_extrator': LayoutLMv2FeatureExtractor}, + } + + self.backbone_config = configs[config_type]['config'].from_pretrained(self.model_cfg.pretrained_model_path) + if config_type != 'xlm-roberta': + self.tokenizer = configs[config_type]['tokenizer'].from_pretrained(self.model_cfg.pretrained_model_path) + else: + self.tokenizer = configs[config_type]['tokenizer'].from_pretrained(self.model_cfg.pretrained_model_path, use_fast=False) + self.feature_extractor = configs[config_type]['feature_extrator'](apply_ocr=False) + self.backbone = configs[config_type]['backbone'].from_pretrained(self.model_cfg.pretrained_model_path) + + + def forward(self, batches): + head_outputs_list = [] + loss = 0.0 + for batch in batches["windows"]: + image = batch["image"] + input_ids = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask = batch["attention_mask_layoutxlm"] + + if self.freeze: + for param in self.backbone.parameters(): + param.requires_grad = False + + if self.model_cfg.backbone == 'layoutxlm': + backbone_outputs = self.backbone( + image=image, input_ids=input_ids, bbox=bbox, attention_mask=attention_mask + ) + else: + backbone_outputs = self.backbone(input_ids, attention_mask=attention_mask) + + last_hidden_states = backbone_outputs.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(last_hidden_states, last_hidden_states).squeeze(0) + + window_repr = last_hidden_states.transpose(0, 1).contiguous() + + head_outputs = {"window_repr": window_repr, + "itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + head_outputs_list.append(head_outputs) + + batch = batches["documents"] + + document_repr = torch.cat([w['window_repr'] for w in head_outputs_list], dim=1) + document_repr = document_repr.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer_document(document_repr).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer_document(document_repr, document_repr).squeeze(0) + el_outputs = self.relation_layer_document(document_repr, document_repr).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key_document(document_repr, document_repr).squeeze(0) + + head_outputs = {"itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + return head_outputs, loss + diff --git a/cope2n-ai-fi/common/AnyKey_Value/model/kvu_model.py b/cope2n-ai-fi/common/AnyKey_Value/model/kvu_model.py new file mode 100755 index 0000000..d500370 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/model/kvu_model.py @@ -0,0 +1,248 @@ +import os +import torch +from torch import nn +from transformers import LayoutLMv2Model, LayoutLMv2FeatureExtractor +from transformers import LayoutXLMTokenizer +from lightning_modules.utils import merged_token_embeddings, merged_token_embeddings2 + + +from model.relation_extractor import RelationExtractor +from utils import load_checkpoint + + +class KVUModel(nn.Module): + def __init__(self, cfg): + super().__init__() + + self.device = 'cuda' + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.finetune_only = cfg.train.finetune_only + + # if cfg.stage == 2: + # self.freeze = True + + self._get_backbones(self.model_cfg.backbone) + self._create_head() + + if (cfg.stage == 2) and (os.path.exists(self.model_cfg.ckpt_model_file)): + self.backbone_layoutxlm = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone_layoutxlm, 'backbone_layoutxlm') + + self._create_head() + self.loss_func = nn.CrossEntropyLoss() + + if self.freeze: + for name, param in self.named_parameters(): + if 'backbone' in name: + param.requires_grad = False + + def _create_head(self): + self.backbone_hidden_size = 768 + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + self.n_classes = self.model_cfg.n_classes + 1 + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + + + def _get_backbones(self, config_type): + self.tokenizer_layoutxlm = LayoutXLMTokenizer.from_pretrained('microsoft/layoutxlm-base') + self.feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) + self.backbone_layoutxlm = LayoutLMv2Model.from_pretrained('microsoft/layoutxlm-base') + + @staticmethod + def _init_weight(module): + init_std = 0.02 + if isinstance(module, nn.Linear): + nn.init.normal_(module.weight, 0.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + elif isinstance(module, nn.LayerNorm): + nn.init.normal_(module.weight, 1.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + + + # def forward(self, inputs): + # token_embeddings = inputs['embeddings'].transpose(0, 1).contiguous().cuda() + # itc_outputs = self.itc_layer(token_embeddings).transpose(0, 1).contiguous() + # stc_outputs = self.stc_layer(token_embeddings, token_embeddings).squeeze(0) + # el_outputs = self.relation_layer(token_embeddings, token_embeddings).squeeze(0) + # el_outputs_from_key = self.relation_layer_from_key(token_embeddings, token_embeddings).squeeze(0) + # head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + # "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key} + + # loss = self._get_loss(head_outputs, inputs) + # return head_outputs, loss + + + # def forward_single_doccument(self, lbatches): + def forward(self, lbatches): + windows = lbatches['windows'] + token_embeddings_windows = [] + lvalids = [] + loverlaps = [] + + for i, batch in enumerate(windows): + batch = {k: v.cuda() for k, v in batch.items() if k not in ('img_path', 'words')} + image = batch["image"] + input_ids_layoutxlm = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask_layoutxlm = batch["attention_mask_layoutxlm"] + + + backbone_outputs_layoutxlm = self.backbone_layoutxlm( + image=image, input_ids=input_ids_layoutxlm, bbox=bbox, attention_mask=attention_mask_layoutxlm) + + + last_hidden_states_layoutxlm = backbone_outputs_layoutxlm.last_hidden_state[:, :512, :] + + lvalids.append(batch['len_valid_tokens']) + loverlaps.append(batch['len_overlap_tokens']) + token_embeddings_windows.append(last_hidden_states_layoutxlm) + + + token_embeddings = merged_token_embeddings2(token_embeddings_windows, loverlaps, lvalids, average=False) + # token_embeddings = merged_token_embeddings(token_embeddings_windows, loverlaps, lvalids, average=True) + + + token_embeddings = token_embeddings.transpose(0, 1).contiguous().cuda() + itc_outputs = self.itc_layer(token_embeddings).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(token_embeddings, token_embeddings).squeeze(0) + el_outputs = self.relation_layer(token_embeddings, token_embeddings).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(token_embeddings, token_embeddings).squeeze(0) + head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key, + 'embedding_tokens': token_embeddings.transpose(0, 1).contiguous().detach().cpu().numpy()} + + + + loss = 0.0 + if any(['labels' in key for key in lbatches.keys()]): + labels = {k: v.cuda() for k, v in lbatches["documents"].items() if k not in ('img_path')} + loss = self._get_loss(head_outputs, labels) + + return head_outputs, loss + + def _get_loss(self, head_outputs, batch): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + itc_loss = self._get_itc_loss(itc_outputs, batch) + stc_loss = self._get_stc_loss(stc_outputs, batch) + el_loss = self._get_el_loss(el_outputs, batch) + el_loss_from_key = self._get_el_loss(el_outputs_from_key, batch, from_key=True) + + loss = itc_loss + stc_loss + el_loss + el_loss_from_key + + return loss + + def _get_itc_loss(self, itc_outputs, batch): + itc_mask = batch["are_box_first_tokens"].view(-1) + + itc_logits = itc_outputs.view(-1, self.model_cfg.n_classes + 1) + itc_logits = itc_logits[itc_mask] + + itc_labels = batch["itc_labels"].view(-1) + itc_labels = itc_labels[itc_mask] + + itc_loss = self.loss_func(itc_logits, itc_labels) + + return itc_loss + + def _get_stc_loss(self, stc_outputs, batch): + inv_attention_mask = 1 - batch["attention_mask_layoutxlm"] + + bsz, max_seq_length = inv_attention_mask.shape + device = inv_attention_mask.device + + invalid_token_mask = torch.cat( + [inv_attention_mask, torch.zeros([bsz, 1]).to(device)], axis=1 + ).bool() + + stc_outputs.masked_fill_(invalid_token_mask[:, None, :], -10000.0) + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + stc_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + stc_mask = batch["attention_mask_layoutxlm"].view(-1).bool() + stc_logits = stc_outputs.view(-1, max_seq_length + 1) + stc_logits = stc_logits[stc_mask] + + stc_labels = batch["stc_labels"].view(-1) + stc_labels = stc_labels[stc_mask] + + stc_loss = self.loss_func(stc_logits, stc_labels) + + return stc_loss + + def _get_el_loss(self, el_outputs, batch, from_key=False): + bsz, max_seq_length = batch["attention_mask_layoutxlm"].shape + + device = batch["attention_mask_layoutxlm"].device + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + + box_first_token_mask = torch.cat( + [ + (batch["are_box_first_tokens"] == False), + torch.zeros([bsz, 1], dtype=torch.bool).to(device), + ], + axis=1, + ) + el_outputs.masked_fill_(box_first_token_mask[:, None, :], -10000.0) + el_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + mask = batch["are_box_first_tokens"].view(-1) + + logits = el_outputs.view(-1, max_seq_length + 1) + logits = logits[mask] + + if from_key: + el_labels = batch["el_labels_from_key"] + else: + el_labels = batch["el_labels"] + labels = el_labels.view(-1) + labels = labels[mask] + + loss = self.loss_func(logits, labels) + return loss diff --git a/cope2n-ai-fi/common/AnyKey_Value/model/relation_extractor.py b/cope2n-ai-fi/common/AnyKey_Value/model/relation_extractor.py new file mode 100755 index 0000000..40a169e --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/model/relation_extractor.py @@ -0,0 +1,48 @@ +import torch +from torch import nn + + +class RelationExtractor(nn.Module): + def __init__( + self, + n_relations, + backbone_hidden_size, + head_hidden_size, + head_p_dropout=0.1, + ): + super().__init__() + + self.n_relations = n_relations + self.backbone_hidden_size = backbone_hidden_size + self.head_hidden_size = head_hidden_size + self.head_p_dropout = head_p_dropout + + self.drop = nn.Dropout(head_p_dropout) + self.q_net = nn.Linear( + self.backbone_hidden_size, self.n_relations * self.head_hidden_size + ) + + self.k_net = nn.Linear( + self.backbone_hidden_size, self.n_relations * self.head_hidden_size + ) + + self.dummy_node = nn.Parameter(torch.Tensor(1, self.backbone_hidden_size)) + nn.init.normal_(self.dummy_node) + + def forward(self, h_q, h_k): + h_q = self.q_net(self.drop(h_q)) + + dummy_vec = self.dummy_node.unsqueeze(0).repeat(1, h_k.size(1), 1) + h_k = torch.cat([h_k, dummy_vec], axis=0) + h_k = self.k_net(self.drop(h_k)) + + head_q = h_q.view( + h_q.size(0), h_q.size(1), self.n_relations, self.head_hidden_size + ) + head_k = h_k.view( + h_k.size(0), h_k.size(1), self.n_relations, self.head_hidden_size + ) + + relation_score = torch.einsum("ibnd,jbnd->nbij", (head_q, head_k)) + + return relation_score diff --git a/cope2n-ai-fi/common/AnyKey_Value/predictor.py b/cope2n-ai-fi/common/AnyKey_Value/predictor.py new file mode 100755 index 0000000..3a68bf0 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/predictor.py @@ -0,0 +1,228 @@ +from omegaconf import OmegaConf +import torch +# from functions import get_colormap, visualize +import sys +sys.path.append('/mnt/ssd1T/tuanlv/02.KeyValueUnderstanding/') #TODO: ?????? + +from lightning_modules.classifier_module import parse_initial_words, parse_subsequent_words, parse_relations +from model import get_model +from utils import load_model_weight + + +class KVUPredictor: + def __init__(self, configs, class_names, dummy_idx, mode=0): + cfg_path = configs['cfg'] + ckpt_path = configs['ckpt'] + + self.class_names = class_names + self.dummy_idx = dummy_idx + self.mode = mode + + print('[INFO] Loading Key-Value Understanding model ...') + self.net, cfg, self.backbone_type = self._load_model(cfg_path, ckpt_path) + print("[INFO] Loaded model") + + if mode == 3: + self.max_window_count = cfg.train.max_window_count + self.window_size = cfg.train.window_size + self.slice_interval = 0 + self.dummy_idx = dummy_idx * self.max_window_count + else: + self.slice_interval = cfg.train.slice_interval + self.window_size = cfg.train.max_num_words + + + self.device = 'cuda' + + def _load_model(self, cfg_path, ckpt_path): + cfg = OmegaConf.load(cfg_path) + cfg.stage = self.mode + backbone_type = cfg.model.backbone + + print('[INFO] Checkpoint:', ckpt_path) + net = get_model(cfg) + load_model_weight(net, ckpt_path) + net.to('cuda') + net.eval() + return net, cfg, backbone_type + + def predict(self, input_sample): + if self.mode == 0: + if len(input_sample['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = self.combined_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 1: + if len(input_sample['documents']['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = self.cat_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 2: + if len(input_sample['windows'][0]['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = [], [], [], [] + for window in input_sample['windows']: + _bbox, _lwords, _pr_class_words, _pr_relations = self.combined_predict(window) + bbox.append(_bbox) + lwords.append(_lwords) + pr_class_words.append(_pr_class_words) + pr_relations.append(_pr_relations) + return bbox, lwords, pr_class_words, pr_relations + + elif self.mode == 3: + if len(input_sample["documents"]['words']) == 0: + return [], [], [], [] + bbox, lwords, pr_class_words, pr_relations = self.doc_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + else: + raise ValueError( + f"Not supported mode: {self.mode}" + ) + + def doc_predict(self, input_sample): + lwords = input_sample['documents']['words'] + for idx, window in enumerate(input_sample['windows']): + input_sample['windows'][idx] = {k: v.unsqueeze(0).to(self.device) for k, v in window.items() if k not in ('words', 'n_empty_windows')} + + # input_sample['documents'] = {k: v.unsqueeze(0).to(self.device) for k, v in input_sample['documents'].items() if k not in ('words', 'n_empty_windows')} + with torch.no_grad(): + head_outputs, _ = self.net(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + input_sample = input_sample['documents'] + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['are_box_first_tokens'].squeeze(0) + attention_mask = input_sample['attention_mask_layoutxlm'].squeeze(0) + bbox = input_sample['bbox'].squeeze(0) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, self.dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, self.dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, self.dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return bbox, lwords, pr_class_words, pr_relations + + + def combined_predict(self, input_sample): + lwords = input_sample['words'] + input_sample = {k: v.unsqueeze(0) for k, v in input_sample.items() if k not in ('words', 'img_path')} + + input_sample = {k: v.to(self.device) for k, v in input_sample.items()} + + + with torch.no_grad(): + head_outputs, _ = self.net(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + input_sample = {k: v.detach().cpu() for k, v in input_sample.items()} + + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['are_box_first_tokens'].squeeze(0) + attention_mask = input_sample['attention_mask_layoutxlm'].squeeze(0) + bbox = input_sample['bbox'].squeeze(0) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, self.dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, self.dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, self.dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return bbox, lwords, pr_class_words, pr_relations + + def cat_predict(self, input_sample): + lwords = input_sample['documents']['words'] + + inputs = [] + for window in input_sample['windows']: + inputs.append({k: v.unsqueeze(0).cuda() for k, v in window.items() if k not in ('words', 'img_path')}) + input_sample['windows'] = inputs + + with torch.no_grad(): + head_outputs, _ = self.net(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items() if k not in ('embedding_tokens')} + + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['documents']['are_box_first_tokens'] + attention_mask = input_sample['documents']['attention_mask_layoutxlm'] + bbox = input_sample['documents']['bbox'] + + dummy_idx = input_sample['documents']['bbox'].shape[0] + + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return bbox, lwords, pr_class_words, pr_relations + + + def get_ground_truth_label(self, ground_truth): + # ground_truth = self.preprocessor.load_ground_truth(json_file) + gt_itc_label = ground_truth['itc_labels'].squeeze(0) # [1, 512] => [512] + gt_stc_label = ground_truth['stc_labels'].squeeze(0) # [1, 512] => [512] + gt_el_label = ground_truth['el_labels'].squeeze(0) + + gt_el_label_from_key = ground_truth['el_labels_from_key'].squeeze(0) + lwords = ground_truth["words"] + + box_first_token_mask = ground_truth['are_box_first_tokens'].squeeze(0) + attention_mask = ground_truth['attention_mask'].squeeze(0) + + bbox = ground_truth['bbox'].squeeze(0) + gt_first_words = parse_initial_words( + gt_itc_label, box_first_token_mask, self.class_names + ) + gt_class_words = parse_subsequent_words( + gt_stc_label, attention_mask, gt_first_words, self.dummy_idx + ) + + gt_relations_from_header = parse_relations(gt_el_label, box_first_token_mask, self.dummy_idx) + gt_relations_from_key = parse_relations(gt_el_label_from_key, box_first_token_mask, self.dummy_idx) + gt_relations = gt_relations_from_header | gt_relations_from_key + + return bbox, lwords, gt_class_words, gt_relations \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/preprocess.py b/cope2n-ai-fi/common/AnyKey_Value/preprocess.py new file mode 100755 index 0000000..365c745 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/preprocess.py @@ -0,0 +1,456 @@ +import os +from typing import Any +import numpy as np +import pandas as pd +import imagesize +import itertools +from PIL import Image +import argparse + +import torch + +from utils.utils import read_ocr_result_from_txt, read_json, post_process_basic_ocr +from utils.run_ocr import load_ocr_engine, process_img +from lightning_modules.utils import sliding_windows + + +class KVUProcess: + def __init__(self, tokenizer_layoutxlm, feature_extractor, backbone_type, class_names, slice_interval, window_size, run_ocr, max_seq_length=512, mode=0): + self.tokenizer_layoutxlm = tokenizer_layoutxlm + self.feature_extractor = feature_extractor + + self.max_seq_length = max_seq_length + self.backbone_type = backbone_type + self.class_names = class_names + + self.slice_interval = slice_interval + self.window_size = window_size + self.run_ocr = run_ocr + self.mode = mode + + self.pad_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(tokenizer_layoutxlm._pad_token) + self.cls_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(tokenizer_layoutxlm._cls_token) + self.sep_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(tokenizer_layoutxlm._sep_token) + self.unk_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm._unk_token) + + + self.class_idx_dic = dict( + [(class_name, idx) for idx, class_name in enumerate(self.class_names)] + ) + self.ocr_engine = None + if self.run_ocr == 1: + self.ocr_engine = load_ocr_engine() + + def __call__(self, img_path: str, ocr_path: str) -> list: + if (self.run_ocr == 1) or (not os.path.exists(ocr_path)): + ocr_path = "tmp.txt" + process_img(img_path, ocr_path, self.ocr_engine, export_img=False) + + lbboxes, lwords = read_ocr_result_from_txt(ocr_path) + lwords = post_process_basic_ocr(lwords) + bbox_windows = sliding_windows(lbboxes, self.window_size, self.slice_interval) + word_windows = sliding_windows(lwords, self.window_size, self.slice_interval) + assert len(bbox_windows) == len(word_windows), f"Shape of lbboxes and lwords after sliding window is not the same {len(bbox_windows)} # {len(word_windows)}" + + width, height = imagesize.get(img_path) + images = [Image.open(img_path).convert("RGB")] + image_features = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + + + if self.mode == 0: + output = self.preprocess(lbboxes, lwords, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + elif self.mode == 1: + output = {} + windows = [] + for i in range(len(bbox_windows)): + _words = word_windows[i] + _bboxes = bbox_windows[i] + windows.append( + self.preprocess( + _bboxes, _words, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + ) + + output['windows'] = windows + elif self.mode == 2: + output = {} + windows = [] + output['doduments'] = self.preprocess(lbboxes, lwords, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=2048) + for i in range(len(bbox_windows)): + _words = word_windows[i] + _bboxes = bbox_windows[i] + windows.append( + self.preprocess( + _bboxes, _words, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + max_seq_length=self.max_seq_length) + ) + + output['windows'] = windows + else: + raise ValueError( + f"Not supported mode: {self.mode }" + ) + return output + + + def preprocess(self, bounding_boxes, words, feature_maps, max_seq_length): + list_word_objects = [] + for bb, text in zip(bounding_boxes, words): + boundingBox = [[bb[0], bb[1]], [bb[2], bb[1]], [bb[2], bb[3]], [bb[0], bb[3]]] + tokens = self.tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm.tokenize(text)) + list_word_objects.append({ + "layoutxlm_tokens": tokens, + "boundingBox": boundingBox, + "text": text + }) + + ( + bbox, + input_ids, + attention_mask, + are_box_first_tokens, + box_to_token_indices, + box2token_span_map, + lwords, + len_valid_tokens, + len_non_overlap_tokens, + len_list_tokens + ) = self.parser_words(list_word_objects, self.max_seq_length, feature_maps["width"], feature_maps["height"]) + + assert len_list_tokens == len_valid_tokens + 2 + len_overlap_tokens = len_valid_tokens - len_non_overlap_tokens + + ntokens = max_seq_length if max_seq_length == 512 else len_valid_tokens + 2 + + input_ids = input_ids[:ntokens] + attention_mask = attention_mask[:ntokens] + bbox = bbox[:ntokens] + are_box_first_tokens = are_box_first_tokens[:ntokens] + + + input_ids = torch.from_numpy(input_ids) + attention_mask = torch.from_numpy(attention_mask) + bbox = torch.from_numpy(bbox) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + + len_valid_tokens = torch.tensor(len_valid_tokens) + len_overlap_tokens = torch.tensor(len_overlap_tokens) + return_dict = { + "img_path": feature_maps['img_path'], + "words": lwords, + "len_overlap_tokens": len_overlap_tokens, + 'len_valid_tokens': len_valid_tokens, + "image": feature_maps['image'], + "input_ids_layoutxlm": input_ids, + "attention_mask_layoutxlm": attention_mask, + "are_box_first_tokens": are_box_first_tokens, + "bbox": bbox, + } + return return_dict + + + def parser_words(self, words, max_seq_length, width, height): + list_bbs = [] + list_words = [] + list_tokens = [] + cls_bbs = [0.0] * 8 + box2token_span_map = [] + box_to_token_indices = [] + lwords = [''] * max_seq_length + + cum_token_idx = 0 + len_valid_tokens = 0 + len_non_overlap_tokens = 0 + + + input_ids = np.ones(max_seq_length, dtype=int) * self.pad_token_id_layoutxlm + bbox = np.zeros((max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(max_seq_length, dtype=np.bool_) + + for word_idx, word in enumerate(words): + this_box_token_indices = [] + + tokens = word["layoutxlm_tokens"] + bb = word["boundingBox"] + text = word["text"] + + len_valid_tokens += len(tokens) + if word_idx < self.slice_interval: + len_non_overlap_tokens += len(tokens) + + if len(tokens) == 0: + tokens.append(self.unk_token_id) + + if len(list_tokens) + len(tokens) > max_seq_length - 2: + break + + box2token_span_map.append( + [len(list_tokens) + 1, len(list_tokens) + len(tokens) + 1] + ) # including st_idx + list_tokens += tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], width)) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], height)) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(tokens))] + texts = [text for _ in range(len(tokens))] + + for _ in tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + list_words.extend(texts) #### + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [width, height] * 4 + + # For [CLS] and [SEP] + list_tokens = ( + [self.cls_token_id_layoutxlm] + + list_tokens[: max_seq_length - 2] + + [self.sep_token_id_layoutxlm] + ) + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: max_seq_length - 2] + [sep_bbs] + # list_words = ['CLS'] + list_words[: max_seq_length - 2] + ['SEP'] ### + # if len(list_words) < 510: + # list_words.extend(['

    ' for _ in range(510 - len(list_words))]) + list_words = [self.tokenizer_layoutxlm._cls_token] + list_words[: max_seq_length - 2] + [self.tokenizer_layoutxlm._sep_token] + + + len_list_tokens = len(list_tokens) + input_ids[:len_list_tokens] = list_tokens + attention_mask[:len_list_tokens] = 1 + + bbox[:len_list_tokens, :] = list_bbs + lwords[:len_list_tokens] = list_words + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / width + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / height + + if self.backbone_type in ("layoutlm", "layoutxlm"): + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + else: + assert False + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < max_seq_length + ] + are_box_first_tokens[st_indices] = True + + return ( + bbox, + input_ids, + attention_mask, + are_box_first_tokens, + box_to_token_indices, + box2token_span_map, + lwords, + len_valid_tokens, + len_non_overlap_tokens, + len_list_tokens + ) + + + def parser_entity_extraction(self, parse_class, box_to_token_indices, max_seq_length): + itc_labels = np.zeros(max_seq_length, dtype=int) + stc_labels = np.ones(max_seq_length, dtype=np.int64) * max_seq_length + + classes_dic = parse_class + for class_name in self.class_names: + if class_name == "others": + continue + if class_name not in classes_dic: + continue + + for word_list in classes_dic[class_name]: + is_first, last_word_idx = True, -1 + for word_idx in word_list: + if word_idx >= len(box_to_token_indices): + break + box2token_list = box_to_token_indices[word_idx] + for converted_word_idx in box2token_list: + if converted_word_idx >= max_seq_length: + break # out of idx + + if is_first: + itc_labels[converted_word_idx] = self.class_idx_dic[ + class_name + ] + is_first, last_word_idx = False, converted_word_idx + else: + stc_labels[converted_word_idx] = last_word_idx + last_word_idx = converted_word_idx + + return itc_labels, stc_labels + + + def parser_entity_linking(self, parse_relation, itc_labels, box2token_span_map, max_seq_length): + el_labels = np.ones(max_seq_length, dtype=int) * max_seq_length + el_labels_from_key = np.ones(max_seq_length, dtype=int) * max_seq_length + + + relations = parse_relation + for relation in relations: + if relation[0] >= len(box2token_span_map) or relation[1] >= len( + box2token_span_map + ): + continue + if ( + box2token_span_map[relation[0]][0] >= max_seq_length + or box2token_span_map[relation[1]][0] >= max_seq_length + ): + continue + + word_from = box2token_span_map[relation[0]][0] + word_to = box2token_span_map[relation[1]][0] + # el_labels[word_to] = word_from + + if el_labels[word_to] != 512 and el_labels_from_key[word_to] != 512: + continue + + if itc_labels[word_from] == 2 and itc_labels[word_to] == 3: + el_labels_from_key[word_to] = word_from # pair of (key-value) + if itc_labels[word_from] == 4 and (itc_labels[word_to] in (2, 3)): + el_labels[word_to] = word_from # pair of (header, key) or (header-value) + return el_labels, el_labels_from_key + + +class DocumentKVUProcess(KVUProcess): + def __init__(self, tokenizer_layoutxlm, feature_extractor, backbone_type, class_names, max_window_count, slice_interval, window_size, run_ocr, max_seq_length=512, mode=0): + super().__init__(tokenizer_layoutxlm, feature_extractor, backbone_type, class_names, slice_interval, window_size, run_ocr, max_seq_length, mode) + self.max_window_count = max_window_count + self.pad_token_id = self.pad_token_id_layoutxlm + self.cls_token_id = self.cls_token_id_layoutxlm + self.sep_token_id = self.sep_token_id_layoutxlm + self.unk_token_id = self.unk_token_id_layoutxlm + self.tokenizer = self.tokenizer_layoutxlm + + def __call__(self, img_path: str, ocr_path: str) -> list: + if (self.run_ocr == 1) and (not os.path.exists(ocr_path)): + ocr_path = "tmp.txt" + process_img(img_path, ocr_path, self.ocr_engine, export_img=False) + + lbboxes, lwords = read_ocr_result_from_txt(ocr_path) + lwords = post_process_basic_ocr(lwords) + + width, height = imagesize.get(img_path) + images = [Image.open(img_path).convert("RGB")] + image_features = torch.from_numpy(self.feature_extractor(images)['pixel_values'][0].copy()) + output = self.preprocess(lbboxes, lwords, + {'image': image_features, 'width': width, 'height': height, 'img_path': img_path}, + self.max_seq_length) + return output + + def preprocess(self, bounding_boxes, words, feature_maps, max_seq_length): + n_words = len(words) + output_dicts = {'windows': [], 'documents': []} + n_empty_windows = 0 + + for i in range(self.max_window_count): + input_ids = np.ones(self.max_seq_length, dtype=int) * self.pad_token_id + bbox = np.zeros((self.max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(self.max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(self.max_seq_length, dtype=np.bool_) + + if n_words == 0: + n_empty_windows += 1 + output_dicts['windows'].append({ + "image": feature_maps['image'], + "input_ids_layoutxlm": torch.from_numpy(input_ids), + "bbox": torch.from_numpy(bbox), + "words": [], + "attention_mask_layoutxlm": torch.from_numpy(attention_mask), + "are_box_first_tokens": torch.from_numpy(are_box_first_tokens), + }) + continue + + start_word_idx = i * self.window_size + stop_word_idx = min(n_words, (i+1)*self.window_size) + + if start_word_idx >= stop_word_idx: + n_empty_windows += 1 + output_dicts['windows'].append(output_dicts['windows'][-1]) + continue + + list_word_objects = [] + for bb, text in zip(bounding_boxes[start_word_idx:stop_word_idx], words[start_word_idx:stop_word_idx]): + boundingBox = [[bb[0], bb[1]], [bb[2], bb[1]], [bb[2], bb[3]], [bb[0], bb[3]]] + tokens = self.tokenizer_layoutxlm.convert_tokens_to_ids(self.tokenizer_layoutxlm.tokenize(text)) + list_word_objects.append({ + "layoutxlm_tokens": tokens, + "boundingBox": boundingBox, + "text": text + }) + + ( + bbox, + input_ids, + attention_mask, + are_box_first_tokens, + box_to_token_indices, + box2token_span_map, + lwords, + len_valid_tokens, + len_non_overlap_tokens, + len_list_layoutxlm_tokens + ) = self.parser_words(list_word_objects, self.max_seq_length, feature_maps["width"], feature_maps["height"]) + + + input_ids = torch.from_numpy(input_ids) + bbox = torch.from_numpy(bbox) + attention_mask = torch.from_numpy(attention_mask) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + + return_dict = { + "image": feature_maps['image'], + "input_ids_layoutxlm": input_ids, + "bbox": bbox, + "words": lwords, + "attention_mask_layoutxlm": attention_mask, + "are_box_first_tokens": are_box_first_tokens, + } + output_dicts["windows"].append(return_dict) + + attention_mask = torch.cat([o['attention_mask_layoutxlm'] for o in output_dicts["windows"]]) + are_box_first_tokens = torch.cat([o['are_box_first_tokens'] for o in output_dicts["windows"]]) + if n_empty_windows > 0: + attention_mask[self.max_seq_length * (self.max_window_count - n_empty_windows):] = torch.from_numpy(np.zeros(self.max_seq_length * n_empty_windows, dtype=int)) + are_box_first_tokens[self.max_seq_length * (self.max_window_count - n_empty_windows):] = torch.from_numpy(np.zeros(self.max_seq_length * n_empty_windows, dtype=np.bool_)) + bbox = torch.cat([o['bbox'] for o in output_dicts["windows"]]) + words = [] + for o in output_dicts['windows']: + words.extend(o['words']) + + return_dict = { + "attention_mask_layoutxlm": attention_mask, + "bbox": bbox, + "are_box_first_tokens": are_box_first_tokens, + "n_empty_windows": n_empty_windows, + "words": words + } + output_dicts['documents'] = return_dict + + return output_dicts + + + \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/requirements.txt b/cope2n-ai-fi/common/AnyKey_Value/requirements.txt new file mode 100755 index 0000000..82c2ce3 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/requirements.txt @@ -0,0 +1,30 @@ +nptyping==1.4.2 +numpy==1.20.3 +opencv-python-headless==4.5.4.60 +pytorch-lightning==1.5.6 +omegaconf +# pillow +six +overrides==4.1.2 +# transformers==4.11.3 +seqeval==0.0.12 +imagesize +pandas==2.0.1 +xmltodict +dicttoxml + +tensorboard>=2.2.0 + +# code-style +isort==5.9.3 +black==21.9b0 + +# # pytorch +# --find-links https://download.pytorch.org/whl/torch_stable.html +# torch==1.9.1+cu102 +# torchvision==0.10.1+cu102 + +# pytorch +# --find-links https://download.pytorch.org/whl/torch_stable.html +# torch==1.10.0+cu113 +# torchvision==0.11.1+cu113 diff --git a/cope2n-ai-fi/common/AnyKey_Value/run.sh b/cope2n-ai-fi/common/AnyKey_Value/run.sh new file mode 100755 index 0000000..1b0442f --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/run.sh @@ -0,0 +1 @@ +python anyKeyValue.py --img_dir /home/ai-core/Kie_Invoice_AP/AnyKey_Value/visualize/test/ --save_dir /home/ai-core/Kie_Invoice_AP/AnyKey_Value/visualize/test/ --exp_dir /home/ai-core/Kie_Invoice_AP/AnyKey_Value/experiments/key_value_understanding-20230608-171900 --export_img 1 --mode 3 --dir_level 0 \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/tmp.txt b/cope2n-ai-fi/common/AnyKey_Value/tmp.txt new file mode 100755 index 0000000..776ddc3 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/tmp.txt @@ -0,0 +1,393 @@ +550 14 644 53 HÓA +654 20 745 53 ĐƠN +755 13 833 53 GIÁ +842 20 916 60 TRỊ +925 22 1001 53 GIA +1012 14 1130 54 TĂNG +208 63 363 87 CHUNGHO +752 76 815 101 (VAT +821 76 940 101 INVOICE) +1193 80 1230 109 Ký +1235 80 1286 109 hiệu +1293 83 1390 108 (Serial): +1398 81 1519 105 1C23TCV +135 130 330 171 ChungHo +339 131 436 165 Vina +600 150 662 178 (BẢN +667 152 719 177 THỂ +678 119 743 147 Ngày +724 152 788 177 HIÊN +749 119 779 142 01 +786 119 854 147 tháng +794 152 847 176 CỦA +853 151 908 176 HÓA +860 119 891 142 03 +897 121 949 142 năm +915 156 973 176 ĐƠN +956 120 1014 142 2023 +979 154 1043 179 ĐIỆN +1049 151 1093 181 TỬ) +1193 118 1228 148 Số +1234 125 1303 151 (No.): +1309 116 1372 148 829 +187 179 270 197 HEALTH +276 179 383 197 SOLUTION +664 187 795 210 (EINVOICE +802 188 908 209 DISPLAY +915 188 1032 213 VERSION) +465 240 507 264 Mã +512 241 554 264 của +560 241 626 266 CQT: +655 239 1186 266 PURIBUZANAMOIC99C9CTROINS +90 286 141 308 Đơn +147 285 172 312 vị +177 285 221 308 bán +226 285 285 313 hàng +293 286 387 312 (Seller): +395 279 495 307 CÔNG +504 280 549 307 TY +558 280 657 307 TNHH +664 279 844 307 CHUNGHO +852 279 937 307 VINA +945 279 1091 309 HEALTH +1100 280 1274 309 SOLUTION +90 326 130 348 Mã +135 322 165 349 số +170 322 223 349 thuế +229 327 281 351 (Tax +288 327 358 351 code): +363 324 383 347 0 +390 324 406 348 1 +411 321 605 350 08118215 +89 366 132 392 Địa +138 366 176 389 chỉ +182 368 298 393 (Address): +303 362 334 389 Số +340 367 373 392 11, +378 368 401 389 lô +407 368 477 392 N04A, +484 367 526 389 khu +530 367 560 389 đô +564 367 595 392 thị +599 367 643 390 mới +648 367 702 392 Dịch +707 368 773 393 Vọng, +779 367 863 394 phường +867 367 922 392 Dịch +927 368 993 394 Vọng, +999 368 1053 394 quận +1057 363 1103 390 Cầu +1108 364 1167 394 Giấy, +1173 368 1233 390 thành +1238 364 1281 394 phố +1285 367 1318 390 Hà +1323 368 1371 393 Nội, +1377 367 1424 393 Việt +1430 368 1483 390 Nam +89 406 146 431 Điện +152 407 211 432 thoại +218 407 279 431 (Tel): +286 407 331 428 024 +339 406 397 428 7300 +404 406 460 428 0891 +826 406 878 430 Fax: +89 440 121 467 Số +127 444 158 467 tài +165 445 236 466 khoản +245 447 348 470 (Account +355 446 413 472 No.): +421 445 616 469 700-010-446490 +622 445 653 471 tại +659 445 722 471 Ngân +728 445 792 472 Hàng +799 444 893 468 Shinhan +898 449 911 467 - +916 444 960 468 Chi +965 445 1036 468 nhánh +1043 445 1078 469 Hà +1084 444 1129 472 Nội +89 486 126 513 Họ +132 486 169 510 tên +174 486 244 513 người +251 493 302 509 mua +308 486 367 514 hàng +374 488 469 514 (Buyer): +90 529 136 550 Tên +142 529 188 551 đơn +193 529 219 555 vị +226 530 342 556 (Company +349 531 428 555 name): +435 524 518 552 CÔNG +525 529 562 552 TY +570 530 650 551 TNHH +657 529 796 552 SAMSUNG +803 529 856 552 SDS +862 527 930 556 VIỆT +937 529 1004 552 NAM +89 570 129 592 Mã +136 564 165 593 số +170 565 223 592 thuế +229 571 281 595 (Tax +287 571 358 596 code): +365 569 510 592 2300680991 +88 611 133 638 Địa +138 612 175 634 chỉ +182 613 297 638 (Address): +303 611 339 633 Lô +346 612 424 636 CN05, +432 611 514 639 đường +521 612 583 638 YP6, +589 612 645 634 Khu +652 611 713 640 công +719 612 803 641 nghiệp +810 611 862 635 Yên +869 612 956 640 Phong, +962 612 1000 635 Xã +1006 612 1057 635 Yên +1064 612 1152 640 Trung, +1159 611 1242 640 Huyện +1249 611 1300 635 Yên +1307 612 1394 640 Phong, +1402 612 1463 635 Tỉnh +1470 605 1518 636 Bắc +89 654 158 678 Ninh, +165 654 219 679 Việt +225 654 286 676 Nam +89 681 122 705 Số +127 684 157 705 tài +164 682 236 705 khoản +244 685 348 709 (Account +354 684 413 709 No.): +90 724 149 747 Hình +157 724 208 747 thức +216 724 282 746 thanh +289 724 341 747 toán +349 726 457 749 (Payment +464 726 563 748 method): +572 725 663 747 TM/CK +98 789 148 812 STT +163 770 212 793 Tên +218 770 281 797 hàng +287 770 341 796 hóa, +348 769 405 796 dịch +436 770 491 792 Đơn +498 770 525 798 vị +569 783 603 811 Số +610 789 684 817 lượng +747 789 802 811 Đơn +808 788 850 818 giá +979 788 1063 812 Thành +1070 782 1119 812 tiền +1214 765 1282 793 Thuế +1287 764 1344 793 suất +1393 764 1452 792 Tiền +1459 765 1518 792 thuế +94 825 153 850 (No.) +206 844 356 870 (Description) +266 813 299 836 vụ +448 845 515 868 (Unit) +454 808 507 830 tính +568 825 685 852 (Quantity) +733 826 793 848 (Unit +798 826 865 851 price) +993 824 1103 851 (Amount) +1223 807 1287 830 (VAT +1262 843 1295 869 %) +1290 809 1335 829 rate +1378 843 1542 869 (VAT Amount) +1417 807 1491 830 GTGT +116 891 128 913 1 +273 890 290 913 2 +472 890 488 912 3 +617 889 635 912 4 +790 889 807 914 5 +992 889 1103 916 6=4x5 +1270 889 1287 913 7 +1399 889 1510 914 8=6X7 +158 939 200 961 Phí +207 939 259 961 thuê +266 939 316 966 máy +323 938 361 965 lọc +159 977 219 998 nước +225 977 285 1002 nóng +292 976 345 1001 lạnh +161 1014 236 1040 Digital +244 1014 307 1035 CHP- +114 1032 127 1055 1 +159 1052 267 1073 3800ST1 +276 1052 312 1076 (từ +318 1051 377 1078 ngày +453 1032 507 1060 Máy +678 1035 697 1059 4 +795 1031 893 1057 800.000 +1074 1031 1195 1057 3,200,000 +1297 1031 1353 1057 10% +1452 1031 1551 1058 320,000 +158 1089 292 1111 01/02/2023 +301 1083 343 1110 đến +351 1084 388 1110 hết +159 1125 304 1151 28/02/2023) +159 1173 200 1195 Phí +207 1173 258 1195 thuê +265 1173 316 1200 máy +323 1173 361 1199 lọc +158 1213 218 1234 nước +225 1212 285 1238 nóng +292 1211 344 1237 lạnh +161 1249 236 1274 Digital +243 1248 307 1270 CHP- +112 1267 129 1291 2 +160 1287 267 1309 3800ST1 +276 1287 312 1312 (từ +318 1287 377 1313 ngày +454 1267 506 1293 Máy +664 1266 697 1292 20 +793 1265 896 1294 876,800 +1062 1265 1198 1297 17,536,000 +1430 1266 1552 1295 1,753,600 +159 1323 292 1346 29/01/2023 +301 1319 343 1345 đến +351 1319 388 1345 hết +160 1360 304 1387 28/02/2023) +160 1409 200 1431 Phí +207 1409 258 1431 thuê +265 1409 316 1436 máy +323 1408 361 1435 lọc +158 1447 218 1468 nước +225 1446 285 1473 nóng +292 1446 344 1472 lạnh +113 1503 128 1527 3 +160 1484 236 1511 Digital +243 1484 307 1505 CHP- +452 1503 506 1531 Máy +795 1502 897 1529 544,000 +1074 1502 1197 1529 2,176,000 +1450 1502 1550 1528 217,600 +160 1522 267 1544 3800ST1 +276 1522 312 1546 (từ +318 1522 377 1549 ngày +162 1559 292 1581 10/02/2023 +301 1555 343 1581 đến +351 1555 388 1581 hết +159 1596 304 1623 28/02/2023) +160 1645 200 1667 Phí +207 1644 259 1667 thuê +265 1645 316 1672 máy +324 1645 362 1671 lọc +159 1683 219 1706 nước +226 1683 286 1709 nóng +293 1683 345 1708 lạnh +160 1720 237 1746 Digital +245 1720 307 1742 CHP- +112 1738 129 1760 4 +159 1758 268 1780 3800ST1 +276 1758 313 1783 (từ +319 1758 377 1785 ngày +453 1737 508 1767 Máy +677 1737 699 1764 4 +795 1737 895 1764 256,000 +1077 1737 1197 1764 1,024,000 +1297 1737 1354 1762 10% +1453 1737 1552 1764 102,400 +158 1795 293 1817 20/02/2023 +301 1791 344 1817 đến +350 1791 388 1817 hết +158 1831 304 1859 28/02/2023) +93 2004 134 2027 Giá +139 2006 169 2031 trị +173 2007 221 2026 theo +226 2006 276 2026 mức +281 2002 331 2026 thuế +337 2005 412 2026 GTGT +676 1986 747 2008 Thành +752 1982 795 2008 tiền +800 1986 859 2008 trước +865 1983 915 2008 thuế +920 1986 992 2007 GTGT +1025 1983 1074 2008 Tiền +1079 1983 1127 2008 thuế +1132 1986 1201 2008 GTGT +1234 1985 1303 2008 Thành +1308 1982 1352 2008 tiền +1357 1985 1406 2012 gồm +1411 1982 1460 2008 thuế +1466 1985 1538 2008 GTGT +718 2023 813 2047 (Amount +819 2023 889 2048 before +895 2022 952 2048 VAT) +1035 2022 1096 2046 (VAT +1100 2023 1195 2048 Amount) +1248 2023 1345 2047 (Amount +1351 2023 1459 2049 including +1465 2022 1524 2048 VAT) +92 2071 132 2093 Giá +137 2072 162 2096 trị +166 2072 202 2092 với +207 2069 253 2093 thuế +259 2072 328 2092 GTGT +334 2071 371 2092 0% +378 2072 472 2094 (Amount +478 2074 502 2093 at +507 2071 545 2093 0% +552 2070 610 2097 VAT) +93 2119 132 2141 Giá +137 2120 162 2145 trị +167 2120 202 2141 với +207 2117 253 2141 thuế +259 2121 328 2140 GTGT +335 2120 371 2140 5% +378 2121 471 2141 (Amount +477 2122 502 2141 at +507 2119 546 2141 5% +552 2119 609 2145 VAT) +94 2167 132 2189 Giá +137 2168 162 2192 trị +166 2167 202 2189 với +207 2164 253 2189 thuế +259 2169 328 2188 GTGT +335 2168 372 2188 8% +379 2168 472 2189 (Amount +478 2171 502 2189 at +507 2168 545 2189 8% +552 2167 609 2193 VAT) +92 2216 132 2237 Giá +137 2217 162 2241 trị +167 2215 202 2236 với +207 2212 253 2237 thuế +259 2216 329 2236 GTGT +337 2216 384 2236 10% +391 2217 486 2237 (Amount +491 2219 516 2236 at +523 2216 572 2237 10% +579 2214 636 2240 VAT) +891 2213 1005 2239 23,936,000 +1111 2213 1215 2239 2,393,600 +1435 2213 1552 2240 26,329,600 +94 2263 170 2285 TỔNG +176 2263 252 2288 CỘNG +260 2264 361 2287 (GRAND +368 2263 461 2288 TOTAL) +874 2262 1007 2288 23,936,000 +1097 2262 1215 2288 2,393,600 +1416 2261 1549 2288 26,329,600 +87 2307 119 2333 Số +125 2307 171 2332 tiền +178 2304 223 2332 viết +230 2308 289 2337 bằng +295 2308 340 2332 chữ +347 2311 443 2332 (Amount +449 2310 473 2332 in +479 2309 564 2336 words): +571 2308 616 2331 Hai +621 2310 684 2331 mươi +690 2308 731 2331 sáu +736 2308 793 2336 triệu +797 2308 828 2331 ba +833 2309 888 2331 trăm +894 2308 932 2331 hai +938 2309 1001 2331 mươi +1007 2308 1059 2331 chín +1065 2308 1133 2336 nghìn +1139 2307 1180 2331 sáu +1186 2309 1241 2331 trăm +1247 2305 1308 2336 đồng diff --git a/cope2n-ai-fi/common/AnyKey_Value/utils/__init__.py b/cope2n-ai-fi/common/AnyKey_Value/utils/__init__.py new file mode 100755 index 0000000..12f320a --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/utils/__init__.py @@ -0,0 +1,127 @@ +import os +import torch +from omegaconf import OmegaConf +from omegaconf.dictconfig import DictConfig +from pytorch_lightning.callbacks import ModelCheckpoint +from pytorch_lightning.loggers.tensorboard import TensorBoardLogger +from pytorch_lightning.plugins import DDPPlugin +from utils.ema_callbacks import EMA + + +def _update_config(cfg): + cfg.save_weight_dir = os.path.join(cfg.workspace, "checkpoints") + cfg.tensorboard_dir = os.path.join(cfg.workspace, "tensorboard_logs") + + # set per-gpu batch size + num_devices = torch.cuda.device_count() + print('No. devices:', num_devices) + for mode in ["train", "val"]: + new_batch_size = cfg[mode].batch_size // num_devices + cfg[mode].batch_size = new_batch_size + +def _get_config_from_cli(): + cfg_cli = OmegaConf.from_cli() + cli_keys = list(cfg_cli.keys()) + for cli_key in cli_keys: + if "--" in cli_key: + cfg_cli[cli_key.replace("--", "")] = cfg_cli[cli_key] + del cfg_cli[cli_key] + + return cfg_cli + +def get_callbacks(cfg): + callback_list = [] + checkpoint_callback = ModelCheckpoint(dirpath=cfg.save_weight_dir, + filename='best_model', + save_last=True, + save_top_k=1, + save_weights_only=True, + verbose=True, + monitor='val_f1', mode='max') + checkpoint_callback.FILE_EXTENSION = ".pth" + checkpoint_callback.CHECKPOINT_NAME_LAST = "last_model" + callback_list.append(checkpoint_callback) + if cfg.callbacks.ema.decay != -1: + ema_callback = EMA(decay=0.9999) + callback_list.append(ema_callback) + return callback_list if len(callback_list) > 1 else checkpoint_callback + +def get_plugins(cfg): + plugins = [] + if cfg.train.strategy.type == "ddp": + plugins.append(DDPPlugin()) + + return plugins + +def get_loggers(cfg): + loggers = [] + + loggers.append( + TensorBoardLogger( + cfg.tensorboard_dir, name="", version="", default_hp_metric=False + ) + ) + + return loggers + +def cfg_to_hparams(cfg, hparam_dict, parent_str=""): + for key, val in cfg.items(): + if isinstance(val, DictConfig): + hparam_dict = cfg_to_hparams(val, hparam_dict, parent_str + key + "__") + else: + hparam_dict[parent_str + key] = str(val) + return hparam_dict + +def get_specific_pl_logger(pl_loggers, logger_type): + for pl_logger in pl_loggers: + if isinstance(pl_logger, logger_type): + return pl_logger + return None + +def get_class_names(dataset_root_path): + class_names_file = os.path.join(dataset_root_path[0], "class_names.txt") + class_names = ( + open(class_names_file, "r", encoding="utf-8").read().strip().split("\n") + ) + return class_names + +def create_exp_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + else: + print("DIR already existed.") + print('Experiment dir : {}'.format(save_dir)) + +def create_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + else: + print("DIR already existed.") + print('Save dir : {}'.format(save_dir)) + +def load_checkpoint(ckpt_path, model, key_include): + assert os.path.exists(ckpt_path) == True, f"Ckpt path at {ckpt_path} not exist!" + state_dict = torch.load(ckpt_path, 'cpu')['state_dict'] + for key in list(state_dict.keys()): + if f'.{key_include}.' not in key: + del state_dict[key] + else: + state_dict[key[4:].replace(key_include + '.', "")] = state_dict[key] # remove net.something. + del state_dict[key] + model.load_state_dict(state_dict, strict=True) + print(f"Load checkpoint at {ckpt_path}") + return model + +def load_model_weight(net, pretrained_model_file): + pretrained_model_state_dict = torch.load(pretrained_model_file, map_location="cpu")[ + "state_dict" + ] + new_state_dict = {} + for k, v in pretrained_model_state_dict.items(): + new_k = k + if new_k.startswith("net."): + new_k = new_k[len("net.") :] + new_state_dict[new_k] = v + net.load_state_dict(new_state_dict) + + diff --git a/cope2n-ai-fi/common/AnyKey_Value/utils/ema_callbacks.py b/cope2n-ai-fi/common/AnyKey_Value/utils/ema_callbacks.py new file mode 100755 index 0000000..956e0bf --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/utils/ema_callbacks.py @@ -0,0 +1,346 @@ +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import contextlib +import copy +import os +import threading +from typing import Any, Dict, Iterable + +import pytorch_lightning as pl +import torch +from pytorch_lightning import Callback +from pytorch_lightning.utilities.exceptions import MisconfigurationException +from pytorch_lightning.utilities.distributed import rank_zero_info + + +class EMA(Callback): + """ + Implements Exponential Moving Averaging (EMA). + + When training a model, this callback will maintain moving averages of the trained parameters. + When evaluating, we use the moving averages copy of the trained parameters. + When saving, we save an additional set of parameters with the prefix `ema`. + + Args: + decay: The exponential decay used when calculating the moving average. Has to be between 0-1. + validate_original_weights: Validate the original weights, as apposed to the EMA weights. + every_n_steps: Apply EMA every N steps. + cpu_offload: Offload weights to CPU. + """ + + def __init__( + self, decay: float, validate_original_weights: bool = False, every_n_steps: int = 1, cpu_offload: bool = False, + ): + if not (0 <= decay <= 1): + raise MisconfigurationException("EMA decay value must be between 0 and 1") + self.decay = decay + self.validate_original_weights = validate_original_weights + self.every_n_steps = every_n_steps + self.cpu_offload = cpu_offload + + def on_fit_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + device = pl_module.device if not self.cpu_offload else torch.device('cpu') + trainer.optimizers = [ + EMAOptimizer( + optim, + device=device, + decay=self.decay, + every_n_steps=self.every_n_steps, + current_step=trainer.global_step, + ) + for optim in trainer.optimizers + if not isinstance(optim, EMAOptimizer) + ] + + def on_validation_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def on_validation_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def on_test_start(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def on_test_end(self, trainer: "pl.Trainer", pl_module: "pl.LightningModule") -> None: + if self._should_validate_ema_weights(trainer): + self.swap_model_weights(trainer) + + def _should_validate_ema_weights(self, trainer: "pl.Trainer") -> bool: + return not self.validate_original_weights and self._ema_initialized(trainer) + + def _ema_initialized(self, trainer: "pl.Trainer") -> bool: + return any(isinstance(optimizer, EMAOptimizer) for optimizer in trainer.optimizers) + + def swap_model_weights(self, trainer: "pl.Trainer", saving_ema_model: bool = False): + for optimizer in trainer.optimizers: + assert isinstance(optimizer, EMAOptimizer) + optimizer.switch_main_parameter_weights(saving_ema_model) + + @contextlib.contextmanager + def save_ema_model(self, trainer: "pl.Trainer"): + """ + Saves an EMA copy of the model + EMA optimizer states for resume. + """ + self.swap_model_weights(trainer, saving_ema_model=True) + try: + yield + finally: + self.swap_model_weights(trainer, saving_ema_model=False) + + @contextlib.contextmanager + def save_original_optimizer_state(self, trainer: "pl.Trainer"): + for optimizer in trainer.optimizers: + assert isinstance(optimizer, EMAOptimizer) + optimizer.save_original_optimizer_state = True + try: + yield + finally: + for optimizer in trainer.optimizers: + optimizer.save_original_optimizer_state = False + + def on_load_checkpoint( + self, trainer: "pl.Trainer", pl_module: "pl.LightningModule", checkpoint: Dict[str, Any] + ) -> None: + checkpoint_callback = trainer.checkpoint_callback + + # use the connector as NeMo calls the connector directly in the exp_manager when restoring. + connector = trainer._checkpoint_connector + ckpt_path = connector.resume_checkpoint_path + + if ckpt_path and checkpoint_callback is not None and 'NeMo' in type(checkpoint_callback).__name__: + ext = checkpoint_callback.FILE_EXTENSION + if ckpt_path.endswith(f'-EMA{ext}'): + rank_zero_info( + "loading EMA based weights. " + "The callback will treat the loaded EMA weights as the main weights" + " and create a new EMA copy when training." + ) + return + ema_path = ckpt_path.replace(ext, f'-EMA{ext}') + if os.path.exists(ema_path): + ema_state_dict = torch.load(ema_path, map_location=torch.device('cpu')) + + checkpoint['optimizer_states'] = ema_state_dict['optimizer_states'] + del ema_state_dict + rank_zero_info("EMA state has been restored.") + else: + raise MisconfigurationException( + "Unable to find the associated EMA weights when re-loading, " + f"training will start with new EMA weights. Expected them to be at: {ema_path}", + ) + + +@torch.no_grad() +def ema_update(ema_model_tuple, current_model_tuple, decay): + torch._foreach_mul_(ema_model_tuple, decay) + torch._foreach_add_( + ema_model_tuple, current_model_tuple, alpha=(1.0 - decay), + ) + + +def run_ema_update_cpu(ema_model_tuple, current_model_tuple, decay, pre_sync_stream=None): + if pre_sync_stream is not None: + pre_sync_stream.synchronize() + + ema_update(ema_model_tuple, current_model_tuple, decay) + + +class EMAOptimizer(torch.optim.Optimizer): + r""" + EMAOptimizer is a wrapper for torch.optim.Optimizer that computes + Exponential Moving Average of parameters registered in the optimizer. + + EMA parameters are automatically updated after every step of the optimizer + with the following formula: + + ema_weight = decay * ema_weight + (1 - decay) * training_weight + + To access EMA parameters, use ``swap_ema_weights()`` context manager to + perform a temporary in-place swap of regular parameters with EMA + parameters. + + Notes: + - EMAOptimizer is not compatible with APEX AMP O2. + + Args: + optimizer (torch.optim.Optimizer): optimizer to wrap + device (torch.device): device for EMA parameters + decay (float): decay factor + + Returns: + returns an instance of torch.optim.Optimizer that computes EMA of + parameters + + Example: + model = Model().to(device) + opt = torch.optim.Adam(model.parameters()) + + opt = EMAOptimizer(opt, device, 0.9999) + + for epoch in range(epochs): + training_loop(model, opt) + + regular_eval_accuracy = evaluate(model) + + with opt.swap_ema_weights(): + ema_eval_accuracy = evaluate(model) + """ + + def __init__( + self, + optimizer: torch.optim.Optimizer, + device: torch.device, + decay: float = 0.9999, + every_n_steps: int = 1, + current_step: int = 0, + ): + self.optimizer = optimizer + self.decay = decay + self.device = device + self.current_step = current_step + self.every_n_steps = every_n_steps + self.save_original_optimizer_state = False + + self.first_iteration = True + self.rebuild_ema_params = True + self.stream = None + self.thread = None + + self.ema_params = () + self.in_saving_ema_model_context = False + + def all_parameters(self) -> Iterable[torch.Tensor]: + return (param for group in self.param_groups for param in group['params']) + + def step(self, closure=None, **kwargs): + self.join() + + if self.first_iteration: + if any(p.is_cuda for p in self.all_parameters()): + self.stream = torch.cuda.Stream() + + self.first_iteration = False + + if self.rebuild_ema_params: + opt_params = list(self.all_parameters()) + + self.ema_params += tuple( + copy.deepcopy(param.data.detach()).to(self.device) for param in opt_params[len(self.ema_params) :] + ) + self.rebuild_ema_params = False + + loss = self.optimizer.step(closure) + + if self._should_update_at_step(): + self.update() + self.current_step += 1 + return loss + + def _should_update_at_step(self) -> bool: + return self.current_step % self.every_n_steps == 0 + + @torch.no_grad() + def update(self): + if self.stream is not None: + self.stream.wait_stream(torch.cuda.current_stream()) + + with torch.cuda.stream(self.stream): + current_model_state = tuple( + param.data.to(self.device, non_blocking=True) for param in self.all_parameters() + ) + + if self.device.type == 'cuda': + ema_update(self.ema_params, current_model_state, self.decay) + + if self.device.type == 'cpu': + self.thread = threading.Thread( + target=run_ema_update_cpu, args=(self.ema_params, current_model_state, self.decay, self.stream,), + ) + self.thread.start() + + def swap_tensors(self, tensor1, tensor2): + tmp = torch.empty_like(tensor1) + tmp.copy_(tensor1) + tensor1.copy_(tensor2) + tensor2.copy_(tmp) + + def switch_main_parameter_weights(self, saving_ema_model: bool = False): + self.join() + self.in_saving_ema_model_context = saving_ema_model + for param, ema_param in zip(self.all_parameters(), self.ema_params): + self.swap_tensors(param.data, ema_param) + + @contextlib.contextmanager + def swap_ema_weights(self, enabled: bool = True): + r""" + A context manager to in-place swap regular parameters with EMA + parameters. + It swaps back to the original regular parameters on context manager + exit. + + Args: + enabled (bool): whether the swap should be performed + """ + + if enabled: + self.switch_main_parameter_weights() + try: + yield + finally: + if enabled: + self.switch_main_parameter_weights() + + def __getattr__(self, name): + return getattr(self.optimizer, name) + + def join(self): + if self.stream is not None: + self.stream.synchronize() + + if self.thread is not None: + self.thread.join() + + def state_dict(self): + self.join() + + if self.save_original_optimizer_state: + return self.optimizer.state_dict() + + # if we are in the context of saving an EMA model, the EMA weights are in the modules' actual weights + ema_params = self.ema_params if not self.in_saving_ema_model_context else list(self.all_parameters()) + state_dict = { + 'opt': self.optimizer.state_dict(), + 'ema': ema_params, + 'current_step': self.current_step, + 'decay': self.decay, + 'every_n_steps': self.every_n_steps, + } + return state_dict + + def load_state_dict(self, state_dict): + self.join() + + self.optimizer.load_state_dict(state_dict['opt']) + self.ema_params = tuple(param.to(self.device) for param in copy.deepcopy(state_dict['ema'])) + self.current_step = state_dict['current_step'] + self.decay = state_dict['decay'] + self.every_n_steps = state_dict['every_n_steps'] + self.rebuild_ema_params = False + + def add_param_group(self, param_group): + self.optimizer.add_param_group(param_group) + self.rebuild_ema_params = True \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/utils/kvu_dictionary.py b/cope2n-ai-fi/common/AnyKey_Value/utils/kvu_dictionary.py new file mode 100755 index 0000000..51e33b0 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/utils/kvu_dictionary.py @@ -0,0 +1,68 @@ + +DKVU2XML = { + "Ký hiệu mẫu hóa đơn": "form_no", + "Ký hiệu hóa đơn": "serial_no", + "Số hóa đơn": "invoice_no", + "Ngày, tháng, năm lập hóa đơn": "issue_date", + "Tên người bán": "seller_name", + "Mã số thuế người bán": "seller_tax_code", + "Thuế suất": "tax_rate", + "Thuế GTGT đủ điều kiện khấu trừ thuế": "VAT_input_amount", + "Mặt hàng": "item", + "Đơn vị tính": "unit", + "Số lượng": "quantity", + "Đơn giá": "unit_price", + "Doanh số mua chưa có thuế": "amount" +} + + +def ap_dictionary(header: bool): + header_dictionary = { + 'productname': ['description', 'paticulars', 'articledescription', 'descriptionofgood', 'itemdescription', 'product', 'productdescription', 'modelname', 'device', 'items', 'itemno'], + 'modelnumber': ['serialno', 'model', 'code', 'mcode', 'simimeiserial', 'serial', 'productcode', 'product', 'imeiccid', 'articles', 'article', 'articlenumber', 'articleidmaterialcode', 'transaction', 'itemcode'], + 'qty': ['quantity', 'invoicequantity'] + } + + key_dictionary = { + 'purchase_date': ['date', 'purchasedate', 'datetime', 'orderdate', 'orderdatetime', 'invoicedate', 'dateredeemed', 'issuedate', 'billingdocdate'], + 'retailername': ['retailer', 'retailername', 'ownedoperatedby'], + 'serial_number': ['serialnumber', 'serialno'], + 'imei_number': ['imeiesim', 'imeislot1', 'imeislot2', 'imei', 'imei1', 'imei2'] + } + + return header_dictionary if header else key_dictionary + + +def vat_dictionary(header: bool): + header_dictionary = { + 'Mặt hàng': ['tenhanghoa,dichvu', 'danhmuc,dichvu', 'dichvusudung', 'sanpham', 'tenquycachhanghoa','description', 'descriptionofgood', 'itemdescription'], + 'Đơn vị tính': ['dvt', 'donvitinh'], + 'Số lượng': ['soluong', 'sl','qty', 'quantity', 'invoicequantity'], + 'Đơn giá': ['dongia'], + 'Doanh số mua chưa có thuế': ['thanhtien', 'thanhtientruocthuegtgt', 'tienchuathue'], + # 'Số sản phẩm': ['serialno', 'model', 'mcode', 'simimeiserial', 'serial', 'sku', 'sn', 'productcode', 'product', 'particulars', 'imeiccid', 'articles', 'article', 'articleidmaterialcode', 'transaction', 'imei', 'articlenumber'] + } + + key_dictionary = { + 'Ký hiệu mẫu hóa đơn': ['mausoformno', 'mauso'], + 'Ký hiệu hóa đơn': ['kyhieuserialno', 'kyhieuserial', 'kyhieu'], + 'Số hóa đơn': ['soinvoiceno', 'invoiceno'], + 'Ngày, tháng, năm lập hóa đơn': [], + 'Tên người bán': ['donvibanseller', 'donvibanhangsalesunit', 'donvibanhangseller', 'kyboisignedby'], + 'Mã số thuế người bán': ['masothuetaxcode', 'maxsothuetaxcodenumber', 'masothue'], + 'Thuế suất': ['thuesuatgtgttaxrate', 'thuesuatgtgt'], + 'Thuế GTGT đủ điều kiện khấu trừ thuế': ['tienthuegtgtvatamount', 'tienthuegtgt'], + # 'Ghi chú': [], + # 'Ngày': ['ngayday', 'ngay', 'day'], + # 'Tháng': ['thangmonth', 'thang', 'month'], + # 'Năm': ['namyear', 'nam', 'year'] + } + + # exact_dictionary = { + # 'Số hóa đơn': ['sono', 'so'], + # 'Mã số thuế người bán': ['mst'], + # 'Tên người bán': ['kyboi'], + # 'Ngày, tháng, năm lập hóa đơn': ['kyngay', 'kyngaydate'] + # } + + return header_dictionary if header else key_dictionary \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/utils/run_ocr.py b/cope2n-ai-fi/common/AnyKey_Value/utils/run_ocr.py new file mode 100755 index 0000000..9976914 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/utils/run_ocr.py @@ -0,0 +1,33 @@ +import numpy as np +from pathlib import Path +from typing import Union, Tuple, List +import sys +# sys.path.append('/home/thucpd/thucpd/PV2-2023/common/AnyKey_Value/ocr-engine') +# from src.ocr import OcrEngine +sys.path.append('/home/thucpd/thucpd/git/PV2-2023/kie-invoice/components/prediction') # TODO: ?????? +import serve_model + + +# def load_ocr_engine() -> OcrEngine: +def load_ocr_engine() -> OcrEngine: + print("[INFO] Loading engine...") + # engine = OcrEngine() + engine = serve_model.engine + print("[INFO] Engine loaded") + return engine + +def process_img(img: Union[str, np.ndarray], save_dir_or_path: str, engine: OcrEngine, export_img: bool) -> None: + save_dir_or_path = Path(save_dir_or_path) + if isinstance(img, np.ndarray): + if save_dir_or_path.is_dir(): + raise ValueError("numpy array input require a save path, not a save dir") + page = engine(img) + save_path = str(save_dir_or_path.joinpath(Path(img).stem + ".txt") + ) if save_dir_or_path.is_dir() else str(save_dir_or_path) + page.write_to_file('word', save_path) + if export_img: + page.save_img(save_path.replace(".txt", ".jpg"), is_vnese=True, ) + +def read_img(img: Union[str, np.ndarray], engine: OcrEngine): + page = engine(img) + return ' '.join([f.text for f in page.llines]) \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/utils/split_docs.py b/cope2n-ai-fi/common/AnyKey_Value/utils/split_docs.py new file mode 100644 index 0000000..1b517d0 --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/utils/split_docs.py @@ -0,0 +1,101 @@ +import os +import glob +import json +from tqdm import tqdm + +def longestCommonSubsequence(text1: str, text2: str) -> int: + # https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis + dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)] + for i, c in enumerate(text1): + for j, d in enumerate(text2): + dp[i + 1][j + 1] = 1 + \ + dp[i][j] if c == d else max(dp[i][j + 1], dp[i + 1][j]) + return dp[-1][-1] + +def write_to_json(file_path, content): + with open(file_path, mode="w", encoding="utf8") as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +def check_label_exists(array, target_label): + for obj in array: + if obj["label"] == target_label: + return True # Label exists in the array + return False # Label does not exist in the array + +def merged_kvu_outputs(loutputs: list) -> dict: + compiled = [] + for output_model in loutputs: + for field in output_model: + if field['value'] != "" and not check_label_exists(compiled, field['label']): + element = { + 'label': field['label'], + 'value': field['value'], + } + compiled.append(element) + elif field['label'] == 'table' and check_label_exists(compiled, "table"): + for index, obj in enumerate(compiled): + if obj['label'] == 'table' and len(field['value']) > 0: + compiled[index]['value'].append(field['value']) + return compiled + + +def split_docs(doc_data: list, threshold: float=0.6) -> list: + num_pages = len(doc_data) + outputs = [] + kvu_content = [] + doc_data = sorted(doc_data, key=lambda x: int(x['page_number'])) + for data in doc_data: + page_id = int(data['page_number']) + doc_type = data['document_type'] + doc_class = data['document_class'] + fields = data['fields'] + if page_id == 0: + prev_title = doc_type + start_page_id = page_id + prev_class = doc_class + curr_title = doc_type if doc_type != "unknown" else prev_title + curr_class = doc_class if doc_class != "unknown" else "other" + kvu_content.append(fields) + similarity_score = longestCommonSubsequence(curr_title, prev_title) / len(prev_title) + if similarity_score < threshold: + end_page_id = page_id - 1 + outputs.append({ + "doc_type": f"({prev_class}) {prev_title}" if prev_class != "other" else prev_title, + "start_page": start_page_id, + "end_page": end_page_id, + "content": merged_kvu_outputs(kvu_content[:-1]) + }) + prev_title = curr_title + prev_class = curr_class + start_page_id = page_id + kvu_content = kvu_content[-1:] + if page_id == num_pages - 1: # end_page + outputs.append({ + "doc_type": f"({prev_class}) {prev_title}" if prev_class != "other" else prev_title, + "start_page": start_page_id, + "end_page": page_id, + "content": merged_kvu_outputs(kvu_content) + }) + elif page_id == num_pages - 1: # end_page + outputs.append({ + "doc_type": f"({prev_class}) {prev_title}" if prev_class != "other" else prev_title, + "start_page": start_page_id, + "end_page": page_id, + "content": merged_kvu_outputs(kvu_content) + }) + return outputs + + +if __name__ == "__main__": + threshold = 0.9 + json_path = "/home/sds/tuanlv/02-KVU/02-KVU_test/visualize/manulife_v2/json_outputs/HS_YCBT_No_IP_HMTD.json" + doc_data = read_json(json_path) + + outputs = split_docs(doc_data, threshold) + + write_to_json(os.path.join(os.path.dirname(json_path), "splited_doc.json"), outputs) \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/utils/utils.py b/cope2n-ai-fi/common/AnyKey_Value/utils/utils.py new file mode 100755 index 0000000..2e857ac --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/utils/utils.py @@ -0,0 +1,548 @@ +import os +import cv2 +import json +import torch +import glob +import re +import numpy as np +from tqdm import tqdm +from pdf2image import convert_from_path +from dicttoxml import dicttoxml +from word_preprocess import vat_standardizer, get_string, ap_standardizer, post_process_for_item +from utils.kvu_dictionary import vat_dictionary, ap_dictionary + + + +def create_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + else: + print("DIR already existed.") + print('Save dir : {}'.format(save_dir)) + +def pdf2image(pdf_dir, save_dir): + pdf_files = glob.glob(f'{pdf_dir}/*.pdf') + print('No. pdf files:', len(pdf_files)) + + for file in tqdm(pdf_files): + pages = convert_from_path(file, 500) + for i, page in enumerate(pages): + page.save(os.path.join(save_dir, os.path.basename(file).replace('.pdf', f'_{i}.jpg')), 'JPEG') + print('Done!!!') + +def xyxy2xywh(bbox): + return [ + float(bbox[0]), + float(bbox[1]), + float(bbox[2]) - float(bbox[0]), + float(bbox[3]) - float(bbox[1]), + ] + +def write_to_json(file_path, content): + with open(file_path, mode='w', encoding='utf8') as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, 'r') as f: + return json.load(f) + +def read_xml(file_path): + with open(file_path, 'r') as xml_file: + return xml_file.read() + +def write_to_xml(file_path, content): + with open(file_path, mode="w", encoding='utf8') as f: + f.write(content) + +def write_to_xml_from_dict(file_path, content): + xml = dicttoxml(content) + xml = content + xml_decode = xml.decode() + + with open(file_path, mode="w") as f: + f.write(xml_decode) + + +def load_ocr_result(ocr_path): + with open(ocr_path, 'r') as f: + lines = f.read().splitlines() + + preds = [] + for line in lines: + preds.append(line.split('\t')) + return preds + +def post_process_basic_ocr(lwords: list) -> list: + pp_lwords = [] + for word in lwords: + pp_lwords.append(word.replace("✪", " ")) + return pp_lwords + +def read_ocr_result_from_txt(file_path: str): + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + + boxes, words = [], [] + for line in lines: + if line == "": + continue + word_info = line.split("\t") + if len(word_info) == 6: + x1, y1, x2, y2, text, _ = word_info + elif len(word_info) == 5: + x1, y1, x2, y2, text = word_info + + x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int(float(x2)), int(float(y2)) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + +def get_colormap(): + return { + 'others': (0, 0, 255), # others: red + 'title': (0, 255, 255), # title: yellow + 'key': (255, 0, 0), # key: blue + 'value': (0, 255, 0), # value: green + 'header': (233, 197, 15), # header + 'group': (0, 128, 128), # group + 'relation': (0, 0, 255)# (128, 128, 128), # relation + } + + +def convert_image(image): + exif = image._getexif() + orientation = None + if exif is not None: + orientation = exif.get(0x0112) + + # Convert the PIL image to OpenCV format + image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + + # Rotate the image in OpenCV if necessary + if orientation == 3: + image = cv2.rotate(image, cv2.ROTATE_180) + elif orientation == 6: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + elif orientation == 8: + image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) + else: + image = np.asarray(image) + + if len(image.shape) == 2: + image = np.repeat(image[:, :, np.newaxis], 3, axis=2) + assert len(image.shape) == 3 + + return image, orientation + +def visualize(image, bbox, pr_class_words, pr_relations, color_map, labels=['others', 'title', 'key', 'value', 'header'], thickness=1): + image, orientation = convert_image(image) + + if orientation is not None and orientation == 6: + width, height, _ = image.shape + else: + height, width, _ = image.shape + + if len(pr_class_words) > 0: + id2label = {k: labels[k] for k in range(len(labels))} + for lb, groups in enumerate(pr_class_words): + if lb == 0: + continue + for group_id, group in enumerate(groups): + for i, word_id in enumerate(group): + x0, y0, x1, y1 = int(bbox[word_id][0]*width/1000), int(bbox[word_id][1]*height/1000), int(bbox[word_id][2]*width/1000), int(bbox[word_id][3]*height/1000) + cv2.rectangle(image, (x0, y0), (x1, y1), color=color_map[id2label[lb]], thickness=thickness) + + if i == 0: + x_center0, y_center0 = int((x0+x1)/2), int((y0+y1)/2) + else: + x_center1, y_center1 = int((x0+x1)/2), int((y0+y1)/2) + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['group'], thickness=thickness) + x_center0, y_center0 = x_center1, y_center1 + + if len(pr_relations) > 0: + for pair in pr_relations: + xyxy0 = int(bbox[pair[0]][0]*width/1000), int(bbox[pair[0]][1]*height/1000), int(bbox[pair[0]][2]*width/1000), int(bbox[pair[0]][3]*height/1000) + xyxy1 = int(bbox[pair[1]][0]*width/1000), int(bbox[pair[1]][1]*height/1000), int(bbox[pair[1]][2]*width/1000), int(bbox[pair[1]][3]*height/1000) + + x_center0, y_center0 = int((xyxy0[0] + xyxy0[2])/2), int((xyxy0[1] + xyxy0[3])/2) + x_center1, y_center1 = int((xyxy1[0] + xyxy1[2])/2), int((xyxy1[1] + xyxy1[3])/2) + + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['relation'], thickness=thickness) + + return image + + +def get_pairs(json: list, rel_from: str, rel_to: str) -> dict: + outputs = {} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] in (rel_from, rel_to): + is_rel[element['class']]['status'] = 1 + is_rel[element['class']]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + outputs[is_rel[rel_to]['value']['group_id']] = [is_rel[rel_from]['value']['group_id'], is_rel[rel_to]['value']['group_id']] + return outputs + +def get_table_relations(json: list, header_key_pairs: dict, rel_from="key", rel_to="value") -> dict: + list_keys = list(header_key_pairs.keys()) + relations = {k: [] for k in list_keys} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] == rel_from and element['group_id'] in list_keys: + is_rel[rel_from]['status'] = 1 + is_rel[rel_from]['value'] = element + if element['class'] == rel_to: + is_rel[rel_to]['status'] = 1 + is_rel[rel_to]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + relations[is_rel[rel_from]['value']['group_id']].append(is_rel[rel_to]['value']['group_id']) + return relations + +def get_key2values_relations(key_value_pairs: dict): + triple_linkings = {} + for value_group_id, key_value_pair in key_value_pairs.items(): + key_group_id = key_value_pair[0] + if key_group_id not in list(triple_linkings.keys()): + triple_linkings[key_group_id] = [] + triple_linkings[key_group_id].append(value_group_id) + return triple_linkings + + +def merged_token_to_wordgroup(class_words: list, lwords, labels) -> dict: + word_groups = {} + id2class = {i: labels[i] for i in range(len(labels))} + for class_id, lwgroups_in_class in enumerate(class_words): + for ltokens_in_wgroup in lwgroups_in_class: + group_id = ltokens_in_wgroup[0] + ltokens_to_ltexts = [lwords[token] for token in ltokens_in_wgroup] + text_string = get_string(ltokens_to_ltexts) + word_groups[group_id] = { + 'group_id': group_id, + 'text': text_string, + 'class': id2class[class_id], + 'tokens': ltokens_in_wgroup + } + return word_groups + +def verify_linking_id(word_groups: dict, linking_id: int) -> int: + if linking_id not in list(word_groups): + for wg_id, _word_group in word_groups.items(): + if linking_id in _word_group['tokens']: + return wg_id + return linking_id + +def matched_wordgroup_relations(word_groups:dict, lrelations: list) -> list: + outputs = [] + for pair in lrelations: + wg_from = verify_linking_id(word_groups, pair[0]) + wg_to = verify_linking_id(word_groups, pair[1]) + try: + outputs.append([word_groups[wg_from], word_groups[wg_to]]) + except Exception as e: + print('Not valid pair:', wg_from, wg_to) + return outputs + + +def export_kvu_outputs(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + word_groups = merged_token_to_wordgroup(class_words, lwords, labels) + linking_pairs = matched_wordgroup_relations(word_groups, lrelations) + + header_key = get_pairs(linking_pairs, rel_from='header', rel_to='key') # => {key_group_id: [header_group_id, key_group_id]} + header_value = get_pairs(linking_pairs, rel_from='header', rel_to='value') # => {value_group_id: [header_group_id, value_group_id]} + key_value = get_pairs(linking_pairs, rel_from='key', rel_to='value') # => {value_group_id: [key_group_id, value_group_id]} + + # table_relations = get_table_relations(linking_pairs, header_key) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + key2values_relations = get_key2values_relations(key_value) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + + triplet_pairs = [] + single_pairs = [] + table = [] + # print('key2values_relations', key2values_relations) + for key_group_id, list_value_group_ids in key2values_relations.items(): + if len(list_value_group_ids) == 0: continue + elif len(list_value_group_ids) == 1: + value_group_id = list_value_group_ids[0] + single_pairs.append({word_groups[key_group_id]['text']: { + 'text': word_groups[value_group_id]['text'], + 'id': value_group_id, + 'class': "value" + }}) + else: + item = [] + for value_group_id in list_value_group_ids: + if value_group_id not in header_value.keys(): + header_name_for_value = "non-header" + else: + header_group_id = header_value[value_group_id][0] + header_name_for_value = word_groups[header_group_id]['text'] + item.append({ + 'text': word_groups[value_group_id]['text'], + 'header': header_name_for_value, + 'id': value_group_id, + 'class': 'value' + }) + if key_group_id not in list(header_key.keys()): + triplet_pairs.append({ + word_groups[key_group_id]['text']: item + }) + else: + header_group_id = header_key[key_group_id][0] + header_name_for_key = word_groups[header_group_id]['text'] + item.append({ + 'text': word_groups[key_group_id]['text'], + 'header': header_name_for_key, + 'id': key_group_id, + 'class': 'key' + }) + table.append({key_group_id: item}) + + if len(table) > 0: + table = sorted(table, key=lambda x: list(x.keys())[0]) + table = [v for item in table for k, v in item.items()] + + outputs = {} + outputs['single'] = sorted(single_pairs, key=lambda x: int(float(list(x.values())[0]['id']))) + outputs['triplet'] = triplet_pairs + outputs['table'] = table + + file_path = os.path.join(os.path.dirname(file_path), 'kvu_results', os.path.basename(file_path)) + write_to_json(file_path, outputs) + return outputs + +# For FI-VAT project + +def get_vat_table_information(outputs): + table = [] + for single_item in outputs['table']: + item = {k: [] for k in list(vat_dictionary(header=True).keys())} + for cell in single_item: + header_name, score, proceessed_text = vat_standardizer(cell['header'], threshold=0.75, header=True) + if header_name in list(item.keys()): + # item[header_name] = value['text'] + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + + for header_name, value in item.items(): + if len(value) == 0: + if header_name in ("Số lượng", "Doanh số mua chưa có thuế"): + item[header_name] = '0' + else: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + item = post_process_for_item(item) + + if item["Mặt hàng"] == None: + continue + table.append(item) + return table + +def get_vat_information(outputs): + # VAT Information + single_pairs = {k: [] for k in list(vat_dictionary(header=False).keys())} + for pair in outputs['single']: + for raw_key_name, value in pair.items(): + key_name, score, proceessed_text = vat_standardizer(raw_key_name, threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'], + }) + + for triplet in outputs['triplet']: + for key, value_list in triplet.items(): + if len(value_list) == 1: + key_name, score, proceessed_text = vat_standardizer(key, threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value_list[0]['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value_list[0]['id'] + }) + + for pair in value_list: + key_name, score, proceessed_text = vat_standardizer(pair['header'], threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': pair['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'] + }) + + for table_row in outputs['table']: + for pair in table_row: + key_name, score, proceessed_text = vat_standardizer(pair['header'], threshold=0.8, header=False) + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': pair['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'] + }) + + return single_pairs + + +def post_process_vat_information(single_pairs): + vat_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if key_name in ("Ngày, tháng, năm lập hóa đơn"): + if len(list_potential_value) == 1: + vat_outputs[key_name] = list_potential_value[0]['content'] + else: + date_time = {'day': 'dd', 'month': 'mm', 'year': 'yyyy'} + for value in list_potential_value: + date_time[value['processed_key_name']] = re.sub("[^0-9]", "", value['content']) + vat_outputs[key_name] = f"{date_time['day']}/{date_time['month']}/{date_time['year']}" + else: + if len(list_potential_value) == 0: continue + if key_name in ("Mã số thuế người bán"): + selected_value = min(list_potential_value, key=lambda x: x['token_id']) # Get first tax code + # tax_code_raw = selected_value['content'].replace(' ', '') + tax_code_raw = selected_value['content'] + if len(tax_code_raw.replace(' ', '')) not in (10, 13): # to remove the first number dupicated + tax_code_raw = tax_code_raw.split(' ') + tax_code_raw = sorted(tax_code_raw, key=lambda x: len(x), reverse=True)[0] + vat_outputs[key_name] = tax_code_raw.replace(' ', '') + + else: + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + vat_outputs[key_name] = selected_value['content'] + return vat_outputs + + +def export_kvu_for_VAT_invoice(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + vat_outputs = {} + outputs = export_kvu_outputs(file_path, lwords, class_words, lrelations, labels) + + # List of items in table + table = get_vat_table_information(outputs) + + # VAT Information + single_pairs = get_vat_information(outputs) + vat_outputs = post_process_vat_information(single_pairs) + + # Combine VAT information and table + vat_outputs['table'] = table + + write_to_json(file_path, vat_outputs) + return vat_outputs + + +# For SBT project + +def get_ap_table_information(outputs): + table = [] + for single_item in outputs['table']: + item = {k: [] for k in list(ap_dictionary(header=True).keys())} + for cell in single_item: + header_name, score, proceessed_text = ap_standardizer(cell['header'], threshold=0.8, header=True) + # print(f"{key} ==> {proceessed_text} ==> {header_name} : {score} - {value['text']}") + if header_name in list(item.keys()): + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + table.append(item) + return table + +def get_ap_triplet_information(outputs): + triplet_pairs = [] + for single_item in outputs['triplet']: + item = {k: [] for k in list(ap_dictionary(header=True).keys())} + is_item_valid = 0 + for key_name, list_value in single_item.items(): + for value in list_value: + if value['header'] == "non-header": + continue + header_name, score, proceessed_text = ap_standardizer(value['header'], threshold=0.8, header=True) + if header_name in list(item.keys()): + is_item_valid = 1 + item[header_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + if is_item_valid == 1: + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + item['productname'] = key_name + # triplet_pairs.append({key_name: new_item}) + triplet_pairs.append(item) + return triplet_pairs + + +def get_ap_information(outputs): + single_pairs = {k: [] for k in list(ap_dictionary(header=False).keys())} + for pair in outputs['single']: + for key_name, value in pair.items(): + key_name, score, proceessed_text = ap_standardizer(key_name, threshold=0.8, header=False) + # print(f"{key} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + ap_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if len(list_potential_value) == 0: continue + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + ap_outputs[key_name] = selected_value['content'] + + return ap_outputs + +def export_kvu_for_SDSAP(file_path, lwords, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + outputs = export_kvu_outputs(file_path, lwords, class_words, lrelations, labels) + # List of items in table + table = get_ap_table_information(outputs) + triplet_pairs = get_ap_triplet_information(outputs) + table = table + triplet_pairs + + ap_outputs = get_ap_information(outputs) + + ap_outputs['table'] = table + # ap_outputs['triplet'] = triplet_pairs + + write_to_json(file_path, ap_outputs) \ No newline at end of file diff --git a/cope2n-ai-fi/common/AnyKey_Value/word_preprocess.py b/cope2n-ai-fi/common/AnyKey_Value/word_preprocess.py new file mode 100755 index 0000000..19273cf --- /dev/null +++ b/cope2n-ai-fi/common/AnyKey_Value/word_preprocess.py @@ -0,0 +1,224 @@ +import nltk +import re +import string +import copy +from utils.kvu_dictionary import vat_dictionary, ap_dictionary, DKVU2XML +nltk.download('words') +words = set(nltk.corpus.words.words()) + +s1 = u'ÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚÝàáâãèéêìíòóôõùúýĂăĐđĨĩŨũƠơƯưẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ' +s0 = u'AAAAEEEIIOOOOUUYaaaaeeeiioooouuyAaDdIiUuOoUuAaAaAaAaAaAaAaAaAaAaAaAaEeEeEeEeEeEeEeEeIiIiOoOoOoOoOoOoOoOoOoOoOoOoUuUuUuUuUuUuUuYyYyYyYy' + +# def clean_text(text): +# return re.sub(r"[^A-Za-z(),!?\'\`]", " ", text) + + +def get_string(lwords: list): + unique_list = [] + for item in lwords: + if item.isdigit() and len(item) == 1: + unique_list.append(item) + elif item not in unique_list: + unique_list.append(item) + return ' '.join(unique_list) + +def remove_english_words(text): + _word = [w.lower() for w in nltk.wordpunct_tokenize(text) if w.lower() not in words] + return ' '.join(_word) + +def remove_punctuation(text): + return text.translate(str.maketrans(" ", " ", string.punctuation)) + +def remove_accents(input_str, s0, s1): + s = '' + # print input_str.encode('utf-8') + for c in input_str: + if c in s1: + s += s0[s1.index(c)] + else: + s += c + return s + +def remove_spaces(text): + return text.replace(' ', '') + +def preprocessing(text: str): + # text = remove_english_words(text) if table else text + text = remove_punctuation(text) + text = remove_accents(text, s0, s1) + text = remove_spaces(text) + return text.lower() + + +def vat_standardize_outputs(vat_outputs: dict) -> dict: + outputs = {} + for key, value in vat_outputs.items(): + if key != "table": + outputs[DKVU2XML[key]] = value + else: + list_items = [] + for item in value: + list_items.append({ + DKVU2XML[item_key]: item_value for item_key, item_value in item.items() + }) + outputs['table'] = list_items + return outputs + + + +def vat_standardizer(text: str, threshold: float, header: bool): + dictionary = vat_dictionary(header) + processed_text = preprocessing(text) + + for candidates in [('ngayday', 'ngaydate', 'ngay', 'day'), ('thangmonth', 'thang', 'month'), ('namyear', 'nam', 'year')]: + if any([processed_text in txt for txt in candidates]): + processed_text = candidates[-1] + return "Ngày, tháng, năm lập hóa đơn", 5, processed_text + + _dictionary = copy.deepcopy(dictionary) + if not header: + exact_dictionary = { + 'Số hóa đơn': ['sono', 'so'], + 'Mã số thuế người bán': ['mst'], + 'Tên người bán': ['kyboi'], + 'Ngày, tháng, năm lập hóa đơn': ['kyngay', 'kyngaydate'] + } + for k, v in exact_dictionary.items(): + _dictionary[k] = dictionary[k] + exact_dictionary[k] + + for k, v in dictionary.items(): + # if k in ("Ngày, tháng, năm lập hóa đơn"): + # continue + # Prioritize match completely + if k in ('Tên người bán') and processed_text == "kyboi": + return k, 8, processed_text + + if any([processed_text == key for key in _dictionary[k]]): + return k, 10, processed_text + + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + if k in ("Ngày, tháng, năm lập hóa đơn"): + continue + + scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score > threshold else text, score, processed_text + +def ap_standardizer(text: str, threshold: float, header: bool): + dictionary = ap_dictionary(header) + processed_text = preprocessing(text) + + # Prioritize match completely + _dictionary = copy.deepcopy(dictionary) + if not header: + _dictionary['serial_number'] = dictionary['serial_number'] + ['sn'] + _dictionary['imei_number'] = dictionary['imei_number'] + ['imel'] + else: + _dictionary['modelnumber'] = dictionary['modelnumber'] + ['sku', 'sn', 'imei'] + _dictionary['qty'] = dictionary['qty'] + ['qty'] + + for k, v in dictionary.items(): + if any([processed_text == key for key in _dictionary[k]]): + return k, 10, processed_text + + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score >= threshold else text, score, processed_text + + +def convert_format_number(s: str) -> float: + s = s.replace(' ', '').replace('O', '0').replace('o', '0') + if s.endswith(",00") or s.endswith(".00"): + s = s[:-3] + if all([delimiter in s for delimiter in [',', '.']]): + s = s.replace('.', '').split(',') + remain_value = s[1].split('0')[0] + return int(s[0]) + int(remain_value) * 1 / (10**len(remain_value)) + else: + s = s.replace(',', '').replace('.', '') + return int(s) + + +def post_process_for_item(item: dict) -> dict: + check_keys = ['Số lượng', 'Đơn giá', 'Doanh số mua chưa có thuế'] + mis_key = [] + for key in check_keys: + if item[key] in (None, '0'): + mis_key.append(key) + if len(mis_key) == 1: + try: + if mis_key[0] == check_keys[0] and convert_format_number(item[check_keys[1]]) != 0: + item[mis_key[0]] = round(convert_format_number(item[check_keys[2]]) / convert_format_number(item[check_keys[1]])).__str__() + elif mis_key[0] == check_keys[1] and convert_format_number(item[check_keys[0]]) != 0: + item[mis_key[0]] = (convert_format_number(item[check_keys[2]]) / convert_format_number(item[check_keys[0]])).__str__() + elif mis_key[0] == check_keys[2]: + item[mis_key[0]] = (convert_format_number(item[check_keys[0]]) * convert_format_number(item[check_keys[1]])).__str__() + except Exception as e: + print("Cannot post process this item with error:", e) + return item + + +def longestCommonSubsequence(text1: str, text2: str) -> int: + # https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis + dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)] + for i, c in enumerate(text1): + for j, d in enumerate(text2): + dp[i + 1][j + 1] = 1 + \ + dp[i][j] if c == d else max(dp[i][j + 1], dp[i + 1][j]) + return dp[-1][-1] + + +def longest_common_subsequence_with_idx(X, Y): + """ + This implementation uses dynamic programming to calculate the length of the LCS, and uses a path array to keep track of the characters in the LCS. + The longest_common_subsequence function takes two strings as input, and returns a tuple with three values: + the length of the LCS, + the index of the first character of the LCS in the first string, + and the index of the last character of the LCS in the first string. + """ + m, n = len(X), len(Y) + L = [[0 for i in range(n + 1)] for j in range(m + 1)] + + # Following steps build L[m+1][n+1] in bottom up fashion. Note + # that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] + right_idx = 0 + max_lcs = 0 + for i in range(m + 1): + for j in range(n + 1): + if i == 0 or j == 0: + L[i][j] = 0 + elif X[i - 1] == Y[j - 1]: + L[i][j] = L[i - 1][j - 1] + 1 + if L[i][j] > max_lcs: + max_lcs = L[i][j] + right_idx = i + else: + L[i][j] = max(L[i - 1][j], L[i][j - 1]) + + # Create a string variable to store the lcs string + lcs = L[i][j] + # Start from the right-most-bottom-most corner and + # one by one store characters in lcs[] + i = m + j = n + # right_idx = 0 + while i > 0 and j > 0: + # If current character in X[] and Y are same, then + # current character is part of LCS + if X[i - 1] == Y[j - 1]: + + i -= 1 + j -= 1 + # If not same, then find the larger of two and + # go in the direction of larger value + elif L[i - 1][j] > L[i][j - 1]: + # right_idx = i if not right_idx else right_idx #the first change in L should be the right index of the lcs + i -= 1 + else: + j -= 1 + return lcs, i, max(i + lcs, right_idx) diff --git a/cope2n-ai-fi/common/crop_location.py b/cope2n-ai-fi/common/crop_location.py new file mode 100755 index 0000000..818803c --- /dev/null +++ b/cope2n-ai-fi/common/crop_location.py @@ -0,0 +1,172 @@ +from mmdet.apis import inference_detector, init_detector +import cv2 +import numpy as np +import urllib + +def get_center(box): + xmin, ymin, xmax, ymax = box + x_center = int((xmin + xmax) / 2) + y_center = int((ymin + ymax) / 2) + return [x_center, y_center] + + +def cal_euclidean_dist(p1, p2): + return np.linalg.norm(p1 - p2) + + +def bbox_to_four_poinst(bbox): + """convert one bouding box to 4 corner poinst + + Args: + bbox (_type_): _description_ + """ + xmin, ymin, xmax, ymax = bbox + + poinst = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]] + return poinst + + +def find_closest_point(src_point, point_list): + """ + + Args: + point (list): point format xy + point_list (list[list]): list of point xy + """ + + point_list = np.array(point_list) + dist_list = np.array( + cal_euclidean_dist(src_point, target_point) for target_point in point_list + ) + + index_closest_point = np.argmin(dist_list) + return index_closest_point + + +def crop_align_card(img_src, corner_box_list): + """Dewarp image based on four courners + + Args: + corner_list (list): four points of corners + """ + img = img_src.copy() + if isinstance(corner_box_list[0], list): + poinst = [get_center(box) for box in corner_box_list] + else: + # print(corner_box_list) + xmin, ymin, xmax, ymax = corner_box_list + poinst = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]] + + return dewarp(img, poinst) + + +def dewarp(image, poinst): + if isinstance(poinst, list): + poinst = np.array(poinst, dtype="float32") + (tl, tr, br, bl) = poinst + widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2)) + widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2)) + maxWidth = max(int(widthA), int(widthB)) + heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2)) + heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2)) + maxHeight = max(int(heightA), int(heightB)) + + dst = np.array( + [[0, 0], [maxWidth - 1, 0], [maxWidth - 1, maxHeight - 1], [0, maxHeight - 1]], + dtype="float32", + ) + + M = cv2.getPerspectiveTransform(poinst, dst) + warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight)) + return warped + + +class MdetPredictor: + def __init__(self, config: str, checkpoint: str, device: str = "cpu"): + self.model = init_detector(config, checkpoint, device=device) + self.class_names = self.model.CLASSES + + def infer(self, image, threshold=0.2): + bbox_result = inference_detector(self.model, image) + + bboxes = np.vstack(bbox_result) + labels = [ + np.full(bbox.shape[0], i, dtype=np.int32) + for i, bbox in enumerate(bbox_result) + ] + labels = np.concatenate(labels) + + res_bboxes = [] + res_labels = [] + for idx, box in enumerate(bboxes): + score = box[-1] + if score >= threshold: + label = labels[idx] + res_bboxes.append(box.tolist()[:4]) + res_labels.append(self.class_names[label]) + + return res_bboxes, res_labels + + +class ImageTransformer: + def __init__(self, config: str, checkpoint: str, device: str = "cpu"): + self.corner_detect_model = MdetPredictor(config, checkpoint, device) + + def __call__(self, image, threshold=0.2): + """ + + Args: + image (np.ndarray): BGR image + """ + corner_result = self.corner_detect_model.infer(image) + corners_dict = self.__extract_corners(corner_result) + card_image = self.__crop_image_based_on_corners(image, corners_dict) + + return card_image + + def __extract_corners(self, corner_result): + + bboxes, labels = corner_result + # convert bbox to int + bboxes = [[int(x) for x in box] for box in bboxes] + output = {k: bboxes[labels.index(k)] for k in labels} + # print(output) + return output + + def __crop_image_based_on_corners(self, image, corners_dict): + """ + + Args: + corners_dict (_type_): _description_ + """ + if "card" in corners_dict.keys(): + if len(corners_dict.keys()) == 5: + points = [ + corners_dict["top_left"], + corners_dict["top_right"], + corners_dict["bottom_right"], + corners_dict["bottom_left"], + ] + else: + points = corners_dict["card"] + card_image = crop_align_card(image, points) + else: + card_image = None + + return card_image + + +def crop_location(image_url): + transform_module = ImageTransformer( + config="./models/Kie_AHung/yolox_s_8x8_300e_idcard5_coco.py", + checkpoint="./models/Kie_AHung/best_bbox_mAP_epoch_100.pth", + device="cuda:0", + ) + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + card_image = transform_module(img) + if card_image is not None: + return card_image + else: + return img diff --git a/cope2n-ai-fi/common/dates_gplx.json b/cope2n-ai-fi/common/dates_gplx.json new file mode 100755 index 0000000..b39df86 --- /dev/null +++ b/cope2n-ai-fi/common/dates_gplx.json @@ -0,0 +1,622 @@ +{ + "20221027_154840.json": { + "label": "ngày /date 05 tháng /month 04 năm/year 2016", + "pred": "ngày date 05 tháng month 04 năm/year 2016" + }, + "7ba0f6b2f2ff34a16dee18.json": { + "label": "ngày /date 01 tháng /month 04 năm /year 2022", + "pred": "ngày date 01 tháng Amount 04 năm year 2022" + }, + "20221027_155646.json": { + "label": "ngày /date 01 tháng /month 04 năm/year✪2022", + "pred": "ngày date or tháng month 04 năm/year2022" + }, + "799c679b63d6a588fcc717.json": { + "label": "ngày /date 30 tháng /month 07 năm/year 2015", + "pred": "ngày lone 30 thể 2 work 07 ndmyvar 2015" + }, + "150094748_1728953773944481_6269983404281027305_n.json": { + "label": "ngày/da e 15✪tháng # #th 04 năm /year 2020", + "pred": "ngày/da - 15tháng % with 04 năm (year 2020" + }, + "20221027_155711.json": { + "label": "ngày /date 16 tháng /month 06 năm /year 2020", + "pred": "ngày date 16 tháng month 06 năm 'year 2020" + }, + "20221027_155638.json": { + "label": "ngày /date 05 tháng /month 04 năm/year✪2016", + "pred": "ngày /date 05 tháng month 04 năm/year2016" + }, + "20221027_155754.json": { + "label": "ngày/date 19 tháng /month 03 năm/year 2018", + "pred": "ngày/date 19 tháng month 03 năm/year 2018" + }, + "201417393_4052045311588809_501501345369021923_n.json": { + "label": "ngày /date 27 tháng /month 07 năm/year 2020", + "pred": "ngày date 27 tháng month 07 năm/year 2020" + }, + "c50de8e0e2ad24f37dbc28.json": { + "label": "ngày /date 24 tháng /month 05 năm/year 2016", + "pred": "ngày date 24 tháng month 05 năm/year 2016" + }, + "178033599_360318769019204_7688552975060615249_n.json": { + "label": "ngày /date 13 tháng month 08 năm /year 2020", + "pred": "ngày date 73 tháng month 08 năm year 2020" + }, + "20221027_154755.json": { + "label": "ngày /date 22 tháng /month 04 năm/year 2022", + "pred": "ngày date 22 tháng month 04 năm/year 2022" + }, + "20221027_154701.json": { + "label": "ngày/date 27 tháng /month 10 năm/year 2014", + "pred": "ngày/date 27 tháng month 10 năm/year 2014" + }, + "20221027_154617.json": { + "label": "ngày/date 10 tháng /month 03 năm/year 2022", + "pred": "ngày/date 10 than Wmonth 03 năm/year 2022" + }, + "20221027_155429.json": { + "label": "ngày /date 29 tháng /month 10 năm/year 2020", + "pred": "ngày date 29 tháng month 10 năm/year 2020" + }, + "38949066_1826046370807755_4672633932229902336_n.json": { + "label": "ngày /date 03 tháng /month 07 năm /year 2017", + "pred": "ngày date 03 tháng month 0Z năm year 2017" + }, + "174353602_3093780914182491_6316781286680210887_n.json": { + "label": "ngày /date 09 tháng /month 09 năm/year 2019", + "pred": "ngày date 09 tháng month 09 năm/voce 2019" + }, + "135575662_779633739578177_65454671165731184_n.json": { + "label": "ngày /date 02 tháng /month 10 năm /year 2016", + "pred": "ngày 'date 07 tháng month 10 năm Sear 2016" + }, + "198291731_4210067705720978_7154894655460708366_n.json": { + "label": "ngày /date 05 tháng month 05 năm/year 2014", + "pred": "ngày date 05 tháng month 05 ndmivar 2014" + }, + "20221027_155325.json": { + "label": "ngày /date 09 tháng /month 07 năm /year 2019", + "pred": "ngày date 09 tháng month 07 năm 'year 2019" + }, + "20221027_155526.json": { + "label": "ngày/date 14 tháng /month 01 năm/year✪2019", + "pred": "ngày/date 14 tháng month 01 năm/year2019" + }, + "20221027_155759.json": { + "label": "ngày/date 24 tháng /month 05 năm/year 2016", + "pred": "ngày/date 24 tháng month 05 năm/year 2016" + }, + "f40789388d754b2b126416.json": { + "label": "ngày /date 10 tháng/month 09 năm /year 2020", + "pred": "ngày /date 10thing month 09 năm year 2020" + }, + "20221027_155453.json": { + "label": "ngày /date 27 tháng /month 10 năm/year 2014", + "pred": "ngày date 27 tháng month 10 năm/year 2014" + }, + "88bb0caa03e7c5b99cf64.json": { + "label": "ngày /date 16 tháng /month 06 năm /year 2020", + "pred": "ngày dg h 6AR ag M ath 06 năm hear 2020" + }, + "20221027_154408.json": { + "label": "ngày /date 28 tháng /month 05 năm /year 2019", + "pred": "ngày date 28 tháng month 05 năm year 2019" + }, + "200064855_1976213642526776_4676665588314498194_n.json": { + "label": "ngày /date 04 tháng /month 01 năm /year 2018", + "pred": "ngày date 04 tháng /month 01 năm year 2018" + }, + "61c259385775912bc86410.json": { + "label": "ngày /date 20 tháng/month 09 năm/year 2017", + "pred": "ngày /de l 20 th and 4 onth 09 năm/year 2017" + }, + "20221027_155630.json": { + "label": "ngày /date 10 tháng /month 09 năm /year 2020", + "pred": "ngày date 10 tháng month 09 năm year 2020" + }, + "20221027_155342.json": { + "label": "ngày/date 19 tháng /month 03 năm/year 2018", + "pred": "ngày/date 19 tháng month 03 năm/year 2018" + }, + "165824171_1804443136392377_8891768953420682785_n.json": { + "label": "ngày /date 03 tháng month 04 năm /year 2018", + "pred": "ngày date 03 tháng month 04 năm year 2018" + }, + "107005164_1003706713393058_3039921363490738151_n.json": { + "label": "ngày /date 1 8 tháng month 11 năm /year 2019", + "pred": "ngày 'date I 8 tháng month 11 năm vear 2019" + }, + "20221027_155658.json": { + "label": "ngày/date 14 tháng /month 01 năm/year 2019", + "pred": "ngày/date 14 tháng month 01 năm/year 2019" + }, + "20221027_154654.json": { + "label": "ngày /date 08 tháng /month 12 năm/year 2015", + "pred": "ngày date 08 tháng month 12 năm/year 2015" + }, + "f2badabfd1f217ac4ee327.json": { + "label": "ngày /date 19 tháng /month 03 năm/year 2018", + "pred": "ngày /date 19 their almonth 03 năm/year 2018" + }, + "20221027_155501.json": { + "label": "ngày/date 11 tháng /month 06 năm/year 2014", + "pred": "ngày/date 1 l tháng month 06 năm/year. 2014" + }, + "74179950_806804509749947_7322741604127604736_n.json": { + "label": "ngày /date 11 tháng /month 06 năm /year 2019", + "pred": "ngày date 11 tháng month 06 năm \\/gar 2019" + }, + "197892118_1197184050744529_3186157591590303981_n.json": { + "label": "ngày /date 15 tháng /month 05 năm year 2015", + "pred": "ngày date IS tháng month 05 năm year 2015" + }, + "8265ab8ba0c666983fd722.json": { + "label": "ngày /date 08 tháng /month 12 năm /year 2015", + "pred": "ngày /date 08 tháng month 12 năm year 2015" + }, + "185421881_360318465685901_6968676669094190049_n.json": { + "label": "ngày /date 13 tháng month 08 năm /year 2020", + "pred": "ngày date 73 tháng month 08 năm year 2020" + }, + "20221027_155142.json": { + "label": "ngày/date 30 tháng/month 07 năm/year 2015", + "pred": "ngày/date 30 tháng/month 07 năm/year 2015" + }, + "39441751_326131871285503_8401816317220356096_n.json": { + "label": "ngày /date 03 tháng /month 08 năm/year✪2017", + "pred": "ngày 'date 03 tháng month 08 năm✪year✪2017" + }, + "20221027_154912.json": { + "label": "ngày/date 30 tháng /month 07 năm/year 2015", + "pred": "ngày/date 30 tháng month 07 năm/year 2015" + }, + "199235658_1994072707411372_221206969024509405_n.json": { + "label": "ngày /date 05 tháng month 06 năm year 2020", + "pred": "ngày date 05 tháng month 06 năm year 2020" + }, + "168434217_1698404580364474_5439600436729489777_n.json": { + "label": "ngày /date 05 tháng /month 11 năm/year 2020", + "pred": "ngày date 05 tháng month 11 ndmhear 2020" + }, + "20221027_154805.json": { + "label": "ngày /date 14 tháng /month 01 năm/year 2019", + "pred": "ngày date 14 tháng month 01 năm/year 2019" + }, + "e043761b7256b408ed4712.json": { + "label": "ngày /date 27 tháng /month 10 năm/year 2014", + "pred": "ngày date 27 tháng month 10 năm/year 2014" + }, + "20221027_155401.json": { + "label": "ngày /date 29 tháng /month 10 năm/year✪2013", + "pred": "ngày date 29 tháng /uponth 10 năm/year2013" + }, + "20221027_155316.json": { + "label": "ngày/date 20 tháng /month 09 năm/year 2017", + "pred": "ngày/date 20 tháng month 09 năm/year 2017" + }, + "193926583_1626674417674644_309549447428666454_n.json": { + "label": "ngày /date 29 tháng /month 07 năm/year 2020", + "pred": "ngày (date 29 tháng /month 07 năm✪year 2020" + }, + "20221027_154823.json": { + "label": "ngày /date 01 tháng /month 04 năm /year 2022", + "pred": "ngày date or tháng month 04 năm year 2022" + }, + "138942425_242694630705291_5683978028617807264_n.json": { + "label": "ngày /date 30 tháng/month 01 năm/year✪2013", + "pred": "ngày date 30 tháng/month 01 năm/year2013" + }, + "20221027_155334.json": { + "label": "ngày /date 24 tháng /month 05 năm/year 2016", + "pred": "ngày date 24 tháng month 05 năm/year 2016" + }, + "187421917_1668044206721318_779901369147309116_n.json": { + "label": "ngày date 04 # # 05 # 2022", + "pred": "ngày date 4 them Please 05 advise 2021" + }, + "20221027_154716.json": { + "label": "ngày /date 11 tháng /month 06 năm/year 2014", + "pred": "ngày date 11 tháng month 06 năm/year 2014" + }, + "5b1116c61d8bdbd5829a23.json": { + "label": "ngày /date 29 tháng /month 10 năm/year 2020", + "pred": "ngày date 29 tháng /month 10 năm/year 2020" + }, + "20221027_154850.json": { + "label": "ngày /date 10 tháng /month 09 năm /year 2020", + "pred": "ngày date 10 tháng month 09 năm year 2020" + }, + "1489864e88034e5d17122.json": { + "label": "ngày /date 08 tháng /month 12 năm/year 2015", + "pred": "ngày date 08 hàng month 12 năm/year 2015" + }, + "199990418_1443812262638998_8173300652488821384_n.json": { + "label": "ngày/date 10✪tháng /month 03 năm/year 2021", + "pred": "ngày/date 10.50m Noun th 03 ndm/year 2021" + }, + "139073668_833869180522062_7998364448555134241_n.json": { + "label": "ngày/date 26 tháng /month 07 năm/year 2017", + "pred": "ngày/date 26 tháng /month 07 năm/year2 2017" + }, + "20221027_154528.json": { + "label": "ngày date 19 tháng /month 03 năm/year 2018", + "pred": "ngày date 19 tháng month 03 năm/year 2018" + }, + "20221027_154423.json": { + "label": "ngày/date 20 tháng /month 09 năm/year 2017", + "pred": "ngày/date 20 tháng month 09 năm/yew 2017" + }, + "20221027_154722.json": { + "label": "ngày /date 11 tháng /month 06 năm/year 2014", + "pred": "ngày date 1 I tháng month 06 năm/year 2014" + }, + "144003628_4199201026774245_6202264670231940239_n.json": { + "label": "ngày date 07 tháng month 03 ######## ### #", + "pred": "ngày date0 thôn Normmm 05 adortear 7" + }, + "20221027_154434.json": { + "label": "ngày /date 09 tháng /month 07 năm /year 2019", + "pred": "ngày date 09 tháng month 07 năm 'year 2019" + }, + "20221027_154630.json": { + "label": "ngày/date 29 tháng /month 10 năm/year 2020", + "pred": "ngày/date 29 tháng month 10 năm/year 2020" + }, + "20221027_155552.json": { + "label": "ngày /date 10 tháng /month 09 năm /year 2020", + "pred": "ngày date 10 tháng month 09 năm 'year 2020" + }, + "131114177_1027132027767399_411142190418396877_n.json": { + "label": "ngày /date 17 tháng /month 01 năm/year✪2018", + "pred": "ngày date 17 tháng /month 01 năm/year2018" + }, + "164703297_455738728964651_5260332814645460915_n.json": { + "label": "ngày date 03 tháng month 11 năm/year 2020", + "pred": "ngày date 03 tháng month I I năm/year 2020" + }, + "20221027_155926.json": { + "label": "ngày/date 20 tháng /month 09 năm /year 2017", + "pred": "ngày/date 20 tháng month 09 năm year 2017" + }, + "20221027_154832.json": { + "label": "ngày /date 05 tháng /month 04 năm/year 2016", + "pred": "ngày date 05 tháng month 04 năm/year 2016 6" + }, + "dd09b9b6b2fb74a52dea24.json": { + "label": "ngày /date 16 tháng /month 06 năm /year 2020", + "pred": "123) d 2010 Way Yount 06 năm year 2020" + }, + "20221027_154646.json": { + "label": "ngày /date 08 tháng /month 12 năm/year 2015", + "pred": "ngày date 08 2 tháng month 12 năm/year2 2015" + }, + "180534342_1213803569050037_4381710158357942629_n.json": { + "label": "ngày /date 20 tháng /month 10 năm/year 21", + "pred": "ngày date 20 tháng month 10 năm/year 2" + }, + "20221027_155443.json": { + "label": "ngày /date 08 tháng /month 12 năm/year 2015", + "pred": "ngày date 08 tháng month 12 năm/year 2015" + }, + "25a9717c7b31bd6fe42029.json": { + "label": "ngày /date 09 tháng /month 07 năm /year 2019", + "pred": "ngày date 09 tháng month 07 năm hear 2019" + }, + "a0a8281f2652e00cb9435.json": { + "label": "ngày/date 10 tháng /month 03 năm/year 2022", + "pred": "ngày/date 10 that abroath 03 năm/year 2022" + }, + "48793dfd37b0f1eea8a130.json": { + "label": "tháng/month 09 năm/year\n ngày/date 20 tháng/month\n 163", + "pred": "ngày/date 20 chăn knowin năm/ya\n 163" + }, + "20221027_154730.json": { + "label": "ngày /date 16 tháng /month 06 năm /year 2020", + "pred": "ngày date to tháng month 06 năm year 2020" + }, + "174102242_893537741194123_1381062036549019974_n.json": { + "label": "ngày /date 11 tháng /month 11 năm/year 2019", + "pred": "ngày date 17 tháng Thuonth 11 năm/year 2019" + }, + "20221027_154541.json": { + "label": "ngày /date 29 tháng /month 10 năm/year✪2013", + "pred": "ngày date 29 tháng month 10 năm/year2013" + }, + "20221027_155939.json": { + "label": "ngày /date 28 tháng /month 05 năm /year 2019", + "pred": "ngày date 28 tháng month 05 năm 'year 2019" + }, + "20221027_154452.json": { + "label": "ngày /date 24 tháng /month 05 năm/year 2016", + "pred": "ngày date 24 tháng month 05 năm/year 2016" + }, + "104353445_990772771353119_6131582365614146594_n.json": { + "label": "ngày date 18 tháng /month 03 năm /year 2019", + "pred": "ngày date If tháng month 03 năm year 2019" + }, + "20221027_155418.json": { + "label": "ngày/date 10 tháng/month 03 năm/year 2022", + "pred": "ngày/date 10than dmonth 03 năm/year 2022" + }, + "20221027_155916.json": { + "label": "ngày /date 09 tháng /month 07 năm /year 2019", + "pred": "ngày date 09 tháng month 07 năm 'year 2019" + }, + "195887607_545276056640128_7265052621888807786_n.json": { + "label": "ngày /date 12 tháng /month 01 năm /year 2017", + "pred": "ngày /dote 12 tháng month 01 năm hear 201 7" + }, + "168303942_358282189193092_4968412916165104911_n.json": { + "label": "ngày/date 19 tháng month 03 năm year 2015", + "pred": "ngày/date 19 tháng month 03 năm your 1019" + }, + "6a70d15bd51613484a0713.json": { + "label": "ngày /date 22 tháng /month 04 năm /year 2022", + "pred": "ngày date n háu Z with 04 năm year 2022" + }, + "20221027_155302.json": { + "label": "ngày /date 28 tháng /month 05 năm /year 2019", + "pred": "ngày date 28 tháng month 05 năm 'year 2019" + }, + "20221027_154815.json": { + "label": "ngày /date 01 tháng /month 04 năm/year 2022", + "pred": "ngày date or tháng month 04 năm/year 2022" + }, + "c87b81298e64483a11757.json": { + "label": "ngày /date 19 tháng /month 03 năm/year 201", + "pred": "ngày date 19 thớ almonth 03 năm/year 201" + }, + "20221027_155620.json": { + "label": "ngày/date 30 tháng /month 07 năm/year 2015", + "pred": "ngày/date 30 tháng month 07 năm/year 2015" + }, + "20221027_155511.json": { + "label": "ngày /date 16 tháng /month 06 năm /year 2020", + "pred": "ngày date to tháng month 06 năm 'year 2020" + }, + "745c5d6b52269478cd379.json": { + "label": "ngày /date 09 tháng /month 07 năm /year 2019", + "pred": "ngày date 09 thân g worth 07 năm hear 2019" + }, + "9329511f5552930cca4315.json": { + "label": "ngày/date 11 tháng /month 06 năm/year 2014", + "pred": "ngày/date 11 tháng month 06 năm/year 2014" + }, + "158882925_262850065433814_5526034984745996835_n.json": { + "label": "ngày /date 16 tháng month 12 năm/year 2015", + "pred": "ngày date 16 tháng month 12 năm/year 2015" + }, + "140687263_3755683421155059_7637736837539526203_n.json": { + "label": "năm/year 2017\n ngày /date # # # # 08 năm/year", + "pred": "năm 2017\n ngày 'date 422 ins 1.1 ma 08 năm" + }, + "20221027_155717.json": { + "label": "ngày/date 11 tháng /month 06 năm/year 2014", + "pred": "ngày/date 11 tháng month 06 năm/year 2014" + }, + "148919455_2877898732481724_2579276238538203411_n.json": { + "label": "ngày /date 05 tháng /month 09 năm/year✪2018", + "pred": "ngày date 05 thán g /month 09 năm/year2018" + }, + "c8b0dc9cd8d11e8f47c014.json": { + "label": "ngày /date 05 tháng /month 04 năm/year 2016", + "pred": "ngày /date 0.5 tháng g/month 04 năm/year 2016" + }, + "175913976_2827333254262221_2873818403698028020_n.json": { + "label": "ngày date 05 tháng month 01 năm year 2018", + "pred": "ngày dan us thông month 01 năm year 2018" + }, + "20221027_155029.json": { + "label": "ngày/date 30 tháng/month\n 07 năm/year 2015", + "pred": "ngày/date\n năm/year 2015" + }, + "196165776_1160925321042008_58817602967276351_n.json": { + "label": "ngày date 01 tháng month 09 năm year 2020", + "pred": "ngày date or tháng month 09 năm year 2020" + }, + "162820484_451115505943597_8326385834717580925_n.json": { + "label": "ngày /date 27 tháng /month 08 thường 2014", + "pred": "ngày/ date 27 tháng furonth 08 năm/year 2014" + }, + "41594446_1316256461838413_661251515624718336_n.json": { + "label": "ngày /date 21 tháng /month 06 năm/year✪2017", + "pred": "ngày /date 27 tháng month 06 năm/year201 7" + }, + "20221027_155728.json": { + "label": "ngày /date 08 tháng /month 12 năm/year 2015", + "pred": "ngày date 08 tháng month 12 năm/year 2015" + }, + "142090201_2826170774286852_1233962294093312865_n.json": { + "label": "ngày /date 29 tháng month 07 năm /year 2019", + "pred": "ngày date 29 tháng month 07 năm year 2019" + }, + "0dd2fe8bf5c633986ad726.json": { + "label": "ngày /date 29 tháng /month 10 năm/year✪2013", + "pred": "march date 29 tháng month 10✪năm✪volur✪2013" + }, + "190919701_1913643422140446_6855763478065892825_n.json": { + "label": "ngày date 24 tháng /month 07 năm/year 2017", + "pred": "ngày /date 24 tháng month 07 năm/year 2017" + }, + "147585615_2757487791230682_5515346433540820516_n.json": { + "label": "ngày /date # 9✪tháng /month 05 năm /year 2019", + "pred": "ngày date 2 Danate month 05 năm year 2019" + }, + "5fcae09ee4d3228d7bc211.json": { + "label": "ngày /date 14 tháng /month 01 năm/year 2019", + "pred": "ngày 'date 14 tháng month 0.1 năm/year 2019" + }, + "20221027_154417.json": { + "label": "ngày/date 20 tháng /month 09 năm/year 2017", + "pred": "ngày/date 20 thân ghi onth 09 năm/year 2017" + }, + "20221027_154802.json": { + "label": "ngày /date 14 tháng /month 01 năm/year 2019", + "pred": "ngày date 14 tháng month 01 năm/year 2019" + }, + "20221027_154749.json": { + "label": "ngày /date 22 tháng /month 04 năm /year 2022", + "pred": "ngày date 22 tháng month 04 năm year 2022" + }, + "20221027_154900.json": { + "label": "ngày /date 10 tháng /month 09 năm /year 2020", + "pred": "ngày date 10 tháng month 09 năm Year 2020" + }, + "4b2b35453e08f856a11925.json": { + "label": "ngày/date 10 tháng/month 03 năm/year 2022", + "pred": "ngày/date 1 in 03 năm/year 2022" + }, + "20221027_155734.json": { + "label": "ngày /date 29 tháng /month 10 năm/year 2020", + "pred": "ngày date 29 tháng month 10 năm/year 2020" + }, + "198876248_2931797967062357_4287721016641237281_n.json": { + "label": "ngày /date 29 áng /month 10 năm /year 2019", + "pred": "ngày Warm 204 đón Quanth 10 năm vear 2019" + }, + "20221027_154613.json": { + "label": "ngày/date 10 tháng/month 03 năm/year 2022", + "pred": "ngày/date 10than Imonth 03 năm/year 2022" + }, + "20221027_154600.json": { + "label": "ngày/date 29 tháng /month 10 năm/year✪2013", + "pred": "ngày/date 29 tháng month 10 năm/year2013" + }, + "191389634_910736173104716_4923402486196996972_n.json": { + "label": "ngày /date 24 tháng /month 02 năm/year 2021", + "pred": "ngày date 24 tháng month 02 năm/year 2021" + }, + "20221027_155723.json": { + "label": "ngày /date 27 tháng /month 10 năm/year 2014", + "pred": "ngày /date 27 tháng month 10 năm/year 2014" + }, + "184606042_1586323798376373_2179113485447088932_n.json": { + "label": "ngày /date 29 tháng /month 07 năm/year 2020", + "pred": "ngày (date 29 tháng /month 07 năm✪year 2020" + }, + "6fb460d06a9dacc3f58c31.json": { + "label": "ngày /date 28 tháng /month 05 năm /year 2019", + "pred": "ngày date 28 tháng month 05 năm year 2019" + }, + "7e810ce502a8c4f69db93.json": { + "label": "ngày /date 29 tháng /month 10 năm /year 2020", + "pred": "ngày de it 29 thing month 10 năm Year 2020" + }, + "20221027_154400.json": { + "label": "ngày /date 28 tháng /month 05 năm /year 2019", + "pred": "ngày date 28 tháng month 05 năm year 2019" + }, + "139579296_107198731349013_7325819456715999953_n.json": { + "label": "ngày /date 10tháng month 05 năm /year 2018", + "pred": "ngày date 1 month 05 năm hear 2018" + }, + "20221027_155748.json": { + "label": "ngày/date 29 tháng/month 10 năm/year✪2013", + "pred": "ngày/date 29 tháng/month 10 năm/year2013" + }, + "164359233_2788848161366629_6843431895499380423_n.json": { + "label": "ngày /date 25 tháng /month 12 năm/year 2015", + "pred": "ngày dute 25 tháng month 12 năm/year 2015" + }, + "962650ff5eb298ecc1a36.json": { + "label": "ngày /date 29 tháng/month 10 năm/year 2013", + "pred": "10 năm/year 2013\n ngày the 29thanginonth\n TL" + }, + "20221027_155600.json": { + "label": "ngày/date 30 tháng /month 07 năm/year 2015", + "pred": "ngày/date 30 tháng month 07 năm/year 2015" + }, + "951970367f7bb925e06a1.json": { + "label": "ngày /date 28 tháng /month 05 năm /year 2019 -", + "pred": "ngày late 28 hàng worth 05 năm hear 2019" + }, + "20221027_154627.json": { + "label": "ngày /date 29 tháng /month 10 năm/year 2020", + "pred": "ngày date 29 tháng month 10 năm/year. 2020" + }, + "20221027_155535.json": { + "label": "ngày /date 01 tháng /month 04 năm /year 2022", + "pred": "ngày date 01 tháng month 04 năm 'year 2022" + }, + "106402928_1000018507095212_5438034148254460378_n.json": { + "label": "ngày date 04 tháng month 09 năm /year 2019", + "pred": "ngày dan 04 tháng month 09 năm Year 2019" + }, + "20221027_154458.json": { + "label": "ngày /date 24 tháng /month 05 năm/year 2016", + "pred": "ngày date 24 tháng month 03 năm/year 2016" + }, + "190841312_3057702594458479_8551202571498845435_n.json": { + "label": "ngày /date 14 tháng month 12 năm /year 2018", + "pred": "ngày date 14 tháng month 12 năm year 2018" + }, + "20221027_154907.json": { + "label": "ngày/date 30 tháng /month 07 năm/year✪2015", + "pred": "ngày/date 30 tháng month 07 năm/year2015" + }, + "136098726_3413968628702123_4090292519699106839_n.json": { + "label": "ngày /date 20tháng /month 11 năm /year 2020", + "pred": "ngày date 70tháng month 11 năm year 2020" + }, + "20221027_154440.json": { + "label": "ngày /date 09 tháng /month 07 năm /year 2019", + "pred": "ngày date 09 tháng mc each 07 năm year 2019" + }, + "07966c1b6256a408fd478.json": { + "label": "ngày/date 24 tháng /month 05 năm/year 2016", + "pred": "ngày/d te 24 thán day anth 05 năm/year 2016" + }, + "20221027_155740.json": { + "label": "ngày/date 10 tháng/month 03 năm/year 2022", + "pred": "ngày/date 10than almonth 03 năm/year 2022" + }, + "130727988_1377512982599930_6481917606912865462_n.json": { + "label": "ngày/date 30 tháng /month 05 năm/year 2016", + "pred": "ngày/date 30 tháng month 05 năm/year 2016" + }, + "194993073_539716147195165_8525378287933192246_n.json": { + "label": "ngày /date 12 tháng /month 06 năm /year 2020", + "pred": "ngày date 12 tháng /month 06 năm vear 2020" + }, + "20221027_154521.json": { + "label": "ngày/date 19 tháng /month 03 năm/year 2018", + "pred": "ngày/dec 19 tháng month 03 năm/year 2018" + }, + "174247511_900878714088455_7516565117828455890_n.json": { + "label": "ngày/date 1 0✪tháng /month 05 năm/year 2018", + "pred": "ngày/date 4 Other month 05 năm/year 2018" + }, + "20221027_155519.json": { + "label": "ngày /date 22 tháng /month 04 năm /year 2022", + "pred": "ngày date 22 tháng month 04 năm year 2022" + }, + "20221027_154706.json": { + "label": "ngày /date 27 tháng /month 10 năm/year 2014", + "pred": "ngày date 27 tháng month 10 năm/year. 2014" + }, + "a88ae66aed272b79723621.json": { + "label": "ngày /date 27 tháng /month 10 năm/year 2014", + "pred": "" + }, + "4378298-95583fbb1edb703a6c5bbc1744246058-1-1.json": { + "label": "ngày/d 24 tháng /month 1 năm/year 2015", + "pred": "ngày/d / 24 than chuonth 1 1 năm/year 2015" + }, + "20221027_155704.json": { + "label": "ngày /date 22 tháng /month 04 năm/year 2022", + "pred": "ngày date 22 tháng month 04 năm hear 2022" + }, + "179642128_1945301335636182_5557211235870766646_n.json": { + "label": "ngày/date #✪thá# # onth 10 năm/year 201", + "pred": "ngowdan (Ký - touth 10 năm/year 201" + }, + "20221027_154734.json": { + "label": "ngày /date 16 tháng /month 06 năm /year 2020", + "pred": "ngày date to tháng month 06 năm year 2020" + }, + "20221027_155542.json": { + "label": "ngày /date 05 tháng /month 04 năm/year 2016", + "pred": "ngày /date 05 tháng month 04 năm/year 2016" + } +} \ No newline at end of file diff --git a/cope2n-ai-fi/common/json2xml.py b/cope2n-ai-fi/common/json2xml.py new file mode 100755 index 0000000..8b643fd --- /dev/null +++ b/cope2n-ai-fi/common/json2xml.py @@ -0,0 +1,196 @@ +import xml.etree.ElementTree as ET +from datetime import datetime +ET.register_namespace('', "http://www.w3.org/2000/09/xmldsig#") + + +xml_template3 = """ + + + + None + None + None + None + None + None + None + None + None + None + + + + None + None + None + None + + + None + None + None + None + None + + + + None + None + None + None + None + None + None + None + None + None + + + + + + None + None + None + + + None + None + None + None + None + + + + None + None + +""" + +def replace_xml_values(xml_str, replacement_dict): + """ replace xml values + + Args: + xml_str (_type_): _description_ + replacement_dict (_type_): _description_ + + Returns: + _type_: _description_ + """ + try: + root = ET.fromstring(xml_str) + for key, value in replacement_dict.items(): + if not value: + continue + if key == "TToan": + ttoan_element = root.find(".//TToan") + tsuat_element = ttoan_element.find(".//TgTThue") + tthue_element = ttoan_element.find(".//TgTTTBSo") + tthuebchu_element = ttoan_element.find(".//TgTTTBChu") + if value["TgTThue"]: + tsuat_element.text = value["TgTThue"] + if value["TgTTTBSo"]: + tthue_element.text = value["TgTTTBSo"] + if value["TgTTTBChu"]: + tthuebchu_element.text = value["TgTTTBChu"] + elif key == "NMua": + nmua_element = root.find(".//NMua") + for key_ in ["DChi", "SDThoai", "MST", "Ten", "HVTNMHang"]: + if value.get(key_, None): + nmua_element_key = nmua_element.find(f".//{key_}") + nmua_element_key.text = value[key_] + elif key == "NBan": + nban_element = root.find(".//NBan") + for key_ in ["DChi", "SDThoai", "MST", "Ten"]: + if value.get(key_, None): + nban_element_key = nban_element.find(f".//{key_}") + nban_element_key.text = value[key_] + elif key == "HHDVu": + dshhdvu_element = root.find(".//DSHHDVu") + hhdvu_template = root.find(".//HHDVu") + if hhdvu_template is not None and dshhdvu_element is not None: + dshhdvu_element.remove(hhdvu_template) # Remove the template + for hhdvu_data in value: + hhdvu_element = ET.SubElement(dshhdvu_element, "HHDVu") + for h_key, h_value in hhdvu_data.items(): + h_element = ET.SubElement(hhdvu_element, h_key) + h_element.text = h_value if h_value is not None else "None" + elif key == "NLap": + nlap_element = root.find(".//NLap") + if nlap_element is not None: + # Convert the date to yyyy-mm-dd format + try: + date_obj = datetime.strptime(value, "%d/%m/%Y") + formatted_date = date_obj.strftime("%Y-%m-%d") + nlap_element.text = formatted_date + except ValueError: + print(f"Invalid date format for {key}: {value}") + nlap_element.text = value + else: + element = root.find(f".//{key}") + if element is not None: + element.text = value + ET.register_namespace("", "http://www.w3.org/2000/09/xmldsig#") + return ET.tostring(root, encoding="unicode") + except ET.ParseError as e: + print(f"Error parsing XML: {e}") + return None + + +def convert_key_names(original_dict): + """_summary_ + + Args: + original_dict (_type_): _description_ + + Returns: + _type_: _description_ + """ + key_mapping = { + "table": "HHDVu", + "Mặt hàng": "THHDVu", + "Đơn vị tính": "DVTinh", + "Số lượng": "SLuong", + "Đơn giá": "DGia", + "Doanh số mua chưa có thuế": "ThTien", + "buyer_address_value": "NMua.DChi", + 'buyer_company_name_value': 'NMua.Ten', + 'buyer_personal_name_value': 'NMua.HVTNMHang', + 'buyer_tax_code_value': 'NMua.MST', + 'buyer_tel_value': 'NMua.SDThoai', + 'seller_address_value': 'NBan.DChi', + 'seller_company_name_value': 'NBan.Ten', + 'seller_tax_code_value': 'NBan.MST', + 'seller_tel_value': 'NBan.SDThoai', + 'date_value': 'NLap', + 'form_value': 'KHMSHDon', + 'no_value': 'SHDon', + 'serial_value': 'KHHDon', + 'tax_amount_value': 'TToan.TgTThue', + 'total_in_words_value': 'TToan.TgTTTBChu', + 'total_value': 'TToan.TgTTTBSo' + } + + converted_dict = {} + for key, value in original_dict.items(): + new_key = key_mapping.get(key, key) + if "." in new_key: + parts = new_key.split(".") + current_dict = converted_dict + for i, part in enumerate(parts): + if i == len(parts) - 1: + current_dict[part] = value + else: + current_dict.setdefault(part, {}) + current_dict = current_dict[part] + else: + if key == "table": + lconverted_table_values = [] + for table_value in value: + converted_table_value = convert_key_names(table_value) + lconverted_table_values.append(converted_table_value) + converted_dict[new_key] = lconverted_table_values + else: + converted_dict[new_key] = value + + return converted_dict \ No newline at end of file diff --git a/cope2n-ai-fi/common/ocr.py b/cope2n-ai-fi/common/ocr.py new file mode 100755 index 0000000..5399a26 --- /dev/null +++ b/cope2n-ai-fi/common/ocr.py @@ -0,0 +1,37 @@ +from common.utils.ocr_yolox import OcrEngineForYoloX_ID_Driving +from common.utils.word_formation import Word, words_to_lines + +det_ckpt = "yolox-s-general-text-pretrain-20221226" +cls_ckpt = "satrn-lite-general-pretrain-20230106" + +engine = OcrEngineForYoloX_ID_Driving(det_ckpt, cls_ckpt) + + +def ocr_predict(image): + """Predict text from image + + Args: + image_path (str): _description_ + + Returns: + list: list of words + """ + try: + lbboxes, lwords = engine.run_image(image) + lWords = [Word(text=word, bndbox=bbox) for word, bbox in zip(lwords, lbboxes)] + list_lines, _ = words_to_lines(lWords) + return list_lines + except AssertionError as e: + print(e) + list_lines = [] + return list_lines + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--image", type=str, required=True) + args = parser.parse_args() + + list_lines = ocr_predict(args.image) diff --git a/cope2n-ai-fi/common/post_processing_datetime.py b/cope2n-ai-fi/common/post_processing_datetime.py new file mode 100755 index 0000000..ad5cd27 --- /dev/null +++ b/cope2n-ai-fi/common/post_processing_datetime.py @@ -0,0 +1,113 @@ +import re +from datetime import datetime +from sklearn.metrics import classification_report +from common.utils.utils import read_json +from underthesea import word_tokenize + + +class DatetimeCorrector: + @staticmethod + def verify_and_convert_date(date_str): + # Try to parse the date string using the datetime module + try: + date = datetime.strptime(date_str, "%d/%m/%Y") # TODO: fix this + except ValueError: + # If the date string is not in a valTid format, return False + return "" + + # If the date string is in the correct format, check if it is already in the "dd/mm/yyyy" format + if date_str[:2] == "dd" and date_str[3:5] == "mm" and date_str[6:] == "yyyy": + # If the date string is already in the correct format, return it as is + return date_str + else: + # If the date string is not in the correct format, use the strftime method to convert it + return date.strftime("%d/%m/%Y") + + @staticmethod + def get_date_from_date_string_by_prefix(date_string_, prefix_): + prefix = prefix_.lower() + date_string = date_string_.lower() + if prefix in date_string: + try: + if prefix == "năm": + match = re.split( + r"năm[^\d]*(\d{4}|\d{1}[\s.]*\d{3}|\d{3}[\s.]*\d{1}|\d{2}[\s.]*\d{2}|\d{2}[\s.]*\d{1}[\s.]*\d{1}|\d{1}[\s.]*\d{2}[\s.]*\d{1}|\d{1}[\s.]*\d{1}[\s.]*\d{2}|\d{1}[\s.]*\d{1}[\s.]*\d{1}[\s.]*\d{1})[\s.]*\b", + date_string) # match "năm" following with all combination of 4 numbers and whitespace/dot such as 1111; 111.1; 111 1; 11 2 1, 2 2 2.2; ... + elif prefix == "ngày": + match = re.split(r"ngày[^\d]*(\d{2}|\d{1}[\s.]*\d{1}|\d{1})[\s.]*\b", date_string) + else: + match = re.split(r"tháng[^\d]*(\d{2}|\d{1}[\s.]*\d{1}|\d{1})[\s.]*\b", date_string) + num = match[1] + remain_string = match[2] if prefix != "năm" else match[0] + return num, remain_string + except: + return "", date_string_ + else: + return '', date_string_ + + @staticmethod + def get_date_by_pattern(date_string): + match = re.findall(r"([^\d\s]+)?\s*(\d{1}\s*\d?\s+|\d{2}\s+|\d+\s*\b)", date_string) + if not match: + return "" + if len(match) > 3: + day = match[0][-1].replace(" ", "") + year = match[-1][-1].replace(" ", "") + # since in the VIETNAMESE DRIVER LICENSE, the tháng/month is behind the stamp and can be recognized as any thing => mistạken number may be in range (1->-3) => choose month to be -2 + month = match[-2][-1].replace(" ", "") + return "/".join([day, month, year]) + else: + return "/".join([m[-1].replace(" ", "") for m in match]) + + @staticmethod + def extract_date_from_string(date_string): + remain_str = date_string + ldate = [] + for d in ["năm", "ngày", "tháng"]: + date, remain_str = DatetimeCorrector.get_date_from_date_string_by_prefix(date_string, d) + if not date: + return DatetimeCorrector.get_date_by_pattern(date_string) + ldate.append(date.strip().replace(" ", "").replace(".", "")) + return "/".join([ldate[1], ldate[2], ldate[0]]) + + @staticmethod + def correct(date_string): + # Extract the day, month, and year from the string using regular expressions + date_string = date_string.lower().replace("✪", " ") + date_string = " ".join(word_tokenize(date_string)) + parsed_date_string_ = DatetimeCorrector.verify_and_convert_date(date_string) # if already in datetime format + if parsed_date_string_: + return parsed_date_string_ + extracted_date = DatetimeCorrector.extract_date_from_string(date_string) + parsed_date_string_ = DatetimeCorrector.verify_and_convert_date(extracted_date) + return parsed_date_string_ if parsed_date_string_ else date_string + + @staticmethod + def eval(): + data = read_json("common/dates_gplx.json") + type_column = "GPLX" # Invoice/GPLX + y_true, y_pred = [], [] + lexcludes = {} + ddata = {} + for k, d in data.items(): + if k in lexcludes: + continue + if k == "inv_SDV_215": + print("debugging") + pred = DatetimeCorrector.correct(d["pred"]) + label = DatetimeCorrector.correct(d["label"]) + ddata[k] = {} + data[k]["Type"] = type_column + ddata[k]["Predict"] = d["pred"] + ddata[k]["Label"] = d["label"] + ddata[k]["Post-processed"] = pred + y_pred.append(pred == label) + y_true.append(1) + if k == "invoice_1219_000": + print("\n", k, '-' * 50) + print(pred, "------", d["pred"]) + print(label, "------", d["label"]) + print(classification_report(y_true, y_pred)) + import pandas as pd + df = pd.DataFrame.from_dict(ddata, orient="index") + df.to_csv(f"result/datetime_post_processed_{type_column}.csv") \ No newline at end of file diff --git a/cope2n-ai-fi/common/post_processing_driver.py b/cope2n-ai-fi/common/post_processing_driver.py new file mode 100755 index 0000000..49b7423 --- /dev/null +++ b/cope2n-ai-fi/common/post_processing_driver.py @@ -0,0 +1,51 @@ +from common.utils.word_formation import words_to_lines +from Kie_AHung.prediction import KIE_LABELS, IGNORE_KIE_LABEL +from common.post_processing_datetime import DatetimeCorrector + + +def merge_bbox(list_bbox): + if not list_bbox: + return list_bbox + left = min(list_bbox, key=lambda x: x[0])[0] + top = min(list_bbox, key=lambda x: x[1])[1] + right = max(list_bbox, key=lambda x: x[2])[2] + bot = max(list_bbox, key=lambda x: x[3])[3] + return [left, top, right, bot] + + +def create_result_kie_dict(): + return { + KIE_LABELS[i]: {} + for i in range(len(KIE_LABELS)) + if KIE_LABELS[i] != IGNORE_KIE_LABEL + } + + +def create_empty_kie_dict(): + return { + KIE_LABELS[i]: [] + for i in range(len(KIE_LABELS)) + if KIE_LABELS[i] != IGNORE_KIE_LABEL + } + + +def create_kie_dict(list_words): + kie_dict = create_empty_kie_dict() + # append each word to respected dict + for word in list_words: + if word.kie_label in kie_dict: + kie_dict[word.kie_label].append(word) + word.text = word.text.strip() + # construct line from words for each kie_label + result_dict = create_result_kie_dict() + for kie_label in result_dict: + list_lines, _ = words_to_lines(kie_dict[kie_label]) + text = "\n ".join([line.text.strip() for line in list_lines]) + if kie_label == "date": + # text = post_processing_datetime(text) + text = DatetimeCorrector.correct(text) + result_dict[kie_label]["text"] = text + result_dict[kie_label]["bbox"] = merge_bbox( + [line.boundingbox for line in list_lines] + ) + return result_dict diff --git a/cope2n-ai-fi/common/post_processing_id.py b/cope2n-ai-fi/common/post_processing_id.py new file mode 100755 index 0000000..82c0597 --- /dev/null +++ b/cope2n-ai-fi/common/post_processing_id.py @@ -0,0 +1,51 @@ +from common.utils.word_formation import words_to_lines +from Kie_AHung_ID.prediction import KIE_LABELS, IGNORE_KIE_LABEL +from common.post_processing_datetime import DatetimeCorrector + + +def merge_bbox(list_bbox): + if not list_bbox: + return list_bbox + left = min(list_bbox, key=lambda x: x[0])[0] + top = min(list_bbox, key=lambda x: x[1])[1] + right = max(list_bbox, key=lambda x: x[2])[2] + bot = max(list_bbox, key=lambda x: x[3])[3] + return [left, top, right, bot] + + +def create_result_kie_dict(): + return { + KIE_LABELS[i]: {} + for i in range(len(KIE_LABELS)) + if KIE_LABELS[i] != IGNORE_KIE_LABEL + } + + +def create_empty_kie_dict(): + return { + KIE_LABELS[i]: [] + for i in range(len(KIE_LABELS)) + if KIE_LABELS[i] != IGNORE_KIE_LABEL + } + + +def create_kie_dict(list_words): + kie_dict = create_empty_kie_dict() + # append each word to respected dict + for word in list_words: + if word.kie_label in kie_dict: + kie_dict[word.kie_label].append(word) + word.text = word.text.strip() + # construct line from words for each kie_label + result_dict = create_result_kie_dict() + for kie_label in result_dict: + list_lines, _ = words_to_lines(kie_dict[kie_label]) + text = "\n ".join([line.text.strip() for line in list_lines]) + if kie_label == "date": + # text = post_processing_datetime(text) + text = DatetimeCorrector.correct(text) + result_dict[kie_label]["text"] = text + result_dict[kie_label]["bbox"] = merge_bbox( + [line.boundingbox for line in list_lines] + ) + return result_dict diff --git a/cope2n-ai-fi/common/process_pdf.py b/cope2n-ai-fi/common/process_pdf.py new file mode 100755 index 0000000..4ad48b1 --- /dev/null +++ b/cope2n-ai-fi/common/process_pdf.py @@ -0,0 +1,252 @@ +import os +import json + +from common import json2xml +from common.json2xml import convert_key_names, replace_xml_values +from common.utils_kvu.split_docs import split_docs, merge_sbt_output + +# from api.OCRBase.prediction import predict as ocr_predict +# from api.Kie_Invoice_AP.prediction_sap import predict +# from api.Kie_Invoice_AP.prediction_fi import predict_fi +# from api.manulife.predict_manulife import predict as predict_manulife +from api.sdsap_sbt.prediction_sbt import predict as predict_sbt + +os.environ['PYTHONPATH'] = '/home/thucpd/thucpd/cope2n-ai/cope2n-ai/' + +def check_label_exists(array, target_label): + for obj in array: + if obj["label"] == target_label: + return True # Label exists in the array + return False # Label does not exist in the array + +def compile_output(list_url): + """_summary_ + + Args: + pdf_extracted (list): list: [{ + "1": url},{"2": url}, + ...] + Raises: + NotImplementedError: _description_ + + Returns: + dict: output compiled + """ + + results = { + "model":{ + "name":"Invoice", + "confidence": 1.0, + "type": "finance/invoice", + "isValid": True, + "shape": "letter", + } + } + compile_outputs = [] + compiled = [] + for page in list_url: + output_model = predict(page['page_number'], page['file_url']) + for field in output_model['fields']: + if field['value'] != "" and not check_label_exists(compiled, field['label']): + element = { + 'label': field['label'], + 'value': field['value'], + } + compiled.append(element) + elif field['label'] == 'table' and check_label_exists(compiled, "table"): + for index, obj in enumerate(compiled): + if obj['label'] == 'table': + compiled[index]['value'].append(field['value']) + compile_output = { + 'page_index': page['page_number'], + 'request_file_id': page['request_file_id'], + 'fields': output_model['fields'] + } + compile_outputs.append(compile_output) + results['combine_results'] = compiled + results['pages'] = compile_outputs + return results + +def update_null_values(kvu_result, next_kvu_result): + for key, value in kvu_result.items(): + if value is None and next_kvu_result.get(key) is not None: + kvu_result[key] = next_kvu_result[key] + +def replace_empty_null_values(my_dict): + for key, value in my_dict.items(): + if value == '': + my_dict[key] = None + return my_dict + +def compile_output_fi(list_url): + """_summary_ + + Args: + pdf_extracted (list): list: [{ + "1": url},{"2": url}, + ...] + Raises: + NotImplementedError: _description_ + + Returns: + dict: output compiled + """ + + results = { + "model":{ + "name":"Invoice", + "confidence": 1.0, + "type": "finance/invoice", + "isValid": True, + "shape": "letter", + } + } + # Loop through the list_url to update kvu_result + for i in range(len(list_url) - 1): + page = list_url[i] + next_page = list_url[i + 1] + kvu_result, output_kie = predict_fi(page['page_number'], page['file_url']) + next_kvu_result, next_output_kie = predict_fi(next_page['page_number'], next_page['file_url']) + + update_null_values(kvu_result, next_kvu_result) + output_kie = replace_empty_null_values(output_kie) + next_output_kie = replace_empty_null_values(next_output_kie) + update_null_values(output_kie, next_output_kie) + + # Handle the last item in the list_url + if list_url: + page = list_url[-1] + kvu_result, output_kie = predict_fi(page['page_number'], page['file_url']) + + converted_dict = convert_key_names(kvu_result) + converted_dict.update(convert_key_names(output_kie)) + output_fi = replace_xml_values(json2xml.xml_template3, converted_dict) + field_fi = { + "xml": output_fi, + } + results['combine_results'] = field_fi + # results['combine_results'] = converted_dict + # results['combine_results_kie'] = output_kie + return results + +def compile_output_ocr_base(list_url): + """Compile output of OCRBase + + Args: + list_url (list): List string url of image + + Returns: + dict: dict of output + """ + + results = { + "model":{ + "name":"OCRBase", + "confidence": 1.0, + "type": "ocrbase", + "isValid": True, + "shape": "letter", + } + } + compile_outputs = [] + for page in list_url: + output_model = ocr_predict(page['page_number'], page['file_url']) + compile_output = { + 'page_index': page['page_number'], + 'request_file_id': page['request_file_id'], + 'fields': output_model['fields'] + } + compile_outputs.append(compile_output) + results['pages'] = compile_outputs + return results + +def compile_output_manulife(list_url): + """_summary_ + + Args: + pdf_extracted (list): list: [{ + "1": url},{"2": url}, + ...] + Raises: + NotImplementedError: _description_ + + Returns: + dict: output compiled + """ + + results = { + "model":{ + "name":"Invoice", + "confidence": 1.0, + "type": "finance/invoice", + "isValid": True, + "shape": "letter", + } + } + + outputs = [] + for page in list_url: + output_model = predict_manulife(page['page_number'], page['file_url']) # gotta be predict_manulife(), for the time being, this function is not avaible, we just leave a dummy function here instead + print("output_model", output_model) + outputs.append(output_model) + print("outputs", outputs) + documents = split_docs(outputs) + print("documents", documents) + results = { + "total_pages": len(list_url), + "ocr_num_pages": len(list_url), + "document": documents + } + return results + +def compile_output_sbt(list_url): + """_summary_ + + Args: + pdf_extracted (list): list: [{ + "1": url},{"2": url}, + ...] + Raises: + NotImplementedError: _description_ + + Returns: + dict: output compiled + """ + + results = { + "model":{ + "name":"Invoice", + "confidence": 1.0, + "type": "finance/invoice", + "isValid": True, + "shape": "letter", + } + } + + + outputs = [] + for page in list_url: + output_model = predict_sbt(page['page_number'], page['file_url']) + if "doc_type" in page: + output_model['doc_type'] = page['doc_type'] + outputs.append(output_model) + documents = merge_sbt_output(outputs) + results = { + "total_pages": len(list_url), + "ocr_num_pages": len(list_url), + "document": documents + } + return results + + +def main(): + """ + main function + """ + list_url = [{"file_url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf", "page_number": 1, "request_file_id": 1}, ...] + results = compile_output(list_url) + with open('output.json', 'w', encoding='utf-8') as file: + json.dump(results, file, ensure_ascii=False, indent=4) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/cope2n-ai-fi/common/serve_model.py b/cope2n-ai-fi/common/serve_model.py new file mode 100755 index 0000000..8d1cc3c --- /dev/null +++ b/cope2n-ai-fi/common/serve_model.py @@ -0,0 +1,93 @@ +import cv2 +from common.ocr import ocr_predict +from common.crop_location import crop_location +from Kie_AHung.prediction import infer_driving_license +from Kie_AHung_ID.prediction import infer_id_card +from common.post_processing_datetime import DatetimeCorrector +from transformers import ( + LayoutXLMTokenizer, + LayoutLMv2FeatureExtractor, + LayoutXLMProcessor + ) + +MAX_SEQ_LENGTH = 512 # TODO Fix this hard code +tokenizer = LayoutXLMTokenizer.from_pretrained( + "./Kie_AHung/model/pretrained/layoutxlm-base/tokenizer", model_max_length=MAX_SEQ_LENGTH +) + +feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) +processor = LayoutXLMProcessor(feature_extractor, tokenizer) + +max_n_words = 100 + +def predict(rq_id, sub_id, folder_name, list_url, user_id, infer_name): + """Predict text from image + + Args: + image_path (str): path to image + + Returns: + dict: dict result of prediction + """ + + results = { + "model":{ + "name":infer_name, + "confidence": 1.0, + "type": "finance/invoice", + "isValid": True, + "shape": "letter", + } + } + compile_outputs = [] + for page in list_url: + image_location = crop_location(page['file_url']) + if image_location is None: + compile_output = { + 'page_index': page['page_number'], + 'path_image_croped': None, + 'request_file_id': page['request_file_id'], + 'fields': None + } + compile_outputs.append(compile_output) + + elif image_location is not None: + path_image_croped = "/app/media/users/{}/subscriptions/{}/requests/{}/{}/image_croped.jpg".format(user_id,sub_id,folder_name,rq_id) + cv2.imwrite("/users/{}/subscriptions/{}/requests/{}/{}/image_croped.jpg".format(user_id,sub_id,folder_name,rq_id), image_location) + list_line = ocr_predict(image_location) + + if infer_name == "driving_license": + from common.post_processing_driver import create_kie_dict + _, _, _, list_words = infer_driving_license(image_location, list_line, max_n_words, processor) + result_dict = create_kie_dict(list_words) + elif infer_name == "id_card": + from common.post_processing_id import create_kie_dict + _, _, _, list_words = infer_id_card(image_location, list_line, max_n_words, processor) + result_dict = create_kie_dict(list_words) + + fields = [] + for kie_label in result_dict: + if result_dict[kie_label]["text"] != "": + if kie_label == "Date Range": + text = DatetimeCorrector.correct(result_dict[kie_label]["text"]) + else: + text = result_dict[kie_label]["text"] + + field = { + "label": kie_label, + "value": text.replace("✪", " ") if "✪" in text else text, + "box": result_dict[kie_label]["bbox"], + "confidence": 0.99 #TODO: add confidence + } + fields.append(field) + + compile_output = { + 'page_index': page['page_number'], + 'path_image_croped': str(path_image_croped), + 'request_file_id': page['request_file_id'], + 'fields': fields + } + + compile_outputs.append(compile_output) + results['pages'] = compile_outputs + return results diff --git a/cope2n-ai-fi/common/utils/blurry_detection.py b/cope2n-ai-fi/common/utils/blurry_detection.py new file mode 100755 index 0000000..7cbf1af --- /dev/null +++ b/cope2n-ai-fi/common/utils/blurry_detection.py @@ -0,0 +1,35 @@ +import cv2 +import urllib +import numpy as np + +class BlurryDetection: + def __init__(self): + # initialize the detector + pass + + def variance_of_laplacian(self, image): + # compute the Laplacian of the image and then return the focus + # measure, which is simply the variance of the Laplacian + return cv2.Laplacian(image, cv2.CV_64F).var() + + def __call__(self, img, thr=100): + gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + + fm = self.variance_of_laplacian(gray) + + if fm >= thr: + return "non_blurry", fm + else: + return "blurry", fm + + +detector = BlurryDetection() + + +def check_blur(image_url): + req = urllib.request.urlopen(image_url) + arr = np.asarray(bytearray(req.read()), dtype=np.uint8) + img = cv2.imdecode(arr, -1) + pred = detector(img, thr=10) + score = pred[0] + return score diff --git a/cope2n-ai-fi/common/utils/global_variables.py b/cope2n-ai-fi/common/utils/global_variables.py new file mode 100755 index 0000000..c59f7df --- /dev/null +++ b/cope2n-ai-fi/common/utils/global_variables.py @@ -0,0 +1,71 @@ +MAX_SEQ_LENGTH = 512 +DEVICE = "cuda:0" +KIE_LABELS = [ + "other", + "form_key", + "form_value", + "serial_key", + "serial_value", + "no_key", + "no_value", + "date", + "seller_name_value", + "seller_name_key", + "seller_tax_code_key", + "seller_tax_code_value", + "seller_address_value", + "seller_address_key", + "seller_mobile_key", + "buyer_name_key", + "buyer_company_name_key", + "buyer_company_name_value", + "buyer_tax_code_key", + "buyer_tax_code_value", + "buyer_address_value", + "buyer_address_key", + "VAT_amount_key", + "VAT_amount_value", + "total_key", + "total_value", + "total_in_words_key", + "total_in_words_value", + "seller_mobile_value", + "buyer_name_value", + "buyer_mobile_key", + "buyer_mobile_value", +] + +BRIEF_LABELS = [ + "o", + "fk", + "fv", + "sk", + "sv", + "nk", + "nv", + "d", + "snv", + "snk", + "stck", + "stcv", + "sav", + "sak", + "smk", + "bnk", + "bcnk", + "bcnv", + "btck", + "btcv", + "bav", + "bak", + "VATk", + "VATv", + "tk", + "tv", + "tiwk", + "tiwv", + "smv", + "bnv", + "bmk", + "bmv", +] diff --git a/cope2n-ai-fi/common/utils/layoutLM_utils.py b/cope2n-ai-fi/common/utils/layoutLM_utils.py new file mode 100755 index 0000000..854c33f --- /dev/null +++ b/cope2n-ai-fi/common/utils/layoutLM_utils.py @@ -0,0 +1,78 @@ +from config import config as cfg +import json +import glob +from sklearn.model_selection import train_test_split +import os +import pandas as pd + + +def load_kie_labels_yolo(label_path): + with open(label_path, "r") as f: + lines = f.read().splitlines() + words, boxes, labels = [], [], [] + for line in lines: + x1, y1, x2, y2, text, kie = line.split("\t") + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + if text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + labels.append(kie) + return words, boxes, labels + + +def create_empty_kie_dict(): + return { + cfg.KIE_LABELS[i]: [] + for i in range(len(cfg.KIE_LABELS)) + if cfg.KIE_LABELS[i] != cfg.IGNORE_KIE_LABEL + } + + +def write_to_json_(file_path, content): + with open(file_path, mode="w", encoding="utf8") as f: + json.dump(content, f, ensure_ascii=False) + + +def load_train_val_id_cards(train_root, label_path): + train_labels = glob.glob(os.path.join(label_path, "*.txt")) + img_names = [ + os.path.basename(train_label).replace(".txt", ".jpg") + for train_label in train_labels + ] + train_paths = [os.path.join(train_root, img_name) for img_name in img_names] + train_df = pd.DataFrame.from_dict( + {"image_path": train_paths, "label": train_labels} + ) + train, test = train_test_split(train_df, test_size=0.2, random_state=cfg.SEED) + return train, test + + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + + +def get_name(file_path, ext: bool = True): + file_path_ = os.path.basename(file_path) + return file_path_ if ext else os.path.splitext(file_path_)[0] + + +def construct_file_path(dir, file_path, ext=""): + """ + args: + dir: /path/to/dir + file_path /example_path/to/file.txt + ext = '.json' + return + /path/to/dir/file.json + """ + return ( + os.path.join(dir, get_name(file_path, True)) + if ext == "" + else os.path.join(dir, get_name(file_path, False)) + ext + ) + + +def write_to_txt_(file_path, content): + with open(file_path, "w") as f: + f.write(content) diff --git a/cope2n-ai-fi/common/utils/merge_box.py b/cope2n-ai-fi/common/utils/merge_box.py new file mode 100755 index 0000000..c184232 --- /dev/null +++ b/cope2n-ai-fi/common/utils/merge_box.py @@ -0,0 +1,163 @@ +import cv2 +import numpy as np + +# tuplify +def tup(point): + return (point[0], point[1]) + + +# returns true if the two boxes overlap +def overlap(source, target): + # unpack points + tl1, br1 = source + tl2, br2 = target + + # checks + if tl1[0] >= br2[0] or tl2[0] >= br1[0]: + return False + if tl1[1] >= br2[1] or tl2[1] >= br1[1]: + return False + return True + + +# returns all overlapping boxes +def getAllOverlaps(boxes, bounds, index): + overlaps = [] + for a in range(len(boxes)): + if a != index and overlap(bounds, boxes[a]): + overlaps.append(a) + return overlaps + + +img = cv2.imread("test.png") +orig = np.copy(img) +blue, green, red = cv2.split(img) + + +def medianCanny(img, thresh1, thresh2): + median = np.median(img) + img = cv2.Canny(img, int(thresh1 * median), int(thresh2 * median)) + return img + + +blue_edges = medianCanny(blue, 0, 1) +green_edges = medianCanny(green, 0, 1) +red_edges = medianCanny(red, 0, 1) + +edges = blue_edges | green_edges | red_edges + +# I'm using OpenCV 3.4. This returns (contours, hierarchy) in OpenCV 2 and 4 +_, contours, hierarchy = cv2.findContours( + edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE +) + +# go through the contours and save the box edges +boxes = [] +# each element is [[top-left], [bottom-right]]; +hierarchy = hierarchy[0] +for component in zip(contours, hierarchy): + currentContour = component[0] + currentHierarchy = component[1] + x, y, w, h = cv2.boundingRect(currentContour) + if currentHierarchy[3] < 0: + cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 1) + boxes.append([[x, y], [x + w, y + h]]) + +# filter out excessively large boxes +filtered = [] +max_area = 30000 +for box in boxes: + w = box[1][0] - box[0][0] + h = box[1][1] - box[0][1] + if w * h < max_area: + filtered.append(box) +boxes = filtered + +# go through the boxes and start merging +merge_margin = 15 + +# this is gonna take a long time +finished = False +highlight = [[0, 0], [1, 1]] +points = [[[0, 0]]] +while not finished: + # set end con + finished = True + + # check progress + print("Len Boxes: " + str(len(boxes))) + + # draw boxes # comment this section out to run faster + copy = np.copy(orig) + for box in boxes: + cv2.rectangle(copy, tup(box[0]), tup(box[1]), (0, 200, 0), 1) + cv2.rectangle(copy, tup(highlight[0]), tup(highlight[1]), (0, 0, 255), 2) + for point in points: + point = point[0] + cv2.circle(copy, tup(point), 4, (255, 0, 0), -1) + cv2.imshow("Copy", copy) + key = cv2.waitKey(1) + if key == ord("q"): + break + + # loop through boxes + index = len(boxes) - 1 + while index >= 0: + # grab current box + curr = boxes[index] + + # add margin + tl = curr[0][:] + br = curr[1][:] + tl[0] -= merge_margin + tl[1] -= merge_margin + br[0] += merge_margin + br[1] += merge_margin + + # get matching boxes + overlaps = getAllOverlaps(boxes, [tl, br], index) + + # check if empty + if len(overlaps) > 0: + # combine boxes + # convert to a contour + con = [] + overlaps.append(index) + for ind in overlaps: + tl, br = boxes[ind] + con.append([tl]) + con.append([br]) + con = np.array(con) + + # get bounding rect + x, y, w, h = cv2.boundingRect(con) + + # stop growing + w -= 1 + h -= 1 + merged = [[x, y], [x + w, y + h]] + + # highlights + highlight = merged[:] + points = con + + # remove boxes from list + overlaps.sort(reverse=True) + for ind in overlaps: + del boxes[ind] + boxes.append(merged) + + # set flag + finished = False + break + + # increment + index -= 1 +cv2.destroyAllWindows() + +# show final +copy = np.copy(orig) +for box in boxes: + cv2.rectangle(copy, tup(box[0]), tup(box[1]), (0, 200, 0), 1) +cv2.imshow("Final", copy) +cv2.waitKey(0) diff --git a/cope2n-ai-fi/common/utils/ocr_yolox.py b/cope2n-ai-fi/common/utils/ocr_yolox.py new file mode 100755 index 0000000..f41cbba --- /dev/null +++ b/cope2n-ai-fi/common/utils/ocr_yolox.py @@ -0,0 +1,77 @@ +import numpy as np +from .utils import get_crop_img_and_bbox +from sdsvtr import StandaloneSATRNRunner +from sdsvtd import StandaloneYOLOXRunner +import urllib +import cv2 + + +class YoloX: + def __init__(self, checkpoint): + self.model = StandaloneYOLOXRunner(checkpoint, device = "cuda:0") + + def inference(self, img=None): + runner = self.model + return runner(img) + + +class Classifier_SATRN: + def __init__(self, checkpoint): + self.model = StandaloneSATRNRunner(checkpoint, return_confident=True, device = "cuda:0") + + def inference(self, numpy_image): + model_inference = self.model + result = model_inference(numpy_image) + preds_str = result[0] + confidence = result[1] + return preds_str, confidence + +class OcrEngineForYoloX_Invoice: + def __init__(self, det_ckpt, cls_ckpt): + self.det = YoloX(det_ckpt) + self.cls = Classifier_SATRN(cls_ckpt) + + def run_image(self, img): + + pred_det = self.det.inference(img) + pred_det = pred_det[0] + + pred_det = sorted(pred_det, key=lambda box: [box[1], box[0]]) + if len(pred_det) == 0: + return [], [] + else: + bboxes = np.vstack(pred_det) + lbboxes = [] + lcropped_img = [] + assert len(bboxes) != 0, f"No bbox found in image, skipped" + for bbox in bboxes: + try: + crop_img, bbox_ = get_crop_img_and_bbox(img, bbox, extend=True) + lbboxes.append(bbox_) + lcropped_img.append(crop_img) + except AssertionError as e: + print(e) + print(f"[ERROR]: Skipping invalid bbox in image") + lwords, _ = self.cls.inference(lcropped_img) + return lbboxes, lwords + +class OcrEngineForYoloX_ID_Driving: + def __init__(self, det_ckpt, cls_ckpt): + self.det = YoloX(det_ckpt) + self.cls = Classifier_SATRN(cls_ckpt) + + def run_image(self, img): + pred_det = self.det.inference(img) + bboxes = np.vstack(pred_det) + lbboxes = [] + lcropped_img = [] + assert len(bboxes) != 0, f"No bbox found in image, skipped" + for bbox in bboxes: + try: + crop_img, bbox_ = get_crop_img_and_bbox(img, bbox, extend=True) + lbboxes.append(bbox_) + lcropped_img.append(crop_img) + except AssertionError: + print(f"[ERROR]: Skipping invalid bbox image in ") + lwords, _ = self.cls.inference(lcropped_img) + return lbboxes, lwords diff --git a/cope2n-ai-fi/common/utils/process_label.py b/cope2n-ai-fi/common/utils/process_label.py new file mode 100755 index 0000000..6f76e33 --- /dev/null +++ b/cope2n-ai-fi/common/utils/process_label.py @@ -0,0 +1,139 @@ +import os +import cv2 as cv +import glob +from xml.dom.expatbuilder import parseString +from lxml.etree import Element, tostring, SubElement +import tqdm +from common.utils.global_variables import * + + +def boxes_to_xml(boxes_lst, xml_pth, img_pth=""): + """_summary_ + + Args: + boxes_lst (_type_): _description_ + xml_pth (_type_): _description_ + img_pth (str, optional): _description_. Defaults to ''. + """ + node_root = Element("annotation") + + node_folder = SubElement(node_root, "folder") + node_folder.text = "images" + + node_filename = SubElement(node_root, "filename") + node_filename.text = os.path.basename(img_pth) + + # insert size of image + if img_pth == "": + width, height = 0, 0 + else: + img = cv.imread(img_pth) + new_path = xml_pth[:-3] + "jpg" + cv.imwrite(new_path, img) + width, height = img.shape[:2] + + node_size = SubElement(node_root, "size") + + node_width = SubElement(node_size, "width") + node_width.text = str(width) + + node_height = SubElement(node_size, "height") + node_height.text = str(height) + + node_depth = SubElement(node_size, "depth") + node_depth.text = "3" + + node_segmented = SubElement(node_root, "segmented") + node_segmented.text = "0" + + for box in boxes_lst: + left, top, right, bottom = box.xmin, box.ymin, box.xmax, box.ymax + left, top, right, bottom = str(left), str(top), str(right), str(bottom) + label = box.label + if label == None: + label = "" + + node_object = SubElement(node_root, "object") + node_name = SubElement(node_object, "name") + node_name.text = label + + node_pose = SubElement(node_object, "pose") + node_pose.text = "Unspecified" + node_truncated = SubElement(node_object, "truncated") + node_truncated.text = "0" + node_difficult = SubElement(node_object, "difficult") + node_difficult.text = "0" + + # insert bounding box + node_bndbox = SubElement(node_object, "bndbox") + node_xmin = SubElement(node_bndbox, "xmin") + node_xmin.text = left + node_ymin = SubElement(node_bndbox, "ymin") + node_ymin.text = top + node_xmax = SubElement(node_bndbox, "xmax") + node_xmax.text = right + node_ymax = SubElement(node_bndbox, "ymax") + node_ymax.text = bottom + + xml = tostring(node_root, pretty_print=True) + dom = parseString(xml) + with open(xml_pth, "w+", encoding="utf-8") as f: + dom.writexml(f, indent="\t", addindent="\t", encoding="utf-8") + + +class Box: + def __init__(self): + self.xmax = 0 + self.ymax = 0 + self.xmin = 0 + self.ymin = 0 + self.label = "" + self.kie_label = "" + + +def check_iou(box1: Box, box2: Box, threshold=0.9): + area1 = (box1.xmax - box1.xmin) * (box1.ymax - box1.ymin) + area2 = (box2.xmax - box2.xmin) * (box2.ymax - box2.ymin) + xmin_intersect = max(box1.xmin, box2.xmin) + ymin_intersect = max(box1.ymin, box2.ymin) + xmax_intersect = min(box1.xmax, box2.xmax) + ymax_intersect = min(box1.ymax, box2.ymax) + if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: + area_intersect = 0 + else: + area_intersect = (xmax_intersect - xmin_intersect) * ( + ymax_intersect * ymin_intersect + ) + union = area1 + area2 - area_intersect + print(union) + iou = area_intersect / area1 + if iou > threshold: + return True + return False + + +DATA_ROOT = "/home/sds/hoangmd/TokenClassification/images/infer" +PSEUDO_LABEL = "/home/sds/hoangmd/TokenClassification/infer/" +list_files = glob.glob(PSEUDO_LABEL + "*.txt") + +for file in tqdm.tqdm(list_files): + xml_path = os.path.join("generated_label/", os.path.basename(file)[:-3] + "xml") + img_path = os.path.join(DATA_ROOT, os.path.basename(file)[:-3] + "jpg") + if not os.path.exists(img_path): + continue + f = open(file, "r", encoding="utf-8") + boxes = [] + for line in f.readlines(): + xmin, ymin, xmax, ymax, label = line.split("\t") + label = label[:-1] + box = Box() + box.xmin = int(float(xmin)) # left , top , right, bottom + box.ymin = int(float(ymin)) + box.xmax = int(float(xmax)) + box.ymax = int(float(ymax)) + box.label = label + boxes.append(box) + f.close() + boxes.sort(key=lambda x: [x.ymin, x.xmin]) + + boxes_to_xml(boxes, xml_path, img_path) diff --git a/cope2n-ai-fi/common/utils/utils.py b/cope2n-ai-fi/common/utils/utils.py new file mode 100755 index 0000000..f812c1a --- /dev/null +++ b/cope2n-ai-fi/common/utils/utils.py @@ -0,0 +1,180 @@ +import os +import json +import glob +import random +import cv2 + + +def read_txt(file): + with open(file, "r", encoding="utf8") as f: + data = [line.strip() for line in f] + return data + + +def write_txt(file, data): + with open(file, "w", encoding="utf8") as f: + for item in data: + f.write(item + "\n") + + +def write_json(file, data): + with open(file, "w", encoding="utf8") as f: + json.dump(data, f, ensure_ascii=False, sort_keys=True) + + +def read_json(file): + with open(file, "r", encoding="utf8") as f: + data = json.load(f) + return data + + +def get_colors(kie_labels): + + random.seed(1997) + colors = [] + for _ in range(len(kie_labels)): + color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + colors.append(color) + + return colors + + +def normalize_box(box, width, height): + assert ( + max(box) <= width or max(box) <= height + ), "box must smaller than width, height; max box = {}, width = {}, height = {}".format( + max(box), width, height + ) + return [ + int(1000 * (box[0] / width)), + int(1000 * (box[1] / height)), + int(1000 * (box[2] / width)), + int(1000 * (box[3] / height)), + ] + + +def unnormalize_box(bbox, width, height): + return [ + width * (bbox[0] / 1000), + height * (bbox[1] / 1000), + width * (bbox[2] / 1000), + height * (bbox[3] / 1000), + ] + + +def load_image_paths_and_labels(data_dir): + r"""Load (image path, label) pairs into a DataFrame with keys ``image_path`` and ``label`` + + @todo Add OCR paths here + """ + + img_paths = [path for path in glob.glob(data_dir + "/*") if ".txt" not in path] + label_paths = [os.path.splitext(path)[0] + ".txt" for path in img_paths] + + return img_paths, label_paths + + +import cv2 + + +def read_image_file(img_path): + image = cv2.imread(img_path) + return image + + +def normalize_bbox(x1, y1, x2, y2, w, h): + x1 = int(float(min(max(0, x1), w))) + x2 = int(float(min(max(0, x2), w))) + y1 = int(float(min(max(0, y1), h))) + y2 = int(float(min(max(0, y2), h))) + return (x1, y1, x2, y2) + + +def extend_crop_img( + left, top, right, bottom, margin_l=0, margin_t=0.03, margin_r=0.02, margin_b=0.05 +): + top = top - (bottom - top) * margin_t + bottom = bottom + (bottom - top) * margin_b + left = left - (right - left) * margin_l + right = right + (right - left) * margin_r + return left, top, right, bottom + + +def get_crop_img_and_bbox(img, bbox, extend: bool): + """ + img : numpy array img + bbox : should be xyxy format + """ + if len(bbox) == 5: + left, top, right, bottom, _conf = bbox + elif len(bbox) == 4: + left, top, right, bottom = bbox + if extend: + left, top, right, bottom = extend_crop_img(left, top, right, bottom) + left, top, right, bottom = normalize_bbox( + left, top, right, bottom, img.shape[1], img.shape[0] + ) + assert (bottom - top) * (right - left) > 0, "bbox is invalid" + crop_img = img[top:bottom, left:right] + return crop_img, (left, top, right, bottom) + + +import json +import os + + + +def load_kie_labels_yolo(label_path): + with open(label_path, 'r') as f: + lines = f.read().splitlines() + words, boxes, labels = [], [], [] + for line in lines: + x1, y1, x2, y2, text, kie = line.split("\t") + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + if text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + labels.append(kie) + return words, boxes, labels + + +def create_empty_kie_dict(): + return {cfg.KIE_LABELS[i]: [] for i in range(len(cfg.KIE_LABELS)) if cfg.KIE_LABELS[i] != cfg.IGNORE_KIE_LABEL} + + +def write_to_json_(file_path, content): + with open(file_path, mode='w', encoding='utf8') as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, 'r') as f: + return json.load(f) + + +def get_name(file_path, ext: bool = True): + file_path_ = os.path.basename(file_path) + return file_path_ if ext else os.path.splitext(file_path_)[0] + + +def construct_file_path(dir, file_path, ext=''): + ''' + args: + dir: /path/to/dir + file_path /example_path/to/file.txt + ext = '.json' + return + /path/to/dir/file.json + ''' + return os.path.join( + dir, get_name(file_path, + True)) if ext == '' else os.path.join( + dir, get_name(file_path, + False)) + ext + + +def write_to_txt_(file_path, content): + with open(file_path, 'w') as f: + f.write(content) + + diff --git a/cope2n-ai-fi/common/utils/word_formation.py b/cope2n-ai-fi/common/utils/word_formation.py new file mode 100755 index 0000000..ed5b071 --- /dev/null +++ b/cope2n-ai-fi/common/utils/word_formation.py @@ -0,0 +1,599 @@ +from builtins import dict +from common.utils.global_variables import * + +MIN_IOU_HEIGHT = 0.7 +MIN_WIDTH_LINE_RATIO = 0.05 + + +class Word: + def __init__( + self, + text="", + image=None, + conf_detect=0.0, + conf_cls=0.0, + bndbox=None, + kie_label="", + ): + self.type = "word" + self.text = text + self.image = image + self.conf_detect = conf_detect + self.conf_cls = conf_cls + self.boundingbox = bndbox if bndbox is not None else [-1, -1, -1, -1]# [left, top,right,bot] coordinate of top-left and bottom-right point + self.word_id = 0 # id of word + self.word_group_id = 0 # id of word_group which instance belongs to + self.line_id = 0 # id of line which instance belongs to + self.paragraph_id = 0 # id of line which instance belongs to + self.kie_label = kie_label + + def invalid_size(self): + return (self.boundingbox[2] - self.boundingbox[0]) * ( + self.boundingbox[3] - self.boundingbox[1] + ) > 0 + + def is_special_word(self): + left, top, right, bottom = self.boundingbox + width, height = right - left, bottom - top + text = self.text + + if text is None: + return True + + if len(text) >= 7: + no_digits = sum(c.isdigit() for c in text) + return no_digits / len(text) >= 0.3 + + return False + + +class Word_group: + def __init__(self): + self.type = "word_group" + self.list_words = [] # dict of word instances + self.word_group_id = 0 # word group id + self.line_id = 0 # id of line which instance belongs to + self.paragraph_id = 0 # id of paragraph which instance belongs to + self.text = "" + self.boundingbox = [-1, -1, -1, -1] + self.kie_label = "" + + def add_word(self, word: Word): # add a word instance to the word_group + if word.text != "✪": + for w in self.list_words: + if word.word_id == w.word_id: + print("Word id collision") + return False + word.word_group_id = self.word_group_id # + word.line_id = self.line_id + word.paragraph_id = self.paragraph_id + self.list_words.append(word) + self.text += " " + word.text + if self.boundingbox == [-1, -1, -1, -1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [ + min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3]), + ] + return True + else: + return False + + def update_word_group_id(self, new_word_group_id): + self.word_group_id = new_word_group_id + for i in range(len(self.list_words)): + self.list_words[i].word_group_id = new_word_group_id + + def update_kie_label(self): + list_kie_label = [word.kie_label for word in self.list_words] + dict_kie = dict() + for label in list_kie_label: + if label not in dict_kie: + dict_kie[label] = 1 + else: + dict_kie[label] += 1 + max_value = max(list(dict_kie.values())) + list_keys = list(dict_kie.keys()) + list_values = list(dict_kie.values()) + self.kie_label = list_keys[list_values.index(max_value)] + + def update_text(self): # update text after changing positions of words in list word + text = "" + for word in self.list_words: + text += " " + word.text + self.text = text + + +class Line: + def __init__(self): + self.type = "line" + self.list_word_groups = [] # list of Word_group instances in the line + self.line_id = 0 # id of line in the paragraph + self.paragraph_id = 0 # id of paragraph which instance belongs to + self.text = "" + self.boundingbox = [-1, -1, -1, -1] + + def add_group(self, word_group: Word_group): # add a word_group instance + if word_group.list_words is not None: + for wg in self.list_word_groups: + if word_group.word_group_id == wg.word_group_id: + print("Word_group id collision") + return False + + self.list_word_groups.append(word_group) + self.text += word_group.text + word_group.paragraph_id = self.paragraph_id + word_group.line_id = self.line_id + + for i in range(len(word_group.list_words)): + word_group.list_words[ + i + ].paragraph_id = self.paragraph_id # set paragraph_id for word + word_group.list_words[i].line_id = self.line_id # set line_id for word + return True + return False + + def update_line_id(self, new_line_id): + self.line_id = new_line_id + for i in range(len(self.list_word_groups)): + self.list_word_groups[i].line_id = new_line_id + for j in range(len(self.list_word_groups[i].list_words)): + self.list_word_groups[i].list_words[j].line_id = new_line_id + + def merge_word(self, word): # word can be a Word instance or a Word_group instance + if word.text != "✪": + if self.boundingbox == [-1, -1, -1, -1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [ + min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3]), + ] + self.list_word_groups.append(word) + self.text += " " + word.text + return True + return False + + def __cal_ratio(self, top1, bottom1, top2, bottom2): + sorted_vals = sorted([top1, bottom1, top2, bottom2]) + intersection = sorted_vals[2] - sorted_vals[1] + min_height = min(bottom1 - top1, bottom2 - top2) + if min_height == 0: + return -1 + ratio = intersection / min_height + return ratio + + def __cal_ratio_height(self, top1, bottom1, top2, bottom2): + + height1, height2 = top1 - bottom1, top2 - bottom2 + ratio_height = float(max(height1, height2)) / float(min(height1, height2)) + return ratio_height + + def in_same_line(self, input_line, thresh=0.7): + # calculate iou in vertical direction + _, top1, _, bottom1 = self.boundingbox + _, top2, _, bottom2 = input_line.boundingbox + + ratio = self.__cal_ratio(top1, bottom1, top2, bottom2) + ratio_height = self.__cal_ratio_height(top1, bottom1, top2, bottom2) + + if ( + (top1 in range(top2, bottom2) or top2 in range(top1, bottom1)) + and ratio >= thresh + and (ratio_height < 2) + ): + return True + return False + + +class Paragraph: + def __init__(self, id=0, lines=None): + self.list_lines = ( + lines if lines is not None else [] + ) # list of all lines in the paragraph + self.paragraph_id = id # index of paragraph in the ist of paragraph + self.text = "" + self.boundingbox = [-1, -1, -1, -1] + + def add_line(self, line: Line): # add a line instance + if line.list_word_groups is not None: + for l in self.list_lines: + if line.line_id == l.line_id: + print("Line id collision") + return False + for i in range(len(line.list_word_groups)): + line.list_word_groups[ + i + ].paragraph_id = ( + self.paragraph_id + ) # set paragraph id for every word group in line + for j in range(len(line.list_word_groups[i].list_words)): + line.list_word_groups[i].list_words[ + j + ].paragraph_id = ( + self.paragraph_id + ) # set paragraph id for every word in word groups + line.paragraph_id = self.paragraph_id # set paragraph id for line + self.list_lines.append(line) # add line to paragraph + self.text += " " + line.text + return True + else: + return False + + def update_paragraph_id( + self, new_paragraph_id + ): # update new paragraph_id for all lines, word_groups, words inside paragraph + self.paragraph_id = new_paragraph_id + for i in range(len(self.list_lines)): + self.list_lines[ + i + ].paragraph_id = new_paragraph_id # set new paragraph_id for line + for j in range(len(self.list_lines[i].list_word_groups)): + self.list_lines[i].list_word_groups[ + j + ].paragraph_id = new_paragraph_id # set new paragraph_id for word_group + for k in range(len(self.list_lines[i].list_word_groups[j].list_words)): + self.list_lines[i].list_word_groups[j].list_words[ + k + ].paragraph_id = new_paragraph_id # set new paragraph id for word + return True + + +def resize_to_original( + boundingbox, scale +): # resize coordinates to match size of original image + left, top, right, bottom = boundingbox + left *= scale[1] + right *= scale[1] + top *= scale[0] + bottom *= scale[0] + return [left, top, right, bottom] + + +def check_iomin(word: Word, word_group: Word_group): + min_height = min( + word.boundingbox[3] - word.boundingbox[1], + word_group.boundingbox[3] - word_group.boundingbox[1], + ) + intersect = min(word.boundingbox[3], word_group.boundingbox[3]) - max( + word.boundingbox[1], word_group.boundingbox[1] + ) + if intersect / min_height > 0.7: + return True + return False + + +def prepare_line(words): + lines = [] + visited = [False] * len(words) + for id_word, word in enumerate(words): + if word.invalid_size() == 0: + continue + new_line = True + for i in range(len(lines)): + if ( + lines[i].in_same_line(word) and not visited[id_word] + ): # check if word is in the same line with lines[i] + lines[i].merge_word(word) + new_line = False + visited[id_word] = True + + if new_line == True: + new_line = Line() + new_line.merge_word(word) + lines.append(new_line) + + # print(len(lines)) + # sort line from top to bottom according top coordinate + lines.sort(key=lambda x: x.boundingbox[1]) + return lines + + +def __create_word_group(word, word_group_id): + new_word_group = Word_group() + new_word_group.word_group_id = word_group_id + new_word_group.add_word(word) + + return new_word_group + + +def __sort_line(line): + line.list_word_groups.sort( + key=lambda x: x.boundingbox[0] + ) # sort word in lines from left to right + + return line + + +def __merge_text_for_line(line): + line.text = "" + for word in line.list_word_groups: + line.text += " " + word.text + + return line + + +def __update_list_word_groups(line, word_group_id, word_id, line_width): + + old_list_word_group = line.list_word_groups + list_word_groups = [] + + inital_word_group = __create_word_group(old_list_word_group[0], word_group_id) + old_list_word_group[0].word_id = word_id + list_word_groups.append(inital_word_group) + word_group_id += 1 + word_id += 1 + + for word in old_list_word_group[1:]: + check_word_group = True + word.word_id = word_id + word_id += 1 + + if ( + (not list_word_groups[-1].text.endswith(":")) + and ( + (word.boundingbox[0] - list_word_groups[-1].boundingbox[2]) / line_width + < MIN_WIDTH_LINE_RATIO + ) + and check_iomin(word, list_word_groups[-1]) + ): + list_word_groups[-1].add_word(word) + check_word_group = False + + if check_word_group: + new_word_group = __create_word_group(word, word_group_id) + list_word_groups.append(new_word_group) + word_group_id += 1 + line.list_word_groups = list_word_groups + return line, word_group_id, word_id + + +def construct_word_groups_in_each_line(lines): + line_id = 0 + word_group_id = 0 + word_id = 0 + for i in range(len(lines)): + if len(lines[i].list_word_groups) == 0: + continue + + # left, top ,right, bottom + line_width = lines[i].boundingbox[2] - lines[i].boundingbox[0] # right - left + + lines[i] = __sort_line(lines[i]) + + # update text for lines after sorting + lines[i] = __merge_text_for_line(lines[i]) + + lines[i], word_group_id, word_id = __update_list_word_groups( + lines[i], word_group_id, word_id, line_width + ) + lines[i].update_line_id(line_id) + line_id += 1 + return lines + + +def words_to_lines(words, check_special_lines=True): # words is list of Word instance + # sort word by top + words.sort(key=lambda x: (x.boundingbox[1], x.boundingbox[0])) + number_of_word = len(words) + # print(number_of_word) + # sort list words to list lines, which have not contained word_group yet + lines = prepare_line(words) + + # construct word_groups in each line + lines = construct_word_groups_in_each_line(lines) + return lines, number_of_word + + +def near(word_group1: Word_group, word_group2: Word_group): + min_height = min( + word_group1.boundingbox[3] - word_group1.boundingbox[1], + word_group2.boundingbox[3] - word_group2.boundingbox[1], + ) + overlap = min(word_group1.boundingbox[3], word_group2.boundingbox[3]) - max( + word_group1.boundingbox[1], word_group2.boundingbox[1] + ) + + if overlap > 0: + return True + if abs(overlap / min_height) < 1.5: + print("near enough", abs(overlap / min_height), overlap, min_height) + return True + return False + + +def calculate_iou_and_near(wg1: Word_group, wg2: Word_group): + min_height = min( + wg1.boundingbox[3] - wg1.boundingbox[1], wg2.boundingbox[3] - wg2.boundingbox[1] + ) + overlap = min(wg1.boundingbox[3], wg2.boundingbox[3]) - max( + wg1.boundingbox[1], wg2.boundingbox[1] + ) + iou = overlap / min_height + distance = min( + abs(wg1.boundingbox[0] - wg2.boundingbox[2]), + abs(wg1.boundingbox[2] - wg2.boundingbox[0]), + ) + if iou > 0.7 and distance < 0.5 * (wg1.boundingboxp[2] - wg1.boundingbox[0]): + return True + return False + + +def construct_word_groups_to_kie_label(list_word_groups: list): + kie_dict = dict() + for wg in list_word_groups: + if wg.kie_label == "other": + continue + if wg.kie_label not in kie_dict: + kie_dict[wg.kie_label] = [wg] + else: + kie_dict[wg.kie_label].append(wg) + + new_dict = dict() + for key, value in kie_dict.items(): + if len(value) == 1: + new_dict[key] = value + continue + + value.sort(key=lambda x: x.boundingbox[1]) + new_dict[key] = value + return new_dict + + +def invoice_construct_word_groups_to_kie_label(list_word_groups: list): + kie_dict = dict() + + for wg in list_word_groups: + if wg.kie_label == "other": + continue + if wg.kie_label not in kie_dict: + kie_dict[wg.kie_label] = [wg] + else: + kie_dict[wg.kie_label].append(wg) + + return kie_dict + + +def postprocess_total_value(kie_dict): + if "total_in_words_value" not in kie_dict: + return kie_dict + + for k, value in kie_dict.items(): + if k == "total_in_words_value": + continue + l = [] + for v in value: + if v.boundingbox[3] <= kie_dict["total_in_words_value"][0].boundingbox[3]: + l.append(v) + + if len(l) != 0: + kie_dict[k] = l + + return kie_dict + + +def postprocess_tax_code_value(kie_dict): + if "buyer_tax_code_value" in kie_dict or "seller_tax_code_value" not in kie_dict: + return kie_dict + + kie_dict["buyer_tax_code_value"] = [] + for v in kie_dict["seller_tax_code_value"]: + if "buyer_name_key" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_key"][0]) + ): + kie_dict["buyer_tax_code_value"].append(v) + continue + + if "buyer_name_value" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_value"][0]) + ): + kie_dict["buyer_tax_code_value"].append(v) + continue + + if "buyer_address_value" in kie_dict and near( + kie_dict["buyer_address_value"][0], v + ): + kie_dict["buyer_tax_code_value"].append(v) + return kie_dict + + +def postprocess_tax_code_key(kie_dict): + if "buyer_tax_code_key" in kie_dict or "seller_tax_code_key" not in kie_dict: + return kie_dict + kie_dict["buyer_tax_code_key"] = [] + for v in kie_dict["seller_tax_code_key"]: + if "buyer_name_key" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_key"][0]) + ): + kie_dict["buyer_tax_code_key"].append(v) + continue + + if "buyer_name_value" in kie_dict and ( + v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] + or near(v, kie_dict["buyer_name_value"][0]) + ): + kie_dict["buyer_tax_code_key"].append(v) + continue + + if "buyer_address_value" in kie_dict and near( + kie_dict["buyer_address_value"][0], v + ): + kie_dict["buyer_tax_code_key"].append(v) + + return kie_dict + + +def invoice_postprocess(kie_dict: dict): + # all keys or values which are below total_in_words_value will be thrown away + kie_dict = postprocess_total_value(kie_dict) + kie_dict = postprocess_tax_code_value(kie_dict) + kie_dict = postprocess_tax_code_key(kie_dict) + return kie_dict + + +def throw_overlapping_words(list_words): + new_list = [list_words[0]] + for word in list_words: + overlap = False + area = (word.boundingbox[2] - word.boundingbox[0]) * ( + word.boundingbox[3] - word.boundingbox[1] + ) + for word2 in new_list: + area2 = (word2.boundingbox[2] - word2.boundingbox[0]) * ( + word2.boundingbox[3] - word2.boundingbox[1] + ) + xmin_intersect = max(word.boundingbox[0], word2.boundingbox[0]) + xmax_intersect = min(word.boundingbox[2], word2.boundingbox[2]) + ymin_intersect = max(word.boundingbox[1], word2.boundingbox[1]) + ymax_intersect = min(word.boundingbox[3], word2.boundingbox[3]) + if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: + continue + + area_intersect = (xmax_intersect - xmin_intersect) * ( + ymax_intersect - ymin_intersect + ) + if area_intersect / area > 0.7 or area_intersect / area2 > 0.7: + overlap = True + if overlap == False: + new_list.append(word) + return new_list + + +class Box: + def __init__(self, xmin=0, ymin=0, xmax=0, ymax=0, label="", kie_label=""): + self.xmax = xmax + self.ymax = ymax + self.xmin = xmin + self.ymin = ymin + self.label = label + self.kie_label = kie_label + + +def check_iou(box1: Word, box2: Box, threshold=0.9): + area1 = (box1.boundingbox[2] - box1.boundingbox[0]) * ( + box1.boundingbox[3] - box1.boundingbox[1] + ) + area2 = (box2.xmax - box2.xmin) * (box2.ymax - box2.ymin) + xmin_intersect = max(box1.boundingbox[0], box2.xmin) + ymin_intersect = max(box1.boundingbox[1], box2.ymin) + xmax_intersect = min(box1.boundingbox[2], box2.xmax) + ymax_intersect = min(box1.boundingbox[3], box2.ymax) + if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: + area_intersect = 0 + else: + area_intersect = (xmax_intersect - xmin_intersect) * ( + ymax_intersect - ymin_intersect + ) + union = area1 + area2 - area_intersect + iou = area_intersect / union + if iou > threshold: + return True + return False diff --git a/cope2n-ai-fi/common/utils_invoice/load_model.py b/cope2n-ai-fi/common/utils_invoice/load_model.py new file mode 100755 index 0000000..915a2f3 --- /dev/null +++ b/cope2n-ai-fi/common/utils_invoice/load_model.py @@ -0,0 +1,61 @@ +from torch import nn +import torch +from transformers import ( + LayoutXLMTokenizer, + LayoutLMv2FeatureExtractor, + LayoutXLMProcessor, + LayoutLMv2ForTokenClassification, +) + + +class PositionalEncoding(nn.Module): + """Positional encoding.""" + + def __init__(self, num_hiddens, max_len=10000): + super(PositionalEncoding, self).__init__() + # Create a long enough `P` + self.num_hiddens = num_hiddens + + def forward(self, inputs): + max_len = inputs.shape[1] + P = torch.zeros((1, max_len, self.num_hiddens)) + X = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1) / torch.pow( + 10000, + torch.arange(0, self.num_hiddens, 2, dtype=torch.float32) + / self.num_hiddens, + ) + P[:, :, 0::2] = torch.sin(X) + P[:, :, 1::2] = torch.cos(X) + return P.to(inputs.device) + + +def load_layoutlmv2_custom_model( + weight_dir: str, tokenizer_dir: str, max_seq_len: int, classes: list +): + + model, processor = load_layoutlmv2(tokenizer_dir, weight_dir, max_seq_len, classes) + # fix for longer lenght + model.layoutlmv2.embeddings.position_embeddings = PositionalEncoding( + num_hiddens=768, max_len=max_seq_len + ) + model.layoutlmv2.embeddings.max_position_embeddings = max_seq_len + model.config.max_position_embeddings = max_seq_len + model.layoutlmv2.embeddings.register_buffer( + "position_ids", torch.arange(max_seq_len).expand((1, -1)) + ) + + return model, processor + + +def load_layoutlmv2( + weight_dir: str, tokenizer_dir: str, max_seq_len: int, classes: list +): + tokenizer = LayoutXLMTokenizer.from_pretrained( + pretrained_model_name_or_path=tokenizer_dir, model_max_length=max_seq_len + ) + feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) + processor = LayoutXLMProcessor(feature_extractor, tokenizer) + model = LayoutLMv2ForTokenClassification.from_pretrained( + weight_dir, num_labels=len(classes) + ) + return model, processor diff --git a/cope2n-ai-fi/common/utils_invoice/run_ocr.py b/cope2n-ai-fi/common/utils_invoice/run_ocr.py new file mode 100755 index 0000000..01b3df9 --- /dev/null +++ b/cope2n-ai-fi/common/utils_invoice/run_ocr.py @@ -0,0 +1,13 @@ +from ..utils.ocr_yolox import OcrEngineForYoloX_Invoice + + +det_ckpt = "yolox-s-general-text-pretrain-20221226" +cls_ckpt = "satrn-lite-general-pretrain-20230106" + +ocr_engine = OcrEngineForYoloX_Invoice(det_ckpt, cls_ckpt) + + +def ocr_predict(image_url): + + bboxes, texts = ocr_engine.run_image(image_url) + return bboxes, texts \ No newline at end of file diff --git a/cope2n-ai-fi/common/utils_kvu/split_docs.py b/cope2n-ai-fi/common/utils_kvu/split_docs.py new file mode 100755 index 0000000..cf0d16f --- /dev/null +++ b/cope2n-ai-fi/common/utils_kvu/split_docs.py @@ -0,0 +1,149 @@ +import os +import glob +import json +from tqdm import tqdm + +def longestCommonSubsequence(text1: str, text2: str) -> int: + # https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis + dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)] + for i, c in enumerate(text1): + for j, d in enumerate(text2): + dp[i + 1][j + 1] = 1 + \ + dp[i][j] if c == d else max(dp[i][j + 1], dp[i + 1][j]) + return dp[-1][-1] + +def write_to_json(file_path, content): + with open(file_path, mode="w", encoding="utf8") as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +def check_label_exists(array, target_label): + for obj in array: + if obj["label"] == target_label: + return True # Label exists in the array + return False # Label does not exist in the array + +def merged_kvu_outputs(loutputs: list) -> dict: + compiled = [] + for output_model in loutputs: + for field in output_model: + if field['value'] != "" and not check_label_exists(compiled, field['label']): + element = { + 'label': field['label'], + 'value': field['value'], + } + compiled.append(element) + elif field['label'] == 'table' and check_label_exists(compiled, "table"): + for index, obj in enumerate(compiled): + if obj['label'] == 'table' and len(field['value']) > 0: + compiled[index]['value'].append(field['value']) + return compiled + + +def split_docs(doc_data: list, threshold: float=0.6) -> list: + num_pages = len(doc_data) + outputs = [] + kvu_content = [] + doc_data = sorted(doc_data, key=lambda x: int(x['page_number'])) + for data in doc_data: + page_id = int(data['page_number']) + doc_type = data['document_type'] + doc_class = data['document_class'] + fields = data['fields'] + if page_id == 0: + prev_title = doc_type + start_page_id = page_id + prev_class = doc_class + curr_title = doc_type if doc_type != "unknown" else prev_title + curr_class = doc_class if doc_class != "unknown" else "other" + kvu_content.append(fields) + similarity_score = longestCommonSubsequence(curr_title, prev_title) / len(prev_title) + if similarity_score < threshold: + end_page_id = page_id - 1 + outputs.append({ + "doc_type": f"({prev_class}) {prev_title}" if prev_class != "other" else prev_title, + "start_page": start_page_id, + "end_page": end_page_id, + "content": merged_kvu_outputs(kvu_content[:-1]) + }) + prev_title = curr_title + prev_class = curr_class + start_page_id = page_id + kvu_content = kvu_content[-1:] + if page_id == num_pages - 1: # end_page + outputs.append({ + "doc_type": f"({prev_class}) {prev_title}" if prev_class != "other" else prev_title, + "start_page": start_page_id, + "end_page": page_id, + "content": merged_kvu_outputs(kvu_content) + }) + elif page_id == num_pages - 1: # end_page + outputs.append({ + "doc_type": f"({prev_class}) {prev_title}" if prev_class != "other" else prev_title, + "start_page": start_page_id, + "end_page": page_id, + "content": merged_kvu_outputs(kvu_content) + }) + return outputs + + +def merge_sbt_output(loutputs): + # TODO: This function is too circumlocutory, need to refactor the whole flow + def dict_to_list_of_dict(the_dict): + output = [] + for k,v in the_dict.items(): + output.append({ + 'label': k, + 'value': v, + }) + return output + + merged_output = [] + combined_output = {"retailername": None, + "sold_to_party": None, + "purchase_date": [], + "imei_number": []} # place holder for the output + for output in loutputs: + fields = output['fields'] + if "doc_type" not in output: # Should not contain more than 1 page + for field in fields: + combined_output[field["label"]] = field["value"] + combined_output["imei_number"] = [combined_output["imei_number"]] + break + else: + if output['doc_type'] == "imei": + for field in fields: + if field["label"] == "imei_number": + combined_output[field["label"]].append(field["value"]) + if output['doc_type'] == "invoice": + for field in fields: + if field["label"] in ["retailername", "sold_to_party", "purchase_date"] : + if isinstance(combined_output[field["label"]], list): + if field["value"] is not None: + if isinstance(field["value"], list): + combined_output[field["label"]] += field["value"] + else: + combined_output[field["label"]].append(field["value"]) + else: + combined_output[field["label"]] = field["value"] + + merged_output.append({ + "doc_type": "sbt_document", + "start_page": 1, + "end_page": len(loutputs), + "content": dict_to_list_of_dict(combined_output) + }) + return merged_output + +if __name__ == "__main__": + threshold = 0.9 + json_path = "/home/sds/tuanlv/02-KVU/02-KVU_test/visualize/manulife_v2/json_outputs/HS_YCBT_No_IP_HMTD.json" + doc_data = read_json(json_path) + + outputs = split_docs(doc_data, threshold) + + write_to_json(os.path.join(os.path.dirname(json_path), "splited_doc.json"), outputs) \ No newline at end of file diff --git a/cope2n-ai-fi/common/utils_ocr/create_kie_labels.py b/cope2n-ai-fi/common/utils_ocr/create_kie_labels.py new file mode 100755 index 0000000..2199ff1 --- /dev/null +++ b/cope2n-ai-fi/common/utils_ocr/create_kie_labels.py @@ -0,0 +1,67 @@ +# %% +# from pathlib import Path # add Fiintrade path to import config, required to run main() +import sys + +# TODO: Why??? for what reason ??????????????? +sys.path.append(".") # add Fiintrade/ to path + + +from srcc.tools.utils import ( + load_kie_labels_yolo, + create_empty_kie_dict, + write_to_json_, + load_train_val_id_cards, +) +import glob +from OCRBase.config import config as cfg +import os +import pandas as pd + +sys.path.append("/home/sds/hoangmd/TokenClassification") # TODO: Why there are bunch of absolute path here +from src.experiments.word_formation import * +from process_label import * + +KIE_LABEL_DIR = "data/label/207/kie" +KIE_LABEL_LINE_PATH = "/home/sds/hungbnt/KIE_pretrained/data/label/207/json" # TODO: Absolute path ????? + +# %% + + +def create_kie_dict(list_words): + kie_dict = create_empty_kie_dict() + list_words = throw_overlapping_words(list_words) + for word in list_words: + if word.kie_label in kie_dict: + kie_dict[word.kie_label].append(word) + word.text = word.text.strip() + for kie_label in kie_dict: + list_lines, _ = words_to_lines(kie_dict[kie_label]) + kie_dict[kie_label] = "\n ".join([line.text.strip() for line in list_lines]) + return kie_dict + + +# %% + + +def main(): + label_paths = glob.glob(f"{KIE_LABEL_DIR}/*.txt") + for label_path in label_paths: + words, bboxes, kie_labels = load_kie_labels_yolo(label_path) + list_words = [] + for i, kie_label in enumerate(kie_labels): + list_words.append( + Word(text=words[i], bndbox=bboxes[i], kie_label=kie_label) + ) + + kie_dict = create_kie_dict(list_words) + kie_path = os.path.join( + KIE_LABEL_LINE_PATH, os.path.basename(label_path).replace(".txt", ".json") + ) + write_to_json_(kie_path, kie_dict) + + +# %% + + +if __name__ == "__main__": + main() diff --git a/cope2n-ai-fi/configs/config_id_dr/__init__.py b/cope2n-ai-fi/configs/config_id_dr/__init__.py new file mode 100755 index 0000000..dc8da6b --- /dev/null +++ b/cope2n-ai-fi/configs/config_id_dr/__init__.py @@ -0,0 +1,8 @@ +from ...common.configs.config import BASE_CONFIG, V2, V3, ID_CARD + +__mapping__ = { + "base": BASE_CONFIG, + "v2": V2, + "v3": V3, + "id_card": ID_CARD, +} diff --git a/cope2n-ai-fi/configs/config_id_dr/config.py b/cope2n-ai-fi/configs/config_id_dr/config.py new file mode 100755 index 0000000..d6853a9 --- /dev/null +++ b/cope2n-ai-fi/configs/config_id_dr/config.py @@ -0,0 +1,212 @@ +# GLOBAL VARS +DEVICE = "cuda:0" +IGNORE_KIE_LABEL = "others" +KIE_LABELS = [ + "id", + "name", + "dob", + "home", + "add", + "sex", + "nat", + "exp", + "eth", + "rel", + "date", + "org", + IGNORE_KIE_LABEL, + "rank", +] +SEED = 42 +NAME_LABEL = "microsoft/layoutxlm-base" +########################################## +BASE_CONFIG = { + "global": { + "device": DEVICE, + "kie_labels": KIE_LABELS, + }, + "data": { + "custom": True, + "path": "src/custom/load_data.py", + "method": "load_data", + "train_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/synthesis_for_train/", + "val_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/SDV_Meddoc_BirthCert/", + "max_seq_len": 512, + "batch_size": 8, + "pretrained_processor": NAME_LABEL, + "kie_labels": KIE_LABELS, + "device": DEVICE, + }, + "model": { + "custom": True, + "path": "src/custom/load_model.py", + "method": "load_model", + "pretrained_model": NAME_LABEL, + "kie_labels": KIE_LABELS, + "device": DEVICE, + }, + "optimizer": { + "custom": True, + "path": "src/custom/load_optimizer.py", + "method": "load_optimizer", + "lr": 5e-6, + "weight_decay": 0, + "betas": (0.9, 0.999), + }, + "trainer": { + "custom": True, + "path": "src/custom/load_trainer.py", + "method": "load_trainer", + "kie_labels": KIE_LABELS, + "save_dir": "weights", + "n_epoches": 100, + }, +} + +ID_CARD = BASE_CONFIG +ID_CARD["data"] = { + "custom": True, + "path": "src/custom/load_data_id_card.py", + "method": "load_data", + "train_path": "/home/sds/hungbnt/KIE_pretrained/data/207/idcard_cmnd_8-9-2022", + "label_path": "/home/sds/hungbnt/KIE_pretrained/data/207/label/", + "max_seq_len": 512, + "batch_size": 8, + "pretrained_processor": NAME_LABEL, + "kie_labels": KIE_LABELS, + "device": DEVICE, +} + + +# GLOBAL VARS +DEVICE = "cuda:1" +# DEVICE = "cpu" +# DEVICE = "cpu" # for debugging https://stackoverflow.com/questions/51691563/cuda-runtime-error-59-device-side-assert-triggered +# DEVICE = "cpu" +# KIE_LABELS = ['gen', 'nk', 'nv', 'dobk', 'dobv', 'other'] +IGNORE_KIE_LABEL = 'others' +# KIE_LABELS = ['id', 'name', 'dob', 'home', 'add', 'sex', 'nat', 'exp', 'eth', 'rel', 'date', 'org', IGNORE_KIE_LABEL] +# KIE_WEIGHTS = "/home/sds/hungbnt/KIE_pretrained/weights/ID_CARD_145_train_300_val_0.02_char_0.06_word" +# TODO: current yield index error if pass to gplx['data]['kie_label] (maybe mismatch with somewhere else) => fix this so that kie_label in gplx can be made global +KIE_LABELS = ['id', 'name', 'dob', 'home', 'add', 'sex', 'nat', + 'exp', 'eth', 'rel', 'date', 'org', IGNORE_KIE_LABEL, 'rank'] +KIE_WEIGHTS = 'weights/driver_license' +SEED = 42 + +########################################## +BASE_CONFIG = { + 'global': { + 'device': DEVICE, + 'kie_labels': KIE_LABELS, + }, + "data": { + "custom": True, + "path": "src/custom/load_data.py", + "method": "load_data", + "train_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/synthesis_for_train/", + "val_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/SDV_Meddoc_BirthCert/", + # "size": 320, + "max_seq_len": 512, + "batch_size": 8, + # "workers": 10, + 'pretrained_processor': 'microsoft/layoutxlm-base', + 'kie_labels': KIE_LABELS, + 'device': DEVICE, + }, + + "model": { + "custom": True, + "path": "src/custom/load_model.py", + "method": "load_model", + "pretrained_model": 'microsoft/layoutxlm-base', + 'kie_labels': KIE_LABELS, + 'device': DEVICE, + }, + + "optimizer": { + "custom": True, + "path": "src/custom/load_optimizer.py", + "method": "load_optimizer", + "lr": 5e-6, + "weight_decay": 0, # default = 0 + "betas": (0.9, 0.999), # beta1 in transformer, default = 0.9 + }, + + "trainer": { + "custom": True, + "path": "src/custom/load_trainer.py", + "method": "load_trainer", + "kie_labels": KIE_LABELS, + "save_dir": 'weights', + "n_epoches": 100, + }, +} + +V2 = BASE_CONFIG +# V2['data'] = { +# "custom": True, +# "pretrained_model": 'microsoft/layoutxlm-base', +# 'kie_labels': KIE_LABELS, +# 'device': DEVICE, +# } + +V3 = BASE_CONFIG +# V3["data"] = { +# "custom": True, +# "path": "src/custom/load_data_v3.py", +# "method": "load_data", +# "train_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/synthesis_for_train/", +# "val_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/SDV_Meddoc_BirthCert/", +# # "size": 320, +# "max_seq_len": 512, +# "batch_size": 8, +# # "workers": 10, +# 'pretrained_processor': "microsoft/layoutlmv3-base", +# 'kie_labels': KIE_LABELS, +# 'device': DEVICE, +# } +# V3['model'] = {; +# "custom": False, +# 'name': 'layoutlm_v3', +# "pretrained_model": 'microsoft/layoutlmv3-base', +# 'kie_labels': KIE_LABELS, +# 'device': DEVICE, +# } + +ID_CARD = BASE_CONFIG +ID_CARD['data'] = { + "custom": True, + "path": "src/custom/load_data_id_card.py", + "method": "load_data", + "train_path": "/home/sds/hungbnt/KIE_pretrained/data/207/idcard_cmnd_8-9-2022", + "label_path": "/home/sds/hungbnt/KIE_pretrained/data/207/label/", + # "size": 320, + "max_seq_len": 512, + "batch_size": 8, + # "workers": 10, + 'pretrained_processor': 'microsoft/layoutxlm-base', + 'kie_labels': KIE_LABELS, + 'device': DEVICE, +} + + +GPLX = BASE_CONFIG +GPLX['data'] = { + "custom": True, + "path": "srcc/custom/load_data_gplx.py", + "method": "load_data", + "train_path": "/home/sds/hungbnt/KIE_pretrained/data/GPLX/train/crop_blx_10_10_2022", + "val_path": "/home/sds/hungbnt/KIE_pretrained/data/GPLX/val/crop_blx_5_10_2022", + "train_label_path": "/home/sds/hungbnt/KIE_pretrained/data/label/GPLX/kie/train", + "val_label_path": "/home/sds/hungbnt/KIE_pretrained/data/label/GPLX/kie/val", + # "size": 320, + "max_seq_len": 512, + "batch_size": 8, + # "workers": 10, + 'pretrained_processor': 'microsoft/layoutxlm-base', + 'kie_labels': KIE_LABELS, + 'device': DEVICE, +} + + + diff --git a/cope2n-ai-fi/configs/config_invoice/layoutxlm_base_invoice.py b/cope2n-ai-fi/configs/config_invoice/layoutxlm_base_invoice.py new file mode 100755 index 0000000..292d85c --- /dev/null +++ b/cope2n-ai-fi/configs/config_invoice/layoutxlm_base_invoice.py @@ -0,0 +1,67 @@ +CONFIF_PATH = __file__ +TRAIN_DIR = "/home/sds/hoanglv/Projects/TokenClassification_invoice/DATA/train" +TEST_DIR = "/home/sds/hoanglv/Projects/TokenClassification_invoice/DATA/test" +TOKENIZER_DIR = "Kie_Hoanglv/model/layoutxlm-base-tokenizer" +TOKENIZER_NAME = "microsoft/layoutxlm-base" +MODEL_WEIGHT = "microsoft/layoutxlm-base" +# pretrained model hyperparameter +MAX_SEQ_LENGTH = 512 +IMG_SIZE = 224 # default + +VN_list_char = "aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ0123456789!#$%&()*+,-./:;<=>?@[\]^_`{|}~" + +DEVICE = "cuda:0" +SAVE_DIR = "runs/layoutxlm-base-17-10-2022-maxwords150_samplingv2" +BATCH_SIZE = 8 +NUM_WORKER = 0 +EPOCHS = 100 +SAVE_INTERVAL = 1000 +LR_RATE = 5e-6 # ori: 5e-5 + +# infer +MAX_N_WORDS = 150 +TRAINED_DIR = "Kie_Hoanglv/model/layoutxlm-base-17-10-2022-maxwords150_samplingv2/last" +PRED_DIR = "/home/sds/hoanglv/Projects/TokenClassification_invoice/runs/infer/kie_e2e_pred_17-10-2022-maxwords150_samplingv2_rm_dup_boxes_test" +VISUALIZE_DIR = PRED_DIR + "/visualize" + +KIE_LABELS = [ + # id invoice + "no_key", + "no_value", + "form_key", + "form_value", + "serial_key", + "serial_value", + "date", + # seller info + "seller_company_name_key", + "seller_company_name_value", + "seller_tax_code_key", + "seller_tax_code_value", + "seller_address_value", + "seller_address_key", + "seller_mobile_key", + "seller_mobile_value", + # buyer info + "buyer_name_key", + "buyer_name_value", + "buyer_company_name_value", + "buyer_company_name_key", + "buyer_tax_code_key", + "buyer_tax_code_value", + "buyer_address_key", + "buyer_address_value", + "buyer_mobile_key", + "buyer_mobile_value", + # money info + "VAT_amount_key", + "VAT_amount_value", + "total_key", + "total_value", + "total_in_words_key", + "total_in_words_value", + "other", +] + + +SKIP_LABEL_EVAL = ["buyer_mobile_value"] diff --git a/cope2n-ai-fi/configs/config_ocr/__init__.py b/cope2n-ai-fi/configs/config_ocr/__init__.py new file mode 100755 index 0000000..9f5a9de --- /dev/null +++ b/cope2n-ai-fi/configs/config_ocr/__init__.py @@ -0,0 +1,8 @@ +from .config import BASE_CONFIG, V2, V3, ID_CARD + +__mapping__ = { + "base": BASE_CONFIG, + "v2": V2, + "v3": V3, + "id_card": ID_CARD, +} diff --git a/cope2n-ai-fi/configs/config_ocr/config.py b/cope2n-ai-fi/configs/config_ocr/config.py new file mode 100755 index 0000000..a29dfc8 --- /dev/null +++ b/cope2n-ai-fi/configs/config_ocr/config.py @@ -0,0 +1,79 @@ +# GLOBAL VARS +DEVICE = "cuda:0" +IGNORE_KIE_LABEL = "others" +KIE_LABELS = [ + "id", + "name", + "dob", + "home", + "add", + "sex", + "nat", + "exp", + "eth", + "rel", + "date", + "org", + IGNORE_KIE_LABEL, +] + +SEED = 42 +NAME_LABEL = "microsoft/layoutxlm-base" + +########################################## +BASE_CONFIG = { + "global": { + "device": DEVICE, + "kie_labels": KIE_LABELS, + }, + "data": { + "custom": True, + "path": "src/custom/load_data.py", + "method": "load_data", + "train_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/synthesis_for_train/", + "val_path": "/home/sds/hoangmd/TokenClassification_copy/giaykhaisinh/SDV_Meddoc_BirthCert/", + "max_seq_len": 512, + "batch_size": 8, + "pretrained_processor": NAME_LABEL , + "kie_labels": KIE_LABELS, + "device": DEVICE, + }, + "model": { + "custom": True, + "path": "src/custom/load_model.py", + "method": "load_model", + "pretrained_model": NAME_LABEL, + "kie_labels": KIE_LABELS, + "device": DEVICE, + }, + "optimizer": { + "custom": True, + "path": "src/custom/load_optimizer.py", + "method": "load_optimizer", + "lr": 5e-6, + "weight_decay": 0, + "betas": (0.9, 0.999), + }, + "trainer": { + "custom": True, + "path": "src/custom/load_trainer.py", + "method": "load_trainer", + "kie_labels": KIE_LABELS, + "save_dir": "weights", + "n_epoches": 100, + }, +} + +ID_CARD = BASE_CONFIG +ID_CARD["data"] = { + "custom": True, + "path": "src/custom/load_data_id_card.py", + "method": "load_data", + "train_path": "/home/sds/hungbnt/KIE_pretrained/data/207/idcard_cmnd_8-9-2022", + "label_path": "/home/sds/hungbnt/KIE_pretrained/data/207/label/", + "max_seq_len": 512, + "batch_size": 8, + "pretrained_processor": NAME_LABEL, + "kie_labels": KIE_LABELS, + "device": DEVICE, +} diff --git a/cope2n-ai-fi/configs/default_env.py b/cope2n-ai-fi/configs/default_env.py new file mode 100644 index 0000000..f3fe0b6 --- /dev/null +++ b/cope2n-ai-fi/configs/default_env.py @@ -0,0 +1,3 @@ +CELERY_BROKER = "" +SAP_KIE_MODEL = "" +FI_KIE_MODEL = "" diff --git a/cope2n-ai-fi/configs/manulife/__init__.py b/cope2n-ai-fi/configs/manulife/__init__.py new file mode 100644 index 0000000..624fa5c --- /dev/null +++ b/cope2n-ai-fi/configs/manulife/__init__.py @@ -0,0 +1,3 @@ +from .configs import device +from .configs import ocr_engine as ocr_cfg +from .configs import kvu_model as kvu_cfg \ No newline at end of file diff --git a/cope2n-ai-fi/configs/manulife/configs.py b/cope2n-ai-fi/configs/manulife/configs.py new file mode 100644 index 0000000..4e73b92 --- /dev/null +++ b/cope2n-ai-fi/configs/manulife/configs.py @@ -0,0 +1,35 @@ +device = "cuda:0" +ocr_engine = { + "detector": { + "version": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsvtd/epoch_100_params.pth", + "rotator_version": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsvtd/best_bbox_mAP_epoch_30_lite.pth", + "device": device + }, + "recognizer": { + "version": "/workspace/cope2n-ai-fi/weights/models/sdsvtr/hub/jxqhbem4to.pth", + "device": device + }, + "deskew": { + "enable": True, + "text_detector": { + "config": "/workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml", + "weight": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsv_dewarp/ch_PP-OCRv3_det_infer" + }, + "text_cls": { + "config": "/workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml", + "weight": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsv_dewarp/ch_ppocr_mobile_v2.0_cls_infer" + }, + "device": device + } +} + +kvu_model = { + "device": device, + "mode": 3, + "option": "manulife", + "model": { + "pretrained_model_path": "/workspace/cope2n-ai-fi/weights/layoutxlm-base", + "config": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding-20231024-125646_manulife2/base.yaml", + "checkpoint": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding-20231024-125646_manulife2/checkpoints/best_model.pth" + } +} \ No newline at end of file diff --git a/cope2n-ai-fi/configs/sdsap_sbt/__init__.py b/cope2n-ai-fi/configs/sdsap_sbt/__init__.py new file mode 100644 index 0000000..624fa5c --- /dev/null +++ b/cope2n-ai-fi/configs/sdsap_sbt/__init__.py @@ -0,0 +1,3 @@ +from .configs import device +from .configs import ocr_engine as ocr_cfg +from .configs import kvu_model as kvu_cfg \ No newline at end of file diff --git a/cope2n-ai-fi/configs/sdsap_sbt/configs.py b/cope2n-ai-fi/configs/sdsap_sbt/configs.py new file mode 100644 index 0000000..e23e9c1 --- /dev/null +++ b/cope2n-ai-fi/configs/sdsap_sbt/configs.py @@ -0,0 +1,35 @@ +device = "cuda:0" +ocr_engine = { + "detector": { + "version": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsvtd/epoch_100_params.pth", + "rotator_version": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsvtd/best_bbox_mAP_epoch_30_lite.pth", + "device": device + }, + "recognizer": { + "version": "/workspace/cope2n-ai-fi/weights/models/sdsvtr/hub/jxqhbem4to.pth", + "device": device + }, + "deskew": { + "enable": True, + "text_detector": { + "config": "/workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml", + "weight": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsv_dewarp/ch_PP-OCRv3_det_infer" + }, + "text_cls": { + "config": "/workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml", + "weight": "/workspace/cope2n-ai-fi/weights/models/ocr_engine/sdsv_dewarp/ch_ppocr_mobile_v2.0_cls_infer" + }, + "device": device + } +} + +kvu_model = { + "device": device, + "mode": 4, + "option": "sbt_v2", + "model": { + "pretrained_model_path": "/workspace/cope2n-ai-fi/weights/layoutxlm-base", + "config": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding_for_sbt-20231118-175013/base.yaml", + "checkpoint": "/workspace/cope2n-ai-fi/weights/models/sdsvkvu/key_value_understanding_for_sbt-20231118-175013/checkpoints/best_model.pth" + } +} \ No newline at end of file diff --git a/cope2n-ai-fi/docker-compose.yaml b/cope2n-ai-fi/docker-compose.yaml new file mode 100755 index 0000000..7a2590e --- /dev/null +++ b/cope2n-ai-fi/docker-compose.yaml @@ -0,0 +1,48 @@ +services: + cope2n-fi: + build: + context: . + shm_size: 10gb + dockerfile: Dockerfile + shm_size: 10gb + image: tuanlv/cope2n-ai-fi + container_name: "tuanlv-cope2n-ai-fi-dev" + network_mode: "host" + privileged: true + volumes: + - /mnt/hdd4T/OCR/tuanlv/05-copen-ai/cope2n-ai-fi:/workspace/cope2n-ai-fi # for dev container only + - /mnt/hdd2T/dxtan/TannedCung/OCR/cope2n-api:/workspace/cope2n-api + - /mnt/hdd2T/dxtan/TannedCung/OCR/cope2n-fe:/workspace/cope2n-fe + # deploy: + # resources: + # reservations: + # devices: + # - driver: nvidia + # count: 1 + # capabilities: [gpu] + command: bash -c "tail -f > /dev/null" + + # train_component: + # build: + # context: . + # shm_size: 10gb + # args: + # - NODE_ENV=local + # dockerfile: Dockerfile + # shm_size: 10gb + # image: tannedcung/kubeflow-text-recognition + # container_name: "TannedCung-kubeflow-TextRecognition-Train" + # network_mode: "host" + # privileged: true + # depends_on: + # data_preparation_component: + # condition: service_completed_successfully + # volumes: + # # - /mnt/hdd2T/dxtan/TannedCung/VI/vi-vision-inspection-kubeflow/components/text_recognition:/workspace + # - /mnt/ssd500/datnt/mmocr/logs/satrn_lite_2023-04-13_fwd_finetuned:/weights/ + # - /mnt/hdd2T/dxtan/TannedCung/OCR/TextRecognition/test_input/:/test_input/ + # - /mnt/hdd2T/dxtan/TannedCung/OCR/TextRecognition/train_output/:/train_output/ + # - /mnt/hdd2T/dxtan/TannedCung/Data/:/Data + # - /mnt/hdd2T/dxtan/TannedCung/VI/vi-vision-inspection-kubeflow/components/text_recognition/configs:/configs + # command: bash -c "python /workspace/tools/train.py --config=/workspace/configs/satrn_lite.py --load_from=/weights/textrecog_fwd_tuned_20230413_params.pth --gpu_id=1 --img_path_prefix=/Data --vimlops_token=123 --total_epochs=5 --batch_size=32 --work_dir=/train_output" + # command: bash -c "tail -f > /dev/null" \ No newline at end of file diff --git a/cope2n-ai-fi/dockerfile_old b/cope2n-ai-fi/dockerfile_old new file mode 100755 index 0000000..926fc35 --- /dev/null +++ b/cope2n-ai-fi/dockerfile_old @@ -0,0 +1,27 @@ +FROM nvidia/cuda:11.8.0-devel-ubuntu22.04 + +ARG UID=1000 +ARG GID=1000 +ARG USERNAME=container-user +RUN groupadd --gid ${GID} ${USERNAME} \ + && useradd --uid ${UID} --gid ${GID} -m ${USERNAME} \ + && apt-get update \ + && apt-get install -y sudo \ + && apt install -y python3-pip ffmpeg libsm6 libxext6 \ + && apt install git -y \ + && echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${USERNAME} \ + && chmod 0440 /etc/sudoers.d/${USERNAME} + +USER ${UID} +ADD --chown=${UID}:${GID} . /cope2n-ai + +WORKDIR /cope2n-ai +RUN pip3 install -r requirements.txt --no-cache-dir +RUN python3 -m pip install -e detectron2 +RUN cd /cope2n-ai/sdsvtd && pip install -v -e . +RUN cd /cope2n-ai/sdsvtr && pip install -v -e . +RUN pip install -U openmim && mim install mmcv-full==1.7.0 +RUN cd /cope2n-ai +RUN export PYTHONPATH="." + +CMD ["sh", "run.sh"] \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/setting.yml b/cope2n-ai-fi/modules/TemplateMatching/setting.yml new file mode 100755 index 0000000..d8c0933 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/setting.yml @@ -0,0 +1,21 @@ +text_detection: + setting: TemplateMatching/textdetection/setting.yml + +text_recognition: + setting: TemplateMatching/textrecognition/setting.yml + +document_classification: + setting: TemplateMatching/documentclassification/setting.yml + +template_based_extraction: + setting: TemplateMatching/templatebasedextraction/setting.yml + +id_card_detection: + setting: TemplateMatching/idcarddetection/setting.yml + +checkbox_detection: + setting: TemplateMatching/checkboxdetection/setting.yml + + +deploy: + port: 7979 \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/src/ocr_master.py b/cope2n-ai-fi/modules/TemplateMatching/src/ocr_master.py new file mode 100755 index 0000000..54aa2ae --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/src/ocr_master.py @@ -0,0 +1,87 @@ +import requests +import yaml +from time import time +import numpy as np +import re + +from TemplateMatching.templatebasedextraction.src.serve_model import Predictor +from TemplateMatching.textdetection.serve_model import Predictor as TextDetector +from TemplateMatching.textrecognition.src.serve_model import Predictor as TextRecognizer + +class Extractor: + def __init__(self): + with open("./TemplateMatching/setting.yml") as f: + self.setting = yaml.safe_load(f) + self.predictor = Predictor(self.setting["template_based_extraction"]["setting"]) + self.text_detector = TextDetector(self.setting["text_detection"]["setting"]) + self.text_recognizer = TextRecognizer( + self.setting["text_recognition"]["setting"] + ) + + def _format_output(self, document): + result = dict() + for field, values in document.items(): + print(values["value"]) + if "✪" in values["value"]: + values = values["value"].replace("✪", " ") + result[field] = values + else: + values = values["value"] + result[field] = values + return result + + def _extract_idcard_info(self, images): + id_card_crops = self.idcard_detector(np.array(images)) + processed_images = [] + for i in range(len(id_card_crops)): + aligned_img = id_card_crops[i] + if aligned_img is not None: + processed_images.append(aligned_img) + else: + processed_images.append(images[i]) + return processed_images + + def _extract_id_no(self, doc): + page = doc["page_data"][0] + content = " ".join(page["contents"]) + result1 = re.findall("[0-9]{12}", content) + if len(result1) == 0: + result2 = re.findall("[0-9]{9}", content) + if len(result2) == 0: + return None + return result2 + return result1[0] + + def image_alige(self, images, tmp_json): + template_image_dir = "/" + template_name = tmp_json["template_name"] + + image_aliged = self.predictor.align_image( + images[0], tmp_json, template_image_dir, template_name + ) + + return image_aliged + + def extract_information(self, image_aliged, tmp_json): + image_aligeds = [image_aliged] + batch_boxes = self.text_detector(image_aligeds) + cropped_images = [ + image_aliged[int(y1) : int(y2), int(x1) : int(x2)] + for x1, y1, x2, y2 in batch_boxes[0] + ] + texts = self.text_recognizer(cropped_images) + texts = [res for res in texts] + + doc_page = dict() + doc_page["boxes"] = batch_boxes + doc_page["contents"] = texts + doc_page["types"] = ["word"] * len(batch_boxes) + doc_page["image"] = image_aliged + + documents_with_info = self.predictor.template_based_extractor( + batch_boxes, texts, doc_page, tmp_json + ) + + result = self._format_output(documents_with_info) + + return result \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/.gitignore b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/.gitignore new file mode 100755 index 0000000..e6cb784 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/.gitignore @@ -0,0 +1,206 @@ +# Created by https://www.toptal.com/developers/gitignore/api/jupyternotebooks,visualstudiocode,ssh,python +# Edit at https://www.toptal.com/developers/gitignore?templates=jupyternotebooks,visualstudiocode,ssh,python + +### JupyterNotebooks ### +# gitignore template for Jupyter Notebooks +# website: http://jupyter.org/ + +.ipynb_checkpoints +*/.ipynb_checkpoints/* + +# IPython +profile_default/ +ipython_config.py + +# Remove previous ipynb_checkpoints +# git rm -r .ipynb_checkpoints/ + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + + +### SSH ### +**/.ssh/id_* +**/.ssh/*_id_* +**/.ssh/known_hosts + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/jupyternotebooks,visualstudiocode,ssh,python \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/setting.yml b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/setting.yml new file mode 100755 index 0000000..667c844 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/setting.yml @@ -0,0 +1,17 @@ +templates: + template_im_dir: /mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/templates + + config: + { + "min_match_count": 4, + "flann_based_matcher_config": {"algorithm": 0, "trees": 5}, + "matching_topk": 2, + "distance_threshold": 0.6, + "ransac_threshold": 5.0, + "valid_size_ratio_margin": 0.15, + "valid_area_threshold": 0.75, + "image_max_size": 1024, + "similar_triangle_threshold": 4, + "roi_to_template_box_ratio": 3.0, + "default_image_size": [1654, 2368] + } \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/line_parser.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/line_parser.py new file mode 100755 index 0000000..4782c5a --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/line_parser.py @@ -0,0 +1,236 @@ +TEMPLATE_BOXES = { + "POS01": { + "page_1": [ + { + "name": "field", + "type": "text", + "position": {"top": 1951, "left": 1173}, + "size": {"width": 1224, "height": 110}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1607, "left": 457}, + "size": {"width": 787, "height": 119}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1092, "left": 1621}, + "size": {"width": 748, "height": 110}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1620, "left": 1506}, + "size": {"width": 358, "height": 79}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1875, "left": 1062}, + "size": {"width": 727, "height": 84}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1872, "left": 487}, + "size": {"width": 387, "height": 85}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1781, "left": 665}, + "size": {"width": 886, "height": 97}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1625, "left": 2085}, + "size": {"width": 301, "height": 86}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1192, "left": 608}, + "size": {"width": 752, "height": 72}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1345, "left": 415}, + "size": {"width": 1922, "height": 120}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1712, "left": 501}, + "size": {"width": 749, "height": 79}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1546, "left": 1725}, + "size": {"width": 703, "height": 87}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1261, "left": 1599}, + "size": {"width": 731, "height": 88}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1263, "left": 667}, + "size": {"width": 735, "height": 94}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1189, "left": 1549}, + "size": {"width": 785, "height": 79}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1103, "left": 524}, + "size": {"width": 835, "height": 101}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1006, "left": 657}, + "size": {"width": 1820, "height": 111}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 603, "left": 876}, + "size": {"width": 1456, "height": 114}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 691, "left": 1041}, + "size": {"width": 1299, "height": 110}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 512, "left": 1567}, + "size": {"width": 729, "height": 90}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 504, "left": 673}, + "size": {"width": 598, "height": 105}, + }, + ], + "page_2": [ + { + "name": "field", + "type": "text", + "position": {"top": 3055, "left": 1193}, + "size": {"width": 649, "height": 106}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 3055, "left": 526}, + "size": {"width": 535, "height": 95}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 2815, "left": 360}, + "size": {"width": 371, "height": 79}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 2707, "left": 433}, + "size": {"width": 805, "height": 125}, + }, + ], + }, + "POS04": { + "page_1": [ + { + "name": "field", + "type": "text", + "position": {"top": 430, "left": 583}, + "size": {"width": 958, "height": 66}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 714, "left": 844}, + "size": {"width": 348, "height": 65}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 492, "left": 689}, + "size": {"width": 858, "height": 67}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 370, "left": 1037}, + "size": {"width": 488, "height": 65}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 368, "left": 447}, + "size": {"width": 399, "height": 63}, + }, + ], + "page_2": [ + { + "name": "field", + "type": "text", + "position": {"top": 1639, "left": 287}, + "size": {"width": 263, "height": 62}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1643, "left": 1368}, + "size": {"width": 203, "height": 52}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1556, "left": 330}, + "size": {"width": 554, "height": 95}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1639, "left": 982}, + "size": {"width": 251, "height": 57}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1575, "left": 1024}, + "size": {"width": 550, "height": 69}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1800, "left": 843}, + "size": {"width": 493, "height": 80}, + }, + { + "name": "field", + "type": "text", + "position": {"top": 1798, "left": 391}, + "size": {"width": 363, "height": 81}, + }, + ], + }, +} diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/sift_based_aligner.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/sift_based_aligner.py new file mode 100755 index 0000000..dc3f6c5 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/config/sift_based_aligner.py @@ -0,0 +1,178 @@ +config = { + "template_info": { + "edit_info_1": { + "image_path": "./assest/form_1_edit_personal_info/Scan47_0.jpg", + "anchors": [ + { + "id": "4a1d57e6-2403-4884-b6dd-39c844e32efc", + "position": {"top": 77, "left": 207}, + "size": {"width": 226, "height": 198}, + }, + { + "id": "eca7c11a-f35e-4924-9488-53fbeaeee867", + "position": {"top": 61, "left": 1948}, + "size": {"width": 406, "height": 257}, + }, + { + "id": "befc0ae6-cdb7-42df-b474-22cfb2795a05", + "position": {"top": 3017, "left": 210}, + "size": {"width": 2161, "height": 285}, + }, + ], + "fields": [ + { + "id": "c1be03a0-6556-4f98-a9e1-8687c57ad4de", + "position": {"top": 512, "left": 1567}, + "size": {"width": 729, "height": 90}, + }, + { + "id": "5bcc329b-b367-484a-9952-e20e859404b1", + "position": {"top": 1622, "left": 447}, + "size": {"width": 797, "height": 106}, + }, + { + "id": "8979b1ea-9c6a-4327-b355-22f3e6ce3a7a", + "position": {"top": 1951, "left": 1173}, + "size": {"width": 1152, "height": 122}, + }, + { + "id": "a72ca9cb-d4f0-4554-954b-e8e984a8ad6a", + "position": {"top": 700, "left": 1041}, + "size": {"width": 1287, "height": 91}, + }, + { + "id": "043da3cb-81ce-4cf8-966f-da91d72d3358", + "position": {"top": 608, "left": 876}, + "size": {"width": 1450, "height": 94}, + }, + { + "id": "6e55e678-58fd-4e75-976f-fb7852bdd6bc", + "position": {"top": 504, "left": 673}, + "size": {"width": 598, "height": 105}, + }, + ], + }, + "edit_info_2": { + "image_path": "./assest/form_1_edit_personal_info/Scan47_1.jpg", + "anchors": [ + { + "id": "1cf5b737-06ca-492f-b90b-87bda100b045", + "position": {"top": 3274, "left": 2078}, + "size": {"width": 234, "height": 162}, + }, + { + "id": "4825e878-1331-48ac-8fad-90cdf6cf412d", + "position": {"top": 3247, "left": 203}, + "size": {"width": 800, "height": 176}, + }, + { + "id": "8fb8d488-8280-4a56-93e0-67f8659a738c", + "position": {"top": 52, "left": 208}, + "size": {"width": 1063, "height": 183}, + }, + ], + "fields": [ + { + "id": "2db4507e-820f-48b9-8ec9-424a0207d5ca", + "position": {"top": 2815, "left": 360}, + "size": {"width": 371, "height": 79}, + }, + { + "id": "02cb11ed-ed6e-4125-88ec-cb8043d122a5", + "position": {"top": 2707, "left": 433}, + "size": {"width": 805, "height": 125}, + }, + ], + }, + "restore_contract_1": { + "image_path": "./assest/form_4/8_0.jpg", + "anchors": [ + { + "id": "7b3bcdaa-d6ab-40ed-b70b-96513887cef4", + "position": {"top": 1443, "left": 145}, + "size": {"width": 1427, "height": 162}, + }, + { + "id": "3457d3d4-3b4c-4464-b2f4-f94a4088f7f8", + "position": {"top": 53, "left": 133}, + "size": {"width": 152, "height": 129}, + }, + { + "id": "17c3b519-e4a6-423e-8149-e21b502c255f", + "position": {"top": 51, "left": 1294}, + "size": {"width": 267, "height": 154}, + }, + ], + "fields": [ + { + "id": "deb466f7-09ee-4f0d-9d95-6cb0d620dbe6", + "position": {"top": 493, "left": 689}, + "size": {"width": 864, "height": 56}, + }, + { + "id": "ba00580f-bd9a-40df-bfef-b88a77bf96c2", + "position": {"top": 370, "left": 1037}, + "size": {"width": 488, "height": 65}, + }, + { + "id": "339b766c-1079-46d1-bc4d-d4dc5cb4ca01", + "position": {"top": 368, "left": 447}, + "size": {"width": 399, "height": 63}, + }, + { + "id": "b6f87b41-e151-4fb3-be7f-75a80fcefde0", + "position": {"top": 714, "left": 844}, + "size": {"width": 343, "height": 65}, + }, + { + "id": "cf787f24-47f5-40d6-ac68-6e9a77b61731", + "position": {"top": 430, "left": 583}, + "size": {"width": 969, "height": 66}, + }, + ], + }, + "restore_contract_2": { + "image_path": "./assest/form_4/8_1.jpg", + "anchors": [ + { + "id": "8077ead5-32bc-4be7-bcd5-c0bf31ffb56f", + "position": {"top": 1373, "left": 952}, + "size": {"width": 551, "height": 82}, + }, + { + "id": "a87a5bfc-2fcc-479c-b0d0-f6a5cbc1f841", + "position": {"top": 1369, "left": 384}, + "size": {"width": 306, "height": 89}, + }, + { + "id": "4fc086b2-42e6-4132-9af4-338558501cd6", + "position": {"top": 34, "left": 180}, + "size": {"width": 674, "height": 120}, + }, + ], + "fields": [ + { + "id": "fe118028-8c0e-4b23-9254-eb7605c41d52", + "position": {"top": 1556, "left": 330}, + "size": {"width": 542, "height": 95}, + }, + { + "id": "6109edfc-963a-419f-9cd2-456e0660c28b", + "position": {"top": 1639, "left": 287}, + "size": {"width": 263, "height": 54}, + }, + ], + }, + }, + "min_match_count": 4, + "flann_based_matcher_config": {"algorithm": 0, "trees": 5}, + "matching_topk": 2, + "distance_threshold": 0.6, + "ransac_threshold": 5.0, + "valid_size_ratio_margin": 0.15, + "valid_area_threshold": 0.75, + "image_max_size": 1024, + "similar_triangle_threshold": 4, + "roi_to_template_box_ratio": 3.0, + "default_image_size": (1654, 2368), +} diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/field_module.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/field_module.py new file mode 100755 index 0000000..8ab1271 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/field_module.py @@ -0,0 +1,1194 @@ +import cv2 + + +class FieldParser: + def __init__(self): + pass + + def parse(self, ocr_output, field_infos, iou_threshold=0.7): + """parse field infor from template + + Args: + ocr_output (list[dict]): + [ + { + 'box': [xmin, ymin, xmax, ymax] + 'text': str + } + ] + + + field_infos (list[dict]): _description_ + - example: + [ + { + 'id' : 'field_1' + 'box': [xmin, ymin, xmax, ymax], + } + [ + + Returns: + field text: + [ + { + 'id' : 'field_1' + 'box': [xmin, ymin, xmax, ymax], + 'text': 'abc' + } + [ + """ + for field_item in field_infos: + if "list_words" not in field_item: + field_item["list_words"] = [] + + for ocr_item in ocr_output: + box = ocr_item["box"] + for field_item in field_infos: + field_name = field_item["id"] + field_box = field_item["box"] + iou = self.cal_iou_custom(box, field_box) + # if iou > 0: + # print(iou, ocr_item) + if iou > iou_threshold: + field_item["list_words"].append(ocr_item) + break # break if find field box + + for field_item in field_infos: + list_words = field_item["list_words"] + list_words = sorted(list_words, key=lambda item: item["box"][0]) + field_text = " ".join([item["text"] for item in list_words]) + field_item["text"] = field_text + + return field_infos + + def cal_iou_custom(self, box_A, box_B): + """calculate iou between two boxes + union = smaller box between two boxes + + Args: + box_A (list): _description_ + box_B (list): _description_ + + Returns: + (float): iou value + """ + + area1 = (box_A[2] - box_A[0]) * (box_A[3] - box_A[1]) + area2 = (box_B[2] - box_B[0]) * (box_B[3] - box_B[1]) + + xmin_intersect = max(box_A[0], box_B[0]) + ymin_intersect = max(box_A[1], box_B[1]) + xmax_intersect = min(box_A[2], box_B[2]) + ymax_intersect = min(box_A[3], box_B[3]) + if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: + area_intersect = 0 + else: + area_intersect = (xmax_intersect - xmin_intersect) * ( + ymax_intersect - ymin_intersect + ) + + # union = area1 + area2 - area_intersect + union = min(area1, area2) + if union == 0: + return 0 + + iou = area_intersect / union + + return iou + + +def format_field_info(data, id): + """{'name': 'field', + 'type': 'text', + 'position': {'top': 1951, 'left': 1173}, + 'size': {'width': 1224, 'height': 110}} + + Args: + data (_type_): _description_ + """ + + output = {} + output["id"] = data["name"] + "_" + str(id) + xmin, ymin, w, h = ( + data["position"]["left"], + data["position"]["top"], + data["size"]["width"], + data["size"]["height"], + ) + output["box"] = [xmin, ymin, xmin + w, ymin + h] + return output + + +def vis_field(img, field_infos): + for field_item in field_infos: + box = field_item["box"] + cv2.rectangle( + img, (box[0], box[1]), (box[2], box[3]), color=(0, 255, 0), thickness=1 + ) + + return img + + +def merge_field(image, field_infos): + pass + + +if __name__ == "__main__": + ocr_output = ( + [ + (1972, 101, 2324, 231), + (1980, 238, 2213, 283), + (607, 277, 702, 317), + (525, 279, 600, 317), + (242, 280, 345, 323), + (400, 280, 518, 317), + (819, 281, 887, 324), + (348, 282, 395, 324), + (895, 282, 988, 318), + (997, 282, 1076, 325), + (712, 282, 812, 317), + (1083, 283, 1174, 318), + (241, 325, 327, 366), + (331, 328, 390, 367), + (397, 330, 684, 370), + (245, 388, 441, 457), + (778, 390, 945, 458), + (616, 390, 762, 457), + (965, 395, 1176, 459), + (458, 395, 599, 457), + (1198, 396, 1422, 472), + (1574, 397, 1677, 459), + (1697, 398, 1883, 460), + (1440, 398, 1557, 458), + (1584, 518, 2283, 608), + (976, 527, 1056, 593), + (1046, 530, 1093, 591), + (687, 531, 742, 590), + (826, 532, 881, 596), + (904, 533, 956, 594), + (751, 534, 805, 597), + (1638, 537, 1687, 597), + (1563, 537, 1621, 597), + (508, 550, 600, 589), + (606, 551, 665, 592), + (1295, 551, 1352, 594), + (327, 551, 423, 597), + (429, 552, 502, 590), + (240, 552, 322, 597), + (1354, 554, 1436, 597), + (1442, 555, 1549, 597), + (1040, 619, 1228, 709), + (1504, 626, 1631, 707), + (617, 635, 710, 675), + (719, 636, 873, 681), + (239, 636, 299, 682), + (537, 636, 611, 675), + (1276, 637, 1440, 690), + (371, 637, 444, 674), + (302, 638, 365, 675), + (450, 644, 532, 675), + (1188, 703, 1374, 778), + (1631, 704, 1743, 781), + (1423, 707, 1566, 777), + (675, 718, 768, 759), + (777, 720, 917, 764), + (238, 720, 299, 766), + (595, 720, 669, 759), + (492, 721, 589, 764), + (371, 721, 486, 764), + (301, 721, 365, 759), + (925, 722, 1038, 760), + (1663, 789, 1718, 841), + (1488, 791, 1583, 835), + (1256, 792, 1347, 840), + (562, 792, 656, 835), + (1587, 793, 1661, 835), + (483, 794, 558, 835), + (737, 794, 809, 834), + (238, 795, 313, 840), + (814, 795, 897, 834), + (1721, 795, 1806, 835), + (659, 795, 734, 842), + (1181, 796, 1254, 836), + (317, 796, 391, 835), + (1350, 796, 1383, 837), + (1812, 796, 1895, 841), + (1012, 797, 1074, 841), + (905, 797, 1007, 834), + (1079, 797, 1175, 841), + (1386, 797, 1482, 839), + (396, 802, 477, 833), + (239, 868, 300, 926), + (336, 876, 418, 922), + (302, 876, 332, 917), + (424, 877, 521, 922), + (822, 877, 897, 922), + (657, 877, 726, 918), + (730, 877, 817, 919), + (527, 878, 650, 922), + (681, 957, 741, 1007), + (1806, 959, 1869, 1007), + (1355, 960, 1425, 1006), + (1695, 960, 1732, 1002), + (1427, 960, 1488, 1001), + (506, 960, 568, 1007), + (752, 960, 821, 1007), + (824, 960, 887, 1002), + (300, 961, 367, 1007), + (370, 961, 430, 1001), + (433, 961, 503, 1001), + (1734, 961, 1803, 1001), + (1631, 962, 1692, 1002), + (1029, 963, 1090, 1002), + (1493, 963, 1626, 1006), + (892, 964, 1023, 1006), + (298, 1042, 356, 1086), + (359, 1045, 439, 1089), + (443, 1046, 510, 1085), + (515, 1046, 647, 1090), + (303, 1128, 519, 1176), + (1360, 1129, 1610, 1176), + (519, 1210, 607, 1260), + (1358, 1211, 1463, 1253), + (300, 1212, 393, 1255), + (393, 1213, 515, 1254), + (1467, 1214, 1540, 1259), + (1992, 1260, 2326, 1347), + (934, 1269, 1415, 1353), + (990, 1280, 1037, 1345), + (912, 1284, 961, 1350), + (243, 1285, 303, 1341), + (1415, 1291, 1481, 1341), + (828, 1291, 873, 1347), + (694, 1292, 738, 1343), + (1484, 1293, 1594, 1341), + (759, 1294, 799, 1345), + (540, 1294, 662, 1344), + (394, 1294, 535, 1341), + (303, 1295, 389, 1342), + (242, 1371, 301, 1423), + (303, 1379, 416, 1422), + (239, 1474, 302, 1533), + (302, 1478, 348, 1527), + (670, 1480, 742, 1524), + (439, 1480, 538, 1529), + (745, 1480, 851, 1523), + (349, 1481, 434, 1530), + (857, 1481, 959, 1524), + (541, 1483, 666, 1528), + (1561, 1558, 1618, 1606), + (1248, 1558, 1305, 1609), + (946, 1559, 1005, 1608), + (527, 1559, 586, 1608), + (835, 1561, 933, 1607), + (1305, 1562, 1367, 1612), + (1006, 1563, 1124, 1607), + (589, 1563, 664, 1607), + (757, 1563, 832, 1607), + (240, 1565, 330, 1608), + (1368, 1565, 1434, 1606), + (442, 1566, 517, 1608), + (1129, 1566, 1234, 1606), + (1440, 1566, 1556, 1604), + (335, 1567, 438, 1607), + (1621, 1570, 1723, 1611), + (669, 1573, 752, 1606), + (1855, 1642, 1909, 1693), + (1246, 1642, 1309, 1694), + (1913, 1645, 1992, 1688), + (302, 1647, 362, 1698), + (1996, 1648, 2084, 1688), + (1416, 1648, 1506, 1689), + (1311, 1648, 1412, 1695), + (1581, 1651, 1608, 1690), + (364, 1652, 441, 1692), + (1667, 1652, 1700, 1689), + (304, 1733, 407, 1775), + (410, 1733, 499, 1781), + (244, 1811, 300, 1863), + (302, 1813, 359, 1861), + (359, 1815, 441, 1866), + (442, 1817, 490, 1859), + (559, 1818, 658, 1859), + (491, 1818, 557, 1864), + (979, 1896, 1065, 1948), + (904, 1897, 977, 1942), + (408, 1900, 493, 1948), + (300, 1901, 405, 1951), + (647, 1905, 676, 1942), + (556, 1905, 584, 1942), + (518, 1909, 542, 1939), + (598, 1909, 627, 1940), + (1786, 1941, 1889, 2034), + (1922, 1949, 2024, 2054), + (2043, 1951, 2191, 2041), + (1450, 1952, 1599, 2038), + (1298, 1957, 1420, 2046), + (1641, 1968, 1765, 2036), + (243, 1973, 305, 2033), + (1078, 1978, 1169, 2029), + (794, 1980, 840, 2025), + (715, 1981, 793, 2029), + (941, 1982, 1023, 2030), + (305, 1982, 409, 2033), + (842, 1983, 938, 2031), + (413, 1984, 659, 2032), + (1024, 1986, 1077, 2030), + (660, 1988, 713, 2031), + (306, 2055, 344, 2097), + (239, 2057, 309, 2097), + (2274, 2083, 2328, 2118), + (2208, 2083, 2272, 2119), + (1980, 2083, 2053, 2123), + (2125, 2084, 2203, 2118), + (1326, 2085, 1376, 2126), + (2055, 2085, 2123, 2118), + (1911, 2085, 1979, 2124), + (1249, 2085, 1325, 2121), + (1639, 2086, 1739, 2124), + (1479, 2086, 1551, 2120), + (1170, 2086, 1247, 2126), + (1824, 2086, 1908, 2119), + (1025, 2086, 1091, 2122), + (354, 2087, 428, 2127), + (1379, 2087, 1478, 2120), + (783, 2087, 861, 2122), + (1743, 2087, 1821, 2119), + (1555, 2087, 1633, 2119), + (1093, 2087, 1168, 2121), + (945, 2087, 1022, 2122), + (863, 2087, 942, 2122), + (593, 2087, 674, 2126), + (677, 2088, 780, 2125), + (520, 2089, 591, 2124), + (430, 2089, 466, 2124), + (467, 2089, 518, 2126), + (816, 2127, 853, 2166), + (639, 2128, 694, 2167), + (945, 2128, 1050, 2165), + (695, 2129, 734, 2166), + (351, 2129, 416, 2165), + (735, 2129, 815, 2165), + (597, 2130, 639, 2164), + (518, 2130, 596, 2163), + (418, 2130, 516, 2166), + (853, 2131, 942, 2165), + (335, 2142, 357, 2170), + (1768, 2163, 1820, 2198), + (2230, 2163, 2268, 2200), + (2270, 2165, 2329, 2199), + (1126, 2165, 1194, 2205), + (1659, 2165, 1728, 2203), + (2133, 2165, 2227, 2202), + (1823, 2165, 1890, 2202), + (2092, 2165, 2130, 2198), + (1984, 2165, 2027, 2199), + (1536, 2166, 1598, 2199), + (1411, 2166, 1465, 2205), + (1893, 2166, 1981, 2202), + (1730, 2166, 1766, 2199), + (2030, 2167, 2089, 2197), + (1196, 2167, 1288, 2201), + (1342, 2167, 1410, 2205), + (1045, 2167, 1123, 2204), + (991, 2167, 1044, 2206), + (882, 2168, 953, 2206), + (1467, 2168, 1533, 2199), + (355, 2168, 410, 2204), + (679, 2169, 768, 2206), + (531, 2169, 616, 2203), + (1291, 2169, 1340, 2201), + (770, 2169, 816, 2203), + (816, 2169, 881, 2203), + (953, 2169, 990, 2204), + (617, 2169, 678, 2204), + (461, 2169, 529, 2204), + (1601, 2170, 1657, 2199), + (411, 2170, 459, 2204), + (333, 2172, 353, 2202), + (1657, 2206, 1705, 2243), + (2000, 2206, 2071, 2239), + (1772, 2206, 1837, 2239), + (1913, 2206, 1997, 2243), + (1252, 2206, 1305, 2244), + (1841, 2207, 1908, 2241), + (1560, 2207, 1654, 2239), + (1427, 2207, 1486, 2240), + (1489, 2207, 1557, 2240), + (807, 2207, 854, 2246), + (1068, 2207, 1153, 2241), + (1156, 2207, 1250, 2243), + (987, 2208, 1066, 2245), + (1707, 2208, 1768, 2242), + (596, 2208, 668, 2247), + (668, 2208, 720, 2243), + (722, 2208, 807, 2242), + (926, 2208, 986, 2242), + (855, 2208, 925, 2246), + (1309, 2208, 1424, 2243), + (501, 2209, 594, 2247), + (436, 2210, 499, 2244), + (353, 2216, 434, 2248), + (1462, 2245, 1521, 2285), + (1734, 2245, 1812, 2284), + (1213, 2246, 1288, 2281), + (1092, 2246, 1148, 2287), + (1335, 2246, 1388, 2280), + (1816, 2247, 1919, 2283), + (897, 2247, 977, 2287), + (1148, 2247, 1210, 2282), + (1618, 2247, 1680, 2285), + (737, 2247, 806, 2283), + (1391, 2247, 1459, 2285), + (1683, 2247, 1731, 2280), + (979, 2248, 1090, 2287), + (675, 2248, 735, 2282), + (809, 2248, 894, 2282), + (1923, 2248, 1991, 2279), + (1525, 2248, 1614, 2284), + (1291, 2249, 1332, 2281), + (613, 2249, 674, 2288), + (545, 2250, 611, 2288), + (472, 2250, 542, 2288), + (417, 2250, 470, 2284), + (356, 2250, 415, 2284), + (335, 2252, 352, 2284), + (697, 2326, 793, 2369), + (259, 2326, 356, 2378), + (635, 2326, 694, 2374), + (467, 2327, 538, 2368), + (543, 2329, 631, 2368), + (365, 2332, 462, 2375), + (947, 2401, 1008, 2456), + (1247, 2404, 1306, 2455), + (1564, 2406, 1619, 2449), + (839, 2407, 934, 2451), + (1307, 2408, 1368, 2457), + (1010, 2409, 1125, 2452), + (535, 2409, 587, 2463), + (1131, 2410, 1236, 2451), + (761, 2411, 834, 2451), + (1443, 2411, 1558, 2448), + (1372, 2412, 1437, 2449), + (594, 2413, 666, 2452), + (244, 2413, 331, 2453), + (1623, 2415, 1725, 2455), + (339, 2416, 439, 2452), + (447, 2416, 518, 2453), + (673, 2420, 754, 2451), + (1930, 2518, 1980, 2565), + (1879, 2519, 1926, 2565), + (1639, 2521, 1720, 2558), + (1726, 2521, 1775, 2565), + (1780, 2522, 1873, 2563), + (759, 2524, 808, 2569), + (812, 2525, 862, 2563), + (672, 2525, 754, 2562), + (690, 2613, 798, 2723), + (1626, 2632, 1837, 2756), + (465, 2727, 650, 2873), + (878, 2734, 997, 2826), + (695, 2756, 839, 2841), + (1868, 2760, 1997, 2853), + (1686, 2785, 1833, 2862), + (1488, 2787, 1667, 2942), + (2031, 3036, 2080, 3080), + (1819, 3037, 1870, 3082), + (2212, 3040, 2305, 3081), + (1683, 3040, 1797, 3084), + (1879, 3040, 1979, 3084), + (1275, 3042, 1337, 3082), + (2310, 3042, 2336, 3082), + (2089, 3042, 2204, 3081), + (1056, 3042, 1137, 3082), + (1982, 3042, 2011, 3084), + (1394, 3043, 1481, 3080), + (1608, 3043, 1675, 3080), + (1489, 3044, 1601, 3080), + (1341, 3045, 1389, 3082), + (1000, 3045, 1050, 3083), + (1191, 3046, 1270, 3087), + (1142, 3046, 1186, 3083), + (903, 3048, 994, 3088), + (739, 3048, 822, 3089), + (246, 3048, 322, 3084), + (534, 3049, 592, 3083), + (829, 3049, 896, 3084), + (331, 3049, 408, 3089), + (417, 3050, 526, 3083), + (675, 3050, 733, 3083), + (598, 3051, 668, 3089), + (346, 3097, 412, 3131), + (247, 3100, 335, 3131), + (1685, 3136, 1776, 3175), + (1276, 3138, 1494, 3182), + (1782, 3139, 1841, 3180), + (1606, 3140, 1678, 3176), + (1502, 3141, 1599, 3181), + (1197, 3141, 1268, 3178), + (544, 3142, 625, 3178), + (1030, 3143, 1102, 3178), + (793, 3144, 845, 3179), + (959, 3144, 1022, 3183), + (852, 3144, 953, 3178), + (716, 3144, 788, 3184), + (491, 3146, 538, 3185), + (632, 3146, 709, 3178), + (414, 3147, 484, 3179), + (283, 3148, 404, 3186), + (1109, 3149, 1189, 3178), + (245, 3150, 273, 3180), + (2117, 3183, 2206, 3220), + (1666, 3185, 1827, 3224), + (1961, 3187, 2028, 3221), + (2038, 3187, 2106, 3220), + (1482, 3188, 1578, 3230), + (2216, 3188, 2283, 3220), + (1586, 3189, 1658, 3225), + (1885, 3189, 1953, 3228), + (1222, 3189, 1283, 3233), + (1396, 3190, 1476, 3231), + (1097, 3190, 1163, 3227), + (1168, 3190, 1218, 3228), + (1034, 3192, 1093, 3228), + (1288, 3192, 1389, 3231), + (484, 3192, 547, 3227), + (909, 3193, 1027, 3233), + (829, 3193, 903, 3228), + (638, 3194, 718, 3233), + (554, 3195, 630, 3227), + (1832, 3195, 1880, 3224), + (726, 3195, 820, 3233), + (420, 3195, 477, 3228), + (290, 3196, 411, 3234), + (243, 3197, 281, 3230), + (1376, 3236, 1426, 3274), + (1299, 3238, 1371, 3279), + (1730, 3239, 1808, 3278), + (1671, 3239, 1724, 3273), + (1555, 3239, 1664, 3278), + (1432, 3239, 1547, 3278), + (1200, 3240, 1292, 3280), + (971, 3241, 1083, 3280), + (1091, 3241, 1192, 3275), + (865, 3241, 962, 3275), + (779, 3242, 858, 3280), + (656, 3243, 707, 3275), + (604, 3243, 650, 3275), + (522, 3243, 598, 3281), + (714, 3243, 771, 3275), + (329, 3244, 362, 3276), + (370, 3245, 462, 3283), + (246, 3246, 320, 3276), + (470, 3250, 517, 3281), + (2185, 3354, 2332, 3379), + (876, 3357, 948, 3387), + (734, 3358, 807, 3387), + (811, 3358, 871, 3391), + (522, 3358, 592, 3388), + (679, 3359, 729, 3391), + (599, 3359, 674, 3387), + (461, 3361, 517, 3388), + (250, 3362, 326, 3393), + (367, 3362, 455, 3388), + (331, 3363, 362, 3393), + (2290, 3391, 2332, 3416), + (380, 3398, 626, 3427), + (249, 3400, 374, 3427), + ], + [ + "FWD", + "insurance", + "hiểm", + "Bảo", + "Công", + "TNHH", + "thọ", + "ty", + "FWD", + "Việt", + "Nhân", + "Nam", + "Mẫu", + "số:", + "POS01_2022.09", + "Phiếu", + "Điều", + "Cầu", + "Chỉnh", + "Yêu", + "Thông", + "Cá", + "Nhân", + "Tin", + "0357788028", + "3", + "0", + "13", + "2", + "3", + "4", + "13", + "10", + "hiểm", + "số:", + "Số", + "đồng", + "bảo", + "Hợp", + "điện", + "thoại:", + "Nguyễn", + "Hiệp", + "hiểm", + "(BMBH):", + "Họ", + "bảo", + "hoan", + "Bên", + "tên", + "mua", + "Nguyễn", + "Hiệp", + "Hoàng", + "hiểm", + "(NĐBH)", + "Họ", + "bảo", + "được", + "Người", + "tên", + "chính:", + "(x)", + "đánh", + "(cắc)", + "hiểm", + "dấu", + "bảo", + "cầu", + "Tôi,", + "điều", + "dưới", + "yêu", + "của", + "Bên", + "ô", + "đây:", + "nội", + "chỉnh", + "dung", + "được", + "mua", + "X", + "Cập", + "L", + "Nhật", + "Lạc", + "Tin", + "Liên", + "Thông", + "0", + "lạc", + "Địa", + "&", + "chỉ", + "lạc", + "Địa", + "chỉ", + "Địa", + "chỉ", + "liên", + "liên", + "trú", + "trú", + "thường", + "thường", + "Số", + "nhà,", + "tên", + "đường:", + "Phường/Xã:", + "Quận/Huyện:", + "phố:", + "Quốc", + "Tỉnh/", + "Thành", + "gia:", + "T", + "345968", + "3", + "7", + "8", + "(cố", + "2", + "o", + "định):", + "3", + "động):", + "thoại(di", + "Điện", + "0", + "Email:", + "X", + "II.", + "Tin", + "Nhật", + "Nhân", + "Cập", + "Thân", + "Thông", + "bổ", + "0", + "0", + "0", + "hiểm", + "Họ", + "NĐBH", + "Bên", + "bảo", + "Điều", + "tên", + "cho", + "chính", + "NĐBH", + "chỉnh", + "sung:", + "mua", + "0", + "0", + "Giới", + "Họ", + "tính:", + "sinh:", + "Ngày", + "/", + "tên:", + "J", + "Quốc", + "tịch:", + "0", + "Số", + "giấy", + "tờ", + "thân:", + "tùy", + "cấp:", + "Nơi", + "cấp:", + "Ngày", + "/", + "/", + "✪", + "_.", + "thể", + "điện", + "thoại", + "doan", + "Kinh", + "Sum", + "_", + "thể):", + "tả", + "(mô", + "Việc", + "Nghề", + "công", + "nghiệp/Chức", + "cụ", + "vụ", + "ý:", + "Lưu", + "thẻ", + "Các", + "Giấy", + "sinh/", + "Hộ", + "khai", + "đội/", + "dân/", + "Chứng", + "Khai", + "công", + "Quân", + "Căn", + "Giấy", + "chiếu/", + "minh", + "minh", + "sinh/", + "cước", + "dân/", + "nhân", + "gồm:", + "Chứng", + "thân", + "tờ", + "tùy", + "lý", + "giá", + "đương", + "trị", + "ban", + "pháp", + "CÓ", + "khác", + "ngành", + "tương", + "✪", + "thể", + "từ", + "liên", + "Quý", + "giấy", + "chứng", + "hiện", + "và", + "tin", + "bản", + "gửi", + "thông", + "tờ", + "mới", + "khách", + "lòng", + "thân,", + "tùy", + "giấy", + "kèm", + "Đối", + "thông", + "chỉnh", + "vui", + "tin", + "trên", + "tờ", + "các", + "điều", + "sao", + "với", + "-", + "Họ", + "sinh.", + "Giới", + "Ngày", + "địa", + "tính,", + "chỉnh:", + "nếu", + "điều", + "hộ", + "chính", + "quyền", + "nhận", + "tên,", + "định", + "cải", + "chính", + "xác", + "tịch;", + "phương", + "Quyết", + "như", + "quan", + "đổi,", + "nghề", + "hiểm", + "phí", + "thể", + "nghiệp", + "nghề", + "bảo", + "ứng", + "điểu", + "thay", + "với", + "nghiệp,", + "cầu", + "chỉnh", + "mới.", + "tương", + "CÓ", + "yêu", + "hiện", + "thực", + "khi", + "Sau", + ".", + "Mẫu", + "XII.", + "Ký", + "Đổi", + "Chữ", + "Thay", + "X", + "0", + "bổ", + "hiểm", + "Họ", + "NĐBH", + "X", + "chính", + "bảo", + "NĐBH", + "tên", + "Bên", + "Điều", + "sung:", + "chỉnh", + "cho", + "mua", + "lại", + "ký", + "Chữ", + "ký", + "đăng", + "ký", + "cũ", + "Chữ", + "Hiệp", + "Huệp", + "Nguyễn", + "Hiệp", + "Hoàng", + "Hiệp", + "Hoàng", + "Nguyễn", + "0", + "0", + "đồng", + "phẩm:", + "Đồng", + "tắc", + "Ý", + "Không", + "hiểu", + "Ý", + "Điều", + "sản", + "khoản", + "và", + "đã", + "Quy", + "rõ", + "nhận", + "lòng", + "Nếu", + "lăn", + "xác", + "Quý", + "khách", + "vui", + "tay,", + "Kết", + "Cam", + "hiểm", + "hiểm/Người", + "ký.", + "bảo", + "được", + "bảo", + "mẫu", + "Bên", + "do", + "tôi,", + "chính", + "đây", + "ký", + "trên", + "chữ", + "Những", + "mua", + "1.", + "biểm", + "hiểm/Hồ", + "cầu", + "bảo", + "đồng", + "nêu", + "bảo", + "yêu", + "ghi", + "Hợp", + "tiết", + "đã", + "chi", + "trong", + "tiết", + "những", + "như", + "đây,", + "trên", + "SƠ", + "cũng", + "chi", + "Những", + "2.", + "về", + "luật", + "này.", + "tin", + "thông", + "những", + "pháp", + "nhiệm", + "trước", + "trách", + "chịu", + "tôi", + "và", + "thật", + "xin", + "là", + "đúng", + "trên", + "sự", + "V1.092022", + "Nam", + "FWD", + "Việt", + "hiểm", + "thọ", + "Nhân", + "Bảo", + "Công", + "TNHH", + "ty", + "1/2", + "www.fwd.com.vn", + "Website:", + ], + ) + + ocr_output = [ + {"box": box, "text": text} for (box, text) in zip(ocr_output[0], ocr_output[1]) + ] + + field_infos = [ + {"id": "field_0", "box": [1173, 1951, 2397, 2061]}, + {"id": "field_1", "box": [457, 1607, 1244, 1726]}, + {"id": "field_2", "box": [1621, 1092, 2369, 1202]}, + {"id": "field_3", "box": [1506, 1620, 1864, 1699]}, + {"id": "field_4", "box": [1062, 1875, 1789, 1959]}, + {"id": "field_5", "box": [487, 1872, 874, 1957]}, + {"id": "field_6", "box": [665, 1781, 1551, 1878]}, + {"id": "field_7", "box": [2085, 1625, 2386, 1711]}, + {"id": "field_8", "box": [608, 1192, 1360, 1264]}, + {"id": "field_9", "box": [415, 1345, 2337, 1465]}, + {"id": "field_10", "box": [501, 1712, 1250, 1791]}, + {"id": "field_11", "box": [1725, 1546, 2428, 1633]}, + {"id": "field_12", "box": [1599, 1261, 2330, 1349]}, + {"id": "field_13", "box": [667, 1263, 1402, 1357]}, + {"id": "field_14", "box": [1549, 1189, 2334, 1268]}, + {"id": "field_15", "box": [524, 1103, 1359, 1204]}, + {"id": "field_16", "box": [657, 1006, 2477, 1117]}, + {"id": "field_17", "box": [876, 603, 2332, 717]}, + {"id": "field_18", "box": [1041, 691, 2340, 801]}, + {"id": "field_19", "box": [1567, 512, 2296, 602]}, + {"id": "field_20", "box": [673, 504, 1271, 609]}, + ] + + # field_infos = [{'name': 'field', + # 'type': 'text', + # 'position': {'top': 1951, 'left': 1173}, + # 'size': {'width': 1224, 'height': 110}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1607, 'left': 457}, + # 'size': {'width': 787, 'height': 119}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1092, 'left': 1621}, + # 'size': {'width': 748, 'height': 110}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1620, 'left': 1506}, + # 'size': {'width': 358, 'height': 79}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1875, 'left': 1062}, + # 'size': {'width': 727, 'height': 84}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1872, 'left': 487}, + # 'size': {'width': 387, 'height': 85}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1781, 'left': 665}, + # 'size': {'width': 886, 'height': 97}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1625, 'left': 2085}, + # 'size': {'width': 301, 'height': 86}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1192, 'left': 608}, + # 'size': {'width': 752, 'height': 72}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1345, 'left': 415}, + # 'size': {'width': 1922, 'height': 120}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1712, 'left': 501}, + # 'size': {'width': 749, 'height': 79}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1546, 'left': 1725}, + # 'size': {'width': 703, 'height': 87}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1261, 'left': 1599}, + # 'size': {'width': 731, 'height': 88}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1263, 'left': 667}, + # 'size': {'width': 735, 'height': 94}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1189, 'left': 1549}, + # 'size': {'width': 785, 'height': 79}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1103, 'left': 524}, + # 'size': {'width': 835, 'height': 101}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 1006, 'left': 657}, + # 'size': {'width': 1820, 'height': 111}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 603, 'left': 876}, + # 'size': {'width': 1456, 'height': 114}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 691, 'left': 1041}, + # 'size': {'width': 1299, 'height': 110}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 512, 'left': 1567}, + # 'size': {'width': 729, 'height': 90}}, + # {'name': 'field', + # 'type': 'text', + # 'position': {'top': 504, 'left': 673}, + # 'size': {'width': 598, 'height': 105}}] + + # field_infos = [ + # format_field_info(field_item, idx) + # for idx, field_item in enumerate(field_infos) + # ] + + print(field_infos) + + img = cv2.imread( + "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/assest/form_1_edit_personal_info/Scan47_0.jpg" + ) + img = vis_field(img, field_infos) + cv2.imwrite("vis_field.jpg", img) + print(ocr_output[0]) + print(field_infos[0]) + parser = FieldParser() + field_outputs = parser.parse(ocr_output, field_infos) + + for field_item in field_outputs: + if len(field_item["list_words"]) > 0: + print(field_item["id"], field_item["text"]) diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/line_parser.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/line_parser.py new file mode 100755 index 0000000..32cf756 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/line_parser.py @@ -0,0 +1,34 @@ +# @brief Line parser based on template boxes, i.e. simply crop text boxes given the original image and text boxes' coordinates +class TemplateBoxParser: + def __init__(self): + pass + + ## + # @brief Run line parser + # + # @param images: Refer to interface + # @param metadata: Refer to interface. Each metadata dict, i.e. correspond to an image, has format + # + # { + # "boxes": [ + # (top, left, w, h), + # (top, left, w, h), + # ... + # ] + # } + # + # where coordinates are absolute coordinates + # + # @return cropped_images: Refer to interface + def run(self, images, metadata): + cropped_images = [] + for image, _metadata in zip(images, metadata): + _cropped_images = [] + for box in _metadata["boxes"]: + # y, x, w, h = box + # x1, y1, x2, y2 = x, y, x + w, y + h + x1, y1, x2, y2 = box + # print(bo) + _cropped_images.append(image[y1:y2, x1:x2].copy()) + cropped_images.append(_cropped_images) + return cropped_images diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/ocr_module.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/ocr_module.py new file mode 100755 index 0000000..e1d108c --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/ocr_module.py @@ -0,0 +1,126 @@ +# temp #for debug +import glob +import os +from mmdet.apis import inference_detector, init_detector +from mmocr.apis import init_detector as init_classifier +from mmocr.apis.inference import model_inference +import cv2 +import numpy as np + +# from src.tools.utils import * + +import time +from src.utils.visualize import visualize_ocr_output + + +def clip_box(x1, y1, x2, y2, w, h): + x1 = int(float(min(max(0, x1), w))) + x2 = int(float(min(max(0, x2), w))) + y1 = int(float(min(max(0, y1), h))) + y2 = int(float(min(max(0, y2), h))) + return (x1, y1, x2, y2) + + +def get_crop_img_and_bbox(img, bbox, extend: bool = False): + """ + img : numpy array img + bbox : should be xyxy format + """ + if len(bbox) == 5: + left, top, right, bottom, _conf = bbox + elif len(bbox) == 4: + left, top, right, bottom = bbox + left, top, right, bottom = clip_box( + left, top, right, bottom, img.shape[1], img.shape[0] + ) + # assert (bottom - top) * (right - left) > 0, "bbox is invalid" + crop_img = img[top:bottom, left:right] + return crop_img, (left, top, right, bottom) + + +class YoloX: + def __init__(self, config, checkpoint, device="cuda:0"): + self.model = init_detector(config, checkpoint, device=device) + + def inference(self, img=None): + t1 = time.time() + output = inference_detector(self.model, img) + print("Time det: ", time.time() - t1) + return output + + +class Classifier_SATRN: + def __init__(self, config, checkpoint, device="cuda:0"): + self.model = init_classifier(config, checkpoint, device) + + def inference(self, numpy_image): + t1 = time.time() + result = model_inference(self.model, numpy_image, batch_mode=True) + preds_str = [r["text"] for r in result] + confidence = [r["score"] for r in result] + + print("Time reg: ", time.time() - t1) + return preds_str, confidence + + +class OcrEngine: + def __init__(self, det_cfg, det_ckpt, cls_cfg, cls_ckpt, device="cuda:0"): + self.det = YoloX(det_cfg, det_ckpt, device) + self.cls = Classifier_SATRN(cls_cfg, cls_ckpt, device) + + def run_image(self, img): + pred_det = self.det.inference(img) + + pred_det = pred_det[0] # batch_size=1 + + pred_det = sorted(pred_det, key=lambda box: [box[1], box[0]]) + bboxes = np.vstack(pred_det) + + lbboxes = [] + lcropped_img = [] + assert len(bboxes) != 0, f"No bbox found in {img_path}, skipped" + for bbox in bboxes: + try: + crop_img, bbox_ = get_crop_img_and_bbox(img, bbox, extend=True) + lbboxes.append(bbox_) + lcropped_img.append(crop_img) + except AssertionError: + print(f"[ERROR]: Skipping invalid bbox {bbox} in ", img_path) + lwords, _ = self.cls.inference(lcropped_img) + return lbboxes, lwords + + +def visualize(image, boxes, color=(0, 255, 0)): + for box in boxes: + cv2.rectangle(image, (box[0], box[1]), (box[2], box[3])) + + +if __name__ == "__main__": + det_cfg = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/weights/yolox_s_8x8_300e_cocotext_1280.py" + det_ckpt = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/weights/best_bbox_mAP_epoch_100.pth" + cls_cfg = "/home/sds/datnt/mmocr/logs/satrn_big_2022-04-25/satrn_big.py" + cls_ckpt = "/home/sds/datnt/mmocr/logs/satrn_big_2022-04-25/best.pth" + + engine = OcrEngine(det_cfg, det_ckpt, cls_cfg, cls_ckpt, device="cuda:0") + + # img_path = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/assest/form_1_edit_personal_info/Scan47_0.jpg" + + img_dir = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/raw_images/POS01" + out_dir = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/outputs/visualize_ocr/POS01" + + if not os.path.exists(out_dir): + os.makedirs(out_dir) + img_paths = glob.glob(img_dir + "/*") + for img_path in img_paths: + img = cv2.imread(img_path) + t1 = time.time() + res = engine.run_image(img) + + visualize_ocr_output( + res, + img, + vis_dir=out_dir, + prefix_name=os.path.splitext(os.path.basename(img_path))[0], + font_path="./assest/visualize/times.ttf", + is_vis_kie=False, + ) diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/satrn_classifier.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/satrn_classifier.py new file mode 100755 index 0000000..be33149 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/satrn_classifier.py @@ -0,0 +1,15 @@ +from mmocr.apis import init_detector as init_classifier +from mmocr.apis.inference import model_inference +import numpy as np +from .utils import * + + +class Classifier_SATRN: + def __init__(self, config, checkpoint, device="cuda:0"): + self.model = init_classifier(config, checkpoint, device) + + def inference(self, numpy_image): + result = model_inference(self.model, numpy_image, batch_mode=True) + preds_str = [r["text"] for r in result] + confidence = [r["score"] for r in result] + return preds_str, confidence diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/sift_based_aligner.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/sift_based_aligner.py new file mode 100755 index 0000000..67fd059 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/modules/sift_based_aligner.py @@ -0,0 +1,385 @@ +import os +import time + +import numpy as np +import cv2 + +from scipy.spatial import ConvexHull, convex_hull_plot_2d +from shapely.geometry import Polygon + +from ..utils.image_calib import check_similar_triangle, check_angle_between_2_lines +import tqdm + +## +# @brief Document classifier based on SIFT-based template matching +# @note This classifier can support varying illumination, varying out-of-plane rotation angles (up to roughly 60 degrees compared with the template image's orientation), and invariant to scale. These constraints are given in [this slides](http://vision.stanford.edu/teaching/cs231a_autumn1112/lecture/lecture12_SIFT_single_obj_recog_cs231a_marked.pdf) + + +class SIFTBasedAligner: + ## + # @brief Initializer + # + # @param template_info (dict): Mapping from class names to template info lists. The format of this dict is given as + # + # ``` + # { + # "doc_type": { + # "image_path": str, # path to template image + # "anchors": [ + # { + # "id": str, # anchor ID in template image + # "position": {"top": int, "left": int}, + # "size": {"width": , "height": float} + # }, + # { + # "id": str, # anchor ID in template image + # "position": {"top": int, "left": int}, + # "size": {"width": , "height": float} + # }, + # ... + # ] + # }, + # "doc_type": { + # "image_path": str, # path to template image + # "anchors": [ + # { + # "id": str, # anchor ID in template image + # "position": {"top": int, "left": int}, + # "size": {"width": , "height": float} + # }, + # { + # "id": str, # anchor ID in template image + # "position": {"top": int, "left": int}, + # "size": {"width": , "height": float} + # }, + # ... + # ] + # }, + # ... + # } + # + # ``` + # + # @param min_match_count (int): Minimum number of matched points for a template to be considered "FOUND" in the input image + # @param flann_based_matcher_config (dict): Configurations for cv2.FlannBasedMatcher, i.e. refer to the this class for more details + # @param matching_topk (int): This must be 2 + # @param distance_threshold (float): Upper threshold for top-1-distance-over-top-2-distance ratio after kNN matching + # @param ransac_threshold (float): RANSAC threshold for locating the template within the input image (if found) + # @param valid_size_ratio_margin (float): Valid max-size-to-min-size ratio margin of the min-area surrounding box of the found template in the image, within which the found template is considered valid + # @param valid_area_threshold (float): Valid area threhsold, above which the found template is considered valid + # @param image_max_size (int): Maximum size for the input image. None if no limit + # @param similar_triangle_threshold (float): Threshold, above which two triangles are considered non-similar + # @param roi_to_template_box_ratio (float): Ratio to scale the template anchor to estimate the ROI, within which the template anchor is likely to exist + def __init__( + self, + template_info, + min_match_count, + flann_based_matcher_config, + matching_topk, + distance_threshold, + ransac_threshold, + valid_size_ratio_margin, + valid_area_threshold, + image_max_size, + similar_triangle_threshold, + roi_to_template_box_ratio, + default_image_size, + template_im_dir, + ): + assert matching_topk == 2, "Invalid matching_topk" + + # SIFT feature extractor + # self.sift = cv2.xfeatures2d.SIFT_create() + self.sift = cv2.SIFT_create() + + self.template_im_dir = template_im_dir + + # load templates + ( + self.template_images, + self.template_anchors, + self.template_features, + self.template_metadata, + ) = self._load_template(template_info) + # kNN feature matcher + self.matcher = cv2.FlannBasedMatcher(flann_based_matcher_config, {}) + + # other arguments + self.image_max_size = image_max_size + self.min_match_count = min_match_count + self.matching_topk = matching_topk + self.distance_threshold = distance_threshold + self.ransac_threshold = ransac_threshold + self.roi_to_template_box_ratio = roi_to_template_box_ratio + self.default_image_size = default_image_size + + # validity thresholds + self.valid_size_ratio_interval = [ + 1 - valid_size_ratio_margin, + 1 + valid_size_ratio_margin, + ] + self.valid_area_threshold = valid_area_threshold + self.similar_triangle_threshold = similar_triangle_threshold + + def _load_template(self, template_info): + r"""Load template images from paths and extract features""" + template_images, template_anchors = {}, {} + template_features, template_metadata = {}, {} + # print(template_info) + # for doc_type in template_info: + template_anchors = [] + template_features = [] + template_metadata = [] + template_im_path = os.path.join( + self.template_im_dir, template_info["image_path"] + ) + print(template_im_path) + assert os.path.exists(template_im_path), print(template_im_path) + template_image = cv2.imread(template_im_path) + for anchor in template_info["anchors"]: + # extract anchor + anchor = [int(float(item)) for item in anchor] + x1, y1, x2, y2 = anchor + template_anchor = template_image[y1:y2, x1:x2] + template_anchor = cv2.cvtColor(template_anchor, cv2.COLOR_BGR2GRAY) + template_kpts, template_desc = self.sift.detectAndCompute( + template_anchor, None + ) + + # append to dict + max_size = np.max(template_anchor.shape[:2]) + min_size = np.min(template_anchor.shape[:2]) + + template_images = template_image + template_anchors.append(template_anchor) + template_features.append( + { + "kpts": template_kpts, + "desc": template_desc, + "ratio": max_size / min_size, + } + ) + template_metadata.append({"box": [x1, y1, x2, y2]}) + return (template_images, template_anchors, template_features, template_metadata) + + + def run_alige(self, images, metadata=None): + transformed_images = [] + for image, _metadata in tqdm.tqdm(zip(images, metadata)): + # find templates + doc_type = _metadata["doc_type"] + template_image = self.template_images + gray_image = self._preprocess(image) + + # match against all templates of the given doc type + anchor_centers, anchor_locations = [], [] + found_centers, found_locations = [], [] + for template_anchor, template_feature, template_metadata in zip( + self.template_anchors, + self.template_features, + self.template_metadata, + ): + ( + anchor_center, + anchor_location, + found_center, + found_location, + ) = self._find_template( + gray_image, + template_image, + template_anchor, + template_feature, + template_metadata, + ) + if found_location is not None: + anchor_centers.append(anchor_center) + anchor_locations.append(anchor_location) + found_centers.append(found_center) + found_locations.append(found_location) + + if len(found_locations) == 0: + print("len(found_locations) == 0") + transformed_images.append(None) + continue + else: + found_locations = np.concatenate(found_locations, axis=0) + anchor_locations = np.concatenate(anchor_locations, axis=0) + + # check calibration ability + calib_success = False + if len(found_centers) < 3: + print(found_centers) + print("len(found_centers) < 3") + transformed_images.append(None) + continue + else: + calib_success = check_similar_triangle( + anchor_centers, + found_centers, + diff_thres=self.similar_triangle_threshold, + ) + + # align image + # TODO: calib_success = False even when calib is successful + calib_success = True + if calib_success: + perspective_trans, _ = cv2.findHomography( + found_locations, anchor_locations + ) + + transformed_image = cv2.warpPerspective( + image, + perspective_trans, + (template_image.shape[1], template_image.shape[0]), + ) + transformed_images.append(transformed_image) + else: + print("calib_success is False") + transformed_images.append(None) + + return transformed_images + + def _preprocess(self, image): + gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) + return gray_image + + def _find_template( + self, + gray_image, + gray_template, + gray_template_anchor, + template_feature, + template_metadata, + ): + r"""Find templates and return template center""" + # parser inputs + template_kpts = template_feature["kpts"] + template_desc = template_feature["desc"] + template_box = template_metadata["box"] + + # resize ROI + gray_image, shift, scale = self._crop_roi_and_resize( + gray_image, gray_template, template_box + ) + + # extract features + image_kpts, image_desc = self.sift.detectAndCompute(gray_image, None) + # if image_desc is None: + # print("Error matching") + # return None, None, None, None + # print(image_desc) + try: + # knnMatch to get top-K then sort by their distance + matches = self.matcher.knnMatch( + template_desc, image_desc, self.matching_topk + ) + except Exception as err: + print(err) + return None, None, None, None + matches = sorted(matches, key=lambda x: x[0].distance) + + # ratio test, to get good matches. + # idea: good matches should uniquely match each other, i.e. top-1 and top-2 distances are much difference + good = [ + m1 + for (m1, m2) in matches + if m1.distance < self.distance_threshold * m2.distance + ] + + # find homography matrix + if len(good) > self.min_match_count: + # (queryIndex for the small object, trainIndex for the scene ) + src_pts = np.float32([template_kpts[m.queryIdx].pt for m in good]).reshape( + -1, 1, 2 + ) + dst_pts = np.float32([image_kpts[m.trainIdx].pt for m in good]).reshape( + -1, 1, 2 + ) + + # find homography matrix in cv2.RANSAC using good match points + M, mask = cv2.findHomography( + src_pts, dst_pts, cv2.RANSAC, self.ransac_threshold + ) + if M is not None: + # get template center in original image + h, w = gray_template_anchor.shape[:2] + pts = np.float32( + [[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]] + ).reshape(-1, 1, 2) + dst = cv2.perspectiveTransform(pts, M) + + # get convex hull of the match and its min-area surrounding box + hull = ConvexHull(dst[:, 0, :]).vertices + hull = dst[hull][:, 0, :] + hull_rect = cv2.minAreaRect(hull[:, None, :]) + hull_box = cv2.boxPoints(hull_rect) + + # compute sizes of the hull box + hull_box_size = ( + np.sqrt(np.sum((hull_box[0] - hull_box[1]) ** 2, axis=-1)), + np.sqrt(np.sum((hull_box[1] - hull_box[2]) ** 2, axis=-1)), + ) + + # verify max-size-over-min-size ratio + hull_box_ratio = np.max(hull_box_size) / np.min(hull_box_size) + template_ratio = template_feature["ratio"] + is_valid_ratio = ( + hull_box_ratio > self.valid_size_ratio_interval[0] * template_ratio + ) and ( + hull_box_ratio < self.valid_size_ratio_interval[1] * template_ratio + ) + + # verify hull-area-to-hull-box-area ratio + hull_area = Polygon(hull).area + hull_box_area = Polygon(hull_box).area + is_valid_hull_area = ( + hull_area >= self.valid_area_threshold * hull_box_area + ) + + # return score as average of inverse distance to closest match + if is_valid_hull_area and is_valid_ratio: + pts[..., 0] += template_box[0] + pts[..., 1] += template_box[1] + anchor_center = np.mean(pts[:, 0, :], axis=0).tolist() + anchor_location = pts[:, 0, :] + + dst[..., 0] = dst[..., 0] / scale + shift[0] + dst[..., 1] = dst[..., 1] / scale + shift[1] + found_center = np.mean(dst[:, 0, :], axis=0).tolist() + found_location = dst[:, 0, :] + + return ( + anchor_center, + anchor_location, + found_center, + found_location, + ) + return None, None, None, None + + def _crop_roi_and_resize(self, query_image, template_image, box): + r"""Crop ROI which possibly containing template anchor and resize it""" + # get template anchor box coordinates relative to template image size + x1, y1, x2, y2 = box + x, y = x1 / template_image.shape[1], y1 / template_image.shape[0] + w = (x2 - x1) / template_image.shape[1] + h = (y2 - y1) / template_image.shape[0] + + # crop ROI + pad_ratio = (self.roi_to_template_box_ratio - 1.0) / 2 + x1 = max(min(x - w * pad_ratio, 1.0), 0.0) + y1 = max(min(y - h * pad_ratio, 1.0), 0.0) + x2 = max(min(x + w * self.roi_to_template_box_ratio, 1.0), 0.0) + y2 = max(min(y + h * self.roi_to_template_box_ratio, 1.0), 0.0) + x1, y1 = int(x1 * query_image.shape[1]), int(y1 * query_image.shape[0]) + x2, y2 = int(x2 * query_image.shape[1]), int(y2 * query_image.shape[0]) + query_image = query_image[y1:y2, x1:x2] + + # resize ROI + query_image_max_size = max(query_image.shape[:2]) + if self.image_max_size and query_image_max_size > self.image_max_size: + ratio = self.image_max_size / query_image_max_size + query_image = cv2.resize(query_image, (0, 0), fx=ratio, fy=ratio) + else: + ratio = 1.0 + + return query_image, (x1, y1), ratio diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/infer_img_template_aligner_delete.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/infer_img_template_aligner_delete.py new file mode 100755 index 0000000..b9ccbf8 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/infer_img_template_aligner_delete.py @@ -0,0 +1,86 @@ +from src.config.sift_based_aligner import config +from src.modules.sift_based_aligner import SIFTBasedAligner +from src.utils.common import read_json +from argparse import ArgumentParser +import os +import cv2 + +num_pages_dict = {"pos01": 2, "pos04": 2} + +exception_files = {"pos01": ["SKM_458e Ag22101217490_0.png"]} # only page_1 + +template_path_dict = { + "pos01": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos01.json", + "pos04": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos04.json", +} + + +def reformat(config, doc_id): + # template_infos = config['template_info'] + + template_path = template_path_dict[doc_id] + template_info = read_json(template_path) + + # num_page = num_pages_dict[doc_id] + + config["template_info"] = template_info + return config + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("--img_dir") + parser.add_argument("--output") + parser.add_argument("--doc_id", help="pos01/pos04", default="pos01") + args = parser.parse_args() + + # make dir + if not os.path.exists(args.output): + os.makedirs(args.output, exist_ok=True) + error_dir = args.output + "_error" + if not os.path.exists(error_dir): + os.makedirs(error_dir, exist_ok=True) + + doc_id = args.doc_id + print("DOCID: ", doc_id) + + # load img paths + img_paths = [args.img_dir] + images = [cv2.imread(img_path) for img_path in img_paths] + print("total samples: ", len(img_paths)) + + # reformat config + config = reformat(config, doc_id=args.doc_id) + + # aligner init + aligner = SIFTBasedAligner(**config) + + metadata = [{"doc_type": "{}_1".format(doc_id), "img_path": img_paths[0]}] + # metadata = [{'doc_type': 'edit_form_1_1' if "_0.jpg" in img_path else 'edit_form_1_2', 'img_path': img_path} for img_path in img_paths] + transformed_images = aligner.run(images, metadata) + + print(len(img_paths), len(transformed_images)) + + error_count = 0 + for idx in range(len(transformed_images)): + img_name = os.path.basename(img_paths[idx]) + img_outpath = os.path.join(args.output, img_name) + img_out = transformed_images[idx] + doc_type = metadata[idx]["doc_type"] + field_boxes = config["template_info"][doc_type]["fields"] + + for bbox in field_boxes: + x, y = bbox["position"]["left"], bbox["position"]["top"] + w, h = bbox["size"]["width"], bbox["size"]["height"] + x1, y1, x2, y2 = int(x), int(y), int(x + w), int(y + h) + cv2.rectangle(img_out, (x1, y1), (x2, y2), color=(255, 0, 0), thickness=2) + + if img_out is not None: + print("Write to: ", img_outpath) + cv2.imwrite(img_outpath, img_out) + else: + error_count += 1 + print("Image None: ", img_paths[idx]) + cv2.imwrite(os.path.join(error_dir, img_name), images[idx]) + + print("Num error cases: ", error_count) diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_crop_lines.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_crop_lines.py new file mode 100755 index 0000000..70b5d85 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_crop_lines.py @@ -0,0 +1,93 @@ +from argparse import ArgumentParser +import cv2 +import glob +import os +import time +import tqdm + +from src.modules.line_parser import TemplateBoxParser +from src.config.line_parser import TEMPLATE_BOXES +from src.utils.common import read_json, get_doc_id_with_page + + +""" +Crop per form (2 page) +""" + +template_path_dict = { + "pos01": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos01.json", + "pos04": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos04.json", + "pos02": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos02.json", + "pos03": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos03.json", + "pos08": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos08.json", + "pos05": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos05.json", + "pos06": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos06.json", +} + + +def identity_page(img_path, doc_id): + page_type = "" + if "_0.jpg" in img_path: + page_number = 1 + elif "_1.jpg" in img_path: + page_number = 2 + else: + idx = int(float(img_path.split(".jpg")[0].split("_")[-1])) + if idx % 2 == 0: + page_number = 1 + else: + page_number = 2 + + doc_template_id = "{}_page_{}".format(doc_id, page_number) + + return doc_template_id # page_1 / page_2 + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("--img_dir") + parser.add_argument("--out_dir") + parser.add_argument("--doc_id", help="pos01/pos02", default="pos01") + args = parser.parse_args() + + line_parser = TemplateBoxParser() + + if not os.path.exists(args.out_dir): + os.makedirs(args.out_dir) + + img_paths = glob.glob(args.img_dir + "/*") + print("Len imgs: ", len(img_paths)) + + template_info = read_json(template_path_dict[args.doc_id]) + + crop_metadata = {} + pages = ["page_"] + for page in pages: + metadata = {"boxes": [], "box_types": []} + + crop_metadata[page] = metadata + + count = 0 + for idx, img_path in tqdm.tqdm(enumerate(img_paths)): + aligned_images = cv2.imread(img_path) + + doc_template_id = get_doc_id_with_page(img_path, args.doc_id) + # print(img_path, doc_template_id, aligned_images) + + cropped_images = line_parser.run( + [aligned_images], + metadata=[{"boxes": template_info[doc_template_id]["fields"]}], + ) + + count += len(cropped_images[0]) + for id_img, crop_img in enumerate(cropped_images[0]): + out_path = os.path.join( + args.out_dir, + os.path.splitext(os.path.basename(img_path))[0] + + "_" + + str(id_img) + + ".jpg", + ) + cv2.imwrite(out_path, crop_img) + + print("Total: ", count) diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_ocr.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_ocr.py new file mode 100755 index 0000000..2a2106c --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/samples/run_ocr.py @@ -0,0 +1,46 @@ +from src.modules.ocr_module import OcrEngine +from argparse import ArgumentParser +import os +import glob +import cv2 +import time +from src.utils.visualize import visualize_ocr_output + + +def main(img_dir, out_dir, device="cuda:0"): + det_cfg = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/weights/yolox_s_8x8_300e_cocotext_1280.py" + det_ckpt = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/weights/best_bbox_mAP_epoch_100.pth" + cls_cfg = "/home/sds/datnt/mmocr/logs/satrn_big_2022-04-25/satrn_big.py" + cls_ckpt = "/home/sds/datnt/mmocr/logs/satrn_big_2022-04-25/best.pth" + + engine = OcrEngine(det_cfg, det_ckpt, cls_cfg, cls_ckpt, device=device) + + # img_dir = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/raw_images/POS01" + # out_dir = "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/outputs/visualize_ocr/POS01" + + if not os.path.exists(out_dir): + os.makedirs(out_dir) + img_paths = glob.glob(img_dir + "/*") + for img_path in img_paths: + img = cv2.imread(img_path) + t1 = time.time() + res = engine.run_image(img) + + visualize_ocr_output( + res, + img, + vis_dir=out_dir, + prefix_name=os.path.splitext(os.path.basename(img_path))[0], + font_path="./assest/visualize/times.ttf", + is_vis_kie=False, + ) + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("--img_dir") + parser.add_argument("--out_dir") + parser.add_argument("--device", help="cuda:0 / cuda:0") + args = parser.parse_args() + + main(args.img_dir, args.out_dir, args.device) diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/serve_model.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/serve_model.py new file mode 100755 index 0000000..731c213 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/serve_model.py @@ -0,0 +1,139 @@ +import yaml +import numpy as np + +from .config.sift_based_aligner import config +from .modules.sift_based_aligner import SIFTBasedAligner +from .utils.common import read_json +from common.utils.word_formation import Word, words_to_lines + + +def calc_pct_overlapped_area(bboxes1, bboxes2): + # assert True + assert len(bboxes1.shape) == 2 and bboxes1.shape[1] == 4 + assert len(bboxes2.shape) == 2 and bboxes2.shape[1] == 4 + + bboxes1 = bboxes1.copy() + bboxes2 = bboxes2.copy() + + x11, y11, x12, y12 = np.split(bboxes1, 4, axis=1) + x21, y21, x22, y22 = np.split(bboxes2, 4, axis=1) + xA = np.maximum(x11, np.transpose(x21)) + yA = np.maximum(y11, np.transpose(y21)) + xB = np.minimum(x12, np.transpose(x22)) + yB = np.minimum(y12, np.transpose(y22)) + interArea = np.maximum((xB - xA + 1), 0) * np.maximum((yB - yA + 1), 0) + boxBArea = (x22 - x21 + 1) * (y22 - y21 + 1) + boxBArea = np.tile(boxBArea, (1, len(bboxes1))) + iou = interArea / boxBArea.T + return iou + + +class Predictor: + def __init__(self, setting_file="setting.yml"): + with open(setting_file) as f: + # use safe_load instead load + self.setting = yaml.safe_load(f) + self.config = self.setting["templates"]["config"] + + def _align(self, config, temp_name, image): + # init aligner + aligner = SIFTBasedAligner(**config) + metadata = [{"doc_type": temp_name}] + aligned_images = aligner.run_alige([image], metadata) + aligned_image = aligned_images[0] + return aligned_image + + def _reorder_words(self, boxes): + arr_x1 = boxes[:, 0] + return np.argsort(arr_x1) + + def _asign_words_to_field( + self, boxes, contents, types, page_template_info, threshold=0.8 + ): + field_coords = [element["box"] for element in page_template_info["fields"]] + field_coords = np.array(field_coords) + field_coords = field_coords.astype(float) + field_coords = field_coords.astype(int) + field_names = [element["label"] for element in page_template_info["fields"]] + field_types = [ + "checkbox" if element["label"].startswith("checkbox") else "word" + for element in page_template_info["fields"] + ] + boxes = np.array(boxes[0]) + print(field_coords) + print(boxes) + print(field_coords.shape, boxes.shape) + area_pct = calc_pct_overlapped_area(field_coords, boxes) + + results = dict() + for row_score, field, _type in zip(area_pct, field_names, field_types): + if _type == "checkbox": + inds = np.where(row_score > threshold)[0] + inds = [i for i in inds if types[i] == "checkbox"] + results[field] = dict() + results[field]["value"] = contents[inds[0]] if len(inds) > 0 else None + results[field]["boxes"] = boxes[inds[0]] if len(inds) > 0 else None + else: + inds = np.where(row_score > threshold)[0] + field_word_boxes = boxes[inds] + sorted_inds = inds[self._reorder_words(field_word_boxes)] + + results[field] = dict() + results[field]["words"] = [contents[i] for i in sorted_inds] + lines = self._get_line_content(boxes[sorted_inds], results[field]["words"]) + results[field]["value"] = '\n'.join(lines).strip() + results[field]["boxes"] = boxes[sorted_inds] + return results + + def _get_line_content(self, boxes, contents): + list_words = [] + for box, text in zip(boxes, contents): + bndbox = [int(j) for j in box] + list_words.append( + Word( + text=text, + bndbox=bndbox, + ) + ) + list_lines, _ = words_to_lines(list_words) + line_texts = [line.text for line in list_lines] + return line_texts + + + def align_image(self, image, template_json, template_image_dir, temp_name): + """Run TemplateMaching main + + Args: + documents (dict): document then document classification + template_json (dict): + example: + { + "pos01": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/add_fields/pos01.json", + "pos04": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/add_fields/pos04.json", + "pos02": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/add_fields/pos02.json", + "pos03": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/pos03_fields_checkbox.json", + "pos08": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/add_fields/pos08.json", + "pos05": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/add_fields/pos05.json", + "pos06": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/add_fields/pos06.json", + "cccd_front": "/mnt/ssd500/hoanglv/Projects/FWD/template_matching_hoanglv/data/json/cccd_front.json", + } + template_image_dir (str): path to template image dir + + Returns: + dict: content then template matching + """ + + config = self.config.copy() + config["template_info"] = template_json + config["template_im_dir"] = template_image_dir + aligned_image = self._align(config, temp_name, image) + return aligned_image + + def template_based_extractor(self, batch_boxes, texts, doc_page, template_json): + field_data = self._asign_words_to_field( + batch_boxes, + texts, + doc_page["types"], + template_json, + ) + return field_data \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/common.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/common.py new file mode 100755 index 0000000..26e769c --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/common.py @@ -0,0 +1,24 @@ +import os +import json + + +def get_doc_id_with_page(img_path, doc_id): + if "_0.jpg" in img_path: + doc_page = "{}_page_1".format(doc_id) + elif "_1.jpg" in img_path: + doc_page = "{}_page_2".format(doc_id) + else: + idx = int(os.path.splitext(os.path.basename(img_path))[0].split("_")[-1]) + # idx = int(float(img_path.split(".jpg")[0].split("_")[-1])) + if idx % 2 == 0: + doc_page = "{}_page_1".format(doc_id) + else: + doc_page = "{}_page_2".format(doc_id) + + return doc_page + + +def read_json(json_path): + with open(json_path, "r", encoding="utf8") as f: + data = json.load(f) + return data diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/image_calib.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/image_calib.py new file mode 100755 index 0000000..2d713bf --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/image_calib.py @@ -0,0 +1,803 @@ +import time, os +import cv2, math +import numpy as np +import pathlib +import math + + +RADIAN_PER_DEGREE = 0.0174532 +debug = False + + +def crop_image(input_img, bbox, bbox_ratio=1.0, offset_x=0, offset_y=0): + left = int(bbox_ratio * bbox[0]) + top = int(bbox_ratio * bbox[1]) + width = int(bbox_ratio * bbox[2]) + height = int(bbox_ratio * bbox[3]) + crop_img = input_img[ + top + offset_y : top + height + offset_y, + left + offset_x : left + width + offset_x, + ] + return crop_img + + +def resize_normalize(img, normalize_width=1654): + w = img.shape[1] + h = img.shape[0] + resize_ratio = normalize_width / w + normalize_height = round(h * resize_ratio) + resize_img = cv2.resize( + img, (normalize_width, normalize_height), interpolation=cv2.INTER_CUBIC + ) + # cv2.imshow('resize img', resize_img) + # cv2.waitKey(0) + return resize_ratio, resize_img + + +def draw_bboxes(img, bboxes, window_name="draw bboxes"): + # e.g: bboxes= [(0,0),(0,5),(5,5),(5,0)] + if len(img.shape) != 3: + img_RGB = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + else: + img_RGB = img + color_red = (0, 0, 255) + for idx, bbox in enumerate(bboxes): + cv2.line(img_RGB, bbox[0], bbox[1], color=color_red, thickness=2) + cv2.line(img_RGB, bbox[1], bbox[2], color=color_red, thickness=2) + cv2.line(img_RGB, bbox[2], bbox[3], color=color_red, thickness=2) + cv2.line(img_RGB, bbox[3], bbox[0], color=color_red, thickness=2) + + font = cv2.FONT_HERSHEY_SIMPLEX + + # org + org = (bbox[0][0], bbox[0][1] - 5) + + # fontScale + fontScale = 1.5 + + # Blue color in BGR + color = (255, 0, 0) + + # Line thickness of 2 px + thickness = 2 + + # Using cv2.putText() method + img_RGB = cv2.putText( + img_RGB, str(idx), org, font, fontScale, color, thickness, cv2.LINE_AA + ) + + img_RGB = cv2.resize( + img_RGB, (int(img_RGB.shape[1] / 2), int(img_RGB.shape[0] / 2)) + ) + cv2.imshow(window_name, img_RGB) + cv2.waitKey(0) + + +class Template_info: + def __init__( + self, + name, + template_path, + field_bboxes, + field_rois_extend=(1.0, 1.0), + field_search_areas=None, + confidence=0.7, + scales=(0.9, 1.1, 0.1), + rotations=(-2, 2, 2), + normalize_width=1654, + ): # 1654 + self.name = name + self.template_img = cv2.imread(template_path, 0) + self.normalize_width = normalize_width + self.resize_ratio, self.template_img = resize_normalize( + self.template_img, normalize_width + ) + self.template_width = self.template_img.shape[1] + self.template_height = self.template_img.shape[0] + self.confidence = confidence + self.field_bboxes = field_bboxes + self.field_rois_extend = field_rois_extend + self.field_search_areas = field_search_areas + self.field_locs = [] + self.list_field_samples = [] + for idx, bbox in enumerate(self.field_bboxes): + bbox = self.resize_bbox(bbox, self.resize_ratio) + + field = dict() + field["name"] = str(idx) + field["loc"] = (bbox[0] + (bbox[2] - 1) / 2, bbox[1] + (bbox[3] - 1) / 2) + self.field_locs.append(field["loc"]) + field["search_area"] = None + if field_search_areas is not None: + field["search_area"] = self.resize_bbox( + field_search_areas[idx], self.resize_ratio + ) + else: + field["data"] = self.crop_image(self.template_img, bbox) + # cv2.imwrite(field['name']+'.jpg', field['data']) + field_w = max(field["data"].shape[1], 50) + field_h = max(field["data"].shape[0], 50) + extend_x = int(self.field_rois_extend[0] * field_w) + extend_y = int(self.field_rois_extend[1] * field_h) + left = max(int(field["loc"][0] - field_w / 2 - extend_x), 0) + top = max(int(field["loc"][1] - field_h / 2 - extend_y), 0) + right = min( + int(field["loc"][0] + field_w / 2 + extend_x), self.template_width + ) + bottom = min( + int(field["loc"][1] + field_h / 2 + extend_y), self.template_height + ) + width = right - left + height = bottom - top + field["search_area"] = [left, top, width, height] + + self.createSamples(field, scales, rotations) + self.list_field_samples.append(field) + + def resize_bbox(self, bbox, resize_ratio): + for i in range(len(bbox)): + bbox[i] = round(bbox[i] * resize_ratio) + return bbox + + def crop_image(self, input_img, bbox, offset_x=0, offset_y=0): + # logger.info('crop') + crop_img = input_img[ + bbox[1] + offset_y : bbox[1] + bbox[3] + offset_y, + bbox[0] + offset_x : bbox[0] + bbox[2] + offset_x, + ] + return crop_img + + def createSamples(self, field, scales, rotations): + # logger.info('Add_template', field['name']) + list_scales = [] + list_rotations = [] + + num_scales = round((scales[1] - scales[0]) / scales[2]) + 1 + num_rotations = round((rotations[1] - rotations[0]) / rotations[2]) + 1 + for i in range(num_scales): + list_scales.append(round(scales[0] + i * scales[2], 4)) + for i in range(num_rotations): + list_rotations.append(round(rotations[0] + i * rotations[2], 4)) + + field["list_samples"] = [] + field_data = field["data"] + w = field_data.shape[1] + h = field_data.shape[0] + bgr_val = int( + ( + int(field_data[0][0]) + + int(field_data[0][w - 1]) + + int(field_data[h - 1][w - 1]) + + int(field_data[h - 1][0]) + ) + / 4 + ) + for rotation in list_rotations: + abs_rotation = abs(rotation) + if w < h: + if abs_rotation <= 45: + sa = math.sin(abs_rotation * RADIAN_PER_DEGREE) + ca = math.cos(abs_rotation * RADIAN_PER_DEGREE) + newHeight = (int)((h - w * sa) / ca) + # newHeight = newHeight - ((h - newHeight) % 2) + szOutput = (w, newHeight) + else: + sa = math.sin((90 - abs_rotation) * RADIAN_PER_DEGREE) + ca = math.cos((90 - abs_rotation) * RADIAN_PER_DEGREE) + newWidth = (int)((h - w * sa) / ca) + # newWidth = newWidth - ((w - newWidth) % 2) + szOutput = (newWidth, w) + else: + if abs_rotation <= 45: + sa = math.sin(abs_rotation * RADIAN_PER_DEGREE) + ca = math.cos(abs_rotation * RADIAN_PER_DEGREE) + newWidth = (int)((w - h * sa) / ca) + # newWidth = newWidth - ((w - newWidth) % 2) + szOutput = (newWidth, h) + else: + sa = math.sin((90 - rotation) * RADIAN_PER_DEGREE) + ca = math.cos((90 - rotation) * RADIAN_PER_DEGREE) + newHeight = (int)((w - h * sa) / ca) + # newHeight = newHeight - ((h - newHeight) % 2) + szOutput = (h, newHeight) + + (h, w) = field_data.shape[:2] + (cX, cY) = (w / 2, h / 2) + M = cv2.getRotationMatrix2D((cX, cY), -rotation, 1.0) + cos = np.abs(M[0, 0]) + sin = np.abs(M[0, 1]) + nW = int((h * sin) + (w * cos)) + nH = int((h * cos) + (w * sin)) + M[0, 2] += (nW / 2) - cX + M[1, 2] += (nH / 2) - cY + rotated = cv2.warpAffine(field_data, M, (nW, nH), borderValue=bgr_val) + + # (h_rot, w_rot) = rotated.shape[:2] + # (cX_rot, cY_rot) = (w_rot // 2, h_rot // 2) + # pt1=(int(cX_rot-3), int(cY_rot-3)) + # pt2=(int(cX_rot+3), int(cY_rot+3)) + # pt3=(int(cX_rot-3), int(cY_rot+3)) + # pt4=(int(cX_rot+3), int(cY_rot-3)) + # cv2.line(rotated,pt1,pt2,color=255) + # cv2.line(rotated,pt3,pt4,color=255) + + offset_X = int((nW - szOutput[0]) / 2) + offset_Y = int((nH - szOutput[1]) / 2) + + crop_rotated = rotated[ + offset_Y : nH - offset_Y - 1, offset_X : nW - offset_X - 1 + ] + crop_w = crop_rotated.shape[1] + crop_h = crop_rotated.shape[0] + # rint('origin size', crop_w, crop_h) + + for scale in list_scales: + temp = dict() + temp["rotation"] = rotation + temp["scale"] = scale + # logger.info('scale', scale, ', rotation', rotation) + crop_rotate_resize = cv2.resize( + crop_rotated, (int(scale * crop_w), int(scale * crop_h)) + ) + # logger.info('resize size', int(scale * crop_w), int(scale * crop_h)) + temp["data"] = crop_rotate_resize + if debug: + cv2.imshow("result", crop_rotated) + cv2.imshow("result_crop", crop_rotate_resize) + ch = cv2.waitKey(0) + if ch == 27: + cv2.imwrite("result.jpg", crop_rotated) + break + field["list_samples"].append(temp) + + def draw_template(self, src_img=None, crop=False, crop_dir=""): + list_bboxes = [] + for idx, bbox in enumerate(self.field_bboxes): + left = bbox[0] + top = bbox[1] + right = bbox[0] + bbox[2] + bottom = bbox[1] + bbox[3] + if crop: + crop_img = crop_image(self.template_img, bbox) + cv2.imwrite( + os.path.join(crop_dir, self.name + "_field_" + str(idx) + ".jpg"), + crop_img, + ) + bboxes = [(left, top), (right, top), (right, bottom), (left, bottom)] + list_bboxes.append(bboxes) + if src_img is None: + draw_bboxes(self.template_img, list_bboxes) + else: + draw_bboxes(src_img, list_bboxes, window_name="new") + + def get_template_img(self): + return self.template_img + + +class MatchingTemplate: + def __init__(self, initTemplate=False): + self.template_dir = "" + self.template_names = [] + self.template_list = [] + self.template_dir = os.path.join( + pathlib.Path(__file__).parent.absolute(), "templates" + ) + if initTemplate: + self.initTemplate() + self.matching_results = [] + self.activate_template = "" + + def initTemplate(self, template_dir=None, list_template_name=[]): + kk = 1 + + def add_template( + self, + template_name, + template_path, + field_bboxes, + field_rois_extend=(1.0, 1.0), + field_search_areas=None, + confidence=0.7, + scales=(0.9, 1.1, 0.1), + rotations=(-2, 2, 2), + normalize_width=1654, + ): + if not os.path.exists(template_path): + print("MatchingTemplate. No template path:", template_path) + return + print("MatchingTemplate. Init template", "[" + str(template_name) + "]") + temp = Template_info( + template_name, + template_path, + field_bboxes, + field_rois_extend, + field_search_areas, + confidence, + scales, + rotations, + normalize_width=normalize_width, + ) + self.template_list.append(temp) + + def clear_template(self): + self.template_list.clear() + + def check_template(self, template_name): + template_data = None + for template in self.template_list: + if template.name == template_name: + self.activate_template = template_name + template_data = template + break + if template_data is None: + print("MatchingTemplate. No template name", template_name) + # logger.info('Cannot find template', template_name, 'in database') + return template_data + + def draw_template(self, template_name, src_img=None, crop=False, crop_dir=""): + template_data = self.check_template(template_name) + if template_data is None: + return + template_data.draw_template(src_img, crop=crop, crop_dir=crop_dir) + + def get_matching_result(self, final_locx, final_locy, final_sample): + x0 = final_locx + y0 = final_locy + + x1 = x0 - (final_sample["data"].shape[1] / 2) * final_sample["scale"] + y1 = y0 - (final_sample["data"].shape[0] / 2) * final_sample["scale"] + x2 = x0 + (final_sample["data"].shape[1] / 2) * final_sample["scale"] + y2 = y0 + (final_sample["data"].shape[0] / 2) * final_sample["scale"] + + ## + ca = math.cos(final_sample["rotation"] * RADIAN_PER_DEGREE) + sa = math.sin(final_sample["rotation"] * RADIAN_PER_DEGREE) + rx1 = round((x0 + (x1 - x0) * ca - (y1 - y0) * sa)) + ry1 = round((y0 + (x1 - x0) * sa + (y1 - y0) * ca)) + rx2 = round((x0 + (x2 - x0) * ca - (y1 - y0) * sa)) + ry2 = round((y0 + (x2 - x0) * sa + (y1 - y0) * ca)) + rx3 = round((x0 + (x2 - x0) * ca - (y2 - y0) * sa)) + ry3 = round((y0 + (x2 - x0) * sa + (y2 - y0) * ca)) + rx4 = round((x0 + (x1 - x0) * ca - (y2 - y0) * sa)) + ry4 = round((y0 + (x1 - x0) * sa + (y2 - y0) * ca)) + return [(rx1, ry1), (rx2, ry2), (rx3, ry3), (rx4, ry4)] + + def find_field( + self, input_img, field, thres=0.3, fast=True, method="cv2.TM_CCORR_NORMED" + ): + max_conf = 0 + final_locx, final_locy = -1, -1 + final_sample = None + + process_img = input_img.copy() + if len(input_img.shape) == 3: # BGR + process_img = cv2.cvtColor(input_img, cv2.COLOR_BGR2GRAY) + + if fast: + left = field["search_area"][0] + top = field["search_area"][1] + right = field["search_area"][0] + field["search_area"][2] + bottom = field["search_area"][1] + field["search_area"][3] + process_img = process_img[top:bottom, left:right] + try: + if not os.path.exists(os.path.join(self.template_dir, "crop")): + os.makedirs(os.path.join(self.template_dir, "crop")) + # print('MatchingTemplate. find_field. Write process image to', + # os.path.join(self.template_dir, 'crop', self.activate_template + '_' + field['name'] + '.jpg')) + # cv2.imwrite(os.path.join(self.template_dir, 'crop', self.activate_template + '_' + field['name'] + '.jpg'), + # process_img) + except: + print("Except find field : make_dir func") + pass + for sample in field["list_samples"]: + sample_data = sample["data"] + res = cv2.matchTemplate(process_img, sample_data, 5) + min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) + logger.info( + "Score:", + round(max_val, 4), + "Scale:", + sample["scale"], + "Angle:", + sample["rotation"], + max_loc[0] + sample_data.shape[1] / 2, + max_loc[1] + sample_data.shape[0] / 2, + ) + if max_val > max_conf: + max_conf = max_val + final_locx, final_locy = ( + max_loc[0] + sample_data.shape[1] / 2, + max_loc[1] + sample_data.shape[0] / 2, + ) + final_sample = sample + if fast: + final_locx, final_locy = ( + final_locx + field["search_area"][0], + final_locy + field["search_area"][1], + ) + + if max_conf >= thres: + print( + "Score:", + round(max_conf, 4), + "Scale:", + final_sample["scale"], + "Angle:", + final_sample["rotation"], + "Location:", + final_locx, + final_locy, + ) + else: # cannot find field + print( + "MatchingTemplate. find_field. Cannot find field! Max score:", + round(max_conf, 4), + ) + return 0, -1, -1 + + self.matching_results = self.get_matching_result( + final_locx, final_locy, final_sample + ) + # draw_bboxes(input_img, [self.matching_results], field['name']) + + return max_conf, final_locx, final_locy + + def find_template( + self, template_name, src_img, fast=True, threshold=0.7 + ): # src_img is cv2 image + # logger.info('\nCalib template', template_name) + template_data = self.check_template(template_name) + if template_data is None: + return + + resize_ratio, src_img = resize_normalize(src_img, template_data.normalize_width) + gray_img = src_img + if len(src_img.shape) == 3: # BGR + gray_img = cv2.cvtColor(src_img, cv2.COLOR_BGR2GRAY) + list_pts = [] + + for idx, field in enumerate(template_data.list_field_samples): + # logger.info(field['name']) + conf, loc_x, loc_y = self.find_field( + gray_img, field, fast=fast, thres=template_data.confidence + ) + if conf > threshold: + list_pts.append((loc_x, loc_y)) + return list_pts + + def calib_template( + self, + template_name, + src_img, + fast=True, + simi_triangle_thres=4, + simi_line_thres=3, + ): # src_img is cv2 image + template_data = self.check_template(template_name) + if template_data is None: + return False, None + print( + "MatchingTemplate. Calib template", + template_name, + ", width", + template_data.template_width, + ", height", + template_data.template_height, + ) + + # src_img = cv2.resize(src_img, (template_data.template_width, template_data.template_height)) + resize_ratio, src_img = resize_normalize(src_img, template_data.normalize_width) + gray_img = src_img + if len(src_img.shape) == 3: # BGR + gray_img = cv2.cvtColor(src_img, cv2.COLOR_BGR2GRAY) + list_pts = [] + + for idx, field in enumerate(template_data.list_field_samples): + # logger.info(field['name']) + import time + + begin = time.time() + conf, loc_x, loc_y = self.find_field( + gray_img, field, fast=fast, thres=template_data.confidence + ) + end = time.time() + print("calib_template. find field time:", 1000 * (end - begin), "ms") + list_pts.append((loc_x, loc_y)) + + src_pts = np.asarray(list_pts, dtype=np.float32) + dst_pts = np.asarray(template_data.field_locs, dtype=np.float32) + trans_img = src_img + calib_success = True + if len(src_pts) == 2: # affine transformation with 1 synthetic point + calib_success = check_angle_between_2_lines( + template_data.field_locs, list_pts, diff_thres=simi_line_thres + ) + inter_pts = ( + list_pts[0][0] + list_pts[0][1] - list_pts[1][1], + list_pts[0][1] + list_pts[1][0] - list_pts[0][0], + ) + list_pts.append(inter_pts) + inter_field_pts = [template_data.field_locs[0], template_data.field_locs[1]] + inter_field_pts.append( + ( + template_data.field_locs[0][0] + + template_data.field_locs[0][1] + - template_data.field_locs[1][1], + template_data.field_locs[0][1] + + template_data.field_locs[1][0] + - template_data.field_locs[0][0], + ) + ) + + src_pts = np.asarray(list_pts, dtype=np.float32) + dst_pts = np.asarray(inter_field_pts, dtype=np.float32) + print("dst_pts", dst_pts) + affine_trans = cv2.getAffineTransform(src_pts, dst_pts) + trans_img = cv2.warpAffine( + src_img, + affine_trans, + (template_data.template_width, template_data.template_height), + ) + elif len(src_pts) == 3: # affine transformation + calib_success = check_similar_triangle( + template_data.field_locs, list_pts, diff_thres=simi_triangle_thres + ) + affine_trans = cv2.getAffineTransform(src_pts, dst_pts) + trans_img = cv2.warpAffine( + src_img, + affine_trans, + (template_data.template_width, template_data.template_height), + ) + elif len(src_pts) > 3: # perspective transformation + perspective_trans, status = cv2.findHomography(src_pts, dst_pts) + w, h = template_data.template_width, template_data.template_height + trans_img = cv2.warpPerspective(src_img, perspective_trans, (w, h)) + else: + kk = 1 + return calib_success, trans_img + + def calib_template_2( + self, + template_name, + src_img, + fast=True, + simi_triangle_thres=4, + simi_line_thres=3, + ): # src_img is cv2 image + template_data = self.check_template(template_name) + if template_data is None: + return False, None, None + print( + "MatchingTemplate. Calib template", + template_name, + ", width", + template_data.template_width, + ", height", + template_data.template_height, + ) + + # src_img = cv2.resize(src_img, (template_data.template_width, template_data.template_height)) + resize_ratio, src_img = resize_normalize(src_img, template_data.normalize_width) + gray_img = src_img + if len(src_img.shape) == 3: # BGR + gray_img = cv2.cvtColor(src_img, cv2.COLOR_BGR2GRAY) + list_pts = [] + calib_success = True + for idx, field in enumerate(template_data.list_field_samples): + # logger.info(field['name']) + import time + + begin = time.time() + conf, loc_x, loc_y = self.find_field( + gray_img, field, fast=fast, thres=template_data.confidence + ) + end = time.time() + print("calib_template. find field time:", 1000 * (end - begin), "ms") + list_pts.append((loc_x, loc_y)) + + src_pts = np.asarray(list_pts, dtype=np.float32) + dst_pts = np.asarray(template_data.field_locs, dtype=np.float32) + trans_img = src_img + + if len(src_pts) == 2: # affine transformation with 1 synthetic point + calib_success = check_angle_between_2_lines( + template_data.field_locs, list_pts, diff_thres=simi_line_thres + ) + inter_pts = ( + list_pts[0][0] + list_pts[0][1] - list_pts[1][1], + list_pts[0][1] + list_pts[1][0] - list_pts[0][0], + ) + list_pts.append(inter_pts) + inter_field_pts = [template_data.field_locs[0], template_data.field_locs[1]] + inter_field_pts.append( + ( + template_data.field_locs[0][0] + + template_data.field_locs[0][1] + - template_data.field_locs[1][1], + template_data.field_locs[0][1] + + template_data.field_locs[1][0] + - template_data.field_locs[0][0], + ) + ) + + src_pts = np.asarray(list_pts, dtype=np.float32) + dst_pts = np.asarray(inter_field_pts, dtype=np.float32) + # print('dst_pts', dst_pts) + affine_trans = cv2.getAffineTransform(src_pts, dst_pts) + trans_img = cv2.warpAffine( + src_img, + affine_trans, + (template_data.template_width, template_data.template_height), + borderValue=(255, 255, 255), + ) + elif len(src_pts) == 3: # affine transformation + calib_success = check_similar_triangle( + template_data.field_locs, list_pts, diff_thres=simi_triangle_thres + ) + affine_trans = cv2.getAffineTransform(src_pts, dst_pts) + trans_img = cv2.warpAffine( + src_img, + affine_trans, + (template_data.template_width, template_data.template_height), + ) + elif len(src_pts) > 3: # perspective transformation + perspective_trans, status = cv2.findHomography(src_pts, dst_pts) + w, h = template_data.template_width, template_data.template_height + trans_img = cv2.warpPerspective(src_img, perspective_trans, (w, h)) + else: + kk = 1 + return calib_success, trans_img, dst_pts + + def crop_image(self, input_img, bbox, offset_x=0, offset_y=0): + logger.info("crop") + crop_img = input_img[ + bbox[1] + offset_y : bbox[1] + bbox[3] + offset_y, + bbox[0] + offset_x : bbox[0] + bbox[2] + offset_x, + ] + return crop_img + + +def test_calib_multi(template_name, src_img_dir): + list_files = get_list_file_in_folder(src_img_dir) + for idx, f in enumerate(list_files): + print(idx, f) + test_calib(template_name, os.path.join(src_img_dir, f)) + + +def test_calib(template_name, src_img_path): + src_img = cv2.imread(src_img_path) + begin_init = time.time() + + match = MatchingTemplate(initTemplate=True) + # match.add_template(template_name=template_name, + # template_path='C:/Users/titik/Desktop/idcard_2June/test_MireaAsset/contract.JPG', + # field_bboxes=[[184, 1256, 242, 142]], + # field_rois_extend = (10.0,0.3), + # field_search_areas=None, + # # confidence=0.7, + # # scales=(0.95, 1.05, 0.05), + # # rotations=(-1, 1, 1)) + # confidence=0.2, + # scales=(1.0, 1.0, 0.1), + # rotations=(0, 0, 1)) + end_init = time.time() + logger.info("Time init:", end_init - begin_init, "seconds") + # match.draw_template(template_name) + begin = time.time() + calib_success, calib_img = match.calib_template(template_name, src_img, fast=True) + + # base_name = os.path.basename(src_img_path) + # cv2.imwrite(os.path.join(output_dir, base_name.replace('.jpg', '_trans.jpg')), calib_img) + end = time.time() + print("Time:", end - begin, "seconds") + logger.info("Time:", end - begin, "seconds") + + debug = True + if debug: + # src_img_with_box = visualize_boxes('/home/aicr/cuongnd/text_recognition/data/SDV_invoices_mod/006.txt', src_img, + # debug=False, offset_x=-20, offset_y=-20) + # src_img_with_box = cv2.resize(src_img, (int(src_img.shape[1] / 2), int(src_img.shape[0] / 2))) + # cv2.imshow('src with boxes', src_img_with_box) + # trans_img_with_box = visualize_boxes('/home/aicr/cuongnd/text_recognition/data/SDV_invoices_mod/006.txt', + # calib_img, debug=False, offset_x=-20, offset_y=-20) + trans_img_with_box = cv2.resize( + calib_img, (int(calib_img.shape[1] / 2), int(calib_img.shape[0] / 2)) + ) + trans_img_with_box = cv2.resize( + calib_img, (calib_img.shape[1], calib_img.shape[0]) + ) + cv2.imshow("transform_with_boxes", trans_img_with_box) + base_name = os.path.basename(src_img_path) + # cv2.imwrite(src_img_path.replace(base_name, 'transform/' + base_name.replace('.jpg', '_trans.jpg')), + # trans_img_with_box) + cv2.waitKey(0) + return calib_img + + +def get_list_file_in_folder(dir, ext=["jpg", "png", "JPG", "PNG"]): + included_extensions = ext + file_names = [ + fn + for fn in os.listdir(dir) + if any(fn.endswith(ext) for ext in included_extensions) + ] + return file_names + + +def getAngle(a, b, c): + ang = math.fabs( + math.degrees( + math.atan2(c[1] - b[1], c[0] - b[0]) - math.atan2(a[1] - b[1], a[0] - b[0]) + ) + ) + return ang + 360 if ang < 0 else ang + + +def simi_aaa(a1, a2, diff_thres): + a1 = [float(i) for i in a1] + a2 = [float(i) for i in a2] + a1.sort() + a2.sort() + + # Check for AAA + diff_1 = math.fabs(a1[0] - a2[0]) + diff_2 = math.fabs(a1[1] - a2[1]) + diff_3 = math.fabs(a1[2] - a2[2]) + max_diff = max(diff_1, max(diff_2, diff_3)) + if diff_1 < diff_thres and diff_2 < diff_thres and diff_3 < diff_thres: + return max_diff, True + return max_diff, False + + +def check_similar_triangle(list_pts1, list_pts2, diff_thres=4): + list_ang1 = [ + getAngle(list_pts1[0], list_pts1[1], list_pts1[2]), + getAngle(list_pts1[1], list_pts1[2], list_pts1[0]), + getAngle(list_pts1[2], list_pts1[0], list_pts1[1]), + ] + + list_ang2 = [ + getAngle(list_pts2[0], list_pts2[1], list_pts2[2]), + getAngle(list_pts2[1], list_pts2[2], list_pts2[0]), + getAngle(list_pts2[2], list_pts2[0], list_pts2[1]), + ] + + max_diff, is_similar = simi_aaa(list_ang1, list_ang2, diff_thres) + # print('check_similar_triangle. max diff:',max_diff) + return is_similar + + +def dot_product(vA, vB): + return vA[0] * vB[0] + vA[1] * vB[1] + + +def check_angle_between_2_lines(lineA, lineB, diff_thres=2): + # Get nicer vector form + try: + vA = [(lineA[0][0] - lineA[1][0]), (lineA[0][1] - lineA[1][1])] + vB = [(lineB[0][0] - lineB[1][0]), (lineB[0][1] - lineB[1][1])] + # Get dot prod + dot_prod = dot_product(vA, vB) + # Get magnitudes + magA = dot_product(vA, vA) ** 0.5 + magB = dot_product(vB, vB) ** 0.5 + # Get cosine value + cos_ = dot_prod / magA / magB + # Get angle in radians and then convert to degrees + angle = math.acos(dot_prod / magB / magA) + # Basically doing angle <- angle mod 360 + ang_deg = math.degrees(angle) % 360 + + if ang_deg >= 180: + ang_deg = 360 - ang_deg + if ang_deg > 90: + ang_deg = 180 - ang_deg + print("check_angle_between_2_lines. angle:", ang_deg) + + if ang_deg < diff_thres: + return True + else: + return False + except: + print("check_angle_between_2_lines. something wrong") + return False diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/pdf2image.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/pdf2image.py new file mode 100755 index 0000000..1e2c30d --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/pdf2image.py @@ -0,0 +1,45 @@ +import fitz # PyMuPDF, imported as fitz for backward compatibility reasons +import os +import glob +from tqdm import tqdm +import argparse +import cv2 +from PIL import Image + + +def convert_pdf2image(file_path, outdir, img_max_size=None): + if not os.path.exists(outdir): + os.makedirs(outdir) + doc = fitz.open(file_path) # open document + # dpi = 300 # choose desired dpi here + zoom = 2 # zoom factor, standard: 72 dpi + magnify = fitz.Matrix(zoom, zoom) + for idx, page in enumerate(doc): + pix = page.get_pixmap(matrix=magnify) # render page to an image + outpath = os.path.join( + outdir, + os.path.splitext(os.path.basename(file_path))[0] + "_" + str(idx) + ".png", + ) + pix.save(outpath) + + img = Image.open(outpath) + img = img.convert("L") + # img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + img.save(outpath) + # if status: + # print("OK") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--pdf_dir", type=str) + parser.add_argument("--out_dir", type=str) + args = parser.parse_args() + # pdf_dir = "/home/sds/hoanglv/FWD_Raw_Data/Form POS01" + # outdir = "/home/sds/hoanglv/Projects/FWD/assets/test/test_image_transformer/template_aligner/pdf2image" + + pdf_paths = glob.glob(args.pdf_dir + "/*.pdf") + print(pdf_paths[:5]) + + for pdf_path in tqdm(pdf_paths): + convert_pdf2image(pdf_path, args.out_dir) diff --git a/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/visualize.py b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/visualize.py new file mode 100755 index 0000000..73978fd --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/templatebasedextraction/src/utils/visualize.py @@ -0,0 +1,271 @@ +import os +import glob +import math +import json +import random +from sys import prefix + +import cv2 +import numpy as np +import pandas as pd +from PIL import Image, ImageDraw, ImageFont + + +def visualize_ocr_output( + inputs, + image, + vis_dir, + prefix_name="img_visualize", + font_path="./times.ttf", + is_vis_kie=False, +): + """ + Visualize ocr output (box + text) and kie output (optional) + params: + inputs (dict/list[list,list]): keys {ocr, kie} + - ocr value format: list of item (polygon box, label, prob/kie_label) + - kie value format: not implemented + image (np.ndarray): BGR image + vis_dir (str): save directory + name_vis_image (str): prefix name of save image + font_path (str): path of font + is_vis_kie (bool): if True, third item is kie label + return: + + """ + # table_reconstruct_result = ehr_res['table_reconstruct_result'] + # assert 'ocr' in inputs, "not found 'ocr' field in inputs" + + # identity input format + if len(inputs) == 2 and isinstance(inputs[1][0], str): + ocr_result = [ + [box if isinstance(box[0], list) else box2poly(box), text, 1.0] + for box, text in zip(inputs[0], inputs[1]) + ] + else: + ocr_result = inputs["ocr"] + + if not os.path.exists(vis_dir): + print("Creating {} dir".format(vis_dir)) + os.makedirs(vis_dir) + + img_visual = draw_ocr_box_txt( + image=image, + annos=ocr_result, + font_path=font_path, + table_boxes=None, + cell_boxes=None, + para_boxes=None, + is_vis_kie=is_vis_kie, + ) + + paths = sorted( + glob.glob(vis_dir + "/" + prefix_name + "*"), + key=lambda path: int(path.split(".jpg")[0].split("_")[-1]), + ) + if len(paths) == 0: + idx_name = "1" + else: + idx_name = str(int(paths[-1].split(".jpg")[0].split("_")[-1]) + 1) + cv2.imwrite( + os.path.join(vis_dir, prefix_name + "_" + idx_name + ".jpg"), img_visual + ) + + +def export_to_csv(table_reconstruct_text, vis_dir, csv_name="table_text_reconstruct"): + paths = sorted( + glob.glob(vis_dir + "/" + csv_name + "*"), + key=lambda path: int(path.split(".csv")[0].split("_")[-1]), + ) + if len(paths) == 0: + idx_name = "1" + else: + idx_name = str(int(paths[-1].split(".csv")[0].split("_")[-1]) + 1) + df = pd.DataFrame(table_reconstruct_text) + df.to_csv(os.path.join(vis_dir, csv_name + "_" + idx_name + ".csv"), index=False) + + +def save_json(data, vis_dir, json_name="ehr_result"): + """save dictionary to json file + Args: + data (dict): + vis_dir (str): path to save json + json_name (str, optional): json name. Defaults to 'ehr_result'. + """ + paths = sorted( + glob.glob(vis_dir + "/" + json_name + "*"), + key=lambda path: int(path.split(".json")[0].split("_")[-1]), + ) + if len(paths) == 0: + idx_name = "1" + else: + idx_name = str(int(paths[-1].split(".json")[0].split("_")[-1]) + 1) + outpath = os.path.join(vis_dir, json_name + "_" + idx_name + ".json") + with open(outpath, "w", encoding="utf8") as f: + json.dump(data, f, ensure_ascii=False) + + +def draw_ocr_box_txt( + image, + annos, + scores=None, + drop_score=0.5, + font_path="test/fonts/latin.ttf", + table_boxes=None, + cell_boxes=None, + para_boxes=None, + is_vis_kie=False, +): + """ + Args: + image (np.ndarray / PIL): BGR image or PIL image + annos (list): (box, text, label/prob) + scores (list, optional): probality. Defaults to None. + drop_score (float, optional): . Defaults to 0.5. + font_path (str, optional): Path of font. Defaults to "test/fonts/latin.ttf". + Returns: + np.ndarray: BGR image + """ + + if is_vis_kie: + kie_labels = set([item[2] for item in annos]) + colors = { + label: ( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) + for label in kie_labels + } + + color_vis = { + "table": (255, 192, 70), + "cell": (218, 66, 15), + "paragraph": (0, 187, 148), + } + + random.seed(0) + + if isinstance(image, np.ndarray): + image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + + h, w = image.height, image.width + img_left = image.copy() + img_right = Image.new("RGB", (w, h), (255, 255, 255)) + draw_left = ImageDraw.Draw(img_left) + draw_right = ImageDraw.Draw(img_right) + for idx, (box, txt, meta_data) in enumerate(annos): + if scores is not None and scores[idx] < drop_score: + continue + + if is_vis_kie: + color = colors[meta_data] + else: + color = ( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) + draw_left.polygon( + [ + box[0][0], + box[0][1], + box[1][0], + box[1][1], + box[2][0], + box[2][1], + box[3][0], + box[3][1], + ], + fill=color, + ) + draw_right.polygon( + [ + box[0][0], + box[0][1], + box[1][0], + box[1][1], + box[2][0], + box[2][1], + box[3][0], + box[3][1], + ], + outline=color, + ) + box_height = math.sqrt( + (box[0][0] - box[3][0]) ** 2 + (box[0][1] - box[3][1]) ** 2 + ) + box_width = math.sqrt( + (box[0][0] - box[1][0]) ** 2 + (box[0][1] - box[1][1]) ** 2 + ) + if box_height > 2 * box_width: + font_size = max(int(box_width * 0.9), 10) + font = ImageFont.truetype(font_path, font_size, encoding="utf-8") + cur_y = box[0][1] + for c in txt: + char_size = font.getsize(c) + draw_right.text((box[0][0] + 3, cur_y), c, fill=(0, 0, 0), font=font) + cur_y += char_size[1] + else: + font_size = max(int(box_height * 0.6), 20) + font = ImageFont.truetype(font_path, font_size, encoding="utf-8") + draw_right.text([box[0][0], box[0][1]], txt, fill=(0, 0, 0), font=font) + img_left = Image.blend(image, img_left, 0.5) + + if table_boxes is not None: + img_left = draw_rectangle_pil( + img_left, table_boxes, color=color_vis["table"], width=6, label="table" + ) + if cell_boxes is not None: + img_left = draw_rectangle_pil( + img_left, cell_boxes, color=color_vis["cell"], width=5, label="cell" + ) + if para_boxes is not None: + img_left = draw_rectangle_pil( + img_left, para_boxes, color=color_vis["paragraph"], width=2, label="para" + ) + + img_show = Image.new("RGB", (w * 2, h), (255, 255, 255)) + img_show.paste(img_left, (0, 0, w, h)) + img_show.paste(img_right, (w, 0, w * 2, h)) + img_show = cv2.cvtColor(np.array(img_show), cv2.COLOR_RGB2BGR) + return img_show + + +def draw_rectangle_pil( + pil_image, boxes, color, width=1, label=None, font_path="test/fonts/latin.ttf" +): + """ + Args: + pil_image ([type]): [description] + boxes (list): list of [xmin, ymim, xmax, ymax] + color (list): list of (R, G, B) + """ + drawer = ImageDraw.Draw(pil_image) + color = tuple((int(color[0]), int(color[1]), int(color[2]))) + for box in boxes: + drawer.rectangle( + [(int(box[0]), int(box[1])), (int(box[2]), int(box[3]))], + outline=color, + width=width, + ) + + if label: + font_size = 35 + font = ImageFont.truetype(font_path, size=32, encoding="utf-8") + drawer.text( + [int(box[0]) + 5, int(box[1]) - font_size - 5], + label, + fill=color, + font=font, + ) + return pil_image + + +def box2poly(box): + """ + Convert box format to polygon format: xyxy to xyxyxyxy + """ + xmin, ymin, xmax, ymax = box + poly = [[xmin, ymin], [xmax, ymin], [xmax, ymax], [xmin, ymax]] + return poly diff --git a/cope2n-ai-fi/modules/TemplateMatching/textdetection/serve_model.py b/cope2n-ai-fi/modules/TemplateMatching/textdetection/serve_model.py new file mode 100755 index 0000000..d9acece --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/textdetection/serve_model.py @@ -0,0 +1,87 @@ +import os +import yaml +from pathlib import Path +from PIL import Image +from io import BytesIO +import numpy as np +import torch +from sdsvtd import StandaloneYOLOXRunner + +from common.utils.word_formation import Word, words_to_lines + +def read_imagefile(file) -> Image.Image: + image = Image.open(BytesIO(file)) + return image + +def sort_bboxes(lbboxes)->tuple[list, list]: + lWords = [Word(bndbox = bbox) for bbox in lbboxes] + list_lines, _ = words_to_lines(lWords) + lbboxes_ = list() + for line in list_lines: + for word_group in line.list_word_groups: + for word in word_group.list_words: + lbboxes_.append(word.boundingbox) + return lbboxes_ + +class Predictor: + def __init__(self, setting_file='./setting.yml'): + with open(setting_file) as f: + # use safe_load instead load + self.setting = yaml.safe_load(f) + + base_path = Path(__file__).parent + model_config_path = os.path.join(base_path, '../' , self.setting['model_config']) + self.mode = self.setting['mode'] + device = self.setting['device'] + + if self.mode == 'trt': + import sys + sys.path.append(self.setting['mmdeploy_path']) + from mmdeploy.utils import get_input_shape, load_config + from mmdeploy.apis.utils import build_task_processor + + deploy_config_path = os.path.join(base_path, '../' , self.setting['deploy_config']) + + class TensorRTInfer: + def __init__(self, deploy_config_path, model_config_path, checkpoint_path, device='cuda:0'): + deploy_cfg, model_cfg = load_config(deploy_config_path, model_config_path) + self.task_processor = build_task_processor(model_cfg, deploy_cfg, device) + self.model = self.task_processor.init_backend_model([checkpoint_path]) + self.input_shape = get_input_shape(deploy_cfg) + + def __call__(self, images): + model_input, _ = self.task_processor.create_input(images, self.input_shape) + with torch.no_grad(): + results = self.model(return_loss=False, rescale=True, **model_input) + return results + + checkpoint_path = self.setting['checkpoint'] + self.trt_infer = TensorRTInfer(deploy_config_path, model_config_path, checkpoint_path, device=device) + elif self.mode == 'torch': + self.runner = StandaloneYOLOXRunner(version=self.setting['model_config'], device=device) + else: + raise ValueError('No such inference mode') + + def __call__(self, images): + if self.mode == 'torch': + result = [] + for image in images: + result.append(self.runner(image)) + elif self.mode == 'tensorrt': + result = self.trt_infer(images) + + sorted_result = [] + for res, image in zip(result, images): + h, w = image.shape[:2] + res = res[0][:, :4] # leave out confidence score + + # clip inside image range + res[:, 0] = np.clip(res[:, 0], a_min=0, a_max=w) + res[:, 2] = np.clip(res[:, 2], a_min=0, a_max=w) + res[:, 1] = np.clip(res[:, 1], a_min=0, a_max=h) + res[:, 3] = np.clip(res[:, 3], a_min=0, a_max=h) + + res = res.astype(int).tolist() + res = sort_bboxes(res) + sorted_result.append(res) + return sorted_result \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/textdetection/setting.yml b/cope2n-ai-fi/modules/TemplateMatching/textdetection/setting.yml new file mode 100755 index 0000000..ff183e5 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/textdetection/setting.yml @@ -0,0 +1,7 @@ +mode: torch +mmdeploy_path: /home/sds/hoangmd/mmdeploy +deploy_config: configs/detection_custom_tensorrt_dynamic-320x320-1344x1344.py +model_config: yolox-s-general-text-pretrain-20221226 +# checkpoint: /home/sds/hoangmd/mmdeploy/yolox_trt_fp16/end2end.engine +# checkpoint: /home/sds/datnt/mmdetection/logs/textdet-fwd-20221226/best_lite.pth +device: cuda:0 \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/textrecognition/configs/satrn_big.py b/cope2n-ai-fi/modules/TemplateMatching/textrecognition/configs/satrn_big.py new file mode 100755 index 0000000..3f3b7d1 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/textrecognition/configs/satrn_big.py @@ -0,0 +1,1115 @@ +checkpoint_config = dict(interval=1) +log_config = dict(interval=50, hooks=[dict(type="TextLoggerHook")]) +dist_params = dict(backend="nccl") +log_level = "INFO" +load_from = None +resume_from = "logs/satrn_big_2022-10-31/last.pth" +workflow = [("train", 1)] +opencv_num_threads = 0 +mp_start_method = "fork" +img_h = 32 +img_w = 128 +img_norm_cfg = dict(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) +train_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="ResizeOCR", + height=32, + min_width=128, + max_width=128, + keep_aspect_ratio=False, + width_downsample_ratio=0.25, + ), + dict(type="ShearOCR", p=0.5, shear_limit=45), + dict( + type="ColorJitterOCR", + p=0.5, + brightness=0.25, + contrast=0.25, + saturation=0.25, + hue=0.25, + ), + dict(type="GaussianNoiseOCR", p=0.5), + dict(type="GaussianBlurOCR", blur=(3, 5), p=0.5), + dict(type="BlackBoxAttackOCR", p=0.5, box_size=12), + dict(type="DotAttackOCR", p=0.5, dot_size=(1, 3), dot_space=(5, 8)), + dict(type="LineAttackOCR", p=0.5, line_size=(1, 3), line_space=(5, 8)), + dict(type="InvertOCR", p=0.2), + dict(type="ToTensorOCR"), + dict(type="NormalizeOCR", mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + dict( + type="Collect", + keys=["img"], + meta_keys=[ + "filename", + "ori_shape", + "img_shape", + "text", + "valid_ratio", + "resize_shape", + ], + ), +] +test_pipeline = [ + dict(type="LoadImageFromFile"), + dict( + type="MultiRotateAugOCR", + rotate_degrees=[0, 90, 270], + transforms=[ + dict( + type="ResizeOCR", + height=32, + min_width=128, + max_width=128, + keep_aspect_ratio=False, + width_downsample_ratio=0.25, + ), + dict(type="ToTensorOCR"), + dict( + type="NormalizeOCR", + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225], + ), + dict( + type="Collect", + keys=["img"], + meta_keys=[ + "filename", + "ori_shape", + "img_shape", + "valid_ratio", + "resize_shape", + "img_norm_cfg", + "ori_filename", + ], + ), + ], + ), +] +dataset_type = "OCRDataset" +img_path_prefix = "data/Recognition/Real/" +dataset_list = "data/AnnFiles/current-dirs/2022-10-19/" +default_loader = dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", keys=["filename", "text"], keys_idx=[0, 1], separator=" " + ), +) +default_dataset = dict( + type="OCRDataset", + img_prefix=None, + ann_file=None, + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +handwriten_train = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Handwritten_Train/",), + ann_file="data/AnnFiles/current-dirs/2022-10-19/text_recognition__Train_Handwritten_Train.txt", + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +printed_train = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Printed_Train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Train_Printed_Train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +handwriten_val = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Handwritten_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Handwritten_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +printed_val = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Printed_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Printed_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +synthetic = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Synthetic/Using/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Synthetic_Using.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +blank_space = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Blank/Train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Blank_Train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +captcha_train = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Captcha_Train/DONE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Captcha_Train_DONE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +captcha_val = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Captcha_Val/DONE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Captcha_Val_DONE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +kie_train = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/KIE_Train/KIE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_KIE_Train_KIE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +kie_val = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/KIE_Val/KIE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_KIE_Val_KIE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +gplx_train = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/GPLX_Train/train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_GPLX_Train_train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +gplx_val = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/GPLX_Val/val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_GPLX_Val_val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +vietocr = dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/VietOCR_Train/Data/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_VietOCR_Train_Data.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, +) +train_list = [ + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Handwritten_Train/",), + ann_file="data/AnnFiles/current-dirs/2022-10-19/text_recognition__Train_Handwritten_Train.txt", + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Printed_Train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Train_Printed_Train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Synthetic/Using/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Synthetic_Using.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Blank/Train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Blank_Train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Captcha_Train/DONE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Captcha_Train_DONE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/KIE_Train/KIE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_KIE_Train_KIE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/GPLX_Train/train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_GPLX_Train_train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/VietOCR_Train/Data/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_VietOCR_Train_Data.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), +] +val_list = [ + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Handwritten_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Handwritten_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Printed_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Printed_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Captcha_Val/DONE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Captcha_Val_DONE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/KIE_Val/KIE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_KIE_Val_KIE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/GPLX_Val/val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_GPLX_Val_val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), +] +test_list = [ + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Handwritten_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Handwritten_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Printed_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Printed_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), +] +fp16 = dict(loss_scale="dynamic") +label_convertor = dict(type="AttnConvertor", dict_type="DICT224", with_unknown=False) +model = dict( + type="SATRN", + backbone=dict(type="ResNetABI", in_channels=3, stem_channels=16, base_channels=16), + encoder=dict( + type="SatrnEncoder", + n_layers=12, + n_head=8, + d_k=32, + d_v=32, + d_model=256, + n_position=100, + d_inner=1024, + dropout=0.1, + ), + decoder=dict( + type="NRTRDecoder", + n_layers=12, + d_embedding=256, + n_head=8, + d_model=256, + d_inner=1024, + d_k=32, + d_v=32, + ), + loss=dict(type="TFLoss"), + label_convertor=dict(type="AttnConvertor", dict_type="DICT224", with_unknown=False), + max_seq_len=25, +) +optimizer = dict(type="Adam", lr=0.001) +optimizer_config = dict(grad_clip=None) +lr_config = dict(policy="poly", power=0.9, min_lr=1e-06, by_epoch=False) +total_epochs = 15 +custom_hooks = [ + dict( + type="ExpMomentumEMAHook", + total_iter=20000, + resume_from=None, + momentum=0.0001, + priority=49, + ) +] +data = dict( + samples_per_gpu=160, + workers_per_gpu=16, + val_dataloader=dict(samples_per_gpu=400), + test_dataloader=dict(samples_per_gpu=400), + train=dict( + type="UniformConcatDataset", + datasets=[ + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Handwritten_Train/",), + ann_file="data/AnnFiles/current-dirs/2022-10-19/text_recognition__Train_Handwritten_Train.txt", + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Printed_Train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Train_Printed_Train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Synthetic/Using/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Synthetic_Using.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Blank/Train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Blank_Train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/Captcha_Train/DONE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Captcha_Train_DONE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/KIE_Train/KIE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_KIE_Train_KIE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/GPLX_Train/train/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_GPLX_Train_train.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Train/VietOCR_Train/Data/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_VietOCR_Train_Data.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + ], + pipeline=[ + dict(type="LoadImageFromFile"), + dict( + type="ResizeOCR", + height=32, + min_width=128, + max_width=128, + keep_aspect_ratio=False, + width_downsample_ratio=0.25, + ), + dict(type="ShearOCR", p=0.5, shear_limit=45), + dict( + type="ColorJitterOCR", + p=0.5, + brightness=0.25, + contrast=0.25, + saturation=0.25, + hue=0.25, + ), + dict(type="GaussianNoiseOCR", p=0.5), + dict(type="GaussianBlurOCR", blur=(3, 5), p=0.5), + dict(type="BlackBoxAttackOCR", p=0.5, box_size=12), + dict(type="DotAttackOCR", p=0.5, dot_size=(1, 3), dot_space=(5, 8)), + dict(type="LineAttackOCR", p=0.5, line_size=(1, 3), line_space=(5, 8)), + dict(type="InvertOCR", p=0.2), + dict(type="ToTensorOCR"), + dict( + type="NormalizeOCR", + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225], + ), + dict( + type="Collect", + keys=["img"], + meta_keys=[ + "filename", + "ori_shape", + "img_shape", + "text", + "valid_ratio", + "resize_shape", + ], + ), + ], + ), + val=dict( + type="UniformConcatDataset", + datasets=[ + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Handwritten_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Handwritten_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Printed_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Printed_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Captcha_Val/DONE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_Captcha_Val_DONE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/KIE_Val/KIE/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_KIE_Val_KIE.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/GPLX_Val/val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition_GPLX_Val_val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + ], + pipeline=[ + dict(type="LoadImageFromFile"), + dict( + type="MultiRotateAugOCR", + rotate_degrees=[0, 90, 270], + transforms=[ + dict( + type="ResizeOCR", + height=32, + min_width=128, + max_width=128, + keep_aspect_ratio=False, + width_downsample_ratio=0.25, + ), + dict(type="ToTensorOCR"), + dict( + type="NormalizeOCR", + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225], + ), + dict( + type="Collect", + keys=["img"], + meta_keys=[ + "filename", + "ori_shape", + "img_shape", + "valid_ratio", + "resize_shape", + "img_norm_cfg", + "ori_filename", + ], + ), + ], + ), + ], + ), + test=dict( + type="UniformConcatDataset", + datasets=[ + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Handwritten_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Handwritten_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + dict( + type="OCRDataset", + img_prefix=("data/Recognition/Real/Val/Printed_Val/",), + ann_file=( + "data/AnnFiles/current-dirs/2022-10-19/text_recognition__Val_Printed_Val.txt", + ), + loader=dict( + type="AnnFileLoader", + repeat=1, + parser=dict( + type="LineStrParser", + keys=["filename", "text"], + keys_idx=[0, 1], + separator=" ", + ), + ), + pipeline=None, + test_mode=False, + ), + ], + pipeline=[ + dict(type="LoadImageFromFile"), + dict( + type="MultiRotateAugOCR", + rotate_degrees=[0, 90, 270], + transforms=[ + dict( + type="ResizeOCR", + height=32, + min_width=128, + max_width=128, + keep_aspect_ratio=False, + width_downsample_ratio=0.25, + ), + dict(type="ToTensorOCR"), + dict( + type="NormalizeOCR", + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225], + ), + dict( + type="Collect", + keys=["img"], + meta_keys=[ + "filename", + "ori_shape", + "img_shape", + "valid_ratio", + "resize_shape", + "img_norm_cfg", + "ori_filename", + ], + ), + ], + ), + ], + ), +) +evaluation = dict(interval=1, metric="acc") +work_dir = "logs/satrn_big_2022-10-31/" +gpu_ids = [0] diff --git a/cope2n-ai-fi/modules/TemplateMatching/textrecognition/setting.yml b/cope2n-ai-fi/modules/TemplateMatching/textrecognition/setting.yml new file mode 100755 index 0000000..b10b83e --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/textrecognition/setting.yml @@ -0,0 +1,6 @@ +# config: configs/satrn_big.py +config: /home/sds/datnt/mmocr/logs/satrn_lite_2023-01-08-handwritten/satrn_lite.py +# checkpoint: /home/sds/datnt/mmocr/logs/satrn_big_2022-10-31/best.pth +checkpoint: /home/sds/datnt/mmocr/logs/satrn_lite_2023-01-08-handwritten/best.pth +batch_size: 256 +device: cuda:0 \ No newline at end of file diff --git a/cope2n-ai-fi/modules/TemplateMatching/textrecognition/src/serve_model.py b/cope2n-ai-fi/modules/TemplateMatching/textrecognition/src/serve_model.py new file mode 100755 index 0000000..10321a3 --- /dev/null +++ b/cope2n-ai-fi/modules/TemplateMatching/textrecognition/src/serve_model.py @@ -0,0 +1,20 @@ +# dirty path export +from sdsvtr import StandaloneSATRNRunner +import yaml + +class Predictor: + def __init__(self, setting_file='./setting.yml'): + with open(setting_file) as f: + # use safe_load instead load + self.setting = yaml.safe_load(f) + + self.batch_size = self.setting['batch_size'] + self.runner = StandaloneSATRNRunner(version='satrn-lite-general-pretrain-20230106', + return_confident=True, device=self.setting['device']) + + def __call__(self, images): + results = [] + for i in range(0, len(images), self.batch_size): + result = self.runner(images[i:i+self.batch_size]) + results += result[0] + return results diff --git a/cope2n-ai-fi/modules/__init__.py b/cope2n-ai-fi/modules/__init__.py new file mode 100644 index 0000000..fa2d966 --- /dev/null +++ b/cope2n-ai-fi/modules/__init__.py @@ -0,0 +1 @@ +from modules.sdsvkvu.sdsvkvu import * \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/.gitignore b/cope2n-ai-fi/modules/_sdsvkvu/.gitignore new file mode 100644 index 0000000..17d9c1a --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/.gitignore @@ -0,0 +1,24 @@ +# Model weights +weights/ +microsoft/ +nltk_data/ + +# Visualize +visualize + +# External +sdsvkvu/externals/ocr_engine_deskew/externals/ + +# +__pycache__ +*/__pycache__ +*/*/__pycache__ + +# +.git_temp/ + +# Packages +build/ +dist/ + + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/.gitmodules b/cope2n-ai-fi/modules/_sdsvkvu/.gitmodules new file mode 100644 index 0000000..eb6d053 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/.gitmodules @@ -0,0 +1,4 @@ + +[submodule "sdsvkvu/externals/basic_ocr"] + path = sdsvkvu/externals/basic_ocr + url = https://code.sdsdev.co.kr/tuanlv/IDP-BasicOCR.git diff --git a/cope2n-ai-fi/modules/_sdsvkvu/LICENSE b/cope2n-ai-fi/modules/_sdsvkvu/LICENSE new file mode 100644 index 0000000..21ffbaa --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/LICENSE @@ -0,0 +1,13 @@ +Copyright 2023 tuanlv + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/MANIFEST.in b/cope2n-ai-fi/modules/_sdsvkvu/MANIFEST.in new file mode 100644 index 0000000..e4bd723 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/MANIFEST.in @@ -0,0 +1,2 @@ +include sdsvkvu/weights/*/*.yaml +include sdsvkvu/weights/*/checkpoints/best_model.pth \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/README.md b/cope2n-ai-fi/modules/_sdsvkvu/README.md new file mode 100644 index 0000000..d8b1dd1 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/README.md @@ -0,0 +1,122 @@ +

    +

    SDSVKVU

    +

    + + ***Feature*** + - Extract pairs of key-value in documents: Invoice/Receipt, Forms, Government documents (Id cards, driver license, birth's certificate) + - Language: VI + EN + + ***What's news*** + ### - Ver 0.0.1: + - Support inputs: image, PDF file (single or multi pages) + - Extract all pairs key-value return raw_outputs + + Weights: weights/key_value_understanding-20230716-085549_final + - For VAT invoices : Extract 14 specific fields + + Weights: weights/key_value_understanding-20230627-164536_fi + - For SBT invoices ("sbt" option): Extract table in SBT invoice + + Weights: weights/key_value_understanding-20230812-170826_sbt_2 + ### - Ver 0.0.2: Add more option: "vtb" - Vietin Bank + - For Vietin Bank document ("vtb" option): Extract 6 specific fileds + + Weights: weights/key_value_understanding-20230824-164236_vietin + ### - Ver 0.0.3: Add default option: + - Return all potential pairs of key-value, title, only key, triplet, and table with raw key + ### - Ver 0.0.4: Add more option: "manulife" - Manulife Issurance + - For Manulife Insurance document ("manulife" option): Extract all potential pairs of key-value, title, only key, triplet, and table with raw key + Type of medical documents + + Weights: weights/key_value_understanding-20231024-125646_manulife2 + ### Ver 0.1.0: Modify KVU model for SBT + ### - Ver 0.1.0: Add option: "sbt_v2" - SBT project + - For SBT imei/invoice ("sbt_v2" option): Extract 4 specific fields + + Weights: weights/key_value_understanding_for_sbt-20231108-143935 + + ## I. Setup + ***Dependencies*** + - Python: 3.10 + - Torch: 1.11.3 + - CUDA: 11.6 + - transformers: 4.30.0 + ``` + pip install -v -e . + ``` + + + ## II. Inference + run cmd: python test.py + ``` + import os + from sdsvkvu import load_engine, process_img + os.environ["CUDA_VISIBLE_DEVICES"]="1" + + if __name__ == "__main__": + kwargs = {"device": "cuda:0"} + img_dir = "/mnt/ssd1T/tuanlv/02-KVU/sdsvkvu/visualize/test_img/RedInvoice_WaterPurfier_Feb_PVI_829_0.jpg" + save_dir = "/mnt/ssd1T/tuanlv/02-KVU/sdsvkvu/visualize/test2/" + engine = load_engine(kwargs) + # option: "vat" for vat invoice outputs, "sbt": sbt invoice outputs, else for raw outputs + outputs = process_img(img_dir, save_dir, engine, export_all=False, option="vat") + ``` + + # Structure project + . + ├── sdsvkvu + │   ├── main.py + ├── externals + │   │   ├── __init__.py + │   │   ├── basic_ocr + │   │   │   ├── ... + │   │   ├── ocr_engine + │   │   │   ├── ... + │   │   ├── ocr_engine_deskew + │   │   │   ├── ... + │   ├── model + │   │   ├── combined_model.py + │   │   ├── document_kvu_model.py + │   │   ├── __init__.py + │   │   ├── kvu_model.py + │   │   └── relation_extractor.py + │   ├── modules + │   │   ├── __init__.py + │   │   ├── predictor.py + │   │   ├── preprocess.py + │   │   └── run_ocr.py + │   ├── requirements.txt + │   ├── settings.yml + │   ├── sources + │   │   ├── __init__.py + │   │   ├── kvu.py + │   │   └── utils.py + │   ├── utils + │   │   ├── dictionary + │   │   │   ├── __init__.py + │   │   │   ├── sbt.py + │   │   │   └── vat.py + │   │   │   └── vtb.py + │   │   │   ├── manulife.py + │   │   │   ├── sbt_v2.py + │   │   ├── __init__.py + │   │   ├── post_processing.py + │   │   ├── query + │   │   │   ├── __init__.py + │   │   │   ├── sbt.py + │   │   │   └── vat.py + │   │   │   └── vtb.py + │   │   │   ├── all.py + │   │   │   ├── manulife.py + │   │   │   ├── sbt_v2.py + │   │   └── utils.py + ├── weights + │   └── key_value_understanding-20230627-164536_fi + │   ├── key_value_understanding-20230812-170826_sbt_2 + │   └── key_value_understanding-20230716-085549_final + │   └── key_value_understanding-20230824-164236_vietin + │   └── key_value_understanding-20231024-125646_manulife2 + │   └── key_value_understanding_for_sbt-20231108-143935 + ├── LICENSE + ├── MANIFEST.in + ├── pyproject.toml + ├── README.md + ├── scripts + │   └── run.sh + ├── setup.cfg + ├── setup.py + ├── test.py + └── visualize \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/__init__.py new file mode 100644 index 0000000..1621835 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/__init__.py @@ -0,0 +1,8 @@ +import os +import sys +from pathlib import Path +cur_dir = str(Path(__file__).parents[0]) +sys.path.append(cur_dir) +sys.path.append(os.path.join(cur_dir, "sdsvkvu")) + +from sdsvkvu import load_engine, process_img, process_pdf, process_dir \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/draw_img.jpg b/cope2n-ai-fi/modules/_sdsvkvu/draw_img.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f1581e117cd399ce879017107a64d367a3748f6d GIT binary patch literal 405763 zcmbTdcT`hP*amn*0s%ytAPF6UR0~Z^fY6j)LI(xu(nO?5F98HG#?X5RASD!ON)=F0 zsY;P9pduaVy~FbRzH@f>kNsnJCuim)lbL(o=g!>8`##T`^Re?OfKf|LQw;!t004CH z0?sFZy8s2~zw%#8@n3;}|7$5B5HN&_l8WkoDwKwr3Q7Z|qN2V;O+))%xp+r+iI(oa z5C2`{f2UGVg29xuP%7yE4Eg`moVNf>)X;eF3owWupkM-lnLy`X0iFvxDgVd33uFJc zfGEHaN-F4u8J8{w)G%I{4+dWtO?hGQ#psZW?*RxCB{QFdGSw9WTPVLLi)46wE;T}> zs+HC7$A*BEomT`6?Nv4yoc)@hkg$lT^lh0tNR;f|d#Y;c8k$;0#wMm_=Jy}iJ3Ml9 za&~d`_Ic*(=N}Ll`8+E6Ma)ZFLSj;KN@`kq#+$tSfv@G=GMt)4B|f*Rl?EYYxWQ`Y^@H}Rfb`#MoBu)k zA7uaUfJOZOLiT?G`@e9F19V`}#o~dP07c*|Z1D{a5dOc{Jm^5`<3M&mOT@QsA2X-c zarXD5_$M~WsHuTLV`3|>JM}D}?sQvSYz6F0e)Twd349Lxk)Eq+dfMo=a0bStJdE;D zRW*NjOW&O`Z!1K!?Ne0nT^}i`WQ6@7&y2c2BUIYVNC7wYs)hSj_Nggu0in38ANC+w zf!+USyV<>&vyX^#V7mBNHY=^+@8st<9|R7JX2=iEfuGdS;>K6!0Ipd5GXvMNC$dbt zjj<=rdnR%cK^lH2oLI@LsXwphmL9t))M;aQD4QO~!EaCnIxd`0D%zVJntOj~=rVX1 zal*Kn$O(U|J~DLVq13BN$I$t5L=)DDik7FWDdR`A8BIi{k9BWmy9rK;s0O_QT{}u4LQx&FB^0Cxdp-Shl5O?%U(INIZHnP#&#!UVX`5fRV z6%XyohSF<4aCHN<>*aMUCWROlE8Z^*eyiZ^;Hce_ko3)6);*oD_b*x&TOJ*FyZJ*Ry*b(P#jc%UgP>#h*eGuo3uuYb; zJcYlzMSBYTZ$3_2zffezqPRPCgGn#2JRVRyqI7PZdJ37~Wjc^j4uld&o1c#kL>_FMXq2X*h04_wfDXyFY>!Caws=J>M+jBQ= z&YlBij}ak^8OhQQ?X2tv+$~PWVCR6=DJa0vO!!@;RSp&T(yIcvRA)%2cOT1$W%l@q z;#6eYArD9>stI%J=o7tl-EpmZSb+|Sa0c9N*&W3%1>eK=E{*|~6N9AbThZgk zVU2mc7d^)>>7q1$d#GyDDS26Ya)lS1W;4tSEhnr5EAq64z1j{dJ$N{M@lbdKyg2)2 zq`tv3bB|23QTMR#Vm zJ&sR*&6a_{np~dV>~nx_Ve=Co=F4lPt4j^WVN%wQk)H`5p(?a1jr|{=yvWrh%&}~$ z@2xV-I|Ui*pP4gxH`fo$QkBALa~9w8=(;X?!0 z?~i-O(Fd$vOpo}g8>pZkowEKjAykI~ z9Jog&uV6Mkld6WLuXR}d?mFeKD5YHm7@CLXGkH0vZ%X{m9uhyZ-OV6(mmag87|GW* z=YWV|9YY=Gfb(#@qHW(x=ED6=--!BukW|Pyuvl_rN_n^)PiPJ_sqbfnA@_O|i)@cC zE#JnZzd4PP_nBhb9Eky;Q@qcC>NcJ1%2Zr$a9@n9e#ickLdAZw0ozJF;3&|r>R!y2 zKUd@65rLy_n#a6!^6-P=n32WPPn-N-c~mRoF1Ffdvh|qkIiT2dknN7vTGH`6>=awP zZE9v>(%KtZV_(}IOd(4&I!fU<47+;kt?63u#-~b$^sm`0E&J?(VS6>&JS$xG7f(&7 zs8-moAJB7vyCR1e*uPHw86 zM$1!6&HJyLBDqwX?q-j~gxT@D!1sJ=8}9-i7c!j#@7VXIe;Ve7(c+j}Ekq*5Rx$t5 z!sZyBm^IAwpMAO5q_p$a%*!2vS6USJk}(V`>KjqYlFOldPme>eVXN%Bi^K8CVS3mh ztZ2wCT!>5L)QsVz$ZEX8yAyGWSD<^_$i86ZNh0gV9;uqL?a3Sn#wYHDJc>pBD!#RY zH!J>1_6%)0`XRWQ23-_6eehx{XRk(%^boodzHv`BKf^TtrzzS74=plWZ!^{(5`a;lw@DV|0u9jW;$K9? z6UqG+ZU23q7y6#RD#|fu!}%}vybm~FkYJfAh0H?YIe-%TlzM8VwE(VBd5LWVW>3A% z*rmggLpom?7ymk4U^xeXkF+WvO!NaA=nzV=R9^ua%xkn2EIB}he)9gL$Cp`1NTHz> zy;M=n%o6&{syp?7H`?<#=ABRVPJQ*av!B^}?3vk(*iWUQd zOyy04aB{eLJW8IjL+icIv2lbf!M|i9Vd3$Wm5v$-9_AB+huK%;@6|7r|B5}q??ydj zYdbi}e#I?_r_VTDus&MZ?H&DQe1_OX7X?#ac#IJdIN_Yj)KgJ06U-#3*E!Rqev4S5 zQWM?jIKODsqZeVWx^3@UQD|tHTbWHD?bWOBEk?iNH^kzr;zdo{+jO2fbb0+D&Q3o{i`s?Rb-mOgOSE45QvIdH{0+(Rf2(-7}XJ>=P3gtD&{%?6c*bTgr)}1(ad0 z6kiL!e%UHd?|SGihD(GNS{KQe5%iVf6Sl2A)wy7bT8g0(( z0*AU*#-&HepA&Zgzgz~DMwR@CNW5)yV5}D8elWlH*a&&g;}Ix&O4csUB`D?)yG!oH zHJHkIEs)(w!?t{gIoor9La@wY92;;q+hDvvM%BXtPXyy$;r7S+7@4BSX~0BU*UwAc zoblh7CYm3s7HSGSr#?W)#mp?3-l)DS?sDx_&J=h_bqA!jLvx#5Pb*$LS&g2Qm?>kgeDB;I{Ua@yyiTEfBC_dXBqv+6=s_btig{9? z%FZAG>VxSG3#1r?r;T!+ybT<94~g$?`+Hm*!zWHX^{lkg-*Gl)+qfJB@3`GmJ7*#3 zdqVGO@_b?aN_=aN;h>5`g5;AEMaOI@=p?;|s?|f$?_a&Cjqs1utph*Smiz-j1$>rF zb89+j^=*h7vh3y9wI6yn&A@nvgFTACr z-$)~qy7u3RFs(#8vgg)MxyW7=yp&00OMH?(5bJ9FKG~?Qf2X8!O-s{+=|8oOTdFA?Jym(i> z#FIDIThl#B4Y%EdzE#sj4h+M z-Bo@T9G1GP71m>c5@ktT#t&WLO|Zd9-f{AkH7S}F zV4WaI3>E-P8(wDo34iU|hdTF*S$*j=iB3e{qH^&8TX8iinrSR7US?&M&c0bE*WS?F z18S=qE%T&JsiPRZv?w4J4du}SoE?t~4*QHo;&_sZ-GC5b8{?A|8ij~BU=omBpu)m= zFLD1$UqDK$G&yk7`=g7}H*zsTC^BV>2{1E=b_QL$dBv9s!aOjnx7iTO*!3ank!szq`Ud$+(jb<9f9v60|1N9V(Rfk2+2aXGvCY};ihSB`)U%|I&CV2G32$YvG+h690ZaQ> z0}Xgn|IaVrc3oZ*QBC>S1rufmwSVlEku9)7Z8&}#PZ}0*HRdGcXSm@K=vbeA4XwEs zpNyId@3?DVSqf`8d-$&vq`^~EROkRnu~J%6VogBjxzdAnsb1C7ZB#>^&G7s+RDmaP z#)T#=W8sPO`o<29SROZ$jy4T1on3O0=fBC>-2#2stO$(Dg_8vaUBUqs-n^14UHVYz zm=d%75ZMgD>yLWXWStsVN@it19W0>DryUTc6Z=h-@N+<7BJr@Zy}@<)Bf4WQ*lAOe z=ZH=xVBc*NVSUT&;N7m%d(6S({)^x4@0D+G!eC{KIob{cMe<81taGn0^0{e`2roX| zq9DgwAGu6A6#>b={@HVBl2G`9?hEgxX~I>k4AwKY-1+)1CFr2^WF+>>1`7bQ;WQ=Y zhCmVT%Bdt}JvFwNzFj%!H&YZ@TGoE=hhA!@z~uZP60hjJ2A*aP;yetgJ9|Yut9MRx zo!?xJG3(#(??I|(r=pNE(sTEDu8B*^?yyRV{Cw`lWBuTxk<5yK{=2!VY!y`ur$ES7 zPP|rlgQfjHslJf{+@1I;EParl^pJ+v?O>5NwRvIeB@35R;In2q>W{ZW(yz%GGg5P! z-ojQxYu=splO!tyF@3$z8Xu3(-A!~({WG^MKfv|$9?%h3TU7j^^0bweqLFpET%v7R z_&TseyISBvv+J0@yNkyFZy$%cy#C5d?W%-Y!w;-rWCyE&tj~$loWREAXW|F2`004B9Z7RdbJ_L_NHZ7H@$cfkivI18ec7RB<=N|r1Rw;I|Q#Vz9 zCM%hW(;(sKl4@liyt6#Yg@Q6c@_SH(|6Zxco6p#+oLL_D~7lHq;0>R59zJxC7~c(^J>q17qkt$e-2!kR@fpr=)Z^ zkmy(X7-S=fTo2dg4!j2?~wxC4n{qPo#~?--aY23ExbZuR>_<;RFhv%ItZfH>CkQlq1-a&2BpqE_#_!xb}uErt}8sT zMX>M-_npw&()z>*6nAvB*1SXfM0*~eN62A8f_VH4xB^-HM&%1f03tT(?5ORm$rzJfcZOQhc6^KII$D6|H)M!QHg^m%Us4S}>=`W6-)-)n13~3^JPiuDcjDCbYQ!|2yFV?OVdY9QjW00$ z=f7|wdi~pt4K`ZMU9?DoObc8u&}c(qKfKQ*KixQ<&>?T&2#$LfPFNrg2G>||)Oi}p z^kb_4r$|HftPWrCkfY%njI7@u6>=s3#rFE?UQ|JTxn?&f7$EyxOLMfU5sISqQKKr< zGoRmsA|^?)xX12csV)#6t)$nH``Jn^Q5u%3>xV|J)2=ffCNdIJJektN*D&3{+8uUp zcL48A*ipS|EOke~6Q@aYL)EQh6*lE7JrRt&{u8QDy`*vP@Fmy#-zkvsMooaUP+uaf zX;d^jyA?F}lcGq6gr;{^6uD+WN9Do5lC!3y1V?Mi_k-b{35pahG@{R|7V2;Fs*pW8 zpoOj?1WsO~1a(2IKrBb6oyJ5PDn$}Zxwrf~reAh_qW_un&onC95Gke0!IX>Yg|5hS zJx@}T@!(WDH4Ob)DQ3ZUW@tkd(4@KCb;RT9ljr6DZe7o~M+u|Iy z#k9Z>(!i^Fk>ASTLRZFuZ?KNA&Y^OT0u2tOnPmet7PWvbA!%UN5T_=)JD)B7Fz@ow z57=*CQO;e|ZoxAOm9ah#X9|8VFQ9L*>}Ib99Tn6)>BOndswp&Su*MpG6h4mSBd&XB z6J$xr!s8y)y?PANKV_;QA2G!&8x7|GS{U?Ra4Mf5HIPN{BtT(;aa>al1i3-{y}#HN zXlx+hA;jqf{sS-UiH>IjB)7Yk^nP~-u5#~;WncH8!qj>z(f=9FEJrDh_76V)H)_0Kmi|s|V0Vt)GE@WSUgi?m%jX0&2zU^YHC=CM1nc?7|aR(dP+ykIt z%*LD9uG-o4N-*TIR@HlAINqnIJUv+}PMG53tP_*?LM^J;gB66B@9NwwnxU2KmBLC= zYPH?_#0Ol7@ZDD8VL$tEscz=!OIhz?iJ3{TNh1Po|{*HN3T-Pj0oKRV)oti zOuE^6K1ll03!Zx{2C5F`2C6KeKyf6%+OK)7*bRrt#G4*7NfT_g>@i&On%ERxY8aaaD@~!i0uP>$)XgJXpz5Y zhL2*}{O;pJc6hp4nvMdLu?^6|?(sTpL)Iuou)V)wp|UXU@>q!{BNS0CYcvy{A(hgB z>LRzgPiRXAAeTtVa`#xx=BDC2aW z0B*DUX?8nUCbK|hF;Z-D`@l@o$=Q1C(GVSX*5f;00s~)??4GZ2hL56`b8G9iY`>Mu zv3Clr#5})|P4pxv!{_t2AkSGaALQt>wLD(+CW7=Bbe)8>!umMpc%z_pK-zI$&ve~9r#$b__p_kk%A9y#jMa1%|QTQPKp+7oATisn`5 zJ-mBY2seEj!PxG%#FW-W46@MeksBe@l3De(t@)FD!<0!41Qfbt zn(V+C*^WfiTzebD2wj-;xh)-IB3TrglvRFJOkqx^2sJUbTljHa6(tvIC2B@nqP#xI z?JSYUiog?m`z3PCnVR2sAXvN!r?Q48t#xpEDT1mgRZjR=_1DVOAT(S zae6c}6ezO;cj+z^f+)SHXkP+UKWOBoWlv))K6D=pu^(*KX0AkW9sGoE7)JxC7a_@ha3A@7z(c-d3yrx}p|ZfK2p)s2kINJ4ovg6DbM7fg=?{G+#K>-)T@Vszb?f!d-)#*6x%&~A#c?J(x$T~HzWCOz0P;Ia@V1}U%ifnT1QTSK`&`TLpQQcSr-ys4_?xv zX^Lw9?VkgAZcnGqN%A&QMDmix5j06e_!}+{D-FOclu8GYgvz0^ z6hF>kc6t5|i}#MlN?_8#viJr3rbhSO=t)UnS&0D;{4~J(7VzC=vk_O5(CyMGsY1$! zy+#o$;fi{){a`T72c1Pmu>T;Qc$EEnvK7Fafij1MPr2 zwEOQaic}<9)^iU_yHRt_IIjf`dD&*}o1>%$>NBf27;gB9`7DZ5N&MQ*Zt@6uEg$oX z8o}wITqrCjsoaI?JO>nZHO*7jZYS48_M#U#yG;VPE^B#&deDGJ!}VJ{Q&2*KBWdAX zj=ZO`J4JE$6wb&Nx3i=z4ayh#ff@W9#y1&43$aXD9+uhmi6beuFEMgpgwW2A^x!7n zPU$H&*eVH`L;2fZ-$PB;G;U2m3hUj1DASbAMS&B7hV70sJ!cb@=p#^UDRSZRoQAdZ zj32Uf$pzPSn7kMUW4D*|qNF&qro8-b9P~pwva3I=9Q-YhJ#qn^OyNgqtiq2CzRU*u zj0~bxiT(WmT;tA3o={)03$^+N9DtRH!z zBaJa?#<`2-gjaxotWaaxH(;}8LS7GX`F$UnsKxK6$h*|SU&us>?E6M|Y{G0&**P6N z=izNApcEhH{RihIx&!4-I>phr@z}Wz-Rzuay3y_-=QC4)yocZw2X=Um1}JCZ`;Ch^ z#K+|VO-fuOxsE~b+offjHMv>B;l29w_&9k0O~`bj2b zvezDa$dLRGbsyBlXl~7d+d*8ea?a_qUEm5@%!5xZPa7edv94wDm!tAk%kSAL_qpz1 zAWPYfgL%mZ42gIzRc~&lAM^#qQK@UW-)MJWZ?=Z7N<`hG4OjnFl-WO%Mw1_Kc$J16Hew zdv;hm$~G&H9QJ_5i!XqM>$d@F=V*`y z=&YR6cf1F!E2nqyfaF!J+d{_A- z0QVuwj@|1<#k;s?Mb|3T2U4bQU4Il9G*aw**Y7H$PTHlxiUOShfX+4N*zrxcwflUW zL!d8tb@I*(^kcy7HyY7LG*Tfy>cE8|qBM4<=>{AE#B8JAWx5IUEU{LolG?@4frqIu zPKF0|{dG*nT?j5hkg;fPIT~2S2*3#RXi?-haQzf1PpAykpJpsz zd7r4#T9a5OTK>6A)_qr+^28A|VHD=!iA44{xKu<>J*}>Wv6@QJ)REC8s!SpDX&Y)R zaHa;3zTEL$Km_eF7x6j`;+XevHfo}yXgWWUgKA}SXd27R7hz7{e8C3qF1_nSQhG-I zCZ^LwE%3idF;TfgbQ;HAr(qqsOym}Ub{AX%PoRqIm7Cn%C4Lvit;(@nBprzYk2{t4 zs3-;KXoNgFP=#uQ#g^@*@ByNEK!7P1sw)SGGD8aSZtDFyeuJ?#GH5BSPI1?qIvRS% zswhT3sZZ(k=aaB3eZcJ-jqR4}JSwUtUpQXl7_9Urhkj%wta`~13b8j<$L} ztg)!$rK1H*?LOTCM-FB0!r19<7qSiPQ9+||Ja%<6;LZrvPq)aXY+>Ovn~9q)DgIj*6*@e)4E6=;_)sMz$K zTAov%qMX(VitN+6$9*3mRJ^M3?v{#@L7$#*fSt+Ba;XV$nViH!x>p?E;wNy=1|f&$ zv*7WJEQv;7yG_zeJ>Kp{@;`LZ`%7<3ujvaTq#J6VB(PJ*YM^S@Tp^?f_R|Q&2yXn@>N?Ovj)mp7y{jM zAEx2ICR!cjkm=GJ6g0HeZOWUoIl=!*`~wY-b04@u>)BgD!-1<)-ni0hLpoEpY9-9B zo5#FLJ|4Os^@9(luR`=38oCMlG}$r{9{xMuu1|2$!4`0sTuzCkMC| zIe^`vI{lQUyv4K($4Z7%@!|LxQsCevwerS^^(*b7Dm|aZ7<#d+`b_)1P*_*KT(~I1 z(}5(}RaBpqQofL{R`_5UU`A4jeNSW>taas8^H4_=X#2Fr2tndhRj36ht;{ljOd3v( zcY5l-xI{JdU)jzpfR+>WQNBtX9NzJ!N)()Y2!di)4#P$6#$!tG4C(Dmp0aD$mX*Y* zSg+W?MdzcA_KE$o-K^qspi@+_AfO<)SLk#)^zEMTIY6rrbHQML{g1{5hH6wseoTci zaI&ZKUwgNx7J%d#N!8IASFQFkBjPHbj72E@3rrknF7dE@nYJy^Ir7JSuw|DbDq0On z#JW0$Qxvxdcqasq`HOVy6V&J*yBph!6yB>s#R48-Stwg5D5gCKM?%70=Y#-?WlVx+ zdZI%Xl3ob&{={eq&jXUMrfoHGVLZR?RZ%P$GX)`vjNXN1d-ioFdGqSFAw3M9bk zlHO~gL!M6+g9s-+g%9tYX%Zw--9-m9%;03~S4MSCg6zoeWZT={l~3MjGgG6cj33ay z@_WW_p~|QzOj)~AzO1$}#2zTP`B6o(11+3j%KAenS~`00Ml7F0VmVZK$s$w9(p`SD zvvA^`MCTN})Ogt?yKZXx(S#5&QeGpw?kgv^cV7Q|H#?UfI>y`F_^a{7A)XkHTiE&b zaMa*lk@+)GO5dwhnopyCc7lse#n8Cg6rtMZQD9#C?BY_!&*oRw$J0t{v?2eHJs8fQ zxD+cQZA40>u?>;WwwE>oVw;4e>5z}z?$Nj;>c-U<7zj{9=V3Wi^^9LB=rLqv4HhXZ zJ+yvZw}Dy-A3-1Jm<6MLBPprvl;JCsmTbox$Q&R@HrmS0gmr+MRr2McF0|-V(E$7L zOnW^{uuuL?+bBP}1=LdKbcqf{oL}(R41c*sq456I+07PKEgS8_7mBNa`uuRXOaf{F z?_lKiBR}h9uMxSjSkUMM0c~izk>u|+JG($M(wfnl>-_QaYmiijyooL*iCG?hwOHGX`_vj6 zUNqBjUaw;pc)3*wCng2D#aL?e>$^|k0vD-9gk%pjdK&AI61jx_ly3W2J4QYK9)W@6 zCj_-1_T<_Pio|wZv$T}BBDAd2$OmT0rz$=90$=YkdiVXsyx>=v`?zl7QkzdFVl+s+ zP`6`+ycFFiZ{MR4G@y3sT3t2yYtYoyF_NDaL3V*KxbG-My_s5FSGt^)Y<9VQO~g!4 zG0C3r=1tmRBBiJjG@dXlEQF`qUQz;pa-gGG<0rU${qyTgf*PoAS-Is}r~I=XfYw9& z%kGjM&Qzd0a91#$kCvLNaXKxs7=p!=0p>|r*vP>Pk~P$h?Gr4Y-q2cY))A02UKi+-$XYPfeFxp_P-k zZm+PI+3CC&(D=ay{({zaSO{`S>0z#cxiGMu{eTwFNYf|Jq*bpv>&uC=(-@Gj4561D zzLql}_=;MO-k+@I_vpiRU(Ib}$__Wr9+9aCHZYDGyTc`x^zy}`Yg59irNk^r-8JEF zi<^D)lF0&ZM-=ZSp0G0rX)hi}ave;+@NW6$u%XJdRhs>mt6Ai&Nx?Y1dPAZ9?9Fd| z$fY*<2G4Er6SNCcz^|cxOED@g&&hu9#A8MddHUbUhBI?(is98ktzm5(14Oc&|;H8{7f z#3<(e!H)V+oqPC3gJi=*pM0(B$5WgG9FmP$Hr*LB=ZnKH^!HPIhfOhyGqCJ{!6fhg zqd@_aZIAq5az-3usE}uj;zohb1dVj-p-eta5oOG1m@u zwzSR~IY7VtVow~52r}{l5b^O{1%B2a#t>XeIn)!Lui`NBAg$*%2v0&`awU^eAD8nZ zk4Uk20=@TPKUkgP9m50r3vVy8++}SdiPvgOz*86E_BjG$KVBxr_-ibNDYg@Jacc@h z+4!Ov2Fp;A7zB6T$V=n`?6AH^g08!QmBp!tI6F019{TqiXL#R0UNsye4kmhB26Qgn z0XEPfE~#`^(Ix)v5=$6U)rVg9^}#q$4$Y0E64G;0lMxvyr{=$(k0oBcl(B%QasZ6Ys z4^6IcNyA#s;Ca07UoFw&YUmSvZ2FVXPdzjG8++|$+etkv>zdg=g%&n0eMF(V{SnBxMUMn z#4+UM+|Dl7iy8^RYp(!th6*jWt;4cGp=Bi|X{R1DdHabn`HM~DKRc-Zl(K7K$Tv5Y z_=M9t14x`R1i_$%sxp`)AMT;djdX$8y;Y61HibpxqMSaZMcQb9etqi_DM_uw+&)2e zIhHS!=3!E9T^Gs!VY+NDENN{O4%5JsvyoHu#{zd}aF%m^JV5l*55`(KR z>yJp*H%T|*WRFc=q1i2Jj`2BoD%P>ZGwP}+xIP$9ona_H{acH4Ng;5b5y4u6{>pigFA^1M& z1*|;c=R_jD2zAEkQ~X`B^w7Ea*l({mx7IeAHne`-bZ1re{yKFBmp|%l=FqSho3w>) zT7$zEpNBQgbZ&a{LITDQLW#Bgph1Bx)rYTH!ih{1(6YNWHA7y3vqNnf`UI$z2(FQy z3Bx6-L-Er|9z2qNGj7h<4;-dtNS%u80=SbF#c{nXVb$JB2G#4}6W0Wraj|!)AAZ%x zrj}iIU=|NFEXi6q39=bz=SawLAfcvA7!FlPw;Hm^4{s^2YIuU*IdO(mNI8JmW zEKI@5|7{CSH&e-jUe*Mzr_KM7UaBv!@!97&&sAJ5U+?`5i66@Ax`Hi{p_YFurP^JG z-+4B=0Q5DwdRt&0bg8nrIvXT`!N#>#M7mC}&PGa5l6v)isw7Xfn z(epl5%2K{~a*A0_f4IX(LEYE}Q=zO=|5W_18S*yw-W~eqq0LJ(-xE|#cvIEtUy}HtR1N66?M2;n7f(RDW~91+@E1rs-x;|;<4J-2+LHy6 zQK_^6UA-8Je2AJQwGrjPg$7Tw^96aj+lFu%!V^l7!8MLg)4cgqD z1E2mK8;S#eyF-Q;Y5gtbWE0zp{ZerI6(t-DYm={AYeiIb?#}EMf}4o7>O16a1%vYM zGf{m@N;WLQNN`=CCfD#+f8Hw-M8glsLc9juoOup5#Z4q~PRVw!@y4kh6h)D;6A1$& zNd6@yd3^WZ8QaGX_0Qy(SNB3hIUP%v1~m5PA3btzSL)LbaoLuG`7wJyA3Q687(}4f z39gzq^KM=o@M{?!2r&LQ+#rt&8(ZOC{k8)x_6K!a>zZ&PN(`V>KNie%&2dQSLW{fJ zP??vm1F0KDPOzfi2uE=cXAWK)+{5$mh%6NI4CFtjGL%RL!+P%BxsohkGxRFS@Yf|@ z7U{IxUU0op(JzE7(YLSDgDXM6ZfkMYn8+ke>+F@0QWdTO7YE+$!GYJ2b~c_TP6VYk zRa(|CBvT(?{KdscNGHDEx3F*l3`OqFW3O#>^VwnKarR!JHb@kww_y&LbP>xP4!My9 zO7K@&djRPv?AxH72Nm)vE>&<8ca1~4(RD92n3dOMvzoi}DSg3-89;WDN^``~kj1MD z{I|q@lj{!=*U1$^8POZqXI4T#<2ikL_i5 zR!5_j@44mU{T7`e_UL0zH8_id?*dgiBDu>eps&!`1q4HfXyp18-})Csa4FJW@Dm!9 z22p(UH*mP zO0D?=j$fcSPhI~WBE~1L4CEyI5>(L+@m^M9oe^@)F{E+Blak5Kz7?oN??mG#0YofI zT(jsRhcO_oIgSn*j|EwH0m&!VD{2Px4_vA9vDvH6=n+ zsn4@bawgSspDRR|qB_RGjSAku877-1j02Px4%D}cX08>WHP>9#FB8;V0b@wkL>$xm@tyBF&6;KSa#Xyl$n2R8H^01J(O z#Nh{WIegVK4UX4`rr$P!>HPTSJxth=5M{0t2TnS#IBtZ`M4=UGYOhP?Kkw>8ak*-& ze3^YmBw$TH{upc5O@30Aw{=oKMcc#)>Oq>{`US)C4KJjn#_o##;=9PlKq@z=FW= zW3jLszqUB;vz67WCd#C1@y1oU|;!i{>3u_%((qz&X$Hlo`@% z=PY_D*kh?%r3Wsuwt7QKl=5$~YAgWk!3HPw0EKs(u>o35EgGrev>0$fo!n=0ut{l` z6c2H=q4#}$oVQs-R-*$bHQl`eO@J>Pq6rppNcbCGYk)#-mZ7^q)FRoEZU)#Tm#`cr z`CR<9BFC4r%FzHJ!hJYh2zGV9j@l_2Vz_~6K%!k1{8DfSvo>~bdZ$ILkiC*heW;Q< z%bsmBY$avB7ZCu%5aw6`Q=xsWa8RJmwUk zjnb>RrZyf525rhryzfX~J`KFRPo*exHg-G&dO~4Cn)PLweoGam<@`CCl3F31EOG4) z-7IGavV<_5@B?IHjYRIR>bKvWWSrd`yAY%4zI**9;_dQYt7ruD1`^jbO{831GV-0h~?;v=u=XhbT{70+74#<20OiAu#-+sW9 z;Rn#btqiEi*+wzr&EFD)-$NssEcU)oH$QSaZTUPLF`gn*i`w(gr_N@WwVRunbQ$;ynj(8H|}2FLi|-AkS0(BA+X*?b1uNN$zU5 z$z#7w!Vba_licVd4RmeJ5uM;OXjPd3N$>hCbn%uAU4sBAhg6MYxoM5+fU|&DzbO1^ z?z2->IaZ+v+Sw@Ru06CCsa5lfnM>!eepO4VYqb4@&z4T zuTTw=NMeLtx!7MNYb*OVI$+C&T>`Ezu&^WFqQBP{%}EzI#@hD;Rc3dmK%)h+PLrP5 zZnWlO9aq;#_3JwE?X(!t_st_1zKvuxG=27tUvvR=$m{FzcpBj^NE_;FisFtP z?k+dJNWmGD`=?*xK1gkLO=r1A*`F?0o#;cp({NkMZ+W;N4!pP`Yad>L?ENl+Nlh5K9A$ZB!BIEVfc%oxL z`YW!yO|GNipyy8|LY0i4UC8wPkhc_3hMtW%#ctDI@tSD=og)*cWS!E;c6I*pdot=o zdZ-qF+q30x*oDR$r0aO3b12G4auoHj2}sbXn=apVh)-ODmI10ZQSey=UuLYE$@GsW zw10Bj?$TmuXW%OgMoL1I62xaY&;)I-=K4J{@>hZx5H#ehb(5wAe&W(2nbwW&Ik&uu`y??LI_xCTi zn;ULCujh4L_s9L&J{F-zh`6{o#@Su#488z6sC!==#0hLqz*XejUwv;#SHOSPqYw~c zPaSxRaZ-Fxzjd$n)u*&-)Om5#DwT9Fh=J0Z5FK_xbNiy8Q-S!|iGQ@cf@2gOny(!Z2|Is2*}uTljby}O7q2Z-VAL2;u9O`}qn(%#pJ=>tDT%B!N62u-);Rh}vJj z4>qmACyO=X)^32W#)48`<_L={1IoTU@scnGx~7yafO-Q$qB6PKT%rBJbm&*t z*?jTaxu3LvwMKn}7Ym)SrgA-tSMVX3m4xxSRT#~s`dRd9Rt7JDa;Pf@rXl4Pilby^ z?fyoBAE_$I=lz-rMl$5&6DALM-!2&$kS9IGvKfadM`N;A-2Pe#jd~X~`jQnS#>?^i z$!51|nYJa-&`YB=33ggNeZra?U;zCvH^13nYS5#+t>9H@D4Dcv*Y!S!r*G}XNs=lz zV+}`BS*h|hx4k5N8-0F22U$Xj}Fkkj>j8^ zv(lZo4S=a2zohg=2XRolIG9IN8F7zUmsTDXpt9C z0e8!q1;8s!B*Ntx>y~m?nH;+dMSK@zBKPXSE^9M?{n}pml=g!~Yd+V;Use%0hZ$s* zgwi?K+k$HJGhMH?-n0MpC5iYv``6!}i7JU-xx`H|Or04~GimWY@nO!QlxKN!nV?9| zozC4)X3f*NF0;Sy8^)k`uO41gx8Yv??PMd+4>ZAOauY8k5kPMSv+p=3m!JOoYIBcuTp8GP2KX!GhK#>55|g6Cn0v;QO+yeS zl@D5UyWrdlYxuXNYvHrfs2!rh1l$rgxL$JWQ}zu|7z*ePc{l0Q%?%3~1b?m6-bbE! zu}5{w#L2@JOM14Ipa+QPb<<=7H#xZm`u&Q`A6fGv{o&SegO>J&LwF7uh8+QlO+Z^# zdzWIFWjfiSt~WeYGRTE$0yX`*n}h z`+Q7s_=}nCt%6vlM8rzOU2Wrg@)zVpizTs1#s=EYkfWk+RrS_!z0j{JJu?ESAOK!j z%4fT3+frA2X~<1*iCJ?4oKrUo6(J_q_JVRhBDVA=eZ}rvNA}fUxShVdIdTI`=slic zHtX_VLo@X8P{ryi;a^*Czg&v{QvVFP=!ah>umB8=BU^pu$Lss|c=nnU?f21goKR2W*hi6ftuX!a``>~`7GWaf15rFUM>8>L9mAulV(Q89bJ4K^L)xWq znSTp@s`TiG;&s#EG=$)XmrER<7dew-m?7dIvQn{mvz8oLN1cBvGNA~7l&_;L zOU#Z>A;Dqlu<-$GwP##wv1@V-qk|42O1V=B|fj39GLVYF(o@F+ecJD zU|CDyY`K556S%hKeH^*3sr!KlkzV`M8~(RR;^Nxb_Kxl*Qs_k~TOEe; zn?>bi_VLFGQ|Ut|l4GG%9jF_d4HqQd6k*%bw^#d?(2qt|nA?QU^Z!vgRVnV)TnIvi zn^!;wGs^B%B+oc@@`HQpjUV2(_rgslkt$X?{0%+G| zl4h11gC$yzuLQ~X++4S_c_*ho3PrH0J!8fF1P^~Zs}^u6sLuQ0*BqkME~tput8Bla zd`d$fa5GLM5tAQD6ed2tvvzC^@_ugp`lTO;om7&%)OvQ*YwAISIg(>@AKLH?ppHhp zjfT%Y?e2&XM$kRdaQ~)z*Hq^tpw}0PIP1$C;86r?y6Ap|+Z!%LWWAGb2Xz$2>sK>P z8xt@*Q((uVIpa7lf)UKJxJ4!l=p8w}3uPhcNJx{~73fMIEp`BT596(ACJ|Cm+JEIF zBbWIeeaLFMa^|{mrwnCpurJPHJ+qbf?~ai-d1s;Be5gb$0J^-4Td3zee|rQB64|}G z-)QvllmY@NW;w+Nn3&EzQUu^g>WRpiPp`Z!_!xDl!If_AcUJ_=#0NY(rQ8$|pa^S+ z@#j5LZ*wD0r1~K1{&6@n`LdhHz4%l2ETrmwQnq(n3)@DgR4Jrdm2HH3ov2dTdd276 z$>^Mq-<0tO?D{>NN_3EhLFxKqw>l2I%|%COu4E823_CB0!k{9^z=PoeUX)8=}z5vvUgqisu` z$yj~3L#d2a^{^BfgDi@8XOz5XU zj|x_XVcf$75KuQu-?DTAb+j~oR2oczLR~^1_Kqr$F&z9mbE2sikc+I8Om}YQ5hXUA zex&-SuVurlXbdcpQDvR3W(q=0V9vS~Ak_yz7O>)xrLpg3jwZRFZYPi@6f}`@)8TPv z?`p+eimx=Vr--5QY<}>|Ggi{5A`Wwn@G8Urztl5p^9jLF}2C7#Is{xP&=bG=15+ zUwWZ;wKfCPgMof2X>iB;{>scx<&beC(Gu*~uRb@me2(r}e{iaINKk&2a%`5>iCiYl zme?$Gj)t4D;rV@pO?hKXsn^KCw#E0v(#hmfwlfZp*2J?o+|}#d;;rx^7d`<(Gl^n$yaBc4%g- zIP?SsIDpyYq|5^lbkR&)9%&Rt$+t2wkLWxwe3v8gGaS172T7&)J}4HjfGTE(M27K+ zq;w-jE{@BnifiejWN2bZpnV;_BG4rnwOCMQXmdUXC3=xRrq(IapASE(QU~1^7upKmipdN~>IYF1fj7_7^Sv>qDw* z&)<2OxL4vb(p|Ay3IBoW{sW!)Zh81E=J({QYq@Bz3*TM$CxJeB`NIGDWP`>(_dV~v zk!ZP_{#GQZ?1;%3KX|{ys|z@9e;Cf(?EB?fS1(S_2tXY5L4LWm#+|DP28}`$dI%SL zlqfd?r%Yv|6va35W(!StqbXVmnennoo&J=zb$fTVv=KqwMiboAdI<^sF3BC+nO+~| zJ``n2)%58}b2Qe*yf3t%mt8c;g5cy5D)J*2vwH8jJx?GkX}~H)y^~B1lC0|nowXtR z%O(}brE8@SixnQ5q(kZAiwO>IL>0b~Q$AN)CRys zZUn)1VPFOBjg;xC96i0=yxEF&iAM?kphaK-KRIBLa{|b)>tJ)(fIwrUE{s*0x-*mK za=QlcUZk5?V8g;byy3`LUd0RR=3JLhR7~1%G?kXe+x$y3EYMRs4M5za!>I?cQ+B1D z^PjNBu55~?-p;+-g?qKLVJ7nud234fDP*Zv`D=gegnqYQZ3DeTbj&7D<^!KS8Ch0@ zB+(Fkjk77Cx6~6=wZ6J8+zAss31kU5gBgPo_gOtBbN>F?7?8&!II?S0Jl!%o%&yfk zF{fq!OuaHw+;Cbs*E*7ja}5HkkE|>T?3Qk+DmC%VmePBNp_B!TFCEFz!(8ghY@buMiCK`KX)po62RwvwO zOcPhk4Ss6IY8_l3wmq{6rB9TANY_=f^C0C1)SJKSCo@Ci_)Wifo08~8h9#73Vd#|$azp`=p|La`6#YPK`ki9 z_LtMs(g_a6My~4RgbT#6@_Q7ymS6`5 zd-FgK24tTJ6|fgBMktGahVdVradvTj#}oy97s@v13I2ES#8BKpm0kA*pP-4!@p83a z>>Om=j2(wVc9D!^!o&*ujmr=O3+0%ryL-EDZn}U}`ka3;VT?m~X{P{4MtJ4^;5cmE zYjh;My5LV>AseFMcm8}L=XxR(t3fu5S57wNrnb5_I^k(c=Id=hFF{1CsIgGHn>JDB zmAoRj5R!G^afJB?UTtDmT|nX?I1cjUMrWbUt>Uf`j6(hBZ4PSkogfhKn~CD)%a~-d z;dJMy^8X;oc{xOmM3?+=0&rmf|D&}V7`9t-iiG{%x-D3~>VcV&1AEfT2<|V`D1RBp zjouRjAVbJIP?X_F5vgNeioCl}b)7#tIb5(>1;6=H4?CGK2C}6B$0p$Xl&h4gZPF)* z5dWBk_;uZS`B83c415-#P3O1>1rjk>Vq4Wchu#>L>^+&=OmPT#%;!K{J|$eG$l&ij ztT^Z#yz%a<2N=vL6FsgHi&vbB+=M z{c$C$XFWx9lS5_sh%n*IwdF>{mbjnHH~?qc0N)o}@KIR^$g#_Gi@ax$Z)23bdOJS* zlBJ!t&vSz+;Bt04aX1%pX#;c8cj{_VAG^#%Fphl|INyrw*+{= zcZFl?Yt%3fbGENS#4XMjGUYxr_kf)J7@f2@gS`$vTV;wdk9-cKT01;OS)7A?6s^(= zQu^^hifnf_jmWJ&qM690;s#g~Vcocq5M`}py`?R5hzmHjPpYzs?WWuH_d3+e-N-e= zQu%%#XO85HLN+quU9gzY$f_uH4X5vw^{dI3CEP*YW_6-#Pr01<_hgGOgQ2@$^(V%J z4Su3+t?byY8^io-Ei=rsHSH-}xaY4!5-tu*=8vEz@bJ@n4?Hty*?e2+pa8gvX zHsF#=KUQVEI6S`l|0}W8h3|Ium&It2|JMUM-g)y)u>ZtWw&f=;Z0@ws|B7vf)7ar? z+e;-of-{y@KuheuH*H5j`$;c(fA>?Ol7EVA0!pkTk)h^e7tMkl*nE`cV90}QEMQ9UFhQzr|f03>C zP1^r*f9x4-->}I*XXuT=VqmV{)ei9I5^JqNKL^M9WmZ?cUus;jPaG=S(hi?E4-*J+ z@+z}I2ZNrnUF7#q$_(faVaYqz-cAN75-W!XS)Jk9?@7_$ZmL1IciY7YHcLn7+^qvu+r z%j=k&x-`w0FQh>wXcZ{b+lLhly8%zx*B`{JAiIm)1kBnT6DBKuwhg*FRLdO_F&!>ifh; zPF&LX{7m%k4wbk1jxk+c1RwIL%2N(RD%cn>d&VugJUO=Mj6{qA`A1rLQ31WjR>yNv zg;ir4dL~&mhHI2#zY+Oo>zI_3J>DRj!0!{YX=`I!t+$v)5Za*R@7cvz(s#rpKwIUH zP|HG|vXHmhCvU#YBe|+&=Bqx>MGUwQbisQ>|X#h-BD zd;rL4jwT=Gmg!lPiJurSWq;{Iz)fZretWx6HT^x+xHsx6Co;Y8f}r=kLLDW&gT=sm zcE=Z>2MsnW>ZTQoY+M%mI%`Sq!jZ3EMbrC`p*W2f`q@^N@EmJ?G3drR zVZ7$bJxG&$m$=VIbRGEVrB!A5JQ5I{G~)1uZZTMm&dC<+u?$GhW!a~>OxWI+JmdC^ zRl^I4WveP}?r}ru;*NA()JH}3C$V))S^kI7%YoH;)*nMpJ2OI~R=d{vKV3!r{^+XM z+)N3XuUKekl#m6V%*Ckuv=i)db~G z6lS%Ne)QZ#xtQ`DK7OCOD#^xt{xCnh?1(%cdAz`mrp3i`6!<86AwVc;ynrtkH_;*r zmIIeQ5`++V`BNioZ}nt`T3%CsO`03OPk!nfp$|asXo<*#eA1|G2u73VGrf7Qdagr~ zy1yl;EBaKFml~u9IItq$J<-x0Wh)N81qS25N%<9!ZQklg=uo? z$>E7{HS`{Lq5z1l^UB|(3V9Tm;U8#fzrS(3##X(nRNV3r45UL!!4}*9zEAu&2kx?E zkt3YSpSRGs4yTKCc3O2pFNJ3>Q`L#(Y&)2ELKE5yi|M35`2jPcs*wG z7cY3j0g~&EpKB7OOOzpUW98YNPZWY?p`wea-K^Xc3-rnAHxY8eX{n*;n9R6pkv<}jBV)ihX z%dwZG8#ZkyE9$rw|9eu2zlbwQic;Xq$TD!uzBG-Oi@?lpe&#CTCJpkD4{G&qA^5p$ z5op6VMdAgW3|2Mh3e>(|3bR+`jgZf-Tg}-Dz7QnC6h|w+4Z<@?03)y7-erf5t&rhc zA;RZG38{7JBeCLtBdK+!{shWtbRCW#YG_3U$#w_$LHUZ52W%JPqQbDAot2A8-8f-ZDJ_>r^B{2_3pla+wN+{ zEW(z7r7mAPd?Y@*T0ZSig$r%X;a;V6^<8zoppi+);w`UT^RHK@&ncpN*KL&B8tR#- z9Skp=EOso>t{<-c3rG!f1pNG(;OJFjaBH+3q~)Th0+Cttas0wBqTh|Pa(jPbY4qB7 z4J70qb{a3PcXjGvZFulg@{@W0*IpAd!i%Yj=(rjS!p&R2WSP@sM{v$wd+Dy=VElvM zNgrw)48J#1{#o0e^H7UDe<|nZ)HzUmApEb`ZdR;#`tOOuJ7!I3e|drSx08tfpIhv! z;S8VP;;7Kszb9PgnHv&-MIsSoms4)uTy}S{Oh7Ol<&YY5bHDg_{MPs8av}Gseb6F| z_hMND$~;c6L8`=miUU9zQ>rO4Ste z=2;|Jo;)tkSjGRvZl7R5Hu&qHrW*)hjM;k!fT6aZVtKce-7Wv)g zd)aO0D#cc=@j`O_MsniU$I5bp17tBMFK8`fWk>a&XDDIhfa}u?++e?s4A)78iZL z+{jK$Z+Clo@478~g7ZZ5J{yhCm zM#IgdHrj$6ZD{$fHFW`AXgzbHw6^-j2C+V{cOQp0Ytp{b{^Q#ebaxV{KgC&km+Md6 zuyO;Ks+3`mWFfH^Z&J>R3aC`Sy@~!63}2}QC!FcxivEsNOetxcZ_Ru8h)K{qeDpz{ zw~L!ciU!)OQ4>lR(YpSslq+~y=tuVGcwV$fw49H*TJ3XVm2PP@ASMa~rmipM2s#cJ zo#pU@(}ABEI~fQq4>;PjWmgfsz`UfX@)8ya9I$m=xAMdF9W@56@9DhLi=$basTxw) zo`Ai%RNkhht&dHdJYOXKXlH;8`{K-x``p=(q~S}bM=W!5Ch})JNPqY9;i=^=-EmU893(}_ao*@K~H`ng8>uu7LBVtEXJOeu3EEOqP3C0IJ`68{>+6P4q^gx`aw(gd8Iihe>`zdwUq&p&N|IuYWz)JFywxX{g1rlgarHO(*eJ`6y+!v z{mNj}I*Fdv4+@nom!x{Rz9b@RvmfT7>Cjc+tDHM5Vhcwu3R$eyIo^9mL{^dBG!L0C z!p=PT5hf!``P6EBQ`5TeG&Xw`;T0`9P|8;z_gtZ5dGqVVy~g&q$^1}_w9FC921QA=^?LD_)=J0c{X+VZ zrt#eGmYrr>=zZDD`F-@4P^s)O$(|6{nqIuGclb+XJveppi{*=|OLlo9} zcQLZBA=&EqE#b(&iKB<}Az!~x&PCSoYDwjdK@R^XH-ZFW*f)oz2WWz@oe~a}7DHtw zdS|@gD$*z7Znc)_S#7#80yS0}5;lTd-lRe-&bsS?d!@4w!fzpK@rzCxx8G<*Kqubc zUhzzt0h}$`hBoPhi7;2yU4aCgEk~}6nRnvdQOkeoaudJK%WPuU)U=vqjw_R9Nh1d; z(xo#h5Rl^+>smP^8dEo`*44+#y$tMnC?&vio4Cy?R&r0sD$TnaZD?LwK8Q5i7hV6~2PrT4`~R#QXS?zppRCAK%~=1x!8PSSP;xBdruM+t4hfwkojHFu zEm4}^q+<-9_krzIF!0$J!{e--Jo@pKIUoVqTyhk4(*A)c3+A)m?9X@1`6mkX*mmdh z{@A@=X{PFQ#9+g>>Q-_tVkBhXOTE6C18$sZ)R{LyN!*|g=<~GmvDnkrGvhhtd0RuvG_sQt8x zqfT5-9B8Qg5J>&R8{(&nUu!(p1_tA?jhgk`b!M-$oqdwZLRuG(rlvpchbFKHq z?`<7NHM;V;jOBoQw74ZhB%JJewCV3SE$+-@m_E@Il$*tC2b%cRN+LI0e4KL@qv&#y zDj=sWj(r4Z>JdiFD%IC~1Gt1U9?Fw_7-sna3lr=;ocfsZJXGh4O!;YVg`m@LH#s_eO_Q+N$%}p-{!7 zlBYYu?yp`Rok>criZ%Si5_>eyHn(j1ux8t`OG3?MFO*op6aa<+))q@dl&W4-nuRg)JnA?wubYTZB!7SzAsR+fM8?x7vWdRmWO4aFp zXnAaH7!MN(b1;5n26$Uv@$RxS*irFLH)0GHBS18`bD+YvwsYBvt0NKEt{}J3P_LojaY>8Qe4!uYEztx zkb}#DA%OZwGkFLkHx}Tm9%8J{aid=i+dX;L^2HNPH!_pbZoE|TPx%?D%FEqL0UZXP zOt1|($VF+7XQ5hx_IbMrQ!WN}@1^4X(G4_SnkWprK45Xq+TCUAa+cFg+wSaZ9u!Rm zxADdE(w5CP>alg*%VZ5vR?iLS4}2u@Scx6YB*k<`sVBLH`uU!kzLf27anj-K8M~~> z2>5K%n})j6NyNhjn*sWeJGVDzS#Ub)y;))Www5wQksCsYaKZpQPV53>#C z-tLF?culwq1+(?dlS(sda?T)V`v}}nEfDcI9``dtg}3WjrTg0He^=JoyQW;Nh@@$L z)Rl*CDOi!{Fgt~|GuKv@1N>7m8~L8i=jFZ=fUhO04E;MOJA(co<~^-d28ZO4N_ft> zvtRh6xU=q{?c=kL`m@z8OygTgj9)Euxw#>Y8`hzfrn5oh)@=kD$5zn0>Kgz`A z+b>gQ_8QgBbw$3Z`-+hV#@o3t^De1l)^0-tJWf=AuFyQ2gYlgJ@n?eKbEvmaLD}({ zu6^95Jd!G=KIvh9&BS=KM!Oywy!>rvRqf}-5SRaM>*X}}^bwvZnW&<3`R~qRP)UO6 zkf2Y(SkUO%M?yB=!)?)VwWj5xs$)#4HcsAOf$QUC?L}F+{QCnA{)E!O^Ik31M$Z;Ro_K)G#USZAg{=jI zj&A|+4oaUNMvwYpct_q3ijQJ97cydKRfO;KB$F+@Z_XQ^+f}x*Oyh$#GC|>i5W*9! zJNxpX!@0e?6X-h9Og&;*07vQ3cer<`1@9z&s$)aan9qoUP+igP=XH`6tlj#6-o&b8 zGgyf)=X?vK)^{3zdWD0{Lh`U`7JB?Le)o!uGBBC4%~qxXL5OvrA}#>Yra{`%k#VJ{z15??OsAY6ZS%8l_{BI;Kx z{f{Q)m2Y#sVQ9hm@0p4W{ir$I=%0up*Y{D2tA^W#^6{~@OThfwOVRv)(X74nW0B#V zgfD~ldv83xe|!c2$!f<$v+fDt25xu#2jbYx$-)i5Qy8rsh$DT#K9y*}B@W^Te*)$< zn?C=6K7AP7{a;Zp_I5e1oCr~?2t?l&cP?Zzl#c?oMd}hznE=V^A?r9^1m<=`s>~>EF7*9A3FYUzBRAfJfq61prK?>C8DnHvp+v~gW8e07A`=uP;rNXHh)~7GAbF>jx7jk{ zayNhqAolEbcM@Y`y7s}@_fNo^v&l58m&~xcqZ`i4*G~a(6)OAMNNvH4p3Pm6@x}$1 zSq_2@jB-^5H(DCcdx0|_vzF;=I8ur9eqDn8O_Z>9CLcUrN5d z@(BjNqgbP%-0x_24w|lxJ$ayc=c~5DMd^o>g0gUP_LR;JTix4BY7eBSK6&#~TuyQQ z@ZXgs$vo=Z!GnG_5IM=OZ$Eb%FMk(o_IO}zdIKP%HR7Z9swXp|PNw;};d%C(P7O4r zT=7hPI*eprJp@ck5j=G0esz2LyKG@{S;)|uN2Ik$0=&_pR40||O45OiQzTpCU<3jtl&kG92$=q0DP`600=Jw8 zBl`T~2|V9S;%*iFrl_P!yzlNr>4Epf;#YXtuHXm+=U9JVY21iyyOEy1J)czrh`vLJ zuGcU(|M&r|ZEKGAvp}#*Yn7)5j8Um3c17d`T z)DHQUQ7>k@-f9H&Tc|KHY;QbN?W1H{$iY)vN|}bWw^bcGL~?i7aY9CwCx%4yEN?6) z8WZ9pD+}^pGT1q!yccY}XQZZJxhJZ$g2u_;sh*n)$TP)7C<%~Q{Yt)+`LDH!(pnyL^{m6}*q z(tx3?JQ_EX2@25gSV|_`WJW8yyAJhnz;N{VsQmN7(wf9+bo;)|(AZJ*NCAoauYmJRGGB+>$&>uRsSYt2Ta zVW6L&BuKUqWR)%_M@S4A81YLr%}OFieMzox^)aS|C!Er~ayHh5E*Y}_298f6Q;Ht1 zn3h~OetC*!M6I-a|J$I@E7>L%v0Bw(sz)_9Iv0VpdYwU%tXtD`V`V-Hsqzm4t$%0& z=^2~PIFd%$Dpcxt=61mjjY{EHf^-J`E&O8qq&=6@xt-CwmaZnL-FP(%=QX3c2VA#dA_Q_wz1t~i{ctmb0*L{+!~vQ) zY_Gw(yw*?udfYkrh2SfD8Fn-lFc;dxdr#ku37Ib2MWDzDc~&k97o(RaDbZ7X zo0G)g>S&!OG;=u^Tey;ds4)6syq$3a{`I?D48MZzAal3DLeAu|BZoE8x6HAm-~%~~ z#$_g|CM!}1D{*_1riD0iJnbMZ?rtW1p<4cNxg{`C?#}c+V{^K!NkF`sMIxR6fjMq) z@iaUcSoX~hd8QA=2m0^T`4aTJqPXz#N(**&*rf41xg1aG2;A21Qko*cDPp;_k-^FY z{evJmon52|C>x!VAP5a2s^BVRMZiO(M`Lomy{xfJP)yMrt}S|z7kkbTYw$^#$~Kq7 z-ut_sFrgud;Y~+Hj#`48ok{$S+0|!NL}7ny5+*uln})tG?h=Gy?STkj`*GF zV(}keq!s0wd(^Dlu~K6|_&Y9r^A3QjK=_1mUJP`N9&;I8Q~fYi)?V@K_*7RQM77=% z#v)u#-*3*}cr$c&!N9CK_3!B+06km0_`m141k3*eEmD91zSyn%dvO7^z^I7 z0I*nmn)t>wrMrEo(z{CrJECecpURb`)85`?UUQq*bG>Cm5b~bp`}0Z4TlLq_6K9Ug z8MN5NpAADP(n{NX`;Ee3&Tn7`2Q{>Eyo?m`NhR6s7YIDYKlR{7YAUEx(!37hZ3;#O zHqFx3v@q-)`eU4l_8qhE-%Vq@O(Eu($G-*{z3Yx4ibDtl><6bwxn<7#UO+Z9(Xy0N zJcI6{1^XK+_adeC8k3~$uxoG!H6N3hpu;cE5(@2#XF66vt$#u|`kz^Rt^wwN*YlT8 z#wt{rKqZl^4VN6R5@DBlV)^UT)iN;2>H3S9s<6R4Wc>g~IYC|@0lFCaiTIqLe zJ-D8W>-e_FucUinopo*Da*oMh81E~_Lyg6}L6}Q)ML^tJo7XC}RtLkJ1*K1&S~ezh z%BPBaA};>VE3hR+Cy;j>JQtCn{C;@YRXH0wBnEr zc^#`z!zPf4U#>+GV7yG3uK6+L$H9DpwMv6A=(gS}SgA#1-$DHiZi|Jb1n|HWw)!`V z{xMiC0&X(PrmH*U-tiGgABgQ|9y$-Q zAmX)y-L!3QvP%^@Q>#)wDIGh>Yor3If(87*I(jJxxSO#9Z>Sb{ff5e(sY6kFkIxD% zlgfHC%lXoS@;WpTji-N9NZA;87l@Uhijq!K8PpRe5UA+);%KuFA3^n{1q(Zbn1L~! z)N9Y6hcr(9yvEX@H|zdX$OY1ay2Y?{;AIuer)>PW?|~)F9e;LJzdPr zzmkOV0j{vwS+`%77NLLDb6>4hGvD821iK(p|?52AzQdx!B!Bf(K@H39c+?F+dc zL}lZIS+$l)xtoS@c;tFXWrTlsflm~KLMgajI*>s>bT&0|`KW7}Ey6VT$+UG3GmXRrM&+I?=i znbD%lM@8pF56zjclttjqvO@ zUYy=;P0VO{|GGBhs+R6Oh3zdRabEy}zY7QW!QQ42>}-|OhS7e=cH{5$ILq}xJK8or)_5za-u6dBQ<^p`sTc<~Y5g9@B%y*_r7T_;<{$ty_B zG3L3NTV(vcX?I{tzeqb*)|Zlj%f|Nh6) zPV|7KIX`xkN)NftxTOXI(?NEeai#fOuG^ISeLD;QO+1pV;4DpbliP6kPTH3OG=EQr zY|Lzy$Ec0a4{}7Tu~1e&`G@jXd>z=rFP6$RXiN*gh#PiI&0Tp%8a0^Gd0QuoxsyI! z;d1tN%bhOZb_GU#)`0J^RbBRl0*U0D5H1HVEf%p-Oo@bN=fKU|K}zpT*+2XxS10*7 z;>QfraBl)A&bG}>h9SDwe%a4@NB=yx-D%F<=pTB$aJu%8QYiS#_J3^o?g;gN zd-czE7b&$d11BP2f3^i$kDmZ7`kA9~X5KTT!Tyi)$0A>>UqhqZcC!|skDgZ#_atH! zN8Sp#s^havp)O;nr%!8{tXQ7kt2c7U!t|^c#KQ!{E^PuU3Nm_nAhgMRaJL#c*`t2Zj zoR!1%49QaWUmMfpI{B=3%!eU6*syJHl%$(Nu~D~@0b!nj-=0GVY7Na204B}K;sb_ zL;o-`T8tK>8-%mToUq6NuNV4GcwA;(lJ`o?dCosJD)~9U18{JRO)TtB@YjqXNiE*} z#`C;tMow5Vp3~Gvwa`>tj%20wZs?7fa_8=3;A3~73%~U#xpGAp`ruvo5Lj-Z$^EUo zOQp^_4G6wLF%2&ihBDWDZhu3f z1g5;*{xzS)ebsikuEBcGzO_5ukbh&rf=8Z7AbfeKr7&2jFt>S$g@P=T#(2AfrAO$m zZ4l6!fxuO5tu+0IME&2Bt$9#YS#r{7c}+<`Th6d0_Gaz-^Kvc6!MD-3me+v?A|=#Q zoc13E2V^ej(cFZ&H$4h}$m@N9*s&2+O=v}oWw$w*Wb^fsLT*0K`%c-9KJPzfgk9ar zm-pdHtSy%#D?zQM*V5ye%|Ass9INL7BeQDH~sNDdhG& zYM!Mjc17a_jz)b23EWUl+mX2n$_nwxIXx-G;{+3o3UY0cF|&*i1trEHgMSz}03P(D z`V`SgqUBzLH8#PXH(8>P3?yzoZj z(~6rzjD3vUvB8t>im9)&?TXeun8Ea|TUg8%aYR)7i`8gO;mSB~Vt<}$C?yvX83$&@ zf1OHYF~q~m1d+x+N@klFwY5myIV7|5>r5<>ZARa`4<5Ar?u)l)4&R zrPpe7%~QQ>&JmXiy#Bo?4phk@kK_t{dV15CF5#Y@nEFyiLMJ?6_oR|^c}NF6houE5 z*jYvqQ4Yr-Nj@_2b#Y#_vuQkcI{;2G)U}X@)YBq8`6?MAWvYq2i~PC zh_A7w)SM{=BU}&sk`xitQ+A zMi(Rwnd_Rj8WV&FGuJuoRIF4DJvFGY=KacFrWicj4Evkd0yh7hE4M0 zpdYPSQ@Md8_23@$L-uG~I!B${9=RPU=y4d%K7ykX0yYKDZYjZv4coZ%G}Ey*QFlXw zvnKt8dv&Ppm=HD+Juy;@6~;$g)q9!O$tsx#9epaxdlkIkU6d)o;+o~zgh1fqC%rW{ zBH@QE$fUOg?}WZ#8-G(lCX7QVwV09xI^so0xPSL}?@Wy0i3!gDVxqieVgMuP2kSvs z&`T90w63`)JmdWORH}`VeLC~bDI;$#J-s;Ntw!ur@aGuES_#mO<%@!&f_i7StvMr9 z9D$s56$bM?+MviFboHwe1J!frMF3ZxMlK^~$v)KzGQ5}yeqNY7RK^n083*u#jCQ6$ z661J1dB!LLKt|EY1n>{1=~AlYduO>F>NG=^WFYpp?BM0kfXC zrb0}oc{t~hQk{v^emS59s^IR)$?MHMMh7dL@#&gG+q;p+Y@WQ({{Sum@yAT`pbiW6 zoE!cJYk2-2vmEEI`b<^wMSXeuM@7Hje~1(>-GT$b^tp<@L$4Du{yOyiS?J*Z0EYd4 z$S9(U^Nz5hiYNozzu=$p1o+i{qyxLdbB=%Ov)62m00ZWEn7`tYOarmcI6bMzr{wRquQZ2(*#|WwY60Z% z2YT!|iR^OH7w0{&YE4ms+LbNUyL9)W(H~~)_02=7KMJIk!RH)`0B)adS93NAJZBxL zmw^nVFVVQE?8sPE$?kZ@F;(tzCS#rn#{;DRKF}`VyBGqa)QNB)F^mKAXWpcsM{F&;CT3i*l#2T_`Z7mSwRnn~HAws(w?@6IT&`PoAL^s~0s8=3twX_28a zs{lwNkx@yV$`?N58o6Si%ae|uFM0`WvdlKJ9J7w2lb!`V2-wI#BdO<_o;6P?k~RZ( zQ&pZRAq-gOCx$+i4{(m*3Wt5@M;uV-0)X7`I#cZ8ac-pJB%JpADdrY3DJ%d{&MP+c zHdiU$v5wi4;c_v_>r|yhSO!1?fPX5{y|r1~LI)uAC-thAkgzy~d0x03YFv^jMYdC# zBmfoQf!EjD^s5$tf!t%IBv!IrVPlA#H%04O5m^Uu2*%l^{ov)BjkL*VDvtfx2OCl@#WA&hrybOmAGilqk_MsYukJe z_^2b;pANNmPyou=x&D-WVv1VH4dLr9hZ%69DMGOAlTX1QEca^vJ zQLj1@K4r3Uf0b+4e#w6yCPgFR7l>T;BFyLVH9uOR{K%Dttoj~y5fl>b^f;)c!Hl1j zgYDM6Zr}DM{iLqQRroutB9p^3eSND*zhF<=YU)&qFN8XpFZZq#D*kz>c>F5+MSW7Y zW5wc!Y+DZl6v?esiBuUqfBjYT6o2qff7)O~sD2+VeID@!KQmKW{{X=`KWWL+%{~ma zw;93nFBg&OF+$96i|rtII?ti=LuuZArX-Rx@{9mYPdXUTlBAG00=|xq{t3bROD)4C z$A_#LTcah?@%5)){{X=;KWZ7}No+LT5T_fKO++JO*QG!0JR19GO9NZ6^G&9nw-<5C zkrxll_ycqqB!Sm=4sFW00g7>{{TPEzA^BuTx6{BxzFWMEY}JA z#eGV@W917aiDL^h6WDsx456TXC$BgatFd8m7Vf78 zzNv5cC0~v-`{^!+#N7@?ClN7?!NgT2(1-X}?dmxEYF1-~{$=-v)arITDp2At zGd%Oo+Rk{|BxVHXInHb7JxBft@B3Cp4Y>GGBZ5YDUPjzkl>Art2l1=n*0*ipZC~NW zvkm0(;Q+E{3_Z4kk?&U=ei}~7L_8&H>~eM%=*rE5+r3_b0JsH5Cz30n)V>aUNzx*U z+W!FI?zo$PUJ@;1TS*2#`sat`%@?V#j|C>z`!JRHL|`6*6!xso*dMqZip7fO#c8X zms0p;@fPsg+G*bmd_#0zKo66qIRJZ{Q0vlNN?3ZGj0i_pXP-*#=l!4lI_Pu#r^H_fwfl1@ zK3S5*@xk{MjjQ}Q_==Z_ZF%s&UHQ5^dVGwd>OieyRvV&yjYYaJk}$CaLOxN(YK`*9 zXBlC&cP-6VPo^Rg0-w3xYK0g^sM+i4dXf4?q~2evLk2t80nI8k6<{f_K2Tl zH_*=0t(_dZFzbPe(2ft68OLA$09{&N3V5ld7S{d|)whql8%Uc2@!N{No)qy+N##F- zbz^~(w@Z|T9=#~`uuE184^mwWIgUALrb^i-=(Lt5it1O@YbUXXZo8yuU$;zmt4k2uxes2x zwQ>&$Yr@<*_+wZln}q(zn?L#uNWK{HB;(4q@P@qZ56z7dagTqy%{by|KB4R?t6-MGqIHYTNwF`8SdZ(Ta z4N}BWmduGzsnra?SkELVJ?gdCPYW63N7G;mG-N2$4%B*WD72}>M-t317 zZaWDN&77S6w8ElEK*2tM9+hWe2^KQg$0H*Y7|1Njr;c}e)s<_M`=pZ#hHXm*__YnGMr4tN`C)Y1?S*Vm<2w1~vJ z6N*81YY2=(Ar8@|bVa6~x=e-KsV?_jm{9I@8q++r~f_wB7 z0JiE!&9o3kc=VuM2|jZ&Dh1k>%}P3-5X9As2CQZ(9%#>C)s81GC^VeuhY!h^{kl_tXL z!TM9YqqiZ5_9C7m7zIWNJv}HG*wlYtuZ{lmL(zbBy4cg+^W6cj-VJ zZ|w~l$?#LeiUvrtjN}fOo->O0>b}eP@{E29c#%&e>Df5wF(M!7Um917ng0M8dLD)k z_;1uGqKflQz@myM1KGb}34FhgD7pLEE!h76$3gy;_Fm%Alh?mm__y{NVn1jrBnR*5 zmxKQRAhG_H_Lu=;5wc^RN3DApAK-k?Hy8Y0ksjWGfjrc)CgJk~^JgDgXiuMNWDIfd zR$`1IEO|NIoReLLDPrAxxPr5udkUd@8#U{5^8Ww`HKePA1(yJFbIoQ*;ihKKQ`Uec zP$7hlz;&upyE}}G^PaV4N#qf1BLwuvdaQ`zMklGD3%2(8fD@g^@~pj4-DGYCLBPPR zj0J>jJ-=GWzE&~_&Q4gG0Hz{beb7NYhc%#Z5HhYa+Nwvp&-qmRqnfiC38Vu}5i6M@mcr2vc8x@+A9SXHx*Hj1@3g>J5K3ab_a zA!A&UE8johoWBb6FN&WU=CSc+ogLPlrP(#au(I&u-2H3sT_@nb!JQ^!E}!tfNrp`E zB3lA~TJdq&q;T`)OHV5%^?PCu1+ z^UkV0k=q;)D_*Z?Q{{X=-e`U=Dy!}ZvO-P)4lTVG_{8zpgINU&}rn?KDQ@&5oaD_(oa9!?1W z3~*|k%ovhC3|4NvR<)5_vAv4++GmBV-^{h}mX?I{38%-${yS1Juv5AnD$y7o)~G*_ zrZVg}Q^*|BZ2*<)k7|`UzN2wgEjF(<#f--j1?m2dgCCHlzN>H?N=`e2v=8Z3)j=U~ zl6`58M?ru`9QC2O+j1SPp>6f;KsOhT2VqQYX6WQK!{Gb%HB;pyj`X-0>%i+mlV!y# z6Mt$?gZt+h>>sJ7wxcwFDV_KpwFy|pTWIUg6s#0<9CpB^sa8TyaWvm6zZu}3xTZ2r z6d{?g2N^Wy)L@p19*8q3QxhERlSnyLU9^xRiA{_st4$LjX9W3^H+2z-vn* z9i;R$#<`LuI~xR_&XvnAKz^B~G3q%!l~=ixn^G9pa%blH=hl^O9R}>JF|pU%u%?#B z$_7F9?Lc#~Q*|*J1a%qWn)4TImgl*q6X;HR@lAHXdz?d?@AC3Rp) z$QGMmNGCwo_`9}zLY*yTNpgnZNjNJ zEzhk~TfL0T_m*XCz?=if-Hg(WM(s!+X>v1xlblne0Azuj^{7t=h36a-ROKk>1ny$H zLly0XyyMbX6~CCTK=?tY_*&OYY16^lQp$%7BI)t&{PA9U0Vf~>$3iR6{uwBa#m3|F zb^6p*l)atHhOG1mwad$7+i4_lfzU@1u0J7Euk}4n=?Rlio!IgLl$ucq^3|gx{JE&M zFm1{J&$V&gnweAFUMv3qt$%Nr)KcGHHzwi#0Pmw$ZmrSWNEqavp0!eIY`$Ny&q_jQ zfL-2pxnb?Pp_=CLpUhS#9{H-(kS^r@|`?&Tmz z0|CkX*yHr5#iJ{k7AyjJ&w96KZgyY*J#kj5O%tPf609~C5^uWEtYl-x`7Glt`QoQb zT^VEl0FVxvFnWt9v;P2~(g;;>0LdLQPKMueWltIB(yN}Qaq`77SS7T#Kj_*uf|J}> z#0dA{YFVR$QDl8jPPUw<3Fh0O+xk>-TlrF#k+|##r^d`r%nozT4K+Egp~(@lX<8?k zB|1f>af~gp0r_IA+uWs!1hhntAKZyXKT3qMpxo`Wd(&P}tGx2Kr|l|tVaXoFzwLcM zjjed(cI`;o-S{{cE^;t(O;9tjd;!;v1x~T7r*kM6_N3(}yAD(-)HMbLmhMnbLKy!5 zjWzWRQan6&vh)7{O%7@PR@M3N4>ZvLLOuG?8nTPrO+n~!pB42jT2!)0bqQHc?C)ZK zopD0;{ZGko$_eBS_1b(?%$kL*v2T@PErVQ(+Tn>O0DiUgI4Ydjc(!uRHklh;P9gxZ zx2|FwiXznR6C;2#!mk4rZp%iy)St?;fVsv9+g&b+@Y)4rms13==gaI(ZBr3WoNn0) z3B8Q34(R&Fh%N*c>68O5@+iO_mFV6Wv`-CNDru^#As@VxV)B0~TN`L>VDfFDR2={V z9DiDv0k>cP3i-@NHlYMw*a1TX&!&{ z8ia*7$OG5bnz=*29D4Vze`zPwdlrAS_j1;jpo75{ss0qB#8+T|761VM0G$G?!4&hg`AK{6lqYQlrt}nzN0e%4Zmw)^uUjsZX zbmy$HXz_;Q+zRg(36tqfkrq&LIq#aN)~OwsS}gIcNA@TEk#%GVaqv3I1uc@cH%|!9 zt_5J;{{X=?KVtifnJ+DT5v|#@i^|*UOjS>EppGlwX49i2FeIMfR4-I_Tk(u1JPBlt z!2HF1NA_aab_DwKLnE&#o;rF~k;h_V_Sr6sEwS<*ss8{3f$$VhB)Sjn1FnfN`?08L zkUEdZ16+rRzu=Rf672)Gg?=+=muP?0MR{WJus-E@uex$%Y|8lqrbR_G0z&!pBY1yf>;!N6KvVa5BF~1D|T)JYoL;1pfW6yd|dU z9xCxi!bgKf(rwJwm+)OAii$d%1C#hy>OYaRg#nMaB;)m~52GiFv|UTWvgF08X_ppk zGJlRX1NE-T8O|e}DQb*rU}(7Lekp`eXdWZET{5^GMP~vg)J`*m01S@xZpPkCD@*>( zyCh3zH1_O0S~*z{>MFIX20m+($;bp()W&K`=OgRNV>teG1fkMEirE9L zDcH-$QTkBosk!$aoaV0RhoI(>g5v;Z-lCa;$MbWVizzE$6Y`$J6xVXjRQ#ZFNTvm= zR|qykHa86aX}&1}B^fYKABR z9=-b0$i_2_bj=`K3=EDqAW#P_@wzbI2s}#}T#=;6bM(RfmGOOjsqu|V{3-D?U<@4+ z;PuHkLUa08#dYImf5nfX=wSW_>J(8$d52(8MHB(+Kd~&LFWMtX4biOcLH_{2X%F5Iefp`H$WN2`0p4003{}`xMy5SBN;7$$m2EZ;6H->f5`K3Yxln) zU@FHfz4-R4F=eL;i;_p>QCld%ecW{V)reFp79X8nLG1dMPh z?fk&5(oI*5!H}Un2PF5PEQm-L+z23d%`zn}_)~_?MOX;M@;Y`Vp@m7fU7P{m9`p*u zvl$4@AZ?(0-z{TY+{ZNMYXT$VIqh4wZjtHXfdx)UHO{u>F4#Ru0D4djSarG(#nhj~ z)x9-#$Yvv)dUUFgorqn=I$+k0mlFugj@%v(9cTou5w;~Vm%--(s$RAgz+Sxw?N=nX z5iUXR#%n(Fb_pcpu;U%58;cg)kq}Tv0OZ!Cj@67a5(YUUvmjJ=5ryMC;NrCo$Sm2i z0NcqUr6;je<9Q!({{X=t^cdel@xP2@gc1Zcmh)rof^yvc74|GBiZB->^vSQBf8doq z8%-1TTD;YnKY6L^t%61}s-LeU*U`ED5>LH+4q-P_6()T(1=v4_pDE;V&s2TZ+f!ELa2UiizZ6GswrUdKrN{ zo;rh37UZ^f9Q3V{*LAA?{a|vimcRt_d;~lCnzc&PP`c)aqT@aYb$jIlpsTXL&fHBT-PE`R& zKTbs~XhFyyK|`&siH-_RlD$U=~~4}A9FbBD@2NJ z=&K+kZ5$7Jh#_YHjO5hJ7boZ7dem|T1TaEK=cOkbvTdo%_}jwwy6yZo7BGNy;e))XamHQc3*j?HJ)g@_FWwR2IhIc+a(GDwCe<5=c-eob5jLYILP`61ZqxkkDIBgZp5W>CT9#)xW}NSUU7``O2|x`a0U-^PfP{DUOh3z zP0?JaV=mrQocdCQQn@SBp~Xmu56l1po#h1?!RC%+9IUA6vBrJPT4>5SR`uY~uny;@akiNBWitsNfVjt} zYO~>`5o<7HoEgv@9>st(JWiu_F@f5RK0#}Amp~8Q$QW_Q2S3uPGPIKyE{Edx$Nech zFY$}U{u#FgKeN1PsX!18Fm4(D0QPF-t<@ulM8KSNHR~U@?udLf`)_X7^o+&UTB_gB?xQ<1Fz{(M=%vga(9F*G4T-5?|$z|?b~agD^}=dDVyvH3{IP)O#Zk;6V0usx}#sh+8h zjhL`rzPwW2&SQt=Lz9n}G{qo0FkEsmNhawfRolSi{{Tv|$x({H6@KwKBCJMCYI1UY zY7K<4xj$TY^rxeFgz^tOpI+6OnOgoir?^sn?;U>%n_y;BjBNuP(^(0h1ar4Mj`bsB zgvsg0IL&CbEo?60qXQj%YDkfCGCBIwH=rOeIRqbLPG{fq9y8K`j$o?CaN)Dp+M;J5 zGx5;lJ?cZ2;Cg!W_o&>0rabj-eP{!o_^zg3gF!$YD%?1cy9i^*QTKNeYsus2p?ByN+@j@ML4(8R=2m4=ytJ zL>pf?|_Vl5U;r{?2n2D$Z(#(vFAx3{HxqTqmGEYzx4)siwX2<~JsXWkz_a(RU6^BqU$zN*Gm^b!wNXxtBCj-{A ztqxAsQGw{aD^=PHdsFA<8%|plok@F)x?dUkh5G?$&7gkAJ`}fhCGOhd?n92d-;c;w zuOJ2*c(dTvzA#mgQbg%=%baK_YGc z052qPcqXEfW=6?4=luTwI*x0FiZC3E=OFj0mlur8K*%1stddsLZR$%j{$r7z2cb*)Zijs^K+R_f;+1zON_guJx5)+Uo}y{IN!4o*8(1^kzuE!0%Zr7dA^^mvHC z$Eo+KUk$}ASvH-n9dswywD*f>#DV+!{VLts8*5WN%-GwEDes!IY>?Soc}SpOa@edt z66+SyD26xP^c7Ks_a)f_U&O7ju;)0zroO3cu0pB8^2FCWaj6lJlDO&3L^VUZ5=+~q zU$S;5n(XQf%OUe3ZV1jm&T1LuCk05yPJ0^5(=~V_UoEBp6Y_&n-s%v>+geaEbDCU^ zF2hnqzct9}r18xmVnHJSXV$XDw`$S^Yk|8N86%2Gb*p4M_ZCn-V1uPngqZG)P~>9< zlu41v9Cgi6)Aa~72)x^O%KQHSDwN7q@%OXVrRX)OV|L}ne=3wp8AMIg07$BfBQ8!e z&tuZ1w{7wOFg@`}-r4~_ByQuaBawlXr=Y+fj;5MOZihJOP~zGq*ANbVRr(rf#%ayD zv9#cK#W8_)Dgoejp}`=sur@%#(_1^h$I5&9Q=M?zao?I_N*CqN(DbQ{Zp4+wIYLi3 zz~q{aLQl*F063}|=B+-LbqlmZ%awxXJm#(g^8vUH`1Y-IENcdAl6d;@Q8V-l>6}pK z%wo~69mh(AT59&l&Zra>_B4VmaxK)q!=b&j*N8OO*ge zQo+Yl{uI`ty^9x5xk>e^a&EzHhc#|?48&(6)~Qb4F#z{D=C(@8#63nzPTs?suH$O_ zr;3%1;kEnonu)@%0D;DEX*i~UbT|$-b~)g8^shnqYce#5SBxAFdh^(dGbkA2(!2iv zhMOAgH#QU=1}hqgsMxq}dRfYaaol>;a;6xuJRW$dV}APV!M6tYeL|8k!?^q`c`Ymk;+m>Pyp>u zm6_O*B`gj-c|O%5ah^H#ra>%u4hReV?D0=oW0-D62M4Zt(y|K!Hr#*)Jt?mzIXyk8 z8es}F&nO51C5=Ka?XDC(yM`SO(^t!KwPUWXjBqr}q=Zsf4{GKh_>Fgb#tl15b~wod zjN-SiXJpgo1ZVd|><8PeaDD{VtTda;vwd=^U2qJCr#&mb3r3vbCna%8TG-jX@s75O zmCz+|xCS`rJ?PQAbWPgDs%g?j8Hmb^?W_L)@RI3M##TK-F~=D>%}1*}rln;KwwoGF zIe)&Y0q3<}Bp#f$Cz)LDw7B7l$!{F0#>u#I#yx8;_r@|x<}DW3$%Z3yDZ!|GQ#Hn! z;rlHu*_GZfuF87>Owv3vMTEP^JE(wu>V6m(tO6!`#as>>$jRuTum z>*-pt_!T5Xoa@&IDhXC6Zfcjrmw)(3p+Z1ai7-#Dde>iT60}kApWT8;`g+k)Z9#dK zin=b59*<@ARk>+0d7oV5u z!Kp|Y#y-DVK!gl!1Jb3klCaM&V-g9^p{9^VOA>N(Qk86Qew5Zi-8)hNBLEC0KLJWq zt^nkF(7f@-wkgar$n8u9;d6zg>T*;Zjx$dxq+>lg&_KpIaYfjyRk*e!;5W*8p5mJE z6>MX#O11{Uv<`=lQB0B|d1IdZ8mPX2Rb(urFC*5NiU0^>+uVccRtzOh0-sD`o8_v$ z^U!t8RnV@_%KrfPD;BY9j8V@+1%D>iUBrve#vqY!SESfC7wXoO;yJWMV=NbImcM+Cew~cJ!$ig-{zb6j9b!?M{(k@WAqWRY@?6A_IF=;?~D1cwY6dY-wbPR`LYlafYy(_mLnG6y5E{&b&WOV%-f za@ZuEGAd1e;yYb6+H8#N1KT80Sp|1g&PF+@br?UiKFz1BdyLo6Ml;U_Ma^YX;APW( z@%tI@^38M~g&q;nh6snlR+zxX14a%i^q0}XyGY6P^rmVPC54WkVF?>y(rplXVil~( z?_JvI*PdgOkU8s`f#xB0tDJi0nyl9jGOZ!o?@G5DhSTT|N{&(8 zo}uu@&rcr?H9L)JNc^eq0E`v+v$vY#@oQyqDqFB++;N(rY2sV>Y`?Q4oxG6Sqd<5E z*A<_6cRR?-6Oq3;B=s~#DHAC%+W!EE5mlj$_|GJND&_SpOcRwII+0Aimf|_y3xHQ^ z6O-K5W!1xDc728`dU5JQ*mU=6D;X4?dFG*M&P$B{0JWNeYpf|QdBzVFT6@$851E{F z#cq|zHLWL50T@5t2DJ36J1gaTspk&K@`P@~*0@{05=W;u+9b$aWZ}R9vf%jVqTWKo z!x!<+`^;@DL={uqWE>JI`51LNttx#w@5XalKyw;`N^^p1pt-Vw2&bOgo zJ64=p^_|9=H7Mo670D;D^sG-0Y7h?$q6YbP5J~o`7dH$%D8YW|pAuu!Ju4>^-laXx zf`1enYZkptI#-B)5a%P;0=1J;hC664%kEo+9#|dQ(y3X+6rLaQh+U@J8;{bLK+_<; z@Z?r9P7&jmbS@7d{xy`MmZMDDvbwuze$97eV`frA4wVdgg!9}>1%tgyB^sPocO5LPYD)F}2 zQ_#|CbKKh5wav6)nHMB6>?!sdZI+m`G!qvyHdvG1hi@@n895H6PCE)(+fh4QsUEBw zM;G%p{{WY8+J8!{`iys4Gav?&Z2+ zYM`e=N$FYF`n|=TtlDy0nIza4A6(S>B$Da+jmC|5z%V4iBXAgcW~j~y@w|y6ovm&P z;PfK20c^Fqi=9cdo4YoOP%;Y~0xB!5ay7e_(ja7LxM3L0SHEK?#3(T?RV;}h^(QqC zhKOmeT&M2^!)_Swnqj%2rt7xzhmqnC64`qtyD>bMR^8(zP8!!{yNfHtTc)1cOmH^D6{o)~C43evxTL?h;)@c^f%3 z9hK8xSPPR8jIiiy9>s6;?LJQr%HKOmFahjqaSg4xO9h#C1>rYxYtNK2;R&7(C=2~%BfM$&5}r~yj!v*T%)NwM`Jl@2N56TTpxt2thEb^ zguYdOFXjV0a(%14x`I2~Ng*s(%tE`dkSmOD0c%=z_tW)w!DE8KasL48uFNe+LNZO* znO(D}@P~pl3)v=}?Q^xp276YAh&(^0XkJ#Kr5`3u$7l`H-l%*m@Xw1bWxR#;g=|Ko z6&TumYp#dk@blK|S=5cmT>PMLYNcKfmE^)PY|rs6mG-mY%PmecCF4(=5y>93nl7=U zHIhYf6i~clYl6Hty>#*_MuId^wO8d_ewCYd;f)UVUpehIlhh%{>0Ij*7Of(=FR>hY zrh*fCv=|3Dz^UTDv7XWI?d6NfP8_J|RoB8E9~i_8aBuBH(FA%=oU!ge0OU-$B4^CY|f%2!3)8zwiw8QJc2NaJr7El zssol)KIM6#j{d(;N#xW@a+*jiuW^74z>b}9NzQZW&suXX(a8L0IOtdZ0IyjThUY&s z9w-Z*2jxz{IXUAvq$B_oDCGC2WGzsKoRB!|DUQ2FK0{RRi55W9QCVK|@W}ZRg>%pWrEDL^A3}^skcA$br zG5FG+cwU*ILhj*z6F>|r!8>~JYBz{)2ex`uumbYFohUGQ%bc8wEcyx9{K)?Rg5+D9 zPul~+*9yQJ?}yM5amn)JAd2}KSrjH5lg|Lx*+1}Ina-rZN}9FhyV^76=fz( zqiN?HXVSe>fe}ifR|oEoe)LPUL?n_!$o^FokX&O+mlfd=G z5gqPMPs(r!GyuB#`Q9GXAx^EQ&M*)CGhtr{*WI`O06v{_8E?I7AO}5igkruDuOl?x zA3xCU!F{igQAHKy9l=EuPzP!IIm*y{dGL&j*ZvX!dS9dG&ZQ2JSzdZf| zN+I~);TaA%@g+QGN5S}<0 z=7&2~w`0&}nr5K;5mC7$;}za=y^Q%5@lbN&wpcF*14o!*#O^XBrox=BNe<)&A6U=0DBs1 zUpCjaak@4iHtML!#4U4*x-tg^i3+@6ju6-cTqfW!z;eTy z>%+-Zs*w}-V0^#(VSm9&{C)AaQ@i+e@CQ(rNbuxr(Ov7n5jEf(A1k+XT(cu*X5q-Gv>iGWu$IE@i=?Dv6UaWa z%j^FDEp`Z)<#NP|tD!#zYdpgED@?H zj6;kR>T4OUVX(7~c{7(dBz85iqhA}lY^o4Fhg!y?xYp&%YY3x|U1u&2R_#o>()8JG zP1{DxBXwkKW}YQbyz)mvX_7f+`AOsStcw}s#{z|ip+Amj#!n>Y>q)?Dk%81w;k>{X z-#pYxR(2X!1&Rff+z3YY7-Blp1+)>|o3Q}%88((3aaIcDk3UiAN@E%6*Vxr2x&@g^ z=Gxxv0$p1xpHeEl+z?2BNeCctx#qMdi^?Q!&lO7W;ehMh6lH$Li#!TL)=?VV`+JlXzNxXA6FoQ{LgSAFpBMUA!lJ#`4msS^5;T&kgW+N2Lo zE4lb=td^*}ed00?Cz{T4X*P!x+>cB;bTeK>s~%-k=zG;y(^BKaqg9a@2vDhCQb%gn zvQRA*m=Z|hi6Ie&kw!@v0M`R|X2yFq@KKcEk3&#>p3Wr2y}F?t@mHGMw>i(HLg|B# zLyxTia*($|yNqke$@yvOsDlwEIO7#yrx?#(wHbwgKQGXcPR_$3C4(7C@_51TRh%#- zw-KLuw=8VA1Yi!h`c!PFSAaO_O0ImZs0b_?0zl#8t-YPzmq% zzxvhF&yb~uQaB#9h4A*(p}(H%cMTQ?wQb%qwm@9gQfZd9D>!^$pTeUckCq=Ohk_WXklh6U}#Wge>q8#&q^`rxC2=CgC`*ZkG zoS$>p;-5MWRx}*s;}oO?UI!gT2OUAE z!|94^aSdR4V?Ro18Q|xyaZhGe=kccP`LmpU6g>vD2G2qd6w{3GFh+kGk<@O;{3*Q- zO8sbPNsEF9LQkiqHJ2xktvn2zus-IR;B+6A4QMp=8I~U)I0W>kEPDpi*E9idGnhA1s5A*}+!=+4m+(jg!rA>AUKqq{*`7#p1%F(wTB?f3nA=j@#AJn#2& z-`9QJuj@tCAC-|K-EC_E4Vr#;ocj84-iI;Tq2Yf3Ca@+QU;_(>-Ov8W8n+t@?-qZK zkE?z&B>3ngRpzg>Nj3Y7qDHxA6LRcXHA}_LHFttOm`9!1h*=(&$igTqT0~~Nd{=Nf z$j~$RK5-G?PGPo@S)=-s$ptq`Foonc2mI>Bky*_m4RizOQ4P!wIMi-R_73`noRO;6 ze#3D~Gn5h1O-aS}DQ{2gl37%4x59|@`PG%;HT$5l%7%_Qn|)#FqEHc!XWfjjxH^`< z8QE^T76!GKDSI8ks~6utGXpn{H>b*2;~^FIN}vhM2^9Brjn|iuOsZ&G zs~!Pr25NR{$m}Qa^D7TO@sruPx++)Ftn>@{d#FF%Fp9gAcQx`#C)Jk^pDmA~JPrVk z^rqqSRR(IJm3uA|O(DKSL>1Tc2&YQ5r$KBc={dp41qXibdzMEv`G0h!w_yj#fWr`S zNER}(Kh3`U%QM}FH@qP+pd%d(zDj2JMg24+ifR{QfLKUcyT@*8j2>DSGt6bY&Die= zGO5hM(8!26ZN=^<+V-FE5FYC}z9A!cMsh+_`!10V;VJsUi1oB|l?X#d*o~K(ez0wu zx+}C0#*)*kdKy7Y6g~8O)l5$GXpqpAq@3rCM-(4-I~`e}DgT}C5>XUa-BG$J$3YoNb=8HkJa7F(f%AV{a=eECKItI(@JHdZAv(!aQvI{20m^g9s~ia z)+l3so>*n8`?J^mGT>&QU(eIhzR7Pqc}9hV3;88R8Y`|Jb@v3MJ~XL?`ALwYhNveB zzCpd#OQl%7-*_yvpTFiW>SZUHVLv2ZO_HJ>#^HhQ@wGTMrB3+3*jb1lWmC2~ny`EA z3qSQoC_N4IlV6Zj7#X^KbF_7I9b^pkerzQ5-ECWIb}6I#NBbQG%m$*L(U9Vc2#IL? z=3)`WIo~Aziopl3QRZT!2#Cm?=^a!FrQbaA%mhzA{ka14%E|NO}K+Mi>o8Y_bcTMCyP{7 zU&^O}D79QI;&9~X*_pr5$Xy8T6`+OqVyVJ`?}NIHH@(P-L?Dnohnsg3BlAXmAS(4$spywGqJb~?Y zdFiqx6*gwtxB!9Xa|pMf?eDA|X1}O3_dx7D+{7%l_l%5n6ndp@@BK)(y` zfhkGq2Z&W$Oa0?DkwnoxBEOCTgIDs0;Z2^DpKp^!R+dlBPhyaV5D@38%|Pg*ddY)Y zA5&{3gG}^rzK~hR;UkT-|tE87`w12KcGI%_R zG;G*MY))71ol4plEwuit6D&%MCO5 z&5O;JcXYh~FWL1RjRu$k6wp$ASd*bbsEdLVJb8%o(Zl2x^A)@htcf`NJsB7iQ>Rxk zDf~MBt<64JSjcCN=$?}Km4o?_0MhSskCc-gcd(%P)gY)}JP@=3aTXMz;EhRy#nU=w zYVY1xV(R(33f)g^`qQ`Yutk!#AcfiO_u35=UR$AAc#5hRic_bo;rU zBseuMIWTst^@Fv6_~$&lA-sQq^6RaAk4z!wwqZE}{CZ?}S>7 zs9W*l>km3O=R=;X=9T)$6QwoOmZGBHodJ2Cy9y z;-xR|xbMexO)th9ZzIpCsTxb&1_bz)SJ0Ivq-KsR%onC&*_Uw;g&tL?oOhL9%=>{* z22l^ylEcS&-GRvKveNc63;X(Jgpo+NYzi zH9mA!+udMZ(40~Iq;u@n^*w=v$Y_Df~Y`eeCt z03YyrLZ5=Bb#;fc{B$JN4>d+*g_fHEPl)j&B=Y$wrg7LfYbi(2pf3ygveP7m1VZ^6 z_X023AuNt?)|c!8L&G|AITc^RWRCwXgv%Z-<$Ny4u2df&+<19xR;ES1W9wV8le$dD z!1~iuVv2u_KnLEP@;=aj>}QGV+$i6{d3r@0R_Vq;$NL4)8bvQGJm9@E42VY#{q6TGgo|miOClqfpdh zakf*G8OxpTj#VVVHi4L7f}1|Fbd0gtWWYZ_OG6Htyi)-pko-38?&g8)0`LnhAcsnS zr*g9=5r=j6gryTo`|KZ{TAQRdA!0AYwub0Qh&8y9%A2{ze~B8w+4eG(l~32x)U3_o zaBZ0NWKoA!9Sd(GtVx_Ejg;EB>>G|BBu7Z+-BgRDMJ6W$%0^F!-Ebu&ID%Lb5{P5EQ zz1+Wc6cG4lC-hH8N+^7|t7gFg{15P|lc1|d#SB}rM0Wju$+C*}d@valBD6j`eva+T zxrIVfMw8iw<)P71e;c7+2~dTXoAlaXs}m#JDJrBZ5*;Kxq`CW^-J`}tQLN~@Hb!pLL4`1TL+_f1{?{QIM( z<8RR~@vjx8bSA|X@UQoNbjkW8FEnd$?0U8PM(HO&!vYP~E6khqbi76%=aagPF<;|m z(v!)!7lv=9BvMSUIcotF?%sCsa&i3ElrKfmso%Pqj>Y0t@P6cyGfzJ@@b~1(LXmYk z1e>kU&f@LW+M8OXOU59n?-=Wg7&ZQTdR_F_VRdc^J7CZ}EqSSvz5GXx9fK zv{!$=I3n~^p~O`3MSYwwtL`!bU4NH)wg&44=e{+|OQ(H{+K$9Y^9J~3ef7ZN+aDzX zJ~k82UU>4opQ@c%K0OTh;@JI~j*)J+ z|KCyv&8GrKe($$=aviQhSvJg|9X zTx{BcW}KXqfjLlF+kfoHSMnvfshoN0w2D@lrFoyk;T$=QI1W=a^b*mw=w+3{gqSP@(B`77h`YQNJ%jV4Ovo6=9;`27T3>}2sI8&X({ znB+;n_pt?DGaYKb^G)+YQ)^B(bT!)P0IPd$(?)O#Z^J6R1 zI}d-8J}dtFq!7M8{>yzGAypl25h7>KYe#b|S^>uLHK`5EsziB_8sQ5E`SJ+2c;V9% zuq=$#sZzKmafgsuoG0x0ALtD;T*{GMSGVnVouqobe%&=(WX+FQEv1|v`=YF=a-O=Y0AH-3zT(2nxUKon4pSdV2crUhuzf2)DAH-NJVY! zPo}1~#pCD8Z)=e>_|tACu-d7TBW7X#Pw$1ipztBJG}GlMGggpzoVrS*!mD^2nbwnC z&y5~H>b^k}-06Z9k$j2GjV{}v?1zlEyjSThD=t1{9{at!9TZS;SkZl)u>337OymuO z6>P3B)`@J5J+G@dEd$puh>g$n()o^BJV@yXyw-ZV>atE#jF(0wwc*e3vDG8ZT_U;D zg12>(C^dZud$XOIat<+pjS#v6{{U1y!q#Zxt>FT?S`4s8tNdxeumHC`foV9q3DZd6 zwC*1}HJIXDYe(y^me}?+4Zs<8m{E0X#WUyaTX3gq6hZZPTx%@TR7nsw_^;Ug@3TPC zzwigrb|i2p6z3n{!+LTY2L+OThowBa=RdTU%kK^<@iQ^+>ZkO82zH6o1h%E*q2K5L zJ037?ymE$J*J869ng7RaxhdB6Xp7M1D8dfwh_x4o1qi*ZHIc#P zOyoqs_CzSzp&8wov{ZM=Px(Ze{laJMicQuwVLUMMX zcs=>ak=D0w&oXy;hQ}E1%IALq?L^jIS>9LV8uk1{zpQe2-of|9ezuwFG=Tbv)@ayf z{yQFoI&-b{s_=m3T!KK*j>T^VKJ0^4r<^|(^4<737<_HEC|Fp4)GCQK7PoT zd`bk6Q`S1}Ht?CEg?3s{jz*7NMNP(PHgxOD7UzQ`I?H1Dfg0^JK;zG+jHEwHRV=M= zbZvb*0b-V5f|Er?v#KdL#i1weUSjl+6ISU;&1qu=lU_~&MKOPFm|@5hbo`x; z2CP$1LD8IV?!#>6JTuP>tr&hdS~6WyXjY~r`jngH(mM{>0!3DeHsi?@DGxg4-~gcA%FM(E03RdpD}=71qaJ09ld zB&^Y*FW%zpDG{;V85*dYdisx49%={RFdv`mYx8qY@JaT1JrCdR!y)fxJp#x4y;>a!+?T|z?lTI*bb)OVe z7<7dyB$y_`JJu|Owy49IO8R60>B)ia%xuVGBWlsNNwQbd4t#t}w+sG!iuqqle|ZwT z%lwhzPN*;=Sp%>hna@!_pxRZ};Hh^rb9gm2I(!mjfOF(^oUSy2pjt|SUd{9fP#JQQ zM2c+LY|GNQdf?^t@Eqe;yv>tD~_#; zv8Gz9Z`2_)7}o5pXFWoTU0uoM&Z*XU>b-rlaER2ksnkPdz1P@?$R_a#X_9ELmj=^c zQL#~snC6l46)aVUm~1QVx`PAULv4uyFfxzS0ZA>QojDy1#;e1}PILpE(`;}EMPJ}) zyV@5PMjeSNo$IRh!(L5`Q>ZH1P2|5Y_z~WVA#iPx=q?cyH6hi7$t~#oHqP>8LQRn> z)BM3ANh@)vs z4F5g<%m>X?Qo@a$3l)-gbAnV+;T?%zy|JbAK)#@^;1J7K|GU;pDhzmeL(Jo8nvU1X z@?eOpuE=o5VCbLU!bA_R9WNzxBY+Z}1W5G>Y-r<%0sxC)!99=O8b{K9jzPP91ftRP z3ogp?)>hRZ9(AG#eM083^JHlc=yq} z2J0-r2NnG$8Vwm@U7d&;CwvX(lrw1R-%q(EuX~&nVd~gD`eTzH?%jiJUOf9NHuYxV zU%SRjXh?8Ngz>W0)Sr3e{D-q|f%*`x)M~rD`<}ENUPx~yP2Tx;Cvb7Clj49qnou3z z=zZr0e9Gq!^z7P9%Usp(chdVtB-LoXJ$Nl%&uH438d&7r^eH$R^P1y$DmL*IM!ruE z!^USi*M!Vn_+De97^1i$h~yVc)i>|L@R390gy3mrH-H^e5!WzT=l`hdM zfAM6Gx&?3~b4iQ<`$f2ATx%+hbBGaxR?=Y1@2YxF?o@YC2@K!g-|>AylefXm?E~xv ziHCX*LvXE)%K}z{9sioLVWenU#~VdDj1Ddyrx#7#!y=Yn)_pIc`{g{ zS=3LvoB->9CuO`4CXP|2w3ysN zGmoyX1(v-N7gJ~E^Nb>CQ3Mb2Q4MBx%*-R;xCETM)5;QKJ`ZNR z*+u8^!!S|ZieojYXn=^yjHWcdESL8W6LaqgY35$p(tK_jT&%nIDbcYGis%Q6M;`4= zpx$R&xXg(otAgk0`}SbczV)KZ>3?Y-n9^W+ah4szx?U?r!;>*Q=K0&^Jnf_G3RR75XT(g$$M4qc zljMbJj%oda-XNcUe_l+?Tk~I+>NkKaFIHF2|9)(+bUXeqbzYQho5&9LO>8Cbk+G9G zcnN2(eo4$@=S&Zmm8qPIJqSVsJH6O4op-G(Zj5-KeO56={!!^ooFWphawJr<+jZ%3 zqx;8nw7QX*`tLqliW^TktYi67^9ZVL(c(WNZ&DDl_DFuKL2VfS)hODliaKdWSvwHl zFVH=C@?rM#kCtT*u3p(;k{XeO9s*9f2MRP~ThJ;v{edQw!U`1cKbQF~VNRTo%LkAG zdj4%eeF`n%$MNM(9HRi~Xs3=`<+T4WB8Yb@vLPZEe=3W3lL+lHp{{hIYdpVvW z&LD;0_sM3z!CF-y=S*DlT-0R=O}?m2aM$vmhsS86T1UvQ=GI<85xVRmm8TLfmvcgj|47}E* z%+hk?3m!;aph28T9BlJIn?D4npO7YBuG@V?sCY&m6o!6CCP9--JR{oAbFOq(JBJ_W z&|mJP{aJ@Oh?*EWEFL)HHs!>ykluxXyGoei@zZW|>lia5$f*SA?v-Psnh+`rIx1;S zl#V}m^bgSOan@_2cs(34qlmx!KzOXEhz4J-wnm(R&7U_zWs1_hTacD}TDgAg5fkHg zVpaQHDh?~CV0j{Uafzh%Izd$ScOG>2-Lo|doL$H%)hDLg(4uK2OmFQ0*kgP2T&TuP z%E<{NY@vqlrBh+~56~fwUCr4oXc@h{|u_urThNEMOSyT53ee3w5o5Bjb+Tz(G1kd3j760l4rJoi>wL4hW5Wa zgCel+W8rvEbfr{(yk#l~p(gl+WK9Vifz6jQ#wI*44)?HhAvaXj%r~ZOQ z&XWnGE=)SoCpdJ_iXMGz52K=ccVPTP>uW3ZrgJ~#Ho4PveQ6gfT!b8bJj@}rAS9{9 zxQJn|Ge0z|-N+Pc+H_ifb|By1*`|tDPp=Cfi3^eUXWZ`e?}T!OKf`ELVTpumwNcmT z$gy8q0k)vo)Ju9>{U*;>ES;T>j23I$k!adGbN@zE47x9L+bMZ9euRT&Zb7%L$C}1J zXZ1szDgE$D#J=wyHgwf$I#=rvrEijKeLm1Od5s9QjJ`$??xQNvW7`l?5C=H~?N;sd zg1)*kM5pWvysLo=nC*P)MPbyt$dQuUTuK#8b)LioAPTyw49UFB32C4lm}L0ypbH?chYFL;jCMVHOU;;nO~3G~?VG->Bc zARrH?SO7O}ZZFBXLw!S1WK2y?n$`)Ot`Wnd;yv{}p#o66 zA^uCb$0a{Xi$YDHsUYp#Y&wuScN6uF7 zP%p$cHWTsT+qNGC8(yMF8ou6^MMO2P< z@_{C|g1OUH4whSQ7|YC6$F=0AtxRSrZPKr6)>>Lw1D(Ck1_(S!t_$sY_J|f205dwl z$fbNyy4L+1V>_}0oS8k)yYPtaC<#w*{TX={*Ii@0N>o)WP3DFyXLiXmu>~%_k_|ej z_mw0YxvR|ekmXsF6K?P(MAK+}xt{HAUci|{F}oCyIjsTDVNCfRnFlVGtZv((ot7=t zQy=Zy%db+y3HiF#R{!A}0BYcMT}f=SEPv};meIH2wUDk7aZ9o0bt7OX@+u){9vX-$ z<&#g=iHJ8PIP1k*b~s!ri6;xw-BuS{R`|R3oq8fdjp+RC0hJd_1TB&(g2;j%?eFO_ zIx^uEPSt7DPrum{R}FT$a(f_~tx=@JJdx}0@4q7l9{*ManqgoY^rSQ%vuuHW^;Kh# zKU932nAb#s6$-a`!2-GCY2Se7I9lC?6wnL*G8(ZkVxn^Tlahc_ORCXNTm`N|lJxq94Z?Cz_V-VuK*qxOID?!|nUh9;uX5fW$w6ioy4_sC%*AWuV4j%#=$vZM#!6mE51~Yx6j=v8QGPF0I}E6U(T}C zo-&511xCM7qx@|FXzQz#viJS=I01U0Tl)GPq9BEy!cni6?g?No>%%l^$7NVzK8s7d z>;z98T4yQCU3mRtl7!20a>N189zSsQ_#xrlZC)H53>#!DAE!n%N~+sV7v>5G!>-(7 zldQS${J{GPASO{2)}FAri0WuKZW#4f$!~gM%X!9`SsiY)AUzI^#oBf};ILc>VPj23 zY1?ZrtZ3@wgp+!k5SlYeIjT*Zze`5fKjvx~9b@lPme}oW)8(bQR19zaGUg0+ zyAMz-$jH{#8U}o%pT=FqVhD+wr#I!gay8f}b)2@#OR}%TRw9fx{EF>7!be=2Dg9;7 zu~>`pMm8J8#ov0A|H>oljBTN)jBlg)=5NE0M-iK+YaNZ9}yp!>gX~v z9~-+u#Yqv

    (B)JbwpU)^pKBEP8u>cVs$BS3{js)+P5v}j*J-A- z9oO;-$KxD1H$wlo+t+gM5yPe20FN6NUgwYuZ1ouivA1TC#!yopO1dQk))nz=lKn&` z#grVW=Nso|N^?54{Yt1H_>oHgU_8SWIOoFb84k4abd~*DplL(`98`Tq2r@Wx+ zQ{q_bgY({n`pDFGrF?ZOs1xK0n|i z_D{VKa>I&GmwNd89rtyX>>6^718RJn$g&OptP{WRAfHOs?)goHp^wLMa+&KfGRdgq zhrz|SgK1!8#BNkyvT=sXUc`}iGkA~{=kt5)PPK$3(MY%4$UneOBx3oL8WA&cn^}pt z)LrsEr}BqvKeT%7lR0UPEfLqb@N&CfjNyU!A_#ndh__QO_(Sd5z56bS(CyHjg#1pO z; zi&Hgb!}Svug+T588vokmyHC5bc}*pIKM?(Ydw3UqxWT$&y|3R3l-e-!dBL}^U&=W! zv(|_T!b(FCihQhXoC*~-jI(959vkw zb@FG34d-fWKWCFC7>w8tW1*s}ok^4W&v!?0=TVPIx!^F61p!=1N2MUPu#1`wRBHv> zoI~~WD&+?++y)iin$PZZd>oe7mX!V|kL5!s7op;d=Pr8F^MB=^8<*3P@|B|ZDaDF$eXH#eT z%V{5ooxk+~yVzm%aX`YDUTYeKNltb}YR9ZK2PA3alT_ZCGD4;M-z}ARE!x5H*pbq$ zH5InwkwSb=s@|3}rwHmh_t-9=I~;rTmaxc1PYS9#zR}n-QbeU%-bVL#*W-K^ zq+6JGG6i}#$c^?0OJdfK;QgwBKm3wQwAGL`#_=>4zBNKN+Y)HfKSM3)F<4bEiSsT@ zEIr-o+O6aj2ubq~@Rp&0hoVfWp$aF%-JB=6>}jwn6YmqHB6Z8s6q~qd(WSo0IxvzC z%e!-=u)`$EZSPLH?S79}fJNxGu?x2W*Z%W8@Xi}JUNO? zkx3)T{XK(-AIcBUekK|I4O#h|w1Zc$)bj~@dL4U({{d{;q*TI9P8ESjwuC`X^r9Uz z@TEC_Q0phZ3t3`$ySq0>)*L<}QExXudS>qqW*uoPC7rFE6GmSkZ!)UGk>$fwYn;8D zz)E>Xpf@S*zIpvb+sJ}A`_e1r;Ag{XUVc9&< z44DFtpNQJ^9j(w`io}GFq16UB5qj1nXuSyUJm(&1 z4Cubb_P(S?5W}|!X5QW+K#T9cLRb}4i!w^5Z@x*h7(d2vQx|e-PM0Qf@ zvp&q--o#+EDT|(ku+D>dVeDMBcXbm(ma2co0MJV6O2Lgwi>)Eus4emke(Ymof}t)1 zYokL;(P^8&)kyutXU=H$g_sMeB)Z%pNO5F#<$`)wL- zNud}2)pOoghhk3{X~+U6!ayXthZD&+DC9SwRTeT6n7G=B*j0IH z4x0Od9#)Y75xk?@=IhUa2{%rP?cil;)?JrLvKAej1342*dk$MQ!Mq+q1qXAQ8+kXB zja@o?)rUY+rfQrBJDH;=4NOGz$6DZ8i>E z6GSyEBvlf9gK$c#(W)2qe-~e|XZJ`6Ww~Cp61b|h5`Afm*ejzdn)-Te(ry5GUOoe9 zI9UKICTjH>346_=`=ha*2td2BW8pXwrxVZf7W#5$BS;;P$5f7$yS z7o^C6bV)m$VF5L0xX#K?o`y|a6x=4i)l(la<>J8d}Z8vMXE;@#z`U5G} zL34Hji`v(h1CWtt${XuOf`#m%KzxTDz|Y*^o~Y87A6$2WcfR0?91wt>1Xl2PGgX^$ z%hk^03wzh2pXRs1o@8Edel~MW%lSpeurR@MVHQyA`|=}RKmbNNn81T(gj+NOV1)zC zd%HL2hyy0SuK1)~&eLk;>7^}*=Vw2IoopI*YOCI%h>(<7B}WvXpa6I>*^r%gq!3-q z+c%7BdJ zj%p4Bo(dtH^wIZ-IAC;OND+J97dShkX@H%<%cSvBds}!XPWao9Ss>9qdJ47$K%c)^ zPWn=ZCL0#4T0J5oM78}&^|dsmyq)PDTF0&h-6;j?W`F8zmw1 zO?PfF9efcbZ4a~N4&E9$PjW71I}t;Zg&pv^7baNx8-8=!vJk}9jT|hAbVWDU0#LZ!(%VG+S7-^WW|J>A*ZkN1A0R3QNn=gN6nFZ368VGp z*9$0DDOv;3s*YBW{!-^1^tY~H@ZotkQfOU=&>$$&*cRUh&y9I6*C6ecXJU<~_0Qm; zA6pn5NQ88vE6{<$7X(jd&A90&z6F;g*S^Q*Rb3ZQVQKDgMXW~lX|va%0IRmT`PeP; zQU{1?NP0bs@z~ZRk8oO4WW|VK%h~>HunJ#vU7Z~VFVbkK)7$kOE!$;^1uo#Ps!R>= z3-FiQ$v!gr(xL}>K7h*CrP<&e?s2{ra9O&wRf^|{ON@3Y#5;a%Sp11n-=z#(;N}qw zD?sxS#e+d^Ph8|p?%od79GZyCJAu0NsWnXQ#+COKGqytJLa_>oHCjlz7ZU4x(@L^U z^~FjiIP`5e<;TcD<2B2vk{UIaBC^WO9rx6yqvdPJSPJdK8V{kftov=I7z&h46Y}SW3p~iK|M~O^!g~KyO;D4hh zpwBL{Gdf=Z3DUMtp?am?-c#l;7DDaWgF%d3JNZ;*O5d;>a^+mYD@1wTWj36$cn2Vx z(G>BPEzK8Hl}JAf;}1>=C|`dj$=x9G`*Fezr#)bKZ{qACE=?EzBSaGd?Gj85<0Ret znpQq6otnql`OPqSa(ORrfq0lwRoCpcCUXnxdJFzL$2wZmV@B=~p*|(r!(?;xa!6uQ zbwcocQbY#MR>Qk9l+G`0&JR!a6vIEwO6MG5zoJG1=Z7o{xG+# zCaLtT5qXSN>b1LlO|YpoPEy+(%=?|PX_h($mnoA0$jS3YcZhc~pXxU8M1CRA>k|if5?l(%59e}aP_4&?5F+MuHKY0s z7%tiVB3rWBfLO8Nzo|gkkc+=;{@tmMC|4%l?>{L?cf$)Hk{rcuR@TxxC?SvllOUjoLv;`DrIH6$x{Y@=skt)NxqkeT<2J1h+}Mg zB{e6IM(|n;lEHla_zT?^5^{~1ZRJj8CXND4qPC|D>3)UqoRY1>$-w0mhI6d{-(ZP-xq)t=R=e~_0&V)V0FH3#i$2_=#;9EStAl67;WSfgv5Qgrn`nT-peL&x4HAjy;WxV$cf znt%nFg1CNu_;=p5^$c&mgzgT8?yiyBFN~WFdg8j5q&aQxoIeRB_kJZ7#iV%6%0}B- zFkC9uX*0n*Q&j5j={}GWz|qY6D2$EGx0(Uv#!o;3ub1L&ZU~`QG?2mTwr*~E_lTJ` zEf6OT;LM;bF2jtMer{~&jFK!>{yq*O4NvT~eaH~9Ad=zU6G{oB)k)N_sAMy+DU-W> zo07rAPGw=x9u%iaxvjf3gLkO^ePtB*rF4dgjYSn9) z`XLIgK=hJ>zB$+MI}L^E_$P;*4jrB6!-MedU{6k`Xo`jKH*Fnl>CqXn3ZB|JMP8F# z8c;WOi~w32DnbVS53!1*KM%iJ50-Vd?N$%@joVM1gkmCk+}s6%IziTKP%cPde?e>x z!E&^SyyOjJT@nkesUo`eLJ)5o7;x6RY|LD@YY2Nb(Kw9nY7GD^@` z6`p_;y^L7zw{AQ?<%B|rR?y};i{8V?=h)~>WvxU_IcB`#0Q;18Kl0_4n?hDODX@A9 ze4E8zy3dKEtp&LhS;JIkx5hyxSj3%hd&(B<1i7T*y3o4R-y^DvcHbM1q*1r;gUbSpq&Ze) zsw9FqpJHV1bFtkXmJmjYWbAh%Mqdt~9ypP@2|oAA7kjTib^eM&g^CNj=d$s~k0^%( zF5MS~+Wt|qypOv|Ud@7jDzbO0MEecq=(LvnIXd4ho%&rnpvCj+=fU*5w7}m&tbLo$ z?mHA&juf9Zq7PvOfDMNS86;s0IR{5JHzVP+t}wSCb-R*lh=DkjRI1;dI#_f0AHd!% zlI3~y{403D7Y>2+{$HC@gj&sC6I!K}t_ae^d-MMuo0tE_yHK#-uH8S4;T&#Rd=NRb zqQc5Xik824r&GZ*UUZQ}VIc?}jy9=y#3jmk@#-HSa*O?e!him3wWjB(|A57!GtVo? zMBL%a2vwqD!Kwm%ZN$@K2mt$MV6RVmH#*GAZX{(4l1Qs#NuD~ma+JI!D>E#(Y}BSI z>TM|+CG1igPO?`5I$Ho+kb9NNP zz&7pZ57dTg652_FpeAH@N}>HIWbz1*3j18KBRNI}I6@;`o4bv%wh3KE{$u+7!73vC?|%|` z>GC7z@^i~AHA;`%kHqT8-#!Z~u*oxtco;*mxb;K6STlv%?Y3VKX2ILLRJIjfyiHs) zy8WUxa=%N%NHn`#4wgrnTln|<13+iuF=}pputdCpCBNIDsV4FiaSMsZnpHtGvh3CS zA~82x&B=oW>yak&VEb(D+yJ2f*6Kg7gf2sc6)0Qfz@N2H&}sW zV98d*0e8y9l;^_FnUCUvTUX}%U&(Ps6a@E@!#AIxk%(iXc`!AMYL(k0iQU@ltJ9`H zq)CL4k$B7MhP$b<3%p`l@W|!y68|TMfrzRRPECUij}^G0kiVb)Z>(^zT0h8OOry{e z4$}@XUri->2VMtMy+h$6)2;UX0(`u$g8Dh4zjzy9bxzUN-1-~5g85g|o{KxKoDP2f# z4k`rHpYkGfQO!t^QxhoFnluVKOO28oyP}E-Opb1jgjIjam)6Rym)lKL<8gmEV|M7Q zD5HcG`j%rM_7m+bBW|Uj>QcEP4m-?j*M%}w=%2C^lU($2FDEU~0f^?084CT(0BM56 zgQ>UOh`~aLsq50L=+&2*duN|C%z3~i;dx1p9BXvBZ%~l%9)|lONh(UqwTHPL`B|yc zk3nCZWV?W+x$ofGh2j}}1-U7~fe9kFCakfFa#4(pDnfXc74H=G>vcatW)$M)Uh$gxLGih`jvJf)PW5Hw>ha z+F}E1<-+o_=yUyNMi@4Mhy;Nr+s~7ONbqEVG@X5otm-UwA@G5y)aOZUftakL{A{)9$3=#@AE>KG1$?EhGb|=W&N86M_wV8$B`wl96cp+1 zn2M6(Bow3-B&BP>V1zUb5CKJCAT0vYDWgNAyJJYlhQwfFzvuoxuX!_e-@C4Fobx#s zJ!A&6bwkt(I`5h6@it?3~LkL#=fEMGHQ1HRg5?T zlecvYzSr35v-O8+`EfIM>)RHa@bgd+++L^vaW*S68u?_RU92&NF;cALi+z@xLPzR( z91VtZ#8aKZcsxe3wd^;+0UL8TiI>8uKE5s{io^6V#kFgP;6DGLfFziB!OmZI{-151 zyIF<=K)<+IM^t9N!8;!lm7d}i37nHK{dZnv`oU$mmOgb2`nzMpUg|#_N1ELtb0YZ6 zPH&XLJ52-;5S&m+kAG^r*0e8MBIt7Wy6mRS)T};M#3N=!1gOLkcEEHniwz}TJd%vS zF)e8ye+evJl1=K_93WpK#?J;eUv>%id-+L$-B6rPrpW4Fvj{mPlMCB|duypv>YR&; z_;I>2;$%zCM&i+A3^?8MKN9wK4(Je(`T}dyfntY|VheiO?_(5!mfjgqb?`xLdKKYL zA4R|Z5U)6_MK)>L${@bA&2||LqmeiP#%_4bsDIV{IPB0fOx=aI9?c*hvcil1J=*n- zr6~${agFo8n&Tkryc)C7pgM4L;j?V-Mri-vC(|gB1lQ*=je81_22&-IIpNZJ!tcyXvl$w0yrWSj}`)HkoDPcnX_96#_j z5YYnUAI*i9)%|abg!#Q)J_uBC*W!#HktNz8PD^}l$&Olo`fzKmAL2sd|Hn3MQBDS( zovUi(&beSgnI3Rk*K+$8iMo;{HVm5o#VgSVQ8z7~{F&(cVRN&=K$M~rqXXDxCpvhf z4tF03CT^Y;!bnG-`St60JiOs8J&|(5Czqr+F_+3~R_?EW+WBdjrX51z#3|{#iB77d z5~BAk+VXAeOUv~bjp@f4z=9{@#JoK8518(VQac^J+lVyW#M*pOJnB~MF9V@%N>CVU zMQP%o21&*(4Ubqi4zVDI8}On~>%9?JL+2i_mYT9h9bNIcD+`X3i`UCQth4>g_?0b> z)f}_lgrw48qfU2^As^OmbT1{EHU$<%(U{pY?+)wyEOp!5UP7evKu2`@g12%L=R09x zD$EaQDhK9x8THD`IH#lO5u{J7Kz zdSoO}?U_G7VjmVX_4Lc#K7!Y5bML*%-Me(7(=AAd3&i{2hgEVW4^wJ(kn#LW(=CD7 z-054MH6($`hke5C`cFuIf3zs6F!7hkX?`qYC}W`v*wjBNlfFL^n!S>Oetf4=>Gq$8 zZ#p~F%QqniY%HLBhy$wF&tH+FlsZEgvsF@an?$-W8H99VqY;zT$X_Ga5rMMPn*NXF6*HwNGb%ne? zI&v$h(v-v%dTlI!OLAguWG(hv~yGX~lO z<|rW`hd!&`hd+L55OMPe%KCgNqVsXCx`BS+9cYh>mTH6(?}W$$^3zL*4Ls=m8&+C- z<~`N17*6ZU9r_0^lW6R!Sib1L*+ug6ZRXZ9|CIcDrrfDqmJN&l`t#wicGv_3`E6CM zOi#hoo2mY(VJZ$;Ysbo?>c1XndB}ZK@!g2H`KsG7_gi|0%Wc!SD-%Vgn3qFo8QZMl zZO>9E-h#Ohc1`H;@a!@2F_q6!q~y5_LDO@Gf?r=u+)1PaFR~?eFs!&>O>A#sMWhDR zqnvk*YjCYk61@QQtdg#Snyr-bwJ4otJ)7?f{RP3%Sr3*!g40Oa)W1)F2hreaV#kfp z1@JWWy^N(FE09ZFJ?O?y(cg4FC#R}lZRjo>+Z%4nzx9{!I&gs=UeL+-YJA#pd&y&e zvH3Fnp==!{=!JCI&7_|RkqDBKhz_)W$Uhw&cN3ah%;RwBhYSohBxuh5yG5c+YY#TE zlsXPi3s)5W>Xf~EN^`@WZ{yPd6|d5Mu82bD+O;z+oj0ntSNF-chj}-k$620q+&+pP zU0Gp=Lu3aoPc6@PACd_A?CW$ec+JLpuOYuyofJBZ@;*xt_Cu>+cZ_uyc0i?k?Rk)R>RVtv>dn5}1`Ag$DFo zc_hmx&w;Hb4%bcKw_L{e#NE9sN-bXN^N#;V0v(_Rn=e(*XsCwvtO7amYaTHGasRv; zy`ST{VH#Wii{V%|m5nwycs6N6CT*a91Xy9z9>0z`e}nsl={~o@aaR(Qe@X?9xWtxBm6nq&7di<1UzwU|OtS=mcVv(_gfqnb+yey4pII z`l#dS$;vKzWDxWtMm$V5Qr~~B6(ktCilVGXCfPJ3cP&cN><^c?THnSQZZNVz-PcW3 zk54ZJas>GO=15UOa8h`$q?n9hiOFT=`qaEqA zHcl?p}I!ysz*v}3!l>8HCb}nrKi$_=E z6+`UIg7p`?+TQt3`Cnxg&?3dXClABxbqBi6HX0%=3)03;l0^HmF1nHP%&)HV4&^^J zv`(R%(_5w$q$I8arv<8|c+P3lBImLW;qN`zVefGIcuU}h2T`!@46xRBeBUt!$i2j=B;(qR1AVntb!--j2YpH4NyZ<-_ z&I|}EkO)3yO#0MlF%GGZIV{ff@^K&Bg3ze^&PFI3U&hZn{csfj4M^1y;gv36%nN;F z{3_$|c4Q9_Np#!kYOl*-o?^McB|1;tu#XY>;hoQj0x$)vOyjvb3NN>NuGsYcmf#eRJztL}oaP@AcYh`w zD@z1`i|`Na0!yv9%5F5@SIV+`V?zVt`Ao+e%{rX~7YoCsy6efERp2L72a1IGRLnUA zS#nkUS}6+laY?^vs zd}$G^Ik+$f<=i5X zhE@;p@NgKJ({$b@N9Q(G4(S3r%7j&nAtMD2*@d_Wf`qrz>@~8Rf8*))^wTzJG*Kt6 zdlYG7zb|%}_xHmi0IT$NB>#-YYLzQ!9&6Ph7=2sreC41d@&wI!Y&~42TY_`!GM|mD zb7LD>+34?Pf6Q82|K##b_=*zoh0On!zM?^9%jUoPBSU9GIj0EprBLnjj|L8``k)2P zu9m_Ytqwi0dmo&usMTKw=zeUEZmwFmExD))d8_+RnchIkY$>9>7ys%o>;cCWY0&K(lU3Q zs<*#pl>a#;w;BV+=-VWL>#M>D77EQ}1Vaq(YQjXAB-4n!2IX~hBhLIs2 z`yE?nVq(s!bY7hMjq;{@mR3W!+v->_AdNwb4r;mI|T> zuJ}I^vrNt0iPi3_%EAiQ&1$O?G5y38uYY`B%f|!~ThlH=l9YD7NE#0kBwgJk39LfB zvntCq`X#HxKMk)Gy>#+F{M7&6W^`xtd)no!G@il2b>#*Ign-+ryV_VR8?Rj?IK(`P z`V92)AN)7t4*DQg`ST%76_DiBdd7oAphFB!BDfVnqXF>3RdV<%j)OME{NZ4^mWR#G zl#l61?i&vtTOeyz`z1fv)FHl9!n|<6w=t~vGUG~Iwl~9k=Chl;zO$`mH}4g7h?qHFLrwPh-Tw((av~H#Sf`GTLqBfh?++d7p@MjNdOn2jw)~^@ilu}e!Dz%#%Pg=%K1MMP8h{?7T6I?x9`tKq4L(1cz2fYT%2>|d~q~ZEZAFXY$!W5y=?l9u+==D zGgfsipvQn6n3oJ;xYsf}@n4g`onVl!xW{@%bXa7m1!L`~CmE&ZDb|$PT6LRQbK%&`$5V?h{z)QvUDBNovk_;i%x+dg(aF61 z>xKtoV+_G!9mG1izRKZ59k6vBf1)NM(}W)%e|`PkcXUB>#N35x z2;7b2>n(O3zauzY!Xo`OY_&&YoTof>GQKKK{K=x*ZuLPUJF}IQe&Br{(M+MGiKore zv4VEUPySUobxe3m;uxPKZW7c5UQih7I$ncwjjoSUIvY6|T59^iDe7e90wm_|a5P)1 zFq^eYT^FhW)(6~uwqMxVK6YcMM{kSY2^1Ji&;7Y=pmFMgowEv5##hu2+IT;hW~w*{ zc~$*PfYR)1ZuqB7tRb!0#@n%s{uk7{*76wiIXPgDvWYbpO2|a7_WVb3YX#@BJzAIC z!zU8xww01^+^z=u{W5uIGV8pIN3Jl6KR0h$Sl6lF%~w=6ZBTh5kF@%t`D*GPNx#s< zH|n`?WKchjEZrb(jb-c zz9wDu;PxzwHz_A!=V!0+EU)yEZ_z7q2C#|;C(vTkVUZhQxJl!7)r5)kn`|Lp{kg6inKyCp#fXYUnkt*gbJVH&_-J9P(Eq!}g zJ6fdo<%KMAB)rDfvx(PhipDH~n$#Vomxy|+R3@?aSG}e-~ug{;?;1q?W$|E^ms-bcgGYup8B6C$OifpLl!{gyb(n zpoTTfh<>u7kmuUOqrfJ-70WVifw_TtSpVkl_2=7Z}^Vr zl3Ew6soR4G^nB_J+yx!jH|!&$-&5L6oGRs5i&EO32xDq~UvqlVM`7xez$2`d>tif% z8&t*{$1{yP)t6p>iM{Rmv#oD?(DfD=+D3-jagveAQEdEVMx-h-OB505E6D3$(j-`AyiUYA`)X6>|1g7nId0_cVLKx8khZnJ~^-Si1VMq6ovp}p1+F- zuPLwi0z*u#n~0WPu9uAvaa_k3tR7wy0Xx=$xXi^ z#XQ%7N4B$wj}nX<9@>pPEVwew&m?#&VUaYdEXIE+#e7DUiZ8gmsJp%0j);{r zmf&v;x>zv-Ncg-bkoRKjfSbmJpw^AKA$}YA=p< zkjLw37$w8&#e5*V`=!&UyNe=6B(Q6{F;Ey`WanWxhBuM76*4Fh@NlPp2@Y)Jla634 zGSi*D70u7%sq=jzWHRwxvX@bOywD^W_vcwP%2B@Q>SFP>+y|2q+aam_fy}zO-G9t` z$ES0#n8Hiv!PD{?;(PYHCvwX*W}U{X3?<3GYb&d?^amC)u}oKI!QA@Zm5}=j%%fg^ zD>SB=Sj}?!D^#u-zc7&zE!X-&)apyoZh7cg<*KM4h2V=<;`eWCt7S1T3NjrV3ViHL zE*2h6_ZZ4H;`nQ;yJl2&PoHeE^r95|k1RJLJ0`;C^vB=(`gXHIiKG(|k9G}y|Jze{ z<~)&MWJ#0YrbY4JX2ebAzY^ly^mdVMN=3zE@`OZX$U%-xT(+f3p6rz2~Wf zY2NWn80FtuWY?EFcf4kIpK}QCpBDlQjS_Q!>&G$b{(!vd&yrn*DtG$}=ZZQyrW9@Y z&o|LNzE{RN>ivy!=K>s*X%xYJ+bpEq3^E~LmDm)Hr0yaEe#^fh(9(N`oc-i}5-0QxfW8`V1@49V?wB8~>VH&MHEB+^&p5ArlWl)J=1Dyag&a&vG~-_b(5KrdmtfIm zahOUj{nz!?2oFKake`Laf|ozq>=hd{P7URpe=Oy^(xXI4dn#@F$3!DR{0h%|b zMA4cpXiBdJe*3lg>co;nMe4RJ=_>2NzIw%URB=JcQonl6a<(a(k9G&t{^3sd80+13 zW2|Fel4K7Oqs<61a4WRZ$X`d1TSD!6RLt}LiC3aO0j>qm`V#BlsdANnav#8v)x`7d zf^Nffga4Z0;Opw?n~CtaiBA6qR}JHUv*}4)3)ezbf}3v^bFeC3OfH5)q?V{ITjtJs*XgG)dH;esp$6H~~Wj77(knq;PsVj%pw!@HU z#RGudNCmU{h;;|X5IZ329ESPnLbx(Sy?+R2Dv5W%W&GY=Cj#^Y3uk5v-709N*2Jl3 zYiYbix1dCtKw%Eq;bTfEpVa5#XUyhvd!e$h0Gv8r3HGWLYhAS#Vqt;pX=gF3tRSeq zon^PvLuh$5n|`BO>fW_3!c4xzy+NZWw6O{6Fg71&qOw`NMZ&&Q!`Io9F3J?Yl{{A4 zKyt2)cE-GVpcIHF*kdb>Qb7xK5PF=Q&SX)F`(3uMVg7CWpZ>cw@VDafZIY$1Ae=#o zjp~jyHEz9h4(IacM9j@0UNs|1okdq5Mb#lKNP$fiMNb?--l&sgf4XselfXCrUcO9d z^gODl^fhPPh|gIi@a{EwbRc869MXR5UvPESPoMEV?o<70!1a0~!c{c+?wZa{;r-bo zXvcY#C8k4q|C}s<@T)fhyvn+1^RuC~*cnt?+|1A#@{WaUqd)N#q~U<<&GZqYD&E^w z*}7Zrs&>Z!tO+PbgCEG}L!z{2>%bQXjzv#5k{o5qS{8e3)K%8h?SUc_9&pPo)Y!{oBha{`4e36Wn5dWcOf%m`}_vW&<^1nw_D;T&6#w~4F z1Pt!@yhxvXrfXvoqZtjTe1uJ@KRaw_{PVA}D@dOuVQmKSz2wk+Y) z86o?*MN~7bjEkB!$n{{LH_xrn%TkOXN-bF6vUX(~R{BmRj)v{ih+pE0VubPpxlX`w zDDL(fAe?a3;hnYBBpa4fB;%g$`YJV4wrjy^^Mih>X@)f*7OVfXP|5vkrTU%w2=?Ds=`yUDE({=b~)|RU$1#sYg>oT)}mQ zxb^s!2rAejMVSIcckQZq9?&=`e~c5JJtPbKwz@c6Y>hMSz%2hNC`-WW^23#Q+dP=i z>X_+HnA3!b_QL;>tn=ZL)UTYgzz3r)?stB+Itd~Vxw6|lEN_vJ%+?c!kz~YM z#6fdK!d*D+>7ZNP{8?3lwdIQb*3z@1$H_99Ln%_E>LjdIx3(rU9^eP?M3!2{d#Y7wScp_$gq>r#RwUYWQA$U1V4liMUK zZJb+tt(i~=xBFwiU-D1#$kf`4&<(b3@!N8pKo(Tj91n-^s>DC-lvqelG7A)Jjtj=v zq4_d5EX3IjB8R4IV-9--=!JB>h2CQr9`4(`fGJ`?Q5yF{m%$C-?opOScu!^@8z1Y0 z`%p%2XvE25jqBM9PPDXu!aVz1qS+|Y3bRt zU%S6Qa68CY`K=|QvwHs}MuoD{aZRyv2!NCO0FthQ`r`dYHmntHbuFV}{g}zXp#$#h z>IyzD%QXgqWprN;(0M_ zEvPN@Pc?cfYZ`~gvzmwY6Qyy?*n|!ci122k97#j4oA6}?T$>MaIVwEH54WKZlP>>r zzI2h9riq}X(^Z^M+8}!`Xd#ID-Ueu4$l@g0Cw$ZVl(65ep(x|JiqS_ca(8D5!r!$_ zw1ZbVYPVIlHTWk>v^dv1*F(RxFh;}cL;4RU@h?KBko0vnVH(m{Yg9RL%)Fs--MGM6 z?A%x`Rhvg&tZ5;kMM`%!z7@-U9*S#RTg3?~_Ea(AVjc&t#)X2hPpcBuAcYIC4jY5= z#U)*m{uik|<*7h)#JI5Bs3l;>;U%teff6g)o&qcnIHsZ2Hp?_qG3L~Y^sSGm$bo;h zU3%w0zjD<(5xTEohbd2?nc;9Jj|Kb&;N)X`4ro?1C8OJ9;Vh8vxQOjurr&R3`}}MA z*u@sZC?Nb?7eXf5_Ef46R>k-rhS{ZPr}k%uoX0Odk=AINH`eh%O7tr3-onwTm+Myy z&f30UVAb+eX89|W$a5_mR?TUAX_WlwaDc4cc}er9QlXMM{T&!z^e5&-#^-^>Bwo3s z7jl$>o{)(M8cg%9jutvM$jDa7{)|_>)H}cza56oFD(yM*q(D6KiaXp7Ky5ciEyp0k z+5V@6-E5lyN5YR2ZBK#ZsnMC*rC(u@tf`|C#dDNk6|&)>U@; z$vV;TOH!@N7EH-eOou1YYs_TDz3aJb(}moCK%&G36AWws4m^WjkUy)4Xy#|0;+rbJO~IKMIjf$eqwBDp^1x@Z z{lUGSTO> zA~a?^BvZ-yZ$CA@>A+6?bJusX8_U6Ov$*kn<*4vXQKTSL#ou?6WG9fBk~*#XHm=?!VCMQ)>7r zO`#>O##DuoxmGR^!8WX$XM;pZt$$uIY>5kg1e3hjjQ6N1VR!S8}Y@C!EN`KSs z{g4-Lq8`?mul3TwoAT!3B#_XMzUKPU;U37aL2GbRIQ>ztW>HC+)2(E*V1(23yMU~j zIW6ko0Uh#xJ@Kw07w3{=3kla7;5TO^vL3ncu{?f!pzDh#S`)w3%dS3jJ*`hwHZC^5 zglpcOyz`Q}3GuC+lkn_{;`@{M-mekrgC(^`oxReY%|aR-lZ_u`f-aN_UlguIsA3-u z@+hdw>ge_h#J?0uJJc<_psW-&DiMRI?{fZBNSjFioZdg|uR1(j*@feDEXM>U3Kv|J(SRH(`F{2Eigah7JB z@`z_pJPl~Xn?gQDT{XzKV{k;7hgokaI5h zWNkXCG39R8t6NZ&$W73Cs`3Tq-TcZq0DAP|k0y^@n9}gGp2Tc4rj(=1+tZVhG`}@` z1B@}OqJm!ZA*&^Os6#n%8zJ1Lw{*O4fDH|DkD z!>Dyg99MlqS*g?c>J2${*KWnk3wr+AmYjjC37H^4*A(WTRfQ;Lo1|P`94)4Ozpwql zxZ||Z*1DKjoy9v!E4@HOZ#e0mh0_1f$(>b;Bse76;S10Yy~L4iNJeK7YrhRC0<56N zaw;D@eFSfs9;GOg{qlf3dG@Ip07_i+NYcVPH8eOEVO+R@iwT#O*#v?RgeFVBmwoa|H!>#Zy8TPpUh0!Dm)kmp859qNoVj&0(u=U==#uW^aN#^l!lP-58t7M> z7@diKGZtT-voQAj+kM;w768<{Yh_5j+4Y#NV+I(*B*6(LAAl7yQV3`{xcRznSzLO1 zfR)m_`!C0C7OBK85q(pG z8FmMN*GXgQ5A1rb7^({HWqcD~-9oH=PtLllZT+Q!g|%lH{ax(>UDdxzhQkx7qp*@2 zE2-(QA=`4Bv!W?^Eu%`T@jSa=>$aLKsKNRu`P-W5M0Yk>GlGHwpn~tG~S1X{VeKuWJof_0I|SH{K>`9uV%3^QEs- zr#+9uQpL72!&qk2tT3%zpoNb(>jmmJ;{|BHPwwRpUa0@#q3C%2Ly^%;Pra3{~rlko0!H;mn>gvF_oT)orW}s=eOYOpAT+B z($xJgPK%FnKa_(WLp^$e?tcOLMG{=*n*Oz(XfSYl;Ox3pwz3=Pm=C2j-}8UIqbB{z zNash@oP%o!-0C`C4PJPx0*()rx3qC;ye)aPiC9tMjVWD;p)4a#q3D2xF*pIvrU8QL zqtmxa3ZUP&4?S^;uWOoJJ?a%#J`Vh8W9!pNJlkeH3gy6{V$>+s_Ce%O?I+2Q$*C|`QT z9xwo5c=zi$BmD{#?6W(&tzX~#n(VgjZE`;xVz&r1u-T?<+JIln|rgmHikg9B9l@L1orBux{IA(Wo@oJ zi0D!}F0MxWwunk}ce?fWR&F63MpRK~*n25GIi=o0GEN0ZL{miv9E=0B{weN0@J-)K z-(iL8^|yXE4=M|g&s(BmB%WAkt&cnn$XvZ4WCMTzg|M&y_JAP9m^Sda4Rg1*VqR1H z+>)kR2_C+b zWbN_H%wbmNY0uwdG7_+$4UfL#1+sO%oH86Rx90Ry*BUaJb<1!@PZ~xneEz)ca%dPo ze;uJlyHOQ|s_9Ez1Mw{ky29yaP#h8ds(BF`!O8cu^Qigz>pfe9dTbWC&y%hTVa`P` z@@7nQ3}$kDJ880ep)$W>QCEJe%8Tc*{)Z0|Hz)=yud>cY=W91K^08mn{WH-v%(x&o zu06D2Yh8OyQ&Q&Oi944}w?DPfcMecc-PP^C;$I^1Ac6{(y%RYv)Pbae*}PBL%jKCY zZfG-Gl2UF;o!aypcw-%bH+=L~rk}bQo3^8VUA|p4$By<$4poMkUKbT(dk;tF2%=b- zMLo>S7f+OUdS2K`Hk=a>p>S=3*YZJ}dC6#-PDq&f{dymd0QTM_Dnsnuh3Xd#lRD$Q z+&_ufZ4a;KBcis`gSr1g*669UuJg_q7XnPMB8!0hm+_Dw8_2sTqDdz;@{d|E7a7cR z^$Nq~5i4irCoVyxU8EhDv2Y#IozdB{#0>1|yy>^I*2in@0K$oVJalKZ29o${Y^J&BnI7|Mc;UEjJ`^pYg3TJZ-il^TVifBn;b8s8 z7#}KN2?KBe5pys$Y;w%ey{{d)OlfAA+)OwfvZZzAZJqN`M40DqG79lY0oHv0F9e$J zUiUbpz=Lr3*VvJi!|zZ~=T&j-`d6I&!puKGScHm9>fHFfcb={mvNL|muItv3m~sYR zf&@wK!*&cLuFa+l$P@mCa{ z#TyeW(fYBZ>MQacXI0|=lLEOz=7-LgXHkw-^Y}Gi^R_&g^_bsNuB`;|LVtyZaY|RH zeF}a5-%i?>yg_NfxxLuP^Jd(15zG(Ec!0$V5eF|;na+;s7d5@mywAmT#VWove^Kjg z1bjg(`9Wpdf~IbGNkTx)by54xT^n|BUryhI@^0~l=DcNtE#0Ek?mM~_^R4Ocj;$#Q zLuVFN55nSB+k-ECn zxn1x&voch0uJ27d)->5KN#D9!iX*r0NI$}yO7pi&qJ8l6>k`|E{`QdK$GwS(IsjED zFOSFXw3sx8pYNJpBpchfpEbPKmmbrni3kc2;rt{_0~te!@l?Gs;$t8qp<*eR1!?N7 zk&vCNfncYFc0X-{PMqxOHYL{$)Vy+q72i~Jq_BGq5}b>Tf^u`p1Z|Dqs4-_xxuq@c zAM4~F^w5{&mpZ1tpb+zhC>=gmjU^-5`aH8@A2p)Q*JkALlWimI;x)P7=A_TRw_j+| zjY{qp-iaG6(KOe7%eUz^nRr&RP$m;TaGdP&I4`o_Jf!Ke@c|HgZwL~GcX}2@faZ{0 z_n{|{o@`1opmr?4xSp)M=lORLhMtVSHUNGkYsJTNuHA?^)zG`&!jsJyai)LW|09v3 zX1PU!0MlHl<*&qT+x>V#38d)lhOb^t-TszfvasigmezWjvK>dB- z2W(F1tz3x4rg7V!v=L9`;s^BoThbHhBqGZ48 zy(k?4;hCgM;OC+5u42J9d1`BH*IW8a=l*ehk5=>TS9hXmi;zAoUeepS!QeB?GOl1l z6^VFi)FPYUjJW5YLcLv8UB(jDTBCHB>=Mfss9_wAc*N`S+|z54SJt%o+sC&gM?luw zY1J2$%d`NReuyb?&H}#k6I>J`AN~`T=a`1rXm`9WzX3Rq=KKw34I(d@u0M z$IN4ckSq^)?z{d!et@K^9qwS)3Iv4w$UB`F@mAS*O$mgWak#YDQM-TtM<(sXQ7SQ&YS^4;h((y+rO@ElBKT5&_Tz5uO@`UfTe*4mKCh=6YXc+z=>h&GqM~r;-_hl}F)_4}4S@I*A#Oa?+Bn;51$jqM60z`N1D5PVo5KSIXbklz`$moLr zKdH{!2$B)ye!F1~rTu}k{O7t<72G$e1eD9Xe@-<0@`_Ry_RncvOk2%*d1<-@ic*p! zKRh;cjp4^jJ&T(17r0;Cg-+FViZykqbb0()N$$5^k3C5bcGSiDj;#qDU*VPE-y;}X z=ct#__YZyJ$hQypI(EqDBGU}q;W61~_lx**V-|OWb6*?I)(iP4-q)fs8pavxaz_K+ zB$0w>WsP(u!|c@FhZ{$-UbL<4&WMNPh?s0gR)Jeok0!o(D`Y1RjJ+n0ikIq>fo^?C z%;!I4xuCn1qT-V{bqD=!E!*yHboc=jL}1bGSiRMzi%IK5GHI|v^&m|9AcoL;yDI`s zm?cZy@8O zD`^U4j0ia^t*y7A7Yh}1BdEw~P!!bh@Uq<1rp}{RU0}mBMsk_Ji1FD#L>3q*ji&pR zSuKAXs;&LpH>R_jZCVxDqY}`cWC!e>@GqeKy+PNXRODvI7F-*uJEIlFT{OA{-3a`^ zk!U)K;(m^;*l>0~eLkXiY)J17Z_A(UQLGjD# zQRm4OD3@ersPXzJfpvdfC`se4E1UVNwC_;n9&_vTG9ZeWVp_5E=@L}CPYP0D4^{13 zp~po6-&Q1+WrY^|4pGi=os+w0^=qQYl-{>Cg{%d0kXOd*m9-CcQCD1}>RFA|xB;YB z*yDnA+Gpzoo9r}q1PSioq z8D-q`f|JGuk5=A{1kJz!qVWnH@c6+n<#t@7t6xZPVd&@$ZfM1iq;c1}jOBZ1s;$m9 zA4My#k;Hpu#HrkyQ$zu1ayQ%II(DP^E@n#cgHrn8eWyQjYGhkNWV=?X^!%ao8Rk*b z2b!!GN_Ks-B6^xW$@%Ka2f3DcaF&$*}Oynb273pQ(!(JsmOl^D8qb}|nt;kUb6rtZhu z3VH(X)Ysrn2F6@9+C7hoAKl$=XNZNCcbTSO<$oO(P&aC=cInyk__6M-(h&L02^Kif zHIMk2kHFyi1F}nj>%#~<-r++a3WswUxGv)2hD6)kHadTLl8=clMwnqb%zyUpTCN`4 z_z0TgUi))<^D}|=J;VCIkTNKr<-7TSxNp!$YSxQ8o8I)WGyGcu4NiR{IRRBm6?JA4 zqvF?FS%A&j6Wv#SLzmVoUOpu9Q|*=&ncbA>c+xEnPINm3wEs}Ab}4kirshM&i4RrZ z>n{zt{`49S9^E%;Cb|(HRX~gTx|rqBgUj`0#vYY-*$XTv_BKgJxSdxxOU7_e^(9oj zIz0K2HGUloD41@*$gp=$;L)qpFwp|5-4mV#qI%LT&-75WCi%9=N?Df(2!3*GM{`O7Y zi$cq!_i?N)RbM}MZNz=Sg1V4^S;rO5MhGaAuZ+o?lbj@&H#*4$Oil*H>>sL6GaxHm!y9Un3c_>3qpU_|=cU&A=4bZaZ3_dRR0O$U1{Gjy{EK?) zu>fBO^7F8!8^8)T6I-)wG!Cxr&HRFOxC+j z2bxGY`98e5F-C)tI37yHRmZjpFix2&)8xAW6MmXuPQGA1`TBD>*SJz+zJY}gaF;}c z?t&SK%mhck;?2I(t`TFJtdDh1@_FMq+ZE`3ur`sPhp!1pe`gyg__o~>*NMfi%_eB@ zLT%2gv68*CswgJte+b?n6CU8Si~E3O zRPFkCk{n*pG)9*lv`ac7GeAi_TI}@e?7+BDx=Ng&vQ1#K>mdqHNIvb$EG7P)Q49*{ zVh}8tehz5vVT6@rFA+7S}f#;(Nn+4=rOm7I} zJO-0_Xn_~E*?C12!epcdtPdSC?2lm0T&~0>*qJ;M-*Men{bU;tem5E(Dvx`I{@>XH zYM0$Jcbi3M2<|CtY&r-3QSr}tAAYA^%Uy5NU5&xHIe6yojCSuZUI&7)=m!jc@Gv!2 zyc!e{NTA0$_L{21HpM|2=;X;DXW-rzp#G9 zc@G+}T7naSDWxRw0l^1%4|``~z5;j>l>ixQDluNM&3j&#i*z49@nL)4imI=QatUUF zlSU(>FxFzs|2rA4EM;?J|8))g@LCi!s{PnhRIWX;TE9Lc55WmI#Q0JCndu>@V=VS2 zp7birWj5+w@a6mSL5h5BFeV|2om@8EkO+SwbK|7AyWBCNoi(9Tk?WZgpR2dwJ?R>{ zn$OKbsGu|MM0r3-3P}^&D(+XzN~QCopTT zQ2{zgsBxb{t{k=`HaVh_ZNJ+buu4X%=OVq?J#x5s(v=Pu>y29KQ-$xfD*SIQ$_e=F zLh$aynHET##vP{xp;#Li<|e3%97c_v?coLk}%IC@3Y}Eh-&DC`cpS z-7$2Czz7H^jdXWOOAH|(A>Bhr&yYht@A>|&_aESbvpHwqvDRmGzvgw+1z?#m=2+|1gYY&WU)@^_g8o%QiQ`c+5Eb)jpvFyF0t= z*Vok*5%FQGU+bF&2}~vLR?1F?>dJ_h!?J8TTaV#<7GhtN=$x2$BW815d|xxB9;(rIR z0U|clrSH)MSK>R_^g`RdUd)XJ1ngM0%OSa^_UvT@2k!NC+HFrl`5(0yUKdVfTKct>F{;np#HTPoF9|P{B3@S{LoyzzT4eNhcN!F05)-${u$d1p3RkrTv zzaMcZxbpv!yt$A0782JdZbkew`AfFrwyZ+t0CCj41IDLo=|1_nf48kG$v{U?G|K!Y zxZ3jGu^s!)cJHPjh3-ydKOmu;OED=@np9Kr9h&`0iR~SK?zf9`i4#2w=RP~_;bX^H z>5YaXoe?}Y$(;)FEK6E;5t-?rOq9ophXa{1Et8V&E3zmORx&)Lpm#zid{L|?p4T(z zRN^f@@x7cz1zJH@mfhOT@Y((^#XY`Q9s!d+JKP2kkd)tmGDcwuo?o6(Sl)~aTCYXk z=baS(ZfLMA;A=7AhE1CnT3THFpb>{B~)f>c~PV_;*&I? zo^qo7R_%A*7WFP|g=+r13*&AJQsq#j6-^=V6fWMgTV!K6V$+!#N*Bk@63lIAzGmx| zw-wK0<&rq6-qqnHkvOHu5Xe+qTX84qs)kfL1V^9)0ce|VoL~AcL(lD>XOHNo-!Ujk zkqRx(>i4yrb%Fh!X(=-FF1j=ShxKpI@LyF}>OS|zbF|QE;t&;?n^YE#juTF*_{A6z z-T2|wZoF=^l2rNsvIZOmg$C_^5#X<86YN1h1^xbUJ_e;g53@3HF>e=Hh#%?ueAogT zX1$Gw7`oA&qG1&;@rO31Ag=6}+IZi}8W@Px&@w%i#(4ofPz!Hm?({3JaF)W}ckoXg z7?-jeKMb%bnty$aa1k#aY_Ch2Zc@(MN?u?ppE8PCVl~bYxS6a4qp3LtL8HS|)E5x> zH*zgD)8JTIk<|8fNX^~nirnS&`p9RQa{ z7n4Ibk?d1@n=8fHryowt^|_Swi1AtThpPF2`YHET;tmHL1dxN?pbqT=q6Tv>#WK`d zz7`scQ~FB@9VK#4zpxm21(PCw&#s=+5tK!o-pbzS3Vz~Tbl^>G~WRDNQG+e ze5-70`My*Z&@y!~VxYabWOue`vA*W1fileAq1D`|%2$6LOFN{sX@A+)Enykjb3;G! zLBx-DsW*o|EN5}rtZtTPM3iuOQT=)Ry&!*Zg4Z|@9ut42-lM}Gc%~kGc``70=*!0*?_FX6L@9L`3UaB85xEuCs9y1V#PfO+ zN@h@%a5P>LFb%zYKTPiG;zq!Zu6h5(3MUtEj2+x&QGs&oa90tZp|^B*yp0$zp!Cfl zm}^=C+b!m_(3COWcwgUYR37kuz1HkGBpwkISibq23#`jzP~am-CC0J;;RSjkPFTkC z*NyazcTB9bROx-rUb0sE=i1u^?qr#foBva_e&~pRsTfuv-1_uATT=cGV z;D4k*D}RlYOR&|XIsNc3N#7QKobX+q!#zG?v8hq-F=Ls8F>%1ObG>U(f<0Ce;A3QV zjK@@<3JlN_19A~v(v!BIo@`tt7SHm{LXlyIHW5T8KNaqTOnxYJ@z0Oz=gIyJfhK&8 z;u&-Zps5PXnmZCek(w>qu2<#~@&q~Z#kd;v8~RifA2g$%BoA&*eSGP`ae-HL_Xq<* zA8gbN5QW69QHMAsKoK;PhS0}@pJZdG$d-lRX*xYyf=NkXCgWNBy>JVD?f_gfzO7xk z)q@XediK7K%u4II^CQ!Jl6H-7WO`hrmZPLkzV$~}aW%(~`iYlSgf(92zZW0c6#{Ao zYNDBmFdL`}GmIE=Nh#$Gnkm9?t&p$j*D-M+L~De^@q>*B!qITxi2Bt9jYR;z0$(-IoR=IXw>PL^!IoLfrC21CSskFzB#Ti8fGO9doi7d-b z^cX&Oz7aCt$|>lc&GV1j)}Q?<;X*jg<^=w#^~h2nVjS9SsD!)Ab~g7S#UI4;%DKn< z>>>{oGjcSsH}&oJJ}G$1UhE7}qe@4_tBEIR6DD=e(2;WGm-ob0rd~={UU$NBdHPRc z8{NdN=AnMNh2K|}#pN6NKr&55_dV_Y1q%ha=|?dhG1Y>vAVtcZ?r{nDF9}%T4qr+A zneOfB+CBFl`;cs*Gzmm-3wB=0a4kL+IPzw4X#A(Aw=&=ij&!D83H`##j~8Czw+n|l z_$=4??E9_jJZ2>QYH%!d&T2jS4`UImlrJ2IEfva|*f{mtQki>>?nVbG zfpvkTq8>pqNm+#Y;0(eV9o-3)9;+ZaMSPAW*gRKL=Sk&DudJgr{ZCXnRh8ZMNi}8M z)xRcVu~{C-Um#EOz~e>j3EU)X{J1#1$T-5Ir)2@8eg{gPi6vDgNw{B*#+AqY3TU0e z;Dg^H*)>-qiimrNcZ$;5Kl3h|N5IuBG6>HnVCPf)%%q+t%En1a8IBgVtw%j)e|*Oz z=-}${^o}0$pm^OchkZHUsZL7aK=ZoZA`eKvqPSq-Cu~!v5OKuh_e!-d!GThhnc2 zgR2xp8!THPU&>gYrtJH%Y1zt#b5O|YmrQ*pQ~H*wC9(afgu*hakW_-v_Oz%)dy9|x z;)mW_RndfDtlp7z+hT>M*P5qNRz5{5S<%LfrzC8Jjc+-a0=$rSam1AhX26+q?ox<`pa2|jS$Vn2%4`=yjRJlrdN zt7qHiA-AFB_X27L`e6BRPdNWp+F3_vey(?{Nes68XJF-NJApA*7Ba8oK0EX}SGKk6 z85|exl%m6e08xWI6@K97;ba}jM034O$mGhirP@{`#K(mfDaUDd?iH#zd6X`l1?V_S zwUD1v-C29qhZg2zh3%7Q#aipcNQL2E9%(yC;4pPM*!0rADEK(2M*pe$ht-#?aCs*G zokHVeP5(!E370|nZd_A2@+UL8tE}oaSmOjYHZc-(pek8tcQ5UU`o}@&S)pNT-_U{9 z@4jRy-?Dl4l=r*lc$*#PH#42l@4jpgv)5D!v1C1XUE#Js5mQMs5$!cyiQ%aG~J4h$f0ku`TvV3A?BwKGtvd zr_iPMLP(#NGWf2@Ky_8;w0;lDvsv)t2wg{DF$-Zb*b+1!d9g+;rmfSh`kS85niec( zRcsGVNo~9fT0@?fj=rozNb3#`i;*R9T>po)vYTQ4cMz;6mmuiTIz-LVk~+7oh{eb( zNpIEM@Z%(zb$EUES-6f(U=%3gHT)YE=N$it^;%PHfV(;!D1T^OOK*trg0-dy;NRh4 zv0Po1WnmF;InaIgVcea27AB)jL;)R=Net#zQ~v&58H5d|PL^k>XIwwiKTqy5XOel0 zd+gQhLQgFPdem5%^e#SG&3EEa>4(SU&up^-^iq>)ykGX5?Wr8veJ`_}`v`fh;9^Mg zYYX*iTuP58=g)10%jCO3V&`Edfe&h)ug^7_Uug!Q^`r-1%J1cHWvN>|O|K51uO*Je z$x7P`Q>lnU&R@!AZT+>_hd9E%Ua4U9J;jO-UqLkK`C4otPxHA+x2DKg1#R;xudRGj zum@?~KvkY0ln-*ij8v=~Ncg~%p&=TReBQT_EmKvl){9pZ0>4VJ02Y}zb=4-A+^&mu z>R1F220FMKE-Z#yweDqRp~@tUYDQ9#2$nO0;MD6N%_h%8YwXMJ6i(=gz# z7C=Kkv$aWrql08ACo(vm2>#w4BA^{&DCYwNE77irInrq8uL+=9{wqzgS%cRZpRxM6 zd>O$>lkPyQPq^2s1JgaxyqPy_T|T$=Syr6xInlHbC2wJcqy{x0U6&{?n=n!vBt}S} zMqE%*l_LgJGc+Wk&bClAcXht4jmyJ0Z*h~Cxba3kDZWg_Oo(&HqO9wJN`wfAUdpaY7)KeO$qxz5m|qa`!&@j&i+bad$HYQ30mF zn7v2aq?TX~HVXcgG3@Crrr^f*x=HvbdXEPZRvXAh8NE5E@ZxH<9G9Fy88v<<6hCOq zMVEPh#wpsTm=D)Pum}-TNWu|#ZLV=q;2$^81 z_Xd%u1QR(6LjydQ;xg+iqb!u(^d4yll6gkVre0>*q6Qu- zzUtY{X?*VW;&70A*B#Qg_tq+e;@)1h^xtDK@%8fonaCA{EgKfB>+8k!X{h;bga)Bd z3ZjdvpF@6FJ8VKVAHoX=o{OnED3L zYd&7IY5+uo!A7b#t6#knvo7i}a+$)2!WY)i2zXp({#QGs+|H z()OhQ9?n*^C_3&Rb`LT0!g=Y=*TEtq#_5%8{bG5>zkv;@1c}9=jP)dKx(El~j1Swd zaQ?(aEku0GEiqHo3*jd%Nfgf`*syUb|V$nnj@?-VHN|5ukin0eAztozwPe%{Be%H za|%8Ngoegf9Cpv^QU;N{V~?7v4*L(Q>#5vqyPMqLHRF8u26O+l?zm@ju8XnKNW_G2 z3)W*c#(2wT1+QL5O_-K+2JC(_u)M8Iyf05!a@OGoSlS1AMQkj46Krw$LNTe~=LqAA zJi;j^1g`llb84AWF}D?VtavZhU)q$bQ0kLC{o*7F{t;x@D=OD4{L_iQ*LbDkf*R9h zyE5e~lgo-w?ThlXKJT9Rr^7PJc|>XbMa2TWd&+&}LgZKa5(BcYjwr%mriz^7uOg4w z+?O)2a@B3(dMQJNvMetun#ZdnQ^o^lgIitsByQ;29CJAs8H2g3>)hwzI%fn*2B6WT zmc_D_p6AWqlHa^k!eP$)J34WZ=>bA&Emu2jTe;}HoTY~0xhicc0m}4Wg>tBZphqS! z%k?E<^nY0DQLY0tlPPog)NeMD8C=7=9>p2EzIkqdc{ zQS;R1wiDc>zVapK*AuoQ1l@tY1yKubFtkeBsm65qOpm98ozU-st}No1qgee8-Lsn* zFwLZiJ+YzV2K#uREhWGfD(NGT#WG9aUhHO7TDS2aR^hK3HAKud$*)=c;v z!5ipP>g0s~7|sOfZB?4ffh&Da$4M&M?|e)l==O=l1(U;zzn!k8 zsp|LM;&inkuvVSke zP3ea#s9El8n`w2l@ZwH-)dH|rD>sz~-xz_kX-M!J7iYoQsF zmi)x}byaAs;e7!SM;HHLDJFN3V<5MbemH{wdQJ@vf=$twQFeHj_&p%{Qv8ZX^`Vr@ zYPdQ^#uBF;X--re-w>U_3rM_A(vLUotu=JY>h7!<_D<>h2rv*l$T-695Fp z1bwACElYN1ZkaD%-dn`k{zqqHo5=BH1g#34F`mSrqoKpz(#WP1Mx|_{H@0{1+aJhPDuiV0cS;OQ-E~!eDy)0wf zUrQk++LpTaya#u$wjR6}Kd9X50Ws}j{lNLzXB+iNj^~&7!8Vb788uN=8BemM(*{Zf#8zo4IfUd+X^bwmuz`hOcze}oh?V3Fw8 z{EiDtJ+CUR0mXWmi1pVl5Sbmx@GhRJX{_xtE2l03O7waaKs41I_rM1a1r7iz14dSM zkx5=Or=b4BQfcp=Qt0_S9_N;M%>qw{DfjM~4wRU@sq_m`Lz5&+``qNf?E`Vu72m&z zVk30lYllQx7d2A4^viP3YRWl%??-e_j zTjJj6AloxcEZF3c9NZbvqkQ@JByIH_n=hA#q~){DTVz+^jTw-62G%At$V0s-UB?+7 zXwTB$nXPfQz7>Ow2E-it%eJ^(hL8bd8B-ZlRMYT~3UBo@qjRX)cj{Oser^Xh9mCI_ z$9(b3Pwp#%TyNDdqZ<=FHE|hAQNkgleyYC?RDz*uPYB8!glPDP#NN@Gf;+uK>eKu0 zjW#EKEt$L-)VuuSm#f$oEHp^t^P6zVY_MN2-U}CN?TulEVgu9KkrdkKjknaf{W)=0 z$ZjEJvP@m0`-!RdEy14Po-7}LdEKqIYYDRVXJv170O!F(Uj zq$gWl<9$KWmz9N$^!m1c(jti!1HGyF;%&N`F#4M-Q1$k}zzRA1eKEas^4tv0A1%I$ zbmuJZezwf#zH3b$b|=s7=n9EphZN~yKKcFh?S(L!@r!@%`PrToKqSEp3d~^Uu6g(? zi3kf>Pn9LRFT2yNWczv9zJAYS+xGd9Qdj#Ig&nRCN^r+J4@E~?L~id zruR$h`;|kC@hrfETcft87g}O#PK?Z5CH$9Y=@!9ybTyfPw~ZWAgO<9KKtX$JsONDf zJ3xhgVwU=IZrG0^?#j9pIq`x>TWcbpmQ%_lR+A#4y(+VKr|_lE6IvOOYzPo zhfCGlQ=`VhlQQ(VgL5As1;%sZ?+R^jWTOOl-X(z%r%(yQU?{hk=k>HpcVZIHy&ytBX<$j4Ly0MBAZYG=A))UJ_Y*wMLrt;V7 zD`>MYVIxRlhSHJEV&fe5hFJ`Kan|gtxyJ62#;3^I_0uh4VrEZMYQPrch&nlHx%CBX zv{$IEHN#0^hLA{lKnBMh3>$NRXzq;1ZwAYt?C{#p$cY84gD0SRe|;*&+k?XV^D~&nCosZ`Htr4 z{I>Aqvfd$#=lhJ;mwsBWe@#R>1nd2)3oV{ zq(|-bfYTT_xI$(Lj0TWzA=V{;5z|HEMIvC#s^CO2G1GOqJW zXIuvR%>RQlJpbGI+l6hqi}!wfu%hzNjdv7uKI)6N5Cvu$%Q( zaT`x>bl1+v(%RRp8kpoIu@NOjgQ^L4PiyFaCy=djtGImk3gej{^m0U@6dQ zQJi{FJJl{5joG_>7i0h+YUQYdBvEFEN=TqQGX8fHjyH3@w;nyX|HNeUrH;fd#H(5; z$)u(SN*(kH^F7~ae~sdS%*^k_sUw;(Krug%@l;rX_Df?B@fkup?(j~d(!|8EMW1}R zRsY)-H=}L-gJSxxD~B%DrkqWuXTJr1I~dLG1P$+50!G7msP3%ni#yl*LLl>d>0YIH z+B+on3r4^1+h>QS>U8GWj|ZO!aOWn;quoQ@a}7}^7TGbREQ9D=L}tx-0%U%%a!HcKhBwosP3HDSA&C%1Dpp z!EXdHY>o&fz8;)oA?+-GkzO8+n7h`!$UKsQa=2dLvj}$0<4#4L(VoAb&^?qWedZl} zCiVEkOk-3c7EalV*sk!OkNSzN&JVQHhW5FNlQwy91|)+F*P=yknN-KOah6v0=2V1r zUMMeDMurRGBU@!t%~O&q{`i!4UV^-23Qe-S$2kjISp?(~JVKs4NCa_UK!x^;Cj&K4 zUZjv#`8azm{~DhYdi>Jzy_TBjA5E^9p6*5V4agC@bs6xv1$Df;$)Y#8THwP?t07W) z@n@cJokHT^K&4Sis6&s`jbT|g{0qO*pTB009S!y2UYH9r;??IXjme`_p8Y))!cHR? z+256Ngaa5gwAfp~Rz0eODcN}8dbncqBzY-hmBHF?0orOe52L#NQaPTgEZ2wmb36u@ z|D(f#%;}oRoGukU4)v%t?G#YS&7|D|tG?bsg1Q5+NqXpttfU%onEWJ4+@9bDyDNW4 zbnh*2WvJ^dep>VfJ;TKpTYDpIwHtQc!+k z8osA^8c__T%L9LxTW}>6Np|LY~)*Fg)FT{aZo?+8NBOVXaUwjyUI*q?E-Uo{+ux z_+d9x(p|gsnWit9pu?+j23qO3OTFE1yjXi#>z;8zPTd1cuQjp6*_o@LDmdmqgy=yY zXgg3e(5uwtNuYTx7V*p|fAr)LtES;l(IeJnbN6j zDxkvha+w)3#_+u^M-X;Z#DV<{tLMgCuWosSrwyOVk_9=Nre0_p5gN$+m2%s?rKZL~ ziKP@M<{T232Xu+Vmz7p8c11P-VllhXt;9i(zGx8y=zbc1$}xC`}dm|j6*uE*r9U53#>OmCd7w} zdg6H}C%UDz-sxBzF-%`M1cz`XeG#XN_ukAf^2-036gDOY5v+ihQBj+xlC$E5oh(+e zqe6uS9VGOVq6UM@EqavYCq@do1EPgvc}*fC@s2R>9gE~FI&#@kW3goL90H@Cs#&6C z!^>CH_cjvVXkod#J~cDVcH}BY*_`X>Djm*+%LDCbgswPi?~RT7e^{nNj;4&}lykJZ zP1YFBH$md44uc^m(Ow?quy|8q6IHnU$2g2ikki%Vvu(}b;Jp!;sXpUXaoMQVZrIe9 zuC=d>mZbwv;S&Ms@kp%AjAR-oXPntk_nAMqiT#0$7sA7YH8_31}cp>2o)E!|BVIwTyz6k;bMgp6EkU_Y{4&n5lf zIL8cWo4pgM@w3=hYWnM(|BbEg>zCwY7H{?}GSAl<^~Xn^`$YULTHF3CRsnyFZc|&r zRkT92&==*aB!8V(_X%G7gp&%8kqYkqA-%}l3^pDJ_Ae}mdbaVEsSlLXXwZqHP?q5C z>mFDpyy{Pvf`0VVEWjy_Ew>`=0n~4orzWv5Td;m1`!k;bD z-jr>Q`FWEfMVZ~wm^H{_J79Nbvst7{m7~BYv%OUlO?zg^!WJ18Z5lw~A=|{mJ?}88 z!{L%VzVZBHa&;xXj6_XpV@Py&^IUJu=Qa8VhC3?M^8_GE8%R^@l$PIs(ypyUY0w;> z%A~(gM(}=~(2UZb?fMEgp4>spqk<&I8XPJtFZ)Ue!oIy?uur!&$wNUc-(P@f{e0j2 zj2<;azfb+zpj_9-G`87q@NFHtHGT_isCe*k88f=xayW3Sbq6dO!u>J6Z%}gK&Q1RV z01yI~s}um;oVPcO@3s6}K>9Sbg+h>twpL7cki6|-L^%9auK!u==LiW){KR#JBpPk~=J3fYb)u=+-jTTd#YTQefUQl2cpBhlTEbF+lH2Vz&~4e4K-{A)bg7$;NE8aOPcBa zx6O;G*<0xX@!q%*fN#P~ZP6hh_~>Fa$-Yd1=O|is!6t<>nQ4WHL&9t@)3wZCwV{JP zOR!!PK_FPCBSi*=sJf?SQm??j1Jn)(d637{0}pImu8F-58fMrMFTzK#Pv11#xp6Nb z#KkjEh`->v(Ypo@I1dR8p$^gjz^LqI)6(`RNI&xB9J-ue2r@V>3wY7R((W{0;}Zooea-q~TMf$H)7YzLDs>hwwttrYI; zbbe&B5C(0Yw{LOWnIL1VZzLGGW&V3<8^c>w4G6G4fBrxtJ`v?LRu`6#`!C5I<5iJP z$u@hspd5=Dqf~mS9^qK=h?e|MxFERNaY)YDY%ycvQV#P%_E~PsVs)D^depgKX>Yc2 z7Og>#_sCZ(sPxxamKR=@nS4`lA`+QhUKRc7fULRx%(4F#Fp z53PB=9giJLv+v?wJ9 zb4}KAhl@XJjI62?wJSrvx%>vuRE1${z+;y|mzZR#G@x^%ym&%h|9xCAOr-K?rB9@R z8srH1QiC*t)IvTLV!p3K7=rAYnp-0@(1?D{CK5u1);MKn%a%ASjP%BLB5+keS6Ly( z(=6NNPHDqgOgo8HpfDpU-jl4;Jq;9}?68{VbfhzahbD>urJx7cv;y){c&8#)Ts%|A6 zy9pYd`Q#(C;G2;C&rnj+#_{!~H+-w9q8H}M2ZKT%x*MJVVn|ZIlTEYOF4`5?#LY=x zE`@{Wn|?Xl*{USDma5PG8eWy1CdSUJDT#g+a!qRk6*W``|be1|LUXdLMD#mtP6 zPSjVWe1BdFqr6)q3N9KJ-$CvSjY4OtHh5l1(=F3-bESWrMZ_A2T4{5W4u*}nT$KL) z9QC3$mdq;Isi>!H@tC_kvNKxGu9{3a9+9Sp9d?)Cz?N234881(1~dB`H97^>v zXL@FgRn$V?jdQQh#VqOH$I3!5K907{KU*>K1s|4UqZi)s{0M40=pAN`oEw*Gh7Ldy zO>yt*A8426VIwzd{UO9aK5`wjVAz`VHC~H2Aun;?HgR00YVF{2hbuGkJ7;uvUxOtQ z{lgxxkr7?+ILc2qnpg}P%T%Qv%IYlh|Fbko`1Eja`yR5`2RL_*o&au*=&;-80Cy)k z*BTA(UsPYbx?`hnwCBl5EjAj79eXC~Vg2$wEIOS#E$Y6&_RcEr5+R10Q}7Hm+2W-ci;G#Y_9(s?!W z(@`;!sRyRZX`9}9sH9`JPT)}O7SsuJ?WVydbL-7|Pv9^jlx<`alhJ!rc1d5bcFmnY z8OHSNuthT9T0O^56;E4*3&ErsKO1xSAJ(ljaDX}Ebrl%3ceCCJw*7)^87HwFZ(uI2 zt|RNE{$SK2K`J+b8ZqFZX2JfLvI5D%!j6)lNLhx1jN-D*m1mAdB!TKu&W$Q>lcJJ0 zewJFO#+^eJY?08oE-|}+s1mp=_CYsTL<4V!h_ms?#f;x1KMKD!?iH4`OMuq^+uzZ- zue&E9>p9)^FIv!wCrI8J)RbBc(7&hOdKgrDb7t^0g_Hh=#Zjkj<%OsUG;K`ldRpHx zbrI;DHmB8;ws=h0<5&1PzQgNUlY94|d$D_SH>&YJthf+LS+<9Xz2ZA5jC1*fBk*pp z(Whi)VhTEqECrY9cg=~JzA;+niJU*`pj_(ncXUgUcYmbeDs9Wn=F*-Tc7^+1ejtTy zcE>Eb5#VA|Vcy&4{hJN3MO{Tm3x5n>shzKHiA*k$@yOeq6FDvE1^7asJ|?672SU_p z+bzz1H}M^7xg#)y?h>7Gs@iOB==iC-%`fH4k&b1>EJk<*T}sq=4W8FWG7Wx7C-DS_ z@|g`m;tnEt@HFuuByJNfH%>zNGP{b~L| znWGk`&xjiXE%e6S|2d>p+WX43c0}8~;2)QD(?kUaA24k4^@G7dPce=)LHcL|h&k#Z z>&lZ`c1<7#KlG-7tUJLHwq!;j%MwI>pW9)NHrr5&FzRP!o7`-c)QnF%;WiOY{?Rg& zEA&x}S%y3GzFkBub7DcR$GOse9Q+yf(vP{%ph4?hrLUO%%R0dv<==s-xvHO3_FsQ? z+IavyOv*K%#K`jbG4z?WK*H|0i~m7r)tesxWY89#gs`)U{#e}LVM>hs4{7H1r~uOR z&D_s`5QZI51GwFYVHB8ZKJXrd*t&UIvw|eUn90(_FMi5Ebn_9L|63>P`kC&QEq*x3 z9b z<{`+xGq8TS`qd_YQgORUJrUdC>RL5`25o-Ez@fL|?w22mXivY1p-C~wFjc;fO%>9} zUvgljqois~<|aH|TBYPi3YC?t9@h~isb^v-~~2HQr+9A0W{@47R!>t;-1hcj{_ zdX*gG`(u^OuqM&F=5WPQcbbjaq~1vL$Mw70ydVKuV*|J2{yu42~pD9E=>C+J0*}EGP+XTV$yZjkm7+Cx@5Q9m5+bn17DBl_u zozR+mp!Wk^T4O$M1cNr3DkVdro17`Y#Q$zMbD2&0g;q=&<7n@;Bdc=m^h=Y~zsqjz z&UU0<7PQB$Azcr59dz+dHZ{wdBHTpn1#^^U=BkE7RkgeT%T}XWXEo6CyZ`tAysu4W zhxwM3OP{JT5f9IjjeLy@t4LEBWARVMLnP0$^+?2C-51KzBJ%$E7zVAf&((O188R@4 z#A;A1cI=;Wg_06~z&%SjF?|ZWMw}SnD;x)8?AbuTeterIYmrTsXes@0CRNyL)KdCeh%n@hJP97lzUn@aK5#*c(2 zgmEr5Pv7}qk%#?z%tP*3pBPh($7YMV(Io}#*u7Fc!YRU~=ma$pbQP;^Mnq%fbaK97 z1U60IWQ_$FrxS^+szlE@T_i`0a%#JZS}f8)S) zXfJx082NQrJ}D7Om1C;Ha#?OlVn#j&)Sx=(oYYGmz2gyylS;`ns__Zk zvi^(65jgjgO&AkZy>j%rtbLuHrAUbJ>uz841vqN+8&4xbao zicKmf=D6eBWdOVbH@2NDugw@dQ^o_pXJdQ{7Z->ok+uYR3*L9lxIa0u8z49m`j}o z{^EZ`Om)1u9EguXZFPR?Uxi zNQ?>SEK2UzPm~!#NtPS)@~VWDEs+z&yNQn*?~X8x-5y4;#Cz)TQ10#xQ6J_Q=v;?+ z@CRH%Qr{B0q-7%Wyb_rT@K)EzrkOkkviVV>UAq%yuBzccY2TCzjZ28)+@vn9vnc3 z$M@TsEj)yj6CvxsfuM4-ApPSneX|p}a+#(G;W`%%{hBpG#e6*`P3_dmbVF}ZPjK~h zcqz*!O_BVy0RpG2a74i<+;mle%R|2BqFk0O^AKR+C!}C+5W*tk;~K5EeV9u+-=PD( zH>NcZWe;Z;EamPQ=$j(zs-B$WHvN2r-@&kPo`;nZGgc; z^4NDGC3O%ZuL`VGaaw8p-eE)a8|K*b5c7cZ0+x%Txw!aH4@G+PIHwMP;+rhO#^ysbvhM41UGCA;<+`)FfMOU2$z{PF`-DXQv2u}d8k&yR=xRhs*%rWISGzd;JXgqU!@OMJ3&ch>4wcDpJ2VH{cU6eoXN zN?)ZAFsHktcYEpmF(=NF3Cbf;Ux{V{e%|Hw1nxv`~=i_Ag9wl&MNG!QYboK@b-{pJsMASu1 zT-q!+&%q2VEw%CQBxh}$-lxbKr^1TNt*Rsb=5JuR9$rh^*<}neBnH^>5des_n$T|S z2UZ8>mna~M1>3RO@AdU@VaAE(&#Y@-(bsy{S%q=!P-Fe(he?15n0+hve}amdSmdT& zkuMt5qi@LE;98-cuPS^xfSYz$5Dgzq`3R~J$*#JynR2rG~(?{T)up zkFut+e}YYjLQwCaQOqDr)h+IwHZ$KejS-yGPyOKL`IPKza|e8sr3~`x%@?Hrpki1r z5IQKhq(%`lGol|4ri4`arT1Ktlr9YEsd;~U)({_jNJa`ay#P{!wB|_*ZXX9(-cp>{ zcRi7aXBl=)!+UtY-QChCWCrV7!{htW5*P~9oifSiN;68Q)(voDPpwKk{UhI7nSX1K z9@rm#=g43*HiHf42I%<)Zsl+t7LNudlT94eyLosWZS)QW`^1}Du0qxjH@H3iZHi#{Gddk)t2!1NwL&w=aAl)UX=N;p~aj`p3!Av_5nC$QU6p{omZFD#3mJF zu(~`=5c=goao72(0E{3-1G1&HmII6P;+OThG;BrMoX|tP)*A zQjdST)R*Mxie#B#nR70FMz@~g^4^yi>C2MzT2=K>^-30r{wm82-#Bh&v0LjW_Y2sl z%2{x$@p~$z8ZqC6Pu>48jJh*vzW*-G(ak&=6My$T)AY=WcX3gLvk>|JzVOZI|Fm!^b-yMW91avdc)0PGDe-D8S(aiB5A3=;VFs53*|qm2fu&5WilQH9h#tQcG&ya z`%F0C|Bt1w4r}Ut;~w2D9YaAW>28p(bV)bJK%~24v~&t6;6UjdAR!^$2uQ=|mfpxA z;d_4X`~JJ@T-Ua9p7T8S{kcB@+2QpJ1O2|#A%+5qfM;g!gGg#354&)D5^s$rRw0yX z*EwrDoLKBIEx&>W>|m%ZfJ|b;36T4m`a{JgwZ;m$D!|TNgWmgHF#zv&Wi~`DozjOt zIBi${X7qpY&?(1%uMc1XeCzj`Xr6CS$AAdYWogFXw=VX*ztpo4crxMg3Wht`x^laVr(a_!B`1Wp`QuI>sozR-}$$LqqoA75?nvo7 zwC=c999f?62jT#fA!BEMoEdlH#5=RySL8v-#l%5|)+zpa0qfWG54oN6e=6U#e%R_* z6u`o^LsJtS-&D6B+W|sw92vY#8w*s%OIQO|(r879Uvt=V|B9B^ol;7bV;o^W_iOW| z8{ESG!|fh#O(Da4_dhD3=E=PxYRp87w#S+x40rL52f9HckOeWOKjUCep$gG&3Ghz( zYse!eme;x%!qG$l1jaY|yv1ytlA-qWD)gF`jSlac4VQ#lt8TkgU??*GkJe;gqhf&m zeE0hCw|vv{pHc8w8SAP)2=T}!EP8`Em&Rq+*i(T;Y12ILh5bzXfS0*C{Trvj&H)3(j2X5LW6Zv@ZqAoR>#`4rLQmo ze!cM!3S`Op6DAYWpn4DJ35( z_dFm`cRsh5yx#1gVd{bhVIFOX8} zcT*GWS#Rz|=j8;$ zMxm6Qb>dvwf(ynA;m%e(zn8x5_PBP+$rrqS}- zXPwkyL~b?($8`E#8u6F0~f_9aSy&g*xlTr2KR zvYvEq!20i_H0E}1kYhL9kxOhR6uo@+N5s4O)(#hvPZ|WDF!2GFEQiVN4K1&>NB_D6antN zFwA*?7fo;Sb0X2bR6TdDZTWM^m@y-}UQ{-nTPSEe1ejyuU_M02L%=|$o5Bq*GP@c$D)lI_x^RpFjjQseos)`&*mB5Rh!AOac*x4^3MN) zbz~e$4DyninOx?p|6*Ndp%;Kj>a_#fxHcpdT~%SJ2&{23GX&%9%bJcQ%?zCX3@wr} zyw~SD5+4cQR2&lBQJLP`{H}I$e};7LcP9v#4t@`jbD0;g#IwO;XKo0iKD2HG_kyt> zfwLS>MiI%g)krJ!-ikc~uk((n)8gyb%wMmnrM&+#96b9zlc}mh@&cCW=((t3H=m=< zY#&3;YJ_*@!Of_n$hRLKwg8huhoXiCyKp;kLc~!qFnHCKPt`WX;xZf`)-h78M+3z0r zcPP=O{%ve?Q|FIWa+h;P8{BFD3T#yApG*e8+{EgTS4bH%UU;M&TPKitH`u+p{HoFn z(q6#}%QvoRJP-UjNIN$8y9h(!81Jp7JMW0e*A&^4*8Vp!#6##5sRD1c*HtF2S91Eu zy_!3<$NooJR8G(2hjq**D3jGHdRcEyhM;QkSAl(_#x28^c%DCZJe$~Q$6m|A^?P*8 zM(R>gcdmF;Q`|0~Sc&O))i&S4?uIQDy|7?wsfHVb)%pqAZmr}*wK_yP@WTdl0Q=Tp zpZ48>Uaqw!gX7Z$KM5MZ2>acW!nnbi{rMfr-}21%IkFiHzwsJQe!=te*r+h1SeJ;@-4o zB_Kg-BQ%@mi}Yy1E3!9TDLe}ul2s~S19F#5ApNvFFGBG}VJWw(KT;%bO4Gz5CPM$H zk2jdL$=qBspkKou$K}7@e0Zb|=saJu)XX9TuTueMEFLA^7}y@yrVO1j93lGa_;Mmhm}wg>rCdW=c!SR+hz16 z(x}F5jv7IT9tKBlw#f5v5>1@OX7hN<;xA8QMI3TX!A^tW_McO2aMj>MXhW#uo$I}G z;`XxXXxNhw!P$~Vbg-*bR%*W)!v4jaDKvqzRrQ1BZbd;;OJay4%74fVdQUa8;d8#^ z%)fUV21I8q!uf{|CzenDCv zv51&d1qg%{pfb`0zO$ju*;V-Ny3J>dYB8$itnJqWWs9qMmaqtF3%nWDDLzq$7X zev8Z|0D?hJ{kIFau+ATFs{u9GgAeba?-Vab$(_%g4&Rd6;$;rY&0XHAAa&#!YO7zJ z$L3`S!c@!p6HZkpr1$f^4bv@4N3gJ(%o2n84I&njP`TGS0(S6bF}q20E<|;Vj(!KF zA&mR5rYaX(#Gip76V*NLgYmqTJ0baCMic_rS&({3oanMz;WMR=s~7Kka;$~3>tCEn z>|9Mi2@Q2gm8h;tnPPC2qGnQNcV&Ou1^>ocsZ(kKoO0M|y8pQb-cukC|pbQC? zpYJ7g@tK1XJl6)jjBP*IEYh=H$z398*&G34Q_oHnRm%2!7PiBlpCq7yGIM!bZSt^a z{b`#67Cfy?Z|*5MNmn|?Cxn^O5y|x@pObY57I2{TlhDhLY1@-jfkURi2?bwfcLQ$W zhxHVGJ_WWPTwt_uy@fp+NBuSu{`8^V08H@S1y}5lia6*UAkHBCpCv-O#^1waf%T#I zi>oJZtmT5A{$FneBOp`A8XD1|M0VpFU=B7(MH`R$Udi86&Fi3xJM=!w^L=#+xJj`_ z)>hNr^-7}TdmMf{U&wg`x^b^K^@?HKQk&t7=;xoF8Ktm>)Be#4R>^*6p-I<}pRzPA zxBfPjSU}~clGmXF$NOd2kBXCp-9tgxdl7T0ZSu`vSe6CSGHT~}<+^uJr87S9PMqQ1Du)64F!OCn(&d{j z&eJb~`EO$1#L7VCBXu;VIk^Rj;AWB0cmM`D(}0f%dXSoGjF{Y41+o6dtOI!kqf}Ms zWx3Sc1cGj`N<`|JNTW(XJQ=v2Z!}m3Ny(mQuz3jRfWN3TnDP!RJQHIlAk$1KlI0S& z;XeH9mRW0765CR0zV=`0PqrUP63Wc@lS*eMe?I^Oa*Q9S*L!)JB=S15GS($ykXk1{ zqtS+ys$ScbozSf(SBIYF8KZHS^?Igt9T7k74CHwy&|8AvU#Wh1(<4~miOBqJD_IZ$ zCSgT0Bl!VO)aoYXpHaOC^+oeliJmO#PZewnc+}JD<_C!UXC&ro%|DZ^y@moo>4qPa zF}_FXE*-YH&J}hx%)0JJ{o=Gs@U^MNRb^)(3-W18fn{)uuGrKOfAYM*`knelg}dI^ zKCnZ6^X%9>m3aBRnBU*fSa{9cu-0uTL4c#+7C)7#>AU^o zZp&3}D!$caTpivN&ja8QsC+v>h^(=;&+%^7M^e39$WW(|*dElNk-?*VU3ZLNAtAcKevf!i%S;CF}nMk=*q_TR494#(nkgk>6US!0(Pjb0FU zS|G>tcm7U+kmq$M_BEneIqrZU5;KGjUnrlY(Nh`Mhw0W~cef^ItvyO53HZ3DwX1<7c&7Ps+>PVhUv6?7ID6x~F<=_k>LdLdlEOjy;hS7b`e4YC` z!%}3KGrDc`IY}L{LjXyEnRx+A0O2Sdya~+oem?uWd3e8d7LrV#=nA5F`(r;v8l}I2 zj#6wnFa9yl`%0@{Z$v64GoP5!Be1kOVr^i)t5y8jA8jQ}I`T!DT} z28j1WlYnJ>9~dFWHFGj%!4B?fH(2IDmU{(o)KzNva%5$@ukk7q<;g1JOXWlcT z^Z8;wzo>DpP~rWUXQvUz=*9GxMUf@8z5H9kA@$#I(so4!FM&8Fy+Orlq@Wr5?*dyeW(YOfcM}|!8DcyovYd)m<*2^{FW2(Hl%AN5 z7!Z4WmdS^R!ndH2UBoiMTSQPgbZT$Ol`|uk3#2{rP__ShT5K=DK#xYTz2Flinfmk$ zL@)-l4-2fR5`}yz_mEFy<{{vVc`+rML~go7B7A&kbH`}b7@DLR1F%}x*%SKEY{<;b zn`0DmzHgJPSL^Aqh?dw^aNJYJq}09fry0sGLroSGnOxbh<4INT3VkbB)s%z-U;7ml zu$9f1Wm)T;EtWx$d7GNB=RZ)1I4w6f#kQ7iYVo>RP2Uh(+xcH}5R1G+^*q#b*>Z-Bx{uJ+@O zz1FB|Ofs)>aP%75#lxdNN=S~*3(in`LW6u=wul&Tcf@pQX=zDf_k@2d-ynSUmK?;d zz2RT|(|V|j?9!6us6Xf|rO{bSs9))N{8i{{Z1UT#!xlLY@$0J-69nD~0g`qgR|go2 zM286kKn)u#otD)ywzWG=jii@jmx*~(lJ+AmMOxnS)TA^_bFm9#*b%gT`9esIEowR< zcwzBW&-hIytg$LHDixrOhLJxatnoX=@72GfO*fK>1~j0)?(9<*hb+u6BJ0(PmooFZ zg-8YFNow1LYk;mRS~br0_@t~KAGTn42l~0T zy9s6Pp>FpiD0$~gb0?DR^`$dwqrl^wjRRcwu0Sy1l zY@NCq>e_n$)`7OC3vHa;_A*x#zU;72@_B9THs@yZ6;lIS=$f33QxSVw?&+jDBtz^6 z{bh+_{>3cP+X}KNPwn~+{5(;E_#i7rJ55vSB5lQpXAolH6JA&NvOFd-CKk$U|5=Dmder z(PWwEjyyndbT~uc4S{LgL*61MucsDdo^Foy&`gq{Gis*wu-^Pvl6Wr~(!(8C zK{C+zrapI1gyMqBAMi<Nw&cd$%fkYuaG z8(=2?2HY4sl`OTKO$f zX~9Z@x5=;_5!h=&v-gh0h(Ev8b~P#|%axm_Iz5|;Io`_vl7%~z1mhD7z=F- zAi7Pm4hDF4(%N-%MH%MdxLygx`{B+~H`i|-^4!BnEyE^rpoFd*NW;DIwwV17CF%E{ zYoD;LX?qcH7GI2voax@nKcsirbwvYt*Z0-{ToQ=9j;c7oh|Pi8V_djmg`Om-u>mZv z!UEnV;m2o>bN}-Z?>i%NmV1VBA9Q`OHq+O(Vy4|@`!4<1oqcd|#}K=G$G(lsY`$QN zKhou19xq{cVcS)1YXr{PmaJyV+j}ZR>`S*-3viwZAD>vPI{7Cfhh7Oe)3SusnQJOt zFAR#S3dXv78*er>L=C6R58@X!Cyq78Ji~yb(jo8^1Q8WnnP2-tox;>feq@k+z@^VgSHWM>sDS9=Qu$T!w8;z8pJAMWXqWm_~YqWQFxTK z*_QmPJ9}0{<^x2p<6Y;pqq&eXz3UV*Tu6?AM_^_f{mqcV1Z^}+@5&}ffSnOLICU=} zKQQ1FpaINE6FW?DnE=&SiOieg(x$+jQPJQx6!q3Ge`F{wI_&;kD5@>;cheYR=)>?%~nqhvTv8fJ_4hukp9GH)@z;;-97( zHGe6u{#ZRFu|&1>%Oj-K(QkqH3-SPt4eI8yEiEV2h5k0`wc5JU#P+ky7hxV{DZ*CS zZac5JGXy5TZWY1&ZPna_)het#S6S>g!PH@MTBuAtHP&ndU-X<=$k2NJQ5J}ic zqfLJqxI!R{kA?YDl;8^ca#&>0pR36t;t$tUxNr=!tRwqfbk;77uoY1``AmThhCYq!7QB=B-3z}1|rdw(|gbV%>4=WTY zwa1GX%33~VzSX%|hh~G_BFQO1WI^cZ(>1_QADDIH@Fw=X9bfMOFLgrX$}=Ku7GxBE za}JR-4N5*fag(fe)*3LC)$ZV>o6GY;L#qCP_&=XQ>&bfgY0%)!i6Ye9HtnZ%im;d3 zZG)Z9kIGD7t}X^pe1jgg*#)=;YgTkBRpSe7lTGv5R?pqq)hRJ#Gbd{|lEXxio4DE5;ehYws=oTAX zc62<1k?LNa4SZmoq29Z7!$^d)k^5i=fFsLl{PzN+UuZl&Z25l355-FtCYw{ZcRr8o zeMo8u4G%`+z;>w_YZB5mN(THw*wq#Bf72|M@9g+ zPNv!q;{4VbaQ**8frY?b4pQw}l;ev#e?1pJGzTmp!rQRU9N_c7R%dJ4UNY&+i`093 z>KTCd6kshs+(&g!``{~=-E28bK+jfRacaNnrep)D`v;OYiv<17uLJ3ymHcN2^0jzu zI)RyHpZ$onmD;N{7n{SvN?B|A&pl8eh-whW(d@-7wPje;eB^w8TSWX@#t%rJ>LVS& zw>`FM?EBRICi(Oo`RBM~`vz0dZ}C7F8Jw5_@jNZ(15#KEN!y6rR+Ae+Str#Wn%iYI zTmp>jKj=?!xU*gfd8TKC9tvT2l)(Z;c~~v-lQ8nPJ`_?a_qs0U)nOL4*|TBW5$6-W zd6wFyBJ0ExYm@ZgJ;Yog^v$@%W~L(w4=bMm?$KW3*2A`^ttg+YW)~~!$5F8n1VA7S zu2WZ_AxAMiQ0ORASW~ifMBbFu!z4lnSKWN`Lu82s$r}E2T_>;&nR!BzQnc&>==1M+ zk*X5gJyK>ZEZM{ICC+ShTJ80dAmcuRN46{3-TJ5O-tA1@s^I$4(|u5=L?M&siLT&4 zcQnNnF|04YnbeX(gi?Nkg+AfTlh7Xyz0(J1T~@uMV~a-acM|crJFZSK}@)N6(J-K zDRNk06YJz>k6a;=iT(Ulhx2gvDvj!ZSm)waVgWT}3En@d!*K0)2oQU|0j^dn-Eve< z*=P&%(%vKJA+^h4J*Ja6r;mjId0OJ^>vk!b8Eq{d$)8EqFu)%mp`>sVFK8X|%yk8@nd8F=b4r89jttt|> zQniajvg%04d6`s(d(RSH))m$pU43)JA}@FJ>gSt~-;(c!M_bcu2gtDnkj@zBR}NKt zTcJ9)WDhe*SN-FMGP^*l^tOaC=Jm7$F?X~X_OobSYqzST(s6n(Q&&ay(ce4D6l&wk zCrh)+Oul|dg4)Xy2lP&Efn($d7Y}lqzj!{m_BOH7VR|zL=3LW-zD!R&og0>4x+*LXBZI0(ZeqzP4$<%E?HVz- zvIK5fFU@TD#{;Z6G*3U6%;pE+jG(8bJiYoNU`dg09N2bt3!Z7_j+)m)P7y;B+EPCBW%f4h66?4jxM*1(nX~f_iSqV9LrmUybSDoEAiL8&^t(=9*S>Lo0{GLc5_ebJ z+To$Y_u6j_xDZnNP0Hp;fK1&I=8g)!2GksnvWJKlttjU{hLj?{h%zEZy6w5*WGiw- z%vWR+0f&JVoy3o0);Pd|Ocpl&{YdTDa^3c>?)1WB-HG4h*=mmgpH~s$Cq@)Tj|4|u z`7Sn}V49BEOSs3LpA4$Yfehm{GSWTruX0JXnPVoaa%-xdw^yMbB~|)?UWm#A?4B9L zFo7668+hcLW?-iFsW|n~viusHXg?^vLvEhhmAnFT{;3)-vyJ(gJ8jW@SG!jdj(;yU zBSUQSK?B87;*)kj(+qN=x{c`vMeumuEP_AX40y9hJ>=336i{uT;hP1?kKD-B)piBD z8Ihy#Yhw(86*wa_go(+xDC^}C$5HN^^GiDLboi0?$fIo7sN6I3I*Ly|E)4uo+2On~ ztPE>DDRjhOm<)Is@|#Rja{mv{58!dMTcLfCcPdDl7LCGMSt0X64x|c`U_j5 ztuD=0EVxs1#cbq`ybF{PexU>JxB@gbhvJ%6%~jCt+eb$_m z(zCKWJo;w$e$ogn$Ihh5l9@G%3t*(+dA{KsI22hM0+)o1cagEp@%koyfmr2+%5Cek zHp(MXDl-x05BYLaJ(R3t6&o$04qM>kpGdZCvN$(DU_#VOxwf)ys^m~U$Zx0W{xoJx znLUZQ+8}e%g5kI(1!UoKU=-zQfw5llO0_6?M*dyu(z+7Ts2(|^g_bW)C3BuOSF3z~ z!}%`OFMN;b1IH;Ws>xz13?EQQmd3&ukU2f;BnM_bQpF6>M$J+No|UVwmcQji+DwOe*`9DACBy<&%q0Z|-WZiQ`H+ z-3cw@K2oTe$8aCO$bY@WG+8f?0LNT_v1Cq5MSk@p3Jb~=PjjEj{`FQhz&X^JPv_J6 zsc2{8WsY@}@Ig1nbFmsUByvmI&kKvHF-minWJ8J2+N}4eFJ=~(Wr%3I4&>Awd00)!ZB30WWA`e4Ef)MmE{ z-2=)(p4#Gg^c(d$?T_^&4#b;h51a=O3KIJ#NGW2 zZ6w|?v}~bB)$ErHbGE`SDCzs$6TJJ97uQhBAfTbi3LBj+u@?64(N9ufOB#(%x zD6g?z%IL&20U!SvA07Titmw;gL0Qal_UX)qAJeW&EIdG0K=4q>*9G|pTC%+vfKkpM zZU$k3?JP`4>Lkb3wMQS;QCf7g^W<##nI+jV?;Rif7;?vfhHhk+aF1;<_WBkuSdn#k zR%na=cz3vrfskjo1sD-Vvn4>4m=m_j&~@^Ci_!5pS`q2AX9?c8&XJ-{xzpW{D?AK% zw+(I9g_4BALw!YeCjC91IE2azOT->E~}m_m*75 z!4f@31tli2XDfafBS$Iy`j#k;oP5&D2n&rp2#TXdT)x9m$a830f$CHepWsOE=^xeY zwQpL};&=z?I!tv4jQ#oW=PN|Mc*vtSI!`Z};Tw*>wnU%*99Pgt`=x_LH}Vg3G^e-} zUeBGEF|iU6PGW*3KWm^nIM&i)6{2c-Lsqo z6V#7o=T7oH4iGgrN((73BU%YwXb#lgrPKMPFln`4ks%HdyR|0}S&bD*zi z>5sWx(pioRdJ8%B>DVqRp1UYN5ueY*I+G=5=YYwatvV?8~YJ}0~4&1XvQLHS?p-ssaFNw6vF%3 zso_d?aXE`}gT+xW^<%`4j9v_$%-vS*;-Sugt{^@=cw6<<-*xCnakStkq6l(9tv?AY zGEzy_4YDAWdLdY2dNN*XSDx*Yb}U^P9- zw;0DcJ~H3#qFYqY`NvJPp47DUO0=5}Hp-AvG2_X(j+xE-Wr6faznh9)S2nyRqXxc? zh=ReIdd@r5NhYkdaJtE8R^sjHj3@lrJxuxze%uP(?B}W#Uoiwcms<;QUD!`frS?9) z&=D0>oMrjR#jYz5+Q7|p+`ohcFEguoX%ogAcwn^R;Q=*+`xSD!`0wWy&nSEu)hp*8 z6g+z)McyyCji^>FByF8^O3_x{Ak%#JX;?ieTR$OI6}cP>W%%BMof&cHM8H_E@FJY& z2fLkL`TE{%z@WFD=CU@%b{93R3*@p8n@;XyjNuSZ@=r%JsgNqm>^w+dP#E?eQ=rgi za$)*2_vvj{iTaMf)~8Kq+=Ik*A>r`{Pqvw^8Zq{lI`SpzDwCP_<*9_KSnwv08n%N6 z2RPZhF+usG>zg4xt=Dc(lKTypsh`FlO9$z`80QrH>31>Rblmy29yjp7l$_I}NM^@7 zr3r55m!=O}@hjm?hz|qxgiBO;JoO)uV2P|U+g!|?O=$|_jAxT^nE$RlHTx|0zo}9Q zT|8|k-y}Rd8Nz2tAi<%o8qN9?Aq>gOaxNbH_?FD09+87**dQpw$Ec_XAQzh56<`R_ zV%(N?9_VUc{H6Fv<(7M)HS^S zt60Q}#=K8t$ROp{JID-BTrp{I@Eime`yjsUP)Y@o!b$yg9+w-dJG?t?V^hW%QI< zZvN%5TCcuY*xlak&R=HnhJj>*YUZa*g*~IJ6rt0JvCoRQN_aO@Rw-8d2Dp^VA_*&> z32;Y1@h5i>Z5T8|SzXvv-O>04PQzLZ*lWt63Us1TZ}cnPOm{c)1G)sg%7?G!&*cVsjrbUXwOlnbHg$*lr1 z02}TCSn}~}I?VTeb6e9}8Xg)wC4u4ULrmp<8wIC4#`C$a<7S&UPl|n0WcD1wXhfL4 zQ!RoB#zPeEuvT^MoS{7ioqUh0ZIz5?L?Png=-|!S6oS~*5kbBI89a(ANx52QhUq&F zKTIyId1qDUi$9^sNR-}BM z8aS;MmUzMjq?8MfQqezX>MJIM^8&s9rdwy;y#D%=^GTWn$66PVg0s;)x%b%Z_fqgK zHSSuI;2@KT@IJdWNlm~bP(E1nDp35cVh22@ zFUJ2BPSbr=AJlS(YY?#J!`$$V;vbR6MTa&bCAmJ?mGd-&hQi zSb@ozL!(ai$$mI4lz}6nYz`PY68pKz#6tL6xqh|~jMJdMGkyG^TII9pB2FL$AkfuAf? zTs#M8SQ7uAhNT3sl;zmj`e7V#ER0&==>=-4k4+Z?p#Z5B`-*!_9L1!mBPHTG)OheH zIu!$elkq7}trF#`GSq|;!acZaZuR<8t*#tD8{Fpaw+`rjj?N%gmM6R+<4u2sCj^l^ zm7Y&>8zBSv05#_OZ9_$RI`(~uD#~{}VNWvEJ~s2PUJwahZLgIjJ?mC*D+`x4@Qi3A zFRE3ce(HrQm=XHk{yz%;k$j4j!i!wf2yA`}Z$*zEY=1n}JT??LV>_KdlR_;3)-h$MaG!f6Zb(ZfD-m_;hJXV@uvHjK0u1vJ+%g5<*eNrxWrD$5~m<7PK#0#klDpkJZUC+`Zd@ zQ<8ZlfMqVWLBspNvsoG9ay{Q)lL5FAPp737=Z8v`ytBY67!qYvl?}>`i2fqn!qGxa z)vE0|xvP_*lPLoW^-PFWHUjf&PGgi@Gf$xRf2!8vid|P;$Zb%oXg5wu_8bk(e+h3A z=^@W83iJ-AW&@G_s4(rX%93Q?roJE>SVst7XQ1FGUn|3gpbZHDL2Gw?FbXXoNNpkR zIH}%jbcb$xFzf)^{sWuHa}T+QS&p>2ciXT=^I^>h4Ie->>5SSs8n7mBB8#-9YjoBj zLXW*hUVXW)eA>49Rwi?J$`)Zf_z(1n2j}R+kkc+8eOhmNQFfAD=hlkex46dbWN?n? zt}P8{C7OL3<8#WV$)Yy~TPYFm(od@IP%q`Pq3w`i}H9m9`ASV6?dcxbmy9vko z2eKv7p?3Vcuw<8hRL{Ay#~&u7BQn+Y(%S&D?plZ*5f|ME*qAz7l{ zzgCH))i+3lqaSLZd|be^?HW*z{)b!Wp>1V1cY>p#C z%$~WCTSgMeUM83OH99RsKavVG7l1i3(%>wifT5EpZ17PU9fka^VW1)VViotNdjRFO zc82M^>|t50aN29~_6*gm*0ULsBERhn50xN@FgMk1y~Ll_AxC#MVY82N51;;l0Hh}t zh-jpDWC7%{EC*(|Szoon+SAY6s;AccuOyy#Hl^aF$~!_Av9@k{fkn{R)vxyWA^@^A zaIthK$KKjxa@rwVBVi<#?y|dy6HnMzC{d%A-}Jgbn^wrYz4|3|RnOC4w_se4T1{5= zXqUuzGv~zN&K=EYiEcrbT^G-X(ZXYELlo_c4%%F_>z##`EK;AB(c|D!&$d`YrhiP5 zL3>mlc|(jI@*eRH)@Y|{Y|*JGsv-AHSEI-Oh}^8E|1;gAJG3k<=eY86@6`mSNkF@m}By%vq_|q0~)LOFW>p*`-b~2(H<2yOgu6%p#cOrft%v&zZ z+;8`2!!&^5{Cw$*=qgcvwl!jnVZO$f$Xc{W7@^6mRME%99TbJ=qyo=O=)NM}5_D!n_5Kv92rVwvLmh zmEk}(bNWC@S0~GLHc!ao!WKN)7p1*>zf+AI7;H^zW`Dbu#ZcrnNx>2MF{tbzZ5;df ztE+i+|3q**m7s64Yw4yx$UP#&nPmSOFB#6rwZCj$ozW*^6L$Q8pU&1}*-O~MKcNfA z7lL$KF#}}w$+-3J5>t;)-mjCpgh+m~{$=6hulut*Hn^4TPyPOL^FN#x)l62u{zm_* zY#(6__AQ3A4q2(Z;@QzblsytMqf$5BPr8uX*KQ95_jqOKg4gKe4W3YQP4Vmx<7#la znx-*<+ido`iJ$nm?FCEx*ZJs^;5+MkrH2@(3oorA5Dj1zPt#fqkmRycNt)rdCy{UzO_#RLcN3>VFR=`H}S=8(HPt$vt)4 zF;1l^mh53?t04K`?sZgVsyfxKIa!W=lY{QDIO)&BFH`)2^b=B17SQAIt#vROz^iPL zWc}boG24ZKp#2X63vs-!E23X}?hTiOd>{3w8u0Buw>a0*^N(vwR}Y39ys=s{KbWUw z_;U7n*IZ%x*R!Pd<2RWxOqnUOw;F=C^+TF7CvGod4KsvehkI_mz99J%a&|`vyuz<| zp)S^l{ednF88YN~sJIgH=EH7#!<;DIv%PDBFSgP}+oSsJ;enN5tPkZ-*8+t5^=EY1 zog}nJUW6ZS8F9~-fbYxy1U6hgMCt%C;zm7y(J8xR*^pKa9 zkmlBEUOV#+$C;^ zA8-S<7#N8LFH`PodQFvnC>rqm8F;6diyM@~DA4&w2hEjwB7bKVHYSx50}y!KF%N*D zC((&$9tTQ1(LbJ}Rfw|Yf5Ygioh0#~zw%pUeKks3;!$KDF8RTSRzAy_ZL?mpj3zk(1$wn=J#x)kgNV z@p^#K3swlFchWzQk8Q{L=K670Fz}5`=qiA1;XepYVG%{HB^Ft)26%6dr^a6SHO5Xe z1}yuIMVt@E%tp-|S+xTt@6-Zcm~e<(!F9xZwD(~UAku1*rsFS4#i-t#Eirg4JBv*> zM18*BceZr5Er0)n4JZhjRN@~A9%szf`zA@>uRU!A)Df}wqQwy;o%LQ-Y-%Ju`sX}S zEx#nCTg$Y0>W(Mj@*6NP%?tDr(ibv&Qf3auW^`q7wY{2nP*B7WTA^I^ZaK5t7riLB zrz``k7Ax)mrG#48izAljDD317fK<8YB=kq>1rpM1&iEDErSr3bn(U2{|H<1s3q%OO zPl#0g1I6wHcO!HXu2zZEkjcBNxmo6F(Tu01q+iU(jIdg3%^wM7fccyT8Yp9MWTs>! zkvRi5#*b7eyk6B1=^|$e$LW}Yey?vD9@U>#>0bU==>z3T>K`&s8a{k{WDf9tBusQg zoo$t!$d)RWJKp#&gpnX)qjwtK>)1Igd6V7Py-e_4mw@9wl-_%9r2@aHM?4JUaa4{S z^2&r?0=%`fs|efo9Ab(5E_w@^6QIzk3#< zI`t3qZdv>Xs%ig&*}6sw4W>KSB2g6WO)&*cZ;Do+$~}dXPV8U#V=Tx995cY|LVex` zN^QM_zO;_oTRnW=9e=wIQC0w*6V}1YkL@$24O%0WlYj48lH$w9Xb}5-bB;gxpA@{~ zhwy4%uy5+bN2Hxcp&MicrsX{yxe&DX5J!-xpX_~Le_{iYG_~2NeHjQ z532~0o3xW!xJ_Ldl#eiZAL4;47x}S{-61L^T2~0XD>AccsFq$x`?+Dnyw z#|J*|j^@=9c!*;AoI)ed;@JQA^yu7%GvN)Lq>Y@d^uqK`*J+pV_~7!Vm`+C}VT?&^ z{ofzy*jWP3qEx3&LWo)38|XjPmB9Tu{NAHDmx<{%*h}_6VfB1}SLOndg+Y*Hb916z z6){U5I8@`L5F;e#rEp4uN57FifFH4Q8xBrY*JHNpk?1*7z};@AXZPPLN#a<;!b>s? zq}06c;0q`-r@@Y;1L#JyAnelzM@c4#yt7;+DqH`R{bAQ~ED;w6|E%S!!efwf7@e`3-IxAM)M4*>5omhd&9^*F6yK5a3HHWNR>I3u zYPR8H!4%>CU+;V!M0~QYgurPPfgw8B6C77iT@RYUA|C`R1B?9h)S!!d!j1fTv9 z&z3fE&-Houw+z=eY-`WhH>Y<+jFQ z!MkZ%eXz1F=fiJ2CoaWVPdlpjsPN?`AWW`~fd&`TqxETe(0hh%&isDpCGm#H3o!mO zfrsctk|B43w*jr9OCZayjhk(KSnWv9^d6SVwRby{bFMs7qF5C z$WamOlT>qjJRz!*DV>(kyfxKqa04fJmip*cJ{5(ag?tfGaK=WE@=WfL!YX5Uj*YN| zW2nkGPZ0spoIj!=4ke;Xa9{5`8 zLzxKIU5WfqY4Vqr^n`yfl0+9WKA?F1+})|jd~f1vO|*q>C50ho3P$Zh?t%G;07sY@ zx|XFL)k?3vYdD9Z>^iAWg39AmF8RMW)^dcJ9SlgE3M>jhqt%H;As+Xfm9+Uk7E>51 zdC~h`kY+V$A0$YKncbu?Is-j_u>V;Z!D#E3=zv&oJ{atCNdu-;B8h*5WalWUe_iBmm!)5i%-!cDM%3ZZ#{96CmS&xc;m+dI=ZC{^Fmvms%8haHxUt(oCE zXB0*j>?FP(mp(Y2;MJYPibP~6Fi~wax zGcCW~$;VM>K?E>P#s?=eYRBLA!hd`@xjS){d+)5FLE#C4FWG}N#;<_T55)3plN*8Q zKobyo)cRk5fWX4>AH}y{easG*P-&T}&7+Po227 z_wxB{%-x4a2_3ji-SD#0zyI=y#);LD%d*Xfpp+EhbFw1scE~k z$KxO9plrJvr3|e>02pOg<3D|e>Q6b;Wh`;Rz2%bty zF29jFl}v3=IPkI>%+G+dV@ChEHgcE{_3S2OJmaNzDoMK8BB_Bu6A_}g3%F%(6*%!I zeo6oCv7|D-D)u{JKF0{Ow$Q)2y2Ufnkm>}WJRV>^5*WMcgd)&jo@utf+qI!4ry_faG`wq!1blk%M+Aj~s&z3nS@bEr_rL{5Im&*Q=D>@GoM!)2uCLiM5y zlMnM^LjfK_`L(dY$NgTg^se{9!Z^SIMxu$}ygkvjtn*PFVCo#dzat4#2}EhWor)>( z%H@s>eA@!Q>r-R-0-A2d4W!`zREp*qRM6AIHEtjkaI5@ZhMawvV= zbAAy^F0h~@Z+BP)ZXe&9q==9)t~D$9TY)Qu#%!;rO?Iro_7mx2U8aEGr^)JFpK#0) zU_DsAT=bz0 z;idm}`cWG(vougZS;WyLCB;>6;ak}s-2xClVQyRT8fC<(k)`5zfyfhsqDXfBfa;-f z5v3$GoXoI%EzaS9Q*QBVIo`{8&p+}71r<}A0i zkCbmRk&_#t4p+W?O1IvSbqwB00yp2p_9n!)B9nx5y4$`kxzB%|o2~^-3SW?Z8lpwt zjs3>Jr~4WRdD~~OlP=Dch$i1$#(wrUbQzhh6V{dxpN-4tN&(6Tqeqj(ZWeUz3 z2)aoM#hvHAE?LPD*o&Tf{QX7EHNBr1dq09~agfbH{3hG~sI~()2uT}D!(w_S&kuP5 zcSqX%c{ANyO!>?-|7w&MJg-L<^il19IioNQ6CWO$!w{|QLsQ%+1CgQA;ff5fJOW$S z|5l{k^A+?G(F_{aLhcL&(-4AT2_W&Wb0m=l<1z#MC*y}WC*{Bwo293;;NhU<-d$)svD$bc9DYRdULV63R!4O%Ld{{ z2@AykWO2dX{Vi+*g@K7N+Ifaq-r8El5F=H8kJZPiFN>bj-x{IQchT4hvVAa(>53GT zB5?>$wIX9T`M_Q}TEoDmDPS8pNsZ9Vi#>P(=?L>ejS1Yv3-bjqPY1hHbg!Rqoo{5yYMI#KOE3leoD>SmY>LQ2zA6C6Edht%B#yV|!8HSkBi! zV|~hDd#nDE_?yw-pvUAs0Hv6KIU`q;9SM}OfOG4zq{-oeM`s#ozmJ!rv86&uuM-hv z9`l7~3gmyeecUlq;LH9oSG_ESl?iv9YmA_Bt&^*4oVuRAItE9ByfxtPCM@gCae z4Z~K@e+M?PmWu3@S)lMZxRyecC_Hs-S@`fEiDsWi*o5U*&{{6>%h(qNb>T3M-~E0f zA^=iDQ-M&;m&XFaG@RL2xr7dLLh#L zphRRnbX3F&O;^;&{X>p?G9UWq*1+8L%)d8SVzAv)*F1&vy*#IYYt>557r?vQkL`#z zeMGL+0_UoJ8t*p5 z*xwAvvs`#Sqb;upsmKoLSw3k}adk{2iX1vh67qm)(H8( zx%VCpyzbJEE^U9#s=2h>9-$Z&Br&NL(J3D7i-xTv<2!JH$6~-PNh7@8k{3X87sm+y z0yCe}lfcDHZEC^msP&cmf3GXIf0r}@`FYZC;X9x3D(Y<>nyVdGnK)$K)8GTwJxZy5 z`}!lz;ulAaiY)DR9ej{kn^4Pbj}W=q9ah7#U$-=_DJTz!dKN*-oL4U_9(b8PP5NvX z4tn#&OkIQS^IpCueT7<$tbTd!CtX`j)g4O82#LT7!nSEH3_kRC!eY zlDI~z&YVcx8QO5H>Ar$C*Uiv2V97EHpU&@>j`SDcA zwA0LJa0i#0(q2>G$e9eXbAB06mg z`X@?fdAvv;@s#cpv7$d4mslB)*x18|XP#|feyYdnt{P$~v(oMY`Y-?4h@PA#=Ci?!BP2_5JJjdUE&ZPqNpR)D(<>b6E zUgsD`=|TYx6dLAL7?lWOIW(7vz2-8Xr3EY1SN^x;ExJayywg6JwAb%tCnCEAe#fg8 zoVy42t@PFW9~FQy!7{4$&19!UB%VItI-~n~?dbz5uHPbG#p6J?4;Tdmdem=$|5DZf zzjN$AeE+gMp5)i*bmGMWV9Ph|!^#5VmpDYsWj2RAm(@*jsM2D~te(G(KvcAX0^u|% ztnWYauIj`q5v;mizV<4cc&^Se-_)I)aq9(LxmOdd#<$9YVADE%>y}w=y0n`Tjo-F+ zToHXNnwHF4p4y@3Q+fg2`i2J^IkL_>a-^;2#3ve!4{N=LT|w^`$9~i;(VSIGyeXz7 zUAZsvQX)!N--|c9sfh|8nZqL`w3NL1}NYZ42%$zcwn^Nq;OotC7+!5`25p9yt9z zbTerzy4O3c^eA|`d3-=wzWX`<^p_Tsebw(VJuy@24G{qP%b&l@Bf=E=EF5WR_sslX z($S;0I|{n^dLFz>D7Z;|C_xZTxa${wi6q<1OOkE1NzWC~IM-;1tFN;t+HpV|*4K~> zDtIeV4&NgLLmqiEU45g|Y%;A0(;RovhAV9WjKWAZ$M?#th-(O)=FMIz12wBx1Gsr} z!SoZ>#|A3xa*g#QGNjVhQ7IE~@4;o`7X?mooGYX&&|acb3~G}W#0?E>0|xD~G2goW zj`ThZWHG4mTI>Sl2SOaBZDJMkMCEQh*{O?G3X{F&Ixz~Zz|>Uw^>4MqnZxLRuBsvj znZ~y-dxp@$U!SD;KJ?wVsuO~DCG26PFq=d2iU=@u%bid=fvE824(vLOY|{SH0Oe93@4+UA8x~Zk&|3c>P`Ijc%|@&TK=-wCMTV#Vx=ZUzwiMM~ zgGaz-&mvsi-iQ@#6&53yHaPgOS2D5e><2g9o2J^1jlbyHPK9iav8DI7w;!d$N}yhZ zGBolEA${;)dOGYY)L`parDm#F7W~^N|JL_eeWM*{*$ZC}`18km^CChqiX=gT0H&V? zdXG?sJ=pjkl{s#@RphM-`Gxn)gt$9)La=u2{iI-|Rr~O^A{&Hm#P)nFb-O!Ct2g8s z(Xn4VXlrAwn!X1iG(Ae)#{*pK97N2q84DhU3G9Qk;6z~}`WnAOdDs3By>yC8L{l4C!jd^k8Ztuf!#bC0-OA6aA9 ze7|?D7Yln!Qr~i7L=j8}i-g){Te>|nkB{nZtnw>&hKoiuO5v)e3(E}vFS|4$wQKVV zbz-d@J@ncW*Hg0InZ96BqG>aMe$h53`o#bZUW?OEZwUS)xwtMylq5^ouVmrY?eYO2 zMEZ>;T*=T^V3e2Z-%A@QgwT;)0i04!l7ux0(vk&zJlOTI&no{~KXsRKmp?%Ltzs_5 z{H7e|zt#@2KQp;;;qW`sa9CNkczKP>`D2Qi9fFx%j{RO`^3a!*O9aN|D37)??Vvww zW09DR&o?8VCH`fPa}$L-CtcKF6pugQKo|@RfFaz$z@Xf?8%V=IGTA%;r_}vc#I(w_iBMNG{OXiDIJrfU zZ7uL+me6vC^}o2s#;!V%7K8Qv2JW57L`lkO3=6rUkCp&O_cjdcgbV=_CDy?DP$Vxw zP(Pr-?=r^;6aP)xN5#*t@_oy+PdJhQKBd>@V1Xiu@T)yypP@1!p(d<7cSE_))$z%Q zXn*l0L?TA?r605N8~l3gZ089xeZ6+^ajXzI!lscGFNcCl=pSjRZ?QBU<2HaJA zA2|g${P5vu^BeYgHrX3I)Ya6DRm|K*cz&A4rqG;oJ?Rd=+PDGbCwG7(36uC*oWW59 z@qWxS9>&%uAw=t7K6oYUwNAr3XX}Co1|N%wBh-AsN*&L+?qEQw6jXn^=TQZ=wNDX< zlKQHaK+g6L&e-(n6T+>T-()s)T6cXBAmg+eyeviHmcpAX7Tua#_ePbqG&L}fr-xHX z{h;|b2i~fb+(aXjK#Vbn8Q$2J6J21>W+V$2PnJ}s;-Bz>6USKiZ6 zp{cf?jC_ST!aEDDRzSLDnBy|AOpxFu^i z7+gN?AF8z(=OmzFnj|cXQyfktfg?UcYKCmX^PE-z&UDO)Ert&80}1bAoULsL=+S*9 z-@1nqT5{7nftKBo>Wh|LKXP}eq(1fC>0izsfHC*b0oz@OT%j$GEi{TXYHFNmNk0}6 zMy-jfEwQQ8Lu-TDf9@|YSpdJ{B-aA;?C}h!=mLd<@Nmf{b_}HEuQ-Br@SZisCn4#$Z+NmN@NM~&Ki z5QZzN{@lb`U!zQrKarTwLIU^U=Dk=XLWJ$m)-i|q!PHu2fPX@u#$H_*xHzJDk1Ugm zVct&3Fsiyx-z9>yd$e~0NiTU=({7?RuHZl`Sxkk8L!Z{Vwi?$JP9==)H6K!sk?6*H zWL82}P2M2wI_8|St(w|gW(Hq8;o~scd9}g?n3%MX^69(2 zB)<%8d_c|-SnWLD9ppN(YG&%r576p)n{88i@#c$>>f9hKU(Br?Q6NU>b&ol6@Xw!DNKSm=P+;N3f*%3#)67vH@1w8Ic+pts^i;Cr}oCnT* zF+ZZjxIH>&y>%k(fdmz^fWWY8#==AO*5rWki$V&$|K44qCXirnwc4NnFj#SimUE#E z?dvVG2!DK3_%nr_#yu&i#cp__$hCe~cp|LL^DT)6U4e^k7%^pleAzUebxWUapR4FP z-N|tcNESsoeSE&f2t1Xo#3hyxYzZEaB3}}~XU!LYZt44D=+137fmM3@y@nVPGaC8E z?tXh0d_gv6J@z@_Lmy@gex4t;h0m5X|)kow4r~{B* zT66x&a_+g>1POaxQFwfmuda(%`kas4D%D*&t;#RoHNm7X{pn0 zO8!g{_H}A8cn5PomfW<=8x`hhi}!a{cLM2Q zjOR##>;%xzT`nUm#EGcJ7gT`H2?_tKGJ(H7mRFF6ZM}yzhgZ-Z`pT=1QkVAYV}L| zvh}@GCPDWl?|?hRu`Vz{0?Kdo4(z*$>PD|VENJgdtGsxZ=Mz)t)>~dv}v76}bBF`6H4%(egjLC^~IZ zF%%@$fG{Q8uk-fTv$|Y|M;jk5?%-?63v5pSOAl%FE|BcfQ7r%*xE)8}U4JZWVQVOx zPxgJwFG?GhHw1!qKEgY*?1X!k)`0OMu@WkRfv-fM`;w`LYatPs^W4O;^~zxbSLycu z$XOx29O;pS7!ejijU74v3*ld5Aaoqp`&bRI|% zW_VGydKohJs5#jJn2x;;7<=NnnYtQX4KQj<{=Nh9+N1zad#J?rp}(%j56qA-%~}vsuVN#0kT$znF*kw72)LmPluE1Auu)mZ1#SLdVoKEe-!($0< z-i+N_0Evow7eW-nsJ{-V(|k{2xEzk`jaoX6z}N*Q^^3$X5dtPhs)FjgH?*Cu+R23U zjqASH!qcg4!P^5#S0}d-ufXo`ba+d|D}WR~X|kwlN4ubHT)cj8saExPZpT1Wux%`Q zqT?fnO|-4?(<)i`=gp=+w;}0U5kvW&87DnhP|{p?+hdbj_e8L+?=F%cX|ntGn2Bgj zneA_LaTtyEttRYii)~9A%KflGdESuJ;fOWJ(D-KcqmQAz9UFM0O%a=IsAw-xd`

    620 zDq7f^Pz^CbL)U>NdQQmP2BU_@gf+3SH_&MJkZt%I5jN*}@r<>%cAp}RtNhE8L`_dV z+z-l`RyuLSppmOI_sFTh*yVpzEHO7%9mOd(ftF*xK=G~0!o{bP(yvdxFB8A0OlJS7 zH>EHt16yT=oTYt<$P6_j;I}vQV%3zOhUtQQ^2}%u&x26CA$z%Z7uV0{0sYG2fOt z$_8%52#a`i9+QKjdD6W)Sd)|REz=>Mf!C$?MTpZ& zq}Y_Q{q$ggrQF)Tc?<8!dqpXcX?N(d_aAEvZfM9v&2juyI0||Aoqp$UY3u!V*E=&7 zNv~6arRjIB9?s8x*UfJ$3PF}h25XR5yxPQr2A?Yp?A1NhxWE_Hzoe~;mwUm+a{P>2 zcc8jR?<4Nv-I^bH;iIc*Mu$mZ&D3|r!=|t3@?OgI2@M&8t3NIB_U)+itU7~{xZjuq zb%v=~z|8cW=8NpKP0!A~^M;8}x6yaFGO}1rMZ+3^LPe09KMo<%SKj?Fhi1*|Py77k zgVoMh(T2&<54G?RCzGs}O7FEZT#ZY_^>jX|j`FOEU4#y#(jn<%7NAS~JjookSKJV- zjP$(Zs)vAI!BL+Tq9%xZEOQj}KPvkPN=};y0e7xkLsE-Q zRel>if@x#B_?hvL?RyWrPVEW9b|+GB&fXx|N zzQ{LoG92ktC4mm!nAXY0+A^=~-n?>GW3OmF_HWAYM^37W=@>KJ%bBxPv;#7X`BGkQNEC3okPRYu=`uxTJNPu1;4($UD^+6bb0ZUb zVlML0oVbKur*qnyT1cc$hRH2n<`>bws9*k|+8`q}YA>v1I$e|s=5ihn(9`sK+JC?c zuLM3U{H5{ROz|tb@*5a`o@uJ*Cv>JEYpLhF%jcX-cY(*z(N(LZZ@cdqO3>T0CJ6cX zKqde5DWAvU4SG7e$tfU?O$TR3)5~@Vc$uRPvTV=Uoq_#h$3UW}PfZnN6h{B?rai&b zH$AK)$Mit<2mh0j*d&6V0WSadVEwbl$kQEl$qxeJx5{7cNLs&D49*Q_xZdUd6PyP_c+%KWEL6rvpDM>;_{#K zYGdR0VTE1GA%ijBT@lo!Y$Kqye2baVtjtyM2Rv1B5~g)G2EJ3Lhjb_XE7xCJH#~3P zCRc-9(&=Lsz+51HlG$R9Q*^04;Zi*Zx>3UMx3u-GRZ*=Hcjx(V)jDHXY7rlPa40{FWGRU#)9+L`}Eroo!5NLORGzHid@SYXlxNCrjzfp#)h6$$#(K)Q40!fyPs0(ed$G(q4#DmVC8Z}v~7mYYBhF$SZ4lugtE zvdlqqAZe%zR_X94wfq~TLFY=Q7A^>A$P#B<7DDuZF z0Ma1wOWKN+BqyfND&B2UjL42+ARvbBD92Z)g2Xk6pXf~rrm_81PYp+Vu)rHsPc|=u zXh=871r(w5I5=&O7SNy;BD$lZi3K*xr0MT3U4cANc7oIv5@4+fQN&_?@L$V@`9h83 z2g5qxS6qE9P--00iv!}q=+-^|{==`eSrR<}B9!#^S?gj05S`?R{*9RCj^N?H#)^Ge z*JV=LAW)cn0Bl(xk32yeFki;>AJyRI9$`&#Gq;D8GmTrBvRsI#UHbM9Wd9KmtRYggZl&zfVN1Htta zTEAm`k&TbVDaKJWV1oe^c9I4boGAZ0eJGuN^T0M|_kE4FKHBmnG~P{I_DXhtr02WP zLbwR9mQjASP$Q_@97TjA<={E2mYQ;ZZUj1@GpWV@k7hgKXd5fA6`0mz00<8hhmBd- zEO7e6)usV96GN}UbH(eA+_vUS2hypejvuHW`k&p{GsSPx0Bcqnks)I(|^IZiJFe;xW4>MhWt{hqU>J!uF~4)STV6Gk(bt7Vz|pm zk8OWF9>0jiTlUTL`ygW@D~{QSj}T=JUdm9jB3MFOSMN7*OIe>W-V2vY(E}N5A&#HX z;Pgg91b%J?<|zX3tu5i@go^EvfB5Vw$Mx#yFL>#;4_t|;feKO2k3r88jvf#d46t@j z$e+?*1%S6%=Ff*~kR7dMvC!M&%wsaWxOH*R`4Ru$t zG$sTZkDVUa-shWecUZ3}z_JacCpa;kry4t9EtBPVP<`1I`(1JK&H2J!awVr*W~OF5 zRVNM>1)yC|d(OybZM&kzX7g%CBT9l%mi5`xy=!;h<77U1u35-J9M)Ztm(Of-t6f4f zsV)S$5Vk_uKcY=@Va|}BK*rP|(I17pep6}=uXLPvEq$iu7hAehtHB_61^x`{_!>9- zv|w6M(5Gp|OI_;m)gguK0Iu%OF1mC=(J~1`rYj9YUnr-j@X`j)C}W2ZeSA^VQ1L;uQ#d0LBVXnM}Z=eIlKW#K2>g zD*OGZFg_=cXq@p)9sM96Rdcg#Y1ottvWhPNj%Lo=(bQ4nP9(5Du=;HnJMy(3uB2C8H z+y72^ylonW>l)R0hRbF|3eOvT8xGX_3DA^ckl+@}*Z&=Dap}i8^^p9+?SRg0?xxv4 zJ{8;gr%@C-4Z!d=y5ir%xTuG zzk*5U!q%>$TYpAx42l`OdHXNLLRw|y+%xWfRKrR!NXlh$`W87BgKc(#7+tqfG~0fc z3k-Mj@l9L19`e)MhxVSsPAha<4a00HHMkR_JcoIb32P!b&o`>!pe_P$t#f1jn_y4@ zUW8xP2rZk<&#Cu<3SaA8QY5E5bf7c1voyQ?OVIqfISr1}*IX3e4S!BU%2%RJEB>i3rypuqG7`?4Kf*LgTmlxA#3C`?3_@_hi|XH*t-A86T?c>s=fHqb zrb|JZ6*N^l^G|&WbmnfLd@Vy5Y;x7xyNzhrZx67)6b+7+-7P(lkI-CvSA`a$(Zwai ztBL^X7X8?#v*%HkZSyU}%D)EM2Gw7G%S7?|wM;1>@6XALwY4<1q@ku!5|^$g(k?s@ zdPUv%8+fyX8WmYfaIhi#37=Lj#__e`;lT#tv~-?wlcWfYA@DMXa1IqaCA5L;_TP@a z2M+ho$rIXOLEKX*ZY||VjXF%fX2GffKAZmS`b*a+eb#%w4^!|}v`jk`-g>5Ca4XNz zB&PqJ5>yh)h5!`4hcS}xA6eCF$Fgn~%`j%zWEYaKJ))^e3e%H3&ipOX*VJS7nywKQL0QIvD}T53sG)$n<5v$)ch) zg)|4%oz=w@Ukp;hF7!X!Mu>*~W{GKl4SF3F!w6ElKg%>vh>B^U@igRpM);4Vo#U@r zSHGUUo`=u&rAxIhtU4gI_+s2&bY0P9k@`^(XRKc;h*_-YW@%BPTaL7NVf5X7P{?;J zX zPKIBsK0VK+*VfW}yms!;Tuz}pgt~jYBgXMozJsGby zeir}S=`&`6z*SmRWC_jC<0 zF2K(Rd7Wft<{;sG%d!4R$%EZLmcqSk5 zFnK|Q3jkft=>9+GatLnJiX#_??tl>$8|kFH5VGt<%{mG{*n z%$)0o!Q#nmEh1V?FKISDlqxGQ9L^?JJ-kkHXNyt(>8`u$%FC&!z4xdGi&`1Xf)VAi zwC|~c;>z<}(6@SVqnfw%8pSdfb82-)K7NX8f`|*67eCTNI3wyFgh-!J(~e7Jn)^v} zZw+gd6mz|KS^8;Z;#cI{2M%^R+8nxXyhXalk8+mP=&)(tui29<)5VKA0)4ul#|s3B zR6OyK0C%o`%rd}CnO@z+d-6UVm87=6!+3WezSykbP?&Q^pwX(ny*a}!4N|+pL|Vh$ zvt`wKzhUw6+rRLdT&2Q%3oB9lA62**`pHZ2qMWLduV>70kM9mtRbdN;(O{E)7ujDY z{VwqM7(2E<#sb6$(Z9wkcSRZw9SipYkl!l~WQSo}pZ>v7U^f;m)7uID>Z1jvEa}^< zuJ73u88i7Ai-{=vSW?B*xK6m)b{^Es}MPWnNot;Pv zn`^G(;S$Hg({^`;op;S+!k%}!Dj{)8>TgzddLhj?%r)U51( zU6*YLh3pEZcz&h>=csn1E#54}1b-B6t*96oeRw4HaVZ}vMKHzH3@ZC@1OC@wAL9S0 z1Z^F-h*#Gk!KlS3?aypy_o-kX;-Bb_DcR41n;MQ)<>S*-6bUyI5<^#Oe8mFmN+9~v zXn{q}ZU)Ro$khw?XDMI5?e%&~&Mtc^?9Eu-eeb^tKbviFPTiioP2N6DvHD<&)kDWr z;jD%>RGY|EVUzMahCe97^^h2r8*AmQ8kd>cT)?4w;f8|l`mzzjn+!cK%OSxSy(O?$ zZ=gkIru>7x8nsW1#!PzB;V)_iZ!63@&(Vn0g*=@4<=eFOJC}!12qQL+Noo?!^SaI( zJCsr00~_pb?nj(3Lx`rh+;(CVq}_3@t|8^GBX1{`Tx@P2F#G%=FIBwlJra zJ*Pfgf3mg#+won@VYKXM7xSX=_Adprb&GNCYSY0K{n|b zmrc5ZEg_iwMQf^>inp@)Ur-*vJzb?b7ehVp{y-^mTbN_u{R1rMF zGt7_TFxrxQo!EFyw0*W}eLoM|P#ENXTUz5vHkHU&2Rvc38JrL!K+-miHw7G79K~A_ z?EUYt7>^Xp6yUFTJu;zw*r9dHnE6kDZvlaccnzvXE>tDf1r9lJgRUP=M7;6WwnBR= zd`bU_WPA+fpRwofJP7jYdi&jRW7?a}VP?$Q6kWbtC{dBB(|B zXOo-=Rpol+n;Yv7d`k&p)704GlaYCC6oIUyYg-ej7_qJwI>5l@_s*}x?uDI2%)s=5M>ffpVs9xEC zNBdIxhP+N+s8{VQWk3VT??6J(kk*;HRzUP}RpnC2uNm)P*pLo7A(^PpyL$8NulHT; ze)EkM5Cdfr1k7V<4>XZLOGemp@;5g!0Udy^5t59*a##!f5_lV<)DaSE@ma0&SA(zC zxQn4+d$FyxT)T-W)W z6Z7*OM{32PCD3fky0A)B4k2Cdx>V-X?YXz@2a=*L4SM12-&0s$vIl19U<+%yI$@3AK3cauH)~% z>EqblGo629i@P|%hjw(i2%nkM@V2280bUv*-*0_4DM;a)ghI3uI*=h|p-2mRi}=WF zq5=Zd#ooTDRWg$NQT_56e7akNg`~9bhs>p|6NH0}s8>sUDxVMJ2Dcbr%XrVR!>V?N zQ~H3u6s22~;2Fp=qI8j^`-}cy{AC@JR#ZM!M;edlU#3yW2@ufbJJLsuTg<~uS>gK# zIq192{(=#aw2d!0zV^uSpiy>nEZ^;=XmLdLhAdCCvPBaxzJ1H^g;u4L>cUWOooE!rHh zbQDwGD}|Myn%l9DK|is!JFps{iVf zSgdz7{Mk1jASqt0uyD!ZP0A$$xw_)0x=&@uH0UM&QaHN_j%RqXf+_v_IY-@UYu9AT zg@-Ot?|E#>p5OvgV6q5(s}s95!RbDsal2;qSbxWEdDZ&x#iO}zcdzsK3N})0g=tjt z8ysPX8j!&46OxQS)0RPke2+q49L5Xo%to_zspa8db7vba8&!(U8`yjWV-0xTNqC9X z8tS^%wuIZ%1Hz9H0un=>e$~3;&lz^2ew0YEjtM0I#34zauCk?sKj|p8VfBL4qViB; zvVa|FI`8^7UOYR4*87yS+ia>87*mwiR^V`Yg01H$kUoOG8Oq|a`+P2pSIpbYQ)a52 zk%5kWJ<_GMr2#l(GKA_p+_U-7ca`At?f1d)@A?3L73-(FpO%{+jws4mXPGvr{;;E$ z-?sDeE4F^W`ZsdfG&+qx3^ZSo;#@X4P-OqF1*LA($ulJ_{eAIj7_dvQE zWJ|)fpR5B+E=9QUv!$olL(DA@mLva{={3_!e#>~Bw^GM>g*L>4Dcr`IEN_0<%Dw{_ z)7u;yYg%U9r~bmD466WWWyd`3vjpZ&{faSQhjVKsqTfWjUK2?PwziRxwTap!?U-1c z(?WY_Pdx>3fa2YTzWVN+s#am8{*Hnc+qnmQ zeji{n-A=^ds&lxR@Ax{mV zFaVq(3o|Lu~~7|6ZTLOx%Oya?~5a||ZU|2KG)>ddcEUe*pzeO;&e@CDik z#tq&sNkYlaf_5AagP#@*)(1XcERddO7xs`z-lrUjdpW9v+v9rJ#KA*i=2vz4qEz#D z)|iA>cCTvWX^sCzZ{&ktVocY_AXo<8e65l-j3fLjUq(w4`2J#;Lm_SZ2F# z%~uCWQgoP4fqdBh$^hl^jeaEM_HlbAcq`B^GdS|XEqP!qC1CryUV8rK45Xy(T2AjO z@|}dOp#qs0EQHcDGOF=-iJ2ays-HBtkhf|?x}Q3~Cq;siLU&1kU^IdCiG3{>;V9K& zfY(0iiAmsj_s+N-h$$oc=Y$GeL@`b5p)w8Qshk#vtzGXgDCRN;P zN;`?e4&$5!Gwt|wOH$@r)B?;ypqwUYbq)LL-W^~8?RBGxPdD@&IrP>E`#y{^NB`&p z!Vd{lX0F}h9-kB##ttC_jZCU)*4KW2`%RI)DU7SRrr>MOVwbtIbUO1OhN5knyS|7t zf5t87_vKgBITuW|b9aSn=AU*a&jD*{_`QMC3MI|=o01GVF>i|f6|?>L-*2u29;kvw z>s_9-E}b@Sua$hJl;>W|pH;phRpv6zS3Ws0jl38FMXpAr79SdA4&_k?@)Ez3qHoKX zGz0=7Fa`=b$7uB@)DIHDbDn&ub4SW7ZS2|leBA=@Awa4XCL~vn%TH>W=hkjTsQB^cJh)n1yqtv((U&@$w`amr4PM z$vm}k$}z3DO(tVQ_1&|K1_Hme6#n67rRO2vEfX*^#B=Y7pC}4XVd5qH6U1GK?=y2` zV=@rIV{aAz^wd6CH;Dv%`L^cbS2)tHH0wJ}*cXO$eXj%(b0=kI4{uxxUEZRbvbf!I zJgM$^@n5j(Vm1u;(4iVxWT@)P1bG?H2Y02< zLP2%G1Jc^e*S)h9UpV9d4`UFs`Qp3RIk5XX3LS#jljd23M9w-P zDG+R*Ec5&3Y()0*c?U^&zQAgB*)-mi@*|o0t@NiWtgD#j(hswyKlxMJ^ZU{@qihb* z(Nwh%;t#)AI$c%Y&y1D52nhAD!$_XX=bQ8@cu8f8G=wV66ULJs7MQ)ZHf4oY>BNPJ zSu6=4h_}wn3A;Z+m~b2nB6vVY;R~wB&lADVk)uHLLgxQRFDRpvSuTNwz8gt_d28a- ztkad#wZ@7}*yFSr?f8^kZjjGq%xof}zZL!L{?=jW>6oO(p7d

    -XhNUd~f7NvbrKeQsxceqTEovL*78OB@jbtJ+QM_XI&(l|VEXZ@j- zf=r)mHRy;OjN;bLFuVp}Rh6k-f{N<}a8*guI>1b`Hq^woWecmUvsM6IP8qXkevPuO zFTu{e8}*w61A_sa!X?1H8PN3*O+N!0=2=VvVdqu&UzGk$ip<&*o*r5o@-&fSXsWsYdIB2xR{ZS zd8MTcW0sgpb{b!4!r^7KraHK;B~2-{4oC`!L0o+A>a7QkWN!VSrc$HY(M(II8nzZf z=&{V^!9Jf`>eJF`GW|&GZozrgO)WzNyDrk>LFR;^S^SuKv=)%U4HDSa`vqz*?DGF+ z3tanl{+i!2UbP9qSB6}>&Q??ZuhNjMVn|6NcM$Hp|Zk=5NewEn7phQvOc107EkRb!{s@Qsg#Oes6_;^1L$GVh>%&uVFY{L@d_dXq3JgN0p#a6>6EQeC^P9KZ z_x4AIU`n&~l@tD+-kF zSNJLRF9oRQ5SOvwsR`eI4Ep>{6Xs__8;}^_5m#^bKFwRIa^bh*%ag#$^|cY~NRBV| z8m!@8XBotmKF85+?^msFIfR?hlcoYaOs5NmEc&F?VXU(ynx=-#O@(Op-(OC6R zA*B8Yv1P(6*_zf~+5EdYWaN3?Ura_?VverX{eogc)vA+MZ~#;)of4QO``RXiUqfAq z;!5$f>VXp_imrRaa||YXe53YL8RkX+xE8$IBfAZV%igM5%pCK#$j2n_zojtYfKgHC z=plEY3~OZlTg|-T63Vu?sn&=Rvn3{k$^iQ$$ILn^IUkqJL791aTi0|Q+>==G0@l<1 z$|tpSx>xoOnX8!y;|ty5g=I31930|NoO z&Ify^_DmoZ{H26(=>+V=?bTT2?|6HPo~>2pmSswg=y46MxGzG*;p_@u)4f>J#B#*| zW!^J0lM$A-wiFedR2EzCsJujj>7U#$lOMoE%r5Rq1D5#;0UqW6EkGLNxJ8H(Mvgtu zJubAjvn?siw{okfDeU2DellAdAS~-@(paZzI<#KkE66 zkg>BRo7GkJu{7I9k1GWQH7R9QkIyZVT%vaDh2;$|!o|mvNBaVlZJvPy}-(^0n^O(bY_to#ZO1aVO zU?vtj>e)I`=eluw-PgB%FR4seYj;UI)gG)m^eynf*t6X z2yzPsTWT>PW5%&u@O-Y~J8mE<|DvIwQ>JZ=HO|{OWaHOBgu%!r1tK`2Th49gKMd?+ zvG?uVJ-z7xZ61Hf&q!&-TIz)D+ni_nZ2Mv|`&!&iTZ^4RWY$^Taug#M5nwMamlhKz zpSas4($S%o9N(~a3dd0`H2RVe#!aXGtlz;y5A|Cdyb0=}2lNZUYA;UvKjw#IUxFh0 z8iAR9gtp@=DXO{FmGduj>dxr52f9n=Jk(TWvA>hRXHnXHg9-wZfiDYQZu$@nAC$?u z-X|8>y?YOHlUw$#;1Pq>QcZ}`9i6m*x35;q+OBclnJD>?woiXel|C_qlIzY~|H#1rR{eE#K z^dAN$GVF2cw;m=}cjss37j+@2JoVfbM0PhU_4|-_K9?q9L@5d&zFVUEzWPA%-h(6 zCOGe1#AZVxf@mLy7lFV!bC_^q|0V^xeH*UOBMEsXD44H0+MWhX3RF})rr(B68}x$N zkUI|~K`S8OE*YwS_4t>8=8H!)m<54b;$P|K`*Fr-@`i}V*{9vlr|C_KkgdUbXcgt*tB6t~LMtZ&ez?YV1Qrp z(E@LsYx?Ao-@yZ^n_KszQ6<4r8>h}K`7}yv2eRz7qn-@QiT-K^!XI8Tb0$5mIk(n_ zi0Zxth(+^Rt4oTN%H%<+$`XIbwG?I{T+x^>WLUqSV&E)6R`JfzrN}?B)4hyH0f4l} zBk`gJ^`RYJBgNN&B_hD&&Wn}*ktPXa17GM2M0VX{KD_l*!fOXaq}Zpo0LBBdG5Ny( zY)Gb6Q{v4xVgx3e3kMA`v=j^`CIDzDP!!1eKMXPlqXWRCU4*3PC2`uMMzDQnZO`$- z5r1-4(sY`0FdUBOF8E`o@qDYbeN3s(7I46fZnfwfWvs>^WvB3Gr=VWcrv90HA9UYc zG_HhPv{P)DCwmk^>ioXKei7~b^A_iQvuCf@>Kys(D?7P35Kg} z(E;Xq=Oc4+A%cGrZiFg-ytwl-t&BFo_3MgVgQ;^O6kd<`V+b!DL_LbkOByj2$Fu69 zI@n8qcpqBBlB-IaZ6ciUJ0-Y{Io`|Y4O<;2i}7Nx6DPOatq#)iK@A{7NBx1a$4Us5 zNjD93kD*JvcYj9^pR~#358HJTsX~{|1`wH}<`jbFD{_Oq_yq7+s87jGt*RQr_B2+i z?Kb&e7^O;_;yz~)p~fb(_cJ;NWe5Y|Jr3tnqhqC5x0F`zhIm(p`l%BVnxhCG=@-~j zE?4H;bq!gPaYZPhy)EbnURV>lu+N#>=va<=;KA|)bEQLEn*baX)>WnHCZoDxAh8B@ zJBzP(3T})Z(paOB0enH9Z??WsDQMfTf8C*B?VSfiZokEkxu~8Sc>O^eT@(!b(1``jY43gR z7*-i5!5Xik&s<3|!j5{}i~IlOjkMUG{_(X0PIS~r(F*wo zC9rzz5W_2ynED$M5<}!~Kc>cCt)VvfSVb7}-#`6K@;ReCGPgp;tn+@mFk%&Xr7n8E z6!kcB@3OFU_8-Or{l?FvwiB3-WkSWNQpNQM_%Fpzx>)N!|6w?i&}uyJKTdC9Ns#uw zo8O1`*1j((Ix!Gyh2#nCmN6%+8t%Gv40On{yz-Bm=l4poJZ*)Km)zZ`KyaGmZT$&)=fBsnEh@&<0oe5 zw`cOS=g*d;MyIc7Chbnwo@QAy1ju${ADKd;x<#!E*CHcuQ@i6tg#??9?mrqE`4wr+ zC}YP=xqYLjZ?}?IVI4U5W7c{At@u`VMt-U*`r0juP%*gHFd__-JyB&hdW7h?I@xLJ zlfzFjYqQ-N#V4HY8eKTI`mpPIF+ZaQgES){o!9{C=s)bfQuB)zihupsYXw+{dn6re zcIjmfTD=V8H;U{;VH9M1W3G84%FKj&G9w?xVix3?42ih5$)=<$?SFh*tHiMuVmn)* zI-A#X9$mx4xw)?3jxwLtRjgq9OSIbviWj@IuoD$*D;MFmiX-VtS2;cb*I)n)!xfDG zFa%Tk8MGaney_bx=-i&?V1BZTI8(YL?K}t_C+A$#oVS@xdPkY^^=(a^K>vxh(srG) z#KSy~wA}zmMaM_hO@=aMSwd$=KKZtBOz7ole!zYXdG|Tvwl77tBR<1>z;)>spLzQH zxcag$u?4}zTznP;#9219+uC0#Z^wnlpjCkr+%&-Kiw>sp1!Q5Tub&0(8TE0`Vm{@_ zXebgchPxWB`r6Vpj@`AB;yW?lB~FW)7xxMsimTQZMv@-oB5_CAqH7#fDN zj#as9RVOR0MK7ik>r1vAVU)zLptN7&;QlGG->PN`Q8?t0P+(*g%6Px9Ci|3hBP>6} zs_ojJ^i2eDZJf8+ezPgg=PD-JB+-jAqm1O@aW=nxMhn6@)yh|{y*nTJ$IqQf#@-)1 zQf&4C5`dm!Gi(U@UBF65@(){`a#5i?7^jDD*iAZ7+~J*Ey~*H>+qNC(GcpM1wPZ*Z z{x9aT4iGyTdloRD_zkLhY#HLzLACA_8$o{|d#FHobHH(o*nJqN)=?GNc=G|WZNJXy z=}97brf=9t`m@ihTbD^W*$>u1-u_Xy8B5HnH{?=r6#rY7b?VY$0S_6C29r};KC7g#Ol}(xMJN6W(nRO&B3H+zeFRZuiRMnOqDWd%lhN%!5aSK-* zbih12=Nk+}`(nmUzgm9^{uIWE*4pnBf%82Ed9xm@Kdm&Su)#CRF|ipfF$Yl9*KCVyZw2D6 zaeG1h`Xd5sLnq4Q3i;!~3umypfaz(9KO+6LdhJ(IigLW*JV7=~e@O954vWq4xAerh zJ;zw@?4N*TucHgV!3ZKLcE>HOAaTD;#YKs)6^`a>8nq5a{_U-ObE`KcaS@->JVFd* zM%DcL&V0+kmhk9AsppS#=Gfh@yr2Ma1J|!tjdwVRvhCq6K%qE{%Eru(FZVsHAFJN@ zuz-rESHzj2C6|H36)aqJ1R}a2A$FuTt9UD#y)S-~L8UA!W01)5!cbY@)5`F_gMJ|Y z0mTIW5%edB4IZ4}B19xRbeoK*h(^g2YDDI5N}A~#zFKk;t0uguFYTsr@hSrPD*8Z+ zl7#s0*00R%t?&vx=H$JW#)GR#5A!z2>96fRpvO+n-hfntXR5UzTBH~o5c$!*6+^Kd z-bEhdnWQY?xx#;+9)%j`voH2?Bds5w!7BY00jT<8XCYV`ohMi;b*0yu`X`1+VLa2q z#=^iO>1-_*wNjod>P=A5xEVCV=NEIez1Zlo4WU~_S{we5r%$chT>S@Motk~4J%N(G z1X0f-g(|?G;nC6ge{LfY7ts=W-gG5$l2h?TLUEqb2=dIoxQcjhCR5&t9+}vcz##Ek zq;6svo(uHI-OI{d9%f2Na9o*NBT4DKRkW?_mBzFA2{#e0)(EkFAq`h*1OZ4 zokXHf$J%*+SU0nw?-g9n5)@lqs7mYp*hd9T7hnXjqsW2#T_=6fZBR+>>C=4@tbB=U$a zhBRg2cuM)NUCG^iWJWc#bb{6a!5JiWs}V?;iwt@>mwlnP1Saty&T34>5K-gP=k3kgPJ_a?gS}a01M-$eg=CvE3*|a;3=fc6USCNSqLN{L-KDqkp5@ zqQ~x%ALsfAK-&cYcVqS81q|Nt41-4IqmUOz!Ou~`mX`P_+;30x_|z%~__Tp=X{jw? zikskgAXKMdB?z{|K3Kn^u0iHZZ9`AetLD3t&{=j|?N;-z=);^Uc`4ndg@p})hFdNUQyV!2-qJ9;vqWu{4IM`ZfQf$!zrs%>Td~hJ5{j?0> z3==?ofY%T6hSR@67!1|g{fD8Ez^x(w{`PXj!`^-TCyQKiYbV4^Oh*bMRvB>RCzw5N z&DON)Kv>)|}YR}Txb(R}sAPjb3hy$qkM1jzYGoo#WZiejwVif;8ihVoB*?G6a z-$3h$D~r7uu`nrKyUGT3PWeJB`ptvqBK?$5)J8JV!aAE4qz?VL8gR-CEm;A*;OMU~`;9y^_9$?i5n@;O>|TshiVH zUZl`ZKa$%_bXtoB)+dt)W2L5GQ1@KP9*KahE zo8t?Lbx*qA|Ze?D+Fs*4a2AjkEdcIS- zeh=E{w_IaHWFPl!SNxs>OTgK3+}>yi-)6&IF9W%}1Pp|M)vt~S9@B{EBdY^*)(;i> z6+oX{M1mE|jG3i|mrCeamz$f*_lg^4pUcwLdy0hw zEM_KxfZ4#=2Cq21 zfVyF#u;2v|W%JAn-3H};(|(ZQ<~81Q;-czKyFA(JXUbzc*`sPNQ|y)DoICY^(j05c zNh}&D;#Qp>$r`nr);%|^%=eM=LqHQvdXQQ*%DK*@s0wuuqJ z>!D#Zu+Q^L!4KYPLSa`HlV$)+1)*y@>`42LXpDQCHD9kU zk353slaQFLu@#jgOCF!;h?^-W@vSSgokcGY7t$AD%3vod;uuTm?w4=yigW2t;Pf8w#K_o$}PF+Ihp)heb ziWR>jkK5aYMK7-Qh{S&m!?{6vt#H*_DTMh@{c14a|7qnN+gigO>3ap1+4;(LJ~UJf z<$h1e++_R>)z(v=u(Bd8;c{Bh z!$DGrXPsZ>TPNdhzly!5bxrg+FgaN4jqU0(&iV+IjQNx%Px(w>nt0(d`V?3{IJGV4 zACF8baHFba3T+F@rsFWnq9$VrSi_+ATswL|9&B`$8JaU%hA zfxcE7_+I31OxqGlw+*ln(XfJ0u-C)0sRlROD-re=H$cVp@znitJ=) z>$wz3&~I}fQ>y|IYtS+e99l0%q&N!n=4*`~WnUZ{eO|)%Xm9WwO9Vs$?-_`|qSlDD zQHB<^#=O=G>O*RuP#+?AZ<){)DA9U!qB5dvD+~pKQO+P}m#72BBukra z$KzLIZtIQU(PyvVOWPX58mqA*pcgmD>mJTW>S&YhcA~qB+!2~@)4??F-(j4Xtsu;I z`i9i3cYLEB$(-{k!u|)N9l1$Ah_=kNS=O)*#rYNxhB4OT8M_^fTl-j6*%vAJ&t}2f zUr6=J!>Nw-7ufa3Io`eq?SnyIc+nYSxtDHbO0&p+7|V)s^Ve<>i&ET+u#Wa$i0IgQ zux4AP{wPOnac6F~)_6tU+B=QKPK{+&ZdN26v`19~XCM#}JKStME<*zoR%|@WUFwjR zI6QgV{x`&B4aN8ctN9wutUBXDoKtwDX}J0i@}|8l;t?&Zx-lDT^ zloM~ae-F`boV)v{pXswiL=}h25;wNjr*l`m_(6)f4d1cNTm5Iaq@>4@xulkA!G_qJ z65RazB($T?4YaZ}az{_w)i7tUbMU=xg(v-hP30KFP6%~_7VxgK)tIC-P4QZ zm$-8mIZ_afB7+T4YSCdAxD?LCYaJep7FUC@-o$kzx$wNDq_VBNZ5xJjo-Z zq7Mx0C=HXi$YRm1PUuc}67a*x#b{rkK$a%V7+pUNtk@?6a{yMq7tJmiEB3ies>d`o${q&s3 zq_NX)*ErVky;1>xa&jC%?REa6Z0+3P#_Rx7n>yZ(H0!K$Rs^2!=Oi&*7ei`ZBdb0I zQmI8UKLy*DzLWka{p|iJ?iO`eO)2K`E;jvIDha;WAe2W+v$V6}KD1f)ip2~0>>!A` zq&{Nj*A;f@0a)!G-ZwK3O_iPIi@DAGbBuqV z0-#07QC}fFa*l+U*yjshr5nDz^=4-lL~pwb#6<08r71EHJ{3JPd|zwB+SM4OoMovg zP3wJAGr{94I?)d?nX5>V4G}C&RH@{KeoRp%4gR*k;UHXO%gcyyo|*06Vfgx=v4mGl zExA?_zO={kY_y*Cw-D?l@;R%H(nt`G(*3-;^fsx7ig|+KY!+u7l)xfkEIa)|ywP=- z-_j2^i{KIda!MdB8`I!mHL_on_G&~YhQm{O zczdkIT20V+*12Z)&Ic|pU=iOII#y4?&mq;Zll)iJS=*2pbl!@oM0E<}+ESx!&iSuQzb(-)YH4Gce zwNKcuJW0GQAaGKi^T~7P`vtn3_~~!*S4EsKboyC!WjJ^Iu02Z*he7CZyk7T5n!98K z5fCWBzt;k^jPOB+H|52ldZQ*z@Syp8D!9yTk5 z?AUK6Wp1=e_m}3qD@%2g6i~MK_|Kx7mqF$0=;@9pKB(*~{%&0K1gYhi(UwipFjQIc`!kHTg zmQ2`mSCBX8^2)T5GWk+inKt1_S89dH9sV%~cb8(kA6oX2|I5QL;!KlD_~Y;!+UfX= zp&^XD#wm~4nw|JwY8?A1p$Gh1Fvi8FK#Egdog|<6_DJpu@GN0D&Ex3I9Mw`eJgG- zW@moE-U`TWUq_MRVq*3XakOfSm?@g8bUx`--_~F#Q);Z#CXw*1BlD8v)~#o}nzt%b zRT>{KAEFSEhish9=B=Hh-!u@mG{^pZ{6yjYRzdjrQ|znCLYDWwUI1G#$>QERTiVJm zK{d;kfc7*->QLvx;inQQ!dL?eRRo1N#@@xpve_mX&P;foAot7T|w= z1S(Jo#nKHh6GCn8HonS4G^D3ej_h?9p}hyYXObsgnO)vqRbB4b+}*2}_qyf|zxonp za53`@2bmxku2Z6CCu%(@$CQ7U=AabXqxu#G*kN9C6qh6@a=B0Fg2z{WskbiZbSZ(& z<9midI%lH}>i(JC4C7F`6os~=U)bG)b{Udqxsf;s)AWmPEtFF40HM#5M_i|-`Y<8$ zh+fXlywyv?!sbQBj`nBTj>;TiV|!Cl)_OjioD7Pq%j+${PSWr3bRF>r!?wJY5AjdM zZ>z~nHU^7l+KXgmbOy||AR1*sK!a#NF6imWd!X|hBxquY`iRiavfbHQ2?XFwjdM3u z)@>aNMe~cBFOxAxaTm9}CxgPX>gul5$TWNN$~u3+H*+NC>+kXnnj`ZPJFPTen$X4V zYejAZ1RCEyzz}Z6jGXzoJQ3)!lgF>R7HgAft@2aqPy!~^GD!gSDS&AZqr?#9(e@mT z$nc_}D#K{^(G9b|>uFYE>8pQk=&E^v%49PYX1?M)W=4p>X@7BapAj`}uI$WcNc#Fu zM+rSYUada_IHPJH+A?c_s-6w3FmIWuiDX95wZ!v$i8nYCW4%pgS!9WXF)`rW$1nxP za}FN#velQxHkO+p!$$EpZNIzh^?mR>yVK=-?-@vZQ%je-c)IBO^!H3B4W&h*$$l>f zIvZ#g1+R?Vt|2*KTdS(DgK*ay4QgcdXfJ1D!;+3(HpdsfiUPLDGuZ|`hb*};nFGn{ zBa-E2sBdhLz~czRQz;&>%zXBh`4Wl%r>p0xj{4Z4B76DwX3ZdlRbYx5$3}6jYek6$ zl{CE+Nsry%zf+^y%C4P4^_8|dLBxC~pPHLQnX8J$$>wXlseic;6nv!B1=&9pekDTT zII28;^w9>dVMP2BxU$NF_OxtP=TO+ouvy+yMlqF&)?>5H(7j{SYq4tcL&sq1n6}d# zoARfFI_A!a?*){iV?DMfM)kJ}$sSc3VU25pQ>P4+7@p3a|HwZYUo&7eKI`di^x6NOIzdpzX{V8Z}D?nTsAamTHlORQ8 zq-Ahj*eJGYI8*%eGT#Vy4H7udNg_`-c*PXCs5Uq*{f=JW#aoVoe~~P zf&%`v##+VBX-qF0KO;dEDFXT^d9rarX$tzF5l{5dk*P zRh!$TH$268EZ^O2l1-j>+o!IDXIH_al@VmlL0kyEv5_1Ga-^@`0xXztb>->u;qS&L z*(-S>pF7t1m8mL_oI8mT65KV%cU|1I1FI6IoY<8WP%=d>CWO+ZHY%HW-7zS6)^$1cNTt0lLVva$8yhlOh* z`al;>LxgLNrH7AFOKb1Eb(Auk->3Q=cj%GvT>rf?vkumVwe)QF@Y|m-@mP9{G0`J) zF+cHJ5V|1P0XU_@7={^E#ry4tRm=UFAPVpy=7FsIvaHi~HIo-;puPyMlZ%~*+MKfT1XWF5}QO68j6(|N5Q z4`m>723-8nK;yl_$(LF>Ah&eb=INO0Ha^vfLG4P4$xD%g9*L1$J*$~x5_J#y}!J|TDeu_E5vQV6`;@+7L zyGic^R`Y5&J%5pgDNP6_beeJW+b1sOKmP)aJ$}`5`kvAc$5*t z9*z@Ac8LNvC$+Y?^&DA+4W-Z8Dik?>5}^ZhPG3T0&rA}WM0Ksh97zpcSoT`+lThsS z(LBBaGD`Rhvd{vt_TQs7^|*_2>D|G5UE}r~CS4BDNp9 zIyx(OyOg2j6@cBy*%`JTx+}nFA)#Fci_N({X-(@o>U%l4TlLS*g(Rv^NV9Ko2}-hkl8WamNW_97bY93zBHV~qVY+v1vOoCxVAYG*qEKZYB*zh zJ!m++P%88+rlFLEpi$7a91VH&XPvH(H(MAe?KzakgETP0$z4+kRI?w@H&U;1xO~yB zJ?EAlk|&v>_vY)Q6&M!@phyXpMuJ~{+2H+;z0^OQ2VLMBQx!}+v89g+)J@wpLQ<7`08T;!P$&4j0nCspHWWMm#6qF zbzkTulo^bbcwRkg+?Ns&tzmX3=QkE*iM#hQi-sl=1`o<%O}NUck)lcqx}{pjO_6TK zO~J3#q;$|rYe~+B4WF{jMig!x_N(#D(1hM?ng@3l522H!BU9R3f%`4i@W)^7JIu4? zC-e0b70z&&UZ>i9JXE@(Zpx^uBCh0#&rEg&`}Ce&{Bm+!>aqdegFUifv;Y_-pcO~* zaXKBq#&yVOW#Ah}oXU5k*Prw9%nFBaP_h>BdtcWV3f2=i+$&BJ75a<+x(sT)0y^|V z{S2i4a}kh&anT7tzS${KP8Z=8cY)Gjh_mG0KVn$U7?eIF4`Toy|Mq;RX+Q)9zm*?l zOFI%@KLc6Shh+z!HX{DqmZ8(Sg(yY~c1)iE{SSnFn4o|39~ZjYOwj6H+|4sri?KqE zGjsN!iQs3C9L{Y39N9%e=D`3Z-_5|#XCF*|BE{00I!p@v^T4BQHf=2BD<7qEh5d^b z3y{>L6MU_C?fwnXdiD;YK($b)`m-fz2te+gIdk~JLhl3IH{M9H-MPi zG`pj3dGn^`{67rKsI>IXR&=Tq8$RrF9kqb<(oS39p1R>i->$WbXG6FUpc{e zH%|dheakP%=?QyYaaR_M^OBLBa-yzzz?{67CTPB|4GWpNUEcS;#CiK7nmQrJ%bztZ zsq}J_;YPZHHVWg#^3t?cJI@%)VujuVwWBXRIXVW6T{!urX~K4gL-=E22MEF$o_|1O zVjuBE18EY_KG;X>Y{t{qn5YG3-7l*)UY)Xb9*IJda3xRRJSS=O494hlP9$+OZ+;Rz z;1-dOxg-SoX2g^1ZOazvT|VBps?;=2wdcv#&%Or{T0{{xExv-f4XA0D&hCXBlY~=; zIq?ZTk%CBMswgoxve?t#bW(q~-q|_B8)SIN)e=vYP2=LG;Sk*H`O&GNO2WPZw*&GF zMTyiy2;BZd#uxil3{VG^eur<#@s0U^ZhCt6d*|D_$V0U$Lf#E`nSYm}@e7($TR<@tNGwwkN8!l&i?sqn1>12a@3InRJ)Hzu-d zKv~+ON3PPFF+@R{x$(BBjC;!OoO6%nmsrYllJIXE_Ifv!_$|kA!7UNO#!9=am z`MMest;u(Ez3DV+DES1n50FviZCy1sV~lHxoL!koD_c>wtmxSi{+(}>C(7MdF%hZz zFS0Yvzq#%rQoODhDr!a3!*V^HD0hc!Lys2om>X!kmZ>YNTDxS~&Yc@e zQX$#02jv=4>BY*QwHVz=D6db{r76#jG#hbX$@erO=t3JBL_^DnvK>2UY#%n;5GSu)=I)uL9C9dzd*h()K}1)ml2aJ zOH==L^F^V(Ic$$lARwyBKj})Dd)614!@4%Pp%*b3_i~i5J`hY(0PahR_N#ZglQ>fN zy&IC%`}xa}S|&&^I$`KQIkQ*kZ9KtKZx~Z%8I^nv@;BaI!As~g z+yY`2d6}L~q6<;$A8sr$XI$=l=3YGx|J6kozn!&dN^Y z(R#{Xq%=%8{ZBLInA7LKN~FTU=j)M%NRobu5RT*Ey&q$=4T8^o z>0sjlfJ?2`q*E3Au=&nJ#jwH{Ziz3Qe98TulNLob9R31G6YebuBL5^dU zU{Wm4B8$frsk1>{xo1XMmP%=#J0V;^WpU_qF16)VWG2@gWY5#W{Jggdv&AdF ztf}ZA^=^v^QAC@H1J_!!G5>T9v0~m7Vs&YRds6oDAdRFw5?>g4f=;>&gpIeJ?ec*& z;Om8#T{l^%3YAK*gErFeZZo;Zjx)t8B^a&eq>~fh?V@*b=)_IWN|km{&T+|ew9@30 z5Z&{YY$jjTC~BRpRg?S>95py0| ztjCm@3^Af?;M(2wlHsQo2C+sdO9I@2-KXzU+{ryQD+m9&rZ7h1m6=VEDtM2-krpI< zTHBga?zLLwLgLW`zAklhB^_u$s9MqB6zQIuc@mpn9&?m4_<#ssbr*)q#ujh}_OIx=oT+%#SQ@y|z6UM{U z1eQmZ1c#M8zUUDXfOv_gfDo$>qr7~BVHI&#G04+D=%f678l+Mud8lqv7{5OS+qT%< zPdNIl6ec7Bku$;Fc2|(Rw@cbpyz8Xzp53*^&56q|jq^Sb#Mt(f{CXv0*%C$9aa-t- znjaRa4>G&PG;&bZHmIy~gSriWCE1Q1>M?-c#h^g7P64i=SnFn=kO2dA^=zic9SN*^ z37-NnQ&+nNbFasyb!vC>7f>gLF}dEQBeq_D0qRwhXUb}tI>lb z&yh~6CqrTan-gwT5T68!WdDJ0^tq`1Ydi+EEZd&ljPUIj=l$azv>Yk;8@0@!8MoI%7wK&3)D>D@=-{Cv%k&A;VWy!IPy zV5UN8t4A9)99K8DS&MQ-RK%bR0JU;_ba)VbECU|EX&+XTXmT{S>3&>$#fDAm9HKH_ zIMMO-B5i3}QM()OucIpI%9DQwizWF`7kPR*X>_qDc}?g)G)sqNB9>0eKuT3rvQeoC!C z;T8sRi$R^UE(CK7^Uo(;+z1;u{iiutt-$C#oD|HPDwgDRYs!Ibjiy;;0g;_LJ|M-5cnY{RrRT*nw(a5S8!%HyWDVHJxx*;I&D+Knf z3Bau^8Ean!gqe>_$oxCS0!8?G>ax>wC$E;poVr(giLd2#^3$(*PsO)2D1rI+x1blO zw}{8w&o@2T15j?+nW>Lz+zTUvRd)pZ;n($=&h=tb`@fAHjA$BTJ2ok7e+0KY{rAiJ zy1dj^*Z@y-&!2rZbw54E#n_U!RmR)qmjYcB4P7;#UjM)E48pa_9_*Qwf9z=PJlYme zKR=jwmq$eXe2I;rX}L};PD`DG?I>~&%?+^t=iZGM{lfNnt4d75+-|LVQ8M@vyYgI2 ze<0R(Q+#jnqF&0@qc z)gM@E?)2{rUV2C@{u(K*DQpq%Cu+IH#Ew-vF@wnG_x}F=c3rqmPR=>c^W69A9>inPtmiyDcHL9& z-`$qJ#tak!ghi;lzZ+02kly_i|2|w^I36xX%3A7k%{n(_#ZJ7Cn0xULN8h0ObI9zT z`+aX9Ny=8}*vJ0plSWo;g6qz^9`&ktcB?h}bps8rH%ltR?c8@&@Jpe)JOyc}IC|i) z3r*S&P(qHxxL1_qeC9m*^n zuQXVa{kdPzeNw$i+xU_lpC~`_5??Mm&>&@X#cU9|_q}GOX-I0_HtXzBvW6++U$sO2 z_!oE-%8sw{JUzw(l=PychCr*d8>i1-jJ&SU<~k_`B=J zagi51Qr)DVB7HEG*C@?F;=donuNvdWTbv{>`ZhzjN!e<*bO-~f!$9*?tpET+jku-R)tj<(I(NJAAK4c^!SE|Jpj# z|5mj$3ezUOqGv#eP8E}Eef|$Ghgn`ZCkCJ6^FpC0ON8K{9LqG<-C+N5;8oENf9W?; zN0~anD8`i?pB6>tTv`-q`kJj4pAA9MOJAk$AYXW65NxpSUA>7UX;=Eg*% z_kG%8NzELXZDrh$*B@Xa?j6-uF|HdYGhRjf%P7d1Lz{mw?gY35%J~Xutq8Ve4hruW zRN%E89igvGL{F{mVw1oXDu+*?{>!Csoe(+3&JfL(v8ieKI&9N$();$7RphQ~?S31Q z->hFg`|0l9iSZ366ic(w^Fyr&VmPT1c`lm(Qqh0#boH;#l9<4<0L25=z`5Dcwh=2z zD&`noNiIGS)on-OMWF!ID6bD>86Y`N6wF zSvLP!gS&@P;;9tng=U)DOHPKOYTFt1$QI=%#D<;n#N|OYoY^!|cHYC6MXO^-({h== z54dvTwB=s>s{L~D?Bx*8T)E;$u90ER$0cXZ3%{vcmvX}qFtUGs3Vfu`Vg62>Zz127 zi$(%j&dh%beO6#K399K+5S?=7e01zkXRgdkOQ~c2d4Sc4MeB)9#DMzqx2at?%9<%% zaSe>2MoI_w_E3DFks|p_vha5<{w`kuzAYiMAQ`m5#+Uh=^uDdIF9VTYR3QHzm;KO6ZUiO+u$(oe~hR({5YtKa=HY z_bGOfqbS`Zwzud(7SQxN6cCIywN93m!{hVQ-Gbd z7lk&#@kSe0U4Yh$xo4GYZgr*DTv8)NB}CApsoLN{;Z?p(9@BQy*Ll{66NQztIfh@9sXZzD zc>01>>6M*+#JFXE+D@GB!|{^7cW0u)yYU`xZDz6w@%^z5&>exHG5+d5{a_QQ+t`HE zfI8lF29ES~$ua`hi=ZoMwMVvMl3fAW+yTi_4gO6gw!1-T!TNzwZrY6va7EP^hAb3! zPg{qBdK+V5UXIFu@nh-@f0k<3Zm6Z&*WyD;S=zfzh3hkgPESfg1Ludj{efOng^iXX zntc$Qcdi~sgjIH~(_GxP9dp8jD8EVr>or^LfvwPUi7;ZtnJG!iG=>B1hzw~KkD1I> znQwJ=p9cT(nlGG-^(y;G@g&cj7j9LuTgmBjd^;Xn$CD%BQ819|-`dc6p%r*ukk*YI zfPd6}^*e9Ih`IU|MK%TBD=30?woc%O;FD(o7efa)ItQ<$7Z7CGy(((Rn22Jj%hr8A z!V~N}hxdsQ69jm|gN(?W!uO09Po5Pd!6hiwBUr4wKy!6+QiNm2!d1$vZF@i8kGdH0 zrVZ$=$UDk%v%4<5JUTJ-i5{8%_?cN=bSe*i^}_m8#y!AraJy8{Sq4DJ6kPY!q-M`F z&!nivxW>PH{(ztZT&`$er4r632XD&`k!flwZiG2(LOOXW#Wg&*Y)Vme-G@07m!4n~ zoAv!v}eO6uvVfTGsNHF7P&VX=w@L#p8(IEp8)?Vdu(Xy!`1v_tBpr zkyy<0iB40$h!P%GyawyorByAN9YfhOm9#3$6E=y&SwpgOw8wbArNq z)vQ8PF64(wwyP1(@jjYJI@8`C({B9T(nMDgP0&D;!t^w#dwGk6CVuKoN!3{wPq~7! zUMMSya%Hoe#&z-Y3*JDc0cGXTFK{8^hN+@-eWnLBS1>Q#ml_`N@V95NDJZJgbt;vQ z2gXA!6f%RiUbqMO`2#-1P=mQwR72cw!W4>w$d5NW?Q5n!s{LR}$7@O4gNBW&A}Bcq zR{6uubdAQ?lmm`j#J9!9R91efx82%gJwT#I<=%cjp2SfrSvOpV_7ernls?#tr_^)o z?%K}&{6eLiH^%EUeAJUE4H~g5!h0(XezCV&?*Edtk(l)cnWU1O4@zB*^1^tYk>kBP6>-mI0h)Bk|L_LfzJTv>PyE2NA?GH4qPfnD_l%VL z9r7JIiLDsYV(r|!IOtYY$Q%PGi_3W#l+?hM zwL?Q5LEXv#?xkOhICGoPo6I-Oh7-k!j1nq4k7M;(%&eAKE-M0&c1N4Hf>c>C zw@(lhuRp|l8I=diLZ7wI`j|f*?iEpc9_QvHj}7~)87BMrtyTf-8w<;3VSu{12KTJb zE85^ghMA-P@cdvIsNbWSM+v6`_v1+hLmAWWGtYPvh7PiQopLtPe1D4Ra0V}aaxt8K z14t`}oimIOzZ~j;eyp9@Cyk=ps^i5(Pa?0_Hosfl;Bh9u-qFDI<2p2n28bHs*;gqp zOPa&I&Ksd@obocl(!T$t_})YEY~MI*?-oiy78#*S=zAkp259S}gSp@GgASU@>FMJF zPsnALo_JE~s4=on0G|amrMiVYtrvbadr`l$-U=~b7IaW$+ue{tkLU}=OyE; z=oxDI^5iZJCq+pM$=De0U3~706>wnPjpk~btPl(ya^v`F{2*D8!+n=kW-h1WXzBJ5 zoXG?$mU>x+iVwKkO6Dqp3r?4&Z4adXxO5q|w#r4eD0;d0JS$g7Ee1}}*V|?f&i{Q? zhp)KP60;46li}`TtV%g85giY)e6mUne>1fyO|O%Wq|GEo;;Dqse{ENDWNx*~V>VDK z8*EvDzp#dwnja9~dF?h0)1eF5^*GuJ5nzr_n5e=kU08=fqo@zM9-_5|H~k`PQ7vm+ zDVR|4M$KXb^KsvfYp2ye|FULyxB?4i4iFZ6ncoIe&|IqVr}y320G-{}+8pc_Ob9bHf9eHDpRU?6+JJs&CFTtZ>;r2)Ue~YfZS{VSOb_cK7&}CFIY9!;r-ZP9Fj--^|;@y8g+$0Pib>0J$C@}nEW3eRp2ci&jl7F#!_h)ayq2$S}ve(Wb0e;O` zz}zfA9s)K&33TDBW z_E`0`KLKPMRt2tcIPO&QUrTVwZ)4caokOot#f=Wc^rgnUoSPl0N8>IZ%IAR;R6bvH zK)jX7Y|W6w_w#p3B>iBdDHPsH6zowtmgOogn?OO_^bNoNn#9!=uso7U zO!UzenD}HWQCIifGhu&Yc-#5>1%BrstB2k>Te{r%tGjYQBVMU8bFoG$(c#-{k>K-7 zg9hRYF$oOxCFO&S=(fhAh;^sxg@6%ZNMy5LFC{rvFyzQ@4y#Kh$>dUPpFsqEsyZCQ z57jt#(&iNUNDQ`4=t;xSD|h%6RR=F>#A5E zKu13iAoRVWC|#`Mk-eLfxB1WK)r4->c8$)0(>?*}j((^?GD#R{G8$)485HQ-Z+tuw zFZFCdte!O6s9PwO>8iI}-nA7tA}mk|Lsh1cRdchi!^~4Zc5%2b!GF+&qbY&6z!6Xj zQ)#0HE=6XcE+L{)k5Nu2)4~>_YiPfx2!kidoxltm(&#@tV-8{BQZbDA;6h=fjBe0# z*;@aQ1cj%;OH6LvmG>bv8$ySH8|C_aH%L_|5U1kTue>TmnS`%&W_0Izn5*m&TeU$+ zg+s`Ur3Jtr;ixbKJ(<7TA2l^i8EFE7^z=!`gC|r=TI$EAivVJJJ@b5Ch{Yy3n$t7L zv7lOA8B6Wa#cFSKtdVvuXViTag3@alR;{CqF|q5?cwjrV_0b`3ID~74UUB-<;Cn)T z12Dg~t|^?URrb~n=-#JY9nPaQHs+!Vhktfwrgg-7-^LUr-q|jnFve*8TLq^Q6Z+8Y ziI^@wligCrL#IfUNc@pCZuX(&-D{%I{?4ZV3*&Uz(}oE`#%a?4sOk_0WZSD_J~@sO z2N~9$MTg|igjIhl2-F`EmLBCgfx1`e?<%}utPTiRuUmXB#T-vwn87N37M63aml7C(f^Zi?l(7m?M8$^ z3&esM-)N6pmjxjz``g(%rs^KrwV);74$fCHU)~14x7NG&x$T?R!F$UGV5?E)^@hGI z?Rd&tJ**MBu&+JG%Z@t%dE{VImGt(9#ZpXpp}W;(4n{NTQUi$mrwUttI~ zm*j~%anDidWhD;nkJz-LoD}M0wgdfvno_&P%N*R*f#F#@+(u0g%mXn1a^k2RbD0zU zX`AX+>iy}z355BSO!-w09-LSDHcZzpuq?AGfP|w)`V_&IE^;-(X=6O5 zgZMnbajbvA6wz1mBq+JUy=hEKB;aP=h4+l&RPE}#gwbQ}73B}QVg6Kj?_VQR#sKXw zpd)ue+3-cok{q$*AZRbBoWq1qtyp81PG5b5Y)r7b6uEFZ88;rIAwH?$E~til3Z;xPYm84p-zj3J;e3Iz?=5Jm~u)Ef= ze|d6Z?~LbPQ+{SxIYEoGQ{`vNnw`S?*)qkQl_aYYS&r#*G%I@#Ig?&R@y3o064coP z2jYAWC7b+r^jeP~fvKjr`hrrKL#d2uPe!)Hx*2vpp+WvHSxN5Uj;dld{Yf9&0H}Nan*bk@8YOP4zFq=&O_3 zvq9FH*HtXpb_+8i5+WiL4xt>X!e_j=ugqyW?4+l>9Mn~1a{bx9;-9Wz9~WJL zs$t<`*Hp#mesw5TghGlk! zD;3NA-NGOVBicVhxxNBO(LrdEvPi%L%R|Y>doy-71{IC*RymS51GqdSmVu?X1)%-+J$G7zo`y=f{-FE zEsZv#^BUaD;eQuY2t|{ruirn|GdFD(#fwoMc&^MABP!VskszDQeY1qV`gt1m}NL zYUDrC{yetu-Az4tpCY098f#{x<`^k!twy|TX2mLC@cYX-qi>7*{=*^czQEnM6AK>$ zf!|1wlD6t*+`vqgGL#G^D12Z|t>pP{%KT;N9 zHFH()?i6PuA7szxNH4dPgn0sE575c~z#zr;IgxKn=c6h?Xg?oq%mX}k%DtBqp)csv zSn7l89LY{>4-yLuT$u9tSS5Dg4|{E3Xqqry{0za2cT&fDr#NSu+K?=lEb#Bie zb3T{7C-9Vtg7O_t>S|f~U_q4X>v53f*PS9;o`^&LUr2Gux0~+x?)c&yg9Awc6#Sg$ zf_|KJU!v|Q_XnWWO8$JRpXM2Aw9CyGdo$qaHPBSF#+hpVn;`j5wjZ9M{?xN6rolTv zz89urulsMU>fU}}T4dE`*|lQ-i>m$@*vR}O?`OZwJ&V`359~c58kejXmx5M&@}q>+ zLS9RxsF?!u!Iw%4Glq9j!Nu|d8)%A)t-vJ1SULl`Ay68B7=`v`th*lGfEF_KiHdsf zpD|D&J08txrU#21Bf~+g8v!Qx^TfSPxf>lRf{vL9;j5_1a-QMr^cx+#D2HbOmzDgO z{99$79-$@5_2R?D5|vNR?a!7R51yTc?{5|C~2jRnmAdg7I$70BXS85Xi$ zhyeD4_ENq!=48DrbvJnA|Fvfjbb5Eg_utB_3gxt(9&5D5^7xM972<0Mf*v*^^gyB* zjL|~T<|l@^r#Wb+xI{)&%t+FUHt{L3&a0FcqGmf~fVVmeqksDfqte;If+NKEu5AFU zXM=XOiB_kLJO7=ghW2!kxnUpIP2ni;bC39|BMZx0b90SNN*c(m=>&VCrV!Guuj9*& zZS2GgHwDB;L2Su_6qqyi0rp@H7(5@O&Eh0+;|kREjXIefM=FKF30hzuH`d;@q=j1E z`or$g_lqd{0aGv&ZUNv?*ziI-BsNms5MD6%INGnf`6|}c zMNfQ6&aKqm@UU3H5%Kl@e|VBW2qFhcz%s5I-cWf&f2(8hTac|3UHk^cI_QhgTRVJ2 zQ+p0>wb`6&p~P?lg1g^iT(cjt6FOxfkdYjU^br!dI+UPcf=gOf4bLBKC=s8CruqFLee}!s{O*s1;HQO zzQD=qhPj$*nlbtgkBgm+w!c50-SKfV`<)j|ms82!mNwdEihP$5cWw}}xMnaV_qx)F zjU=isn0~5GcG`t?`%B@ynnC=Bk?v9Q55&t*Wqln~J|~fYBsngG@(?SEloepkN-$Eb zo_&0|B3EE*sAg31L7F(pX#__B@#;ZH9?fpH5p$plDLi;@-%nu9CTCwF;qDnSF#pt zV5@B>xGM)f%~Ygq)_NraAd654X2%5fWcKHi7s3)P^&2u*9_x^KX*{@2S5OWT*i8rq zYOpRa>ZLX{WS~z%e`y7>Y1)KlY5ySTK6evY1zMbKN!`Z(9OPkXh#OmLCtLv|3WL8AJoop?94QCmHoWhLgvFBupXGu+AKr7Hw*4>JN?{BFUJtKM6(blg+S z?WRPEG;OkcL++ml>HWrK0e}Y@63*mzxP%ISiy_|_^*qs?^3Hj!ld47k!L^~jF{jYf z2DA+06q=d&EAg_0Ww#5C80Z9paB>H5JODT&)K?a`vC@5JIwNMq2ImYD^Ne}HNfSko z%@QW?1p_D}@6!M;@BGd0cR3f}D4g6(tJRCU(L?}>4=K`FuvNja%cQde&X)xD1Jz$R zAeCLC9M7{D5^HBitHc!3!;(3Z0VVe+b!u+#;mhEAje-`VKpSBFH;@sMDIS=>a|uDS zBGh%LjgNoEKUP>=zn%Euv(GY@Dr5OSOH0qmV)(`-91R*^V??u6I{b2+aJUR`i#(t9 zf}Z{@45hkYw@Q*8r;|K9L7%Lj9BHFY-eD3@mO8V>a^Qf=;HMK8M}miStj{;BLuc!7zUeo%nAehcLcrY{M z=r?C;R#!u_n8{ZQGlAXc)kDM)LiPTaSjf2g#qA1}gXD=dv+TX3h5mP6Ux1V8t`||) zP&?p6Bbt=_()#JCX>XP$lQdrV9XzPqWSGKhekVn-FZe6wp9rLVpGT)iFuA(&*U!rH zLSEC7X+!C9yIzR^w&I@-)C=PR2hAnZN10@gSRy5$_1bUw_H;3DBsij#7hH^X@tIh(hnp0*B=nZBU^=VB*kb+K=b(ksM$t&dM!x+({e+u zOc?OgW>H#L=E@n_e#x>`@D|L3T)T`zg&dgzJLLUctO6HSc)r}HgIIfAQ+a>KNpP68 zI6CXC$m*-Vom6NMA&|+g&etsEQi+NHcrTzo3u@@mPGEu^0^?9JE3}m{da+|5LTm-B z9d)?MF+ITt*J;SEpQKf+?mP>%SVNxBzuqWU5;GT4Ni~==nG)!q$jiC^*HkQqMqTAf zcbQLKL|`0iiPRVZwpI}wUrd@)gjp!8U7^j;D!#%80Y%2DK zH=BLA3)4fP+|zr;t`EeZKP%0Gi%s)y*I=w zrscq1?NTs0!b#rc$(dB81zBB|&mP5Dh1I*8z7jrvwj4M=+VK#_4hifu47u0-1oBO* z*{9(s4h`$2W}xw^HLRCRNMps>+^b7auuq-2HPxF>sBg&MyVO&j*cYv?4bNF1To3Zc zmJMS+-s&wv0Ag1Js^{`)l*t!UNW(@&P3eKJ8=er`&tH~WB)df$w4EgfeYU}WaUwHN zP2x^C5hipOH&;DnJq>jr->1tQHwt`k!8>`jHrNy&tT20a6${ny@k7-hwXt*t_Hz}h zEO#|x%;ThrTv$s4vEo#r=F>}rh}L$ZN~7=MbeP9#gqpBZkoNBj%Gh8I;3HqpYX%)& zm#xal5XZI??K91V5-0cdSK9e8!gmkSKIh6G@Zp|e;tViHXu$tBm*EDA#L+<+!uD*G z@QUi>^tG7f{hsLpVTF~5u!pTcO?O}7E(PnGj&p^DAWt4ch_*>=egYv-#x6_pFXS?S z{LM4Vu=IA0`3`GJMAl^t+N_lPs!s@`v0YVX_6xD#;OiIQ)4`c~=I^}vIhT(=Kp-+w z=uip83>=*uhY1^?gbt?v{nv0R@thDOPv|orW=#nVCo6}1x38-4jrawg(eGl|lLj(x zGLP5Qq@BG&Cq96nzN1h8%RzY51l;Cdgbs&M!#!XYtT3ul8^W z9n(1+>Eew#(7M#Yr1;MYA4FH%lhe|2>dEl5*}|gkkMwaKE0&xDO6L>HX8ic&8%07u zlkH_>4X`af%|ZFmaVD?-8a0Za$n!-y(&Z2yq6@Mqet$Ng3Q+tr@t&<#JL*J&k0}o^iSw=?$zE8l`;-)Wj&rxY}KhrU1PF>x$4zH1yn zQCGHS!JhhRrjqTQEx#nZxhEOssPQ1vRirKM66VAjG}7`EEJ0Z!uI`j&4VBw}I3BRu zYPTtUxN8nlkN-ZtmO3qeU%v*WbhO2NVAh^&lXQ$I$N85fGND^@!V%swrdcggGkndk z)UH*24N1Hv!39rpPf0L^w*-6J#W=B)P-aGNbNO7Vn_GdyDb;jBk)d4Z6uc_V_pRBk zkwUR;lygQE-+t$y2>;{x^9W9f!iQ4-IO(G7P$ya`Pb7YLp2)5bzVj(CXD@AC4)4;?9EQO1964O&nDl&haiDZq?`h z!U1735VxjofA#XY`jOxuTiRS@IF$QOnkJVH&4e((wzLF{RxA6wf8QdabR#dh(ih{D zH~H7yXBKS)zRges+!{y>(fxktLio`t!S`qQgm0nqu?XEbU9Y{W8IXc1lD)~D@8H>@ z*so$@1tuoWU7f+_OG-^xJb(pI+l+aQpZ)4LBu)oR&JNvHoQd+h_j*T2tg8+XdC4`o zfCeSf{Uy;$(Lu(N^bJdQv)$EHU(yuf`+lHKikWV;LFqkJhSE-X`MrvTVaeT0x}%aO zX7LxLPli03#NQPnw7@a}X6vww;($-hf}qsgjtHalPhYCA_L+Op-JU`FX)9x=c#ONJ zc|r@<`Z!WG|NG;I*QhJ?uShjHC$Z$DKu%&Q?7`VkcGY5b(gF|N)fM(}ObX~r2r+{4 zdm&n@e22`A&{PRFvC~eV^Ls4t_~5|uUAoYvwg3D6nZfLIXF5h!M;^^6|E{4@t_)Wq z8TjAi`YQOr-HU{d`-veQT%qGcc2cu9%X6(F=LKOxU!t*J3`)n$0$BZQW!JUlZzd1YEnHstUOVotj&lTmoFN&!}d;@$yW7(5wZ%gDZ z3lOi4rFUmS_|rHq`RATOx^p`1reoh{kZH>Y=>EBGG7DXsDj-?<<4N3Rd=K%ESvTQT zPD?0{LfTZu>W|ZhW5Vxc6tsTSo-duwEidEw`7k2OWs_DjV>wnODLzk!;q4~kYE=@N z_SH@ha1tTTY_@OZG|i&EjR!7;sP#=mkwD`YR(v`#f3vNRpLb)E34mQaz}P1YqQt(5 z8^nZG^xTcw?uGCHiGP8}>|mqblSjSaC<`E{m+^q8-7th%z68t5`!hBr4DO0E?^e5; zC%!1seF@6fGCs~8gskM+b7^?9*pUUNkvWh2n7OAOKB~pREx1K9!CKUw#y$RsyI0S+ zlXJJR^bh{Kah^)rK4!D~yHz&^CHpq3D=o2xRla`~3H-Af8Cb{Ft*w8rvk%EP>*Y(6 zx4^PG95oCLTln!gpL-mwb=BzycaPbbG_p$AwSTAl3^|;Quyb^tO1Pf91*0kzfe9l8iauP+s}Lfj z{TX%{|I`qi9oc*&p)dZYvtKlM&p7jETwvq*znc1~sRy+96P)VssoL`o3V$u$lFy=M zS$D?$o!=T9OBbo}GBB5#hjJzoiR#jm4}#Q_Vbo;zi7OqQq==LFvJyR7#9}vMmQ~my zKXWBiyQ^NXeNA~W7R1lN|J+{z`O_&4_YAJM_wq3#I&6StKHQ#-unyC7tBad85sG&% zU~+9jt@X4MS7jm>qPN3LHA=UvIX`xfvX9AF8#OBKu9-c<_Z;iwW%Zgc8pX-n)dh>L z00p68NA<~JDF}v@UeB>*2Ge6IHccN7J&Tq!TQXtq%CxrpDxn^^9&719R4*64;n(d3 z1K$IGS|dbYvbqE26M#g|)JyoES-ICA^OSAj`jfQ>3`pZ2#xJJslc(vfCHA*zt6}2& zZcTz$EXnm>VVsb6^lva;WfqXK_Z?`%I0^XHFjs3j>NSH$3`6aY>VnU-|8vUPU{2S_ z3Q@QS)Iq0TE-yMH;Ynu51IVk@Q(1>fxyGKqr+;^y)w8~e0_+&C5F)TsB@EjFQ=E&2|Z65D5k>5|<6+S2{?)7;qEiM?P8riRdZ7m}M z+@jkFp&_;CiKfi`vrWHf=xY~VE}L7WY{$!Lm>EMVTbn@MUG%x29dOr&`9V3Rph}LH z4H_Gb6=c?F_VLqd(roz|Jj8Pq&=+?_92pzp@PS&CSTa_jrD=8xO&`ta!IJ50nCILC z@%a8UgPOead}&@Bf%U?K{>E6oo)r5JFS4B&ct(`z&sK7Tt4$rcQZsyi%WWSg-Q6GG zh6Ti@hTl`AVl%Zua@l5b1xs=IXBU7-4- zv8JEU7jyrMdA&8ocu@Ab_y&n8$07|fS07>mji2L@Zu##o6AXS(;?WWea}i*rXh8#^ zqcwg5Au%D0?dJ!f6RX98xtzwV=H96&mC0oD_f4A`uvk+X6SQJe44 z&kGv#l-|Em)pL&aaead*5> zk@X&h%PITki*{6atT9~1*SDJEC5Mk#-6ixdS?gTjwDuK;w*TXk-G{)ibRU7yTo&%( z*eZSIj#RBCsCSio((cUwe!K>7P7uvHB?90VViwU_bMxz!N2Kd+w15A? zihF+S3yY60##{%R5fLOBl^pXY)@*>ups!J^1EIlu7_9-bDRH{^eHp_-SI$_Ygm3|> zz(~Asvt>Au0yzZ#{DcZBhY{acA#q{b9c^fw@^0KnQ@nm3shg{$uGYf7<6E`4_6L4M zi30v0mIg;!nE;}11#je-s-pekDlQUe-p%OA&Sc#hu3rbM$u5q~;^BE^-l$xLpyw*l zM(guma4%Ld3Ma4I=g$detS8-=sZFcMLvy7*5Ipxs^hi~_M)Djk-9E>x8L|nZEf1I0 zN8NOCNX_=?Z=UI??vGj9eo_HqP~V?rp0`g;-?6HvrYg5N9N9eaz5$Il6#4M>d zn_4c4AmtSVOfg$$uGVPclO#HDkRH4GMQ6pX3Th5s2tiO%>`~E9pSSUvBHsK~Fc^{z z(YJ6zHQ*jz-!fw#-Q_=55W@0dbiCUf_qza&{wSyKREy1aw5W}SSCSf0Lne&B;UkYP zFkr#6*puu)8Nrz`pLR{_>ShY>GHbtyl`%KXag@N0)Of!2HGb|FXJq%9pUOi4?auQu zHw6509#fWQj@UO0roV%W1<7cXsimt1&pH&FFKZH^JQ`i_;xELD(Hxhr|A$urN@!*P z*bAsugrepH(oQ0t6;Fwkl}j3>_j?f3)W6QC7iP^6ucT%!|@gHkdOPm39bEI$5Y(-j=IIs zY9CRS+kuE12*N z-_oMG+tsD5*B%~@&jq&8A!!;|cKEFsw5s5a|Nwzv?qW)^mg| z!!d3@FdBw6$)-mQ1O~A#uRU;FM(;ZnzE=Vi+0TOi-#B<%bywm&?66UVKItyk?p}SC zc^_<>c{#09*I1Kc=CLED{IcNU6mw!Wx^M>%=KN=bW1htF6)l-yr7&ql6%B<8@40FP zDz4T1UH{ROa(%LW+TKTTW7f>l#q@_8Pz z_PmLIbu+dPwRoP@`;5*A#SXl6BBdGh8t`qYJiGqhV-$?qw7nq6iJzxK~(Q8SHquXL-!-u z6)&E)S70r+;GX)Bi78HYgYwm&0)G&ayJa; zPRFgvL9QT>;Di=A=nO|vq$jfx`ZwavvW!)O5bIWbVnE$T$ndmKz8ZQPCkdY$y1tPfHxOa`xD&Y>T^^^m#l`o z%z!#NXuH+TC6Ms!?%SZt%(1EU%A+xb1E#{DF3F&T4h(-6y0||C!(BJu2YcYu9GQwH z=;7#5P(Ur;0I0s`i}xQBrKi~8VsTYZEPII#UP0!8>& z9*v?~aU+7aQg4aQHzB|ljz`0vzOcBMmBbXh@XGn=h|enMFS{`N3ilzBUF|~Y2p3cBxmLS;`}(0BnjO-;ce;_<4CL_c zCOFIzV5r&i;rOu1cUd8J@Wr%NqjHGyxQ0du^?v4-M1Z%^Rr;`JQsXCvqy$(7XNu@& zFLR1C3C*p=1NvaTKXo%5)w{J>V;@glEL*|lYC&R7UL zW?`Gm4Cllg<-Ti_!F&MuqT_I=2pVwr_2VBG%RU9#3>{Q`7E-vKc7{Wbvnsnt!6una zLFnwq8^!(|su6 zRkP@KrQTJD(FPr){0TX{_e&1E;mw) zO4sVcNwEYkWK=J?AvwpZs+3R7K?VIHSM<{%j~b!{SnyNS6E8ZkJeWSTMbG8m86X1? z@$lSqoVGIRDViu02_`pfeQZ+x-MoRMs)K(LOk72%d&E~TqvJVTx+l6nzlV&eBV}m4 z&TsTN{VwugOh6T~+plM%9GaX8OG01IkA<*>IGx0S$wPo4?Y+BPZ_03)#N>@TCyVse zvtE-{k`MDgP5peGBrLu?Q5zTE0|Q&v!IB!dJ*lybw+dLANn|71DE?<_Tb9nu%UkX^ zD?EYS5t&AjkG}D24!5d9P)Q1Ntz+%Sn16#myzf6reDH{kc&@&#g91*ohjzW2LF4^h zkQ!o%Jq3q{)WUjQXg05+!M2r%c@0#pNkts$2y2s%$?wwFyp;dc{aC;q#?mDGHp;$} z(^J#mHVxf_Bk|aTFNXJm8{8u5I-+FkBD6BND>vLCuP#O8FIH1oTH?DmAT$hLlZwMA zt@>=FZ|BwfJZtu|9tLym8RA|xByJl;*mbdcv&~hngwT?OVR(t76c{01 z13If1dH`d%jV81F5j+9Xd=Lc0qHk%l+-v($V%F?pgdq zVuIWdrVD9XiY-1lzfmm-=oBnZc{}avSI~=dWUBvCHctHgTMdGt3p}(6{50@WV**W+ zbSlfP^i_c!YY;!d)1zfdlQ(c_{Lces6T;K#w8;Q{oK%%%la-&b;@uK|8~GbM>CS^( z)x7kf#Z%w(aaO*MABq$294j*wnG;{gO#Q~JCwZv`NktsY{Q4_hQ!K4~J`oWS;)m?BW z1k#pmb)L%$99v=p;c?~GsDUma{61&sfoX#6bu_DA?FZ{S`a-DW3k1OvMvgy~ zzlOcL^GEuWWWIac#{c1!$n%-Pvpl+Uzg4PdwI&*-Bz{(~xaS$D zS1FLxB%i=<1CcrypxriLp;Gc@eDH%VlzlzdQJ;Q{A$uLg7f$7?v_Nc$)LG*JONnaCqO|a)^w-ybW^B8sx|-A{KS$X#MAWPp#~Y zO*mEQ%5Sf#)nKb6a5h*Pfom@5>)3_Mfr*BqY2)KZD<#e*$Sm>J@4%{Q$>? z8^$$iXM`??Je!p#dMO7k9{mg*Rk$Z{HYYCbu&r6(HC|d-l=ycyB~yKbgE><1N}ShD zRgFig2=Ck9iWf4rF)Ni96Er2UTH z7At!Dr!?(=&-jcHCv-%Dgt*9Al@3!`+*rTnj_6VUUsicS?Jn#9v|8Q|3 zlI!Gr@B1?&Ji}eBMt8%tT)6%|6}}|)8>J2wJ%)a2PJp{WV$7fP-+H7qIymsQzJ*;; zAV;)wli!`u=V*hQuZ~@B1$u@9jpaa&rS-EUNurNvLVvk@{pup)O-1i+83o;x7Siof zHK1t=eHP$WvN<3mLcuW2Qj>pm_cn)WNzQe@T1gNr_n92h2znjlc~?)aDnD(0uL!eX z;@_EF#+$wP(OyY%sSTtv4`fzxVhl9}9{U_#8WSK{cM63h&=7f0SM{GeRXSGHHklVWHgaTr=~Nmx1b$;_I{Y8#%NVK*p-&J#(#?EqQ@yhF3^O!9w}pE} zL+qT(!0W8zZg0|?_>o}uL)h)|iIzh9=)B9bM)bw7g)Yqw*)itYCDbsa-h-@p2u6Q! z(cSi~qQmCRf`=c=B59_b(FE&@>-{|zx%*ws$>3Je$Z*B*WhlovG`irsGYnl1wRf|7 z*7Xbc@BD(4)*G}&gNtL)>{UKVe=7k!d^(O_2T&UTD*owJo8bcYVgPkudOp7SuxD;F z@ITNtcM_sCcO*h?J$=}*hP9awL##}GQ{iW`rm)j|468KL-QL5IR0jqOwH)Mrl5Nvo z$VU0}zE%0*HWjk_W)bago@k{|@(qZVgBsx=0B`DrsgB+6O?Z-zezLl;^ls)skwukq zSMV8tHgdW&CzVKqNg9+uj!_JQxh>}70WUn{-?#5ySjR+XpIFk2!MJc>Y~M}v8=Mdt zX_@B15V*FT9`9lCEMsR+*siO?BK}RV+xx=%-?)Z`Z)tE3t_^Y3Ltr|^w{CA8F?Y%R zHtt=aqMm*}%r&&ort|rC8EfomT?Lace(pSbDuYtk-riBL!94;G3l-b4Kb-TUA=7o> zQBCw?XMg`7RUH3fC4v{%)vSeq4#SzkmubVNZ^>~N*NT5iSbizrx3kRB)*iWcrB+rM zTfl9Qp6>t_vy39(3~IiEOWhG_<_aGIbHZJ==y3kGOM@lSH+70~4I2Yq?mr%vaD2`b z-b9q`eR@;&jl8)6;s#6XBga&u9$bsyAvo2YBi&aghPV<%lTVWsn?fpAgs)u#9{9u_ zi2ot4``unC6+E3N>f21-S|WvkCAgif(V%1pa@L&Nem2q3f?wH!xY~vT=XaF508cDA z1~!_`IOgEGD3iBT;E=OEpp(}k8AMzCF2J9~dOv9w?pz3o0RznV`)TSxkDW1;T{TX7 zz?!{a&9Dbez3_tobMMndjCqeH5Iy#5fy!TL#23lQK#gDf(Q%uIP;?S-1s<&%!u;n-gTdTH!t+ z2g2Fl7~$PIqHm*Rzc)Q0@$NSj{v%=fRpz*Q{YWfR&T)vUMyp*7|6f7Qo4=pWeO z891T>)~cQ!E`7V;uPhZkRHVS9~&Do23T$*Hn`!v<3K*zPnGWXze|aP%{(EdQuc=X3aWR25*OdUGQB}cS=^9 zW*tAf-wk;X;23bEmpY!WqI6lCvi<}tunlfkPgG_$KiVSmg|BX6_w{hVSb2vNz0}-f zpXqzqX10^qx_r`(@6+@U!~X|*7#;(8ccgzOe6bzap@t?Y_2wGvi(CspW4(gB|cPp>}+?=eP;2`EPbN}ReXL>70yx*gg z*{>@9Bqh`aUS5V0q>&iavkE5}Iws(y(RsRRfGfZYm3dOXbSAhXkIyv7Z?ohnT{|+?g`^f*lqrxopm46 z@a4)(;+Qo zp;38rA&crX$4j58^Y@Dr!ul`>qC}Y#mx%Ge(*D9bgjH{*xFwCiL7chAtA-5-b z!+SYi_sBw1Ojm8bUZBX(5N!1fA0~c4KM`wwg7rI0w`_a)zFF$9&-TmuQ-p-K7!5bF z=|zy}@ifNVCay#TqaGhBg9x0;2jpBU7`0fALzxfV&tF}6%gd*@DVFLs4oov0%8L$p z5CSAu6GMavEko_WiOMu+FrNMn@Zju$-$(e+0kMj&-|n#dbyvw9 zGkooyDO_QE8dR7cII8!;HGG7N0ew0^^o!v5qrE#MK7WsY;nSghaCnsvbN= zo;F-IbE+yRWZ!PRTDAHQbUcPhNA8t$S|e^J^PVv0{`8h0rsO4*m(6-g-k6@82X8rv zhuhNJ!L!`d^@-u2gYq(o;hYCpLFwfGKs-%QAIeC2kfuUZl*&kGN+>G^6)Fc{vaC4% zoek$x)45)DM3~mrpnNb8lOG^MXA>IX-9Lu^@(PYx3FY5S`XmvlpnNpqqkD} zxQ{lu(3{1Gkb?&jn+LDz-ZFocFB>-V6=)md+v(7neRTxT0J_zml+hT{SZGN3Yn->P zE}{8aKW~Bb)R|d-%IN~rKr?U+H5ceLBSmo!n-ju0En;$yhWixJQ?ag*_%9D8M^@5k z&CvCo|Md0R&MuO+wH4L^XBUX)j{Ex&=`CZIrjuawOSy-IScSQU@@!$zv2uy zKHRU!AxLevl5ZLYm^Ek3$57O0yM9&`_JN?37*n0ag-E*;~z^E~)f{5C9cm9zM7nK#7{N3>3B94pw} zQ?iv|jaB-$4cZXCa@r4WIWzAVrXh4;`)~o^lmCI-JSbhvo7Z;Lo*Z%X5YsUk=)8$i z18RSl-tL^N3Sq5qlGkRp%Ur}am5QpRH#wn()kmgWY#1uQ3~^rQR7Ok(Ol?OMrzWPh zyQc+ftLBqLy2WHeqM+Lez`EDRsRgG1h$(QKFElw|^`>{#Zy@k7{yEVd>n9u<e|J3k`NRW42?t!sT}Q@C!ZiZ>&x7)>1t{8i#9 zOmos1V5QrS#5D zF`i%s;<%M0ub$WxDv2@lgWlUXFtv7heeh-Iv607HJn2&cu2K`9qqp8`Mkjc>X8miL z0kQ?gio{yH55}^359paohCa5Jso|zOtFuABFl2%szd+QMc9@ZL42!b4B`FhINGnuP zkiC(RZIZ#N-hArgx*f0~QWnAaU`sG2>2dtHx0v{08F`u2eTDP!bAkt`y8cr7kz^7U zWMhC0TC^Ajdh? zF8_f(GY@`j)48-8<>H~=#zfzADEs^e3Nwwx-^Y1fTe`p~`-GggS^%HC!Y4}r0s`Z$ zYL4zHKaNwy9`K~vYnWvJ^&szusCZcexxYTwWK~;J+f^>Xe>|i zN6swD@43vqB?g8IQw7yCKB}c>+v!Q+cW)|yyJwa4U0f`>aO9r(MNA)i=-qFxFr4Z(-ZRYsc@nZjOdt6D+FcAkJ3flFh9jK$3PujK8Av;ol0EPx_v)e zD&~nv#k7>uvDzIaT`^;Y=FYx(+}_zkKhs<|M)pS8#IAb(jB(&2mF(f|R66?6fQOsp(Z#$Z?Y9Cis3Aq z^Xu7=`1QMR=wh4_@X4oqvh0LC7@2s*55H(j zSEu>jreqZDj_Ro^Vyr}0?`rMW?waP}{MViXLKv0RP$(SwuqojoQ1^hkxf0tIMQ&w31VnmREL) zKgh)IBUOsydV#(YSOIoGLQuMwli#UyW&E>@Z1QCR!h_pW$*JX!*YgUZE_e>18#9|t z2E)KbO~GHrUAgh2Q5UfdjbrBFl3!1#Mfe}ozx5jSmLX~d;c z^)xNhx?u0Y8OlrBc}<8C=ppkwiIB75#3nopp`p?RJGqjKDC z`1(JeDVE=t`14d7M0(o%whrcUwq>lckmX-2*HHD4S*oJeoRUyT*I6plHCN}`n=xUL z+t6Em-DgYA*)u12TD&5AxR^PrV&Lhs+D$d4R?+1x@4Hh$#ndUQlq#(?GBNgD9H#Zc zZ9j++gl-rUic*J|Tbs1oMahTE@$wj{1uy`~GrZy^4-)w~Yd~rf%cLR5xdsdFjF`Iw z%H>&tO(ma<%t%3Jq;0K*2Jb{H{0Gv4N_8}rg+B11$@oiaDI|Aukw=RqyeSIedbh0@ zuDgNAzl+Is#Ut$!Pjbu%Y53b-ztID|EwFvQ_gVgm!T{&<8ziRWqAHZj{qlWB3+M1f zEhA^wS_)XJnWHJj)rjn=ppD$bl>FI5X*m*_9+95P^>8}O+U~MwiN^|tqAIZ&h0~>5 zsg#t|Rds&94Bi^et5+(34p~ROV0`5e%LoW8%d8pmrqKS$LUi$SZiT=YSS9qT&@01! zmuS*4kRF&dY0mCQ==loq&My)->Us5eaFn#271JSgtHxSdsD$nYvxWYt{B>9LPgrd& zt10U=;{|Wvn>+m@1e^&K3O4*?{DciFoRg7b!D=rx5|RF-AZd<}9rpFKkCW0}+TK`| znD!v$V(0ptpx1-@L`2epNbISfh$tpnmIP8VvP3O43 zJCS11smh1ULmQS5l58PZeo>ZF=udAwpCbbg-dx7NT5*Tz!K89k6TB=}Y{U^*#k4lo zepXw7XPy*I5H?VTEF$g01j|0_;gpjM$1c^CvE}D!a4g6nR0R`bLmkeBOWz3_EfhWr zXPb4GmU~?0*(@;}`&~ehQ02%>9x?)pwT9sMx8ZBbkh{y5Y|YWe7GD;uryhRmh^ZAO zD`0*QEhM`A66!Mf&WaK%gBxhDsLO4^xz+2`ZLl=9aIoR32cfOl$Qpoa<`2IIC`*Be zP+lDjS)5KcG|g8b#;1mpjfLxEDph4L&dGpYe`8X_qCQPf+ysrvOEw-T;3{@eTx`#JGvfVXV6FEai^*l$T>)6j7%K5v7B>wvp9GAhQv|9T z;zFu%4949ZnEg2E4FTciv%(y!HNGVAC`-rp=O`1%yX$&gc5D~!M_qD_u&|;qI(_>* zYfDos)$PNIrUL!&wyC?Ol59Pc1(4}%{4d&N=p>o#VIES0I~hrnpSTWYk?(K zV_JcYf%Y%4ZAdfoQMbYm+M%#C_QixjeXoBdg069CqB}5@0GdYq=7SV8tscSYvBNwF zWjqw8bOBQa+MxDp}x_FhmWPk4%-+NR)pN6CBN$ za#i!#>%3_itMlvJ0=A;x@Ah79WD^vVrL5^?+ckoeMw>@x0UhTNew4A^TSgYcF40ZQb5 zpv2XMg7AFoaO;=2@4u=$yE}dU4G}+*`M&!fh$87<6)+z~+)hF7_KCVcDB5mlh8F{} z0_TK9Ef-LVQYZ%pZ%j1fk}(y@E7qYmy8>s8G)ac4NEl84G60ENP2CMBkcsc5Yzo9~ z60@rJM4AY{oA$f>n6^R9{F@kNY=PCoWnW8#x-Eta;B9V-q>J@*JuEQ7F#KaHcI)|v zU*Mm6(!e70*i*r;^^XvUl#8Y&nlu<;C2q*=T4^Qp^_+XJSAdJs&v34m!pUNKk`0lY zF7l_RaRbeY=$qlOTS`e}&$Y^ZHXwz)`cLplM$Ze>cW3LD#@gw2cPWSdaH6`yLsq)MjjYnF77%ga{_1)LnA(BX z<6@}8$Fr_JMcmC3~@`w{&3U`ElhHM^Uz*Hk(BT5Rx zT+C^EsK0)JM99+8yi*n`SI(UU)SJ)_`}eo5-V%~FklPvM`?F2q1h=coGB|!8I~Rno$EY>+JYK>ME9juTi~|(HwETAdO3HZqJ$d78!HpEh?8U_TAjbY++{lj z$HHjc6gjV--mbuz&QK&LZ^0aj(jJ@*d_rRK!D_?tbYWqO6n^PlZgu!%Q*;xhQ;o%lWcT9 z;+zaJJ&r9iUkkjz=5&Dyb8pJrg+(5CT`aPbV`*^YK9{q#+D)MVrX7_L$h&R1IKIh< zripG+G_0TG$fvjY2{4p#cS5MhHVKYW(mou|3JaC&Rc_)uo_~1JjQjC)ZjV?z*<=xg zf;k=dk0TIE`gO@x_sDt|&$Wewe8~s@{JWAZTU{31v=cL8=Ot6zPXeZW^_vu^gXmh} z2pAmxARM52QHn0eR3RiaHX7@ub!!hBTD5#cKW(oyY3`ev8pRzJMol z@=voBJuP{~68ny02=zvfH)!k()#>QTMip#(ZOmpK|G((T_?Br5k&eqdv{nFIkJOSj zbG$$?@%=e7?RSRy8><#X90=2P1ii|E{7aw1cM4n~z-l>_2WwS=$DV_!LSHW_679e! zZM1J-NE#~TAG=?Q{nyP2^(Y`@)R~!cl{nUGofB1g#&gH-Ai6y11OF{aKm3u_ce~#t{i8}QN+q#Em3ADVbZAHt#4#0&b;mT7 zEESLzmN^xKW;_!x?w+>eOA6sUELa0_cCJL@fIjT|_-ASOO)Fp_1r($O2i?f;|4_Ca zX*|cDBrg}63P9fxp<|8K{f0%zv~x~(J(+qweW|P^T27e+P;2~!08iXpm+uwECkaNm>;UW9TqsSbHXT- z;MXGrrPIs(w?$9xMXR7Pewe~hwAA9<9bQ};n686)IV-a!WyN#0MmELdmd~VRSN+_) zcvs;gJ(Ml7$|$_|U4bU+sYG#ld?=6I&NOY{pS7V^Q zpQ?EkndUeF3dOBupTSM4MMcVO$V#=!=lQnIZHtY}yc`{)hPv4vjpT2AG&Ck$VcGm}i(V?H zFX_kd;?>P3NxHqQr`N~~yP032%%OZZehkr$CTg~Tv!&&3VeplVSsv!FF!|S54s6Ludc{EKdUKgROpb~eY4oHPg6t%;u zt<)N+WK0PH>O{mCna0m1bUexqd#= z7jNq{E7w(A-=;}*=!>xM8qhJg&qS)f7#JlncIt&29vR-r&fSQjmS*x?7{ytj=K=nJ zxQ~4WFO2i5lrBkq8xr8>=6u@nA?vr@<4)lzN-nauBdQoM0+=6QDgI(>p|5e8+gr3h zTzot3<(Ze{Ft$A7=Ah$Y@AjV(^42W=5INS!=l+pu>}XO!2sG)TJp){DYjCna*t6CJ z-!Qe|6=ybwT^>Zq7AVN|TR+Hdo+%Fj2ZaLpmZd;chzR;#_V$KYr;lUB>SK*#QUX`+ zu_N~{?pl8jsuL@k^D5az3jd3L5ZA)BuuJaU|;c?!JN5!JA*JOjO3-oBGLX$ zn9B>&;pIM_IYme_uKqcus;x?0xqds*F1LZlvdq^B2@Cmp=OXc;aUTK{3_cH&wl{O2Y; zj`d+hUO<6#V%RH;-t&vBuB;$}Ki>PbP2Zi|{@PwEEznJZ70U97%J4L#rJX%=pACG9 zewT%me1tTzi*<+ORg?(V>Ic<86l5wOa4i3-R*E|>e}+okeWcv<&iiXMAbZdy zD$`y2vuPX)QBpeUG2E~2%ZJZL;e#b)^c)yq!c?+BKz# z=W^vn^@8biB~Lj16+}vBTnNnS`T57 zdfdxjF^Ifwn3alwB7Y$=B_%w((!@UX!HA1IXVvp9a4A6vg7#_A(VDa@qsRMSYP~v< zo`l6*!=RXHKp@FKMeb!<0hi9!=Pi`t-}4Jf9Pch%PEQqvAm=oj2-?evv%~ zs zr1IW4@$&LU9-6b{+I`RNh*BqG6tMMHl~YV|EGSG006et9^6$FV|7tp*3Ml%jC!no? zW20NN9OvE7E{Eefll}ry%h)@ZAuFQZ4Di&Zq~(Lq5hzmo*R5lVHqP)&6~H%M{6D@C z7ApBc6FoL$w9kk0AKqWO;S{oEcI58t=T9+}LuU@d$V0_OE`fUJBNU;jHpRX>8Mo;i ze<1HDY*;U!BUh2D6?rkv=`$v^QTkNXTH$x5&+j{AZ}jxFNa#*tNG5gzq#4$wA~g+< z`YXCJE}v%?Avug2KUwbnBPC;GG*{>?B~|5RxsX~Cr3IaF2G&~Zj`4GKPO8^vw%;F> z9D5ovBi)sId1}=Gy%&tFvIo5myqKgBkfol%PO3lt$PZ4Gxp)Jn`4a%XXTRF9SxX;UBHsUjzWw@##_}f4@Wy5T`Tk>? z@bYhAfHsx5;8iyE_pE2Q2L3^##+J?3uj%{N{ly zHRUsKXHM74xWc6dpy-|HrMEq^U4IIa;1*>;cIlOKdu=no(aIcL(7?66MXO)38TDLz3qp$$e@HV@wzx9hFa0k zX-7#xt$nVt3w@DL(UOLpdeVuTLBGh86Rk++~w?VCKQ@<5F^`z9*(n{6*iTQvh;|_Z!K<*(JZm!{@d> zR}ZGImXCZ5>=}hHDF$sLL#l=xnh%wk7gN8Lv@zB;!NcM*ASK5s*C0K2b^W;=g>u9% zNL)Bg@^<`?647>@v)Yv~jb_q-h?VENH0jklU{%cxh3MQ$FspK8?Ui zgbifdtbZ3)yN=g3eQ*9O%}O$|BxkRLX2nI4f=}b7;HESDtJ`RDh1z=C$VQJU z-%;Aw_jiitYv$I1Hz^IhrkH4bTy1su%&q)vYsl*znQ)@{wkNuR|A3f!dtcPOw|Ytk(bwiH2eniFR{#$6d<05`S3>(>J#VU^j*vK4TeMXk$_fDhU5Bb2V;;hj!;;hVG7cmrUd!A}XErE- zYno7Z+`#n!h^6-AZ z32bpK{Nq{y5TpqMs8buDv3;T}f#FYf+;M4qxB5pRCdX%LYa?dAxM8!c!hx|vKuO;X z;6`b*&@`!eh6`>RyNh`9$5KbWZ@7Z&^JFd02X}8sLaA*5iO%j#8Qh&_#bYqj**~~A zuB;BPJ}=$#D0C!&v`W2`N^S&5c7xmNuX-5X)SQW?f?MIJc;;aUB~IUB z!4FD~^L_t&)kUZ<;s;w#bRxa9E5Ta3P&3X0Ejk=deNztv!>3)fcQ^{?jWo(=e4Q1} zNxSxIo44Ln<>w&c^;e86@T?_|uib;A^}$JOu_wpz7#i#huEV{+Aw9hp+7NgiIz{Y1 zxXNPLw;BC2^2%Sj7C0}C(Rm{P^Gz6-hb_l#doZP^^hy0$>~1Jf>Xvv|T-Be-Sz%$3 zp;bJ#z|;)E4WN6FGQf&}djRBDEgSuKR`?QFX+-VNY4ssxPv_STu|8I<7y5i33CJ>r zNtHBYr>MjHs)VcK9p}#0?r^z;J=|QKBM@>ZdBkhB#J^ByC?KHl>A~L;0fgxC(Eyk; zTp%zW`}ZbISuU`A`(!xP*r<2DO(=g?(upngwq*29Sx@WR#QPqPgaXEXc1llL0KMTg z5I10p2RP4W058oRv1UOBq@9O_470`6EtvzQKqGZl=K9>g5B!khjs5y9rJPgHhUj(@ zihc!+v}ORR3VvXHX7O}8&4hvffrf)~%Mn5-m*K&+SLm~X7+7CtBHy>K3qgMHW7l9~Hi$}O)P@lr)P^Kk~YHq_KQYi9vVPyHn*|Ny4sj?!w6Q@kz zyaa2{xj?xfX*g7&_wo=V=;^3dZE~$=wvK#TdIO0HMF=YY(O^@b4j;M{yDyETZJ8e{ zJd6Ks`HC8Ahiz|I=*OdEUP%y-89}bjW3%jD$T9*peYS6JO1s{1m;5T3qKlKDw)`qA zeo63HpNP}x_2&@wV3*i8EZJ$Liy!K|x+2D0FB*=76vj>XyV@Pp!Ms?=GxMb<9W55z zZ7-atRPmKwaPa0@QRcw@@`8myS8}b^GZq}ap6lxPehy*vg2!jAcApY&xrV}(=swj; zue6*H`YnkWW930DwC2g&r!2&qyfIGDiQ$CbXNJrougRC4z3civ<+wha+aGj1$URP^ z%I_y)A*|Q+-h90od^TyCix8E&GI1%ygZCz#SkB4n`En2A)VVWPKgRQB;!Ny|i!Ztq zZ$D^)d+E)x&@YXGgxU*kSD^i-8J^prHnlTUaiFPjKib%)&-otLe zCvi$aq#yI1IlqM9{UjvzMJIpLzF?98U)QO|W#I;QwY? zfEI*(M9zKwD}7YU(S%^+YBRF3`a#>Er*6(@bmivD%YPz47fbl9EWHJAOT(U9k?Fj>qK@z6 zt_+*4y;t1p*!YjpW7UfD^nOqEb$g4Jm@%i=D4hDWcpxMG8E$E3WbU}cU88pKf(kcE#P#0T%m7g>@kp`bQW`Ge=%|GrO4Y5^_QnqJ_5g=Q20cz=J9Pd zP+y8D4r)VK5k_~^l+jmgXc#$gu&YDtVVt-^Jv1y%QMXxJq_Vy}-=$7_`_gEjDH76i zt;+80H4+ljKf;qzqIzC|m|6YU#SU;gGR{9BW78?dtC}hHH#A#z>q?Io|D;fU`2U)iYiG4@$ z&UJ#*p{=kX_{tQ!Hkk3SV5+5IYxzfz)ArU~r{BM^7J*ZXd2bFPJJ$mzeZ$EyQ!ots zK(|NgS*paB8<`HHqj^5;Yoxd~gDQ2JTFMiTpLw5lg zjM@5`po3-ppSu|(s{0c{_jrjulSv(ZB<58Ll&c|A9b-YM-zQY_hloBa!5eGS-{H=Z z-^n;*<-9VA0v(~w$}{h7clh5`A%cICiZ0PWR>>Q+5M-8=RAm&2A<=c3JWn=}W~uP~ zN*P72Ft15P6p`FMe6GxY$ZLtyDI#S(>^7QpQrq^~=FlBmqVYQY#rkn1tT3sqRB*Cs zMde3N*n*wAWYK5vVr)IbGX4yJu!_vgogiofkHk%`R{wvkhi6B=b`uX=KdnO~T!z12 zdZwtUGaYVqaJAn)zUfjH3rzf5V52a`EM6{l%YsgVfI9I)lkdh;ypH9X>xTGhgt#0m zPQN5}Hi<`Xehg9+l|{|Gtn2A>^)l4xv;^|dh0}tINd-u)hN;rTob2C__6Gs=Rf4rb zac92^OuY_qzuP4FULfBnhRo$`0u7HDDivT~w`t^NF<7WGO5#1i6icmv*YaAzOj6GyfCe#rbC9PY-voPxs#oTbg_>E;*AyC-?-vapqk8*ZgZ+Z9f6Yh4 zhEVwwxcDe+BstvGo6~QT`sDsQA7m$~j8NxTI3AzOA*TAL*qIB#UDVaQ^=`# zvdmw~`*yr3Nm1lGE`z6M#0a}ev%bLRP8{5S-TMYxB_!Bf4tm!!Rv-e_J8e>w&(oI2 z>vJ7OC1)1YRz%b&E`GP749oOI9)JYVG^#3#-LU)N3z;AL_Oo?08-LS^}*Lq|MVLiM-4g|g3Fi1CkKqPFJF$oq-s{3tJnRwyIw8m zHC}6^JMQUqBeXMZQ7fj@qHh^>N2!zPZXlUdmBC#ij}l=R4*;J^;C`3b3N(!VoMe~;t(UBP6lr|SMvrPPlIQYf40TYtsx zKBxZYOzhgZ{2X)4RDnuPbuxEp_BG}N&YAb$5f@=bUfkEP{F>#p{PHl?lV)Gbmi@>1 z#CWM*@Fio@$l%a*2e%`i`{5Gaxwt5aU*@6oFztl0zT9#O4)2e|G$y(wq%?)>GZjkt zcWdj1K&8CQhlqZ7Nj1i#aZ4PJeI}9vYVO-28y|=Il!cD~=}(mqdq6!692(C~fZ>+& zeTl%Y&92Q*jg=yiu?PCoucVYW=fikIFcFGuYK>WHX@KsWOHs|(% z+^xyMo?b9=o`?lo_rZgB#ng(5QvsQm(y9->{RO8~^r!)%A-A!a0J+3mA5@mLrU9Sd zy=JjXN#xC^48Px-Hw5__9}h?M%#ySt#vz#{?T;wIoBg&;r1{N4QYbL*GRa7NM?+?PLM?BfKtZ|s_ zBa3B<{i!rbzD_)=YK`;JQ-%O3xI_9%`x=%S=#)ZkTQ^^8;Ce>y{Rd*QF`PNChca3h zCNNmH$5^WiXK@q;Y6bln;`40F!@v;LQBaSQs8`^&t)W!t$Snsh6Ht+2J>oj#)f0AG zBi$dnSC^0gNp)F!ISaXeDk$=RRD$g7X`1kLU}a@b%0 zZ26>C9ZLh}c3YzQBg$lJxveA^t`tVtJOI`&0 zy-=#`idA68kmsiJy?mCq0~!&(v$2nj>CRg)qju9(`zzBaC|YltQJ)6SP>nb*VfGE? zTsOnlTi=Wka(C?$GHaY;_3Vf&vc zZ-+G1YEPH4u|Ki|AOJNsJC~v*n&hdz)Xh7i;6nV?%MA~H$vmItAzBZp(yUc)v+!51 zH0AEf|0eQ{QOqODIC8NCJR2&gIl2KMtQV5GHN>|A=6 z*`7OaV$fcr?YVNHR2N^M`Jnr}f|yX-Z%oR2?d@HRkc)YBOj{$vkTUn}#4iuucn_A(d->Z9gN#BTc3&4dZE-Z|{4ZhQa1 z;P`tmo(&ME$NCk^49Qn7`zcJ)|J%~(^ILDf5Tzuq;4O;S^FgildEHd?@#DE~a{mLp zae=Okjg|1vp~875wzc1(OESw2?Ieyyft#CFDq(CYo9=E2AUfku$Lnko;_P;y316Ia zLpM3lH;Xlo%lV{s7}pKna5yft1?i*mYw%BSYrtOKi18TppnVXvycI~gW{IxEe|^E1 zYo4F=B#ou=58p;txFwhTs|?NY$1~k+apf4ATM7d7HNYUUNU^i0>Tkmb$Q4zUDrOo! zPt@7OXa)!oHV~#s$Y)zR4)>^{qmg5%yb8Sd>(+xdNJOw{Qfy->_~5GOb4;#A#I`EE z%#7V!k3NwL8eq5v)P!jg-OZ`hN4HvN03XdZiwpe2ABG|Nw(oI@9+vxL*{?Sr;BpW! zlJI4lHvZ968yyeYK)V4Li7z$Hh=2PV2u#c%?_OU_j4Mp%RU#yl9+yOiE5!PtJMi>T z^S88HT!G4u8v9sZJSibrR|`DPjS$G0ahNlHc>D$Q``c+hX0v0+hHA3TGT*&o=JDq5 zH$J~LW@?2?E`dl~0O5Fi6R9Y4%^ir`A?t4@dr!L+*wbHP6L&J5c<;AbiGM0Gkyk{k zFWh)kjXI2dCmkHATa15cO7S)_{Z6i&(|63^0NY=y?vL;(on65fzmwD5z zXw!tF!SdoNk)o7;=9ZoP!`ai6almj)##$9h>j0z@u$- zC7&*ZKJon~zoByP!bjp!2E){G54}Er!2~d~M7}rQth{Ey{m`2^>k{RQH<%OoyOZD$ zTOl($A&KmyoZ!lVpz@LMce4sh$$lI=e`|hU8zsO zB;&l-%s84)W_1bAxQ~oaZKKRz^&rQ&sLtRar=htS`rtx|QdHL{^1L2SBCQl)3>3pD zADFUzsMekJ6XsG-px+%MPJhp(4k&2Gnq+snw#e~DI3M)MErsg!^FYW>^KdG7$k8hL z`L9-?$q?hTMan|^1huEFF@*h8$hkHkiOwQl(GLSb!cYVi>y=~icrWC8vUpKmIg120 zZrYyOgK;y@p(w#hzfPfLY~TV4*%UnA9&4G|ul(U)u)+>pBJl2kF}K&rTvh;`xe>u? zg^vO3qqoGJLp^UwQZkTV)n5Z>BS=m*t~FNGnXSHb!G~d%xy8AI? zWAKC$ZpY2MaG8bIHgfz7Vv|1KY%leRl$<(bJfICnZ281tEyK+75&G|n6Ds+TO-e4u z0-_t`6NRBa8Bqk?3`d4`KJ<6RR}=Z)z#m<)nSCW7feRj1`=&U&yIWdHnc+2ghn3i! zAND=3Oi+BRrZO|eF5v9M^r!wK!6?$-7m9w>r4(>MFTlWdR1rIrI`rkx$c_46QD#XO zN?pu5Ey0&vMecWlU^DTRZ_SSJL@fbDW#T^Ld}m3_mHktV$${}7o`S=Td(SnvOAuGa zu90ShnMEm4Bz(IBSv35d&U5z{pOvcE(R)p+CS^?EAIqW+oOqW(ChR^F6G!@9=^yFF zj0--=)1eYODT0)Gj4M>uWvqX(Yo4aqRRy2Ex<5LfnMUfo*gt2-W6Nhy-630VPm~({ zAl@K|>`+GoNp6->gp@kTkN5c_zL-y=7Fv02t;UPn2v*0Kt#Uz+lF|>Y?X0V>-r%rPOp!H)FBl0fXpS-WVNuqs;hOSqIB3*? zp}y@!Mv)u;`V!^!lSFoeW9)Bhl`o#&VGlD*{Tuv?fLippk`Ltv>GwZfY+R>2`X`_g zG;JPgM8Yn2q$A<(nWW;bzgYZ5bt^i1vsG*NSkNtql1l|;+L+?C@`Iaeh_HoLV-dyo z{2m|Lg2QAl)G4XL9`-HWyy>NQi|qTMM@OqQR_!(lnqw@ajZ zFa|o`X;M^Gr2%vrnVvT^9Q#t#?n?QTj!xWf?3ZStXsw^ksHN@nk8U1!ofFe~KenXL z^?CE`skXohvkR`6Z|`_{&xCI%-%;^?7j2MyROqjGYqI~F0{5D;)6sbXTrr!){`M&4 ztd@~U5!|TCWu!Y0jFzp%wA%456yVAiXMz0UU(iY^1v?VBR_eqlkx&uAT%0EvucCjI z_};`m|G|sL-b_Z`8fTNwFQ<-?b00n*kae+^-mxp(8{3$auDJYacD?Z8bP7Uctn-o2 z-hlX%ts0H0*4G-j{q*1_YaPz$Y8|6HtooDd1|(njK|nu!TF|u8AF%k#w=h;#GRA>N zd=cJZ?+NV$&k+Q3AGC8(IJO3d2Yu*i*SUt-+98c#Y(tm*#ly+m6->%qeW>%>B0Hkf zNmCguO_IKnUu-`wr!;=oXYqa&z4L!0orOQ$@B7E~VT>83o2i-Zo=r}7H`BxP(G$}# zOiWDAIGAn|hfPg)oFgU<=g5P@=lA|Tet*GqT^8qQ;`_nRr9Ai3Nt%h|#)%Ig_^ol73A&e-E7Mf2lNq9${; zww&uBeWY~gqnfL$6yJ}e7vd%2_ehcH>2FJy0zaHA^0ROmB#z8%R(4f4Mr4u=DclQI zW#Snp^pixjSLlaxz38zPbdoXbRhU;u>ptY!ov!#_bj;v&r(K8gL-ho)3O$k|oJz&! z3I13nIO<_hV`}b!3H{{w$X7{k0!lrCet(A5-&M@dB?dq z(<9blLX!z}v`jx4)^!R&aXzP!a4ND)NSo=Fb|_ zwMDqX&ve4G=X)^U3yfa@2DusY2_ml$`SAM@uQKAWIA*8m2n$ek31 zBlqe9Fib{)d&DmQ`jS|FOIr?KXzgTakcFwEnxsQU?eah?c;NS)p3hT}s8d8@%KpHO z;#7r6@akzeP8o&JC`V|bqMD{R2h(R(TQx@23YGDCNxMRL-E%cb{;{oT{75t+N?R}c zCgIkQrCMt|c`eIql$JKwHg) zT^B~fgS5VThHX~Lx2$CcNU7{kKT57;+v(>lNB&Rzkn%GG|Aq<%bY7lC@0zhPMQO2J#F0b073319l%2z+U;i->@2LXa%^~!G z$mou2#VunKNUlE7^vI3ddbh~L^YsU=*BbTcht25dW}pS2_g@2VG8I)qq1=;Og_fgg0qctBq+RHR3rR%kt3hJ4p=weI>tW zki19`on{_J&gIGzSAV8O%A>v+jmCD!WG}=bz=kL0Zc6}wdyy@h!F7vmg~2|Y>c6te z3`sB>giKXu`-1E+?}QH@W^zfEX{qjBXSfIpxqJ2xD4RUQRBUv{r7r3%*u^gEyrL=q zW~3xEQ1Ovs>jbC6`OHI)pPj1D?quTbTZZ}yXmJ4s_gM*v5a z$@@5Y2KQELHssWRd;FZ}YN_1Ewv~%BC}rf1M2_~162uB!g*^Y&M}q-HAUB~o5c%xt zHH!&Zy|I?IL6%HmG7sSx;}28RK@{fVL?syIX@Bauzr{M5kBz0ww+)>V@nyICtK1%5 zAg$Bt&Cmv>sNO7>+QUcM1?B69M2y4JlOl8!x%;UyGa)B*xsYUuPqNets44vM=k!Uz zS-$GD^coHSl&JO;9s@wUW#uhs3UtUjP61D}htKkjk2g5wMX`&(AkBh`Q*vRE;;l)E zb3d1Z6QXD_c5{Ce3gM?4Sq6D`Bz8IP`qPlF@JQ9KL-|jcyO~zRJJP1#`lPH;IC} z0M$=;_VZO?U3h0TH-|M^74C=T|?d4XG?uc^Wx-ANDe%%ymL4%AZjM2D!H|b(QzLcz!LzF5>cA2@2xwNWBG7;R<>Cj5Q#R;PZ?F3E5X}YK z90_t7m(U)+4VL?cK1T@IVOnWW%vCC~r{%(vcf#H)Z%-wOP0i!{JkQ8# z4y?>1@LPpY6>M$N17$kZ*caWQ$o*mC&V^6aBaRJoKP|t1Z8FwGijW|16J0qPZW<2~Lq|;5aSwR~q1F2fnSliPB$+{# z*|Y<4JZQKSfa@I7Rd{4;{3Y4t-RNbtf1T|MTa=S%aEH9loE(#9mTauEhf?DK03$P!lQMg)? zehqJqOclxG3p&gw_?yIRS(whtgPr*%$!8>(SVwZj2R+OBN*lSZ6H>SSsWC?;wnRnL$0&hS0a{zkvgzt_ zcXTcG>cv5F3U#7Co?pJ?{myMD6ETuy`+^!5x88MNJ~Aa?LU4<*#4%vl+@6y6lwMou zU_$)igirE6tYDU#ILeUO`JjGD#wO_n>-ImKXRk}_97f?i=EAvlA65^Z&za-o(QNH6 zv}HA8b#2(XGn3Fdjy-$|X=LSms(wW~wwCd*AD0S@q@88iY`J^-1F-X{L(r&en01Cff zbWi^vQc9NYHOp2Ul+jhweyM?G3Lwc$l*f%=CCM5sOr?KU0l7h#7BU<_CEIS}h0^;C zlVloFA5A*+shVU0f1rs)`$f*z7P7aTqY^+bB!``zsu6N$xmmJl`iyUt$lJ}^ohEo^G*db&X!|);-$mcB;$ti|unkTer z9hGhzb+ZEg;jVEW$f^pkhOuf>{Lk-Mk?(opoMelwv8&&nSZ=q=K5?yMF~$PSApHdh zEF?%nSzJ&AJ@;6+jOq~7!`g1O{FGF(gec!jP)p3pFH=R%t!u4tOFnc~e#WeUjFPB} zmYE9D%d-j=U3~d~zv9|v{W5O6HP-!yIR9P&`)-7X<6<6I6xD{IOr09i+O|oHAd;us zaJ_zV+Kdv2yQF2!T#nr3j4TwC6P6gS)MaVAmlK_J zHJIbt{2x{nP=Uny`3BrL7zXFJ-?Eq>nAU`0(OO6`s-0S%w7~n&+|y`Q|GIw>*x_^o zD7x8Up;w*k=+6_CHV5)%=Uj>U|Nj2z!{RIM%8Cwxm}CYk4oKo|BzRSD?Q>ioX|Z%3 z2Vci^+ugZu2E{FGLKUzvtmUTawWj<>D?wRPLd#=bTC=57(iSiW! zSy$c!6q978yUlB!*zrS>pVCSMr*?s>m<3m;n#fS0CbivFpnV|rDQZOtwaKk~Ln@*C zwf#klgmbxM*Xf8n2h}P32i?kG>M?hpD&Raz{V!3{wM!XflIw zGqp@I_nh<@;CuhHh*+CbC6v+GsQM#V1cntLulE}_P4CRub*ByM9lCQk8z+dYHaYMf zJvW%85sOF7!lw{?NGQxXWxF_zxlz0kMLn3G5c%@?Koy}j?v&mU&Gneee>P1%M%@zE3iDfNfA>JL-!p<9Omwa{<$_0`7(St1mV zEYK$UxtT2X_?fHh7g-ypM`O-E3lWKmRlbf}-Xqvz8QaV&-Xl!hNTal5fbCl_Ed6?s zNi^n7=ZsgLd5|aPD=h#BayDVH%hr{(OUy!fXH zv)tbyFwK1fL|!Op;i&(%__zf{F;Ka&r&h8cb|-qr*l)}tz8&2$q6(`1@!0zcimJ~; zIt<%9bGiSFu@k$F!LQb@A`d7c=G@Up$Rk|w_^hj3`nUsQPb}k5g^VW7kv+aaQ0I!9|Jk#99g|>erZNoOEY&SmJD|F!Wuqu>(ZZ*A#kyS zvo)P-%xss`sF!+mzUvA7ho+8;o0?L~v(32877zl6^odcxr_fxGPy9?9yYLz3*~ z;8BN>s2mI{M`vorx@!eU`F#8c-+dp*qwg!ufPC{F80!N#6#x!gWC5)64S0U3WR&7 zS+fAlbx$y-`<(c=L|!(V2KZaw53)LI%v zcAN&6dvyLQD;u~dA){WGbA%ILB1Z`OCxuB>h@^1m**l9Ecz-K03c4~~`99d?{lQK? z5uJIKBU`(DiZDQt!N`(;74dQIl?*EGlNW1C`@{PDZmJ{`Z5gSVs=i@nc>r5mFBjMq zwLd6FH){xh|DAgxMg-(`uFhBA<^Fw<9_zi{u|6lc!?gZ5U{SgSGo4V`8K8G<5}{H5 z(j~zw@l~=++<{hY@-A2gl?4rFL*B=A_}obyt%%J}Am+PoNo#_iuD#h^4Q`b3=hPT` z&OYTZVgElUSuy2sM6(GZ%l*6Mau6V1JqZ5s#+yLvaw?U-PH$84#;}yb z0koKZbkZG)+JJA1TEA#Qv%7f07LgOfvuJ%SM`_jSvWJt!$w%0v-6v2`;t6NZ7o4so z_(rYz``xzPpK0bedt9-XKoB(dDkNnwshf!Os}JSY!2(jDV0tI>wV$<} zJIdtYkpNc$o=7;<8jtREmiV7aR2Fe;0!8!|2?#35)PlfKeJ?z8%Wz%8#?m$}1+~1@ zZ?HU1OWfePZ{feJqt?1F?tP4AI-&33abAFc&3<%pR*cCe#L#E*-h!mQY^AP{i}oFVAR)y}{-U==+)jlm&(WV9YnW z&j&4^bnCc^qLBTaRB7KcGBQSwtT-~ed}%Hj|2-9?|2egg9esTMm0A71(>-OD*o4RORC$dbw)XvebiE%r6RFxs4h|fLhFOmn@%-?%7 z-F|ju9PT8sX&GD@(}*qd&HUr;MT1c|qtdBTfb(ib5mc1RnR@zsGme?G&&`G;DCYH? z{PTy|e3yiaE*XkvkP7APH&Kv^{imP6<0GmJwyAPQxWfufr_er{!TRN?yTG8Tx6O0@ zk|)g(#}m)+bG1KNC0Tgk1t%CqPU-5YPpUy2RyN;973w=h^-Jz^ZG4^f6G=oB4`{iJ zgeb&g|lhBL1?N6II#lP1AP8X>aOkEmf=FzbD;tOK%Z$VB=&F~WnH z5xv3#77@>;N^NaaWgy+c_AT%`uV#Mf7qhllBO?@%9Y-ivZMlOJ-dtBVeUrupkiJ5> zqWvE$Sj|SbK|bYDx9Xp^yWg8}CSDQ_BpR~g>&U|(;CF!4@-gacR&%D?9VIf7V=V32 zG$@z)slKH-5Y2H?;LUNKyjamCBuNr1HiB`#FO$a)pIG6K>q0@HjsF_m{6x(?J6Gj~ zhi9wtTUBXZD30z|s+IbEpm1i6>~m0vVt(9B4zr1)W5#d<-hvV@@-Ei(2hC9&*Vb0u zwehBA1s}}d*>Z1m|3+*T6*mTa+J++QAOlfi<&Ah5UYCE`UWgP%04_Da0f-YWfnmI_ z#PIvUZK57Tt&E{MeePODE^Y3v$4*9Tum7sYz2O`6iU$=&^L;|5Ay9V$K&rc^>X#Lt z#r#&T!CbNSG)619^bb?ocdHt{oq>RGWBdMUNvuMwV9NXL6}5lz?BSydO%fx_RLry( zW57Nm7ii=uPyf8TC;$BjO&q&Tj#6}4a;n-<{y$lsJE@1sa2&MDcknZ$_ki59!C&S_ zT}RS5wARF}{)EHVQXbhx4CFKI5x`LXUw{)PkI`sAOHdEmJOkhe5&_YN5oAFk+7wzY zxGoOAO(P`&tlL?xsTKjML9@c()Jnm20Y)1g=w7{)#BUD#0wDb0Lv@lHOoLyVt;BxP z)WE(tN2im&_^g#;psxwg1R(c~X8ijA_Xd7*9a|WQ=6xDx?O1`1h`g;O;9smQl0oAm znPE05A0VHK=uV(wF4uv!?X&|Dc2a_uusqU0$GE2Fap}7^nS>+TIbA zR+fh~DvE?8 z_cH!lsn4bQy{24cv&8o+GpbqAU>eT;&)qJ)mPu81pK_u+oxok*xtVHt3azuPIxrP4 zk@6iZhF0F5B34vQtVMYdMy!O~%kPwkYqqeGpOkP;ocT@8@Xj>z zeOXkdA9_BrBIFXG&vvl|Z*%igA-pm9$EVhV|NB*l?N0JV6)zLv!IFQ4>CZ7rmb;p(a-zNCTIGfvAetm-fD z$dIDB>Y>cjfi3a_b)FF+Ksax~NDiM^PxDoI1e|->9N~B|YY(DBv~OdI++Jbz%XKX@ zTK_B1ikqmiwI)2!F};HC%ASu_zE%cz7e-Lmsr_x2+y{zLnbW0#fKT=9v2Hg*sw5aX~}lZsSss*cZ`QBd?S^LW74hdMrYqc|+uD7l*h zjRhRqm*Bi1eFld4NR#LoTnLsC@t3H1BH^dqOsY2*wSPOe$O}I(7*#h4>;9$`ylF>Z zYM+N1=jx9-L<^F&g)A=72@^Xo;F=xr^+I-rI{GIgD$E1eTu)Ng*(5!Gyk=u5za?A9 z+)ZYW8&YB|3MbF5@>) z6;_ySY&?NiEby>A5I;=+%x>{j5`+K!#pRCwadGR=8+rBvZEe4jlBt>C+L5Dup@>sq zIXAk$2|xpN+=Ei@4OUpe9jEwzSdr3n@F>nDfC&Z3QyA%aYO6v-b8$hn*Vlwg8;*P6 znsv6mPTF@dpLPk+jr~0OW6M@R1yWXuA-rXuC?2_28E$Ef3=R0_>i8)cN6hQq%_>T_ zLt%K93V*2NZ4F&K|D(RAXb+AcJYg%UtkyLomV`Ai5-&5 zBI(tA^#fB-+;+c~z|fU&E$R6A_r>SDq=vMA<6R9^kJ=JmCb}&W#$&s9iS+N)w7}CT z-qRj~%+rSTvr&u}92rTuD{D_)lar1y>}w`^5SnTcw%D4ACjk;!zgU0 z(N1koXrKI6YhDjo!De5wq>_0Kj0;x*4+Hq;RuUPSm*u{h^Siq8juBZ$-^Noq(h8iQLg4d5DCg;L&DXkXK#xSP767vXm9$lOhlLdG%qA zpE7Gu-4hInI#e{cndi3pB(m164BI3QqJGsj2k1QDI*n;NQJ0Lb4BIq61)M2zVHHwD z0h!HT(`aRzfC z2<^p7diqr>whMBxW(i+a?+-PF$C+aKWXgq!7k7w2wb?Q=F)ioG3C63D>o(0N0T=iQ zV^@&R3R_>9aeFDiBJ<(m?`}bn2!1h}#~ADGzxpVX>rb{Nnx0QCbeR(m0Lz^M1z5t$ zMy&~@aNG#jNhb5A`mF;%ObLPZfy*B{BH5c@dQuwO)c4W`JG}{>%SD^ON}PW>L>3p$ zf+UN3VA(($7|wqjuT>`ieRz@RKUazpuc_3;-@IK7D*2L7 z8gZ5VD5eP3=^cNjT=}pNK_%)$NLsd?0x~o<+HGihr5Tcu98T$Lb{Kta&h*Wj0WuC> zq@r8|#qm$dh0d$(+Ybja*SlI9$?3?KEhL#niQNajMefH#FlO~@!P4Q`RTIGx-209~ zrGXxySXy8`GR!lV!371T79xLzn2`JZU$o7{JV525ad;#DZ7j1s&wp6_o~8}@k}MEf zE~=~4I)|#D7{xgZKZLO}ysm8B^pHd$n#j&E07IFDHrbE(g>ZoD9M_@F2l^h%bApAD zr$d`Ksz?c|Sx*;aS2+R_WaPCHXsb=W-^SaY+79aFEv0X)OPjXvbkLkX_Pp#NHcCDF zb+yjb*p+nhhaz%x@Q_e6^R4B{^^w^F!F)F{EFunQqs0Fjd#XcxxWd`97@xC9WIx~L1!+KGz8=$b(9t2cww<1lSaoY!g zf+kM#`B60Agsjz_c90)1`)X5#Awsv*zYC{EZYIh;>P(C6*8INq1>QCHOh5P8tkjJz ze!p(WwJ6`FQrW)yX__?|dSGjFRpx5${;TNSSF*t~u;AYd@w?YJE1_Slc2apxR;U!Z z>@%BAPD~dY9u}-=8_~dP60cW>B*=J%uoP)su&wzOf1KBRVcVO(VCt3X?|na~t`n(4 z_X&9pQ`gPruZxI5h2sM|_wTOeX@d~5C5zVd?<{jI7PMvU9cR&7GQsaA+Wny+ZFidC zi#B))?rF<1g9=zcsR)wrY=ae*n<~&PP` zstO53L}(}suz>+#{=wR~0>#A#QL)L~_kTUk^$<^TRze&BsS+9VbQSl?9W8O^4UDN0 z_j%gHON+LeYsXiPA>qR_K}wfA^>uWD&QVB6p`?r&7745c@HKifC(4ST1mH$hvAKgl zM7#!N4mUxzW?U(GT`lN+>nB#hBI@!|Wc=>D$SN^+i#wQ+G!gQ94EjgxSg-iKCE1a+ z)ZHWZz?BPTa7blhdZ0w0$s;?kDu6@$)`JO;qSc+-{7-IVj45Q~(49 zfDwucD#rLL%V$*=xRS-q7D5FiVRzS}bS#acO%59mO{J1YKEbe^OD znH{H~zn_z-%;;H|3hjF%QF|>Vf9_tPA8ihabKlmDWJ8{3)kOSJgBiC84;KAXGe{jW znfwon8bgL6-s*01dP)3Z&IFgyTTD{GTy>4$riPtKqf=?=ndl_#t>ll z0|hEFfq*H%;Xs}TcnrMB2e*pjAMpAl%pspXa5d(88s9oH>DN9bG*lXuoo(65=Q#}B zNvMZD3Tu4(p?{4`Y1;7PiErS0Nm!ns_lu?sXpij!n?}<<7Pemd@2#|76};Fi3=WAY zQ5>(mZCZ*(TdPu*WDbR)2yZ za;~f?E6RKl2L0yE%Fk2frWklddYZ7V>OP6{f=5+Cl5O{Xe$b^ic^Wgsa~j zU6WJQ$lr0c#CczqAN@xCi)>2NLH=J4l{9_LfE@z8Xer5>y%d}iLc!j+Z^;av7kdS) zo)t>{B{^vY5uVPQ@D$oBa=^3aS;ao%sxZ=R-&1uplOT}TEQLLZ5Q6UVMWh6^fKNp3 z_f%`*AO7dW>d=Z#gv%KAy_Eh0&mpC3 zrd~Oy&zYNoL~-DI!(eYP=jP%}t4PKR_CKGp6oM|G<~jJb@_td%wuYf?O^n{3PiBJr5K|-H5)S3v^JD*!hEIO}S)$U;CB0Tzpr0#bO&0t?kMrMVj&< zW7n$Go}RIPw7QPTO^ZsaLNt6ZsL$A9lUyepigPI1t`(p5l`2j_8cN-THomTR@0aIi zl0YkOLFir|59%Vblhk&69n~;x_r@({&XRKOa2Hau=Fm^;)H7#FnU-fVvrNZrzw3Qg z8H^$%e9xg(Zn!xNg>&V?eu37wU!5y-sQ{POS%5vzxj==&8s&1lUmiZsB#E@n5*YMf>=tv` z{xeTbwD6)qNV=&LQ?E7>RH}09wkRiZm4~!JfVLx2naLvDB9?QvdkbKQ(7}Atn(dyz zLG#`ce*2EudbM=HEY_HK8k(t@h6v|0^t;W3%~qS_K3C?{VqBZ;oqz~31X!-@fL+(3 zyz{xY(2@q$26Y=Q(VIcLAMp;|x0TuZ z1bqz?NW3P4rKmy=+^dfkJihHcmbOwfr8ye~6a|)Mug|T;2;VU- zlvHthm3D3A-g?J?SHhoPCmu-+GgD{hVEr&NWwM^`&j|3A~X9+flh?bX`kA0}A zdIvj$Tf8%74cr~6<0vvmMkW_N$__7e49F2q3f;PH%!!@Pe6yOa7iG+xA$HS;D!B5z z?*xv4z|BP>PY4!3r)(cIfAzCOF=Znm#)9OW6}{!j*TyEa%)HsoEF@d|p!`c4G}j_x z6{Y|rYFeRbz0WYp|3lwfOD&$7Y}fu_&DUBdX$E-RirQO7Rcyhu|=G(<)jwj`b;TG&M3! z@4g85f~N#vqPZ5Qx#!5rj0RF`rlgNkx{%${146N;VtR8HM zhC(*iA)hdz!T#H|q5Cgi9N+wY4b6H6k9t6aoNBiKs)&cH7fEQN+ zzIB1n`m!&3fk5ctJhP;TO2o<@+0Mv98!*RY!xxHv<9O*6`@QwW*&-p)#SMJw8=5cD z(jezbI?YpY)j&K2PQ?1U^P-HaMXEOKS4Zhazk?xN zQ0St9gb;d^h-Es;2h~PbUFt??xFMjGp$1KA{kPbPI48`4-jjvN8Ylhm_Uf?_mEa1p zr9$j~($jcRB1I2{A1a88yQN7lB8%p}Yal^=8d&dHu!N!#Y1JnM!)iza?Luz<{~Jl5yRUGWNy;eLjXkkr54{SQi zC*Y~@zT`i*l9`;+PzIZ0&AT@b6GCP|@c+?!I3tiUhDa5J_b}6wMRMhqYMuN88eRf9 zf^olg+jRHIZtS}NFvnk6t8InEz|96cCK9wiypMdU673D^`L-HXxwl(b{rOi$4Bbzc z_xy!S_k5#3^O$+4mu|X~{li|%};)h3! zC@$E`t~b4S1zip@6pcumIBo z@M@(L0871$^M^3F&T5rTt`<$YNN|+3F-tD>r$WV2-YLBrr50_GZ);9nw>IPIZ0IQ>Ey54r zH{i3~7Z{20`CNU~f9HK%-e#Num*9+vXJ^6bdQTRft-qV>Arsmv|1rp%nmc#^p6>+x zLxbmE1F;AjO0L2k_wYM4JPCKE*=KO>xXnx$m0~HblKq?PGi9#VKj8JbT3DOR&C@K~ zr^uQNw~pu0pj_khY;?}nUV0TX&-dtQ6MMm_4dzsyF5&lrI(3pfv|KH?c$oejQW`z6 zlG(YW80dkD?$?qJNBMpvtroKxiCBhAdq24;BezJ-X9@(@Fb@Pp)Xaj2qqlkL9`(`z z=8z`7^z0X=sd+~>ToD77CtAIE$e-$FzD*l$BGOT7ELe*RjWK(%Sg9clUAlL!o1=B>$wr#Lw}z9hZQiDwTr*KdaCtF&C+(~g0jR;V;@o{&mtZ- zdJ_z(Nm<^};E4oE`iTxsHq#kt6>o)-b53=0$OQh3F+TQwoP7JV8iYqadr?Epz`OfC z#J=oPRCwX_K9#H+cpx?JdqO#0Bu=WYpya7B|F-EiiP~5-^z`{7>??wb+&wQ<+>(p$ zYH_B>9d5l7ClA@QjI8NL9pTC<4hdUD<%vn!i8QuG0b|7Z>e-Iq_=EmB0!Q~3+cEg; z#3T4_XmGGNOFU~itu4OZLYcQTw_(_6x8P$BQ;Tx(fk42$keaPy`_%PqA{AgDR$}C_ zb^V6x@U^`Z4vJJ_-)v!%!R{&y+^UkrAbC>KQcT@F3Q=WVjSL#U765p4AOp5I@ zWTHJIO{1kDK4IR*HT@|mh#*hs(ZqZ4nV zzseW0@k5L5+sVE+#O;$>M5hwg+_P31^>d-ju%9!9a1Y0i0~e)!(Plme&=3bTI$81e zWX~h{RM4`iAt45l`a)eave60e-)UoG%h+*J z+c9D*@;?@=+0|B`aR0@aOLD^zAm>&${)NlolzGWu$Q_opbT^oIqWQ10wY2iL`i`f6 zhUyNK>M*_Q*)SM5MB()>ILcF?qqd%XX<9in&hOUi_*Y*^4TTx^W+ti#v?t?)n2b5F zjwNKut;u%OeDmi>m-rxB(XguO9b%8$UihJkicxug+JwhLf0>3+wNJxHYEl}WrAFx0 z_xm#m1NJ4n0!yZqu(DO@kY|Z@tg`9kM&%tgD~JO-Q)++;rHVjp>CkKdjTQX|tfL6x zUjD-(+(occ>7Q>^iK5Opq0Wi+We^e?o#vXY7#nwJhqlBAs$J%0g=64NPdo)z{l%PO z9+6Ct9RZ|E5H-PC-hGJlpM$Ik44960s#{t_%h9wb@l}|>@#P&0us6h5p$ha+Ic?v9 zly!qJN;UnBW15%Kb}$dkr0+7EpRog+P^RDUn| zU!K`+*X@m+6Q$%u_RHx=Qr&u@fN)w^JajWATnH@@QLkC~23E%e8+Chzig$MoH@b54 z!j2pr9i0*z1^f$n9=)OugXG~OfCw_(zZskz7r(#kowbp6y;ljLn6P|a z)_z;0b*GDl{Mu}QAgbUBD;kT&?%p7bJ34^1asEYidGgUTO%aYyKZPQBoTP$K4d8-` z>?|HAMsq9N6#SWBtZ(^uDtZ?1aIQ<;I1WO_s#vD3YK2sdcrUyEieTz<{KLHL z2h;w!KURTu{sq-WJyJej-8AcO7!076yL$YmNSWLUk|c}k4lj;g1QvvDiy!JfOto7B zC5Pr89UgPtvP4zl&SkGBLf(CZWl4E2!?#_aeKhVJg-#QDn+7x&+bc{E>yGnIVo8M_&+SbSt52{`tWQC?Klj43=zt!1{CiJYmFaV zY0aQqAehI(!N!Ev^2!7m;-9GF->^@F;BitHZWL z3gUvc+W>h;=)Ke3KrOhjvSOVfOY$-KX=XjW6e#~{LU~ZyJL*JM4|Oz5S66(MlfBKa z>4Z-HrcBtH?i9tI*60@_icHdJ6|WYtK=Wx*G341dL@>EOE(nyS>@Z}vLx4TlGq zDmxXZ(BA5({g}&5oTK5`oW63)gGDE5PW$}lDG8wV^?Vb8O9d7%F-i@?b)Vi#3$~dF zCl5MTN2SF+aBIS3$e1+CTwe9HW-4aAYD$BVv_oGhQC+2Bo+)?3%i)0bv0;z3C3=qt$Vr%L-gcy95P$Z@M6ulpdIoG@dKL)8h}m&^V`= zvG@*x#X&y1?0aGB(?l@^CvRL+z(Sh-K#iu>`nucJhP3*+B3t;>%Vf%Xw3n}Gi^1oC zO0$21Dr@_6P~kCHtYAo>HsY5=$*QYs%@`B*%jxw&V@`SH$h1lnE9IW>e#heOVN=<*6FhUL-Ah#@UwN zsop-GDQXvvR#KrVw0?QPSWh8`-Ho+6xB1D9TF9q!lF>HS!2-;U9%641< znd_E365sYqEt)HVLu8#J8HQehluy;$GcuPzEv#$W$3E&E64`5VHzjx+CzB^KkE~U_ znm6i3h`8D`f?kl$NKIgI+t)^xFz@U0rMBO56N;xS$;aiNHo!{|8HyA*ntqFvPN%I@XITuvjb^xZyX86U9LKlpLjV<#ylK%d{C(tAk>yMTQO)QDi>W zm>Rs*To?O#b&#>YlBn*;@6F0M&0sR~CR;FYKastsJ&i_+(6^*MAZsneAZa(dA1!Z# z6$HJy1gx3nL2cV@AN{qczy@24G8)K+MgF4MJ7d_&-rvvR7cV!3JVpopnfZqNh06e0 zp}VVw`@9FbhylP&vg|h;{&dd6>%*!5L(r;Kqf%u7{o@hpl+WM_-RHqKty_-yci%(D z*h%budB2~071e$>GaMG+V9T&oWXRM4S>Rk!uPv4dDua_D1^|D=vb^w5v<)(5#O9ei zjhi*)Og%=(v9sXYfx+s^Fae7Pf_U%s_BKPd?Rv0;!d6Znu{>)K`+&R@8b|~Fq7DIl zlIOv^33Jz-K5RHmCY+h6a?TA#WObnBVx1|GWb_ke+b^;qV&w0m&LU=EYd?X_pll5N38@@r0$2Y z`A+m^2~d##Yc6MRf#B6bSmVCyza(ST@nWV3JDr41u0es1`HRP+w;y^X(Khf;{)k)? z6&eUf=Oc%H8_XB&W4K&-{5?FTn!-SF7wsD5D z1jWs7^>nYL_dcm#ATBMg7Ub~<|3c^UV!nB1YV{+9-7MGA>sMG`3^(n|`5Eqf&UFR> z6ZepvckE|#PlnEq66+HtrBRr}*Xg?);@B8JZFU90(Ng|hyA~54#`qAQwxZ{JFT9iA zE3za8IE0j{F-T8qWMnhPvKXX5<$|legQgf|rVA{dye4*$f;W`auve;TgmN=@!Vk1o zi1zVr_mW!sRd8t;y!{F`}& zmLIAl1Rq4Fp|*dX@4`4@iu$>9hl{-dAM8+{E$3swcx1C<3TDiJA?tue|tp0`Ae$8CUHuRZF7_lJ_+ikTEhs!B$U(TRi*8;j36^`Q3i!mmB53AD+&B*cbzOxx5X;=va3sMA)=+WH0 zVV@n0z{aQ4vy9t*L#Ydz>uWeCK%3R5n34oVYs)t%KTsiFiU0velKq8l%`4h;Ab`_3 z?|@s`QFw2chT&U{bo!ir-?cbx7qJctzKlp1BY{xb$q0=+HlOu&Qm0!_r9ukJ+!;wx zuo@Nj;Z*%hNcs}c3qtu=5jfS%$&K+=MNOD=*s||ewbw4$ABV#G5Z_PHeBD-IMj)luBkQY$jGo(&~(TL^LIn8D{(BEB@l6 zeyPdnL{LLIghS-!Wd>F8saP9o0a zp}oON2cm1lWbZqel;$1m-exL`u}q2@n0a|rtzca4ANdiX-ttKDdudO0t=*FJK1U)q zU)aCA8TFRBFsus5^~K{s29>MvQY!x_IL%x&HB0}Y$H2X`*eL{d@pCZaT_h0+qEyYD zL$T7<)P)9H_{Z;<36I9}Q;h~|m<&qfU5^kX82(UY-F9iF3DaA$=pBXywZ z943k9nssfDw!{u^89nkg#61oz;BQMP?(hH66s1zNe(yFH?&cfUBMfGTs$d1TYRSzQ z{~3jrddT$iKRqByiB3dp^DP%*oY2ODYmi=yAIhTIUno^@-IJ44_Q~mixRE21;q})r zyXAR6ZIGOnzg4nX)D(fhW`5du=jxt~RI|rn3!+3Oxo;^egG@OffZ9+SycLGk~AB6m1nbj2#v90qu4Ab!jeURF? z{I}$?iMM5=LRqDn7g@h3?LzvC&Ftmm1~*s%S*2t8nvV)*E&+<>ib}i|b00@47#XXc1Z9<^lU?@-J}|)3dMrc01FGCi zEAE<%vu|GI`l=e7iO(p6PKGiRXB1rBDjEI`3N6^tL7gRk%5a`apP3N!keRfqxZ6?i zy|h9xXAGc{)ZJ!wQCHO^>>!ZjgEq#qZ1&lq?Jr%nGC$Ki;*FUNmO_5PI8Cbyl;QuI zCnz0;bdWX3r+L`?%4kaqydQC;lVVWU2l47}^EdL2*rcU9ljQr=o^qhu_6c|@Y&ebu zL`*5TUh4jC;UfKwz_S$FS@*4M)_>8DEQ;;zO8bN?U%g{Amrtj&_4kYe;3v3tW*cI) z(Nlvx_q*F#ygq!8&^*mae}J2+e`mmPz}xb1qXp;)kDxs{3WUw}&?I8y62!~ra#a`( z*hPQ#PkCNQKt;B??_<3>z=LP3Bd7m4;k;9lX_!bgECcwJH8XQ6p&+b%crfNxXGeMa@U~b5z!HoD%z3R6W_V2fawLjfS<&)kG zn^);kfHH%r9k3YFBu#Ekx-Wk9o}A-zi>C`S4Fe^q(^eUM3dNNW_+aiTHQ;O;^@3t6Ghh6-;NyTkfXob-&|kC$V8 z{r5|cIoa!>(A$D*9=I{8H50uu5Sp7+m)4W{%BxmisO`i3a~*c_ebTU(t=L{v!OXUi zQE#FqPn4D9Iui1F-pMl2McIAnUZ!I>XV&ai??9*82eB2t7#L$qp~ zxyXCsj-yG1ok=gk76%Grc!fGT=DKuHUj|iU=h16#14D}+|1sHEcO9`pFV@mLhRI`p zqTqctw7m&HH`s6VQKf~KW{J~!+hWrw*0^z*8+Sdf#iK3{?e z_Nl(abN0leF8^!nX?Z_-_be`h`T0C7Mmh+=7LMy2GCdeNAX z#tq01`E!pHKI9WE&^lBzZ{Z_`D$Zl5V(2%X6xJhjCMk6*C z5XH8TLOpEtw~eE0@1WK;jFQ8f=UlIHM)r7~1?YM};``xr!DmjUjF9rJ&^QGRJ@@$s za|#l~>)UTxw5T}ND3!VCOC3uvnaW*k}PBF>_5 z((VNQX?2-Os9hAT&X7$+_kO|~r1wv*cDHRsnud?E5dOUAMV7UdB?$a!rrS)HSgG~X zk~D>ad~VYz^8J_O1#nfXH{wj}_frj9C_TSOG`-1*341j9L-SMK%eY(>Q$W22;ys1Y z_pDYR7ba-=pU|ba11;3>WD&P>iiP=M7KR`+MdM0)ahN?7IRZclb(eQ6gvb zZ+Chb+`}H-ekKTGVYIgj7Vn@Mvz$C|o+u>Wg`FF@J# zJ_naO)Y0WYT=^u~^d8;-8~+XKt2^H%gH!h+2+p{9qxzL=iG=-WkLu`Aw zwk(vb>M!H5<5o*|^8TpaG2HUyhRud;#MNEkJ#gYkteZumuws@hwvj8g-9Jfk7x(pY zB1~Lnb#074nOe@Tirl7>tZnP$>;)v;1Px0LtYp{=YFBVC*o?(3B`KPyrOb;Kp9;oe zV|S*IO@qi}Od(*!bNbza3r>+MKh7?Y`opRxO6{8@`J&n-e7l356q3E;(}LPQutJv) z&~oXcgINE3u_xnWl*eR07u~y4=;}-ZIrO*fD69Mifg5}K4#OVKG}|gK24#nh6!SDY z!n*~C<~u)B+l!9ke4j16c~#H)?bI{fVMGEpsrPu@UroJ{F4T>CI{ccNHv)As+S{u6 zeMdqL=()j3VC4qadA|oJNSGU=FV|LA%4T%Lh1FHoni&i>3m_Hh#z4M%Mc-fsv4=vO zW^C2E{-J$ad&)*&6_VC}>)a;hfBDsYsqj-Jx4%P18Mdk4W-}f~vh5b9pdw=YyT25h zvaqjj=O!Tdu~p{GCheTtz&2w-E!&G*BvsuKjU3#X<18PPitKMp@GsdoY3=*V1ACjfTMM3MI)@0ij*SzuhkE}q5Nn_0c}Zyg>OIO&wg zh;~SexVQ3i@7_39j8(;UdY*W_m28s4KzQ5Z)$jTw|m7KgZOu&$i zUOfet$#3aoZ=xoVL%Ig235FSsb%AskdN(}@1SZyLls_drlxi`3EOEDMf=~jQHd08s>!IC)H|>4GK$5}gDvpzRP>Ex)Fpc}Bsl**X8*{U?#Yh>$7g$fR6yXYu5NU& zw?(3GpjOIs9wrJ=yd2FcbmuJlCZAw#)V;nt^pbqCOrh`Sk{cz~Y@~1SXSh4=QK3u0 z>$R3Y<(gM{89D}AFm`M>rlhex5tQ;jB37tWwS$iO*WS1*a~gBK!M|_m$3)+1wwRit zq|S1L%WGFX3!4{XzdX%(WlwF=f^syJNSsVL{loh2!((1K0!pKLmcKdh4@JO`f5E&b zt)&0|h{ORh(7tnrEw|KAFA-smAb@5qtj3evR$U=Ttp?oyka&wJaetM>udUPzozAGQ zb!4l~Nq=h*caA_j%~E9ROHfa2e-Wg7NN&~O23A`Nu@1sYd@N(MVF}=)y$m@?4Lc2* zkaYVlYbQCDrlg|ozuvUf(urm*-?P!}gs$6BfZiVvXLx_Bd&?G0;3X_!!tgio02(iT zz@{!=qPoV$A$c>d4f1-Gsy%ZxooI~zKxK#hz;p;qO#mJL-_Zr8BFv?`hbZ|V%8x#5 z8A#sEp}O|GKt3p5>sxsp+fNi{ z-p|t7As=(`;tly-jr&f{YXrPwBTTbbpYnrg$xF|E??Z zQ~yR5RzOiHgaSSW&-}=3N;SkO)i};0mm0VWAVtq4^%1XZSZ|n9Cm#=uljK;qbnG`?D8;h}NF3^4Mog%xr zX>5Q6(U~9*%qCQ4@67;CE{T;pV;B|)=xa|)J`>i~B^x1VjD(a)Bwd(jUpw#oO>`;N zeLS3ag2P3&!clog;UB;|yQ|dL(E!}C-$QL0dpj;^dzz>oFX0;gQM`nUUdv{OogNHT zo2wfWcis6wU>-{(R&oqnWDy}I@*`L0o)7S&RKMYSa+CE|mRHLO_9|&7?fHKUv|owC zBSi1=KYSjRVoz@j%I5kw)a+WIKYLLgQUIr9C)HwRdVAkAfy7yf4-+5=lhgE6PCX4mnBUTu6a6hy*UtMMu$G2}2X&G>t=_ZgA-N3y$QCeNRmi%|qJB276iUat7BFwmHs8=Lxk z9gz(%*iCFGUupa`BkD$S_0=-NHg(qVU#_9yxF8Ag1NOxuKwh9~7Fda+pTjUW3I*z! z`{;%HTeLo$Z2XU*aWttQ(6lFni@AqVXg)_m5qj9(hd*v;3;$B%{Ox*zHfN$h5a&BY z7QdB4t{oV1=cWd<1y*02?rpuU2UBCiHV^!I+Mm<=rhx`}0uY6x@UGtM-7tvh=Kl%g zACCaD@H@@I+nBzLzT8zeZ1CqPQ>)&a`E&9A5y{na?2x300byOo5!x+2#>ZhI1rQKPoYG>DG6uyrfTsPVg@$4|Gib`Y4PC|W_;34)g8EqtX%mvE{t#6IkE`+ zzJ~vW+wXY#Wl#700h1wse~mkg_anlCM!s-m{xc%TtKD@vnQD+P7%C64n1eP$Uzw1b zv{(7qy`wyvsq@rEPF%br@}baDcqSS<$Uy%jYGG)h$jUJZH)BsYAp(IHvRIJiJ_h%z z?LimP(aL|nk}+uEmN6QY>AjgGjN>D!H?s+$;6Q^k4!6WrB{D7BRjTAA&IOBa$N}Zf z*zu{rr)`G3sIhKxb_FLbKdTV-lbH`Mon*;9OYRxcb#>Tl+KQFC4k;~MLc3d^)X;Bg zAr^RSZxzpig)`ttZ~o71b%%5Ny~5YK^u>0v_r6Pfctlb}o_Onw+H3v;(HR&ylqw9~ z(onSk$hSUSx&QD7hQeW=mChSG`u{|yXgqpsbqFiIvG@TAIppi`qnAhwD&Wh^79(N3 z9<@Phd{cY#3nZSj340Mqrs)f|L4v8)L1upl>gL%4tAA>2E17Rg{`)l`pci=3S9UhV ze_eZl)f4hF((c2^gJQ=)qKu=0ERr?PtPTo+HSN`~9Bei(WWPfh`bbcCyVrR2nL3XE z*J{;4#e|eMLzBveK;6B)r+F@SURcGwVLtY^FM)C7E-x2p8UEe1u*BcD6bo1x8J0BQ z%_%t^+1ZmObyg7+)bKZ9ba4zl_9_EYZ4=j2y&WUYXNx)b1Uu3a`7VFktgYLWE92sr zRH$RtPNgR^J|b4%ihUa_Ut|-EjHs;LREm3^{^V=XrsMBwEfwp7b;wA~6F-X^fjH9x#&L(xtqGbe=8{On^R zp-X=>wM`o#5P>308nansKNHwel{ie3`5V8y%Z^~ZE$HEbEcWxEj6rd{8mPhqafyp& zQ2YcOpbxBY2z11{<;T4G6cz6y{kG`khb1O&_ORYmm>^JIU)yn;2IZ+Kq+rJS^$2DR zo7t8QPhXn!WuY`#a^>=uCjvL9gW$-(ksfAjG)^39f&E+O?x2KHN;us|{T=QW(T=sX znQ{AS86yEE31pCq3j7YlbW%UA6PiFX+LS}xNur?ltSiXNE zG350p;RV8Mp*ir05~rnp3T%=C9GVYD#&@*5OqvRRqE%vP81y0Dn zu#4649}=Rx-Sh1>r%U{ju&~h*^i$x^H}qnBF_oH{NB=nH2rLXdUK{~{1W8|Si&C^Y z9F?Kl#w&#muAEo*FYBFn^CM%Jwq{mOnfFa8^OXpT`k{y2O(Dh`H9F)?jtM}i!&wzN z*Gz{N!|rc*St|YU$$Xuc^VphjEOwWqo&`y*I6{RfBmCN62}U48F=4*}fB4jZ1(LN} z0#j5B$$rwvcZdR8neH0g-^J(Az!X+esgRNH?(n1E0jZHJ<9k2O{iB8xNIdxBg+F;T z^8N?Eu5JR;e>MB4HT7S&2cK3F4erwXENTo5M}}IX5s{$RrU`sZT_muq+{HNcM~dZT zc3s*7<6K31rlgJqVxudVuXCEBgkQbHy&kDV*jjHe7+Y8h3EjS%nHy_R_u#*bm9g^k zYQ;q`g5^r;th6NJ3x)x^A@0fMHb`q0Ir^U<`Hkf;9~=H&4Q!&9vxgD(Ggmkfv_*&g z7lB$F&3)X!xQ9H(3{W64$&1BkQY|LDO=Jp+4Fykd8;NjgaZN=?)275lq76MGR_Sqs zvqNk84|A55kK-HrL@(;i(!0IJ{zv4!k$M9kFkLFQ#}04$GHtM->2g~Mv}nHgV%ZwG zA1`teQ0IqEee6WZ1J1S;fxX@@ufi_?{7Cpy0dds;n zVRmWrYVz89NyyQX1$D70HIy0$xHt3)IiXzGzr`!Vh@%1uwEUFU_XjEK51b$Lei{^O zW*9a&q~PWh&vu?4Za=*2z=W&)@AO6!;znUg=A&fxcB|F5>H_@JK7(% z;_NQc>YV3-QAU29BL*Xe)gWTL91G%f9@V%O`ATHao6}7s$_QEPLI|E0i5UYcoqm7z zef$mj8B*IVa6fg$Z3ND#F0e_#6{qTbPiQ|Z@IW>H9ctd4h)Eg^T``nqU$cV zHj=Iilfbl43AEdQg%Y0=+9Hj1NO1&IAQt^8i#x2Q>Lj)F?dVw%P8%zTaRM$xQFvlB zVi-<}EgmXh{SD=*J#Iy^MV}5W8dn+cut z<`I)q4L3!uUOF7@NU&M~HOy7Lw31F#cd&9h&#MFp4|2FJ58X2?+HLDkyfyb9w|!4s zca#(`d?_4LP1MK04yUI1L$71GJ>5bcOw1JFSDturb`|M=DVsj@$KY6WG_mFkDu+6G z)}5gByEbA-Dt^UTp}j7M$0ukOk;oKy>U{@yRQ2eB;9xB(9>S7Uo`UJ zdqWyBqi!%uB#tn9V1o_EJ%XA8eYszXy(`>%h~^|~&h^R2Dck%s+nuAA7O7E-1j=5N zPX7imjMtMuit1V@ko#TpM%~PL2eJomT?N~%2xUANyxjlqc$fKTz54@ZZ?@QUiDc)e zKD!8kMXz#kS$hm$4IspPPP%QGb%BhXpTQ$n)nQc~R(swcau$yi90b{+J) zRBGOAoJx(x(g<*dxZEZKaect0K{5)WBuGOA{;vsj(4bjE$p+2)C0ru;kL_BYq{NW< zK>vHY0(w40Q9i85#OV9YR%|SQ^K($1gPPL!UnCz45V#kEE1@e#S7q-^na#%+^xj8B zrV0fY7kfR`3AZ)fm)+lU0k4e}cJmM{u2f?)8l;q4OsWbEPLD?GNzWheH~ z{2e>DC7_ZU@7~|mS5f~+`}QyXUf~m*QonCvZ3-Kv0G(vU1p6c2kRG^^#c~dRZNW~$ zQBke*iwT=zv$)m0(3~3bw}_Zdwq4@P%nD*@l19?t+AoOMvL_^X_Bafac%16K=8=?8 zJ@;bI7I^La@6uYOm38Jo3Gjnuc1AHt`>)Q8Z^_Gu{?`fb@?Ypsh!dJ5=V>p>-n#=~ z{+>USpJXw8#X~=6vzUN4nf+M~aMk}JyN4KhsHq{G+T$0Q#63@eQ$Z6VG7`o%WDnnx z8p2%$f=1}605AdzUPWx@S8L+AYm~d4bJokH0A++5@aiba2n!<$X|`gBpr2ZeyqwINGku?cMZ~DYLdIx#E$7m)kEepqt=prpdLTM8+6> zf!11{TlZ_c^y1YA1Xge)7R{%>;9dKM4-=6YbgOq9%s3iM+cY#S=N8XYWZ(Ukw`KC6 zblUiV7v(lM^(G05(`L66fMd0LolXAANg9H{sJM$!ZFViiiO7sNZA+hKQ^Ps_!qfw z8n%OhI4gIXamnm3#HsFPcq#N!fK5D|`(_v54@ZS4vBF#Q0jpF$ z^cQ)h*i_x4KyOCnp?^vQN+~oKrVfpWhemvZ3}RllHlyq}h9_MA;D#BG|5ItsH{MWU zHNM9^#?lZa4Qs*Jh0#A?cmB>tg&9|q1Z+>SY3XF05p;JpJd@}+yO8?h-F)| zmW*9!MaAt2YyPkHAgli}qD+hA_h%U6h?7ECD(d&Nj)tYuDMliaUx&y^@^JjO;|PAu z!au|RoQuW~e`4rJLYjn53{AHROnG!EV>MxXpJ#c=6roZbaudYxae@=jOV#INll67+ zcbV!1mGQJ}&T`iVF%8Xc$_t1vRhSOw!@ke)#C08U|JUOo?yfymR2pK`eX7r!Pg3s| z6MSN!IYFU96z;$9iDb$7cQnvIA*q^3rpnAUHzNHq(vDZ`Pc?z+uTvHX^}P3e5*uYi z`H^MJ)lZFj(*J`#-PGRW+kO+FX1Na#Jffy0fG6kI*-1hEV~0C4D%ASL=_*P7jEr>{6PSK57ia3Ekbj6}2jn9g_}FbFCDk z)~$4!ch+5PC&kTKn)!5Emmsa^3R*<9#@hsiu1c+}%h$Dz`i5nFBPjNt8@$$uTw%$s z`ynVjQt=`SNI@^ZpxcdCCMR`jydI;SxOw0AkBB_w>bG*)2aD+<5^hnTo7`J4>CsV3 z?90Zu$H~?Mc_{{mo_jsRB)?PgA5prCzh^#m_6TRt8 z=WA@E{SR;vFCcQojxJWB7;0?Y}wm5t(iMX1p_5#vN5l%CNm z@C+SSy=c??d}Lny>S;dZsb}q}Yz^4mw|#fhXH>pmpd~ZS<}fEdd(JKIsw^(KWSL(; z-_OI3?!a|Kl{4=C#va7|a@0)23Y>oPvM`WM1@Bez!Md}tPW zSZAGN+t#-iWcBxrVG35rmR;aWXLjNBVafS1$;G|@3GA9 zyISqHVtbd%3J;4H`%1@FWY|n~$C0Li6M7g5I~E7E`K9?MJL6zCctKvEJPhVMW>Wj43_JvDE|EulQLKHjyd?bZ78V|VtMUebDPHTkDpV| zpU4oo`H13G1Bo~UM|p-Jh|}vj$n5Z6Am;{hg>ykx%nHp>WG%05^LLaDY(ISQTB4b* zPV$!GNG%xB(8Z}jOt(W5dIX`*QSc!?B%dvs94`Qq|Lxea;lb0<_f!Hh{m`W9@Gv!F zj3uK(Ql{Y|gwuXvC{m)cK z4UQ`6nng6{KWa84^(yxa3H5FLmltfl7b^-8H5NIgM7sgsOg_-7)VM+xn7}GmGVH*p zqkjH^0|?r+hGy7&h!d$cXdn~$u0U-+aGR=|VbYpQStD62K|4fG^Ygf_)C%@^G0leX z3(Vq#(o02Y;UjQL@6?sbsWrrm8)opGE1hsjZA^HM?8|xUxu~x$k`xyXa{^;y$Eq&o zF+wylQ|d%vvo^sG{{dB$r&Bm>9o&EJh0VREmFU0$7(JRSABBXILn5Kp-+sRJtwPo< zw0fsqxv#R4uLkeFC~WZU+em>^rVdFZ6t;+@1|jZ2wXq4?VumfrmXBjby)bIZzd{ZZ zwN~ie*qncpawY@ZnOAf zi+_quC_mv;?HArRvJDrYq&*D1Hwu7yfd3d)7@#+OAl$qCs_wQ8=X75HGicuT>Ua(? zvBJ$Ib`di-C7ho+1DrkkF8{X) zc{r5V``c845O#Ja`_vir1z_L|y%cd+=;ENqbNkU0pzSD+7zgyJulw!GU5onh=NI@c5~;Nly1J=|GqHpmA=*0RG=9QbyAt|;B1QfRe~$f-=>d(bIXH}z>~gw7~qF8 z27Q5({+6KPxnUZou3h4Dhi^fo9~;($fvZ16UZ zc^_w1M({SJlXq%(P>PKc-IGxuW!;Ci!(`xO_};@gG&S+Az;o%nf`;tnu&O$N8P&Zx zSD-9^%OXEkI7LDCTlaV9x}F|=B|<11%mrHob}ptH%W_*}wL6`+Oc&emgx&_IZN*Wj zcoW#hgSG9~QsB6U#fABO1S=tJ{koqGc*?O44qZBbJE;t#izkseAtAhIZbWuSaY~(~;>7VU(U}0)U&o{cotYXhiMJNYb*<*P zti3#nL+UiZ0|j*Zh)zj36cl}*wU&1P;}YHZ=55{m+iyN)tp#GBxcI@`*Ui&WnAw)o zz;WrZ5l#hBIMwAuIK~hg83@3g*>XYegIs3thQKaIZWKwIF}7sHqbe=+%29JSb|7c~G-^w=`YW@*abm%B}oYAd$6CeeX^4bvc2=-Kkxjb(0}IfW=drXc#nx^hCM z)|3>0*iEB#%+Ms*L?mWlQ|TD~}0SKl5 z>x|yO$Y4{yVtq~qJ2YELASr(xnynX4+nx*6L@6c@Ca`cQ_mdX^vw9=>kZEd<0(KgG zvV}Oi>s*3d$5J~?Z_k`v#k`pX8T*O?dU z#y6SMA%9z0Tm(|~MCN$w>#C%^CeF^ZYwx}MW@J)!$Zv78hP7r% z;;SS(ASgZ4;OoDO`~rY zm}q8i^`+;1VgJpGGNq6t(aL}Nf%ZbI#%&+Txf6{=M^E27nN4dQ*_a{OM(Ffpauc3x zWO4|MOEP}q)&dWo|dxXBl?)MvsbA@n-Z+k)+)v~%{;oh^_xner% zHBN8)E;%4K&YE*ukw$6zg!2>F^M!gfd#EoFMtMKk#9KsrR%B7#xYp_@OL6D#-;R8w zHZ#;MJAi1@auR$8=<;-abc>x%-G z>d)svo1Z$|l6qZw>0lEZKnGhdGo%&Q_t!EMMl-h$7F`GU)LCeCmnLQj11LGy zYsbCWALl-jWWWETs^Y~pnRIwAa2a+je8&W3fY;ygWwIZ5 zFjxMOcv0@@=l`lcyjvrhcq`P~lsa&+5=x-rLsxH3orS(8tKFK4%jJui8uXczIpVGHZf_2-GAm2iano>ixo91C+UIh|a zmjgE~-M?px=WJLFgc-*6te;4)`q8tj8&c8{|2-fIMii=h;iU1hx2Ygc-(~nV`RU5$ zVHbbFLv+OGAe`+eixnqvJ)v?6olg(ugS`lnXx}{`E&OCTJwbViA`1_y*$WyE|7I*; z%@~D%WrAPo;>?u-RFtb19!`%a)lr`0BC15Y-bLmi?1C#z_F95m&-i!#lr`I;=ZOZF60#>oHKPGk9 zfKsJZmjczfLJlby6#>b|s;)?2y#0!osq)&O)09|-+?+f`Ep*;p8{o3$HslhO5v7DO z$)#MU;)1dFw%djfOU0wTw3-y(Isd-rZ>-N`(zVPsNk3NTek{@Fe8+cC`G92y+?;xf z!wJBC_BM;0Uh4z45Xg$o~lZ`mLt`$@A zN)a>4kE=vgZ!Td=}RSjGQAta$| z%74r8mbU=;8`yl&5=JZw?*MchsiaU%M&NCYA&7W7$fFL1!o4;cS0SFq7?k?cbT{%n zxD-MynoeuPG7=74mw#f;fJ4w7V8IdQ9N2kFo!Q>&0^PPF&4RO<3xgL)B(Ui;!qVjTbHl9^CsSEv zM!H_5EVbO}^6A*qDX-fk7%^TJ`*!<+4et-Q087KsXI!r+jw>m8c?{Azv*`L@tiIHG7M*+19d-r*-(WtI%LOW zcg@kJrOTP}Q{73+`E*;a*3^U7nty!z&z%QxU9MXVEuSbxyc(p{r8 zq5gD+7ZKGENtg)XC7$V$@voRdgZpfDRVUh#G{=KIDvlP7{5=Bdi}g~e5u^+Tnmb<+ z{I?|pzQaPkULVtFzNU?#TQ<{qb?R4}$-bkit??B$2~P9pMW2=inX5L$?z66wspiIw z>Nejd<9TpqSl`XO{aFLl=sR(I@MbqfG$w-1C|~uD3V*EjKj%yD$M0w&PD4uXx++zc zqjWZ<2EatH#f^KKl2E4G@`IiZwB3dO4kEVUIP0&p%|@J=#N@E|p*E8I2h}-$1L>W! zJw2mG%0dNqp@qOlW|M8;VhLz5y!FMI5R~OayDj(~q9$5pgR9YS9q6k>yB-`5!-eIp$hyTVS6Gu|M6YttEWbKmkRU6CzLk^H z+7gj}4(TW{^cGMOXNDF*6smoQoQQT)78o7YU9D>eiHx8f5p9<=qc-XXNlXzOxgL~t5hDD z6K!IRo)>AOLMYF|Wn#PLy5&{Krc-VDSmXY;BOEezR%Fdh=}7=Ml!BZo{mMcH0GF*cJ@h) zh~w_FUyCw>zJjluXp=$}B*q{8IlCCE5K6#zwR7k3x`*Gf{NcVdHVb`0F>!@79xHAOi==ogduMz%!yMUpmq!0PkJ#h5;6l>o_AY&#HeMN(KDZC}glf-tuSLoxCqm|P1&dUsHUM_Y8V^Va;S$B z6+aN6YRaQq?U}K1%&0?FNKMu(O2tidy3&7DS3Lx80{BP3++anR1r!~1_ZZ71(+Uhi; zg`)`fam+XNw=sGwId?4Rl41i>VokhkB{!Qjs4?;YOb(IU3Swy9_`pYbHARWV^%bbV zX|V$utd{AsX6{@1QTs}M?9p6)bbiu-(YBx_y5!(8yw&YDqyZb`7LR{~CI^Uh3R||`;30gDNc(qBkq1sL7-TTDj+)$>*%5>S^8T2U|G?eRXXF$`#F2z;a1? zjg>`eeiXOZ=$Bcswkw%b*6((8Wsj2$%tC`xSe!Em^uF#k{1uHDP^rHTJc&;Do{0N7I@# znVL*ul8>kQ3NaY^?8s)mr%3f8MdeN!dGMkBt}ztQOz3dHTbKo*-yHdl6Tx%deupZR z;`!%WB>qz~rt=o+DSuVAzAB$F$h|bS$}>)M?WDGFIt+S6sy`1NXKK+N`a}IPUi&)g`)lu75?@a+jf zv(MV4_QQd$_2U#m^x0JU>?+8>VLMwR9b?G=FcVax8cOkH8{T&t4n+ObLqJ=J>aVKl zS+PC4#+6LSJoNju8|g=~b-;S#h1di^3^wR+?=~C0nPmo>0U&j$J8D2r4>>9|2JgXI zUV+Wp4!7Df^p0ODephdGHfJYIq4tnR8BrXR8VP0~D0l+4@eO1uj%|=|9}VxL-iQx7 zPu(gH|6r_AhY-#@&=U`RS9Y&$-h}q8QVM|&-v0uo0Q>bEHVWqMx`X<&^LP#+Z_M=^ z8@)B*^YGmDckKbKoUtc6;nGFMrB2qA+D15?bXIe0VJ97DW;+*aR=1V7Rs12-B3&VX zywz%sfj{b5?TG5GWhP<68>h7j8|#tfYc%VHbJyesx4ydx*=UyN%@xea2<2FA6R99A zm_N4hJhnj6C9}3_&spj>+dqw)x-p1_u7~6Q)w$LfBlWYO^bv)vqi61&A!EQE7PgwY z$tgp0D_VY5EcB=ZPw_n`yXI7A3_gz?~3?pW`Z$2D7SOc`?V6vLCu(7i^%5$q1ZjnlJn?J8J&kWCY zA^F`tG|G!H7R@UV#vY1w8Zmkx~7&@WFN~GoCxGxh<%jW=Zgdn_cGV^dclc3c{$PW z_nd!+3@fk^K2q1VFZHY(Jvb%MF}zlH?C!J)&UE^&m}AUtOW__<`TB5=D>z- z8}wy9wwb^ns+Q$jU9HcyQbbb7QnUr0r~dtJzsx<`#S&jSqX>E>6$$KmdT8;Kg(IM5 zrT3Bw;6(5}#O#oRa&J#k#J69DOwuyR5sq@xb7Muk-O?>I7fu5wt>c28%*0)A9 zEe^dze_}#`>@}WQ1bkZ$-%@D?=3`4stk>(W`dg)Lm$9eM2DwZI&z)ts8K)-nWxvbz zj9t>gF-lk!oG);Kn`&wTS;w!_7uWe*TP9z&=&7)l#?#j-HTmi|-{!GD>OyS%3Ctw7 z04>M{R3F;k%{Rc;1i*rERPO6i-`ko{mZPR@5Zi&9Utat6%ld(@eSX`E9Y~1EH*)WC zALL%NfR;~o6mxKaCwwi4zNwT2Ug{<^I1BNM%@~=Xjki=6wZaR_y+SR_ZeDrsydj z+29P?t@sqZN)CjA*53%L!*k$3&qNAJj9G;pGst%w1kFicu z0s@wAHRLV|8xFME8q-FEd=J3t5IKy-ZI#0Rh*H3J2{N!HzLtWVoh40%LrXaiVB5d7 z<1F6M6`AIm|2MN;G@jRW-WBI2dc zb_OMa87D_L&c+jGl!GS~La3_@@E2R68K@T+D%cF&0?|^hw-_)U6Z&L^fwFn4_#?<^ zb`?htaTyAiLHlV6 z)WMh#PI&Yr)!v!3O`e)k1@ZT+SAE{XgG_&!N$#*uZ&G-v4@_C{kvCs$14Hf}eUi5m=5YL!9Ctq`A#vbyKuqnoehCh6#deZNvGPINWOJ|2s z*JNV-pIF_(F3PPm0#*USZXnNA+#fh2b3Q*4zpS*>WEZFmzrVR+ev?oeKg?(MYb&n0q46dQ$pM0#Vf8w1Zpc1O>EZaS{TdfBUyB4QwOyM951*B*;;#CY182x#yW1m6zV zYeit1KP@w`|9!$=+tl<6e`&SMv0}IG)rt2qm*duzB_evP7>~}0giSZH z78?z-3FGt0Z&xJ{tGGdXo+xs0g>a16K7z>4>GQ0Por8b6Wvc2efuch)Q9XVo7G_Z(!QVGIprA@CnT80>Vy%fz=#YBg~ zqONPxZRPZYMz!5k(%<{-A6dR!-Ve^2e1H%)gY+WCk{;%5Z7%SGS+?_?-WQ!rrhN_@ zJ~9l|`2JGyC6C;ilJ>3{#82H=Oj`8F-=%@huAQpQ$CeQX+9J*8ws=EKZBWC?Q?=N3 z%j}u)Bk|8Ek?y~f{;Bn>MB_|>+ZX7Yto6o45v}Q#%{{SbKF)?KdDXVpWbv&$<=-b~ z?UQxhU#xLrMMp@!SDC}m4sREwwW}jsuSWkjV$(!*DDbw|LoPX-rC>%+PzCLEZ7^`p zIdx%3yggUluSat_9ke`}|H-OED?}(TK6vmB_1dkF(Is}}BacBc;q)vS)$BG?k>ZAV z19-OHG&b)gc~9_P?q{Z2Vmovy#;Nn27+=;u^MF3q*5Os(d<^c|C0 z>H*Fc0oI@?SR={^ajwccbiT`r zQ$ffUFD7l*qL!GRKJW6N`$Mz@xdk#0DNa0^PeDhdUQ`DLP_f)}If?5dy>)G#2KpVg zT6c^RZua%T$bo&~qHi99F0AgFcA$ZZzV`5cIr03g_~NtV+Ug(7t1^~AZ`SO}e`bXq zJReS9_X;+ARh`qY+cKcUBJnC!eA+bSZFCh5%NLBiC(Ag@1zmCX3OHJ!mq0;tr~PI> zEF+lJTpCKD&<$Fqzfbj;VFFUALTP3gJRJ39ra8=!m?KG*`eXgb{WU8fBn z-6_)t3MM{c#kWMhAcc@4s8{<$ie^aFa0|q2yi{TL3w<^m1+hU^Ay-w{gB(?TuD^_( zO%B-xIj9C@5YUX(rN(w)6``1Wefqq<;D_g-^-N6_7eWQ8riMB>AbFLuBITvTM(GOW z2JK)i(JRK`T54}mvc`H_>z=z6`=y1&!qd|S6GV|~403l{JE@4`{MAhRxzJWoF`S5a z)SIgFce{-Cv9~t0n@Z^B7LyN7aP>)bo2x!po#}H{rtgsvPX!n3l(k4;f#w1fvk8bC zT|hJzY}OS0c6ZWb#m?Y{4{@Vj8(dMhmdbiWmB8g0jY#RIexf~+{7@Vj@!_j0#3+!6 zf8ng{s}hxB0@cL@-EOD9Oswed`kG^Yt=-Oi)meKp(ZbhE#cnr^bPIq0LE}#^k!N-b+`k(bs5?CS!W5qEZJ382c-zQ@{EFy-TMviv?w@NP zXVinChvuPxw8FuaSP{4Q0Z0Vl3r+lk_+Rbks{kB9BHQ`Uo+di(fmT+00VX_57Cth8 zfpn*i%q7a$C#Pm*%&2j9v(P=z9rE!hFIF&uB^aCnv>`7jWpck$nA+K>=>6t^45OkY zpQ(v7A2+zfg1y;!Z>!3^E-!U94|4%AYJrr9c+j*uqPea*!GxV*J1>eZm4kyXhoGM? zCVa8sF@h@m^_}!m`-v)!hSkG;n9Rw$!A9Hz>S7aPfLD7g)I90tZXp!2_FpmjX&<$7 z!O~3`u#Fw{u{p0R=E%Qo;%ailWbS+^^GpZ13OifvACYA|cYZ8-k`0wUZ|HZfJVvYl z%4CthMP>d`xg7&j&Py>znj*IJM`L$TO4)0IAu)iG{7=Z@>{F=7gW51ms1l`$1hg!T z2(zk!3{3XeZ~Ic-Bcop+I$zpT*} zrNu~z;3;WR+8PX>e~@gioFu3G#xGthbL!;R1M+)b!L4z^=V(%Us$DiPkaraaFV_$5 z?VwOgu$|QBjvoo$0Y{mHD+%M)Ti=&Mi61OGhc=0wXo`W6UX8SMXmewnv@&HhiBy=- z-el_#-Q7qdra0uMRONWfX<(5#1I9jPfj(w6iDWtvUE-z8i$UO#)=H=Y+xka9akdt_ zD^0(%j-GgLan_%T&yrOjrvT(=puMRbbF|Kxal7Z*;w>nRRU?IFL7hA;xi$7*R zGC1?3!*{-Be>(P!Mn5v7=plqu@ms}Qm#ch!iIJ+TYXM3{F+SL?qeRa;vd+tH;?D!J zfXTL9sB!(_6RGpD{|AALb`~4~+q+wgYSfG0AkSHR@FB2_q@{xJQ6ILP>(jC+V|%$@ z#qNIJ9MVk~wANXv`w97dm4&9rTNIOQ$Xz4d)jJb>&EECmtJKY?_9w=dSK#nhV0mfN zz*;Gk@ZT?rQ#4_fN+bsoB%*>sF)>F5vTqnyb5@6*&=+KJU4ap2qsS_)UNK4ACb8)x zd1dV`?j^4pr_=eeluy6aUv&dYmDH4 zX;vW#87VuWTNLOEJ23|RiKVbpCZ2r^H(Dbuq!~WXP3ni0dzot@_6W=G#sdkU4$oN|MBfg9jcbw*K zTYl(w*o?I%lvXHHaCPzPLK!zqI{SNb&K9SXG*CthCGT~%0mNUQg2~}5*9hD;bD6jI zk1zy5q2xr)3*4uvQ3q2To<6W4Ys0+$EXntoJ~Cb^Dvrb)aT9#Zm_e4_{edFTW39iX z{?I*WgmDET_s0mXTf|7|LLti{MhsnzpGXby9(3zMt9yTGEFAcmzK07ZSlI2;>6aw-&%~IkTN9!*7W~pyj&4km zuG^k?WpYrZo^4MGneX$Z)*EApDz3pFjB4Z?10f&NKWf&l5qY#*>1HH{i8o3whN?b< z7e{9xzbGRW&TE&psS%+{+RvxI&&qyzj5oj|ZxHCx>|rB@rr}sTt>0RcpJZ)!q?{KF z4?-;1w`I^z%X?`;Rx?Y4A|~Gzqy2{#6m+D?ov;BZk(AJ>s;YG1q>j#p7gf$IG=ojG=j}fe3!?oWfv;?m++E+&N-&gXkA4sU036aVhtL%6;*Hz;*=Dy92+!I*d}-q9(KIoCe6q& zw9)!^RbR5>v18E89s8WJxE`Fm`5dUp%7j8eG%A2{knRWa?}lv+1cu(siy4|8sXc`| zc6?uGY>3bwF7C)pkk&7?4kbeZZ|Q$=!0)Wen8+(ag!>|&&yyc>8k6*)f`g6E~KVJ->4V%Z)_k?n*P7?Xn=bfgiy$eHl_4F{spwCv0?^t{u#ZbfwewrV%`g^HQSn zAD{WN)Xw>?y1cFX%4g6EE=4GVBT?}Qk1YGeY-uAc@(3{9JtB%n*NsO(tq>sVrSTb3 zFNoD$YPcXSYHnLaR)^k92`6wm6IX_iU%=RS%kgB;{hZYF9G|iCO%mUT1WMjDs04}V zC2JmZe_wWc<6|Dn+{utE6s62%DSy%4eS~k}PGZkmQk!|k1Ir|Io$RAVNj(0P=Gd+a zGlmDAOm0QcIZabh-iuU4juw9D^li~iDM@{FnUW=YtUHl@pT#jYOSBI5w{mbb^<#RI zSeTbPHESQK?Zp{H>d`4fFaI#56yDDMdl?=5-WU^+2JFObx5=SGWqij+56aFP=!++B zCJ*Ob!pW2LJNjckV|<~DPOcwkx;oN85^ohl&xZ96o1TC!8y)HuoN7aeAq5l{`r7Y~ zRbsqALv;S23Ez7$j$WzW{x>zfR%ma`S}NxsSv0N`h!yvMEm8sa0R-9*pGmErHo~#H z&leSJAt0Cah--&T3tcA${c^tLj&`$s3cx#lZn5 z*G|lUW74z66zMhk8237|?n`_Ei}eO-jRdy=XA3gih~)B!**2F^0Op5f=Sw3{D$O6C z@?;tB^fDfIyaqnqR=DH%&APw!Eu8PN6Ve#zdHd^dHtOde{uhBv0ae)IGZB)aRONIG z+8lEk35@B(dYL#@&J8L9kgjr!s8o|jPCTX^($uSMLA2+DVa{bZIl^e=`=zLZ#IlU> zt8%QOKtk}rTmGX&R~t4feQNRxF%+dFmG*HgOhz@8pmQWAUf^WU{6Dnox}$RYwf79y z7qcjei&2k$n|iMQ&}d`RM%HEWinlgWOP>6fq60>h7iPZo(t!8@=MOZQaBz(46|FoL z{vVe)mNYS}sMW>11~KA7SfTM+IoOas>1F1+mv~JKoA>G9#I=YieRfBq^g`X96v1$s zcJRMXW6vh-YK4CL(=BLY-3IRD+^O5MdFzb~+8@>k~wK>)`;Ke-X&1L+93wT22tD@jS9?( zTBe1Qcbn^9(2Rscd5<0X+RA=fMF5qI+Kp@9HB~dKjIUC;$@nP0(q)X+{GxKt>zCxX z=Iq}Vex1Lo8$T$}tRrb#E1%3=qwUwYuKAX-gT<8@f^Jy&qYpFil+gDpGp=5@3hoe2a;}r+Dg6Rb#WhIpvF;PMyCVjR)e;~Q< z8RRcP60Gg2cBR+Y*`LYr1{m6!YsV58-^=0FxU&#*eh{DKGwR@Vvq-d3uRay8c=s%j zG`R>|xylSqz{I+YmV91BuG4cN6DuGy+Z+K)DzMi?h;7FuRK#IV4rV~vKKfmHkP)AZ zh7G1$2wshG?+_tb1M=KMDG}DA5&W)6(|p=o%2ZRCS;a`S#)j>PY@gvO*7*XY22{(W zV>XbmUXdb@s{!-5j_<}df@~hzXz+Lii6edAx8Ann>5SK7LDhTdT#suA6*f3G(!ht( zN7AUd$L#YbsUD1Os(S-tLM&d0jC|Gj2z?~oLt?!bd~(=N#`%=?TU*1ZLe|p!Pe<*^ zulv*zScVUA>@Qc9G3y;CnR$FJAIg9+@e8Q zTqJz`s{yu;?^?lR2gk0(<7Rk!y(?9QSgLZRE5{rCckR4GBegl=4A(yFwBXsFhB4GT zgxLiL^s+B33FZ-kWu5KkdW6E-d9noED2jDMTMXydGMa?S;zF)%f+(JZqyZxxgWq1*hnp<#Z~T+$xAV)ijMhsK(KK zg)!S85SA&^Y*yv>La};s;&;mEB}Cq|lZbn#F+9(uS)6ayQ(G-I3#ZshQ^d<-*Q7LK z=eYvig?#-NTJ!Q`^cQgH58&^+t(2|G=zXqXGw9ja0S`;R`Z2?4o>q#c%tw?7?giZ)^k9uOJf!^`BIT4Ajj~VBIX}->g#GRRm)Vsi`PnLipiFVBf`H% z$%Vt(Gz#uVk$T=_F7^XY{>lP!a`cJr=Wmp5Ir5vOl=++p)r}^Wu|UOoax5Vg>*`dG zD%~-g5#$*1pm?y!;13|C@lL-zt(?o$e;q5ST@Fax3mKo_3gK8O13a(ux~A>*`QIGN z*FB(QowDslAwhYz7YcNjqe0d~)+r1u2;&p?tuu7+7lY{Gq1^tKab!%*%|-WGn){m6jUeR7`!?NwBp^gHr4HvQ_lOtNeu~;R?YINyl%$ zCi^jyqeK3_-Wa%%3Eh2JCo4~dSz+1~^67_Vd2MPHBRFtWugIpwT^vef{OmynC5MPh z==a!6wQkK#xqiDCOkW%ew2&~AnbBG2-tb}trt~DEdj-vE&{R;iFthzRai(o^iBB1a z+1DHY==j;{-bry9+pY6YnK32w%lGiV5pAFm>t!^Q5)!msY7gj^`rD=%%qs-Nc8OSvYJnzkPcqkTlRY{b=|^L^NHkk)9vIY7dPw^Vz%dH>|%dUqH~6G+#P>VGB(49+PAMSFrI9o{hbBmY=8; zfQ#=h*8E~y$4n;-NKXA!l5sH_51~J}kl^6BtWHXeOsr}`i}=zYJ|Iyd!DQdx&i^Cp zw)?tViC`QSA(@o8le!ZvfvP1Lu_27h&0pEj8MD;H;?eUL^;8X$v);!Sv$;-jOllD#E>I7!e^1gqTXutk=W?h z+XdyFjn`En0oNsScpLC1J@AaORmksh>;bahJ0xgudd_ep>@t;$rn#i&+&kk=w<~Aj zP_jTj`K9vj-x&Xk<7CI?@17RJ5=`9RtCtDK4yl5x&?HM=x)CeU?TS=CajDdAOz}m8JmkHB05^l{<_Pl_anF^j}2e z&$$aPyo+LfYI@jTWU}c;Q8Aj?Fbe3X%mzO!ABSEm)m($QT>o8cq)YxEx>5(htvUMiD>Wr?LajtP>*yb(>v#BJt(*JQ# zclQmR!#_oJCyirrW$weU+P<2dFUwOtqI*`6e)|QT--g+`-}3kPcFmio(A=Y~@?|JlCra92NH6(W)@vA{115|Q7s z^;%)dTq7R)V-`qq_tFiA$4ii_qQjgyZ|73$)w|1LXem4*3R-P=mlF~O0bQ+g?<4~G zb=B{3VK*J_ztO2W1Z{2~G_2v)tV>Dx5%j^~->=RihQdn~$&8EqV}U^u!=DFyHG5=h zbu}nt7)Z7}ct?J1kr9qCp#w+?7lFs}%-Tt}YC@MzLWKQSi=>a-M_8XVnVtvxQ#X2| zKb`&GG4N#*rqUh!6rNZkrJ^N>I~#uOoyX%=MRk(4Yv90B=Be=8*j`|%)KjzF7t@mB zu*!p*Yn$~Z2met^j-{zT`-mOMv$XO8ARKp-twV&We`DDbFal{QoSht}*+QT>t2ACwSyaUwI~ zrAQGWB620I?6LzKo>2gq<18`biY&XPE;IR790SPUxcB1hk*9Z(u#!Wi_`DWjIjMfl zx=S_}klcw$hOrLK_~Omxor>-H_ER_QClJW2N(zrGW460yH^Wfd?fx(4$~^_VtK|7* z4)-kP(b7hZilL7XMBnJtL`-46k^V5t3*~o9M#^juy@mnaF09*>d6*Y8DfB(ldjn?E zQT4%55~rGh_B*Br>E9W-_s|bSrD<_1K^4(Bum2GROQx7Cba;1$I991G&z~Eh4^qL& zlN?5p(HCy(>!rhw>xjmht5fVlC|UH-Vcr|x2h6h~K{WNa(uIcA29KL&pLCg+8kB>h z-$S*O7pwm9D=JD14&rQlj;!}WvmC~uuPtwGWz3ecDKwn;V=0<*r2FF}X;*yK%EjGF zsG|MO3l~P>YsisdJ9Z%(PI+Y3%5vw3De=!$(qIA#i3~Qh`e$U!siyTL2dOaZ$$En$ z|9iV(y}{ZA^JMhhJ$B=(=3Yxlqhudz+X0lTO1+3+rt$CTZX3p3HzIw*avN3ZqZ7SD zu^$ZCET%j8JV?9X-lB?%TODgPct;t(p{f+?{mU@< zdOcMWO~Jy^an=R@>cnVq5qk$Mi+yNW#@poAl}v4N4w|S>Hr$X1`<_4kh_svb1zM@B zn`GBlvUTn@&1v)G#~pe+#=mozxW@5tj;u@{IKh~JPn#$-5U}x@BKo3ljEcm8rdIXQ zJyse^^N;x{@3Aa=5;^z!NasrNLQw1(2v!0$KCwmo(q18cVj#k|2-RqNzQNbw?5G(` zPpOT~!;O)y(!#VM5B>A71bPqy=Z?q%W5DRQ;8EG-n3;;Kn>~OUhTK1CmQXeK*k}o^^kEDKUoo?H&AN@qPC(`{$QT3M+u?N6VxOe7;f;v}acL-tTJxNQ?I5{kq0}h`CGcu$H`zkj z>y-jENYm4=;Qdc-z-$5NKA_NHo}^CU%yXW7G$@I`gf%4_czV!%?Phn0QZGB+RnW)U|InNV*_Lj6#N03Fo2$=aEH{T5 zS3ffEB|~WYP&Ix;Z-Lx=1nno8>xld&DfM?S2`{$_vy5eEK8^i zaM9sJNcOLdvC`<}&Ipvj`9|@F??^l7z4!{LZbx3O;pRnMFa9AOgoB#8ChKi`PopJ(~nm(HQv6yyLB(kxi8$QrPTQkX4fLn zrOg5QZ*j}*Fo-^sZr--}Jf{p6CUd1Jxe@?zpdeRehS;@mM@#N?^TjZbA|M|@?q9`5 z3mT)|FrA|qTR7sR#lnQgEB`ibcTJCG*}%RwzUKZHhWkBjd1-4YF&xcxle+&y$9e2- zTBI~Lyy9;knf2Q2o-%uGbzJ^Q8Ao->Sn#<;viG|RvUF&#TQF+=b%vMFI9%jH8AWbB z5<%VX*iQ;pzUe~ZRjsaBA$tZTXMsi^vApP3@1cCbkZ$MK1GZGd9;(qWYgY7e8{Gem z6r*Dg?jiBOJJAk;Mbl)i(!>Asz3A;u#8v;?sh-daVe-=*)2ZDJ1I$Q&< z^qt|#9coMfQU8bL+7Bo;BanX)m^Ts#zQHrXQ066+dc?MrCxwgm?5p*$GF$pyO*b9< z5%M&Pbe;PJ(ZbL9s#3te2>YVs4E)X7AT$1$YyIg-T1VhE{)-9xvc7m((kKGSQ9K}> z9)RMUCnJ}j!vQV!w%%|e98W6F`33{~n<$h1L$gzDKuK&7D32B}WFUe<-)5N0wEycZ zSX6HRz$+bcigET98yC)$c$>Dey7d!ytgKgZw;+SOR@f57+ABC7tT`W#k6eWG)!)0;MyEyRkN9-!yM1!qjm zMyL_z3B2+Vf)-454`Qb2s*j7f;5O(&@V!F(wsRvzqtVW$g!k@UI9mYBw2nT6~=0)YKwY zVzc38G5U&3($NI(RSw4OFNqIyvWIOLt|bl?@b4D{9odNG@Fk>U3TZObU+;IF>B3`* zuC5O*eYAtIzsQZGi(XgY8|*5wtH^#1oIt(iE_(OrEs+AGf}=p6mHt6LXa=n`!S=Ss zlpN1miIc+vZ|CN!)G*Dqgsg3;!vi zJ)O#BG@#Mo4Y3^!iaWWk`f7ZGfgE<2Oz)#e^#ZN^__$Pw&FHsZ^mpWGaS?H7-E2KZ z%Z`IHAV#$G(4w?C*FM~1lvX2ToTR)R`!VsRc{1wqU3s`6UDn6Ex~o|Zdd>oasP84K zDgBNbh@~j`Rh}K<-xtck5C7akAKz&@OHMO?kP#oR)1W5*+LP?%dv9P=OU%!T?&4lf zB5wg_GqwmPCBD~C^VCTBD-W4W)pYiviVkK7stvYln17v?>0M}8Q;xGMA@PqhLniS8 zb*oT%Q%~zt8xu5uSuoY4*!?4YY@g}!zzqGP7fFa0Q3alby01X`tZ-Jj6M##ni%(n64{D&5duNR5zt0T%s9SBDvGmimyrG2snpp{3hBLqY z4ELaStM%fVB~r0Vkf9b2b1_F%#Y5#voc!5-9wwV(^7{g`OHU{hbsr%(M_~-f?B*!* z#!loyF)RsjF<1K6ojYG{70k@wh#i z%?q!m<+<{;?2iSNg7qu&AhZHHa^k|{r=permZUz%6llSAXFuSK%eFM6XGriU!N93?K%LdKzQfjjnTBWd%DNOpoGlcxb$ zirxajPq!oYQ~MmR;-D%x7Z^XGEZ+r*wDKD{9wV=An}Fp%0`(>tnwK)2vh(13+i4*- zja;fzBz-;M_F(@rJTM{RgydQVc|fL_mF#$4(!Fzfpjsp$4R5L(R~fk3Cnr>;SgweY zPEM;=6cT^dqWq_3rw|_>cA1!}F!<{~wEIJr)%2`|=)hlB_XUm25TBfh)2P)ZgSOvm$Wst8d`58;kXq+m zi3Az;wr6~LJ4s&H!VFTb35y$`xjyn$@SG=si{z2| zw~aOP=1h}h?oKz4M5XzjBWtfvr~bI%&HtIe10!;H!)cB@bEwh2NKoD{K^U<^friF2 zbtJHHVqGGg&+XObWl0aLuo0kP0x@~lOSZG`@?{!C`5`^B*7>bIL0}o*f`3&XC{{#e zR^hV)|Dnb7cP7{yYC6+e>-FB2#ox8AX~q`!@ns z*pDSbub`W09D!Ch?d!{~@-9a{$9}~y0u6}!31fW+?BXfF0Gwt`coQGR-#0J3lyydB zGhd!y@&LpOfvpFK$efFD`(ddJ7>J@Ok{9kAFzI=f)Ca?<=npS4)|2d0gX*ja5;iy- zVQJ9Z8((D7)-=wTEG6>OuqFbOTy@?MCmbCJU6X%c zJiNz=ZwQUpdWaY95i`miZZ_*GJe13%dNq`lxfU7Juz53&pd4W&ep<1TITr&)xG$0V z+p;Yi*nhtFx;KCqPo`5wV@JO0{bgCJ*=wM`EKHYzlYc8espC*Dx)WL+bow!Q=oR+S zv>w*S_t4*(Hxg|D)CP{zR}BhV`zJ&XQlajP{aZ9hF~sH22G_z&@hilCW2CJaBSV7_@F7WtzHL!UTEiul=$*3x?Tl0<@R(~y0rw<#&Zm>q z>xc5fXxu}hE(mLVxGm}}vIe*Zi+hn#4@w8t9iSm=8e|;g2jG*vRuNv0Q2kwWNFa#| zGiZAd9`_~Xe!K?daGz`+OI*#iYlv*(%ZEbgiUjBMb(B&VwZgQReZHeg1Uxpx0^kL^ zPh8;IGvEft>J5UQ@RxL-pGAwvXe~_KzH3@-WsiWFG`!G$Ozp!AHJd|WKInqWhIqnR z5Bo^YXF9{Soji7wGW|MKvqO7No(IAZ({^XS=M|Q(e=5kIUx}V=&r%$iNm{Ny7X4{Y z(W2G$7DHVsa1)#y3%W{~{`CVqRGwrT;9Tt|NN4)!n_3a7={C<^_F*Qwl2c~yqAK!j z5M4M45d7U50W0~@IHV1g8cYYr0QCb-ABA%`=bv5>TO*yZS!JCUVtoGC_NM_8R~2F! z{6D%I2Gab-=dlPdH>{WsP@Fi!(w>9M27~uwu0EyzIGC-m7??LNbt~p+8mFFieXiz| z(~#D7#@6yF#N4sJ{gUrP4!5xRXZ9dg>I{9HJuZF~y2O^;9|0Ly^G7u8v4x_GLpQsP zXZ8uUA4UHNd9u2kiUU&n;Jf{INbU2>VQW%pM}#Q%3&1Zx&?mNTigX#`9wRm*Fa|=j z99D|7_Qda(a>4)OkR6j(KdoE0gJOYVpcK^ho(7+!A}tCqY_CCQ?A(cMRIRo_uNgPl zrf?6&r->^#MPq%hINoM%L&f*iW8DuKB)+AK`?E5~5aO~HCieSQTOJBJY;W!G4n>bF z%@&_)5G9|7`ahq5-6*k=ccq1;es7aniH}f)YV!IC_3LTrk_w_?f(oT_%`e- zTZ3V4GqFbi#N6OWFLD3KPGPC9Nf1lv#o7?`{N%V@^@%89C}o54X*2SW$(~howRaFA z!^P|JADZg<%~$URWR_va{INTWpvC&e46Dk5|EoM0p(_lOAj|(4?b+{~qP*P?Fs3oL z(-Qlb&A^*x0AV(EzHA7tI@hiqA`!aE`8C1R9X@t!`=FoE32Hpn&k#NxPLE8g zRIYV?dWE#FJ=$Pt`#~M2%b^ghZ2Hn`IP^5kpkLLI1bgY!}WUwoPJ`B}W*iTwMR zljm-pdKROFo_DHuzGJSmT8O3L2x4%wHV)W$J1|a!*PVF%Dv7{>(?d?n{MKbfoL&*k zF%%Ko{|w~7hvd8+>z05$=x|*WoKE!oz9LxH84;E8LI)!&YtQr`e9% z>Z`8nnr+Kg7Z;=TkFCb9+OO6E!^GOx5I)>vfr9p z%u;`iU)6<7lfbP#u~(6~YPn|SMm_F?GHUEUUlP(-Gl}IlG1PZr6qRu20Kp4X;d2o! zltX+gj7mt}2Q?JCyT|5sSlsA3j@%N;oxN3Px?wnoeX`8w`=|x~_=^GZo3j*Nra6>t zopV6l$Bw@ou{bsqN|<+sK1bfBR{tupdAua`&m|3P)e5tUz4apSMC6#Mp4x@DV07>8 zL28liytDg8tjxJMjc!Jomr|r5@nxyrsS7U$NEd*~Nf(aiWw{+j;T6MQF=F!h_-zj9 zENFEC*?m#)c13UgdtaKY)Q{deN*E2lebda=ns*}QaE{a$NQi(~O027sgSm`lPFg37 zclIe&htD)$M0*$gl&DQ&Vf<2x(;u8~!5|fNU~%R{?fm{lq)*V^m)Fl)mt+nWH-BPc zh0KXR8=uwcK9h@&r*rJ{)xckdD!Z6!F`_+@Gh90iNN-2JGWwM=Om{UcBOyMxU8?y# z*s-Wx*u}tO8dL4~b156iGIgaOETfS4OizMVPWJPVPp>2Ni;=MI?gHAt#BBZB>DO5) zJxVT0ZWuzdE-870`&Gemm0##u1P0xFp#|lnJy&FF>AtB~HqY*VXV@p9m1o=~?mtN{ zN4SMgMv%9)a|ne>Ei_-73`eM~zri5xJlZHhSeE>iB?TX8+NgA$zo}<@QkO9#cS%~v zM{&RwWB9X_TR(Rwn3m2~VbqTf4xBBC6vNi*x56ZdUb)$0WlRi=I*-Mg2hkI;^gS^W z%of#^HA_qLSq~MBv$ zesf7lXlJUyW1u+>e8g$p^v>7p>hcA`g19?D;?G)VgS%^Hj3$32J91QuJd2K%`=Sc* zE-c7_QS;XdO6?V_C6$lF?7lOm7O$t`$e8^FLGw0bF<^SL@mB&{1g&&G!-&{DD}M%$ zgG%NT7iSeS2DUh_f>hoxFnE-4$_}u324D6o`Czh}-A+ogmD?{;_q+s69Lqjh@-LW~ z;(}T<-PF5SE#s6zJc*BqkdRxkiMU*&Q|aBr#J@0ZHBQlFEG#-lXpI^73~YSZ6ARz} zrfykB?KPwWT)v)Mc|0aSqS+k6P?_tTCXSJK#Y8h7~dWVwPl84?z6JeT~%62y}nD3IMEMssc+31hlns# zkP3cJ#Qwc}Z|wVfe|hqUE}N3TLSgcPj*tNL&MlS~*EFiR2K%L!z|vwZ_nyr%lgA14 z+sQZc?$YNR*+1TK^m!Ye3Ld`;o$p<=ZK{D#RowHe>DupfX)}?E zzr0_N6V5H0Wd2pdffHq9+o8*pBR@Y^5I_?^;*7tf3+24?&K0jyrQw0>VWBV4{dJdQ z7C~hRgK_yyihmm8(O&fHd7HiRnb3K*Q-nUoEoQ>MZVe$3cl5lq=;CnSN46Ls4^>-7l5in}7@FTtFn+RdTcVKa z!!M3g&lOd8pDMnc4kN^2(~F9VngJFun{YivtCls0hsppvaW=^-G-egM;ox{OYhl0P zHO<;m6K~gR52AXv6~VCz)rs%wI7E^~F?0xXV$ro0Skd3Yf}bh&%{6q9C2j!~m$_g? ziR)?OlyN}u9kTG}z4~5M*6+RMz6wfA`bg5`0O2{LIHv4XJ`Z?SE3ZPQd}kqwuEp~e zk-f&Q?_!Q2bEiWd?bHk&%4Kmt8Z{+8Dt6j2Cw`Kp_0azY2mY4aCJ;Y(znxNpNaC>; zp!sf6KTT|s1Ne55&FLj+Wf+0F<{x_w^ah9cTKt>6wmxdG7JTb=kO(c5zbnQ5%g`p# z)mQdw7Psmm+7Bd=5l5viM;_@p2lGW=U6NKa8^`ZHN&43&7rZJ2>#+o~ZD%|BUM z9cWQ0Y+Y|w({hA^%gzOg#L z6e~5woeuF|cxBQc8@I4P+?H%nR7EvyVY!0WUV2&7UlzUzqLIaWn%$sB5Z3k){H+gF zx0B^x2Yc#Yus=<=#Jx9x+}^y|)nB;9u0e$WVwF1>AM!Ovvb5?08;a|EuM@Tz-$(IV zsV;0gKi~bTr^k6$sYoWZd_1H17il!nTh{~jOyh|_iGM-0q&dmWgbEC@!1+Z>MO%Nb zG2sdO@jHt6+!`XbiN@AxO#!xs2b67}5iC>3C~jMdI@t)CXY2o2vA`0SHU`0_?Qg~& zYd~(rEBnBa6areo3VriCocb{LexoMjvHH!ksWd9XPE08{P9b;jgYiPHKv#CSec`$9 zyELA(+>GVae7CxMW!rp&PtXV|ko}4Fw(@cJ*4Z+vF=8p6aXY8J8tggJv{KX3 zlceXDiS9EdXu&xEgHtzl%F?ZdD$ey$fJ4-gKM9kF5=-W9T^Bd#JBgQ9XO2xqNmBVE zW7+(f3Yu~Q1r%E()EK%ki)ZV3D@I57d$j6V1Xu%0zZpv0$~UJW8i%2f)F(8cLJHdY z-4B&zJ_izgmUo-*=gnJ6I3ACUDPh{sEQ2{WY3c{Te~hiSseGa8TWJJfd%30dAiwAW z%{)u4)!E_`jyPO(e8O_OpOP~{tjQx&gvOsnU7hdS&R6p6^9MNgbif`0rc&*8-TaQ46qz2-xxpEN4SOwZS8e#JrY4SUp_G$6fw;C_q^?__pI8Krysds*>{Ob2BZ_( zqV{~DmZQpS&^0Js>)J6V0Ky5@tn-gx{(7(x=RzY2|p zlP|gT(iMCQ`(!SU-;sR$QT~SpY6UXxdNj^U!ug?y>|M|<$6s|4TXST^cwQA(_Cdw(z|x4?L4VF+c?2FUka*8JIRAMOE~Vg+YO)}N_CnaLE;HnP z;;t|s`>#K4ytXRtFuC->=g(9Y{0uQ8Wnx{;$0_%dPoO?T!GmM>xgm_j*3D-p^t0oQv!=%2!Z6*p|%skrnhp5~j`1-(F)!O2YGmuSZtmTjq zamP|Z0GjOF2aN)qZ3GA!aif2(ePUe-HdjS(jDSeNYWst?#-8OaeZ)U0mN?0$n>!2W zhrhKD&(llA7|)(@r~bREk)?ZDEv5iD@j=Z8tlK`-Y|$HUYbOG9A?~lDORDEmf-B4a z4}C#`zA(JmZD{3*#FA}h9Gvd_D~<6l{1kfc$NvDZ{{X>#1H%6RvX_lCy;tEU#P0^# z_^V1fGcBpOu}5Dr+^FHE@{Zs@`JCsieMkEU{>54c>>sCi_74pFQ`Wpyb>mH1(tqtu zY-Z}-*4o+zyK?q>!Tk-$3Nht+J2|6`~~nuri*UU%^%0_3C|o+ zjFX$dpVU{-{yMvk_s6>3KFsKRu#&OO%Q0I})9fe{*p_n%~ zH7c_b93GVr>IlV61Z}X82+yqxP!V#{5$oSI71WNZ#Sa+I)~&{>x%oy9gilVIVW-a4M`(>-1>E(&`e>tGyqTG(;|=n%p0jag((CK=aGu2 z?9CjmgaB>HJ*w@bChx=A=AJ(6{vOmL#y`Dm^+~vljPr`t@Wr~^OL1yX`u#rQJfHTV zas6q-;zT3)`|xpLpxeUhk1Yrv_tC5EAQIpak~`EoEC$xe;Mxb5b22fdk&VHQ!Sw(J;NF!ZObAd82s{uu6&~QDk=COVNj1og z03Z&z=xWJ~t)X}4AQBFJYDlFx^8%BRj+7|{Ks~xtu$7IzMdR4eYD0T*B3hD)Nhbpy zp0y{Fy3IGJ+{=&+OC)ig0LVR5(mao_D#wrqPfF2j1MC#6MmLO{9vd|=I0u4GI60;u zXx9tIK+Zo}bIJmYj!r6Nxs>-ExIAFDIXv;!n5ygo9BJ?IK8Abs7M@p@AG>rka--RcitxG(Z zPnXxFXWTRSjf{-+&38SHcC;bAjubm%Vsp^cFviIJ^Vbz5kq2LxoT}4c-6Ww$IO7$X zcNZ>%(#9hI`t+uvJdDnxI2j_PnV1c%s(m=9UP7^!1cQ#$$sMNv&h3s#0Ghcl41^|X zwT9)44VVCN?^Z!_RDqB=trT<}>?NRRqTrmKd8rhYI48GE(zfMZe+ml{Kqnk?R+WR9 zgHBOGu6ZDI_o;6qOFaVWbz-B+JogRSt1r(uMkMVy&(gF$%(qt7);Y>t>Ne0YUcu%> zKl&A8h-p!5(vxnYKWN3L_HTzgIi;%YHl7mFVZrrV7yJ-Z;6%* zf?GMJhb4g@fuIY?(1jgEGs#ss3NlSc6~d`J@+!(Om@6n8^ckQA!x-I@fI9^~}GcfsY^6rY6!r=iek>XF1wM_lJMZX2{g_p1id_1ZkRABA1DxGoBo z{7pozR=o-D=V@8KoD9}%s!Nw%=b)>SxF^0owN!zU!1eT{t3@l@Qn`dFJ6Q8k%ln`< zu6gTIxdpHXH5~XqE;uLktB%C7lR%`6j&bQoVhMo5IXwxfT}WY(*NSX|C#dg(>r6t# zfrwDO`&FpoJaf=`b*9?F%Lu}NNaNb4F|s0)jFHbh>p8MyL`s4be>z6%*N&W0umt3K z@lQ|(931zo$r~QUKQ?;s1zw2*j9~QZRUiv2p+`LCuc<1hVvH!kHE=~DH;rTYnDLKV z$iII!?67PO*{vN$=~6A5@=sh=VFN!m8S7VNdylBqi~)r_W49FajH2}FXgN{A{EbMU zZ5ci4h9Z2A&s=t?4-1kxrmY<3Jp}+R6!V-@90EW;rAP+vzmI-tn?WRxu1#ks>Ptfy z0C_)^10TiL*ix|1-V|UQ41te&&tgE0&P6uAp!iRaFYQy;} z-;T7rebf4yRYu#KW12z1@ARNTb^{#dfIeJj(vBrWBq>O=#o+_gBWhBOr?wZ-~ zGKcXEy}sSXP{L_FGaYS9D2}vJ;H#X02s_K>` zl@MzeajNoE3gT%VDVm|83(ZwzJFTJ2+9jLUrcnb zyQc3=KO@Mg@iHs9AITsBNf^UbJ0c>&;|Gt%tlpU%C@t5o6;lq+`^N3l*1BMPp~x^{a@?02{dX9XP6%Unm2$Tk@qlN;Wcj4z;nFY6nzZq+>Y86)*cEx2dY*B|tdH z;9{mm8*`D7LfpmmL~kbfwgZF4YR#RnNg8s%md69NRgYw|dBJg4poncUcgKFFv?2@M zC(3X)b5kp;4WqARH5!=J27kNI)RC~^NaT(VI*JWPn8blvCi#b4dkSL{B$&?wzo)$@ zbwj&7Jr7!qWq8=g1~(ov-l;NWB^}9uA<=>D$u%mFWDHNR;;O>?=P1OG2hE>ava%-5 zIqix}$;+Ejl5-(Fe;TGt5?_R5&eNVdR)lh8e8nF(_iBia5qQ&Mk}=n%IPME`GvbkJ zkB2@YK)44&yb1^XaC-QDzT)xZM(4oV++!T**LfNL0HzOL4%d^J{Bb=jAHu?lD6cH- z3Mit0JKx!b_I!8n>CQ8AApGHwSK5dGU#Cx}YWSP>Yhb?}JT;Sy>KM<}7+2dQm0-N| z;=PQ!_%GDs#QbQHqjPR&WFbK7ip*lA%R3xXp_SQA(!d?1amPbakvzqlsf3EVTpW5;3+taQB3qN5 z$E{d=<1f>%BC|ChWx0+Q=*^X1T5EDmiy+3;3xY}Hk<;l`Z9vE6?Md zJc!w`kh1RnhPg4)<-s&iiNGVZ zL-&Xz@lHJV83LGGa%qvc*;oKV4xK&CS4Jn6VeTqN!!OD~J$h3jLPpSo8S6j|c3?qH zow6wwUP)n|c_7lvj%CIU0QK!mbUtBBaoErU%R2n(N4U*T1hQ?~q+z|fRbQNd7xkxk zlSBzok~&ZZ?w0INl_4As39ai%S*^#-&t5B<(`Iq|)7T2!uzV+$!{|+CG|eLHY}qk& z1D-n7phIx{bBxtnAVR7!ImKKtfVja1v9_g?D`9!+jN}tjtQ&w0_|!cBjaZ&`pJ7tQfdD1B_4KD0LXZK*I#59v z0r|~RKuHvYatQiUW#>DXaywIVlgQ`k)}Ex1xcB3lZ&U_Wu?g}RK|zt+)ytJsm55#q zN99ETBrbOzeXDX_!*uO7Tj@+uD(@)5BHWGmJ8}nX<2B3tAb*Xwe-EOY_JZ)PjivaS zJNe>~d_Qt!wVnlYhjQ;?^BBR!q_Aay1ym8{2?RrlR{8sTsl=`ltGf6ef7MDCTGZIcl zPhRx-8(hTc%6s4TR;n$H*|3;G5BlAl3X4!4ApXPDZhR-9$Es?OY4b^@>C;MMORI%K zQYLZtLofq9vtJVUOW`N&Pigx+c-Q_4bEf{-J}zGq+*#_rCeXYK3oM!}+GVxlMlEE8 z@&@Q*ZdZ_bud99__(osa>-JL?z7O%1t>E|Zr-N-gL1n4w670FN+Yj1`D-M8^EA*+< zEQ>!6wSS1-B=~itc!T2)!@Exj_@d?}I=_dkH3M}WqXr>D$a!q854B&JC202|HajpJ z3h+A*j~+hp2mBFsPs_WuA4uP))#3+|DwE$>7NHhoz~>T65J{u2GPG*6FO z5A6kK`$Ito!&_T>1o0okc8rK7(_c@DN4*4OG{#pX@G*`LYH2$IN3)ro03u-&kKBhb`Xp5OY zv=Ye8XpN7$o?@~)9tJ8>o7ondX!X}@Hqv>|1XCE2rvo^z75$R`0BKK-UlYD0c)#{` z{jR z0OV9y5;o8~)rg`cisOPkDezf8m>iymsi$!diC4=|LC73qy$(pgB=q&EEX zp+amU1^CT4ayj+ll;DGv1Y@VICO`$8g#)fnN;hCTa0)^7sLKH%3)4T9Q`CG*qUo9Q z?~voJ4ozjgZ}{6SXMs}^eOi& z-ed{~S2-E2*d&VL*={3X=-@IsoQlq^8j#jX!n|d(80yS$G5Asl_L9JoPf}`TjY!;2 zKD3)bJwHn3dWz&^Z68tjRFQ&x2fa2_eyBPgwInD?2K?v{C~j>AY7Vw$oYxS9Yr;{xYhv<0PWhfJOwEpFw+?_ z2-G4YAMXmxkc?*>j{cQz!P7imEYl=K!5V}_A77kMw*&C&TA3Gt1M2N`&xWe)A5Cm$NGuEA9pfmAFXgQxM#;<&3*13H^ax!@ar|s z*$E8S1a||aF|#bA_h3`)!IpM)V}fy-cHN|AOpto|*HmMsr#$s6T4j#hbmy&Ih${E% z-yDjmXzds~hzIFcZRCl|Z5ihT=Cm^vHkaH8>%h;wJ(bH~6YfE#k`$F*p!BDF(>FZ; z=9=8W*js3}gSl~@q<5>ZuD~~+m#tTbfWvSE8g!Bf&PgD98nR<#u1>RrahBkY1z3(_ zi~v23YZ4oeGU%{grvw_+M{F+D<#A5dG*0A`Fd!=qev}Cq6+$@o6q8810006x=|EE1 zI6JThG|`0gkrkrOmro-p=Y;_0@vE#tTW)T?n5yboSyuoyarLLmBWDGH#sKDv3gcQv zSjvVO^)*yT!N<;e;{?>wLo3UKQhjQ*!x1N!&t8P?=BS)M*}B{yr%>VgOS)(TYLxHykv4gt0qQOECvstJPO(% z_aM1%GGQ8?{L&;OxXShKnrfz9r6ZG!RjWwY;l2Gw6+l_EQlMmDXOo)s@AxO@hnvM; zw}-=91VHjy>%VJINBr|fVt)Z%MhM8pN3Y>uL4Uz0Y!c`8@bJ#Pk_Mlo>emFGK?`Xn zKAe$US%bC`eunsGYRLUI)oz8|?DqwyRy1IwWC5S$S@Y{m z=v7-7=OowX%{%CQ4c(17^%&EVao-h2;`_`!@H!0EWct4Gl|1L>U}*(N+>dsSGL z%^9vswl|JF>4Ie4h98Ae^VouVbI-LnOu+%$+o&`WS$W3bxFilTX`%OWI#pwE!9x&E z2TD)26S0pH-m!9f=(&J zZr#}ZDuT*D&g0YZ%}TPqF`hu62@b&7Ja@$rkPb!mrX8G`V;~uqTVGV(w#}&@oG+PzIGdLfIN}3yF4Y=g>994+mRON?XdU3!U z{#mLS8p5cXc9Txs@_;E$&`t+FloiSA)2O1rCm@hV*EEjGPs+H*0~F%Bhdpa2;s=a0 z?}k4O{6q0KSkmrqt^7NpU21dK*@5R=tP!+^DD%N`7XW&iT6F_W6LSVV>SCM&o}Tr^ z{?0!awU62h;Qf!qjeFppz2FHi?xkC81H^Iza@OD>isPc~5(Ymh9Bn+2&34Yad2De) z)m#WKT@iY17_NU^gcw>?INEvQw=YDDyOijpuX8<4@8R4MN#h+pIHN88kfng`I-2kw_$H;4*I%&~qoml(f?KbN-w|Q6C;nMT z*Nmt*^ehkKUDwBd0sKt(qwu#-@JGTQj+%$Vt#0fQABeQwK0B8Yl_BCPCi%k#XX+_)V(#X5+z@SF0wgKB)IsF?n83P{r6Ddso(9 zvqqhL;Xm28z#bm)EKLpOo$zkj-u@?CG;$3ww<8Y3X9QOdp#H^QvB!pdZ*TE0Nd1~T zGMe?ap*{3Etd@J-XPQGRxmL(OIV5N9=ZfoeKZ>&WgZ7c}$H2S27i;)q!1`XjbAHog zT*s(QECv$2fK>J0cLqLKh0~x7kA4kMoA!G6LvQ;yFYK9T;g2i8$)+}+7LX@z-P^RyF+kma z)-g5H>QO~|Xx0}nt6LjIwrJmw(F6z-`)vSLpA%%?LleW1LcSHz1+hL*&}_V{e{<~V zkH;9V8vg)-qfDRh!%$kZfepXL4+%`p$L_E8f-di-O?#n=IAM<3-XM|18pjeS;Z;BY z51b)qE?FQsKlmtg zo*was#{U2Vd?m8+HKEs&;~X9;v(u7Y&v9d6azxGu>^lAx=Ii?q{5t)rzi3a4za2ki zf7)l_C&E7-*!)epu<@n7fvYQ7NqHIx1KwF?K6A$ZL2kiOqTfyJ6 zN5L%{;8%bm3#Mr@O*uUN`sQM$X^wljP=5;P6UM$B@TbH-3V1hD)fPQp#aDMR=r(0? zZa&QePcilifb;4=?^(TA@2-cN{{X=-v{`iD_$I%CR@PK_rO};v=D%eR+3&%g5%|sH zEerO4{{Vub+3Nlj*1S)s>dWwVK) z)nB!HCze%4>=IY4aF>4yHMjk@bZ^>WFCaQipTWET00~?{zxTgsmhokbf!%f&=dlCU zyXjaQB6jbJiLPgHS&oJ>4^G*s)a@B3^`ho&J5AGurK)On_K*jTKRW7B8kcqoi<8D# zhyZsciDNLzVv$O+ouyAvQSU_tsSZ_8#FfVt9LyMi0U4>%B4aMh276Qz4XzbOu%|oN z9>g*to=;zDh=QXWRi=v>J^ui&twP`rpFKNPWQ%7oifyEfu^iO5E?vF72(0^wl^ucTdn@`$g=z zW*JD7{LNekh&*$w>e4b7FhE=7jyAy~+|`dCT*%%in6vR3g+(WU#dEWQ=kAe{Uqgbz zQ;Zbet(^5LJ&>YTc{fRe&Tvj@M3lMtx%S017XSh?j^y>H5TSqrq2j$1lGw)V7zuKW zoF3TeT@S-8ZhH-WJsRRxHrEKjS9jcT(-nb&SDu5tX!sV^OWzk-JY)e2mvVgotZ|Tx zu#5E=W|CH7dZY29_UN^bO#QgN zF<4}so;$jf`xi*q{-(T$%2byuK{-6vynn$_G|{4e!9ad3h}3@n0AIG8V1Lg`8+Sjc zuL6*Hk?)X<5G(I6lIEiyL&mO>sLNVxN9^R2)x9ae2Kl(;@MyM}3vh_1IO8vD!j5ZG5dS{Dr-_icsb2c1e~+w`Y5Rt z!YRSYClwf65>)<_q0<}Lx}Hg)Zea8$iO&2dOjS$St_|5!CJrzc+v`^&1#u%MUjDUG z>N9bAvc5iFPL-;$D#_|cr(4J;%#$8-+P0!sMCdr`YP2@dkf&$^*!8O$wvbjsjB~{e z+_iS@J#&E45yw&OPG$u1cs%h?s|=TOf!NcUNeSV&&rm2^8O>{QWQ~?!1`i#5X&8ek z+=0RU^?N^kR$>zQl_)RC3@l(SxXB&J$ zWPkEHSKWXp=Q%yIUbbKSDSDiEtY2ts{3@{|^PkS8xEbnBd*Y!`4=lJonXcoJ zZas)az~j>grA8!I8QK8nnsnI%Zr;63M1Z2>IXM&0O`qhOKNWZ#`k3rLbD$-;nsdGySBmz>z@8}O|oBfsq9Pv%Fi;QCc z3@E7>F*{kn8P7GWLGDIk!3!89fs%7pp>)t!evEo9l9MYvC2@`cVrxa(w>JSY3I|5MJ7reuzfR=L6bPaC)+f@ zPC+O9;*%twm+|6_StN}iVpl!7Q%bWODCe~xQ=U2wl#W~8%8!DTYoJ=#yixd5l^?$mm!W19^leDf^vN`Qc%)) z#@u?*l6#-VkYJv;_2!PJ9fbfd-seABR7M1rJbTirjO2AR@<|x)>L>w}RxjUM)AqxAxKlfeS5#y}sk?kttk%wMp4(!>H{$#ecQ^uk5j` zc$-`B4~O-iix$6Xhepu!+mjW9gU=CL%EiwL5SdEkWkxuzbM{&IoBLEl`$y`(vA^w= zec+vA#h1PckH!8H@dv}#Ce7^QxE8lolcOEUDoF9L$mA34RDa;1m)hQ^;D3bw00VSS z9N&0;AB&$Fbl(i?x`vy2ZDwsOM+5L(*UV7;?V{Ff4*QqZll}=sdu12=67RvD zGmB7#ZFHN@7hiaWV=9lb%9_RWk_R8cN+{s^*9-eY_$T3i?Zx2_*<-{ScZlx1AK}ew z#+Mqlhb^7Jvy|gbmUyk>D}W?-L5y_-f%mGpQWARD^sfZ`NbxqG{{RIZoAyZY$@47y zOYq-J@gIWbg;#3LZ9BuLqk+oqE{rqB$Sdcs*plP+;?@2Zr^S6&;y;FTzl3@QyW=fp z%fpcAo@S@3+QomUOwqiAVl@&82qT;UUgO~p0Q?@(Kj5!g2ao3Xk*LY>3QvOuIJ_%5 znInTwj^z=jFZ?d~2WNg-xzE`y?@87_;FtP$h&(59lWDVjD)CHG6_7Evx@ff~mL|s` zgpNTSYfDDfCha39;%Dt0@i*d+fW8;_3-&vn%S`xz@r%MRc&|^?{6z?Re-G$>QoQ#v zsK|ES%L=FmC5gus)&9%AI{2||RHfysyQrhIW>d(MnbXw0KpIZORuiT zbnCC#zer123k|I89gqqSa$9#5$$UikZR1~y9~C}6cu(QDGHYHO{hxjmUg;WB!mgT@ zrD0-kV76s$K4y$AJLetgq?~ms%Tw9EWnYaRKK-M<4!_3F8T>Rp6w~#scIEsXrrib? zl4%RIBA($}?z^yd0DlP;-n=T~V{z-6$M`YvbpFtv1Ux74s!tAFXtV2DN`GnSw=wK( zHuf{++*_6Y9kMG7dSbN`#2y#9@lF2#f-L+`r_ZiwR*~H5nmjjZ*EW#J{;lPV9H4Fp z$Qc!qNlM6$=Zt)2{h{sqP1 zR`GV9;t6!?==Co+{i9C2Skfi{VSL+n5{`4}UUg&t00l?*aR==IseBF9J^^^Wtn}ZB zdXIsm@m8LKTUXU#EU-ieUD7f>M^FZOSHApe(`>K4D|i}NujaL$-{N{prtL*r z9RC0dDInv}SIgfE^oXD0C&TD1EiwJG{{RI?@eJ4TfuA?aiFRky09*B;Q@-PmD?XC_ zoW3t<-?VSQ%@gB3kKxO$B{82@O{{Vp+#{Go+d!qO!_BZ&0@l(WCaKoqg zOX9!8ZAw>^{F`FFl4M__IScsKxBL>Hz#cvDN5Q>2;z!0)F!7(ozZTs1=f#m+Cf0&W zmBbL0{vEI)e7(0Co>du!iL>W#_$O!VNBbV@pR*@`{{U+pH%POG;!d674-nnQs9Jb~ z%egmjU*1P+43G!;#=#K-9hCA8E9pNFe$;*r_zmzM;g62KF=-wmzSaC|r0Mo{IyR4{ zH22qo@=D=&yW8AXrGDy0JA6TbMdd>FNU;V5cvAW)5LFWY2rypwVHc< zDJ+_48>nz_!;#N%N#0T&u6sZ1qx)ujB>l5v@nx@pbUk~*+Sh^SL*iczc*f9MURzo^ z7F)f)^Uo^ap2Ty(6^-Cu_$W`nPl*uRYTg|1_lhFWpz)5Ipj-H=_eyAV+t$6fXd}3W zX25vWz}$Tt9`)zHv-iVa_$U{}U)syZe;q$@J+voKWA-s!1ucMg|8#>-dReU zYl+olVp#!my!2SdHLLK-PmJHP$LvGlPk|q`7s34#;%|lgB(}D?p0DBAx7%8He{qp7 z;G55vy{c&EeKeck*%U5|@2rnH=Wq9mDi z1NYQP>QQ(h_m00mA@3wXo+38AiE>6Z{{nqcv-g0(FU@-LkwhD+u|l?mq!6oh>m zzM}EwnQN+eiS&il<EBy_G{1Cx$rYlY+SoE7+@@oNoERztCP~Qul^)>cStO-thXC_v_hK0)SA3gWWH6@>D&@GkFNrw zlmfZVaqUI5)Hjx~-M~)C5Hjbe2byqF;O*)=8knaY%1HBnA4JRIdD9%>TR8Pv#1P@P z9OJe{be=ze$KnZCoY{aMh-jsnwjDw zkOn(vwKPSz=NQL+#<#tUi2h^yr0b4-tGoDb;a1UXA5pkcCAj&3Z{b?Y@IQpEH7n@# zJKJI{SCB~i$G72LfU$GRi~+z4#dx`nHva&-hs^1vORE?%z%f!jqMUdfj()VDam6WB z&PUVhULVBuG~8x;AJ&?7m)Xx9%}5nby%+%Q}6 z1&9Nhl-qX`*!-;j0D{A4f7##gP`?<6lmUCAX!mTsyR4%b{{Y8p<*T(snQhU&Y-A5= z`z!tnYo^1ce%%o2nIcU~;m(aBjP=_SAwQ*jW=ET=hue~eq4cl0%caeSkD=q@Z5iCY zpas~2rx_>psiO+NGO)*PKhLc~2JkXE)Y1Hlmf&~DuTdOPS7YW+FskE%YIv}%wU>-= zDUhyMA>$n}-lPWv0tS6(A?St9Ic5U`k)E|8D@Zv5apxkN9!q?w_<>K1gk8Z$2bv6? z;-=uIc^<-<3~DAT+>^(yX=Ib;0b8#_j)IZ}+REIJN#ymS_7hh|gh+A*Ps%z9LJKk4 zIQrAkl)^|%lh=xca{M+hdx2Uawj_nvuFx}s)}oGfY5cHx9Z$7N6m(t)<0hky*;-ty zejaR zZOI~~m2v=Y{O(~oSCDsw6j4AMe+^_2d`8eEC+_QZhw_Y-$;dp9 zLGEdzE=RR2s!L>!{81h}&IswodRKYKw;6Q7PH}^dnW$ncB32`j*EJ;UWqdUw5`zo? z~_>c{=n-NzL)EHk(q1MN_#+shu*_=5qGLC?K0 z79$>HrBsfY03NmV2mBK475*o9`d#E0{0{;{kx}vu$SrbFj)0D7cICJOB9#!Z!h_R{ zRIx-jR_aE0rj^HLAWu>GQ?>!X9eY#OAO{0DrwG>|jNtdpT9i@?G8Rpk$R9&Xx18h; zTvON*+ed%Wp9~wp4Z+XnQrnXihQk=on90YbPaF(KL64^o~tj@M!x&O>zd@a6M_d2ZtjJ4h}P(mDgFO(lEGH92{b^l=~V+ zA*`THF>T;b7B=}*R#9!Mv~k8OR`zKR?$09?l{hNcQTf(Lu>%rvPu84YBdtbQAnVV) z4*U2A7^Vb>eGMwG&KY|4sK#-F_;#nJIL1BkKoTPme;z1o0fS8qjtJ?}hhPcMr2sDS zIrgqoUGPQLv+>u#{{R*Z@L2pW;k{>0NC#GH=e8xk0R}%B>nyEqbzA7{EY~k1U~^bM z3A}URKaBnmvhn`_fptN5rQTfL+pM5qtk$mSG7x*ILroIfTO23tllvuj1NPYXF=z09 z$NnVL^iigGir-n$^}QK_-Pl~m!Dxy5tXGWoI6bS0{h0p%;GCZgKV^T~vM<`p!~Xys z{vvpzUxUMv=}jk#_0P6RbsLp9mCAz3yH^T072A9;{i?hZ@pZluMd7axY6{;MwC#HH zM%?Ja&OI(e9L+8|h9KaI>AX+zH^Tl7@ok5J^^NbTYW@?}HT^m`M&)~LIs-JskruV%?}vO&@M(yb4Y}YD4hoV_ zCzG1>Z;fBFf5Bgi-wC_}@JjN}P||!q@XuPZ)O;xxk$Pd4-R)s9M!sO@7{Na4T_=LR zHRwJz{hM@e+FJV8>^gR<;5PAGmI6@oL1dwsn;l3SPdzimYUmy!(!5*X?H9*d8L+uRSq*sFe%wO~!nXbgZu;8{-Wx zPVm=(qK{ShiK{9_r981^PSoV)JiDiZu;F2FD91N5qMmRk>4wX;#oc*Z2AO6bvXNOHPLB56?_%0X?{1>h3T}5;qE`-Du&=Bicc~-K?q`j0SY+>AXn196Zn2S zBl}4FL-^+wwu=wM3x5w;*=hLNZCg#XFscLG5z&3C%zhew!9xBGd`kG=;V*+D;qQy| z3vV1)OL5_k7x<4%`yQRBNUOaejh_UU1LY)hSl{qa-`Y0E;Fo~>DdS6>3tCn1U+o8@ zT59&Tkdr2@Zfzb`NC5u;S&;b0trweP@@KOAEci)r@T0_@G}Pen#r~tF_@~4#tJ&z+ zU>NkNWad-IGG zgX3S?f?wK)_I>@P^*@E$uD9U0bYB)7A52?^)n$puj%E&ajj?T9;~;~AJK}sM{{Vt~ zTKE_CS=4?8_@m;z-nrtPXX2)t2@ za%{BkjGE@HE~lv5`Suz%fuu=!a?yeJ+$RyKJvVjjS1x~PAA&J_Ch%9s%Rd#h=Y+g} z;u{?oLe(u}&Mxkls;r=pV<^Cf9YtD{kY7VD!XE~HJnCPxx5Dp>e;7V3S@?%c@MVsq zx}KL|YG6yfMq8}gU2VtA!U7Qcu5rzB{{RXrek<{Au??+`k^4n>sqeKdMqrWK!j4NbGaLcON@`Q( z9=n`3#E*qv7CsXG!Z&}hmW|(29KJMbwz1vmK)gC$k#(sh{?LC3G>?Mo`%z2c=-EOe$4*>8GaUi&))_-9s3k&T4MNz;P;BBySKO3uDq*#M%L!toS0Yu4LNXP*smEiAireDv!Rh>0 zKZ0QRzv0O>yGa&1ogH;p{?QY&ZB;3pi*a5)P9INR{qG%&K8WJ5`$}e^<)U~CGQnEf6_~mc^00nRTqJA4#{>dK} ze`g;HCx*43A8FTmpTrF(T#02}F3R)G9&|gnc2xuw8R@{UVg04PB!14G8~w39BY1;c z{h;-KfxbRI1=8*9d}FFSYo=@Zd_Wbsfn7>ZaRA*DA^CtHfnPzFESpB|4;35=&)Qsc|*R4YW>Pb2DFr7_wCDi;!tLr%XOuG>Bv9C|YsZ2bmU5MR?$pCud zq;!!%Rsdl2qC{zdkDQp#@~>NxF@=&ayKMC@451nop(*#EQ^&ZiB~c&D&C{o~Q1HOr z;marJTGd7>^qYqad-`;*mBdEXTJt+1n@YzE@i7AUqiT*?6*(TY&wvYYM`~?*8R%Y@jar$4&)(78YtaSnJT`sTp&X!twwe_^yw_z5=<_=3CuV7~otm zj~{q@`c}7wJQHuFvAfhrnqJCB%C*x*TpzePdU0HM%xta8PwzE^74;bnyjJln5rC+p z2!L^lav>~9Ui?#l1vfF|b>^J{l0g6*^ai{(o$k(*(4J9HGK3#al+b_;xX-;zS1reG zxu&*x+7BHwLP-AgC?19I6QhJ|y zQ5Xxmq0KiOh2Zl*5{3nFkEKNAbDD7sNx?qU+!OMian#b?fQUC8pwzJ_Rsfz&HZ&^R znTAN<8Vbl{1y>_H8ajdb+5Z3q<+O?q+JC?rxKJdY;glhcIU+foN9kV<*jpgDlJGX; zC*Ah1y}#hU8e9>0_u%J?Kqc=yBdA!*p59f!BlK$cH%Yc-x8AHsB%FFz+GRT7qIo#f zl*z{}yK&&u=nE*pCz^+Bo5JHBxv7vWe>WVRcRX9~`^Kr(E~WNvY1Q6)1g%|SQ@i$_ zwKqj6iBYsxXw53AU9~q=qo@&E?3tLUL_WXs{rxXqCpphK&vRe*b-iz1$nrd#A+tB) zu~QF^E73gkSK&lefcC}yOAX-&j(_8dUiKx93IB{GB4`KLwi}lq?&HZmB)f+3!N<;u z=u9KQa${kop9U^}M|{XrnE%#8-iH4jm#1Uc)Ua+8){ih1h_729_b=90n9KRx5xiFx zbP_RTr8BVk0i%jXi!T% z$$!y6;wLr*0RsB}#Bvl=**1%4bL2+3+a|+_%|HISX<5O3Cd%)mtBby(5}V*J^Jn<< zU5@A6oK|Y7A4PJg`eA5=Be(*L)Du=K>d#)AGNN<&aCg{tD20lY&6Qoulzr6dt^dC~ zv2`RZCC7^AMpT$$ZNL@iWMu?|M%o1-(yt8*FJ8}@_uGnqMF#WBVpe8dIsehrjE^fb z{jYAH>h>H`I`CH`$uV&pia3SxUn$C`AinAYkK}Dn43`hhTN8Ve1S@7e_EruiK;BH_@alD z)u zzvG_yQDb!{n5xnp7YKe?bL@4opXzd_B?8IOgnmjQFw-d--eB>GP;hkijp1BZEpYlW z|JEuNX*!DvZf?mVRZa+#I(yNo-YV$G%NBQP=K|>J!d|(<8^Z$;9}G$_g@hkR=`RA% z2c0vCq&c?3-k1kB!mCfU<+pa4xQUoY*RQ@BwCs~-PMt$8gT)FHrOtT2>#{UD-Q}Um zQGB){&#B}6o&V#qe)vx9i!=*1WV6Gy$cwngg8eZubCnhgZ*Y7@LX|sg)IK?aZd%I( zhblkQ#R$)Lxqp-`s7xRVX-S#(r$4ATMZQ&>woDu`B;(kvqz^#MCYn&>Duq^if71_n zi=?k{toXH|r7sb8@XY(sQX5s?=O&0u;ADAFK2>Txjlz)Zp4fEhpNk4t3k(rk>i#z` z+;`C*2C}^LX}eQZHCg*t0!;TOl@5CCAH;(<^fc|u?hTiVYaIBIlsjDudi^FCYc(7+ zgz@+l{v-fbn+gvtR+?aWL#;vez;ll$@_*lNLVHQ*QzwfBfu!3x%nZ>dc_*!dqs&!cmFmP z(jLAx_-6F6vVI657u$K7sU3Z&$R-KEm|fz$62~}VUt-i|G*IU8-kkk6F;Gct7Q&~} zJ@9Tok@P;4W~5a|RfEZX@h$|Wj(c{SbqP;gYjQmX0bk}#W@SP&-_ZWLjl4h!df{oQ zM@&=BL;Ne^PbL|nwV9C1C7ovZ%p*s$?27BAjxRv>Utzd=j0DYo z3Kw}+x_b?Csrsa_;NR=}h&bNir1J>e2T|i*RU<0Leb;X-)&C>tk<+|{6U@Kql-bzD z(L|r?O*L)DBl@qrASaS(j^blJjm+W-+voY$FoxlvuNcmOe5JBGx<#7(@`;5syM$%A zi+?mC0{XffFvjc9r9%J$)To|!B!*ST$)S>yabh7TT`XZoNU4W+VBik_<3YARVg_~G zw@dtKv@$-&MzvAuu7SLp$pB*6F_iVHcLqm+mBIfGryjUK&G$ly4lL6UHUDJtt@FYI z!cOlF#+Uku0Up_Mcbo*qAB{zVQb1lylK@wn%mot^gMQw}z)(!*7z}p{?%)2Gq|PG= zTYH`MW*DxDKJ1HW8Saf)641aPv1FmtFVJS6fJ1fCP0ZU4?{ZAt?dztvHzg^~5~7d( zq`KyeAWLdv-mX5lZN@*wkAj%rOd|43>c6AsQ;(%|IZ5_@ikY@d8>r#6A*MWC`(jDF zvh0mka=l(Gg|yLsx*59I++h?kuKy8Wo=qO*RajuiCzL?yGk9KiKuKwtX|{0GI;spZ z#Kv(GP!KJL&(}sT6+eX9VcugUL&27@TCJQ-Lxrt0-Xql8{`&Zb7X`~TA`Eq&IMEwp=JkueCB*kf0dhBY?1c90mVJ+tM%Nk>k2x1S) z9)z>(N_M}qYlRQRybqsG!pq~wIVb@+%IU|Cd?8~UG|{q+t$_B&0J0%X%21{u{*YNn zavCCfHijNg1MF4cZV!4C^^p}k(l)}E#yvRP<~FI6P51LRh7q&Hj=#<{T|0__gMkd; zoDiqmbjiIgZrtN;6wUm91itLN##0^4RcSGM)s`F^li70;jtkhXx{*PGGejGV+irZ&uCO<5KApiaX+v0x;JZKRgY0AL|AvLo}$b zoCu=6t9cF)Uj$4CO+T0Z<#$8>=RHGi0{G+GZ8ZuXW^7-}&utd{-6;^GA`uGP3u}V+ zgIF;k8ZCc(I%JS37dFD)%8xh;yhHr+n!WrCVLQWcDU@A5I7MD$crvA%u6sSF`@uze z;0sh`D#(L9!k`VUkxBJ>*2${E_b6rSKZ0B}z2uAl&*L@3j5ZpYisz1eh9-&ks`GjL zr%T>Ff2fg-mtg-;AS|61%Owg*3}uY(cK%;UY;~Ifa*Mf#@i_y2*;V;@kURV#2G+;Y zuDLx{;8~tO`WO0GPWB~efDf@enOch2LVcaUm1aZn{4lI( z{Px#4iw7qst*%>HTGbnyC*p_2C3&_U##1)bHWsZiAF0=s1V+0(eUp_bFq25TSUVE@ z-BsDYm zHF6{|LH9~e?uE*7Si-F#SoAw@LMt7t#Wx*}r@#P0-;j;M#!mBQtxR7GgYZL}8Jv=n zkI!JvYC#|X!y+*%kLw-MCw5b*`$U}xa{7;e7(dC`WY&z;kDd^PlDM^ksDk1)WA5y0 znd^4XIy472c3L#3X{E{a>=G?=GTr8NJ5ncb04>gVJB;I|a?QqG-f$Q1^bRIH0vYhJ z3;#DYtinW`Dl&VUz_@~}ZhoEf*c~l!wRqc5j=JuHCE028`14$p{c$M?hXs9 z2d%kBEm?H0f6)2;AlRi`1XMsdY+!32@b2Or2FT#9rwq6|=zN^kj3Oukg#|b30lQfv zeCO46s}-z?@LkWVIPV4Q+v~dbWzF}Er=~C)HJ)}&FKlHo=x?C_IZkW#1hif_zWMK4 zQ$%YjV(0pJP&Ly8w?Vqxl}x_}LxCsb@XQz@te!_SB`UY3ddzzhw#eFBECvi6^(}U3 zn?3O%De@ak8GF#<6ii?)321OQbJWhvFo4JeuI4}<1X%M-{-_V8d7;5ERAoaJNZEf7 zu~@mC&Vnn)dcZB0fy#UTG7NHfMTC;-(n9{W8tJvGB9g7HLXP=Vq%=W2X}czKx?b-I zY}qZ5Y+ffSGvSwt616y5T-ZQG4^qJLD|~r=Of4jn=l{xUqDo#?~7pyw1kr+ z^8W2xKZzJYBEW(K5%?XpyTI#b7qv-RTaQb$~Oug($Ab z&mRj3<1te0G_$rS$IeNGgHHOM*JOqDTjk-ulr@YsG8Ns zEv+zP&v(e4(5b5b2x1d%JwDKkEPH{{Uv@F@K=n`nOEA?{f0ql9a8dh^nY*|s-3ayd z7`2`M^*@3!0upv&Ph*Ef1c=IW8x0zAwGI~VP~A`fJs4#u6{mN@n5hLqbv}8$fm2&F zy{lLG!TLT3SU5DEQy8Cdo6R*r*umfKqA+}I^@04iwClz^n?(eeGw02Qo7uN-Yd>FZ zGZoxa4p?bD5#2Z2G)(jTVDEY}$G)51p(me_*JZ)0;CQ4YAn{rti6}eYYfZ#QJsjOP zZ8o(V#In%Nm~y#ip~q4A%s%vAji)N6Ai_1cx{gqgy4PORc(u)D_-^4X|I>hIcR8YB zkIJ`SWM7qjZD>0do%9a#Xf>NAhN$L9wrg_>{4*CrrQ&sKrB5cbnaZfhiT4N(?gf#C zaCF}6bXhiooK5@dmzV=Jw!Fm7i7v|d$v2Whky4LK??Wew9}}uPPn&a=&Lr>C64VB! zuNqs>%*+?_`;}A#UP$)Ep2bKQ{}w1s47|=mGjD#*AxAAoODRu>vjrkP9*|$kUVq+C zdwZwDG0k5OVgBKUZT5G`GotB|03yfYPJR{ZpYi3@Uy7~f();gXjp{a4*}`37ImB7n zhdqj??>U`YtUpXmd`?d?UkmoYYNb!!g*>y51WYm{gcg#3R>)by^WsDM-0KX&3G$%@ z+~N7zG;ND#v3Tdb=p10riEJ`$yZxm65c9=Rv=Ni;?_{G9{Hd0L09?9gSMWm6{~N!S z0kP+ixLGl?i1Fw!&MkI`e&XnSCf@=^X|WYAX7rmby_2>11w9fV&czVf&W6Z7sehbk zAU1MQEh8L|+$u%qocv2ucG5Du@b=H>g_MS`gVQ+yCjgS(sKyd7yO5ATJf3*r`n&CP zGJid#bjnOfGf*X^WzA%I$#>x2Q!8}xOQw~JlDyW5q6G)h7xkFA-_M3MaJypUDkUMx zcgXH`k=W1Feth@c*RHVrd|ij_R=LSilGHz5jtVQZKkSJZhQM5=?>>CZ9Xaw-R~unZ zU|1(yEep2xvL!rv_AKj(VwTv+y{D+u;In{m#E=y+dO zc46Rwd~n&_hd&C>dx!XJ!D6lSN(6%@A&(Uxi6-{~LV4aO32ahxlt72&XB{y?o|*p< zWLkEuA>K2Q22S|f(c*M$U^LB_7jq}ccny#9Ku|nLqMHOQLHj>lR{r;ncrn(W+e9} z)^cA`1-Ln@B#sa%)xWfEST6*`gOhgIf9mc8LQVd&CVspH0cFsi+FUaqAM@cq@kB|a&6?+lsig}P0zk3|11ElpH^_MYiqfZ;U&Y!!vC)zR4flz3 zb1HxSj;)c~a*da;3k24i_!oZ^Xy`iNDP^%hL)wGrtEDhjoWRC7?Y&ox!qI*N!yedq zJXxFj{qCd5Z{w`nCyFlk(8KtQXGo5r4(o0F=58?Ygx58Ne~K)7xI@ zqHbd-`nuesmnB0qDg~ZhuovFa#>3BKPxaU5S!$x}t%rhz?su^||3mZF>Ce-V)k7q; zPNt_N&sq7#h(`8T%T3shd#)>UcJGB=OJ1a%EJ38m%fD3zmI>vD_zO|UG zz!=Tg$9HKZRyESr@;4td_>h4ly}m5@^5>E+uX2J@E)a>(+NqFxk~O8W)+MkJMPegvadmVL5@SbDC_ zdL1mE9j`y=cs8QVs>Mz4fCo6lHb=^%P6vD!Wu625)F&S-F)*ZSLX&P{%6A*y_DpN) zr~;_cU<6eyRxFb851#Y@APW;0t$W5`R4xU4kFet`F2Z$Rh<=aC(cU2<2`{#-PF zBx@HsuaEO?q!qM(AE|Xxjs+)+)Y?rn7ppSg(SoeQGl3^M3XK+?Unfb6@q#Bti=thJ ztz2h~`$Ej|cTw9-i^&!dqrnBSJsYW?A22Q<>ZJDDWb5K<-&X7Mf3`hbxO!p!;gf@$ z!60+aPnuqmDadA7bLi>k)RUnTxeb_qs+$vK!#!CCQdwC=7Bm>cdzvB`X@#}GPyI(g zd9lVITvNKYMh|Lwa-!_Wg;*G!9p5Z@22#L+HYxubUnyB8T>0akdK&52eh zqjLS{P>pE>I#)>ikCdJGD@)^yFKq;F-%B5%OR8Qb(aH_m-yz@9MW9 z{oDhT_MQ$$x6J4~g^(dLmH=PlFZE|G?-zUI?1pL>Q&;ki8BuC3*-pBO2gQy-i1v4* z!>{d>EP>axa$UEpHjWofautb5hVu^C2pBeJ58f2l)aog6@F$t2_&#n=U#O({mqZnFG)sreB|jpp zikCuMgFOA}n_e=bX#9)Tz?P7c3Cz-9u-~8GoWC+;A6-4*WIAlhpe}{y043krCae#7 z7ZE+V%7;ma_^OM_zMB1mXG4T0sp(CWmyR@8tF=-Fw$;CNTy|)KE%a|daeUNJP!o92 zCSv&KNljTO=qk1wFlJi|eSG@Z4pG{zqVgam7+$4#B85C>UMntQ7{-*CMQ05D^rk=%@(FH{meb53C7@{~?s&e@+K0wQ<7_aUNa#Dx)};1z zI4y>IGq>h*sAdl$8Rl$dFxg>WFo`T&Lz^pa7Rz6Z?qhqBf$>!rKrF-EJ!DhsVypFq%=PQ?M+E0Q zT*Bb$9wOzxXRQP5nw!ae-V#?r^|wtO&&w?iZ)Aq<8-W-@p~rEhL9O3|lD5l+NIub3 z4a9_ZYF^P`W`Gy;2Ta>$ciK6V1ICdzZC)2+SuyWCc-(QfcJQ2|mbH8N%Gdj_C1!U4NT}G%b@N%34iW?PpK8(q3y2o*`9FQ!k%w|MW9^Hu|rP zWcCd_se8T9u90y#J!U;7NtxH(uGy!(3==Rm^z(KiZ+%=`O8VMRwaR*jnIbsL#9?*Z zb`-gmvc?cCh#y~T%q>S{Z$9ZxmWp|g7ietwds$B_ll#6*FVLSY;s>P*lputQ{0T!* z#?O-|5~ZP1+T2_TmbSY&D(&y~LJ!oPpJcSQcv|VIKNywxHQLDU6qxH3%yZ`1-*vYi ztBaq6wcZ;(8pNtZhQT(?lR9ir`Pe~Qj_$4*WJ__s!Hm9-y?sZWv0`AxFaN>7Rn~^v zx^54Vcx4*E4L}^}mpYH9r6yaP3_&eFj)EP}-Mq{4XX`XAW?EI0aVZ_riUcl71Pwz{ z6=B8jzd6Kizg}{Z3RkSs0pM;hOMyFcv*-uw%LYs@P?EDzNk)M;U{ z>5y1(*DhburBl^;B|xG@42shil_&qliYLbc)o>ho_=p|}=ZKWU^xr107lC@s0z(gb z?%UVhe5kJJLI27Qg4(|s!HYaIpIU2#_OC%W$)Z{JZ8>gByA;7_5|j8jM)~rU`2NR3 zD)wdY)xL~2<(#4!XT%>_>ZO7K`P1Ih#ie08){Z9?OS&~~(e%aWCvC}(1|Ag|LXFyZ zq2arVifgF6(C7WD&x#lC=0j3ktL~Xh=FTjYr4e5I8@VyhieBx$1C;r-psOg%CE5;r zTSHv45&CA_#VRIg-cm9^^EOd(F!5GQe)kEh3}19SFL zE@R^5c~g9(tgL*+bv=oEDaE?voj-CY-HZ6hT-DQ8_RzJ$!-oRIuX)@$| z+dp5l>-T)1W@NF*v`$qbt$Inn8AI~FEM7ji-;Qi4RAw-yi8H;!>Wlk%|C8LDZ#K}z zs`P}%N2AENp$OyoP1j*q@+t*p7iB(#f7ING2B<5rcuNq+%P!&>{u?68&z<%2HZxWNR9c|GZ$`vSSVrFmgw}mj@(+%|MR7*gCQN2w*R4R z4VLM!SQOpb(@hnri7DUs^d<^ytC#ALf&5Pin)q8)Vf7Lr{AQO+Y+2*@7dQ}dy z77q9A6b#Sx8mon9CL(oKlm=6n5g}XI`_|tul>RePvpBsxF@*5Ra~bZ{Htu;reKL52 zNX!pSB^w^84+LN@e}muE@jd5jsh`e>HNchZ{A6@Q5YLWL#DEjcQms0F8a?*`f3IDV4P0L88m8@*v=k*vpW<7p)WE@&BPU0}Z#v`qs+)pWr zzbobQ@aKKAn}7gETMLfLx!QMqY>pg#UC$pHNeSp^U2wHIzCIixWUhR8wqAJS|8k4X zk(>EY->^7rNRB>%Eqjk?SXGZ-0XkRA2nm}E*I-(|xiM*)@)`NHKBM?umcS|hQqyK2=@Gw)Wo@ln}*O8uI>TK0lK zm_)y6K||U4lKwG-8k8ehN0dRy0am)Nd)FgC1?Qf*%VZ1%dt^PF{1sbxGm|w-oewPh z77orlyW7||p5M)VveAJ$ZIO`R&hkyR7-S_1{%iD;<*7qv8sUxk;Tn$KvmZGCL+Qn^ z+xjRpi9Fat#1}^W*%_tbodQe=wb39wQAWetsX-!9j?FIEWAEwc$~T?P1j4KsVtD+ccOj(Q`as!wc$^)P`3mC2 zH-u@akv`U>HVd@vuTt z1k}S?I{bpY%I;H3yx4wfcwEPKqX>wn){*YAnnji2e6yhQ!U?bJI5*$&_z3>P6q!xg zb;YBGlOYQKd5&hY(DM8sCqI+y%Np_c;b3C1(d#Nf_KLRGwCmMF{)12JSeB>iHIhV6 zIvj!c;|Ub=)Y?cJ%c1*K>l-`_d23KKENg0!1DKV_SNa7Ow8>lM_~nSZu)qP)a{+Mh zI||9mBIWXiSPDBXK%&f?IiauC+LQkkVL0DiQ;;^O4AWP-yUeg-a7ylo=p4a}6lq7L zfaZGdN};dEBv9)hcxG3tlG8hH!M-}M>=A#TX0u8UcCqqnqD^V^hjJ#!2fCc3V`jk~ z*JeH7|BPmi41uT1TARblOo=#}+bZ23OA7W-UJXl$=OovJ)40!(1Z2lSN>~#gzSpB+T!%Cdg8ghtDBQ zsvv~vOl=$bddMBl|8@nk+3*|@E9JSIV(t*=Fb z(f3oEQuru~*;`(>ANdjXWfn?Mc^2IkD-(w<4gNND>t3~vJ3A>F@N^U0bg<5bpTObO66>D4Fp3Lt+IU&KwPk`m3uL)BWfzI1Zgh7wZ6zt-P>AzzHDy%1!vF|rt z#{|A<1UcXS2F3vz2~0=&SM_zH%wygnJmYs}t~}6ee`KyL^$d77hGOI|;glH8zG#xa ziiY1XcIv6q$RXuNQN9D>!`F4EHl~#6r_BHkrg=9O#~Ffl)Wa?(*=WX+{!;(CiH_u8rVQ5IW|{56LyZ zFlE<^03~z@M+#%scsObBRG4#pv`F&hednw-vl-X5hcvvu+b)^6Orn)hp!jHkP)lSF zjJ89_{o7uvgut?U-k;M{qsncb5FkuQ5z5^IqKK5Djt6GeDdgl1K-c5|(vrlHyrW?uO79TcS1v z?#tJFEQ8%*$d#{eN-K=z1W&UcqAb`xZ)JgB`kB@=wUR^b1jQCfK&}6-u}3mu)X|RS z&1jMV_?<tp;?~zPU`GIY3da?*K+U#;&??l4|0aS zldl|tA^^+)N@%CZxjHlp&-uVJJJ+JQ{iEP+!LeBBe+1@vnGH(X6*0a1NAqCdb~xZ3 zC9-kKPn!HtP+cFbwg%zHt?yR?p`{0a;29x0 z%%LPm^SB+1CopAx>fnYaKe6^AKWbR4ym_nXJG*L})QrjA9L z(scm0%8ku${wY1*;gdyq`gQ*!fzQV$0b#|aXd*oMBdl0x!4LwJ*zF2SLHI%ybvJAE zf4kqfWLSb5`*||MCdG(oKBRxlJa~2s3j&ki}_eJZtZjFYSH)O z4O+E}6-J@_^|7JHHpxJ*d9fcr4)U@Ipt9FnlOsVwoda3@L6!Rl4b?H7$p;eqmbUU3 zSJe3?6->GS2`7E!;kW!;(WrJ=wN>!XKOmQ}rW+fb^Uc}SUxAYYP=~Iv(Os(`1gJk# zKGbp37f1JA`>&#u;Cgk(_oQDI3`Pof_O$9+d!p(82HBD0V)WQj$_lP-VFo#b1ZAQH z4uFk`Z9IwqMy|~2-bIH>j&15De}!;YzBlhJ1*LSqs{iM|XtN|BX!MLDZXsd^n$-Qj z4A-XbL+L!qTE;ZRF#pZk*G~dsW-7SD>{R0hJ^ANfxP@sBtq}V{K`5iOq!_BiG}_RZ zb-QF?ocHF_jNRX}L0k?E?|sD1zB#CXxghehF|C)s?%u*qeH~Z4)|w`u(iND0W^^gA zT_x#Tl9tmH!EuP-kkQ_J_k|`s9{x% z*wbwG^tGeynqk+N&o8Q7=HRu%JQNITc@*amdA9lY%Q&DCiMIj3W{kM#eon zV<>Rco19Q(;@dAl5q#ML#YBvm?&iT~AI=~BP=iwbz#firalD<*(Hh-*+oafvC!=(uvX#exJ0K6O)9Q&M>>a7y@8=I3w=i`IRWf z8_oY8fkA!itg_HrXVG!`erx{fhC@g3LNXCI&tph&(P14%9oPxWZ!_^62XnT9vp7Eg zYT&7dheUW*b(KoKe%aVjlan6nP}*{i=#1EeAGM+2NX9-GMI`%dfx(|9_&rS0{43Oq zoB3ms=6uTZ*!2x2F%l-_OJR5G+J}m#8|Fu@DBr6fU@0WH12In_WhqR-K(WAVhrp)p zXp@rd`LP@7wm?F7GbXrDL?UcRaxWzCG%7)ArtT|-xdRF2MBC53gJtuLkXL$7UA#{VKci2c2V`m zxdAV~Yn;dkYVz`HAKJ3l0Jj#{X`PD92co$;P`mGNufOa^^ZiA3&2~t+*1*1JG?H0T zi*z_`1!gB09SGaJ$nv@ZEe#JRZ1Uo09Fe?(<4^y3f1dARezn%i6d4Quy7)$^*BVdU zIgEU1IX>UhK5pKB{aAOYo5*s^;Yz$l{`cG;aiqJf@?YwKwYSA&m?ErnXx^1$*UaO3 z)Y>~7GjH=iugpp$C&g;#`mA$}BAf7;)@P|(McmxSlQ4c3VopSo7M*2;fetMgIccfRUa66XRX@PTOWxkS~X|we6@k)%U(%6DKCz z;cB8~#Qm=ur+>mB-6$|D4%P(g^L-1u-^J&_(h<^4dg1nF<)O{AaGDIQN)`Q=0MOga zAC8&XHYWZLD%rI^_=K$ufMVAg8T&XBIGc9+yA#(KF4yRAYBM00p?WkO0{?w}M%DRY zqdFGn81^qgI51wG{K5{QftVivp){bZqG_Zqx1YW}A!Pi*W9AJT=bt=6V(;K+ZwrCo zl|IIBL!rprY+9L5muud6ZpXaM%JrSb+@=8`zu1q56MErbNHjIZcnH*}-;Nbl=b5vl zYF>9ibp-TH+yyf;RSg-Gppz~Rgs)z4Gp$P3)~C; zO-{Qxr?qhBi{BG{Iivfa9@rg45!ChOcJ~lFF@6`T!b&| zJg}k=IMz8GX?)i%!!94?**U5q<3eGg&fb*$RjBOA)=GMfZQ^&mb8 z>(oQ>KMrrqbF(7yM;9Eqcq_;%?{VPY8pmh%&_zv&cmCVV56=KNTlTD;btcfEj2?$P zr}QO$|5>9PF4uFqoya;0C-}bEku-uqb_xp12xpr$TFOzFY{en?~COJ`y zD_RPRMuXKk1c3^YFVx10ItDdkp*70pV<7{*CGLi;JPmH27JSbp-V=*#>)Gjoa)pm` zL@UWKH5x)cewq-!C*!y0CQB|c_iki+*jLpKT)zeFYoQ>@oHa(OGaHo9D)_&o+S&@- zFTU7jMHv)cjedD8<6s}ytE9ANF``gEpTSr4_B_2a!pQdw1_7o=8Se=vWxU?{`m+!M zmYjR0YM^j4b2l-*P9v1<`m+Y177_Cvzm9{-QQaS!tf+%c$9uZLNcvBnVlp7nlYl_; zIrUc(BO{YKe}pJqj%J>iWfIDAx#kmzaDxoBoD{+)ZPm$t`Q3?)a-(wgS9zj8(b@Qt z+|tyq%@8mXU<@wu7ELBUEr`VJ@5>-_=RkIVz8%*!KZ1_~yXOv`M-S4RxxN#e4@M@VZ$KhQ znRoN^MuGogJrgAGbW!?+5B0xX$lg5r++jTZzpN2CwzdB?tUuqYvSxp7H~WxwzR&a; zoKUBVN=g?F-@7V{7A+5d71&gMCg-oft>fyF!OKO})htT*WkM{qD%y3qpWdy3bcovW z(j;3>L)7IHO;XqdFo^KfsIqIduk@`?M*IrN-PyekBotjiYM~}z+?pIf5kRfuD>rmH zlyfJ$xC6yMH=t0VKsXd7SM{DPdy2-m4I#@-T)?J^QiEu6T~*wtJmauv5r`b{^EX1c zqn9$i+-uMOK=4UHPgx>~`2bu?IAxSM)F*TIM0Pj-mKWGAfm&c1O;hKx_k0z$uX38BzlXslNjS=J6aSdfg@T=bwP~ z+ndb2ZS-}45{saKk101hNx8yrV%6-WREU&ALJUWt$v)LN@U$J;aji`1)syk~bsqdJ zhVdK^iOr`*6~*uV&TNQjPhFXLbKYd{s5~p@=Jq9OL^54k;+ee+&7@SLtNnE3FsE3r zYz>3FPSt7&;WDz;jmJR}(WyZAs?}l0s+S%3;L~!Eb3bk7bt67@iB!2G50x;)JBy2} zsku>xIIe~fb1ic@1czPMP^4r(-`eB6Dj%a$aPhmpEh5lwO zgoCli8S*;ZUbQ5|kfo@sDlO+R3)qk@m zoR-~rV;CrMj6{I_EkX3zs+{IZy2*|<;qD1UFT(t&7x=TCTObGGM198!qpOAimte*Q zUJ@M^i=Z{K*^$eW(2vjAO1>HH$`WPIW_dg@3wko9dy4OdKX1upBulLLihSsOO#h`d zWZ~>IaSdgCy0E*c(e!4LK9p!snYMmpsM%kV(XX!)Ve&3lg*wuF?n)+~^IfD;W$12i z@#BU$9O!G`)9K4v@pWC_fVV-E@|*@?^qZVb^SFnuS4y{EFSHk<+4t9|BOM&Lb_o}(0i*>RG3iXK=>*E+MGWL8_X80{d`mmwgckt0=liww;cdGo4rW`T;xB|Yh4 zq^(+QF1x_f+muzOo-wnu3C+>C`RXmRpbc?3qc708YO)Vc%!HcKuv8aVElp!cC%>GL z%g?iom)SRFIYnQ-@l&s!#;qnzKXl6Xde5TgDby5bWvw=~y!5g?%WdIx=7-C>969I7 z1Jp?%I?#Ht`Sn^8^-<90)^}aZb})%e^S)PKgs+HTn2#3ptl!(lxJHR)&8MeS92Ej& z+4375!-Qdeym9rtlx0rh#iZOnlygSk^$q>(nzm9NwtdK`DB)f+c=A)~Hb|B_7RJ~i zgjK=G+NBp0&U?cz#`aV#yq#9PErK+e?pd{v*0uJ_$9qcKdBv+lJA9L;=dOnrM04TC z)>r|i=KJflX4sLcQteKrf50_|G)7x=$-4?M`uo(Uk@R}hUb%h=B-+bQiX; zE%P45!|d-j*Yx+-RJs!K>y-xF7EFf-Mil>Qmx;5nT$$!ioxRfjGAfMZRoe4oS{Ei{ zmoSzcX2(+N#H;h{%JzI~c|7hR?oP->RJ?F(YDcB)(}^~tRByYVnq-PlIlVlzdYaGY z@vcY~DSP#`VPIU8h#P8~6LO%gGTK-Z5V845ymQ1)llxL~)a2&|6VYm!AK}o+ zTFz71^}>i$0xg3h@-J}J)w*FOXH!K2QI8JNyDGbd=P;el%1tLZC6Z{l63e&2Vct~= zPVwQU_TEDps6ME3P?^tH)`ETRfLS5!LrOJrGD0+Fjq*#?{{4mbIGXcfJ{jC& z$8zKZ2a7)?J)ZD*JC+bzNt9;hGUSXQ>`|TR^7vUpObI4|OF0_+>i&0V`n=|06EwWN z?CJD!rVu2q%WzFuWjsaeEE$(w#KFfRUB9g(R}A^C+$CS*eW$uDAr zHgdXLmUrj!;F+^vN1fQQdNQ+JKn<7rNnp=7zXnNouKF=V?4`ZQHhABZ{k>dwDnF}A zYdZAK5$6E;W&?AV88BhN?p+h9qF%*)KQ0Ov#r)4ndsRzB}u_fygcY8{+syv^=M5-KPAn_5+e5t zx^2Wa|M9?0|7L+2uVnrFck6*`-8%Ww>`a`FhR7`vlKgP{qL0IhZ1aU)`l&J-vDcD~ zRRYbTRD7inQEgn+2v^Z!MbXFd?^J{cJz;9wi}DYYcR(yEp$KX_%Ql72+^@;(qiu@@ zsiNC1$M42XHUoW+r=`}_mc5x-Dy98lcZ-Q-uPd%A8AO=%6?P?$(k@>9F5=15ris<$ z@;Ts3adL_J?MLzy{^4G*|75dfqo?BxBp(~|rM`^!1iypo z*5wI*wl<9r6Njwgtw+<8+~2GXVpGRQ1=E$vsqS@J)c;607rO(c_KGR%w+ji#WOpH;(-|J!tNo~S5LbLY8 z!KJeB-ZcbCzhS7%RVk)&gK4UI8vp2^-djmUM)>eX6kamo?9l)XEMVqbDg5@4{~;vY z*u=})|Dw9?gEz@tj)++u(I1mj^K+GfgX}cR1hPD{pBcFCq5LxqG!wl@b``O+@76jA z#RK){)xsf01KxsRmy+DnQffimnfrRr4k(b7hWqM2Tr+>}GI4z1;dx*4qaq;maF8}P zOPpGE&y9Q#n1Fsdmzg20>SfP4R2K65&8&!m;6RqRhR4p7Gb>C1sI7J4|B`2BQIA2? z^(pv^wsw^?b(-sQY%;!W``t$Qg-`LQ6+ryc47t+>Rhb4^zZkbFa_Vn>0stRBOHSFK zwlGWXj2C6Z-2Mk)Rb}@2+e-Xe18JRvgGB95G|l{;2eDE9kw50wWKwE3(yV8ywq5b> zEnV;tl$|6f{=n!tZ1`HRTSD1?x=a1Gw3m}bQ3V{)vzE^XuXA`QeG<6zxJ&2u%Ln*S z?Hkcvi5D@lj#9lJ57oo*X?r~@Ceg_>0O>9*JPw$uMQG<6N@bSU!$R5r#Vr`Hu=NUT zvS1xI-d6Y~b+P4D%io8|?PK4O)z{`CPtMVS_Z&*KNO#`x`O)CFc+`S3*f9H|fP0uI|v* zvmRkwW<<(!by1-eyoD^cT*&)@w>Z9#;^C(e@4%WE6#bHp8@a4T(G#!fuOy05tJTgc zo+(w!JlKZ9=~DlHInr8huD?jm2poL=LrIe7@6)qd=MRxfTp>M5D9_8WtAOb|@NJ$l z1uhj2?PA*G${UnYt{$p2&M zt;3r9-}nCsNJ~k#fFda=j7F3cMkC$QjdUZRbb~a~AWRyhb08(%IXX9T)L!5F{rUa= z-*Iopw)=Ts*L9xf<0|nqF!}c6V*&FQX1%{nNe6>(xel^)Nk1mVLqsV$Y%D0eh0H9M zN*e2;nX+A&uc|~f&Mz6nW&{SmrAcYTr?vrvgj=nTov48iZ0&dduwvzpLPe^H@1VU|@M5fns-}l60IV#em>8S;58lGa)G^Z)gdBK3KX$(BEk_-v4vu zsCD>7VPcE6><@-`(1WU*$y)?tKC<-aVw!!S>k-V2SGQ& z*+%0N7O6AB5Y0XT+@hw)r_t2vY#QN7W9P8)lFydlqqt8|)t!(Pna{A}P#$nmzLknOJ~{B+1$TERn&BH^zwIzP7P0 zs?#f)vj9JrrtIo>13B5gJZ!@P%UFZQ9$jpA^K)9K&q4>qa)9jJ`6ECO$;&5|wmzdQ zI~fMbUjTC2HQn$?w!ePF$S|d!chq~TX0kP_Aoxi{d~N_!IuAvZ-X>~?hv&f_9EO8kCgN$mk5BGde7Zgb%Hw{Spx77jBaU?1 z?GaG*pnHFA1|<9B$NTf73=RkWI1sao8>~V^c)rX@<=3KWx;D@_03yGKPKxs|n7qqu zSrt=1SqUdBK=xH`iA2#Zr3mR`snCRL>?dLwH&d0r8?0vTU}z3dU$5f^$RdKTpVxLF zHIM%wBP*A3OWHVeqX$?%nXx}^RI^V;1U~oRBZvy>_?EA^@WZ@3_R(wU#1fI+^JJ?FRTsQ zxMq;3{YB=dLQ(S*!E{|#rlbH0Zdl}G@t(+T#18(N)wkUjalQ&_L|QiyXCex3HIHb- zW%`~cjeDHkr&`9BmH*_}Yic3uaYZKfq@{$lTndkU>r)`56D%W$sfx&j*ROgyj9#oS zCR|sdgn_s{=iXo!ZqTyJ7jt|jGE}f;jP+cStCDV{>~Z(TokiO)jz;M1d{oe6eqDaT z-1{upTA2F59Kv}gnC?=F4=F-J#k$}osu=<=^z zO3e&oq`p=fcX==*z;jL{Hl1RbTe`d1y8+9|j_`Mc=RnvwN`M}FUjUw^7 zg`rmwI+)zpoO<|pLlWmdtJ3MBDF&ZmQL^;(h5~uHs`N=m{1n-95Py|W<5Q=?@ZR?NZ6qMsnm3m<>Nt51MqIy&ZMUSyZ<)+Vn-o#; zDDJ+8m4ZtY&t=w8_`7Ny@Q#gK3c7bw;S5+;9&}kAEPNlPoWdKTxBuA9p|G z8$jeUMv!_Na`!otuAZqk%x6WFQr^kX&B~n$U;J{6F4C3ggK#!0Lr`k=AFHX(KNQv% zY?utMCHbT}?6PZT_NK{?!^$XBFUY13h2@ooCuaWr#e3S_eI_G%8jPvL%ArZ%@hGTn z(t%5nDasSyQEAqXu-mT#rVESx@E(!?r(%E?j@s3O5 zKYnk3<+%2H9IbWnNN+S4u-L9Q4?-&xjh=b;8b}ogh+1A-eS;(8J7V)xEga3G>x(DS ztWx=J6{x#=Zc4+RgYwI&T zaEe>c1m>%vcZII7STl>f@I?HaN55GIOOfC8idG|}0I%&!rg0`F&%^r}f!xm4G5zL+ z0+3jI?f`FS9s7j1M|e_rj+<&gp5VBhe$xq<0Xz|8SH%sH`sZs>rTHNSx0jDRekEV| zgR>mP!+~JBfWpuzNB3pD=RmDo^RQ+LK#fS2_fwMnvg-6jcex1VsUB;Be?g+ ztJ3^GbY8f}qGOy*I4d}k7qV)@F}p&JpLpfnLQUC&rWU2?zBEaWwLYBug#McR8cz4e zYb>DTUgOn7+@D(XrEb}OpqKC(LKuo*{$YHSF^#c9dg$VR!(z+-e^{)y$(oMR*{q{O zn|T$WNnb;wt4@f+HU9$*6d~ZGDUS)*I5kw%k6e=@UEklxWj{w0ReZ-jY1y00275mN zt#))f`}t25?Tn;7a9|AGC|0`Tm^jRX2?|KE{XUmyB&51!S(LAc5P{AhJn`XqmmuYx ze6QfB7Gce63kGJb5fO~ibQwtj9G3(2i@+RaQK`~uN4b@z@3nOc)D;urYEKpD z?Wx@8iT$*tc?rk_XW|=rUMY^owj=YZ-G7U8hvU1s65G@o-Uh*aXGh|uUhQCH#r{GxVvSm=pF^6ih^AL(pdjIzUH{Dy9O)74yx z1vXx)AnsqI1=t(jXX*`mqU>dU)&q<0Wa+&tQ;g%i`Nl=Tm5O+!ZMb~6G?jI$Z(*+m zydGNUFUzfSNa1?w@BC?j#G%pSe`=?Dz3s|UW5;2-Y&RnCt?^drolTS&>3`~*7dCl%{}JD&DPDNP3Lj|Cj@ACkGF0`k5fJR?AdAEsN~qE$@~nv2QRK3I#5>C{?@dNc zsY9FAtxg|6MQ{yxQTtdy_rHA7SM($kX?1`L+=ck?^RgMt%@m z(qRm$saZuY!PLPz-(&vbm{H(Qhv?<$y`Q%@uzW=1m}|==igsZAWb)p8P}n6V@6G47 zscK;%!nJ9QpAUk@_>}skF!M&K-aWML0P7$~gmjprO(m1ziX=CzVZw zZ@kh@2@5sr>QkFjHRBaqOhhQB=r!Btdmi>;ehD4e?$#l$PuQssuHGCkBj`49j(ZR; zYA8X(<6<>8iJz5NoRk?OjqeTd!mCwEtXM;i0vhP=CYH2E8nvbS*pKn+e{mCz8!g`3cX< zNO@8#N6J`4F&Wps3lXGn-;@|g0RVTCjgxJpDEEeHC3O0Q0;X`E3Qo*`^6Yvm6Kv+v zjrtgyR4ntQ-B|F3a~4jA?49HG9acjk08nA$ISZf zlMd?-e{*cIKBquCp_@w1ygQ8cl##|A`K4|;5Bm0ZE)mry&CZqr7~AJcPK1fip)ex!9@rcY7$AHP1d>RuD!a6ZrUpIjZbqCBr|76}z;h%02n!m-nKrlBztlu2Iq z>ftf6!=`1$N-{n3Xw<6)_aySwE+4Z_>0wIO z-?_i#fv<&ZUUzSVUqOKe&i-uCe~?iC*&o~l5SzAQT<{;9CJV(tRqGi4 z^B%Y?Y4yQAbRdp=6t)y1**~@WZ!BgwD6l+K3%58MP~NZD&|KEoyXS)a#j-C0fsjb9 zPWg0&>Re&7&NBMR@tT!esd17Inl;*ew%SNqq>1Mugu0A|M~fkVs|1?wS%Z- zBpl!v(n8pk0>y!Kv2PF^V=RBnIMk{@<3}{kMsKpIME9;EpzY6l1_;R~tRoRv4 z+cjCxd98h>ThjDlyMQ%%YxI6?pXw)Xj~9t~{IK#C$v(rethWV{1`YCI{_UWzeRUSRT@pIJ zbv>e&gB?Pn+-1em7(kg=ns9wypyIhRH1S~qBMLjcgx!7o_Xh4l{G*><%96@cXg_!- zYe)Bm2lJBuUYYQr$%{&M&=A(G*BVMEXG}YmS@o!ZY%uG_`a|I@LL$ z{U^cAec^*D(K9pcDutaS48y)MaeX6_ z9s(x0ldH-&G3U~|h}HO*p1h!pO|I+%oiuO3ewx54@m@%=(TG%arICDHc*rZMjw92) zjCn>@M8xio9ZWYYL%DbX>A_lwmxcSC%t|L;NOV1&LC!^;9Iv$!1e287Sc3vxWK7e9 zZSmO49X(WRe%Z}G=V{kFU}gG)`Q=w1qN;Cbo$I5#d*!&_%v>kIbi?v?N7U##wde>i zmmW9ObGx5!L zWxs&#Z`W4@^~wT-vK=%HDO+>x^Ij(gu8LW02B}?P318LaCaz?Z75x4klA^X%J3m0@ z-G3mROzAkI>&t#b>teXN7WY4(RbwQms zQ;7l{nTIBQUQt;ICRSqT@U_reIcNG1U-Iy;@COe1%J?TYGzECoLpan&GdIw2V$_@X zyzAhGvOcFE84P{x!Gg)7O{D0L!@odrLPBh>>$)gQolxSH0hr$O6WU*4@{o9H% z8cCTXQ|Tl7a{&(c5kx*R&A!dHGNf<4F=v8i&@TSlQ1%F%f40=~m1N*tedTvCb82({oAqu4lV1zNVf#hQN4xnAG0!DBIV zrmTP*Az&`?2D-L@a*q=kM`iMaa-3@X{=xZ~RHtE$(Br3|XB$uQ3rn3f$5@haf?mhv z(+u~}GZ~aNaO&2wFlDz~K{KT70n-6h=_3qjV35~!*@JTlt;ldq0An$237_ws7?lL^ zt)x8YQo{ZNy$F5e8XuJoy%6Iw(79!F0z?xa*!6#+iS+{+4|~u0v!>u(@^Gn4Hod{L zYw2zT&v+lNtnbt z+*6`iRkB)-go@Li8%-3m0Hg`R(yy(54zSeV zf~HW;kZn)KM)^R+WEpF$yfoq^V$1I9GV)i)5T22)yFm%U4egJ|3Zt^p266db^8ObZ z5}DNCy$b9K8t~go`ceJ*1^iFTBd+hZ134uUU7v9_GuxC^>qN|a?((Ln$reql<-x?l zFW%m2T_--$t=E6^^>X!nP3fT#sOxaPjY;{Cor}#(M5^FIo7*#xRk0=Sw0D@yA!GFX z^$xnROq$!kSe50^k52wk-D?BWoGB&x%5&xCV3^-dmpv0;czM=|>GMWmrjz+d!?3Bb z^25`PT35F7?|rJxHIqjs;k@)AuI_xI&hjTNM{&sh`08 zZ(*H_swp#elcL8A;)T(Q%>3IDAT2EH75`BGRD~`sjyAc@W@SwnF(1)x3{Ys0I8m2p zR6cL|#oWTK7elg^)f?Qx1jP6rW}+Y9sJ?rV06F02R0S4YAZEIrNZ3`_OXKktBvX@Z z{wS%pBOWQW$E5bD@BQdUXp{91_2k|-AQ*VP&4Y6-is@c?zv@?jATs`kL)B7gzT2L` ziv9#`=9Mw0<=_quSs#p37Bs&sc6P##pxYvjgbFd?8!VyrdJV2pmQDJ;*h8Is2?(iI zA*-TiGo&U`f)XRTM8Xc8O4VH(j63vQd?RkWBqmd_%_lBWqqpqWP~eRt0~8-y(WE|F z*OrfwoitKEs!<1;#8lij-1A;!FP=U`nzlK`)=L|=>&=?hMIHAPqer@)IXz&OI6$f# z2#Q}ee+y8Oz-Q~sui~V)lBZ4-@c-N;+W9(w0W}7wGKZ+067*Dly>qK%SET2O($2rr zw9zTs@9fX4J#uanDa7USR%q|@uirjGiy!|8EmzwNRz`w%caGeoQCm-d|Gas&irlO}{Sm*FmedB%-dg(SHqX(_~9qlf7Auy{CZ$ z9^p2h4tjQ4XKLmhHTv7r-~7{q<%yzdkyU7Xr+cjfKwaswj`K5p7QlrH_I0T^loDxZ zu`P`}p!@N}gv}cikx)vc3M6S7nPSnbfYPEUCAGg^>vCKOcK{Yj9PSH1%&`Mp)nnPX#Xm(%7r=5j^_e>UbNZCO>miqNRepJg$Mw zdJ6NJgWv5H)eg@-7i0@dbItDhpF#Kd)~5cfo+w65DdmQuo+P$nRm=;oo*o#M1vE=_ zg{Sa4d|{^(F(OxL&>qKUBu|wmou)M>H1?tTGSJ{JrAr-%m)ZI@1m@y*IK-&7Gvqw0 ztCMb;Dz)Y#c~H^ti&qP?pEhE!!@@IAU{KxKjLq&joxFqd+>HJb41+0+gz)(cum<5n z-&=Qo%?;;q{zDJVoq{RFLF8GsN6CO47Vju8CjSRX4&h9;>Eg!u1_!M!GbK{Ju$EIv z2gjXkJB{hm*FE>eZVv~80hQgeqfC^KMH5Wiko>(U;j`x~u5GZ|zs55y zUDq%I&KOPlIKgRYv6r>a`^_0YzI~c!oB*3W4^@zij2;%49tULl0k2Z+S4 z&M0>IDPjiKBuJbZA!MN{h>33{^O`%nj*M##`t7Zx6Q*(|diN%1SlqqbVU_~SC5-X4 z>C*rwg!h>Q&sLQezd{w~#Wmej%S*bST{`1LvMeQ!-@5D#O_!AWKB4rG?0t1U5V+pg z+>Ajq-i-XPb2L5Oi0SPS&EdN}P?!MjuY-xSb4XC^LDf47yL)@_FUG(DGa$72!VZ_+ zVlQ;uA%3!MzZ2x04xU!SMJrn|IBh z*z)l`etVr|P3gGM8MOFY;Cftz;5e-6C!(|8ztwp|Y@R zLw^h4CESVtm3hOAn6X9(#ZsQ zM#M;@Y1`zb8K^kzv8P`i7@8m5Q$7s4xr6CkD14GXEA1h6F%q)P95HWo z0q(_B7v&*`a7RMW_`z1qizkj%$phbE@$LR#gh#7GE*N34b@^!{stU zLN!!JS_5hqwkE!{2ikBn<;w}AnO?Ptp|!Juzdz{sSAL)@`BVe#rb5eg$p698X<9EItRHe|F%KR`+1V%}~l^zL-9(;(R_GLL`cN0Nzlx?a|v5(`mU+}DSRX%D)`8tIN281q}Xs_jVlX)Cbr;xNwaTFBsBo_+ibPXKlc z4aVrWZC_$$xiZ_Z$6t}``^9?Sl-M})!Q$5%2orzP=b$~w%Gbr+)`jL0ZRVE}YtN1r zE(?#jjEUq|1RzLKNYYHrKQaFqjo+*Zd&+|I!kPWvJp}WZ)pD$`-5 zXWfJO*ogc-cj^E|z>#g&5+I1FRjER3Srt$}N`^**Ltp8$um!q5_1d{mAC$fQp%Zt| z<|WPMxc0JHfoo`kajVgDxy&-{ARgG4YNp5%D=x=Q=w6WDrYqETP)#G_7`ON${%KI$ z$Sf0Y=~>0>d$^rPUZB0=N>UrNMAn*(iW2RftfqiTUG`$B8$*$`wRmgmV|^#sjwQUK*G_C3T+}(3(PHg=wZjN!FJX2U#lgy|9!~KG_-_+00yZqO zJaIx8n&5@ff2&qThce%JsC{diWxo(=sO2%HN_E~Qr1wz6|3oq};TKP7V?srZCJ8nO z(5!V?qu;iS&W-glkIQ6ETDa3H;?~ZA&v=&pt?b5D_vaPYARQr>tj+iHcBFGmWJ|x(WU< z8y?(w%V-MpO5gkn|F2gvLrYn%${np=T03svgEFh>qdLz$eVL= z%P#U@l1DrJkl;+qW`jaoj<^-42`mrIh<zwHBF#D6pvueQ%J| zeuowXC<)YXtOb<)V0Wz~;q9_Wqm?Wr5Dqi1_UQE!#H?2BQxi{_W2$=&+&zL`+ne~Tl&8}uAN+_asDsxn6Fxa5c!T#OsAy|}+-a{D| z2Zw?T+bC@DZhGAH*-NfxFk?JgjeZ~SsXbprBs@7NwF5KHiA z!wK!TtaR7Vs|7HiSbr7NRvpqG3xa^A=QbXz?>LWA4<=e{^L(8J0X1i@wuqXyyJf_tpAw`s&$xYS1 zUv)8utK~BSLYUk0sJM>48rjMSTtR(-3l#y)l1+CokZbR1KI`N14p&8zH!VTpceFC$GZqO}WTMCH zVQ;we&zR9ye%TKa2%D2YMAj68M^DPzgsLSx$1gsedQC~87`BG9YQZdA7ZQ5}ruXlB zys2J8te2-RE^F`J5(xbK?b>fQ%=1t(j|6Vch+q<-{OI&81};c=KmRIrq%jYLm444= z(rgQ?To^7**R_$#)WFpDhHl9|xLzvv%iD0cUC_X`;UTto0>K#nZL)fqfc{1f{f-|a zxJnuqa)JqTO5Kn3?cj~<{77#To@Qjp(6rNuMf2zIelIenJO^L-_f)PYMB3Ll(i~$! zKOB-iSO>Ft{o3&+Y5O)r0~b9r$$#d0X{(V{!CtjIQgqdI>#dNMv!}yHQN%5r!`1;2 zdQ6eFhDHKdROueNH{rp=8oY0IBG`~3-zX)NDpoCb!H2AxiaPsGz0diW*^T`FjY8Jw zcYidnBohJM-6KXSt1>?S0!sPnaEntcjbV93$;zxU8qA&&KTGpOv`mFphLBG|Mr`I| zkGzr1q5rgIeQTJP#i{xtOm1cNNS&6%u*<|j+|_zqX%CC2vCO&PXPSiPR<-hR)oL>T z2Tiy9>koQKa*TQmFbWIV`tYl9dk!l1;sOydk1$vf@Q*4qU8Dxqc5 zCw`Z|c0Bt}KNd5>;v4psqK*9tw~-&CbwEre-QHy>nOS_;GW#DioqGat;Yn7G;o($w&B9wa}#D*Q(~TE6K_iFo=N$4bOSJQVHpAEQ&ozyO6qf zHn$U{9JDLGvU=r*ZV+a;bVj2%E4>-=_8%6vJVwM`Jth#|Hblc{T~0nwW+1sXp)uJ# zOf(YAy}_71=VK4`tWYi+AH5&UmV@x*ACh{UfnSQ4FOQ~|>H}2Z>rYTrpC*MMw5WO2 zDHn+kemp{=Z)*CTy!rQ|@eUQcuOxJJwCMxk0g}oMhh-3tkuEF9GWOne+?=p!2oqBP zOCB}~xp8iRR0!XDT=DB=-n|=<5No-Ou3#m^9b%rZJG10R%U(3hiSgG3SBRXcz3W$R7c9ni=h zx7};*CjzTILl9v?e&aE3S)%&?m8skd_I6(NQ5c2i#B+&`O$XF1>{rQCY@XOcKV4s# zDAXwtusn1VdX6bLiDYoM_4H`0cNnxRB!4q~0VBlSx3BES8f&={{*{v=vAZt&zHON8 z3wbPz;cnU#cZKn(fSm|^#r~qO#5u(x>)-y|s?eo8J~&~^(3oCx_ibhptomckkVhM1 zyH`=3td~FlOB&bmnU2Lk?J&b3{9`F!89U%`n&xKyTabpCWKibbuTE_VzN#>9Ci}gF z^DMf;L7)Z8@4DLIbwHboaMZeUUXGTzLyUuCG%micaG6uTFU5O$SK_(q9!|$4B6skB z)mrJdraC8TlSir4(rrr|i7ylyH4Jo-Sei7MaBlFSoe|zHrkSN|b4RrQpHd_F> zswASFAvQGmul~!2v8Ug`T1yGv<7{!*X-<|x1o&V=;WX4oa}hxB^^LsHDQ8l$;Dno+ z8E6>o`7g-v^wAEFwGbv`sw!i=Dc~RNlBG0$+QC8RVeQca!_FK_36TmbT?$bN^F3Os zQlm-0iDhgQN~SP+%^-E9oBr7}ZJuU7@WaQD){+o)S3DeOqp7^nle`W!2Wk116&ldS zCA`suS{J4!mD(M@Y3QZYeO(@M+kzF$nYB5OiBm0mLDfA$4no$`WjFe;KTs_9O#p+b z9wCXKXj$~cY=g>b%LJZZGz2Yaa!bWdGYEzX0VjW$ojNK8jR!}jsB*OoY>J+S47%+Y z^)J@Ma_#i@bngG)0xUuC;PmZCe2_(u5p6xaBj|z%r&LVG>8-hk1F6Oo-=~zdU%uP< zxd^H-;*y}8YlR_kc~#m!!iU`-Wvm!g9qO4mCu{h+09hnGgNr_VXEWr$Q1_xxrlifg zLiqkeY6g*Zy!f`8rqQ3*Sd22;I##_yqeLE>N)ZWexAjT`g0jrK=Z|YRt6NaEYI>{m z?g@3y2F|~N@wpxL4oU{<>Ym4Tp<>DyVS41Tr`T$0zWdUgmbsa_CSsb9;h~ME`%keX zhfbyadi(-bBR+-ibRUEciqG1TW>oC|&sW>^H!+5k>Kqk7;tR9)&Ev!Fe!^ZN|CNLy zL2~G_%=vE!h?RPng2&!$t_bd#^axCLO5;c#U~K3#1NsV{oOB6d<+@+{{7FL(<^JKi zmR8`E9KsxspWaw-W4Sp}V4($+%P5t9HK$>!Xsp!QsV+O{3k9h44&Ryr-u+Ioqq_tW z@srbfl+urjcSTj$`HihRo*Ex~Y%0k$K7c5$x{kYhIUf=kE*Ut&Uxt{2Fd6ep#PlhAao8-fJ`GA~Bv`Ix zdE}NaT!|Lo)c>;NUNE0+dk$#=$y?j(dt8;Xv26(^D@{&Zukw&s2)*X*is$rUALTHi|`)~NIPIP~`*QwHk zobKfj&6`r&7#5a7J2tN;mKw8}c_7RVg|elIK-Z?GqjX6%AI9x#D1h26lx|{5=zQ~_ zkbuU!zkhB|cG$@}YfX7~f=*#RUuO1=o~rTc4tW?u>G5)r{YFfhK&&4rQ(Q$*Aq`g4Wxg(3M2xxObB%GAW>806?a1 zVj*f%GCw5XrNCj{%k~c~$c*pS)K2gAV^d`x_!W{Xr5Ue|y!>)cv)wbiG}~S1tgTE6 zF7*EpQ^=+N5h;m_H;(h@ZG$=aL60mi_BPa_?g1>mm1ije1_0%-`w(aFj>aOz&Nm3< zmzAh;&h1snT1M^Z@W^E8^mno^s$K<0W~@@$#VY{05Oo8w79E z7P+C}+_(B?QXBMr?n5%xw%bnz6_`oaIxx|=BgozR{$T6}*?W4_kx66gycC^%vz`=9 zOZ>;jE~R8mQ#&+s9ob02X0ZJY@sjs7+hb;zgMwi~9FTWUCnhl{l4FW)m--N294^U_ z=#-6fzmc~Hm3RdFUK?3x!pHa^9tUZP>8%m~HRfC#@#_J6J;G1e)D+)homsK8msj*p zmt5dMmyUclUm#`fZn-pJ`tn$wmH%%ojZwurUb0o5*+IacaD;USbD8o%tGweoW)Z9pMf|Q%|9JP;PdyU?@wu&9siu?BevafahSLw z+RO8)S6uO4lek_K>dn9(@5l$);s^Q0>;ON{qc1cGRc2O9JEj^cI5e;yf;+8q-}5W= zf)RQN3(gb9kY`GjsD}07>eA%>0Er%!++C8mv9{AQ)~u5*JL2W~Yum>Zv=DoDa2&9; z%&A7{XHR8R-8A8E1s6fFLYoPk*Y1+I(4hqU0D1tSCI@%HH8ntwn~3Bq&9r-ztd};5 zMC}@Se_Mlbfm36hmIPAHhpubB1DvS`o(+n!k=l)!%x?@$;-d>aT$MzK5+d^c^MSh2^{TV zZ{!?)2KNYfu0k}@cz7&<)KLw4p6*i807IXk$Un|K1oUUS^i~%(YHX_s5gc=QqUI3C z$)kzO!y7Kqq`9piM)d1zIDoa-u2P!L{!ZCh-by27CR3b)jGk-ICE=>OpELfmgR)HY z1$bg3u6f$vjbPk^0cBMTRm?7WH2EO=Dg0bmrJs=n(HshIWE4S~Q&(7 zE^O*}leVP%(@n8!{w#}MxZ?MEA2Q#;mTmpI%BTvw$f#|V5^gj<;PTxB2lWEcWsDj@ zN^7Ri%OZe4{}qSR)&(10hc=e!7Ta%-o=&;lyqjoflFHYWp&F4V)1u=JyHFHYj+DZ~ z^Xx5(lbz*vT^3FFn3g~>trA55GkNgW=>d;1Pu5R&*+?%rYy_V;Y|x#3X97K_cvsjN zc$Es>!`kckB=i_HM}AkF)y|-Wz2OS5vk0d8j4+-6Uu1BxA^)jE zkpdE9rJLk~r-lC83PSIIU*NlW2wgx2N+R3@P|5(+4^z6^Wl`mQJM!tk@ft5GW(QR@ zaBiy9329t;U&tzbI!KOx9p|9+?}5XK0v6qPFZq@$X8*XlxU=QcoMF^`VpzX*ybpte zC@fXTkkUIu+Xf20D&Av|rcFMBwAW@I-~5(vTGS6tDv$2;s+f>``pH!GmSDzG0`VG{ zEHZ$Gwjwlr9g)*VO5PrpW!=1ulJDqjKSq}tet-80-c2L%ImJs(sUxB=AWc(#i^LicsiJdHRaKBl(n=Z zcuosc=%q*u{`HPzi{Ml+xQvsUwVq;WnS}E2dZOSup$$jhwxjtMFlm_ax3$vrYGRrX zzbvZ9$Ddq5Tv(KxN9%9*!cW%PN3L5RlLLtFEl>^+|6DJC+4liaACnLTFiwRKnn%hl zTCFT;8N10Vzd;q_E8E^UllP9ydlK3)zjTZ%D!gu?`P=5(Ie@FSy%erQ}iwiMw2&-?(C$KViK(|Z4_top=#mdh~OETmeHZ^|#km6QPw`X46k=1BSoorvs3CD&=E3Xhy zJIS3x%|F&otq&&-uqhv6@7SdZ>?I|txqbk1yEs|#tu70I?~5{&LVfQ(a^Qx7e4clV_V{tt~$a4fzAGVpx*#lMeYjE^CO(M8V4Z}Sou9o)2aRtbC9FLKWRZvoN z4{#8x1bW?2iF6Th(;oT~>2K?Bx(DN5VWoLlx82nED-(_W3`iz!w$u>BSisyDl;5>x zN2wC@thi;hq%fOi&`dC1a%Tpdh~CY7mP<)z@F^zrxH^jdF6Wc?JPfhuJSrXHH^LZ zt%v|iiATGLYkJJ-#wWwlQ}t6jWX@~|w?{iVH)b0N8^+Y^aH6t2ep%*#$4`I>5CaUx z3eqtM$Q+uWhYMQ|jZdc>a?)t;H7XKM=E!k|p`=P1t*qsTEzdqxypFaS7n^HKd^aQ> zT~dj`ZlS~zJ7a(An3RYU)Nt^L5?(5XZw^mq2>hTLk}lH23<~aT4PMfN(4HM9AcGao zn{V@}+*nU&h`f!P4-vKvVXbj8smV)YJ^Y>y zyBPjS`t&Q26`LxxtE7OR&YrcIx;*aLk+x8#enXW+j#F%n$G+m2(FL2X8UJ)4bjIRL zRdAcL#$Yr!YY5OIs6~aVEPWy+@^D2hKZU7>duFl`UOZIJX8-sJ;uTz%x?EzoYMtyQ zPWQ!OEUQ+I1>t~CWh<}av7X>zXN<1g%hLUMlGn&(>C;w~L8h~u$j9FeED9VnUgvuC zgod%SBBbYS_{>GP?oS@-zZXdZ{K=z*`9a8pa{3uZuP}!C0o*6!XA=w@$DdvxfPcG&MlmB|OB>i(5&yKY!R$G692~Exvdvg8PZ(6Bnv2gry%KD+!zshLF_S;6~8$5 zo+EbS&XsfUT`6B+?NTN#$PP~wUd;vidJ5P96WAVBJmwG7(!5|fd#oiOHpjLE@NR~) zGes|=ItSkY#L7$TIxChNU zfZi8@V~4}!>&uw7v5rvL|7iMKI&uduB4dEIMi4zxLwsa%(j{?sNpU+U*&LI5pRXWm zfSbI*ufwU-{czAa1iu)2J-!Oe1Gr^Z2l)rN5?5*oLmtYY1r`5dF;*2->Xzn zz`oK-$Y_+r<(ZbD@0>eb!&1O^8CUAJb4LyE!b%w*(+BeBaOr-Rr$M7xT~Wa{dcF_F zTV64$|7sc*Ewla~OJ5z=o&ViKV z=#-F7VRW+*W3Vy5&-?TH{lC|<-OqjR+~-{9I@e{?ZHC+!P#Txhcm`$a*OMdf1QoUGxTXbt<#QMndIjDsP11|u-8(S=2Ej~0wrGQy{ zKlfrd+0i=jj>Ub=mgnP!LC@SO2XSbrZQ`R0biP&Y%*qUtfVubXELtI?inS4A5L9-V zx6ap!Vt?I^4ODo=dfbf3k}9i^Ue3?7K^EID z=(7jt55`R#$F(P?vTkz$>wNvWiQrns&+BdrO5MD}Jsv|d=snuSuXae=J$-Et{xGG7i^HK8SsT}mPk+7qJmz>*$kLS&mp#9^=Gax~XAo0= z#gy095=dj-7!Yv+>00>Nsy$RKAxf?0Pw<0MS&NSWHuYi|V{_cojOOoJ2+UtEKUv_2 zV+fz>GiN7TDc0IFjD6;zEakt55m)s2SF@*^tM=BBNLZw6)qp*+HsVFo(Qle_xRY>8 zF?Y^I@-?Nyv%L)6PjU~D4Tvv7k)dY;5FD1J3bIC;0>llRg-hKYUo07=h%A z_?GAuWeUcaU7Ix9nRBqzg#IHU`}-|P7WTcjH%J!%m7d&h0jlP-{fY!16Z|vO`7h2$ zI>}A5H>;b54OIKz*UZKa-=g(95w-D=2mN;F*Dl;$X1w1^_V4Xt$bb56HXBi=G@saP z1j}wUKw;?2wc;?sBPc62997%X$g0q|x)LTMXeMBrZ={ku|JrcfQ;O%Vn$rI~KiOb* zxnY3SxSbmsXNgkyR)sBIyA2O?E3w*V)LSBRaVeThR;?n_%h86m0U20sX4e%To}P6C zFU+G1?!u9|9SCueb7UnA4Sj!sx?wcNA!3!$?X_GVaLBV3aGM$9s&|{!foqW#(gea|7zQS53nEHF(U0c#(x? z6c+xK(s;`)PJApH%-sIK5imPga^mEjh1!)kSg}faxh1Ebv*bqz{q{5DkY2rWnnRuC z*f0}b>IlJpL$~y`%TApo(gS8`xuyG`cN_ooI|K$kQKnN1$-GHbrb_CF(E?%l(+^o( za0(FUT~uyQ$x(4LTGp|A%{yrjW=&(l&9f?8!*09_JDA6DKp$Ku5h&~tq+;-!_7@qw zl@b51IRJcN^OWv2 z=E?geHrYq&=6w!iRb5;G939&Jbh1~`DoHpk@?(lP} zfj5Fr6^R1>K6P>eXcn%+I3M4?gt?my2okrr#Feca@moPW##0ECN$gBB@O$4v)t$Or_Dvq#=iwX(6!xvs_wuBfshr z+47ivuLl6az4-MTRI}3KH~+xi7Teb)7~FBsx`|o*d~qrE+d%S3e^W)71~19cRZr};auc?&#P|J$ zDxGS`*61FwyO|Ih9tEB{;1tREj^cV`8e>{@ zA7E+pGu*^0^BHl!bv9D;zOvgD0wZ=_ptMbi!~0R(lehoJ@Cw9}058@I`XW;E8mOUc zz+pyLcF}Sop6`Cuv`yIsDye0azV781-P{Q77JwMuPC2qmQ6qX{rfL)1Hpp{!HTzJF zK}hV$U@WCo;6S4^u1U%wx5G#L5*S?|QM|5f6~^g8A7MYCPLVW;!j+Y0!cont8F2}J zcxoy$DRsrphB>DxH4*a`_s9=jDp0p+qS7($@5WF@*#Sv z`p1(J#a>6*V*4gvfXHO4C|o(O&zOcO0@eO2MY zQ{~?8&YqB}MIfKXDM1yn_kf-EkL>#Z?Lo9^Sjwz z3j8qrpuUw_;kY33J%E`L*#isr_lcEz9=JB#Mmv2zK=|Y2=HkY#jr(ODE@-=PE(K@3 z{z;&M@?R&zR=-0S@UkeU&fPqF8KAM=RJy1AW1-fMan{)AggffXb&n9UtQt|q;TQXs zUT)divqj`_tcwDVFOxbtC<`G!lkMd=Jp3l0mth17!C7Su}z&t&xkuG(_y`bHuDv- zLLqSorXc=(AQBKB`YvKZ+eDWvu`Rt!qV_0@;kLD~-)|eE=;nyPGQE238(m8atPnZ{ z8K^S0u;Muk9l9=29^iG8p&`6ppE;JV`n7=T@Y-<#-uMSr7daN!YU@+tkYQAp|9omI zE4Gb(K-G-Df0X_%_0k$b3-Har@`4VEQR$dK)N#0iD4yaE-f*qF?V0Qf-JaRNGtI`= zC3ev!oD5!dfdXs57>9u5!8aT@sJERLP#Oywv)&r;oj88gHvNGd9=BhNR7>AX(NN|= z6;C9*lvmyz;?tIy<{#0aF$l~DlA)>VpXZ1~rsjSCQb=Hpar6i1JRezv#NdpJzXU4G zD(t8p+j$P;llmyd;0XY&Y$fI&(cK_&!ngoBv|npYTTK!B9jPu@PgWqrQm=8uz##eI z6|=i+h|@9v`mErNY0P|A*1_TD@1FU7F*R|#Qce*4IqMMvr8_dJ2R>1k!}6yto48LV zBG#59Yhf(1l}#akL;0?jpjmBCg~!yPjb3Qc&RB8_=8TcI;PI4aO#Q{V!fSxM09b^AyM&_}n_W}aa${sH`w#`Y=o*~EDWkN~y-rRmPoR^%gdJs5m(zuj#00I$ z4l;6qo0-?z8Gutuxx>lXbp5&g{@zt_WQCjX0pUq%ZEH)N22U`>>mb!Pny0%E=V9;n z)+ZY^(mXl!Cc}^WA7A9ZZ31_-CXTj4fkz|;tvn@kx3f;3fo@~{hb__GO@&&1Bn77) zwzS*SdkWe6JC!mM=KBMO_q9g^Ki$6VpJlw(kEhRHi^xXPVHU96>xZ0MYc61&UxdL% z+V;)x!_|`_bsWjc;;guouWG?TPreNT_@Z3A1DdZVHx5Ciz|#wyhkLCKI61&Eyx6vQ zrS9qFgc#5%dc+O(ft!JWRs;4i?9dS9g-mNgHT)y8YDC)>p2v9r!yr{JYj;i_El5as z4yj53ZIPet%&<;5#Ct>x_3Y@UPeWZsHxeiIwZO2#b-cQy zgt4pS(H=B!BXO2H`6p99yuh{_mZyGk4!DS36Bveg23jZ1clvbyQoX?kNx^@BS6fq- zCA-{0L`wCEvD>dKm%Aw>+;)u*Y*>dnS=#0y1hU%%-6#b)L7?ArO!?d0uftRH4E`DB z65MQurw3y~>TGYoknjI$lC$H{`7pC97=euA<5O%j8mX|?kcm7E-FrfFHUeG2eOxrN z0-B5jg~d_t7k{5U>Hfatj1;(>);H?W8s}IXzXp7PLTHp50Oy01u+7EC1c?~{7Vwd5 zBGg|Bq3?|0z&(I&pqOmIf(z6J-l3-bt0@yyByb);Ak!-;SV8H6BT@Ic=Ajz1hbw&F zneH}_USkAKy4Rg$gG8XVliBH`;JbLm@`P1OO)2^k$UYt4L2h*}O7{T*btL?Wy6Jv9 zry_vQdmBS`t>CF|Y^*&!LXWL9fUQF#8vZ*?o_O+>8ops5l_0U{&oBHvuu=jyQSE-0 z%&=*o6x8%%1bGsA8-(mmw2EZRk`)<=ej~s_7+hq zo%VuAR-gmX;6gEHtC8*Va@xpkwwCh(>;BMjU_35GRYaHLC;0wMNT<6F4L36oO-M=S z)-ssW#&!WxEZt-bQIMh(U&2(Jj_*3hZp%;EpbzhI)VAzzD)yIU1-#Fyk~Z4At;P{p z#-4PeUrABWT<%)*h4&4OQ7ul5jXO^78ucb)$hbGT6E&HM0EvOTJF~i*)wVCxSFdJ% zzrLJF9(a1gN>P5F^RN*gdvdjE82|W;OuM#eDEEuLPKT*OuyYt+j(ll~rsg?cJ9$Y_ zs*wfdwWyx1#6vQsJvB{5^@-c-r{!0cxtR9!@||DIZR5g~b2hk{Q2|jHmxp-{nJM2a z?I=CqmkfCCSPrP2mqqr4&Eq%cUSP(>!{QVhT7E^MO-*5i^0uSTU9W))GR}EX18vk) zw0M_sr6(ii?q7ss5=?V6s{g@}wbVM->>fRk<-66KFozFYseg!x>yVP;BobS6!nSUH zGWpyB*7Q7D>UUjjGb?@Z!&0R%UY%;Jy0YTn{c?t#vCZQp)tg{9v+5=~>g@(6-afR; zq8EGCMuK!Dh2O-U-saO?zl}R@8*w(I#WM*7=6d&`e82N(UK)oJ z<{ZI|V*Et=Zy$leaVjU>!QK;j~4PfaCG}d2D z#SbkzK<5)8*8=ljI4*fxy$IxkZ04{|1p=qCpWUx%TTk#pb3b87U~ZCyL&kUu+YdEp z(=94SJy7(_4?Qoc8QieD8M3HKbS?9mUoLm^Z__Zn?6&Xu_TLIspAzn~vX-Cf#L4)w zSeqWHulIi5`ZG ztP(J_Lnb%-%!NmNaF%6v&nXdzYaUQq?By!_rf=wa|Dohs_DkRVHI<>i=GgZ~`n5W> zq@DxlGmUra31tYgQ@6QDz~CNn=zsmGZJM$n52nRHGKD+FsrD zt^?2lRN0ZBNM;?R$JFS$$q7-7)OV34tno72FY5wI<#(MK+;$0 z{Q$$e|A5E#d1Bc*cgz(tXAe}Kz}=9U0zjR;Vt-{_*Jwram3Bl-YFNX1Y4a(xbv2}T zGZ8?;5*M@xcm5Hr<&qGFt!O61I4E=`eq*daS}jIf;K_Em31H8D%)LT3qt_e+L3N$Gr=2($3sa z5EbJ6SlE?MCpc`uK2mFP^5gPGjNGB-#x*WFLE*j4SR-d@ONt*4+Va;+zS{J<(u&+w zKfg5V8i_&`voGG3__@QD>c9C3_VXv7bEc4|Z$8K9cXZ{D>RT2^EieDp1(X2(D3Cy3 zOebZ}tk9f)QzgA{E(A{?i16fje|^t?qK(gxDA>>Vv3NP*bvP+R35DqWsSxeN z!{o#i$)}*zR^H*&q^7+w+mzN@=DMurl-nj#vCD(ey4zJ1TU-|R;UAF_&`pO!l5G0?*dU`{|IY{Cv&+hZ9D~$1bq1b=YD5w+_SwqxO>Cs)0?p9_7d5Ya z#GEMRD+jvSJd1VO1oqFMP40Ce=$0a~pG94DjNhU|gFr3-m5VA2Kk57XG} zSChCuozQxx?MUP}Z3|-`!4FPnaU2~T(0pZz(D zw=tgw+~wQM4I(H?>Pc?ABTMLLuJydL0Vu$uX7P-6m?mHc0V;(V>A_o6|Le4^?#|{! z3_KD3!`;OdE&+Nl1%vkW`*8;=!lUt|>y-vj4b*aRcblYfdarPa`I3DzM?)e}wc)G;sjB@x@a-TG^Ptg2NF!f7UbNJF_wNI2%!fw@+mk#@jdTp(3Y+cGO|LZA z`a?vUTbUxmKq7GQu^&L7k8t3_OCWm8!(ZHmvNp#m6#P|m+7ZSIMO0lu0C$caf3y2; zhr+Zw$V`yC(1u-ZU;9TOHqtu|mj%rRfu~*0_UD9L2sFCRlUAl0WPU4p^&M;-gB{?? zKvLkCS0duUtJ$1%UfBb)PBli5-l{Ap7C zS=Y_*hK!w%dfs6}w@o_H3PXVepA+g zDv1%ck`?;kB*WPZVB#qiSdxLSq|$Y?TaW^wE0-D4^OxaI{eCXv(wr|)_2y-Shmll6RMkRTbM2~eelaS zQm{OPqGTx1)d@MGj|oFdWaB8XPOILTRZcU_62)k4gZ7B~?452EIe(NEgVjcJY$CX) zW_1`38C=A~5O3kpR)Sa;oE7vwIzoz?v`iFOTTcrnxgTSyWf04s^O9fP?pzHa^~Yyi ze*@eEbr&UjG>|k4nX4BE^QDgdYGHU68!zkGC&`ia>`&bUvwLs!sk6ViGCs)120WjF z`u-zg`3NRL#-ZDt*M5kVu$Vr$A#?VtR~Vs4u?)$h%)*CKGF?RD2e%NG+#s?)-U!`8pU>Vmfh!=rx-(8z6?X&jSIV4LLY<;uZt2UkO7?% z5R#B-=d}*H2Q_IQ$BQx+%r%DUwY7xINN3q>-Gh(SoiD~*ekR-*L;>{N12oQke}zU) zA3bZ1?vFVlP?_>JRXR5OQGRD?W}9;>LHwrFCyO8FL?=#ju+@y9hge-406}!hpF^;` zi1?4?W5n34zR=rBPW#4@L7P}3KIUd@pazW^@&BPJcbyOaIyn{ zUBMNuazZ8ZJXeTMa;6?me(%;d{VIg#euCaF0v8&P7lsI9m3nhY2`qR^EMz@UMr`Il z&I7spJ^;&o|Dn?eqiiCN2uLJKW}BUuug#YU&*+9xg9NwB>MlAV<7+;5uiX9E#Dz|a z-;SSIC{=l|M!z@wh}50sGP2D@r@$(bEhcV3VeCI?5E7c}+zQw`%yoIID13hJDxoD` z`2!5FDTz%yl!!N2RHqRs36k_T-dZ`#L;~TdL4kHwLCC4^A5s6+W=~@~Z?*T63^6y* zHzkN32I?eLwzixAej^SV0!ah&bSVWArEqVjTxW3A&~o@s9FXs>`r#2XyR42H%# zflgas>=R;#U8KN>l^-KOg&+fP%I~UPpW4KS2bp0jprNWS=`(bv(ht2yTL@&*Zp_#5 z1fC`SVNl#}_Vwy&=6^)v^d0Bl~(Q$faAie*Vq5jU{MemI=H*R;G)2O zZ;@S66Ze#=AlPFJ5J!ie7Yxp|As5dl>&vyaAZY@r@5hR~AmQK!rZDluNO0X)KP3Dr zz9t4K=TY)kjXi(ssnrb+ibU%aV#u#@srXBtoBs{vQmEnReb;;5?*nk3+MBC_P<3t5 z{${GehW$i-)q#34^l+GESvM&W@j^L;_??V%I-ftHhr08LW{GQd2XIuZ`NUuTCr^TC z?e(?Zr-bs@C$YJ2`X36BZ_l(!5-0@F!YbI0IO_e}+egAexjmc;(}Hz#Vx0s!c+=2YGhLWhhR9vU7V|r`@8>30LA-RzH_`e~D`s02r z>7q}+A@mA$S(4QjK)hXL)A4cwZb}CW6B$ z9^Ja~zbMI2e12;!iP4ZaE9Q5~%XMjK;&unEq^I(aAG$N$3aUGO5c-P}>xOp93xj)o+M-+=DCxaN(u-qAMY2n< z$lJ4$qogBS7q_#oUXJLyFQca@9ZNCb3kL1ti1>RD8pl~J17S%qM{^zgB z$uMidJc-jy+if3U)Dg$54ny%5@Vo5SDqhbcp5=W!+c*D9^rdrckwuBP0mFSMGim;H zZv!R|*d2=vij5_}?tYkNGg|ETud@}Iynd{7?UY>o^>!+108PbjRj1MKClj=6ES;kQ=adxfv@`>kagGi56C2);$2CZGl-fMzy`&RL z=qrs(DxC{*-mtrz&Q!CzmEz+u>Qtt_!ML{@lJ$8ugj7608F8|ukrdxylhHrLT+1+~ z)1fc8okDt=`qkYl`EUATdq-_i#N-jllHkTh=u9Me?oNoIO-ui9N& zT*RIIZ4{6HS&7a4uRoJ?q@_w3d^pJl=;?UopEc?b$2rQl50@Spq0U7wqhmIygL;hso9;t0h@;~+A8r!vMF1Qim3m3fpl==HQl)yMtS`VwKa+h&;-2%_FsVLn@ z@SolKns(iS@Y;32KQdl!acl&Qeo4B0qo2Ne*i&s?Q+GJdYsr5`*t##`e!Ersw|%3x zZwekst-GnTv*$pCe9zuIsm4 z_2VumsSScv$*L?=0KNF3!uR#ED3h3`KwG{3Wq;YKoMKrN4dyG>7-x^AhrYy0dZOzy z9=2B^qYZ@m;)aL}=45)WXV`5*N1%UzCA|Mn&K#LjM?k5-sv6>2^uSD4E&gb{DCgQn#p0Nc81x9|odQZp* zD#~V&Rg>*f16kw@53np<9Kosi(%TYy#4a9~jXs!w;OFm)pRUJ=er!ntRV&wi@r;RF4YnkoiQ zcM`v`0~#C+)(s<$wRgp<&;vE%y;dz(9<*zfx(%Za+BppPJEuGg$Z=XCzxlGXgYQ=* zKi})eUS?m{+OqGY6so#(pYr(FDX|8w2M0`k@6ZnOBvG485?z7&yYtXk%v=QOSSyqG z_0Q7BAq{f)pY;Zh1{Gx)bF++1M8p-PkhS)({jjb~AUoqO^vDs%dgSlVQkA;+@_FHA z!zd+-85Zi49M75Ar`e7yO14^_`j-82jCp0PoePFqP3m?DQw*a{-&Xq9r_hz^vC}of5hkRMsF3JCW=oW!-YC9|)Gt|`PPMvPO|Bs0LnJED{ z{I!O2k+13#0|FDnlFse3)x6@~%#0=0XYaNs1!Tlq-m75JfIQxU~3`Q;>8;oPGp zwZj4>qu|ybB~UdKs24Oxy5)&kp|EQl&NvUO6m)(Y`@Jm2GwiBw z=!iisr)29hgaU^!@Ry82I1{5YR~bW*Za013a~SvLJg$~#To6Bx8KL?z;9Zx(P06q% z>1Fi?HP(^CfptM-&hYM^iv>j#QIJC5e-FcXVqW^X1?hS2x-Xa?!MWVZa`r^*ljS ze$NGC&Nd<)LsniD`;8Yrxt|z*K|SXn$lGT-V@ z;q{~0Y%=x31%g9)(6WPj6gDyYcnzQO9ff-{c$FXao{s5PLCGFIZwpoDoaR%W4cTf8nA`}SL1BDsCao>@u<=~r|f z=z$$Jnv13qdX9SzRYDm9{Lr8gfanH`1>ZRm3Vo6~3nTWZ5!kv6n^E$>5tA(>%bg+_%8Z7a(esIAk;Kv`wA7Qd-$n;c7 zrSF-lq_F=15JV9RGTZA%$z?%3hz7Ngvun0>nn)S|xd}{)cAH3u_c^c^sCzNXx> z1p56Tj$bjdiHA*m(QhQQ?+t&8CGtK8Nuol8fw?y(1#v6}MjteYXat4S^#Qh>AaYZU z@;%J+_tj{(NVuwX)5eW9i^F7(x~9<^1MIp0Tws$suY}QQuE9r(A_J5P{gybYd^r9GUZ`=rdFW;8L*vr{1&XNiLZVB}>mKn)3_!An*`HPLB>w%_f| zw0|u7hMHtL+t;Wtt1S60Ei&k-SrNe#+d(@S+js^hO!y?i(Ji`Jd@B>BhP>W`b=9C! z7Z_L%7odGtAHjY;!K(hjq?Me)?b84xU71S{u-YPz;2(157m}v1tu*&#{OkGN>xoW8PMBV_^jTY-f%dSEqT?_x5gBwk>o!+D!uxI@Qr`_JL znbu`DHeNlxi3bBTvZ`3rMaPv+taF^{gUO25g6uZZ#PU*=;``aFocQy?M=d(tiT=AA z$=hBc5k|j02c)Q-R=}B{vREH9-^*NOY~m`g6}7+AX95C^2H&gRYsX^W_RU^?I?!!% zQMs$0&X)(wTxD{rt~`DUI0{cc8YH(AWkYUv!y^&oAG8vw0J67E`gfD1rOfDqvrPIB z>4)5IUpo~^8iGY52N_tC^0Fuso~m$p+cc@^jXHA18M#nMjQ=ANz5U>8 z?NhbzCJpX)$-1igQ+NGi*x8SWONts+Md~Qeq>Je@nzXcOo?Yi5tP%4tOhF@Xc_Ce) zd^OF@~JP8u4Wjk7%F;Fz>E2+a>l-u`O$}0Oya!=_(6T^IV?r-JaD*z3(pLe#iQ* zWmbh&idsFFthz1OV#1}-qwt+AUIH7iiU=jp)}uh(AS%GHNNu1VtQE-;U=LdKF#dWb!o9E8NsM?6|t+!v}vOez&{3mt5<8c)eqC8xJzM^ zSSg?kEr?k?_e$Kml(PGMZ zpg$+OT)kkrr|K)CAyG}o#qo=3kO){`Z;4iC{IWmqQ2W#!X-=$APiyw2L9qPjHF;>+ z!U}|0>|7}weTc-6-jBX^>RF$9>%ek1iGFpv2A&7Rt#S-`0j%?7yYQ|9HL-8h?S8{=-`+|tTezn-suR}%axa<&Z~dQJ&#Hp$`s z5yfjzt_EGOMR>&-2%ILECwP3;jn=F(^q&v|xko7SJYOjiF>#pnw4dGJ`>NP+%cn@;skdj~l|GaGTLBw5u?zo4+l9Hy#9Q8Zx^M41jmp04 z+@bP4KJ>cBY8+Yk=K1MSBv`$CwzrRZ-0Og*(ogd6+$ko`DFD zFeqV5P|c5iw14>j#HRG?D2v=1`yyA4`&T^(Bt*=30#b7;7p>;>XZvW+15{)KMp}La zU4);C!125RuRNb>3=j;ttm*0SEmQZ+gZ^Ay>9=+;WGCE~x$_@-L7SKMGU?fE>0={3 z*vBMN;=Hn?w4~c(07g&VvFEWvqxnXCl{hPI>gDj3ofMaKu86?t#^*T$<-6Z(S#Du= z6>q)U%uLZJ`78RFpH#lBS2m#Th95IHp7Qrf3pZDSNIz1MVLO#8K1X#xPph~jW4>6a zn;%?gIBlgG)yGZD$39(tS4b%Gs99AJ6mDy-={)iIVr&LJr?z<=7DV6IDvY7(qm~UU z96N{WJbT2#e!6Srgrl3!mc(#k_3e8$KahXZEpeEe%@=WLy*i-^a*Cb3_i!ulAvoCG zc_Y|k;Vb0U20^-j!`I^84u4e-H9bK*UFn6ol4d6zytImDO}M8NY6`c4-oR?E;dPoWCaL3B_<)P@-% z;`|q-Bt|sPIua!KhqGoVa?6OBD`q|CPFCQando%)*7@;Z^hMqUWWyFO3<#VUV_LBH zCFAATY}~4R%}vAReDk}Sr=I`)Gb*{T)E7uL)L8w-7ajt3bPLYqfUSPbWlqN+0I)&> z1{m|bIDK~^$&zMCzWfPCwX^%M{CX-0CF8DQgng~R4dho3!7k-)RCraiGize^G5ub{ zmHARD?%m9XZ;Q^x;WFedU(1^03H4WywLl7;hIB)eTeR4|HblKG{P3dy#avI?1!h!X zzZ9AJC4|QHN0i#W!S&%o&Z6_jg2lAid?T8Un3I1*V!#-f6M=$h)jV;&`z0FDfw*I3 zhBupbofuv3YD|mJ3Y7mw=JYkUAjTvlc9zF*49mN6@O*77%$O;+e)do~;$qDwdj3dI zNEk8__?X}C*?=y-5vGe|W*EljjPS{?vO8i)|XQt8`0ipJ_Gkq&&rtiAM zU~#fjd~~$o0a5+CVzFW&J~Djdz?|J*Z5e|hq+;<_^yc`yKkny)PD}R@+X@Z zl&cgQYZ~N937GK9)Y)e4C{p3Nl@e5Uz_fZ-lAY6OMuWTS;ghG#3HdRGqiZ5Zenr2% z2Z&s|a>|GXz8CamDz&V7<>V9veRA-cZt$__ zz*$B4xykTtKCaUkW%NZjP@SbCOJ=Mi%_62<87E+0Mp%x*uGi#ABwwfLHr(6H&Ie2H zn&BQNYt+A$rGMgLBTb@Xp4~AUPM85w3g}xmha#vCN{gjlmt={&M^HN8&AdC=84EZ& zVDA%8OAIQo%m!esn`yX|m_$EF)|9Cpc;m|zIyYOHy7hV3L$l0yXZFK|viR=`zr-H! zqAZg76Dr^(Hr`pM)@zM9KT;cYSazIt;)DDlu$#5j;uZ-ZZ)fzCIileJrU6vbzdJF_ zXgus}tTW8Gxkqu|%{9xI1{De5KW?bTwS@0zM(KF`?ad<8QTj)+ld~V)+l#Xvd`a#i z6K!c4JlSER>j6IE^BL>aV#ou}U9B`@&zAb*iNBUtF=C58DXfQE3J0fCYnd%?M@4t_ z7tMw}c;}3T4_}Jxj=86$1#wMO@g*fyn>ki%@pG5VC77SVQ$1Urj6{pv35$DoI$N)5 zK{q(|>sZxHBm5^fqa3Bzh@@?l5$te8&d=FMS@KUP%h}tO#J-Hr?G=m_#D|yZrR?wX z(q0=X$A-v19vP|c{j&1DB`p8Vy__)4=bU-deykFvb{XnxAtWo}JrlQ-^`_GJ?kLp% z_~ZIoDfTg!^EnHTkz|i>v;q-UV|5HmnSujfH`0dWtl9e>k)Rjk^;^a81JN4uv zw~Kg7-J9ENq$KJCox1}&f&1GC65mWE;9$pM+14PPD*(l^HgzU8&)2xGdU4li;mgFL zw!gJagCS3Z-7UJWClwU}kEhuU^IK#dz1D9nL{;MO360&LJ3+j?xHpzTe-?ZWNuLy> zF3eGmi`_^P?LXIfhy0J9Ufv<1xQ&~42AQ4HaXDy7>U8D*Idh^C)v6!=U@mniB~A5EPwKokxSkDc8-8@R9f0geyhk_h~`k1pYd0Nl0EUjwAk^EDLI?9K-sm1 z^wrZ<7GBiyR;W*4DYnkCthY#;{Fch&oZn6NYx@L-0INhM0#)@S(0;~^#a{jP25_v8 z;5fa;9IoU0Wj1svj3oeT{qf_Hc~&myIm9= z5lstN26GdR*2MoRy)0plu-#qEI0fuRF%XolHpt*Q69GVV0OR%LbwaC+=Zt?)jR%`9 zY?ZvgTBWrIy7HdVhbJ_(qfds}OJ~>78(I3uAT}+r@qFq~P83zEat$!{9k01DT?kZo zS|TbPNfLhsy0h)1QlH-IlN6b>_v~l5!27uGHXI$5knn~ea$v(g;mNVGI5WHlN~OLr zUJhKo?8qwd{l`m`pAc1d-`%IOIiz>k-ty3$SI|%fb+$i79e20DLI^;*84`)TFvRkG zEKdH8)f~waY0nh-TchF~>ht9}*To*SG&~TM8v&I^(opWKP_>dGu@z#K4~ArgxOM7Y z3hI?~5K(WJ$`A6UvU+aKzUi5bwWtotQJ*q75)XX-jX_IuGn$XcKUa~o5}J&Aj@LlX z9^%lh+ZeX%4Dr&l6(VfVsZSq>^Tigp?s1Ou8#hc7`^V9s7DR(^|5y~_F4P(P2GC(< zMCCF$4uFx;=rBGB0&9(+WEHw2DGrwt(w4c}*hO<2dJ`q60L((Vy(AacR#opBDYaO= zon!HlC*j5pzfM8<)J9}iqwRCG8ybc=wXi#fTL8OJzk_farPc+ywVO*3#AMHSr5jln z@Rlkf+lBdtnvV@$r$0rPCu97{Bw~5@O8;T3ok%OdiPW`w;F3QR+Dj6>k*)KD8fJ2T??t3nB;pE4|^T?Y_p0e=C*%dmM zOf{6i-=~0;CFI|?e3{mj)o!sn6AJ7QT?nV4;`HNZjy0U^YhhI0|C)&r|mCNNDxIwSQndmbDXBp$|e z_?c@4Gjy)=DNHR1Ht;)E1wNjYe%wLaFI8gpV%sZ-XErNn7U-$UJ;yx@8sof=okj|i z6i36)nCeeIh-F@c^QP(tkX(k~yR&>M{t+#}G3jVGVA5E|eucAwKrvQswdZqX2#kA1 za=xc^II+5MQlsNp;eFcs#8fCOr?^7JBT362ouT(A$wvC3$idx_wZ5=$*x?7iYbDa} z%}O$5Lf+TYm0RnmDkh9S0JI=;7ufn0#899JKqhcrcNIe0|HDZX(#4=Xxe@+6EBRAa zMwO{I$DhA1?=kfqa|Nkc;yNv7=l9iA>PRopwLI}y;v#fI$vNs`-tL%B% z+u0)-+1uS&*>~YwocsLV-{0Tf-p;*W@8@_t9?!><|C)v?6d;nRB7e7s;wgb@GF+Vt zcXRjoI`}4^#JlF{7wXUCKbpPrZND*e|MH8@L|_epRi74{w8<)@$F;L#D^Ffp`=5C^ zGr1SRBSw@5)Z^r}EN}XyJ;jiZO`WfNV={lI%`JITw;KSFKJvY~QgFFW5ix}ToBEpW zA8WQ|?lY)CYsM!&@bQB7J}f+kH#NTGc3Mtmti%I$JaZ%p-2%k6cAcvJ0yhLDx-O{X z)@g?I%I1?x+_k-Z_gW%5@Zk-o0CH05Wy0{g;xpB)`+w^Q{ zV$$dH3d8xpZH6l;HbHwP{+zvcC&S#74W6XxNQ7jpW?85YjGtB>MtSNmIQw{)7K!@i z;peCAniyM#i&ox;I5*JFOKBL9qErTp6h1j~agF;wm6J$r0qZB3Gj=`#Ja>6}Z#!?G z&x-4Aj;jzx@7HeUT>)nb31x3mDx)XcC?1EubZkxbhr~k^en2)SW{iKmDjRH8xKHCw zC771-^TYgAY76UMaZNOd$&3cqmUW@r_E}<^<9v7S(o+qKO+WAW#PX3MvwJ{!nC}i# zcw@$py8koz#J|hVvma&0CfAUb&>`C1wD%n?>XSLNul32M*Wd8Cl4^`Jg1;?VWxN(i ziyj(UzrX6EJgQO?8Slc=QhdLjOZ?5fl1};L2Da))c1F4fgZKY^O6PDH3sx?5lFp+5 z_P1B&wsDCTMr$7%r_$Km^S8<}su^LGzy9EA+G(Z1tJ^OE`&}LcSQ;B|H|f_`$GL5< zuaRy#Q8fUkuNeBG?Rq%hFTj5>`JyTtG1+O=+$5J4V6GP8;71i^1w6HaWF6qb4C2@u zm>vmSm<}n8LGIYzEp`ZBd???RqJJqbVTcy`10B$S*UMe|sV$|LtEIHNm+SVvtA@!D(W~bA5^-&`RRlb*6ZMR{-Xy| z%f19$9wfuRK3jW|(~iE;WxYasWGMvlEZ_o~|ED60A{2oUOtL#Bv~+&V+tV*@3}39Y z)oO%JPrhcjqQmFEnpRq?`Ad?&`JbWk-?lk`?BjiIkr}b!aA#fkbJV71j5p+=3+oM0 z*H;a}F$Z@1P|<>?*Mu(~8iS9*$?Q8n1Wo^0A*+I$4y-6_DR<;D1_`+o!~wv{sRnU4 zDG`HzhMs=*1d7>-Vtb}V6~=YJJPaT!r|5Z_)5o^YOp`-`khTM!wgvwe;+bN{J?Pu|+k4s8s{G3UdN~aP2I1M!5tR9MoQC zQRUCHqt9p3rIC)0rlOSSEirO4ei*SC%e6QA{-!yD7JXh?>JvF***OsZAjtTWOsagO zp_E8ip*<2Z`JR6;kpD}VYvM!P$79YHGKlt|t#4o6CnX<7R)HK}8T#HXZ<|l84B}&S zZ|pAl_0n-scvwTZa_APzPg~xMUgy&)!DNpWI?7f#N@pZ{5*$;zqgb3WC$qq0YCGSH z(L>9SZnf9~0E}qhe(#5Io6~Jnt;r|AdnW2#e-J-YVzR1$Dv;tN4%daWDbN9w9WYl5 zn>v{D?*W_5xO;5|5*z2vMLHrsE^F14zNl}T4By1Gyx-M2sf5IJD*nK6MBY^h-Avo~ z6s-8}hq!CClh@7r&qibkJHzD&#&)VO%+Pp(y0zbwdp5zhvZg&M>SO4Gzvjyqi6>FS2Gt?+cs6bIB-tMTXvS&cB!G$VHQlFe(as2 zW4I!TWu9jy{0G8QCtzE^jmxaUr+ZRZ z3!dHtSJLf$Z5MHaEVXytRh2r%&Fd|nkag{lG3Wo8PhR^@`aQvcFEu3bkdP9|&dUcs26D2L{S`bu61u-DJ5BD5^X zT_?=wCWo(!;GufWO%X`jbPq10AJgggTH%| z$oVKadEaMDhcbMzbJxJNDrfdl;`WGf5FIlJ#{0tftyr9&!Ae7CV9Q*N_7?_eDYd{n zueHo`mh_ib%8$N%yfnw59sYVk5a0P@!F`iECNx~*9WVPOBa>B* zEIQQ^226-DCNY@H(RNF8qja&tq!XY~qb6ubgkA<3)PB}rKOK^8kepwB*)7R9c@K2c z6dZar;z{sYk4$o>SoYwY0Em2lacMrNb4hN8JED5Y(`yMF%#3R$AH2ojlq(-w>uT=; zW(qbDp1jH5TbU2OU=m-dcQO+z(4l$5)bB<-<$&7o5C`^MQuqzMLflj8Q$Eteu-hAY?wwE!r%4^iHL$Dvp4eMqb-T~=di;)C)&)x~8d zOe^I)FjfFeSM&-r28P??ouljbQfNsQxVJxZ_d@j;0?p*hdW8ezmRk6jpUy2k>qXbS zx)T2`es4DS_~@oxu((^8s5g(oV0zzO9prIERu5wlX-3G1X_n^mKVTF+>cTK+MoLp) z7`@}!09PBHc^mv`kXxHZ&Vk8DLv^khdW&e`j;njrGFf%HH2TnLFU>#HH_&gA_3kpX zOOkjLrbTpe9-^knvVLR=Jsr{%n!$8 z(9VGkTj!0n=RI(Ap)D859_Ni-iwJ;&+X4Vi+{u`)DA9qVNdJS!mcHj(bOOT zYKJa}nNR@CkAZ2)Gq!b?m)o&#g{rLMfHIM%BR%YqJ}P6msBso5j+NH*@l+_i>h4>s8>iAG%Il{@xo2gZ_)3v)J0R`+ z(SMe3z78n8W(RGjP1d8(C8(R~N?7D# z20G=yyqx%=I%jq49GPBK|~DI%r|M2GiK; zr{BU``kH38ZEXRNa9pNqD#B*YkzFa_wsaXrc+a*qz~It71Qx^VYrvqyAo4CpXRH=1 zJly~-0|a2G%P+ua!xA9|Z+j44I9d@T(@&OfZH&Uy9W#1$)4XN}Tyt*+Y!aXcH z8rU#cS>(R}wm)Kn&A=PR_uFtX*rs0Zlq$@K04S*SKadvQua-k~wCcN-@sAzhV87rX zuY3NB?dxQE9(>&(f*$1!j?C_4nmfFIAx+<#78G^xRYb>c``LW|G_UNn5B^pN+!N+_ zv(;;t2r?IWv?;shTDxOa5czQWr{ewA=)6{yB<;|8xu@?=hxjvp!n*E!l=8=3p=y>A-W>?YWnVAe%EBh5V`UjZ3-kS&Bt>8#p z#~JjUffsISPAj@x@luk zal;5gk*X=JXDITU#W$Db^~8<%^JBt|%V@aM+9D6&BLUcVompN9@*gzOEYtjgm+#$u zGjsnyTk_xIr-vW7*dHQ?R_YS@U1tiv=~=k@?UX9Hwh7OH(fJ6Lb`4xX?|C+fH3mDg z0pld*pk-LxGInI$YGdv3(V1_3ruxIKRz-b&CA-=J*u3(2p2dsel}K##uZZ+A@#cM+ zH0?xoA^c!u@#E&HeQ)wi<8losTN7YcO1^AhkIL7jxDPL4Jy#q7_&G1!T{ zsuLxWcrp-bR&wBbVY&maMf3uaxDTO*p6*z1T*^FIAjs?vY4~n6V$;G}M~E%ee=5HJ z5|G011a2ON6;Jr<}uS2cypE&sC@d_hHF7(hfd*B{d?)vy9VvD_=UoOM!3$9J3 zG*uuSSI$lp!4Aj;gC5&j=ALy7kD;ZZf4}Olp5uXh$u_@ZH*@E7B}Jot5wvr@-kwdK z(+gfjhzk4FjJRvGUHq4>N;mB?4z;RWr#8)W*xl-?l*H9)_3qwl$I-woCva0)sJp;; z2>%p_rv0fTzgE9|Ueq`{5-x1sy$GjXqA>ZJ_?NBQH{q02t=+m2qZ)G zFl`z(bG@x$$t8jAfo|zJSD&x7X;U&V{DV%zfbuP(pIBi*bS`nX1pIc;OF4*FcHV{k zg>9Kr3zWhVLKpuCx9Wg^8Gk0Hv|n`L+ISK6v9`3bYUdEYXI*=hmjS3`PsOOjBeNAtcD`))3B&*l4|1Ohi%ecT>TVOD zA{KOaYW%Kz>(7*TE7Zc|FYS7|)cfI2%i=UG3Oj2!!=-tr%Qz41|3LRj`XQW=GG{S_ z_%zZnaqgFGZD5kZQ35bDnzAV0XW3aNeb`hEwVX=d+AQ20%7DCh9qRK7%uVo~kHmUK z;Q;TaxKgMd-n!!#fu9M#mRNl%>yWz{wBDKEOW6Y2{@ml+7%_d!yhKT;!2yO%z^E5h z8kh@eh(56I`}}kbWLGnLKS`WDXVSFQ@Q^2x+wSG1WN3H320pMG5ly}Ye3dnSR`0ZB zUZHLn+zaVYlR45ej7%Jvb;5qC zold%rDrHfqDQMB`%|nN3iUGO7#X6XHP`vlNy~!NP+cp8r3PinF7>HOKu+@OeJQ0G| zWc?8xEsSS+5vEdgnB6{~l{AdgS}*R&3E~*Ii-o-w9q%m7P=eMR+$| zyA{@!jif_7z=Ch=Wh+&MR=Jo5&Y7Ed$?Y=z-FgTK%K=uhRBYqcdTrxjT$6_+9rT4& zl^kI}sg653FZbYJ(|GA&+TXOaRJC{j`XVU&ZUCIB%wu6ft^)N?rJ9!{6JlG53yiLH z>KiLpfp)7)bu5jh1*>*p7^WWIFH+B%l_d)YMlT^<@H&ntm^x7y%kklD*>*c|rE-Pn zN>$)-+g4NQTA$Bu@8L?dkt&S+ z7mzmml^XA^HyQc|!OV}xC+OdQs2k87$)nJHDX7*m)K!|88wY!z z_gBMBMbmD0JKV-ItxP$U!2x|%)bzMQM-R^0%XtooFk2c95@Yss z?l6t%L0_Z|vFnX%=9C87JxnHlJoRn%@4sDnb>PKt*Y^A*L(N5jfPT`veK2KyPtsl& z@1-k+ppPyn&mO67vKb2W+#Vi&S_67V2HmgtH8lQE^q8?Qly)B_KQS0y4|Izy56=%) zcRntmY-r&;9W1)vm7&5V#{0H3aj@iRveBL3Ynu?B)psXs z6WRwI;=}=3^=;vJtvkMSH^=11HFm9(q_B{IviA7N{e4x_iick}BJSN&KA)i30A}W~ zH1)vKh;f(KhFKx5xte&M{=7$XqVTugM0mlzK$g@W1_)KDMiK;?>Wqe?vN4BAH66Mv z|A9{T)^|2R5~;8=TicLJC9%K}*MiMso-nt^9Qse%lPRuJCZA?|ZJ9U-t!@`Ru2miyq$%6!*Adw-Nd~FExT8o3Y-adc);2va>hhmhq-*Q`N~G2rKA*Kt+Q~a| zqRf|Ft+KYR^tmTJ;P;@k&WcCdHS0}UHEX~p&UMX`^q(NESIp0rR=?M#*{qE|q&s&m zEn8q;AMj6(r;;AMJO7xy7<8li6sdYyX`BLb{T1rAHwNGNh``uHIz4U*f9Zn?BAlDk zURd;jF|S_!1piUsyb7P=s>tRd9|-G%`*p?vQ*S6{m+F7GUFh0AkdE8%_Z@#IS@44o zI)X$)k^9f?CBs?753^!UKfx{c00~})&h|fGlqg~xuI%Lxz^5J@6q~-0;S&VX?(Hmy zWa!xeB!acZ!iO4cYkAH(;x}ENqn;o6t)wnIJdxqF(70cR3otN0TpOXFrIZd|jEK9% zgvqh`qg9Bp-Q0z^-TEi|FSeW4clMLR6TkkLpQ;YVSCO~TY`)1eHpib7-@Cn_RQu>~ z<7uvlT{C4<73hKY^<4;MM!v*yrP<7TF-^Ut(*x7*W{ml|V*gBYijt&Pb&T&7rW8Ff z@RQpixm@~OW)vU8?U5h;3W7$^&3VXTr`(i1;+$RSuCeQgK>ovQfv`4)GlgwE5L0&aPWKH}9Y|hHY$= zhj)Zt9gO4c<4GeomnHO;oW@HF3=RAF6pPRu953-UIqQrz^P|Cq6@ZpG&d7}N*84Gz z;(Z3^AETa@i`KhMQfb#gBkIXorp9s>aeqL&?i-MK4!{()gyc5!KN1J-k`Py)!fsI$ zV=((x8JhLwBYMf^E={Jy$;!l>3h<)QU)BJ^k?g@N6wU}TufO3A6T!0$tPO;aGnIy~ zs!$xvnqq`0Z|hO)bZyL0shf)T;MZz);EveNy8Xh+CX_*-H1x$~i>4xZ64e+O19k)q z`ouO*qzi(czu?H1!~6GdSz7CGNh*>DD6&lXtXl(+b%!jh>M1--4x;*r6g%OI?c;szN%kk{0D3PqxlHrmQRKZ-tk_`#EN2ZkI)sFPPvI0%xOyx4l5Uv%Pj-2_Zz=b6W( z`Xdo|>ooRNnqv^Pk~r88(w-P5$2<(C2Ye+K;7x~R0{c(!<9#r(382+$mv9%a=mAX~ zLT7$*pqI&ils+11bKcvP+!u&J;fxW{U`~MX({LtO5H-*%kNR4TVoGE?@niW5P06QR zkx3<$ydLH>qTyY_pU(nOEpLMsmgE#2#46CQ9nQLTUj6;1-(lpiKSxqo;`LIxTe4rS zV`Xhy^iz`qml6(R4`Ma|ya-FQ67+Ffab7>>Ewnz3(cE2IeWwms@XVxF?_~*>^;3$* z&@3$?-cwI1SI-Z~n(4%-W|ZCDIGpdheXE&Wu*5s3xANsiwPw~5ZSKXPDm#bYWIAN_u_o@K9g zmI#fCvi+EWvEv%rZTC24xC~wW#W3cBfN>7+m4?F`3|Fdiu?hDFGO*E_*7+!}c3LHV zmS?1gEgDXHJxU0i3G>xel7ze3$Ph`OOn}$g)n9Vpme0*){o!tpjl|>r063jOm>QuR z=T(PWC#2zSW(Q*tl)I<}vJgZ{on^FXvnBTQ;odN1M!Sx8z_GD4f~&f;}OS7#WH?HU;$X*L8{)mkdIyU7Bt*)ZTCY`F2=>$uNwgIH;tD zU$S+&3d(^sFx2)G`o^7EC@=nbYBb3)<=3;JpK_@o{?A(Xm3Z8s+d!;Px=d5oxv+Jy zW%N{MYc$@q;luN7FI6AjtCsST&f$8op0-u_7&%Qqjw5IRMz(42fVn!z9WB#`qS#wB ziYl;-Wh8f$-fZ|$W0B~W@K}uJQ*tEl>$#n#0p_I)^#3&5iw^}Bda)XrmmSAJUTYQ! z&*MJ!Z$7Q=zNZ}siaDwFM+1OG0>S*UfGoR462gTMN{V)`#RqskGSiZ7kPG<^#%YF# z?^!+1_z-aW7XT1YQCvm^crPN*XT3)tsK!i{x<)xk4wWp!Y^(oafIux0AL_ic&|d#K zcvE8(%2)&&Lg0*%48#HO`vhEIAGcdjok>$I_iA>~q$_q>|ACwVSC2L-^eSci<=d8s zt;xXnCxT_&+FZb%(#s;aTL&xQF@(}xQyDi<7g$V0+NO7{&VQ+~2-dIuwGXqN_b5MG zoRKTXR{tys)_eioySPXG1?B-e{Rf(VK0pB15x!wUQxgM6oZ312JTf6*?v2Z52nq0K2-4Fsh@ID?YTljV=c);?U(e#G@%0Ct@6WZIlMx;;4|-Mzo1yeuD}tN`Su z?l&Z9ynnAi)As(w#S^F?u8%u=9(hv5p`{e_wuvw_<>rb_)P)(zh-d)} z(}@y&0b$d2zq_>n)~VE%+)DIE+)J@n<3?Gv>9m}h>EV(~3 z4iL~0spKwHlT54voP)fmDM*aNAo$((*yM;Ky<~pe*}RuvQ=%s`UGd|+>tI>1;_rN4 z>p2puWS4+I=QY@{Efd&9=Dlhxz9dRh(2cbp_@kMp*a43mZ->YW9mqT7WS6eKw3F8z zDk7nbB_#G3^jvRLg6U-yj0y0uZc33pOsa0eltGoG2poZrU+20PQY*zj%DWQcTna1^ zp>4Y`CSCn;nRarD4@{_1mkcb0KEhv*)NA)ld!XKw<2iKe<$?jEd#FTW- zR6lLlaln;gyzQXC2V3S&7T}2rG@y82?(%a@U^2H5MlBW$e+R(Rx0&x6QETjN*hUI) zLaFe9gu_l^%SG^RZ|@=@&9SH-SJ$QBe0l2Ns`4=1!M^nG;tzTs`PHgn_Gczf6?8V7Ni2he z9ismQ^C}5aL(D+8jRxxD5ew-0STdvvt%Dx>nsF*kbJG9Xx_DTl04KD zb?`Lnc!wi}E$&#HXKYI-MI$6K{yZN{6ou@IffKyn!imPSXX$Pnau5;LHT~a5Z!8*% zeKaH56T0}RfdeZSlrnfT1oEMio9KlA{#54~<)!Vf5dQF1r?%zk(}%x# z4`VJuU|r;Em$?nh3vdigk2Jh{(K{AuWf!cpD5`fW!K=t!?bn!9s&J$W7uwebd2tf} zM!K%UyVampSOFeGwobN~!PWb@7A!hjPeeavKRf)HTyENGuIC9r-g}pE>MB^XnSDTg z7{)@)0Sf#4NcaKIcH(W_JsWo{e36C#&L(IO*Kjdt>C46(&w?N;v(N(;dYyPl_lh`Z zT~na(r1*fwo%pRso(kV4p~kbFY?iPA*uBdpxHC_iA&+qbQ8!?mo|tVo)h9>q*!kg_ zuHf~haKpj0cp`Eg*<-u7zV6*M52zpo*beXSbqHJF3VrXExDoSpjfdG_E|k$@n2hN+ z)$nc&u!kol#yq}V0T;TCjIUOJL@B|p%y_;rPjfQvbf}k{yveN`sj2uzxR3otZ6J_9G(D z$S43n%_Lu)LAfwX*iyZM-o#Ex_UAH%BG|Lt|J?_$PvYQxbY=09G#!Ry2h4la~{gnSlDdQcFA8o@=Ul)HZxt&DQ0 zYtdCt-KVCLtMMH~b~p!ocKPBFs3}1_##Nx5djQ`l9pVh0WpeOI`_xIUa)aVSlPy>2 z0=tG6tPvD%LMVojpP{0mp@0%OZLZxKNV|ggs*hwoJh;z_9uYa>>o<3k;MXrzBo+tX zOcu@;RpDi!J+Rdc>P63WdoRj&;bCI{UILYIt=KW69@|=ic$C$;^gd!JeGySe<+xrt zg!}|{f>1Lr*<)q;Q=$a~PJ`M_?EwQX&%9OIdq3ItUs5NQE}eb%zI*S%nEs2ZTt+E> zc>Q)Ca_NF#*V5RDhYYOA4Gw}?8q}B>EFY;3rCTh2yBD$az*xP{x=@;GE2EV1WVf_(xY-`jA#FblWxJiO-@>xpCW<6ptDKhRtW$8tMz^VvY)^^W7|-yI|NtM56(yBC9` z{2^}&-yZ4e!cdvQe4mV!H9vxI^mh+ec8sq)QqipB7V?oYtn#3u3CIkNB>^D@>c?{0^r`J821$_^+SO&i4)xlo;tEm&- z`|P3n(rXa{qf&h=bZt54l#PNL6RkC%Qv|tMnIq(1AD~grh6z@`cwQB#*8F_JUrQi+ z4QCycF0*8A(EM`P9(QKo)U_)wC{Tt>*aq|}J>khrk_XJhyC2gA0iz!oNdW|AGn#|% zVb)gX?YQ>gz$@?apTIKG_Oko_RgQ|qdKgSHP%B=IVA82Fjb7Hdu?TU z3ur(s?;X<9!zVyE;Fwcbyi3J?vbv{SSmcW8X{k_bLodTy4ZWRyLngEyXBQuB z@oY2$SJOA1AHk!KBx5|-1;b5-1^ttnxAWap3jRgj*$q~BX7-SehjWW6gszTi^8Q{+ z=MnTZU3p=-r(JsJ`E;s8i||+VL~KTUXn3&WvK%K%s8flB7R5{aT#0@eO;*WSB$x7u z0-Jd{1csC36;kJpVTzKhde-Fnu14;Fi*O2r{|-W0z+<`L_da^#J1>Wp2GIAk_%0DFYC8BcPFBF09@y6SIN|x5fd)fUmLG8fOb#HeU&@@70_N`wz5+eY z6Y0XLA4JgW3U5TaX!4auWeX%|g^R8okgI&>YLMU6m)=ToU6JA1o;WK%9`F`@xs>_* zOX1|Gxb6sLx-wf)cteaEZ}EInhi-Iv*~PwF{A!Ta%@X4;OvjHQwoR z#q>`5y0?0KvN;{h-o)8uBU$IEX=t($TVh6cKSrlB9_H zg6IMhB^_h-*KzY4zwhs4KiBmKSloB!S-*~4z&m-~iOdf*B>$?LJ~d>pRXDmyICvL! zKo|7Xh`mR){yP#zSSFwr5d;w&$AN&xSz@~aRcG9K!d?)%eddjELdCopj`Lvv;82zP3G>!q_DbW|6ZuBk)+Or#A!X_5=&!?*5-NRR|0k8$)H8>35nwrSe{?eesobZU7=#@=5(w7A*p?&Q zw)^Y^lo|>OQIsAi*~rKqtZno8sV$EA>wloy!?ouIckG?h*InNW4!u>cpj^H&=Dv<3 z-T@!DuCK+SSkV8=D&)byEeMNydIE%x_y`Ui?d&Q*-KaK7;iJBX=(;>K}N*1+9PdWz`9Vi!N4S*itoDj&Ax1# z`!n4s#%-UdvsOZCW7O02M03j&#?=FMr(e8j2>l2De6YM1eI?o~|5p7+#Jn|WVHNgs zt=6_zU_JpeNRnIw>7RX$F4#oHp60LxXUmtLaPDyKgSk)HjYrZUMrt#mrAyByH|F2h z2uR*YXSDbcZ(q_^$A<)Ltt?OeOeNh}6dT7%PWR2cbyI895ZYiMl$k82Qqy3??NS8m z@~Q9nu2%LnYxVi~cd@sOvqeQ;@zbUTAh)l-IJS0Whf3}LY14SMu(b$+x^9e8RaiZ6 zkQzOUy=pHw2VNf+AUb1a<9v}pF)wrzed`|3dO5z<+)XaLFk5dG|4G(qaM(S+n^R%^ z$Un@UvAZb)`p|)~p+h^wY`P=QO-$kK44u)?cIP$Ht4XplkxTOy@tw=!LN!0F-SgwV zpCNLm)NOYPO}e&^+0h7A7&mba%^ZvDR40!v>f|y`=Lg1j6lgpkK&b z3D9$mpb<&r8;yA94|jFtkmrWEDfu*frZF|hM*`s`oMGl0O1F2|O? zXP~uUom1p(BJx7I7n>4)Q-DX4Kj^b3WuK2+!eow%%tv_GgNs#chnw!lp}P04XW0N;SC!0{hyIb3Zdzm zCcS`(3sqocuL>#r{thRNgciA_G#Xo?(MI>O(iG|#Y*u-6Te<&<#SLp^3(C;r_5cX1=_DXpP>?o z7YyWvJGfRU67ZICCSR1Vza&F09k)(6|^kSUUBO^n8AEVPNpk`6Drc2YfE0@m;5XzGF{-~shlbY zF8}ezy^UCcPk<1CE}BXehw4Kl*oa-+%q-X=->mPP7J=rfxxW;a{W1O^RetI7{zd|8 z$6+W?e^Nf0YBvCWr3(k|9lyTJdC~&lEH8KWkAq>>^;v)aA!NH=F`aG-;9dYCGobr? z11Ok!LTvYau}-OOoqUZmB2xjEPpb3RObm{B?X1N3{XVEY*vnTzIsqXdmYFfAg3QE; zqe8?BaV#>&X7v1ClX6bTb;V}-pGZ7F=4##wx+yd7O{NpCB0k3=BdSkFmAc3p*(MVw zRVD`o{wlRk52g17etqOEsXTzj- z_45PYU$g7-mk6~5mWTA7G#xN~@-nde3+}mM(6`ET`My(~q#N1SDL8SeENWNSpV{8h znSJ);lf)72jWN0n3fn@}z+U(V6i=rj5nxz2f&op4K3-!y5}NBN|NM(z>MxW~CN(VJ zRg|M3yYQ_}LEyif2heAt>4g=r*Z|os0-w_TD;b^Z73i|_ zD(>&#x%2P;Z{ihTKM~9dN3?rQtw(WSj0BEc<6dg*uDDn{9lS1-ayZ1vb+R3gf$@=t z5%rKBV2&4jAd4%&D%NqFJC*rjWoWzE2e1=X%Y*7sDUPXkv7O&L-~hx!VlN>)B&J~x zq=L%F`?%RR$-^?5Q}&%RPK!#Bf`5v_H|BVNvVV=(gpVQoBnsotdf}|lr`{(QRR*RL zP>oP2yT`XS^lv4-WMBh{y*jPf?!<27SubD^b&&l7EF9R&v_Fi7#{t+PyevuPF)<0N z0yPNrlsPx4ZzNQBdX*5b7dklqt%4IJ!TVwufe+xCweU_A5E+?!7NOiS$B^1zWxW!S|283 z-u#+=Sh{}ltA-rc?{3ZJa)DaJzB7(_{fNY(M;0d;;48^&DE_;6>NcAA$SVv2g z7(`uh2iFBglIci6&F!{Poz(buMgHZkt&b3DR&8&b<7tewjhlFVO~ry_V4#8$(SN*gcHv(zcw67;F-8SW!+y=7#5B}VX|CzqkWfdsxPkq}}N3B0n}^cP4chRTbGDa4FQ5_+ZQOfAgJd+ z!9a4NC+KkN9|>Fa+X0l8^2nwWWTeHvt&kA%$7!}Tz5acK)^?F>37yuWW^-xb7km2w z<^9kpt}`aMy2%#nUn$y8ycyGBZj!kTrjjU;$S&|UJ6kp_uj3G(L~(wB$qc5+BU~gr zgj_FKtFUWGjr1+tz2~MFvLfy>Vej?h85VNnrsNH*D{J!~B_*1`kdKWmqRA z??^j=WgTczq!oxY#5!+Eh;@?0ar?F7q9h>LCS@=hk!NalOeoy*dn84)0 zAyftfjw@=n@RbX%ANXr86f;&rnPPc?Bv?+tt_1yLW`{lkl%PMC^jzAlOLtWBP;VY* zxqkbU<=X@n@<_9O6)!QAb-}CC4_TNan9XVm@2&k<>w(9Z<#Q6lvG5P9K!R7O?DcN_ zZLjxXk0B~f=#wNPQH%!3eu%1Z-Y-DgMj;qGc>#}wz}`Jcc-*Q0P7gCrIJ+A`5`X{|)h=6l?Y@w6 z`Mj-QV4L_`k+NHouPJM~gf*T(@fSR#`#`SBgkHZaKSl8oCA%#7lk=6^lm7$t%ehOW z*b5#1D!zYBuA2V3?1cFeFkR89L1HAz`+7U${jt>u%!1cfxkZ3U;JEUb&qqsGAqgX} zctOx3h6hNsvtK+8awC`|?$EZlj70IGhrYc`CaQdUVK~qJ&q#+%yNO%E4)*H8OyqI6K46$dpqwo&)&5;dzn}@;y5?7kdkJckr-lL3)PTy<`6@j@?C1$Teo7Rw*r?TB1N@zZAbD{El+Ax*Lyn0kkMgL^QV)tJbG?6)r)3{z-sX?SZv`q3Xp-n(_C;cHD78K$>w z&5VM3porpPi9kSsC*2F8!SLsx*VuMAY&b9cR>$RsHqlBs{t7WJ0M@EgAg^6ZMJ)Ht^NoKKVSY|i`srMC{zxpGet zzL1?>_o!$@#Q&j^cq(B26A0Rlzn~%0hF!ddLMGob18Myak^K^Z@igf*Y4N_H?ni?P zH;`K?dkdlhi`%$KOeWg->o(Y*a7C2;6VUuu^v^tf;gqvj`EkyNAoaig4^v0eS5!mI zIa#l3jcL{=zDu-6RtdhA{}E4Xwk}-cGB}%$UvNk)l@C5D)L_9v7VIUR(gD zeWj%keXkz!SsNtlD8@e>_5KfBHBoU(Fb}WAa#PBGI$v4gww7VbMt%cbw6My6GB)q8 z__M^6B4?*7V=tMQH9yu|*BbPG_Vjr^&(!Qk)>P0SFZMnEvZGPRx;h z={s~O`!sx3JkVKvE8!j(07TIpjoKrGK3p~OHbY2CuYoh@-IDlI*jqebW!Ut)G5P;r z@EuBLcwqq5^y|ve^K$r|{oNSB z^GQj*qDbGQgy%O%@yWqD#^rRW#v6RcN(=dWU=e}v)lI> zd^8(0KI-eHDm@z0kPyGuSGJSCUS!!AThyjs@Lmpdq&1N($8zzFb3Jj&)Bg3-HWhHt z`GlZe_?x~Ey7ZgkpSpIY*sj&^FiW~qMjlP(c+}|xZz*$v11?PB9)A^yOl5jdy4mrU zY@$M+57AGMJE`%~3vcF$nWpTjxOqnZBZt{2*ZsC+Ro__Ed!5l#2*#2dHr)6Fw6Xnp z%y{aVG~e%z5(D0MKVR@uYwvCV0WI#=8vUlW$HBCqMo8EZzPgz9mZ^B&@zi7sbBd7& z(b+!*)A|7bG&)`>H-jr6zrVZ?m2nq%-hJnL{J%%bba%eLrz0Td%Th?9tyqe@9bI2w zl~V@Ey}x1s{AM^2raF6_*XuVkIVFMQUP41YW|i1I$vFwFC_g;i%P`&Ct5N+W5EzOmhvy^VGlKBaDoQrkC3`*!(_8~oHa&s+f;_Qv`_)~do9*UHu%aL^ls z0;!szO!%)d+sbX0+QDF7uWQ=F#;V7Y4FS&$<@LQ+b@rU&!0xtDwm^;s=2Q9smZwC? zGL7i#cCHwmbZq@g>EI#!gF|%~2$>9b0Q4WgHYEXim@u(~Fh(r6&>-dx@~h$V{Cb@$ zE~nGbsL8k!2^-%9?w0!*D8Mb%$0qf8cok3vJ9 z|GL4Zer?)sVJ}tH3H1ciY^^w^#>lj&Ch1}<^)4&zLmcSOmGJj8PtCEMtqOZs$0NT5 z)UV51`NCA$c9#el_2SIahHQJxJPO^E&-A;x;0GRH%C~^I6W1$W)b!_G82Kl%vr@Dn z^-tCq;oZ;S4k+HGe)kd=ewDfz$4t9i-RuYQdY9=)&(cGQROPTZ~V$rCleC>zF7{U>;^>A`<*hcg0u%9td_0-eo;T?&~W8I&<-Jm zdSySCW-))wR>YsH>B$4UVI<@WnAKU#!9NIS%Rn6 zoBh#r$Vo*!^mk_WK_2!KTkQtrT#+HqRSng5gO)2U_P1U>oBzP1YR|O?0X+B$lX^B| zg|B3&Z)@BU$<*a713#UGCqi8@ND$F$y}wgxnyMJ#MDf|_AS3*=!ZD5~(mabikD^m| z$Je1N$ybQ*@+C_*Ie-uy6vcRp&#F+$@%qV8u+Kyk#Z)Dh(EZ(vbUUgQ*DGz~qxnCQ z&cmO||NsBWPBz(2MG@J1os-HA8Bu0P_Ff0aIU$riLI}wwd+&WHaqPX1DDxci=#2OG z`uu)>fSc=jU9a;R&*$TDe+<|r+0w$LY{u+1K>ZDp`R7i19dQ0~cg#JeQqb%Rh9=!61yllU%e5&v zoPP}sArN!E9(8_oXrysS?9lLlFZ@ULQsLU~x{YM$SQvvu%-{KeP6LaTjU0UyKbKic zeQLFRF4)~u5o~GPug28ud#st)nKI5e1?K< zwO6H|>8Ta%%X&(u7QV5sMh-D}`w*U*qmTjRn$Pu%#kfU&UOxP3Z(|;rC$m&26D(pm zv*GtloK~!(M*VqbzWk02wXvHT(}gHBX0V$R6Mw4(ERMaGm&1477dx}2-!cw+Q}GWw z2A(#qos*5TMm(Ll-BH*5Uf(g;LlV0C1o_*pj}IYowTtdf*|23m^gV*}!TgB@okrVG zYCX)Y0*}qIBin#Zi+^=N(!(n}VZRtvi`9W6{}I+KD;Q|H*2K$-F6SVkQwgu~45LkK zQl?moWu3io_zY;wu;>Mh4f21D_5Cl|b({O`PpscZsim z=o+yMOE}|JiylFm0DgbcRN2$=$tqS%ymd`kyb*e z`+B1P%_jhu(#ZDRh@&%ov1*Ot!E&+g0wqX$OZ1zOhXdqa{A`*SKRztR1m>bg4vO5u zET1;bJoJ#JNaKlf%)P@!Qk4gPtB||rcK?1+5)EzI@5JkBISE8Wt_Vsa^-KJpFA+Xa^9N{MM7BFvI65u>Vps6zXvpFpYf}PW~ z4ShR5zu)#(9J#@z>*?`3@}^l;%OGU2hI9ky&kb1g9Ma+Cp_K!H${d5*XT-bu7FXY1 z%+OKN-IpgFCNihj%bZomkZ$N~t%eRjm=JF(^{EFEq(`wkeeZp&f_+fGkw5s#kx(TH zexK$lUE@hli%Y|=ohq*W>-5f~Tfqv5H-9dgncK*2F(Jzs)UZ}gAAsA#V$MZp4AQm% z5`T9Z_QGfLBifb?YIDe~GD;=PuTth0-Fp)P&EiW4noC`^waHmNWd1JxrXyLWG$%>n z2iT4$=$*`!`4FhF=OfGkZw6oG;HM%+cY@ps2v#_CM=@kM_P$5x0mMC8NF$W?E{)lV zt;_xH}5Ol zlyd-|wt)3=aIw&bkzj!I>oYF;zw^3^#0}C9_$}Nh-k0!jQdOe}M$y*P!q0?ZyL>q1 zdf@mp%;;&M)K{_hsE115dzSi#`}12eBS3w&Tn;TjV4x24{u+a&#E zsF|&uDYMth>i6XZU&kJji0Gco*IX4&L)>Z>MmH`GJusmp_Lx|fV{K_SA6%Nw zTw||5TlAjubCp`V=+nD*IGGoM*yA7s)|w0NSvVI#5EoPlg}d0}w8~ai|71!ish~5A z9)47U1k;t-t7)-Mz@8EtRGBxEZs1sfQapkFy9=BHo6&G9H>k`?D}0lPZT(nHjVakS zMe%9L?bW!DP{=6(w??>&yRz1ic$7!B|v=m4&{ zF`vw{kH6AH{O%2wR*vjudimJ|V>2(V5%U1K&Kt}n62kYVC3R0(P-fR6 z_HUa_Jr+?wjvCd2rkZWiN!#SRnmk)p62ILZyJ=y$QFlD(vXIw1w?1GP2WG{^msb(g zUInm|hNhc7h!v{{Ld6NFhmsKgNY--x3OB>)8nHi_Q?_5iE0HN~SsXi_Fb*xHCKdB+GDhyl1K~2z|B>9P+djAAb^xK{? zTU$M!R*U5DojCpvOo?0k>bfRB(MboCM8&E$le9a!Vc;sV1S>1eD0S1@ z8*l5s1c(z+TIn-w#;aj}yF>c3#DP%Efj{ewjQw2>n+(O+pAe?+A^cIUCvI!@`O0})NRns!6 zm{LRKMyH*tB;9h43O&1!v_i7U^q-Qg=OVdiac#*7vUJV|@Hv#{ zbfJ8KIvB@5e#8BQ3fz;&!GSio*nRWv?i`+?Y^OCH0i3zw^=>&_F8&eJe!vWeswVwh zs;(?lh=%b9?9E}6^bm*ab9QDOQ6blV9=Ek;H{lr(eNPE$8;2n4CgL}OL-l?(Dz-Bu zV(9g|U(J%r;a$P_;tyBm(~1>&D!3n*$Lkm00WlY2FXZOYu`H2#6O&{;C3cu@*WDgz z)?)st3_bdKunq8@TdFIz{Gxj@jo8Sup~6*RJJ4POkmZ3Puv`a_6bf*$T(iY?FWwPP zbyY`ZqI-Wq)AmzrwSo25z_B?AsO(^G@VuuESW9N}@8!V0nOU!+9_*q+gAV35k3V!X z<3bsu{8yhG-k(=*Nie$o^m9P~T~hi7A;p~zz|YG7lfw3Fb{AkOk#kjm>Fo!dHIi}t zWSd2pFxTqC0^6x=-ZcbJ0=5j@{J?6zXQMiM`2G8CD7}DwhI1sXQ>g#R7H@!}np;I73}rQ>p@xnuI-o>3YnA>8Ru7(G z{`$ES3-z%0H&VcnxOZ50N2kd7V0?H8geignEq|I{@P{!^XBlHzj5eb>TXdrS#>*%V zxAKQuCb^zSEN5$0YKLSm4wOnLjs0y*u7XjFq zzAhOP{5@c1KrSH7`V4^$6!yQmaoaD-eY-yD5)IP&p(gUUD7991~p%XeL`m9Z^ zhZZ8D{^kPlS62G3Kq`p7`(QbO-6TPYqrZm+#vVoB z2t|+S9uM*>WFs>AnYn{gKm}xpgRHjDp)JV!H7U%O7JhncQncLX`{C zbVXQEBnYDwLKp##2nhIWJ|~!gz|v04I)d}P+~el>;3-!+YepRFn$lCNjWxBwCs)++ z@3b>6@LW)15Qo&5Qw^*{JXn>F$Ml`8g#{GxZ07oE=cD?eJ6z+wbWF+{=B8W6e)ry< zgKL3*p!+v^o=G%a&p^*lTM$ zKIeVPQ(#eleOK1*h99X;-`CVrzd^go(Eq2TnT^&n`8ixmXPX1A4`P#!U|c)R*@;} zy7uy1Sx%POBi|yYJZMQ+yHn;pQ*kPqW8)<;QH~(;UhvEzniJJz%<+txQ=r@B-i$^Y zy=vqF>zMw;=tg4E>%6QY7LTh%UvQJIn!(A7wdM_BHOr|Cdb2er8f?8Ji6vyR_10wu zp-z_*(3A|B#~%CAw~hOZeARDLAKAcW>s<43R9^uNS1Dzg=SiyUln*Pv&J^HlE6F)C zR`Hy##Y&;Df44b1Im=RL}v#mN%e#;HD~7_jpMAvVz0+;q&A4+9LG*l8zJ` z<6_A$k?Jy}^DRoNcN?j~3ob(=VVOCQuQsYcI_G)pLpc?Wp^sa4=!+J`X(HuCMtgUoZAG{r=@nG;_U{n&#RtIe+jw^{)e2`ktlYcYXI!A3xsEWqaO3K2p>24IKdm!St~l z(N@p?_wYwQ_-6yEdm5_7Ms8DD|1y+IDzx+nY4%I$b_-9K@he^d7N(zkKLr zMF4jYc!8L&YKd(D`jRy+>r4cjR4W|g79#RtQ!c~Lm8*>Q#!Umf{ooc|v3fc+PqeoI zP0fVuHJCcaaH!_`B1{t(TBawS8_FrI9>6~u_aHE0!XV~T0O7;7Ipc;zG(Q!t0nY?e z!zp1c2c%q=*w+X2?}}C`)${RMM!))I_sjQzl9`QqHRQs8n9==^G!6t3cO>|2jk>OJ z$RnqpC2IUfrqlDzU6SVu;hXk5$F;&8dXz4Kk)8L&&2a?Z#Y8zH8lGl?5e9aw=)d*L{CCT|yzZCs0g{i5UbINGNdb-i4aWi7I^N$?$X=+4IZ@mz6h?MruGr=AL4{C!zE_tWUbLJRdE{r{LU!|UAmI;HwxGBc=f!Z9=FyGtR`1Z6 z$7WWEpNS80a>(9n^QRl>cU`lJ4iJZxSoeJx8SV-e8tfw9>_IXfCdK?C#&gFfH)vvi zO5LkWzv42ySz16{P-CAyCpX$WH_RLKo!)Zb-_!Q2c@u*VHrfUJn-13&)&Cg!*dVPx zqsUFNJQ*Gw`9T_v!vpeTY#AHZY4*9lXL`9Xn+5IOpk0Do3&)ukEvu??eTjUdDRG3orP0C^GP>?u&i5b>&yi6N@ws*Tqv68)St%~_bgTN zuODCgfMCNNYC-Lwn`q&M<5xX(9LVLriMc?fBpP?M5!HZ|h_W?q$$43e5T}2k$3$x zhOMrcC$XgAfjMr2rs}bB>!p`Nl*mGVKe5pYN@=B|!T7Yb&Xyqr4S}wLv}oalH_qSyV3Lt zTG6}DG6IqN|B*Fnip~1XWcChugGI41$YWOc+>)u*Rw+9c=#Tz zjfowqfrTIgBPH zVeigwfDV8oHC;25XGl5d@Y-gmhrOH9V8zi7Z-p2TLZnQGZ!Cac5vPDRCU{zV4cM~7 zl`OP2r_nrm2^{KEva=d(Q|Q)He$h;%UcHA6qfy?u?FDLzAcvJ1&cA`MV~h#P41s)X zR0~8QFF3vp{&4yB_ad<60d4vTjrc)92yx9#?_^I{^oKw(ZXVdAxrteDMwsb*C#2F6 zH*9Xmu!jV(;{bpM+Ma%ae8g-#-a%tcrw>D9%6UTrVR-R;dR82vM~F= zY-`$_sd>{1=4)*~HFkrl|IAkyO?(>{9@AGHY?_mv^f+CF?XP^^e`G#Mz>7V?wQYcw zcGWRSaXoK=k2pKO8L)ei*M`O?uvjWq8M}SB(^7Aq{vtR2JIML(T=y(4-*Iylae?f_ zBT;_e>VIy1N=5Z<-$lh?|sZY?fp}7n>$#&3wvTE91NqJ?FJD7iQhbGniS@$ zit3st9#Y(D&|;r4;D4kf)}%9BEmA{ZgKQlhM?hRWWb`m*aSWwf2p%XI^zGEn_$7yh z+^?myUm({nC-0@yn%~B?A`ga6=3J%zxRX}%c^)?9)4PIdFZ;*t(>o33-JQChHl%Q~ z!sCpG0tgZ{q`U8vq>D9-Tw}o00HzK{uZ88IOzwNtxd1B(T#W1}3+_2#6}cD7wc{}E z8}|;%S@RqdbU?Q?CUN10OTfep=X0(d4|RiQ#1;uojq8 z(6)@_HFal9=xane(YdEKr7~;dO{0t z=*o(m8%u6REX}!&Ym<1%bk5|?JhL#P7b!CVM7Mq$C1N4qZKGVrX*5={;cpC=En6jj zwF=&kuYczqlDa6Dgy$js*vAN6cwl1=2C&L12qvhPIkvelz=Lo3NWlVz?XM8z%a=;^ z4%wKu2vR)uNGg&eip$LwwNBoBCFZDpud$Xef=%ZX;AOT^z`S$GfV5~0qUfQX(A23> zpa5&Hr+s%JS*mp=>k}i6SqZepKIm*)?=)yjx#$YmC^|Xt&!Cz57 z#=F{Qi^6e8s?fmd-HrKvx8wI9)+2%~f6x8077&A~3J|2JhbE;32bJo>Y__Zd#50-U z!Y-PB*;b4;K@8yOBKRPr`IoY4j%OoaAO*|a-q@w;XNas>brJoOA9IUeDsp4^y|_Qo zzTdDJD)TJw=g}GyCf&^$HQKn5P)faG2I37Bl zyYQ{h-E3Y7`z^wgAF|!ahrf&HFThaMUM0BpN%sg#R{(u%-U~MCdskRopn- zsfV*#kfwX;b2Ix<;rYx2{X*CyjAmU;NqC9d)FZp@smaN;iQ$N+ADAEQD%wPMS$IXP zQO&@~SK;IBwH+Ok5{tI!74z-|k{p~|rDNU^ z_6xmZ8gFjxKTn(inC`FA4Vw)h{c)0Qr^f`gO0?}@{yR_uDH44g&D~qy(SaSy$PEy9 z1cEVqA9|!VzG8+Tu-`b|@QQ*dZCLxEGb5YyjmPJ=?$u4Br}G^==-M6Kx;rF*^F~1K{H$1o= zlMDs8DIERucjr*L*!Yt^x8T>`+}y^7T}-~5v|efH?tvL`$D0@Bm`gxidZ@5zlUusC z#*{NCxo6?-8h5{(y^ z{G3lsT(>WbbLq7GHDRnVN}z;3j0Y?5s1EmcvnJM-S$6et80`io=3}RfN3q4~=cSXQ zxqk|=e;5=vm_I4ffbtzcTYTj<1az3$O;E}ISr}z z@PzpIBB!>(ezVdgjL=Yco0=~nnu3N_^)@&T032I_1>5tk|Hz`x)a5?;`McyfZy9UB zI8d9mSrCkVg2LbuHGxfUL!%|GQ!TVa=dP>FlBMl%T{f+3WfQ~K)0VqEPk{W2mKMN_hnnAd5c-z~xedV>*+y~O4)?szr-#$+o1a)L zK^wNBKACd_uJD+L@t?db;?H@zfVgS?1^CiDxM?gm=sL8+1}iwdND%J8RPHy|TGUrJ zRR1V{HK4XJeDBuXG13n)n>IA#GTnvlq(6ufz*546mtHQr{R>DeFHoC}kFQf|v99?7 zizjE!f9%$TLB)4-!|X5}``zSN$D|MbG3`=39YX7@+VQluWv?M3J61myd_9;6uhNk7 z-HMIvnBfFjK5>qer7b>VHW$aogQSC08r~#iCR$<8T4p1^5BN#CVC6bcE}K;XhnYIG zJJ0!8lPZ--0VZ4S5VGGh{)g0P#t#HJk20Ibme1m>yS0m*>w|(rKcMA?5q*#4Y;We{ zgoKce)15|zYvdar(2aWfx(_&8mOWm+7Iiu*D9LE%b{%e55Lp(>9heBWt16W7T#D2A7!-6ki#NgTU%bW6_vO>$3{Zw$aa)WZZ4KxA@iUOF z=F`HcPs|COx&~nyrmSsZ*7*>i%B$ZA2up2=g(Nc2HC*AdsWUHL8=K+ky(NvGcK1T- z*wTPSl&R<>WUDnP8bYC}f)@q;$PfTE1CyWCL)D{jy*2OHy1C<}$uH+4ii%-HD(Mf8 zFaNG}IbC!ETbl=I#P(*BnE7+e57A^`?Rw^O7GlK<6QN8RuPnCY6^7Ot zX<{;DBj1}e4Io1~@Q-o(&u0h>|DGHJyEdOM=98R3wU=S=)8~Z$!RS6^v zZenUTnfF4!JlW$g(TVYv^p$xocNNbeE={E^RN7=NVCSD0p{4mT{EpwS#YfNgZnbGh zC6vW(jzvm1&kvL3DlKA(?xFO}NR(_F_e!yF?#-p#Z}$)KLT)6;={cOrV6%6A&wO~& z*8RC6llxO##+onGDz7A8@X6ZmP=T|&$~ed&;~~ws zzNAi(>u}&`YRwmhYijh$Dy9ApsdRYj4o#=-8*^!Ka-l-f_3kLDhP(~WFb*20d*f8_ z)#^3Io6)s$2Q1xt)1P80_!sRcgYt9Ap2l_nAfo0n<*KNW#Oc1Vaaq9_SOG^2+GqcJ z{|q+L+q;{sD;7IK_yf%Dsqs_NY&U-b8zZh|uG@LoSTH&AT!|fX;Ys!Xu*HE_UIK=hVOenI*>8C zLI%Gwz1*ISNSOHA_%3(lTCePH)6rX=d5t699ZA%Aq0_N6rnlU8%c#N|YwWO0RSllh z1*NOHtQ6jZ8{4~tSQgbK(z??y8FRmJ4L$V=4KD3yv93_mRh zhW%Ees#M3%FK|#eL@r~FjrF?)`@ue!enYqJd)%{sa8FtfCAggizzu%{*4;4S4KaS; zxH8oKB|>G!O{EbhHeEI(TRp^f>^peJD6^+luH19T#J6l8h+)5BaJT)5025pCHPv|y z4?z!xW68i?>SB8CmqFQoXz7z>X6EIv;2oZxn2-u2*-{;-=D4sb2Vd?G^(~?8_8aU} zEzHe}*OtC1K(#HwAgpsnF8YBjoe4qWL_8tg`CrX1Y%GvO$@cH|w^(5^0I8GoMPkV1 zQ?r8eG$R2nLlphFKLdGVPK))v4!_9s2KV^?N2dFy-|b^ed9Lkd!ycQk;j=)I2iR7j zy!@%kpQKk0y{QzzXDKUe*30#Vce^WOb5u(OEmP{g@OO<=m#T@i358 zu&!ObV;W2wzTa-A9k&v;x}~@R>UlN?n|C)jmVD!{NP59{aAjQ?j3wf z6UFVqHEvZ_B{QlvSWmqtc}-0#q};7*C(xI(y#F^sx1n0NcDj2(i0(8zad+WGn3RQc zhQ|23np=sEmL-3dW)^7A_>)S`)8o7gxLVcY4Q z%qtM>>qWdALBu3NYamQGgPim^oG5ZIRmKx(7xV0*2I;nFmUikiZ&Lr^$6E?X@4+w; z;kO7`54)D8Cw?*`I$ZojX_vW+s$?L)q z-Sd(V-hXqS38CHJ1nQt^ zT7lnhuISZE50ufw%v~E_Q;(5n8xSvXo-KAPuk=bSEUU_SLb(=B7{glb4D^W|SYU2uXZ!cFM51-RtwaWi zJh=`wHsPvyOe{n=Lt+8FO80Go%48=UUTLpedYWKTnWJy=e0qCHc1UqRa>*w=V{}uS zbKK2#Gp3g@5<%vrstad^%E=;dx=n@U`(V@WB>&yk6@#H|LScBeLI_)WI!XbEe2WN! zG0ek-{`A}e3*t(LY#z*>@pz~lAeNQ}bgr)~$)A6^L%QXu>tEwN(?r`Ox-)Y>IYTv- z8u8a^7?~fxext_;5)%Ys@0P+P;Z<}Kjo80ycN}@`dfuWD*cZrGpK8rtJia(5et|0x z+^6AmHh;mw81oNatM?||Reg%&zU}>(`Th(lmKe7dH^f$8_N`rW9+|Yv3M5EpbZQm< zLGXhG5pVa}{h&+uUEIeN}DqB5_Z{%7;8ldqMqLW};$w=a+*ZlL8v!oiM{Pu+rXooztO9DU<_g zbBkE2pHWk=rwgOpGNj%EY8|uRQZb;F1G_J|-??Z;l=x`JlMdn(4i-0#nQ+4!sQ&!e zkkSvGA@d~wMB1ERIGE=}SMtr^#Hnjtresv8Pv(3ZkVT;lFF<0sQEST{fTG7y^&=S> zV2~+-dXdXq-C=X{jD*#&>?A;^R-*n&#^Nn1dI3mSlklSOiF_)ATMx}&TA!`TT;t1u zrMxtE$)(&+wQq0#1Y;SzWps_6=l<&2`)FkNWESD(wN$7{%urP-{hO_sAoKxXag`W0 zK%evn+R9G-)TB6(z#thI^n;!H^259Nh*?Egg+l@Sb64si+gs7eqN;q_OS!fO4TaV$4PQh@tE_)m6H1|A{vnaYpWr^|5vxj^yYWcz?<+{% z#h0~*xLLeD%pChTF4w{~)@^f^m2ZcRMr=S(-Eh?;)n{VXBr<3B&CI6<8q+1mtAO_G zT`0%E`Ue{}T;*mcgafCkGU*WJ$UcKul1I{#r36trs$G>=HC4%2bavJ@Bww`$%r71R zN;L8fNotH7dcFBL?mX!l&T9j>v&P8LHvW}>E4pNu=R2uTWbE9hlBT9KKo9EzIT_1c zEyIwccG#un(*|7r4T~DbkKdW!@kicmcMRS0ef4s$>6jgd=x24FBMHn*EuJ4fcUiLi z{D{?xU7QtMM4nXalh4-UmK2ds+XQ@dlc7tCxV<(0u4%F3#(mLSDVMg4XMr~F%6ElS z?c0);e;mA?AkkUbuEzx=p{r9Z{lj0Sr+;EzCrin9FmeX+skRt#4^W4!1-Ka9mbO?3 z70w>&kaxR}U%{J(B7nM!x4vn@cSvCQ-rE2c`QIkowU4FZ<4=vWkH5r5-AitebVyan zYfKQRTk3t|_hsUKjEvGAAwWrX;EvG46^5CqttTn5@+Z>1YOZqwP0zGDq4+OnISH)c;IP z7&ya`!HOlzk2A%#qo2Qwy5v7QFlYvQ5#zck2u3(Jyb&P;myXtR-kj)k-11m@{OGEyFGARGzkwP40%X)aDH~E9-Kh~Yc zo?t__7y&em>-u#-YnO*`3YG)WtPLtZ8#_4d@T%VP;dW;4fyl0xXTke=^YYdgYY=sFA__#tTA+FT9BbFlsD$Un;6nXA2R3>{s|V zlZEmnSwNPt<^fO{Bd&=~f>mD;0*FNrmzsp237Ci%Wh*HHE{aV^PoG_nKB6QRrBSA? zQGfi9>9R*g*>huKP;^GV<;e>mhp=Ib@%@>8;PQYne^Zg69q$qV+OkX3wpd6?9!AQe zOGLH$*0?dHG43Uko%AJt18-%_ztw+CN0u_=C3>1g)~gA|jRk8R?LMogXX zxLJFbxlF$m`Vh^vtMmvu^ycfo>*lNZUwgPAZ)_uM8^y~@UGurSnO;I8jXX^tpOA2-}`8|?&U5Je3!{(OwvT2vorbU*K&&+fgvN?)dU;#3DV$#pvbwa9wW zfxSFf#b`=gBTZc{F_acKE5p`qHzq8m$%SR%@<-jk{o4VtWZB(0yrb8;V8y%! zNj^~%{lgb}k%o)I8^WFf6a&MBfmaTjHMGzjHPDuLPT3OY(hk?R)t&flX_0OA{V|`O zM#FDu^0vH0^ zWAsbwP?nl}dp}zr_}D+E_h2w-R@v*&W6PGqM=7FJxWzm<5u$Bsr6nWevO6W5YMP2Tgo#<(6C8%ijn z^^B}CAI(yW(M=xt9+lO8W9xYwj|ZcR`uhq@n4jr(s|WH$a=f?sT)t{KcW_j@KgAsE z^3LaEYFpjFKXTPYD4*!nyq;py;Lo1Y@_zG)jjDt3*W1aQHGC z+2sbIi&{-YCc-B^Xv6py4!z!_@i2!&O?#Vnf_yPH{^*a&i(nz@bcjT2$(Zj;Hix7E zIf~Q*>#wO~8)*h(@ARd{=MHCL6JNb3v{z2_duHF!91Tji*UW95HLl%DaQ)Ua^9k+@ zRP0`PI&=`*&OZT$Bl^cj|BUfwN>HYwm4w!2Zv`EVX}N;z||XnnnITR%|0OPyKIfxL*5lAi3)H4gY%a zJ~}c6#m7GS3#Q4+9wR8ui>)rcW{~iDs+AZL3?d56rF-4b>1;H@LKZ zh6HSt@ed#sD2!0v4^qFnB-i1)dS%hAOnuF@XGMlX%q_ep&1cYofd5i%6GWRZp`MP}-e~R7!hP{Z;8T4McL>Wj-pFxW-X%R{maHli5Ja z-c{vm178&rnS>%|f#=g8QRN4(OQ)6CO?0VT)^s=z=}+P>&dblKq&9suH6O@A&1jlM zQhxldeBxGZtgl42hBig&FXiRATqYt?!{jwr za`Y>=ius=XG#`C&n`$?<96jO#R7g~`$G$=m&ntbCq#bEOc_j68VR_X zZiznBYz?jvy5i8&tvlCxKdHfvahm-9QYG7i9N{?hO|mbZ)74s}CqnqK_A*`pd1>c6jBfbQOUs1Y~GqO=+dFoq?pSXyGn0B6Pe%Csi4Bcdtr) zN)5hE6#0~^unA)s@H|EI~W_R}$xv$ih)MJueC6hYnDU?U7 zERmhe$$4z`>rmn+74>CXnOQ(mt;YwmFUwJ1u-m+>6( z2pXG_xW(>1%tBVdK;?QKq57PrVd8a&sW(BA4Vn?HW+ zHiyom6+icQNSN#w22nij%L>Wksj{el$0c8jEjGJia%b>O$aUq}xA`}u zSJ32BIS;SW?9WISt_@67s^g_)vQye>box@lrm5g`T0n2ACr-7J$0(%c?DC>6gWzK- zrZb6@>M2#v*0FNnL293uaX;NtQYyK+`mJSfMwult+Ij?uX4K`@6xz8xL+_VLZ3r{D zdCk)w1;2`uY>EAi3VE_UskL&RwD3Hl(!M_a)#}>&@3N^2O){0d4NfaB zT#n?MwfRCqW=zI|cgaps>=v))p4~j-WjH%{;I1c|AlP9(-{qSpjvK(lGIF*Y><42L z=O1HRKFS71e|%G|G2x_1D+_zg6wHP6F(UY5ZHG`X|B+EOq4){xIBUkZPW9r!`lUY# z=9_!UR0zhAX)9kUklKnBW7txDFODQ@mNOXE5o`x zwG~TXhJb7Ux0(4HM19QwW9<5M65TQZf@{Op7-QEu7>ZS=_S>V?Uw$UkiEmkw*Zdkz z8)pBLu`DjG-un4)g`CA_==qEskwbs_I3PjGvN3x4nFFz0HIq!N9EslHC3O0sTqCuy z3h!Z0JnSl**TkoeJkA|jbW(Z#jC4BW3GD8w^4jP9qb)fH8)4Q;O9{a88Bvb<3SY@s z$0XE513hS59f^6KhTE)M{2;9S-hX7~g1;pDbjG2FvHLylhRi^lcfCns0J7+`8T~nv zbO-0L^+#0)ozJy|l!yARfz1ZBl{-KG_8SV>9qs|{-86t|0CK&jAC=H9t8i;HbNVP_ z)4pfwEr}=2g>j50GBUhxSnf3QfLOf`!sz9O8sS9o`uzbl*9huZavPi%8k|F}<8#p; zu2KE%OPdOt-v~wO14Xy;d4p0}FFtNdZa|sLaiE2Lveh#P;(+PnfJ|(>GKdM6F)-*f zQ1jrUL5ELL)u%agTS3u#`ph`0Tl;kfamRtX*uM=+d+>3Mg=B6P6!{6(RL?q z>tgLX|03GkyKMZLU7}Am-b<%z3a01rKX=W3#(W*kV1DbTTZ_c4tQG1_tMwtkp97)= zlL3jM*yABau7xG_VoO@VS`He|w}HU$aS!JXw)OJ*nQk(8Iw-#QzB!U4k4wUu>*DC} zfxz>gV)-oSB?cihvT6`@21z+S9WUNE|4Ux-zuBjBh{Yf@Fpj)95MYQH-GT@VS~w+s z4-f^>{Mz)8Jrq&6n&-S%EP4fWwj1VhH@%j%wp4)uF-ID5Eg|f2;N{WjPNsRq7#&A@ z?8<7nBDL3T<{1V9Dch$|6|@2{0TIk{>(Of**he{oYalyfVWG`qyDtSAKC-&&`*#Nz zvwW5RnLY;3!A?TN5-MgAXnfUOFA?9A5A~jBgjpXKl5w=Uu+_NN-R-Q@#BtOBFyJPv zEqcDcD$Fwg@*&$;o5VT8TNSZaS4JB+=vx#XnTYI2;=rBuA{Y)2JW02q?Zevoniz|} zJ15Uewj3szg6=go~~4$gVJFWS#SK<4J4l#)=`+iJn-rSFh*#oh7rcmhKCnyB{2)=IM|h19-nMk9R>5; z7J9O0AhO$U8wcnSuc*o^3Civ5j*|h4vSG$N%L*+SbgkI78`{@~1*i)SH4eUqujZN@ z3f4>U_ZJ2YbM_s$jqei6Nc{WVyx2k*zZN#=1MCjpjClxUtyII>lludnFB5sZ?ebmc zaY9%J1Ki>Uy#O+Bjjw0oIDkgbt&7}LzvWgcl4Mn(g%yU`bwto#56yLdg|0)%-7)^U zeXhs_Ok``;3t$y}--3qMVIun>49C^(1ktj4`5y_|f*_~ntO9Aycl({NuSp@ZxWX-H zHJI}+Tn&S)K+fsE?X6T4u~BH#9udHn$W{k5cu8a`3Y3*q103jZloGp>F)iOgia6L@ zptPVuK_lx0`K}v-ux0ASoy?<6e4=8>OQ9UacqRo1_4-s?>SMeEIbV7En`qIqwJ)9& z*UqchtLYrJyyDj}#`F>waAE-vX8fdo{hGgvC#$t~6VYtzH#+rVOL&HsQ{4B_YtauN zj0qzbv_#F9G<{(=`D>|aw(7iU8_d^cCpRNRC)$n5{p9by)1I`5>JJf$Lu@od*kSj! z`0mqnSpORvY3T`nV|2HV?!gjt9FhCxPY(tF6@6@;!Zi%+ZeB)zQ-0eaRvjrQpYuW> zdK2xvKart9Eznkt7p@d^tbAk33eds<9mOwi7mL~Bz90U}JW81_+q)tM^dCxF7VvC~ z_g=lE*gj`Ji#aI9)(qx1fqK^y&F9~YJq&8e4vG;7Ap7juxP=`$ju~g{3LZY##~VYB zFRFlxBwh!5n3&7U&x4W3EmrsNygtl3D$*UM^%Ej;(6f;p5&{(f^t*^d9zV+JB(xjH z$;>S&WV`W5VEDXku;TG%P~+x2UD`PAJFk)RxjI&d^#ulczS0{Mh0_B#Tp!oRA#q_-V>$XG&HZGg@{ z1u%{9ur&f3CNByT+MBJ93couYaLPVcP%k+@j_)-iJz&2;t7(}G*~ykvTX|;0UqY4; zBqp!x@DY9$Vl0VbuVo&Ph$+0QsyRc@FKgZDOzbtFP3H|eA!m8COI-|Wz^lWD0J!t@ zp4WKL5^9kDeoxCBOvlB}r4nb4Rk)H~85wx^9~qRd!+`Nw_CJXW9Zb-neg$w_(QG`9 z2k!lml%M|X_7*kF1&GS=Kg~=b9TYP6|LCEp3I;HEY||IzSf@ceD0aOzJ*gi#8xvD= zMl|MGmEt-Q+iH>edGd+3j{mhW_1o`_@!w5@tHUz!G6a?82@(fE8XK5Wy>9ht3=^bZ zujjT%K;9gByV}uBrVuG(c`dQ=XZiLmg)LU zpmYSinFmO>^qh;GIfQ-EqRd4YA`a1TQPSWF=1tO{eVy&mV`piuq-i(&8N+i%YKMTuN9DtkW6~_x=0pSJSo$v@p z{|#n1YnQhrrtY7hx%0y>BokCdZAijZugAnpeiDEA)-6(|g<8TLl}l;DHbE6Q=I z%DgT(u72ijvpEaLny$P&-(TydCAP!d@M^9Q764x7j3ak*DRP$Qjyfu?uymOV=j4>I z4+xCE8RHH?XU{S3r1y)qoz)ytd!*)2Y7x72Hp8|f*-|$x7+56!~!&_MF;Q@=}uL}5o@n1&x+5Z3qx$x)3F9zyQ zbE9g08NRi*jiMoOr2v$TdUX`*o2~gOa&yKFQ<~rsLjiy|_op3L$zE67cCjNkBvcOQhdc6E zew4_wR+}=FW48sutMn3r-wN&nrm_RW2D;Yn+J!_%UEIz@eLkQu5WwX~N zvMS>>7LdTn0mP1Q02QdV;v`7NArK56pIXn;V;8!Nlb#mXfC(MVYK^V)1K+MHWKu#b zYIp;`Us|3Vq(qWEfi)P~4goxIfl?+|1sw-$)QkpP;NuzSJt&dXA4WYrDHs8f{OUQR z#~|a>XRRjez+=A{WAk>Z(@XO2!TM&OD91v+q1&L+g9!Nabd`@vO@?o=|;lb@q zB+ELk2+vcTQ=Nb~gbb6(Cpi?-SWBD%$;N8Hd29kg@(=kmmc*0}$2{_Lib)ZMc{tm_ z_3u(zrsc$gpcuz2gKH5Vb z?Z9B+Mtfp`M_B4!MIl!VH+J1kJCY*AU}ub)cbLaJ)pAGP^s3V^L$rgBQ$kIO+T9RM zFq-jUXvrYtXRxW{4HSNE?mfSiMHn0W+-K9;nXK(Lm>lmsg*9XQRz{wl<)RKyoSbkf zdE^2%QrmJ42o)Zi3~Q%Ep@_oq)}3+~KtLHJRK3U0%(GLwa{~_wix2mDLrJPdyUsKi^?%hCQsLBDLhQQ=fkMSKbM z0Ywy00bQTK3H*sU}zC8K~y{?4Wa= zxCX7|ir^4NNj&2eNrY0l0znd=l>sM=1Ja~W{{VDk^NwjU-f>h1)RR_p`*SSnyGaUi z!>1LSWtKFJKg?wWW1ez4S3y11w0acw(n5o5XZM4sNtINfBxi$@Q2n79u%`qMN~&&3uLl`EwAp46u1b<~>&*gg+ZN2oya&nX zDGc!rH~tyvS>J8J%Q*-6R84YsnFEqfS^_TSkhv?J#{-@-nhnf&``nXRUukR-z~jDX z6P{IARDRLGP+wuW`pkx`(vj0E|R$|I0&1Z^(4 zuD8WnmxDjxtG^g@e}bMW{h#zdh1$=Dg|@Hagw(Cl+giQAA)&T#it@th;N;}+r?pQs zTd~<1Jg=Ht9UsFl8fw3`cl;ECLLU}18{HGc9}a#JJia*4Qg%ywjb7I1NA`tKaLFo9 z6rb=a_sqqcyl0&5Ud5$B4Q1e`yrcR6*cBs$yO?>CA{5Z7# z0D^J<0KrH61Mxq@`rfzV4+s2J(q!2LAl11d4AF6-?hiUzY1#q0JWFF zIEiaL58_U#KZbl=qF81KZ|&`oZlEu-8@8~3hd!Yhi1o%R zRSP{1XDjY!YJL#5_`UxC1q1k>ruf@Tw7${48tED+@b;N`5N5W!y7Oggl~dblg(n?H zwRWB_(!4$3%TEz}UeUZO44ULRB(k*duGhQrqh?6k9eoFS<9}!mgdZ0*@7j+|{i^=} zXODqo*?uy?VyiurMd@=Z8neV#Dor23(di!-HU9vL5NZwPYuaX?Z!{$Et!d`$VfajQ z^rdFWy1C{50I-Gqzs2v_)5CwXKZ|tBTaOjrHl+rob>#+{#m1p<1-U2YO{xF@;~-a{ z==y(#yeskE+HE6JwX@c|PiWUVj*=WJUdIc>rtF~V$_QQE!N)kSH`BjnKNoy9_^;x> zAN&jW!F}PYPZ8LMZw`wzmdowlY?3srt^MO6`?w<~lhUtz2>qUXN8q1}_aC$$#Sa?X z>;5gd(c`i6r;{;37F5WQr~oRlMdR9^E!fUq{t4OOZ5u+>e`s$SLkhM1=j{ou>14Mi zC}3eI13%t32R}hw$ByLGv|rn+;3bZebtG1vA<%qjZ>hkI@=PGHg5gUKP|65C)o1o} z_(!b#AJ%>jAdb(h;>|8;cMVF%4BYbK7oc{pes2>o1FnlohZ>rwjPp@5zQwEgx zFi&rD3~EkSgPoxDuAfx>vp?XcUN+J78&3;-E7i2diuq=R>^w4&x`Um{17sd~_2#=& z?W!}i_BtQCjI904eWx7OC1&<(8-Oad9X0B4%|oBj#8@H65^z#jwn-$3|}r$c6S-w@f~>9E^O9}&l9 zu$|xCY>t(cS?IwlElyYVOz|G0@PFZchyMTs>-hEItxi7|{8YB_D!#SlB)0axdQB0M zOmfP0t1dcj9V^>^;GBO0ym$LJ{>WNC!7qt$#G1B|;w@F=5X-(cj!9QzsU4UuN7lUp zUj=wyS@EWc@dr`SVV76Xt!`~}-9eOjkVP>=B7%Doi}c66alRz%YMdYfA>1Z?0;VqFX|@ zwooqI6UzxL$oiW5zvGUX@qb(RO|IJh&zk+7nd1bv68L`OQHyjhDmt-+v-fwXE1t!x z!y41}v;D08CitWFQ~j=f6zO_j!##7rHrLH_rw#1}pt8jxJX3$qRCSd|9RMD+movGf zd$Y%X;HZBWK0E%@f3Ro4zY-sSa!56M5BN%TjaNe5D%f2KWV1w6874X6b#CEv&3*52 z7l9?S(ENMiD;+mZn^V&Ch~%32qlun$fi}cR#^zSs0PSBqe$$^A{{U)l*ze)@guEB~ zH+%!I@h^rny(nqHd~*wwW2GzheC5$U+`KTAIDxR{hR(6X}%Khh1Kom zq45$ZZtQf)8H&p#pk#-Rakw@Z^axD}-B?>#@gLj2;XjW50A)Q>NAXUl@o!M^f_R_e zCa-yCV?2gyYcDz`XrmF9ZL$S+T&@AnO8KMVKA`%S?FaDNNw&1qVs-JAo6Bzcd15V= z;Cb(|McPmhcV~_beP?U`00gu6-3N-iA*%Sp;}U8*9-reVbq!C$vb4=DvKxvFDqw^Fz4oU_KpO~opd99&QL0GP9oF|Vz zXKⅇpfAT5_sDC_J8o+x1{)cN!0Z;NOb7k0*+8^Qf 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", + "wheel" +] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/requirements.txt b/cope2n-ai-fi/modules/_sdsvkvu/requirements.txt new file mode 100644 index 0000000..8616d6d --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/requirements.txt @@ -0,0 +1,28 @@ +nltk +six +deskew +jdeskew +pdf2image +omegaconf +imagesize +xmltodict +dicttoxml +terminaltables +Pillow>=9.4.0 +nptyping==1.4.2 +opencv-python==4.5.4.60 ## +opencv-python-headless==4.5.4.60 +overrides==4.1.2 +# transformers==4.30.0 +sentencepiece==0.1.99 +seqeval==0.0.12 +tensorboard>=2.2.0 +scipy==1.9.1 +# code-style +isort==5.9.3 +black==21.9b0 +# pytorch +# --find-links https://download.pytorch.org/whl/torch_stable.html +# torch==1.13.1+cu116 +# torchvision==0.14.1+cu116 +tldextract==5.1.1 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/scripts/run.sh b/cope2n-ai-fi/modules/_sdsvkvu/scripts/run.sh new file mode 100644 index 0000000..69c3092 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/scripts/run.sh @@ -0,0 +1,26 @@ +cd /mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu +export CUDA_VISIBLE_DEVICES=0 + +python sdsvkvu/main.py \ + --img_dir /mnt/hdd4T/OCR/tuanlv/00-Datasets/SBT_DATA/invoice_validation \ + --save_dir /mnt/hdd4T/OCR/tuanlv/02-KVU/02-KVU_test/visualize/sbt_invoice \ + --kvu_params "{\"device\":\"cuda:0\"}" \ + --doc_type "sbt_v2" \ + --export_img 1 + + +# python sdsvkvu/main.py \ +# --img_dir /mnt/hdd4T/OCR/tuanlv/00-Datasets/SBT_DATA/imei_validation \ +# --save_dir /mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu/visualize/test_sbt_imei \ +# --kvu_params "{\"device\":\"cuda:0\"}" \ +# --doc_type "sbt_v2" \ +# --export_img 1 + + + +# python sdsvkvu/main.py \ +# --img_dir /mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu/visualize/test_sbt2 \ +# --save_dir /mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu/visualize/test_sbt2 \ +# --kvu_params "{\"device\":\"cuda:0\"}" \ +# --doc_type "sbt_v2" \ +# --export_img 1 \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/PKG-INFO b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/PKG-INFO new file mode 100644 index 0000000..d9e424f --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/PKG-INFO @@ -0,0 +1,122 @@ +Metadata-Version: 2.1 +Name: sdsvkvu +Version: 0.0.1 +Summary: SDSV OCR Team: Key-value understanding +Home-page: https://github.com/open-mmlab/mmocr +Author: tuanlv +Author-email: lv.tuan3@samsung.com +License: Apache License 2.0 +Classifier: Development Status :: 4 - Beta +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3.9 +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-File: LICENSE + +

    +

    SDSVKVU

    +

    + + ***Feature*** + - Extract pairs of key-value in documents: Invoice/Receipt, Forms, Government documents (Id cards, driver license, birth's certificate) + - Language: VI + EN + + ***What's news*** + ### - Ver 0.0.1: + - Support inputs: image, PDF file (single or multi pages) + - Extract all pairs key-value return raw_outputs + + Weights: sdsvkvu/weights/key_value_understanding-20230716-085549_final + - For VAT invoices : Extract 14 specific fields + + Weights: sdsvkvu/weights/key_value_understanding-20230627-164536_fi + - For SBT invoices ("sbt" option): Extract table in SBT invoice + + Weights: sdsvkvu/weights/key_value_understanding-20230617-162324_sbt + ### - Ver 0.0.2: Add more option: "vtb" - Vietin Bank + - For Vietin Bank document ("vtb" option): Extract 6 specific fileds + + Weights: sdsvkvu/weights/key_value_understanding-20230824-164236_vietin + ### - Ver 0.0.3: Add default option: + - Return all potential pairs of key-value, title, only key, triplet, and table with raw key + + ## I. Setup + ***Dependencies*** + - Python: 3.10 + - Torch: 1.11.3 + - CUDA: 11.6 + - transformers: 4.30.0 + ``` + pip install -v -e . + ``` + + + ## II. Inference + run cmd: python test.py + ``` + import os + from sdsvkvu import load_engine, process_img + os.environ["CUDA_VISIBLE_DEVICES"]="1" + + if __name__ == "__main__": + kwargs = {"device": "cuda:0"} + img_dir = "/mnt/ssd1T/tuanlv/02-KVU/sdsvkvu/visualize/test_img/RedInvoice_WaterPurfier_Feb_PVI_829_0.jpg" + save_dir = "/mnt/ssd1T/tuanlv/02-KVU/sdsvkvu/visualize/test2/" + engine = load_engine(kwargs) + # option: "vat" for vat invoice outputs, "sbt": sbt invoice outputs, else for raw outputs + outputs = process_img(img_dir, save_dir, engine, export_all=False, option="vat") + ``` + + # Structure project + . + ├── sdsvkvu + │   ├── main.py + ├── externals + │   │   ├── __init__.py + │   │   ├── ocr_engine + │   │   │   ├── ... + │   │   ├── ocr_engine_deskew + │   │   │   ├── ... + │   ├── model + │   │   ├── combined_model.py + │   │   ├── document_kvu_model.py + │   │   ├── __init__.py + │   │   ├── kvu_model.py + │   │   └── relation_extractor.py + │   ├── modules + │   │   ├── __init__.py + │   │   ├── predictor.py + │   │   ├── preprocess.py + │   │   └── run_ocr.py + │   ├── requirements.txt + │   ├── settings.yml + │   ├── sources + │   │   ├── __init__.py + │   │   ├── kvu.py + │   │   └── utils.py + │   ├── utils + │   │   ├── dictionary + │   │   │   ├── __init__.py + │   │   │   ├── sbt.py + │   │   │   └── vat.py + │   │   │   └── vtb.py + │   │   ├── __init__.py + │   │   ├── post_processing.py + │   │   ├── query + │   │   │   ├── __init__.py + │   │   │   ├── sbt.py + │   │   │   └── vat.py + │   │   │   └── vtb.py + │   │   └── utils.py + │   └── weights + │   └── key_value_understanding-20230627-164536_fi + │   ├── key_value_understanding-20230617-162324_sbt + │   └── key_value_understanding-20230716-085549_final + │   └── key_value_understanding-20230824-164236_vietin + ├── LICENSE + ├── MANIFEST.in + ├── pyproject.toml + ├── README.md + ├── scripts + │   └── run.sh + ├── setup.cfg + ├── setup.py + ├── test.py + └── visualize diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/SOURCES.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/SOURCES.txt new file mode 100644 index 0000000..fc2dccf --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/SOURCES.txt @@ -0,0 +1,49 @@ +LICENSE +MANIFEST.in +README.md +pyproject.toml +setup.cfg +setup.py +sdsvkvu/__init__.py +sdsvkvu/main.py +sdsvkvu.egg-info/PKG-INFO +sdsvkvu.egg-info/SOURCES.txt +sdsvkvu.egg-info/dependency_links.txt +sdsvkvu.egg-info/not-zip-safe +sdsvkvu.egg-info/requires.txt +sdsvkvu.egg-info/top_level.txt +sdsvkvu/externals/__init__.py +sdsvkvu/externals/basic_ocr/__init__.py +sdsvkvu/externals/basic_ocr/run.py +sdsvkvu/externals/ocr_engine/__init__.py +sdsvkvu/externals/ocr_engine/run.py +sdsvkvu/externals/ocr_engine_deskew/__init__.py +sdsvkvu/externals/ocr_engine_deskew/run.py +sdsvkvu/model/__init__.py +sdsvkvu/model/combined_model.py +sdsvkvu/model/document_kvu_model.py +sdsvkvu/model/kvu_model.py +sdsvkvu/model/relation_extractor.py +sdsvkvu/model/sbt_model.py +sdsvkvu/modules/__init__.py +sdsvkvu/modules/predictor.py +sdsvkvu/modules/preprocess.py +sdsvkvu/modules/run_ocr.py +sdsvkvu/sources/__init__.py +sdsvkvu/sources/kvu.py +sdsvkvu/sources/utils.py +sdsvkvu/utils/__init__.py +sdsvkvu/utils/post_processing.py +sdsvkvu/utils/utils.py +sdsvkvu/utils/word2line.py +sdsvkvu/utils/dictionary/__init__.py +sdsvkvu/utils/dictionary/sbt.py +sdsvkvu/utils/dictionary/sbt_v2.py +sdsvkvu/utils/dictionary/vat.py +sdsvkvu/utils/dictionary/vtb.py +sdsvkvu/utils/query/__init__.py +sdsvkvu/utils/query/all.py +sdsvkvu/utils/query/sbt.py +sdsvkvu/utils/query/sbt_v2.py +sdsvkvu/utils/query/vat.py +sdsvkvu/utils/query/vtb.py \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/dependency_links.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/not-zip-safe b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/requires.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/requires.txt new file mode 100644 index 0000000..dac7835 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/requires.txt @@ -0,0 +1,21 @@ +nltk +six +deskew +jdeskew +pdf2image +omegaconf +imagesize +xmltodict +dicttoxml +terminaltables +Pillow==9.4.0 +nptyping==1.4.2 +opencv-python==4.5.4.60 +opencv-python-headless==4.5.4.60 +overrides==4.1.2 +sentencepiece==0.1.99 +seqeval==0.0.12 +tensorboard>=2.2.0 +scipy==1.9.1 +isort==5.9.3 +black==21.9b0 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/top_level.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/top_level.txt new file mode 100644 index 0000000..f6f7634 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu.egg-info/top_level.txt @@ -0,0 +1 @@ +sdsvkvu diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/__init__.py new file mode 100644 index 0000000..b90f78f --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/__init__.py @@ -0,0 +1,4 @@ +from .main import load_engine +from .main import process_img +from .main import process_pdf +from .main import process_dir diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/.gitignore b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/.gitignore new file mode 100644 index 0000000..1250e95 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +visualize/ +results/ +*.jpeg +*.jpg +*.png diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/README.md b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/README.md new file mode 100644 index 0000000..ca1b349 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/README.md @@ -0,0 +1,47 @@ +# OCR Engine + +OCR Engine is a Python package that combines text detection and recognition models from [mmdet](https://github.com/open-mmlab/mmdetection) and [mmocr](https://github.com/open-mmlab/mmocr) to perform Optical Character Recognition (OCR) on various inputs. The package currently supports three types of input: a single image, a recursive directory, or a csv file. + +## Installation + +To install OCR Engine, clone the repository and install the required packages: + +```bash +git clone git@github.com:mrlasdt/ocr-engine.git +cd ocr-engine +pip install -r requirements.txt + +``` + + +## Usage + +To use OCR Engine, simply run the `ocr_engine.py` script with the desired input type and input path. For example, to perform OCR on a single image: + +```css +python ocr_engine.py --input_type image --input_path /path/to/image.jpg +``` + +To perform OCR on a recursive directory: + +```css +python ocr_engine.py --input_type directory --input_path /path/to/directory/ + +``` + +To perform OCR on a csv file: + + +``` +python ocr_engine.py --input_type csv --input_path /path/to/file.csv +``` + +OCR Engine will automatically detect and recognize text in the input and output the results in a CSV file named `ocr_results.csv`. + +## Contributing + +If you would like to contribute to OCR Engine, please fork the repository and submit a pull request. We welcome contributions of all types, including bug fixes, new features, and documentation improvements. + +## License + +OCR Engine is released under the [MIT License](https://opensource.org/licenses/MIT). See the LICENSE file for more information. diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/TODO.todo b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/TODO.todo new file mode 100644 index 0000000..34df095 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/TODO.todo @@ -0,0 +1,10 @@ +☐ refactor argument parser of run.py +☐ add timer level, logging level and write_mode to argumments +☐ add paddleocr deskew to the code +☐ fix the deskew code to resize the image only for detecting the angle, we want to feed the original size image to the text detection pipeline so that the bounding boxes would be mapped back to the original size +☐ ocr engine import took too long +☐ add word level to write_mode +☐ add word group and line +change max_x_dist from pixel to percentage of box width +☐ visualization: adjust fontsize dynamically + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/__init__.py new file mode 100644 index 0000000..aabc310 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/__init__.py @@ -0,0 +1,11 @@ +# # Define package-level variables +# __version__ = '0.0' + +# Import modules +from .src.ocr import OcrEngine +# from .src.word_formation import words_to_lines +from .src.word_formation import words_to_lines_tesseract as words_to_lines +from .src.utils import ImageReader, read_ocr_result_from_txt +from .src.dto import Word, Line, Page, Document, Box +# Expose package contents +__all__ = ["OcrEngine", "Box", "Word", "Line", "Page", "Document", "words_to_lines", "ImageReader", "read_ocr_result_from_txt"] diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/.gitignore b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/.gitignore new file mode 100644 index 0000000..e56dbb2 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/.gitignore @@ -0,0 +1,9 @@ +output* +*.pyc +*.jpg +check +weights/ +workdirs/ +__pycache__* +test_hungbnt.py +libs* \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/README.md b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/README.md new file mode 100644 index 0000000..d1b9637 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/README.md @@ -0,0 +1,29 @@ +

    +

    Dewarp

    +

    + +***Feature*** +- Align document + + +## I. Setup +***Dependencies*** +- Python: 3.8 +- Torch: 1.10.2 +- CUDA: 11.6 +- transformers: 4.28.1 +### 1. Install PaddlePaddle +``` +python -m pip install paddlepaddle-gpu==2.4.2.post116 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +``` + +### 2. Install sdsv_dewarp +``` +pip install -v -e . +``` + + +## II. Test +``` +python test.py --input samples --out demo/outputs --device 'cuda' +``` diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/cls.yaml b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/cls.yaml new file mode 100644 index 0000000..4c4cfdc --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/cls.yaml @@ -0,0 +1,3 @@ +model_dir: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_ppocr_mobile_v2.0_cls_infer +gpu_mem: 3000 +max_batch_size: 32 \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/det.yaml b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/det.yaml new file mode 100644 index 0000000..f218ef1 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/config/det.yaml @@ -0,0 +1,8 @@ +model_dir: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_PP-OCRv3_det_infer +gpu_mem: 3000 +det_limit_side_len: 1560 +det_limit_type: max +det_db_unclip_ratio: 1.85 +det_db_thresh: 0.3 +det_db_box_thresh: 0.5 +det_db_score_mode: fast \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/requirements.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/requirements.txt new file mode 100644 index 0000000..768fde9 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/requirements.txt @@ -0,0 +1,7 @@ + +paddleocr>=2.0.1 +opencv-contrib-python +opencv-python +numpy +gdown==3.13.0 +imgaug==0.4.0 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO new file mode 100644 index 0000000..634708d --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO @@ -0,0 +1,45 @@ +Metadata-Version: 2.1 +Name: sdsv-dewarp +Version: 1.0.0 +Summary: Dewarp document +Home-page: +License: Apache License 2.0 +Classifier: Development Status :: 4 - Beta +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Description-Content-Type: text/markdown + +

    +

    Dewarp

    +

    + +***Feature*** +- Align document + + +## I. Setup +***Dependencies*** +- Python: 3.8 +- Torch: 1.10.2 +- CUDA: 11.6 +- transformers: 4.28.1 +### 1. Install PaddlePaddle +``` +python -m pip install paddlepaddle-gpu==2.4.2.post116 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +``` + +### 2. Install sdsv_dewarp +``` +pip install -v -e . +``` + + +## II. Test +``` +python test.py --input samples --out demo/outputs --device 'cuda' +``` diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt new file mode 100644 index 0000000..953a123 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt @@ -0,0 +1,15 @@ +README.md +setup.py +sdsv_dewarp/__init__.py +sdsv_dewarp/api.py +sdsv_dewarp/config.py +sdsv_dewarp/factory.py +sdsv_dewarp/models.py +sdsv_dewarp/utils.py +sdsv_dewarp/version.py +sdsv_dewarp.egg-info/PKG-INFO +sdsv_dewarp.egg-info/SOURCES.txt +sdsv_dewarp.egg-info/dependency_links.txt +sdsv_dewarp.egg-info/not-zip-safe +sdsv_dewarp.egg-info/requires.txt +sdsv_dewarp.egg-info/top_level.txt \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt new file mode 100644 index 0000000..89816c8 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt @@ -0,0 +1,6 @@ +paddleocr>=2.0.1 +opencv-contrib-python +opencv-python +numpy +gdown==3.13.0 +imgaug==0.4.0 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt new file mode 100644 index 0000000..a5ce4e8 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt @@ -0,0 +1 @@ +sdsv_dewarp diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/api.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/api.py new file mode 100644 index 0000000..d71ddc5 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/api.py @@ -0,0 +1,200 @@ +import math +import numpy as np +from typing import List +import cv2 +import collections +import logging +import imgaug.augmenters as iaa +from imgaug.augmentables.polys import Polygon, PolygonsOnImage + +from sdsv_dewarp.models import PaddleTextClassifier, PaddleTextDetector +from sdsv_dewarp.config import Cfg +from .utils import * + + +MIN_LONG_EDGE = 40**2 +NUMBER_BOX_FOR_ALIGNMENT = 200 +MAX_ANGLE = 180 +MIN_ANGLE = 1 +MIN_NUM_BOX_TEXT = 3 +CROP_SIZE = 3000 + +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + + +class AlignImage: + """Rotate image to 0 degree + Args: + text_detector (deepmodel): Text detection model + text_cls (deepmodel): Text classification model (0 or 180) + + Return: + is_blank (bool): Blank image when haven't boxes text + image_align: Image after alignment + angle_align: Degree of angle alignment + """ + + def __init__(self, text_detector: dict, text_cls: dict, device: str = 'cpu'): + self.text_detector = None + self.text_cls = None + self.use_gpu = True if device != 'cpu' else False + + self._init_model(text_detector, text_cls) + + def _init_model(self, text_detector, text_cls): + det_config = Cfg.load_config_from_file(text_detector['config']) + det_config['model_dir'] = text_detector['weight'] + cls_config = Cfg.load_config_from_file(text_cls['config']) + cls_config['model_dir'] = text_cls['weight'] + + self.text_detector = PaddleTextDetector(config=det_config, use_gpu=self.use_gpu) + self.text_cls = PaddleTextClassifier(config=cls_config, use_gpu=self.use_gpu) + + def _cal_width(self, poly_box): + """Calculate width of a polygon [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]""" + tl, tr, br, bl = poly_box + edge_s, edge_l = distance(tl, tr), distance(tr, br) + + return max(edge_s, edge_l) + + def _get_most_frequent(self, values): + values = np.array(values) + # create the histogram + hist, bins = np.histogram(values, bins=np.arange(0, 181, 10)) + + # get the index of the most frequent angle + index = np.argmax(hist) + + # get the most frequent angle + most_frequent_angle = (bins[index] + bins[index + 1]) / 2 + + return most_frequent_angle + + def _cal_angle(self, poly_box): + """Calculate the angle between two point""" + a = poly_box[0] + b = poly_box[1] + c = poly_box[2] + + # Get the longer edge + if distance(a, b) >= distance(b, c): + x, y = a, b + else: + x, y = b, c + + angle = math.degrees(math.atan2(-(y[1] - x[1]), y[0] - x[0])) + + if angle < 0: + angle = 180 - abs(angle) + + return angle + + def _reject_outliers(self, data, m=5.0): + """Remove noise angle""" + list_index = np.arange(len(data)) + d = np.abs(data - np.median(data)) + mdev = np.median(d) + s = d / (mdev if mdev else 1.0) + + return list_index[s < m], data[s < m] + + def __call__(self, image): + """image (np.ndarray): BGR image""" + + # Crop center image to increase speed of text detection + + image_resized = crop_image(image, crop_size=CROP_SIZE).copy() if max(image.shape) > CROP_SIZE else image.copy() + poly_box_texts = self.text_detector(image_resized) + + # draw_img = vis_ocr( + # image_resized, + # poly_box_texts, + # ) + # cv2.imwrite("draw_img.jpg", draw_img) + + is_blank = False + + # Check image is blank + if len(poly_box_texts) <= MIN_NUM_BOX_TEXT: + is_blank = True + return image, is_blank, 0 + + # # Crop document + # poly_np = np.array(poly_box_texts) + # min_x = poly_box_texts[:, 0].min() + # max_x = poly_box_texts[:, 2].max() + # min_y = poly_box_texts[:, 1].min() + # max_y = poly_box_texts[:, 3].max() + + # Filter small poly + poly_box_areas = [ + [self._cal_width(poly_box), id] + for id, poly_box in enumerate(poly_box_texts) + ] + + poly_box_areas = sorted(poly_box_areas)[-NUMBER_BOX_FOR_ALIGNMENT:] + poly_box_areas = [poly_box_texts[id[1]] for id in poly_box_areas] + + # Calculate angle + list_angle = [self._cal_angle(poly_box) for poly_box in poly_box_areas] + list_angle = [angle if angle >= MIN_ANGLE else 180 for angle in list_angle] + + # LOGGER.info(f"List angle before reject outlier: {list_angle}") + list_angle = np.array(list_angle) + list_index, list_angle = self._reject_outliers(list_angle) + # LOGGER.info(f"List angle after reject outlier: {list_angle}") + + if len(list_angle): + + frequent_angle = self._get_most_frequent(list_angle) + list_angle = [angle for angle in list_angle if abs(angle - frequent_angle) <= 45] + # LOGGER.info(f"List angle after reject angle: {list_angle}") + angle = np.mean(list_angle) + else: + angle = 0 + + # LOGGER.info(f"Avg angle: {angle}") + + # Reuse poly boxes detected by text detection + polys_org = PolygonsOnImage( + [Polygon(poly_box_areas[index]) for index in list_index], + shape=image_resized.shape, + ) + seq_augment = iaa.Sequential([iaa.Rotate(angle, fit_output=True, order=3)]) + + # Rotate image by degree + if angle >= MIN_ANGLE and angle <= MAX_ANGLE: + image_resized, polys_aug = seq_augment( + image=image_resized, polygons=polys_org + ) + else: + angle = 0 + image_resized, polys_aug = image_resized, polys_org + + # cv2.imwrite("image_resized.jpg", image_resized) + + # Classify image 0 or 180 degree + list_poly = [poly.coords for poly in polys_aug] + + image_crop_list = [ + dewarp_by_polygon(image_resized, poly)[0] for poly in list_poly + ] + + cls_res = self.text_cls(image_crop_list) + cls_labels = [cls_[0] for cls_ in cls_res[1]] + # LOGGER.info(f"Angle lines: {cls_labels}") + counter = collections.Counter(cls_labels) + + angle_align = angle + if counter["0"] <= counter["180"]: + aug = iaa.Rotate(angle + 180, fit_output=True, order=3) + angle_align = angle + 180 + else: + aug = iaa.Rotate(angle, fit_output=True, order=3) + + # Rotate the image by degree + image = aug.augment_image(image) + + return image, is_blank, angle_align + # return image diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/config.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/config.py new file mode 100644 index 0000000..204c2c0 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/config.py @@ -0,0 +1,41 @@ +import yaml +import pprint +import os +import json + + +def load_from_yaml(fname): + with open(fname, encoding='utf-8') as f: + base_config = yaml.safe_load(f) + return base_config + +def load_from_json(fname): + with open(fname, "r", encoding='utf-8') as f: + base_config = json.load(f) + return base_config + +class Cfg(dict): + def __init__(self, config_dict): + super(Cfg, self).__init__(**config_dict) + self.__dict__ = self + + @staticmethod + def load_config_from_file(fname, download_base=False): + if not os.path.exists(fname): + raise FileNotFoundError("Not found config at {}".format(fname)) + if fname.endswith(".yaml") or fname.endswith(".yml"): + return Cfg(load_from_yaml(fname)) + elif fname.endswith(".json"): + return Cfg(load_from_json(fname)) + else: + raise Exception(f"{fname} not supported") + + + def save(self, fname): + with open(fname, 'w', encoding='utf-8') as outfile: + yaml.dump(dict(self), outfile, default_flow_style=False, allow_unicode=True) + + # @property + def pretty_text(self): + return pprint.PrettyPrinter().pprint(self) + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/factory.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/factory.py new file mode 100644 index 0000000..65e4bbd --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/factory.py @@ -0,0 +1,75 @@ +import os +import shutil +import hashlib +import warnings + +def sha256sum(filename): + h = hashlib.sha256() + b = bytearray(128*1024) + mv = memoryview(b) + with open(filename, 'rb', buffering=0) as f: + for n in iter(lambda : f.readinto(mv), 0): + h.update(mv[:n]) + return h.hexdigest() + + +online_model_factory = { + 'yolox-s-general-text-pretrain-20221226': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/62j266xm8r.pth', + 'hash': '89bff792685af454d0cfea5d6d673be6914d614e4c2044e786da6eddf36f8b50'}, + 'yolox-s-checkbox-20220726': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/1647d7eys7.pth', + 'hash': '7c1e188b7375dcf0b7b9d317675ebd92a86fdc29363558002249867249ee10f8'}, + 'yolox-s-idcard-5c-20221027': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/jr0egad3ix.pth', + 'hash': '73a7772594c1f6d3f6d6a98b6d6e4097af5026864e3bd50531ad9e635ae795a7'}, + 'yolox-s-handwritten-text-line-20230228': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/rb07rtwmgi.pth', + 'hash': 'a31d1bf8fc880479d2e11463dad0b4081952a13e553a02919109b634a1190ef1'} +} + +__hub_available_versions__ = online_model_factory.keys() + +def _get_from_hub(file_path, version, version_url): + os.system(f'wget -O {file_path} {version_url}') + assert os.path.exists(file_path), \ + 'wget failed while trying to retrieve from hub.' + downloaded_hash = sha256sum(file_path) + if downloaded_hash != online_model_factory[version]['hash']: + os.remove(file_path) + raise ValueError('sha256 hash doesnt match for version retrieved from hub.') + +def _get(version): + use_online = version in __hub_available_versions__ + + if not use_online and not os.path.exists(version): + raise ValueError(f'Model version {version} not found online and not found local.') + + hub_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'hub') + if not os.path.exists(hub_path): + os.makedirs(hub_path) + if use_online: + version_url = online_model_factory[version]['url'] + file_path = os.path.join(hub_path, os.path.basename(version_url)) + else: + file_path = os.path.join(hub_path, os.path.basename(version)) + + if not os.path.exists(file_path): + if use_online: + _get_from_hub(file_path, version, version_url) + else: + shutil.copy2(version, file_path) + else: + if use_online: + downloaded_hash = sha256sum(file_path) + if downloaded_hash != online_model_factory[version]['hash']: + os.remove(file_path) + warnings.warn('existing hub version sha256 hash doesnt match, now re-download from hub.') + _get_from_hub(file_path, version, version_url) + else: + if sha256sum(file_path) != sha256sum(version): + os.remove(file_path) + warnings.warn('existing local version sha256 hash doesnt match, now replace with new local version.') + shutil.copy2(version, file_path) + + return \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/models.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/models.py new file mode 100644 index 0000000..64cb88c --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/models.py @@ -0,0 +1,73 @@ + +from paddleocr.tools.infer.predict_det import TextDetector +from paddleocr.tools.infer.predict_cls import TextClassifier +from paddleocr.paddleocr import parse_args +from sdsv_dewarp.config import Cfg + +class PaddleTextDetector(object): + def __init__( + self, + # config_path: str, + config: dict, + use_gpu=False + ): + # config = Cfg.load_config_from_file(config_path) + + self.args = parse_args(mMain=False) + self.args.__dict__.update( + det_model_dir=config['model_dir'], + gpu_mem=config['gpu_mem'], + use_gpu=use_gpu, + use_zero_copy_run=True, + max_batch_size=1, + det_limit_side_len=config['det_limit_side_len'], #960 + det_limit_type=config['det_limit_type'], #'max' + det_db_unclip_ratio=config['det_db_unclip_ratio'], + det_db_thresh=config['det_db_thresh'], + det_db_box_thresh=config['det_db_box_thresh'], + det_db_score_mode=config['det_db_score_mode'], + ) + self.text_detector = TextDetector(self.args) + + def __call__(self, image): + """ + + Args: + image (np.ndarray): BGR images + + Returns: + np.ndarray: numpy array of poly boxes - shape 4x2 + """ + dt_boxes, time_infer = self.text_detector(image) + return dt_boxes + + +class PaddleTextClassifier(object): + def __init__( + self, + # config_path: str, + config: str, + use_gpu=False + ): + # config = Cfg.load_config_from_file(config_path) + + self.args = parse_args(mMain=False) + self.args.__dict__.update( + cls_model_dir=config['model_dir'], + gpu_mem=config['gpu_mem'], + use_gpu=use_gpu, + use_zero_copy_run=True, + cls_batch_num=config['max_batch_size'], + ) + self.text_classifier = TextClassifier(self.args) + + def __call__(self, images): + """ + Args: + images (np.ndarray): list of BGR images + + Returns: + img_list, cls_res, elapse : cls_res format = (label, conf) + """ + out= self.text_classifier(images) + return out \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/utils.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/utils.py new file mode 100644 index 0000000..ae02cc1 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/utils.py @@ -0,0 +1,212 @@ +import math +import cv2 +import numpy as np +from PIL import Image, ImageDraw, ImageFont +import random + + +def distance(p1, p2): + """Calculate Euclid distance""" + x1, y1 = p1 + x2, y2 = p2 + dist = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + + return dist + + +def crop_image(image, crop_size=1280): + """Crop center image""" + h, w = image.shape[:2] + x_center, y_center = w // 2, h // 2 + half_size = crop_size // 2 + + xmin, ymin = x_center - half_size, y_center - half_size + xmax, ymax = x_center + half_size, y_center + half_size + + xmin = max(xmin, 0) + ymin = max(ymin, 0) + xmax = min(xmax, w) + ymax = min(ymax, h) + + return image[ymin:ymax, xmin:xmax] + + +def _closest_point(corners, A): + """Find closest A in corrers point""" + distances = [distance(A, p) for p in corners] + return corners[np.argmin(distances)] + + +def _re_order_corners(image_size, corners) -> list: + """Order by corners by clockwise angle""" + h, w = image_size + tl = _closest_point(corners, (0, 0)) + tr = _closest_point(corners, (w, 0)) + br = _closest_point(corners, (w, h)) + bl = _closest_point(corners, (0, h)) + + return [tl, tr, br, bl] + + +def _validate_corner(corners, ratio_thres=0.5, epsilon=1e-3) -> bool: + """Check corners is valid + Invalid: 3 points, duplicate points, .... + """ + c_tl, c_tr, c_br, c_bl = corners + e_top = distance(c_tl, c_tr) + e_right = distance(c_tr, c_br) + e_bottom = distance(c_br, c_bl) + e_left = distance(c_bl, c_tl) + + min_tb = min(e_top, e_bottom) + max_tb = max(e_top, e_bottom) + min_lr = min(e_left, e_right) + max_lr = max(e_left, e_right) + + # Nếu các điểm trùng nhau thì độ dài các cạnh sẽ bằng 0 + if min(max_tb, max_lr) < epsilon: + return False + + ratio = min(min_tb / max_tb, min_lr / max_lr) + if ratio < ratio_thres: + return False + + return True + + +def dewarp_by_polygon( + image, corners, need_validate=False, need_reorder=True, trace_trans=None +): + """Crop and dewarp from 4 corners of images + + Args: + image (np.array) + corners (list): Ex : [(3347, 512), (3379, 2427), (638, 2524), (647, 495)] + need_validate (bool, optional): validate 4 points. Defaults to False. + need_reorder (bool, optional): validate 4 points. Defaults to True. + + Returns: + dewarped: image after dewarp + corners: location of 4 corners after reorder + """ + h, w = image.shape[:2] + + if need_reorder: + corners = _re_order_corners((h, w), corners) + + dewarped = image + + if need_validate: + validate = _validate_corner(corners) + else: + validate = True + + if validate: + # perform dewarp + target_w = int( + max(distance(corners[0], corners[1]), distance(corners[2], corners[3])) + ) + target_h = int( + max(distance(corners[0], corners[3]), distance(corners[1], corners[2])) + ) + target_corners = [ + [0, 0], + [target_w, 0], + [target_w, target_h], + [0, target_h], + ] + + pts1 = np.float32(corners) + pts2 = np.float32(target_corners) + transform_matrix = cv2.getPerspectiveTransform(pts1, pts2) + + dewarped = cv2.warpPerspective(image, transform_matrix, (target_w, target_h)) + if trace_trans is not None: + trace_trans["dewarp_method"]["polygon"][ + "transform_matrix" + ] = transform_matrix + + return (dewarped, corners, trace_trans) + + +def vis_ocr(image, boxes, txts=[], scores=None, drop_score=0.5): + """ + Args: + image (np.ndarray / PIL): BGR image or PIL image + boxes (list / np.ndarray): list of polygon boxes + txts (list): list of text labels + scores (list, optional): probality. Defaults to None. + drop_score (float, optional): . Defaults to 0.5. + font_path (str, optional): Path of font. Defaults to "test/fonts/latin.ttf". + Returns: + np.ndarray: BGR image + """ + + if len(txts) == 0: + txts = [""] * len(boxes) + + if isinstance(image, np.ndarray): + image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + if isinstance(boxes, list): + boxes = np.array(boxes) + + h, w = image.height, image.width + img_left = image.copy() + img_right = Image.new("RGB", (w, h), (255, 255, 255)) + draw_left = ImageDraw.Draw(img_left) + draw_right = ImageDraw.Draw(img_right) + for idx, (box, txt) in enumerate(zip(boxes, txts)): + if scores is not None and scores[idx] < drop_score: + continue + color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + draw_left.polygon( + [ + box[0][0], + box[0][1], + box[1][0], + box[1][1], + box[2][0], + box[2][1], + box[3][0], + box[3][1], + ], + fill=color, + ) + draw_right.polygon( + [ + box[0][0], + box[0][1], + box[1][0], + box[1][1], + box[2][0], + box[2][1], + box[3][0], + box[3][1], + ], + outline=color, + ) + box_height = math.sqrt( + (box[0][0] - box[3][0]) ** 2 + (box[0][1] - box[3][1]) ** 2 + ) + box_width = math.sqrt( + (box[0][0] - box[1][0]) ** 2 + (box[0][1] - box[1][1]) ** 2 + ) + if box_height > 2 * box_width: + font_size = max(int(box_width * 0.9), 10) + font = ImageFont.load_default() + cur_y = box[0][1] + for c in txt: + char_size = font.getsize(c) + draw_right.text((box[0][0] + 3, cur_y), c, fill=(0, 0, 0), font=font) + cur_y += char_size[1] + else: + font_size = max(int(box_height * 0.8), 10) + font = ImageFont.load_default() + draw_right.text([box[0][0], box[0][1]], txt, fill=(0, 0, 0), font=font) + img_left = Image.blend(image, img_left, 0.5) + + img_show = Image.new("RGB", (w * 2, h), (255, 255, 255)) + img_show.paste(img_left, (0, 0, w, h)) + img_show.paste(img_right, (w, 0, w * 2, h)) + img_show = cv2.cvtColor(np.array(img_show), cv2.COLOR_RGB2BGR) + return img_show diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/version.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/version.py new file mode 100644 index 0000000..a1570ac --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/sdsv_dewarp/version.py @@ -0,0 +1 @@ +__version__="1.0.0" \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/setup.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/setup.py new file mode 100644 index 0000000..7887e8f --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/setup.py @@ -0,0 +1,187 @@ +import os +import os.path as osp +import shutil +import sys +import warnings +from setuptools import find_packages, setup + + +def readme(): + with open('README.md', encoding='utf-8') as f: + content = f.read() + return content + + +version_file = 'sdsv_dewarp/version.py' +is_windows = sys.platform == 'win32' + + +def add_mim_extention(): + """Add extra files that are required to support MIM into the package. + + These files will be added by creating a symlink to the originals if the + package is installed in `editable` mode (e.g. pip install -e .), or by + copying from the originals otherwise. + """ + + # parse installment mode + if 'develop' in sys.argv: + # installed by `pip install -e .` + mode = 'symlink' + elif 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: + # installed by `pip install .` + # or create source distribution by `python setup.py sdist` + mode = 'copy' + else: + return + + filenames = ['tools', 'configs', 'model-index.yml'] + repo_path = osp.dirname(__file__) + mim_path = osp.join(repo_path, 'mmocr', '.mim') + os.makedirs(mim_path, exist_ok=True) + + for filename in filenames: + if osp.exists(filename): + src_path = osp.join(repo_path, filename) + tar_path = osp.join(mim_path, filename) + + if osp.isfile(tar_path) or osp.islink(tar_path): + os.remove(tar_path) + elif osp.isdir(tar_path): + shutil.rmtree(tar_path) + + if mode == 'symlink': + src_relpath = osp.relpath(src_path, osp.dirname(tar_path)) + try: + os.symlink(src_relpath, tar_path) + except OSError: + # Creating a symbolic link on windows may raise an + # `OSError: [WinError 1314]` due to privilege. If + # the error happens, the src file will be copied + mode = 'copy' + warnings.warn( + f'Failed to create a symbolic link for {src_relpath}, ' + f'and it will be copied to {tar_path}') + else: + continue + + if mode == 'copy': + if osp.isfile(src_path): + shutil.copyfile(src_path, tar_path) + elif osp.isdir(src_path): + shutil.copytree(src_path, tar_path) + else: + warnings.warn(f'Cannot copy file {src_path}.') + else: + raise ValueError(f'Invalid mode {mode}') + + +def get_version(): + with open(version_file, 'r') as f: + exec(compile(f.read(), version_file, 'exec')) + import sys + + # return short version for sdist + if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: + return locals()['short_version'] + else: + return locals()['__version__'] + + +def parse_requirements(fname='requirements.txt', with_version=True): + """Parse the package dependencies listed in a requirements file but strip + specific version information. + + Args: + fname (str): Path to requirements file. + with_version (bool, default=False): If True, include version specs. + Returns: + info (list[str]): List of requirements items. + CommandLine: + python -c "import setup; print(setup.parse_requirements())" + """ + import re + import sys + from os.path import exists + require_fpath = fname + + def parse_line(line): + """Parse information from a line in a requirements text file.""" + if line.startswith('-r '): + # Allow specifying requirements in other files + target = line.split(' ')[1] + for info in parse_require_file(target): + yield info + else: + info = {'line': line} + if line.startswith('-e '): + info['package'] = line.split('#egg=')[1] + else: + # Remove versioning from the package + pat = '(' + '|'.join(['>=', '==', '>']) + ')' + parts = re.split(pat, line, maxsplit=1) + parts = [p.strip() for p in parts] + + info['package'] = parts[0] + if len(parts) > 1: + op, rest = parts[1:] + if ';' in rest: + # Handle platform specific dependencies + # http://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies + version, platform_deps = map(str.strip, + rest.split(';')) + info['platform_deps'] = platform_deps + else: + version = rest # NOQA + info['version'] = (op, version) + yield info + + def parse_require_file(fpath): + with open(fpath, 'r') as f: + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + for info in parse_line(line): + yield info + + def gen_packages_items(): + if exists(require_fpath): + for info in parse_require_file(require_fpath): + parts = [info['package']] + if with_version and 'version' in info: + parts.extend(info['version']) + if not sys.version.startswith('3.4'): + # apparently package_deps are broken in 3.4 + platform_deps = info.get('platform_deps') + if platform_deps is not None: + parts.append(';' + platform_deps) + item = ''.join(parts) + yield item + + packages = list(gen_packages_items()) + return packages + + +if __name__ == '__main__': + setup( + name='sdsv_dewarp', + version=get_version(), + description='Dewarp document', + long_description=readme(), + long_description_content_type='text/markdown', + packages=find_packages(exclude=('configs', 'tools', 'demo')), + include_package_data=True, + url='', + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + ], + license='Apache License 2.0', + install_requires=parse_requirements('requirements.txt'), + zip_safe=False) diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/test.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/test.py new file mode 100644 index 0000000..4bfdbc7 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/externals/sdsv_dewarp/test.py @@ -0,0 +1,47 @@ +from sdsv_dewarp.api import AlignImage +import cv2 +import glob +import os +import tqdm +import time +import argparse + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input") + parser.add_argument("--out") + parser.add_argument("--device", type=str, default="cuda:1") + + args = parser.parse_args() + model = AlignImage(device=args.device) + + + img_dir = args.input + out_dir = args.out + if not os.path.exists(out_dir): + os.makedirs(out_dir) + + img_paths = glob.glob(img_dir + "/*") + + times = [] + for img_path in tqdm.tqdm(img_paths): + t1 = time.time() + img = cv2.imread(img_path) + if img is None: + print(img_path) + continue + + aligned_img, is_blank, angle_align = model(img) + + times.append(time.time() - t1) + + if not is_blank: + cv2.imwrite(os.path.join(out_dir, os.path.basename(img_path)), aligned_img) + else: + cv2.imwrite(os.path.join(out_dir, os.path.basename(img_path)), img) + + + times = times[1:] + print("Avg time: ", sum(times) / len(times)) \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/requirements.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/requirements.txt new file mode 100644 index 0000000..b247e52 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/requirements.txt @@ -0,0 +1,82 @@ +addict==2.4.0 +asttokens==2.2.1 +autopep8==1.6.0 +backcall==0.2.0 +backports.functools-lru-cache==1.6.4 +brotlipy==0.7.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==2.0.4 +click==8.1.3 +colorama==0.4.6 +cryptography==39.0.1 +debugpy==1.5.1 +decorator==5.1.1 +docopt==0.6.2 +entrypoints==0.4 +executing==1.2.0 +flit_core==3.6.0 +idna==3.4 +importlib-metadata==6.0.0 +ipykernel==6.15.0 +ipython==8.11.0 +jedi==0.18.2 +jupyter-client==7.0.6 +jupyter_core==4.12.0 +Markdown==3.4.1 +markdown-it-py==2.2.0 +matplotlib-inline==0.1.6 +mdurl==0.1.2 +mkl-fft==1.3.1 +mkl-random==1.2.2 +mkl-service==2.4.0 +mmcv-full==1.7.1 +model-index==0.1.11 +nest-asyncio==1.5.6 +numpy==1.23.5 +opencv-python==4.7.0.72 +openmim==0.3.6 +ordered-set==4.1.0 +packaging==23.0 +pandas==1.5.3 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.4.0 +pip==22.3.1 +pipdeptree==2.5.2 +prompt-toolkit==3.0.38 +psutil==5.9.0 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pycodestyle==2.10.0 +pycparser==2.21 +Pygments==2.14.0 +pyOpenSSL==23.0.0 +PySocks==1.7.1 +python-dateutil==2.8.2 +pytz==2022.7.1 +PyYAML==6.0 +pyzmq==19.0.2 +requests==2.28.1 +rich==13.3.1 +sdsvtd==0.1.1 +sdsvtr==0.0.5 +setuptools==65.6.3 +Shapely==1.8.4 +six==1.16.0 +stack-data==0.6.2 +tabulate==0.9.0 +toml==0.10.2 +torch==1.13.1 +torchvision==0.14.1 +tornado==6.1 +tqdm==4.65.0 +traitlets==5.9.0 +typing_extensions==4.4.0 +urllib3==1.26.14 +wcwidth==0.2.6 +wheel==0.38.4 +yapf==0.32.0 +yarg==0.1.9 +zipp==3.15.0 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/run.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/run.py new file mode 100644 index 0000000..3b19879 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/run.py @@ -0,0 +1,200 @@ +""" +see scripts/run_ocr.sh to run +""" +# from pathlib import Path # add parent path to run debugger +# import sys +# FILE = Path(__file__).absolute() +# sys.path.append(FILE.parents[2].as_posix()) + + +from src.utils import construct_file_path, ImageReader +from src.dto import Line +from src.ocr import OcrEngine +import argparse +import tqdm +import pandas as pd +from pathlib import Path +import json +import os +import numpy as np +from typing import Union, Tuple, List, Optional +from collections import defaultdict + +current_dir = os.getcwd() + + +def get_args(): + parser = argparse.ArgumentParser() + # parser image + parser.add_argument( + "--image", + type=str, + required=True, + help="path to input image/directory/csv file", + ) + parser.add_argument( + "--save_dir", type=str, required=True, help="path to save directory" + ) + parser.add_argument( + "--include", type=str, nargs="+", default=[], help="files/folders to include" + ) + parser.add_argument( + "--exclude", type=str, nargs="+", default=[], help="files/folders to exclude" + ) + parser.add_argument( + "--base_dir", + type=str, + required=False, + default=current_dir, + help="used when --image and --save_dir are relative paths to a base directory, default to current directory", + ) + parser.add_argument( + "--export_csv", + type=str, + required=False, + default="", + help="used when --image is a directory. If set, a csv file contains image_path, ocr_path and label will be exported to save_dir.", + ) + parser.add_argument( + "--export_img", + type=bool, + required=False, + default=False, + help="whether to save the visualize img", + ) + parser.add_argument("--ocr_kwargs", type=str, required=False, default="") + opt = parser.parse_args() + return opt + + +def load_engine(opt) -> OcrEngine: + print("[INFO] Loading engine...") + kw = json.loads(opt.ocr_kwargs) if opt.ocr_kwargs else {} + engine = OcrEngine(**kw) + print("[INFO] Engine loaded") + return engine + + +def convert_relative_path_to_positive_path(tgt_dir: Path, base_dir: Path) -> Path: + return tgt_dir if tgt_dir.is_absolute() else base_dir.joinpath(tgt_dir) + + +def get_paths_from_opt(opt) -> Tuple[Path, Path]: + # BC\ kiem\ tra\ y\ te -> BC kiem tra y te + img_path = opt.image.replace("\\ ", " ").strip() + save_dir = opt.save_dir.replace("\\ ", " ").strip() + base_dir = opt.base_dir.replace("\\ ", " ").strip() + input_image = convert_relative_path_to_positive_path(Path(img_path), Path(base_dir)) + save_dir = convert_relative_path_to_positive_path(Path(save_dir), Path(base_dir)) + if not save_dir.exists(): + save_dir.mkdir() + print("[INFO]: Creating folder ", save_dir) + return input_image, save_dir + + +def process_img( + img: Union[str, np.ndarray], + save_dir_or_path: str, + engine: OcrEngine, + export_img: bool, + save_path_deskew: Optional[str] = None, +) -> None: + save_dir_or_path = Path(save_dir_or_path) + if isinstance(img, np.ndarray): + if save_dir_or_path.is_dir(): + raise ValueError("numpy array input require a save path, not a save dir") + page = engine(img) + save_path = ( + str(save_dir_or_path.joinpath(Path(img).stem + ".txt")) + if save_dir_or_path.is_dir() + else str(save_dir_or_path) + ) + page.write_to_file("word", save_path) + if export_img: + page.save_img( + save_path.replace(".txt", ".jpg"), + is_vnese=True, + save_path_deskew=save_path_deskew, + ) + + +def process_dir( + dir_path: str, + save_dir: str, + engine: OcrEngine, + export_img: bool, + lexcludes: List[str] = [], + lincludes: List[str] = [], + ddata=defaultdict(list), +) -> None: + pdir_path = Path(dir_path) + print(pdir_path) + # save_dir_sub = Path(construct_file_path(save_dir, dir_path, ext="")) + psave_dir = Path(save_dir) + psave_dir.mkdir(exist_ok=True) + for img_path in (pbar := tqdm.tqdm(pdir_path.iterdir())): + pbar.set_description(f"Processing {pdir_path}") + if (lincludes and img_path.name not in lincludes) or ( + img_path.name in lexcludes + ): + continue # only process desired files/foders + if img_path.is_dir(): + psave_dir_sub = psave_dir.joinpath(img_path.stem) + process_dir(img_path, str(psave_dir_sub), engine, ddata) + elif img_path.suffix.lower() in ImageReader.supported_ext: + simg_path = str(img_path) + # try: + img = ( + ImageReader.read(simg_path) + if img_path.suffix != ".pdf" + else ImageReader.read(simg_path)[0] + ) + save_path = str(Path(psave_dir).joinpath(img_path.stem + ".txt")) + save_path_deskew = str( + Path(psave_dir).joinpath(img_path.stem + "_deskewed.jpg") + ) + process_img(img, save_path, engine, export_img, save_path_deskew) + # except Exception as e: + # print('[ERROR]: ', e, ' at ', simg_path) + # continue + ddata["img_path"].append(simg_path) + ddata["ocr_path"].append(save_path) + if Path(save_path_deskew).exists(): + ddata["save_path_deskew"].append(save_path) + ddata["label"].append(pdir_path.stem) + # ddata.update({"img_path": img_path, "save_path": save_path, "label": dir_path.stem}) + return ddata + + +def process_csv(csv_path: str, engine: OcrEngine) -> None: + df = pd.read_csv(csv_path) + if not "image_path" in df.columns or not "ocr_path" in df.columns: + raise AssertionError("Cannot fing image_path in df headers") + for row in df.iterrows(): + process_img(row.image_path, row.ocr_path, engine) + + +if __name__ == "__main__": + opt = get_args() + engine = load_engine(opt) + print("[INFO]: OCR engine settings:", engine.settings) + img, save_dir = get_paths_from_opt(opt) + + lskip_dir = [] + if img.is_dir(): + ddata = process_dir( + img, save_dir, engine, opt.export_img, opt.exclude, opt.include + ) + if opt.export_csv: + pd.DataFrame.from_dict(ddata).to_csv( + Path(save_dir).joinpath(opt.export_csv) + ) + elif img.suffix in ImageReader.supported_ext: + process_img(str(img), save_dir, engine, opt.export_img) + elif img.suffix == ".csv": + print( + "[WARNING]: Running with csv file will ignore the save_dir argument. Instead, the ocr_path in the csv would be used" + ) + process_csv(img, engine) + else: + raise NotImplementedError("[ERROR]: Unsupported file {}".format(img)) diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_deskew.sh b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_deskew.sh new file mode 100644 index 0000000..34ecd8c --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_deskew.sh @@ -0,0 +1,9 @@ +export CUDA_VISIBLE_DEVICES=1 +# export PATH=/usr/local/cuda-11.6/bin${PATH:+:${PATH}} +# export LD_LIBRARY_PATH=/usr/local/cuda-11.6/lib64\ {LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +# export CUDA_HOME=/usr/local/cuda-11.6 +# export PATH=/usr/local/cuda-11.6/bin:$PATH +# export CPATH=/usr/local/cuda-11.6/include:$CPATH +# export LIBRARY_PATH=/usr/local/cuda-11.6/lib64:$LIBRARY_PATH +# export LD_LIBRARY_PATH=/usr/local/cuda-11.6/lib64:/usr/local/cuda-11.6/extras/CUPTI/lib64:$LD_LIBRARY_PATH +python test/test_deskew_dir.py \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_ocr.sh b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_ocr.sh new file mode 100644 index 0000000..8ade432 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/scripts/run_ocr.sh @@ -0,0 +1,49 @@ + + +#bash scripts/run_ocr.sh -i /mnt/hdd2T/AICR/Projects/2023/FWD/Forms/PDFs/ -o /mnt/ssd1T/hungbnt/DocumentClassification/results/ocr -e out.csv -k "{\"device\":\"cuda:1\"}" -p True -n Passport 'So\ HK' +#bash scripts/run_ocr.sh -i '/mnt/hdd2T/AICR/Projects/2023/FWD/Forms/PDFs/So\ HK' -o /mnt/ssd1T/hungbnt/DocumentClassification/results/ocr -e out.csv -k "{\"device\":\"cuda:1\"}" -p True +#-n and -x do not accept multiple argument currently + + +# bash scripts/run_ocr.sh -i /mnt/hdd4T/OCR/hoangdc/End_to_end/ICDAR2013/data/images_receipt_5images/ -o visualize/ -e out.csv -k "{\"device\":\"cuda:1\"}" -p True + +export PYTHONWARNINGS="ignore" + +while getopts i:o:b:e:p:k:n:x: flag +do + case "${flag}" in + i) img=${OPTARG};; + o) out_dir=${OPTARG};; + b) base_dir=${OPTARG};; + e) export_csv=${OPTARG};; + p) export_img=${OPTARG};; + k) ocr_kwargs=${OPTARG};; + n) include=("${OPTARG[@]}");; + x) exclude=("${OPTARG[@]}");; + esac +done + +cmd="python run.py \ + --image $img \ + --save_dir $out_dir \ + --export_csv $export_csv \ + --export_img $export_img \ + --ocr_kwargs $ocr_kwargs" + +if [ ${#include[@]} -gt 0 ]; then + cmd+=" --include" + for item in "${include[@]}"; do + cmd+=" $item" + done +fi + +if [ ${#exclude[@]} -gt 0 ]; then + cmd+=" --exclude" + for item in "${exclude[@]}"; do + cmd+=" $item" + done +fi + + +echo $cmd +exec $cmd diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/settings.yml b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/settings.yml new file mode 100644 index 0000000..9107bb1 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/settings.yml @@ -0,0 +1,35 @@ +device: &device cuda:0 +max_img_size: [1920,1920] #text det default size: 1280x1280 #[] = originla size, TODO: fix the deskew code to resize the image only for detecting the angle, we want to feed the original size image to the text detection pipeline so that the bounding boxes would be mapped back to the original size +extend_bbox: [0, 0.0, 0.0, 0.0] # left, top, right, bottom +batch_size: 1 #1 means batch_mode = False +detector: + # version: /mnt/hdd2T/datnt/datnt_from_ssd1T/mmdetection/wild_receipt_finetune_weights_c_lite.pth + version: /mnt/hdd4T/OCR/datnt/mmdetection/logs/textdet-baseline-Oct04-wildreceiptv4-sdsapv1-mcocr-ssreceipt/epoch_100_params.pth + auto_rotate: True + rotator_version: /mnt/hdd2T/datnt/datnt_from_ssd1T/mmdetection/logs/textdet-with-rotate-20230317/best_bbox_mAP_epoch_30_lite.pth + device: *device + +recognizer: + version: satrn-lite-general-pretrain-20230106 + max_seq_len_overwrite: 24 #default = 12 + return_confident: True + device: *device +#extend the bbox to avoid losing accent mark in vietnames, if using ocr for only english, disable it + +deskew: + enable: True + text_detector: + config: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/config/det.yaml + weight: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_PP-OCRv3_det_infer + text_cls: + config: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/config/cls.yaml + weight: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_ppocr_mobile_v2.0_cls_infer + device: *device + + +words_to_lines: + gradient: 0.6 + max_x_dist: 20 + max_running_y_shift_degree: 10 #degrees + y_overlap_threshold: 0.5 + word_formation_mode: line diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/dto.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/dto.py new file mode 100644 index 0000000..8dae901 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/dto.py @@ -0,0 +1,534 @@ +import numpy as np +from typing import Optional, List, Union +import cv2 +from PIL import Image +from pathlib import Path +from .utils import visualize_bbox_and_label + + +class Box: + def __init__( + self, x1: int, y1: int, x2: int, y2: int, conf: float = -1.0, label: str = "" + ): + self._x1 = x1 + self._y1 = y1 + self._x2 = x2 + self._y2 = y2 + self._conf = conf + self._label = label + + def __repr__(self) -> str: + return str(self.bbox) + + def __str__(self) -> str: + return str(self.bbox) + + def get(self, return_confidence=False) -> Union[list[int], list[Union[float, int]]]: + return self.bbox if not return_confidence else self.xyxyc + + def __getitem__(self, key): + return self.bbox[key] + + @property + def width(self): + return max(self._x2 - self._x1, -1) + + @property + def height(self): + return max(self._y2 - self._y1, -1) + + @property + def bbox(self) -> list[int]: + return [self._x1, self._y1, self._x2, self._y2] + + @bbox.setter + def bbox(self, bbox_: list[int]): + self._x1, self._y1, self._x2, self._y2 = bbox_ + + @property + def xyxyc(self) -> list[Union[float, int]]: + return [self._x1, self._y1, self._x2, self._y2, self._conf] + + @staticmethod + def normalize_bbox(bbox: list[int]) -> list[int]: + return [int(b) for b in bbox] + + def to_int(self): + self._x1, self._y1, self._x2, self._y2 = self.normalize_bbox( + [self._x1, self._y1, self._x2, self._y2] + ) + return self + + @staticmethod + def clamp_bbox_by_img_wh(bbox: list, width: int, height: int) -> list[int]: + x1, y1, x2, y2 = bbox + x1 = min(max(0, x1), width) + x2 = min(max(0, x2), width) + y1 = min(max(0, y1), height) + y2 = min(max(0, y2), height) + return [x1, y1, x2, y2] + + def clamp_by_img_wh(self, width: int, height: int): + self._x1, self._y1, self._x2, self._y2 = self.clamp_bbox_by_img_wh( + [self._x1, self._y1, self._x2, self._y2], width, height + ) + return self + + @staticmethod + def extend_bbox(bbox: list, margin: list): # -> Self (python3.11) + margin_l, margin_t, margin_r, margin_b = margin + l, t, r, b = bbox # left, top, right, bottom + t = t - (b - t) * margin_t + b = b + (b - t) * margin_b + l = l - (r - l) * margin_l + r = r + (r - l) * margin_r + return [l, t, r, b] + + def get_extend_bbox(self, margin: list): + extended_bbox = self.extend_bbox(self.bbox, margin) + return Box(*extended_bbox, label=self._label) + + @staticmethod + def bbox_is_valid(bbox: list[int]) -> bool: + if bbox == [-1, -1, -1, -1]: + raise ValueError("Empty bounding box found") + l, t, r, b = bbox # left, top, right, bottom + return True if (b - t) * (r - l) > 0 else False + + def is_valid(self) -> bool: + return self.bbox_is_valid(self.bbox) + + @staticmethod + def crop_img_by_bbox(img: np.ndarray, bbox: list) -> np.ndarray: + l, t, r, b = bbox + return img[t:b, l:r] + + def crop_img(self, img: np.ndarray) -> np.ndarray: + return self.crop_img_by_bbox(img, self.bbox) + + +class Word: + def __init__( + self, + image=None, + text="", + conf_cls=-1.0, + bbox_obj: Box = Box(-1, -1, -1, -1), + conf_detect=-1.0, + kie_label="", + ): + # self.type = "word" + self._text = text + self._image = image + self._conf_det = conf_detect + self._conf_cls = conf_cls + # [left, top,right,bot] coordinate of top-left and bottom-right point + self._bbox_obj = bbox_obj + # self.word_id = 0 # id of word + # self.word_group_id = 0 # id of word_group which instance belongs to + # self.line_id = 0 # id of line which instance belongs to + # self.paragraph_id = 0 # id of line which instance belongs to + self._kie_label = kie_label + + @property + def bbox(self) -> list[int]: + return self._bbox_obj.bbox + + @property + def text(self) -> str: + return self._text + + @property + def height(self): + return self._bbox_obj.height + + @property + def width(self): + return self._bbox_obj.width + + def __repr__(self) -> str: + return self._text + + def __str__(self) -> str: + return self._text + + def is_valid(self) -> bool: + return self._bbox_obj.is_valid() + + # def is_special_word(self): + # if not self._text: + # raise ValueError("Cannot validatie size of empty bounding box") + + # # if len(text) > 7: + # # return True + # if len(self._text) >= 7: + # no_digits = sum(c.isdigit() for c in text) + # return no_digits / len(text) >= 0.3 + + # return False + + +class WordGroup: + def __init__( + self, + list_words: List[Word] = list(), + text: str = "", + boundingbox: Box = Box(-1, -1, -1, -1), + conf_cls: float = -1, + conf_det: float = -1, + ): + # self.type = "word_group" + self._list_words = list_words # dict of word instances + # self.word_group_id = 0 # word group id + # self.line_id = 0 # id of line which instance belongs to + # self.paragraph_id = 0 # id of paragraph which instance belongs to + self._text = text + self._bbox_obj = boundingbox + self._kie_label = "" + self._conf_cls = conf_cls + self._conf_det = conf_det + + @property + def bbox(self) -> list[int]: + return self._bbox_obj.bbox + + @property + def text(self) -> str: + return self._text + + @property + def list_words(self) -> list[Word]: + return self._list_words + + def __repr__(self) -> str: + return self._text + + def __str__(self) -> str: + return self._text + + # def add_word(self, word: Word): # add a word instance to the word_group + # if word._text != "✪": + # for w in self._list_words: + # if word.word_id == w.word_id: + # print("Word id collision") + # return False + # word.word_group_id = self.word_group_id # + # word.line_id = self.line_id + # word.paragraph_id = self.paragraph_id + # self._list_words.append(word) + # self._text += " " + word._text + # if self.bbox_obj == [-1, -1, -1, -1]: + # self.bbox_obj = word._bbox_obj + # else: + # self.bbox_obj = [ + # min(self.bbox_obj[0], word._bbox_obj[0]), + # min(self.bbox_obj[1], word._bbox_obj[1]), + # max(self.bbox_obj[2], word._bbox_obj[2]), + # max(self.bbox_obj[3], word._bbox_obj[3]), + # ] + # return True + # else: + # return False + + # def update_word_group_id(self, new_word_group_id): + # self.word_group_id = new_word_group_id + # for i in range(len(self._list_words)): + # self._list_words[i].word_group_id = new_word_group_id + + # def update_kie_label(self): + # list_kie_label = [word._kie_label for word in self._list_words] + # dict_kie = dict() + # for label in list_kie_label: + # if label not in dict_kie: + # dict_kie[label] = 1 + # else: + # dict_kie[label] += 1 + # total = len(list(dict_kie.values())) + # max_value = max(list(dict_kie.values())) + # list_keys = list(dict_kie.keys()) + # list_values = list(dict_kie.values()) + # self.kie_label = list_keys[list_values.index(max_value)] + + # def update_text(self): # update text after changing positions of words in list word + # text = "" + # for word in self._list_words: + # text += " " + word._text + # self._text = text + + +class Line: + def __init__( + self, + list_word_groups: List[WordGroup] = [], + text: str = "", + boundingbox: Box = Box(-1, -1, -1, -1), + conf_cls: float = -1, + conf_det: float = -1, + ): + # self.type = "line" + self._list_word_groups = ( + list_word_groups # list of Word_group instances in the line + ) + # self.line_id = 0 # id of line in the paragraph + # self.paragraph_id = 0 # id of paragraph which instance belongs to + self._text = text + self._bbox_obj = boundingbox + self._conf_cls = conf_cls + self._conf_det = conf_det + + @property + def bbox(self) -> list[int]: + return self._bbox_obj.bbox + + @property + def text(self) -> str: + return self._text + + @property + def list_word_groups(self) -> List[WordGroup]: + return self._list_word_groups + + @property + def list_words(self) -> list[Word]: + return [ + word + for word_group in self._list_word_groups + for word in word_group.list_words + ] + + def __repr__(self) -> str: + return self._text + + def __str__(self) -> str: + return self._text + + # def add_group(self, word_group: WordGroup): # add a word_group instance + # if word_group._list_words is not None: + # for wg in self.list_word_groups: + # if word_group.word_group_id == wg.word_group_id: + # print("Word_group id collision") + # return False + + # self.list_word_groups.append(word_group) + # self.text += word_group._text + # word_group.paragraph_id = self.paragraph_id + # word_group.line_id = self.line_id + + # for i in range(len(word_group._list_words)): + # word_group._list_words[ + # i + # ].paragraph_id = self.paragraph_id # set paragraph_id for word + # word_group._list_words[i].line_id = self.line_id # set line_id for word + # return True + # return False + + # def update_line_id(self, new_line_id): + # self.line_id = new_line_id + # for i in range(len(self.list_word_groups)): + # self.list_word_groups[i].line_id = new_line_id + # for j in range(len(self.list_word_groups[i]._list_words)): + # self.list_word_groups[i]._list_words[j].line_id = new_line_id + + # def merge_word(self, word): # word can be a Word instance or a Word_group instance + # if word.text != "✪": + # if self.boundingbox == [-1, -1, -1, -1]: + # self.boundingbox = word.boundingbox + # else: + # self.boundingbox = [ + # min(self.boundingbox[0], word.boundingbox[0]), + # min(self.boundingbox[1], word.boundingbox[1]), + # max(self.boundingbox[2], word.boundingbox[2]), + # max(self.boundingbox[3], word.boundingbox[3]), + # ] + # self.list_word_groups.append(word) + # self.text += " " + word.text + # return True + # return False + + # def __cal_ratio(self, top1, bottom1, top2, bottom2): + # sorted_vals = sorted([top1, bottom1, top2, bottom2]) + # intersection = sorted_vals[2] - sorted_vals[1] + # min_height = min(bottom1 - top1, bottom2 - top2) + # if min_height == 0: + # return -1 + # ratio = intersection / min_height + # return ratio + + # def __cal_ratio_height(self, top1, bottom1, top2, bottom2): + + # height1, height2 = top1 - bottom1, top2 - bottom2 + # ratio_height = float(max(height1, height2)) / float(min(height1, height2)) + # return ratio_height + + # def in_same_line(self, input_line, thresh=0.7): + # # calculate iou in vertical direction + # _, top1, _, bottom1 = self.boundingbox + # _, top2, _, bottom2 = input_line.boundingbox + + # ratio = self.__cal_ratio(top1, bottom1, top2, bottom2) + # ratio_height = self.__cal_ratio_height(top1, bottom1, top2, bottom2) + + # if ( + # (top2 <= top1 <= bottom2) or (top1 <= top2 <= bottom1) + # and ratio >= thresh + # and (ratio_height < 2) + # ): + # return True + # return False + + +# class Paragraph: +# def __init__(self, id=0, lines=None): +# self.list_lines = lines if lines is not None else [] # list of all lines in the paragraph +# self.paragraph_id = id # index of paragraph in the ist of paragraph +# self.text = "" +# self.boundingbox = [-1, -1, -1, -1] + +# @property +# def bbox(self): +# return self.boundingbox + +# def __repr__(self) -> str: +# return self.text + +# def __str__(self) -> str: +# return self.text + +# def add_line(self, line: Line): # add a line instance +# if line.list_word_groups is not None: +# for l in self.list_lines: +# if line.line_id == l.line_id: +# print("Line id collision") +# return False +# for i in range(len(line.list_word_groups)): +# line.list_word_groups[ +# i +# ].paragraph_id = ( +# self.paragraph_id +# ) # set paragraph id for every word group in line +# for j in range(len(line.list_word_groups[i]._list_words)): +# line.list_word_groups[i]._list_words[ +# j +# ].paragraph_id = ( +# self.paragraph_id +# ) # set paragraph id for every word in word groups +# line.paragraph_id = self.paragraph_id # set paragraph id for line +# self.list_lines.append(line) # add line to paragraph +# self.text += " " + line.text +# return True +# else: +# return False + +# def update_paragraph_id( +# self, new_paragraph_id +# ): # update new paragraph_id for all lines, word_groups, words inside paragraph +# self.paragraph_id = new_paragraph_id +# for i in range(len(self.list_lines)): +# self.list_lines[ +# i +# ].paragraph_id = new_paragraph_id # set new paragraph_id for line +# for j in range(len(self.list_lines[i].list_word_groups)): +# self.list_lines[i].list_word_groups[ +# j +# ].paragraph_id = new_paragraph_id # set new paragraph_id for word_group +# for k in range(len(self.list_lines[i].list_word_groups[j].list_words)): +# self.list_lines[i].list_word_groups[j].list_words[ +# k +# ].paragraph_id = new_paragraph_id # set new paragraph id for word +# return True + + +class Page: + def __init__( + self, + word_segments: Union[List[WordGroup], List[Line]], + image: np.ndarray, + deskewed_image: Optional[np.ndarray] = None, + ) -> None: + self._word_segments = word_segments + self._image = image + self._deskewed_image = deskewed_image + self._drawed_image: Optional[np.ndarray] = None + + @property + def word_segments(self): + return self._word_segments + + @property + def list_words(self) -> list[Word]: + return [ + word + for word_segment in self._word_segments + for word in word_segment.list_words + ] + + @property + def image(self): + return self._image + + @property + def PIL_image(self): + return Image.fromarray(self._image) + + @property + def drawed_image(self): + return self._drawed_image + + @property + def deskewed_image(self): + return self._deskewed_image + + def visualize_bbox_and_label(self, **kwargs: dict): + if self._drawed_image is not None: + return self._drawed_image + bboxes = list() + texts = list() + for word in self.list_words: + bboxes.append([int(float(b)) for b in word.bbox]) + texts.append(word._text) + img = visualize_bbox_and_label( + self._deskewed_image if self._deskewed_image is not None else self._image, + bboxes, + texts, + **kwargs + ) + self._drawed_image = img + return self._drawed_image + + def save_img(self, save_path: str, **kwargs: dict) -> None: + save_path_deskew = kwargs.pop("save_path_deskew", Path(save_path).with_stem(Path(save_path).stem + "_deskewed").as_posix()) + if self._deskewed_image is not None: + # save_path_deskew: str = kwargs.pop("save_path_deskew", Path(save_path).with_stem(Path(save_path).stem + "_deskewed").as_posix()) # type: ignore + cv2.imwrite(save_path_deskew, self._deskewed_image) + + img = self.visualize_bbox_and_label(**kwargs) + cv2.imwrite(save_path, img) + + + def write_to_file(self, mode: str, save_path: str) -> None: + f = open(save_path, "w+", encoding="utf-8") + for word_segment in self._word_segments: + if mode == "segment": + xmin, ymin, xmax, ymax = word_segment.bbox + f.write( + "{}\t{}\t{}\t{}\t{}\n".format( + xmin, ymin, xmax, ymax, word_segment._text + ) + ) + elif mode == "word": + for word in word_segment.list_words: + # xmin, ymin, xmax, ymax = word.bbox + xmin, ymin, xmax, ymax = [int(float(b)) for b in word.bbox] + f.write( + "{}\t{}\t{}\t{}\t{}\n".format( + xmin, ymin, xmax, ymax, word._text + ) + ) + else: + raise NotImplementedError("Unknown mode: {}".format(mode)) + f.close() + + +class Document: + def __init__(self, lpages: List[Page]) -> None: + self.lpages = lpages diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/ocr.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/ocr.py new file mode 100644 index 0000000..280d0b2 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/ocr.py @@ -0,0 +1,258 @@ +from typing import Union, overload, List, Optional, Tuple +from PIL import Image +import torch +import numpy as np +import yaml +from pathlib import Path +import mmcv +from sdsvtd import StandaloneYOLOXRunner +from sdsvtr import StandaloneSATRNRunner +from sdsv_dewarp.api import AlignImage + +from .utils import ImageReader, chunks, Timer, post_process_recog # rotate_bbox + +# from .utils import jdeskew as deskew +# from externals.deskew.sdsv_dewarp import pdeskew as deskew +# from .utils import deskew +from .dto import Word, Line, Page, Document, Box, WordGroup + +# from .word_formation import words_to_lines as words_to_lines +# from .word_formation import wo rds_to_lines_mmocr as words_to_lines +from .word_formation import words_formation_mmocr_tesseract as word_formation + +DEFAULT_SETTING_PATH = str(Path(__file__).parents[1]) + "/settings.yml" + + +class OcrEngine: + def __init__(self, settings_file: str = DEFAULT_SETTING_PATH, **kwargs): + """Warper of text detection and text recognition + :param settings_file: path to default setting file + :param kwargs: keyword arguments to overwrite the default settings file + """ + with open(settings_file) as f: + # use safe_load instead load + self._settings = yaml.safe_load(f) + self._update_configs(kwargs) + + self._ensure_device() + self._detector = StandaloneYOLOXRunner(**self._settings["detector"]) + self._recognizer = StandaloneSATRNRunner(**self._settings["recognizer"]) + self._deskewer = self._load_deskewer() + + def _update_configs(self, params): + for key, para in params.items(): # overwrite default settings by keyword arguments + if key not in self._settings: + raise ValueError("Invalid setting found in OcrEngine: ", k) + if key == "device": + self._settings[key] = para + self._settings["detector"][key] = para + self._settings["recognizer"][key] = para + self._settings["deskew"][key] = para + else: + for k, v in para.items(): + if isinstance(v, dict): + for sub_key, sub_value in v.items(): + self._settings[key][k][sub_key] = sub_value + else: + self._settings[key][k] = v + + def _load_deskewer(self) -> Optional[AlignImage]: + if self._settings["deskew"]["enable"]: + deskewer = AlignImage( + **{k: v for k, v in self._settings["deskew"].items() if k != "enable"} + ) + print( + "[WARNING]: Deskew is enabled. The bounding boxes prediction may not be aligned with the original image. In case of using these predictions for pseudo-label, turn on save_deskewed option and use the saved deskewed images instead for further proceed." + ) + return deskewer + return None + + def _ensure_device(self): + if "cuda" in self._settings["device"]: + if not torch.cuda.is_available(): + print("[WARNING]: CUDA is not available, running with cpu instead") + self._settings["device"] = "cpu" + + @property + def version(self): + return { + "detector": self._settings["detector"], + "recognizer": self._settings["recognizer"], + } + + @property + def settings(self): + return self._settings + + # @staticmethod + # def xyxyc_to_xyxy_c(xyxyc: np.ndarray) -> Tuple[List[list], list]: + # ''' + # convert sdsvtd yoloX detection output to list of bboxes and list of confidences + # @param xyxyc: array of shape (n, 5) + # ''' + # xyxy = xyxyc[:, :4].tolist() + # confs = xyxyc[:, 4].tolist() + # return xyxy, confs + # -> Tuple[np.ndarray, List[Box]]: + + def preprocess(self, img: np.ndarray) -> tuple[np.ndarray, bool, float]: + img_ = img.copy() + if self._settings["max_img_size"]: + img_ = mmcv.imrescale( + img, + tuple(self._settings["max_img_size"]), + return_scale=False, + interpolation="bilinear", + backend="cv2", + ) + is_blank = False + if self._deskewer: + with Timer("deskew"): + img_, is_blank, angle = self._deskewer(img_) + return img, is_blank, angle # replace img_ to img + # for i, bbox in enumerate(bboxes): + # rotated_bbox = rotate_bbox(bbox, angle, img.shape[:2]) + # bboxes[i].bbox = rotated_bbox + return img, is_blank, 0 + + def run_detect( + self, img: np.ndarray, return_raw: bool = False + ) -> Tuple[np.ndarray, Union[List[Box], List[list]]]: + """ + run text detection and return list of xyxyc if return_confidence is True, otherwise return a list of xyxy + """ + pred_det = self._detector(img) + if self._settings["detector"]["auto_rotate"]: + img, pred_det = pred_det + pred_det = pred_det[0] # only image at a time + return ( + (img, pred_det.tolist()) + if return_raw + else (img, [Box(*xyxyc) for xyxyc in pred_det.tolist()]) + ) + + def run_recog( + self, imgs: List[np.ndarray] + ) -> Union[List[str], List[Tuple[str, float]]]: + if len(imgs) == 0: + return list() + pred_rec = self._recognizer(imgs) + return [ + (post_process_recog(word), conf) + for word, conf in zip(pred_rec[0], pred_rec[1]) + ] + + def read_img(self, img: str) -> np.ndarray: + return ImageReader.read(img) + + def get_cropped_imgs( + self, img: np.ndarray, bboxes: Union[List[Box], List[list]] + ) -> Tuple[List[np.ndarray], List[bool]]: + """ + img: np image + bboxes: list of xyxy + """ + lcropped_imgs = list() + mask = list() + for bbox in bboxes: + bbox = Box(*bbox) if isinstance(bbox, list) else bbox + bbox = bbox.get_extend_bbox(self._settings["extend_bbox"]) + + bbox.clamp_by_img_wh(img.shape[1], img.shape[0]) + bbox.to_int() + if not bbox.is_valid(): + mask.append(False) + continue + cropped_img = bbox.crop_img(img) + lcropped_imgs.append(cropped_img) + mask.append(True) + return lcropped_imgs, mask + + def read_page( + self, img: np.ndarray, bboxes: Union[List[Box], List[list]] + ) -> Union[List[WordGroup], List[Line]]: + if len(bboxes) == 0: # no bbox found + return list() + with Timer("cropped imgs"): + lcropped_imgs, mask = self.get_cropped_imgs(img, bboxes) + with Timer("recog"): + # batch_mode for efficiency + pred_recs = self.run_recog(lcropped_imgs) + with Timer("construct words"): + lwords = list() + for i in range(len(pred_recs)): + if not mask[i]: + continue + text, conf_rec = pred_recs[i][0], pred_recs[i][1] + bbox = Box(*bboxes[i]) if isinstance(bboxes[i], list) else bboxes[i] + lwords.append( + Word( + image=img, + text=text, + conf_cls=conf_rec, + bbox_obj=bbox, + conf_detect=bbox._conf, + ) + ) + with Timer("word formation"): + return word_formation( + lwords, img.shape[1], **self._settings["words_to_lines"] + )[0] + + # https://stackoverflow.com/questions/48127642/incompatible-types-in-assignment-on-union + + @overload + def __call__(self, img: Union[str, np.ndarray, Image.Image]) -> Page: + ... + + @overload + def __call__(self, img: List[Union[str, np.ndarray, Image.Image]]) -> Document: + ... + + def __call__(self, img): # type: ignore #ignoring type before implementing batch_mode + """ + Accept an image or list of them, return ocr result as a page or document + """ + with Timer("read image"): + img = ImageReader.read(img) + if self._settings["batch_size"] == 1: + if isinstance(img, list): + if len(img) == 1: + img = img[0] # in case input type is a 1 page pdf + else: + raise AssertionError( + "list input can only be used with batch_mode enabled" + ) + img_deskewed, is_blank, angle = self.preprocess(img) + + if is_blank: + print( + "[WARNING]: Blank image detected" + ) # TODO: should we stop the execution here? + with Timer("detect"): + img_deskewed, bboxes = self.run_detect(img_deskewed) + with Timer("read_page"): + lsegments = self.read_page(img_deskewed, bboxes) + return Page(lsegments, img, img_deskewed if angle != 0 else None) + else: + # lpages = [] + # # chunks to reduce memory footprint + # for imgs in chunks(img, self._batch_size): + # # pred_dets = self._detector(imgs) + # # TEMP: use list comprehension because sdsvtd do not support batch mode of text detection + # img = self.preprocess(img) + # img, bboxes = self.run_detect(img) + # for img_, bboxes_ in zip(imgs, bboxes): + # llines = self.read_page(img, bboxes_) + # page = Page(llines, img) + # lpages.append(page) + # return Document(lpages) + raise NotImplementedError("Batch mode is currently not supported") + + +if __name__ == "__main__": + img_path = "/mnt/ssd1T/hungbnt/Cello/data/PH/Sea7/Sea_7_1.jpg" + engine = OcrEngine(device="cuda:0") + # https://stackoverflow.com/questions/66435480/overload-following-optional-argument + page = engine(img_path) # type: ignore + print(page._word_segments) diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/utils.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/utils.py new file mode 100644 index 0000000..3c4332e --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/utils.py @@ -0,0 +1,369 @@ +from PIL import ImageFont, ImageDraw, Image, ImageOps +# import matplotlib.pyplot as plt +import numpy as np +import cv2 +import os +import time +from typing import Generator, Union, List, overload, Tuple, Callable +import glob +import math +from pathlib import Path +from pdf2image import convert_from_path +# from deskew import determine_skew +# from jdeskew.estimator import get_angle +# from jdeskew.utility import rotate as jrotate + + +def post_process_recog(text: str) -> str: + text = text.replace("✪", " ") + return text + + +def find_maximum_without_outliers(lst: list[int], threshold: float = 1.): + ''' + To find the maximum number in a list while excluding its outlier values, you can follow these steps: + Determine the range within which you consider values as outliers. This can be based on a specific threshold or a statistical measure such as the interquartile range (IQR). + Iterate through the list and filter out the outlier values based on the defined range. Keep track of the non-outlier values. + Find the maximum value among the non-outlier values. + ''' + # Calculate the lower and upper boundaries for outliers + q1 = np.percentile(lst, 25) + q3 = np.percentile(lst, 75) + iqr = q3 - q1 + lower_bound = q1 - threshold * iqr + upper_bound = q3 + threshold * iqr + + # Filter out outlier values + non_outliers = [x for x in lst if lower_bound <= x <= upper_bound] + + # Find the maximum value among non-outliers + max_value = max(non_outliers) + + return max_value + + +class Timer: + def __init__(self, name: str) -> None: + self.name = name + + def __enter__(self): + self.start_time = time.perf_counter() + return self + + def __exit__(self, func: Callable, *args): + self.end_time = time.perf_counter() + self.elapsed_time = self.end_time - self.start_time + print(f"[INFO]: {self.name} took : {self.elapsed_time:.6f} seconds") + + +# def rotate( +# image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]] +# ) -> np.ndarray: +# old_width, old_height = image.shape[:2] +# angle_radian = math.radians(angle) +# width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width) +# height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height) +# image_center = tuple(np.array(image.shape[1::-1]) / 2) +# rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) +# rot_mat[1, 2] += (width - old_width) / 2 +# rot_mat[0, 2] += (height - old_height) / 2 +# return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background) + + +# def rotate_bbox(bbox: list, angle: float) -> list: +# # Compute the center point of the bounding box +# cx = bbox[0] + bbox[2] / 2 +# cy = bbox[1] + bbox[3] / 2 + +# # Define the scale factor for the rotated bounding box +# scale = 1.0 # following the deskew and jdeskew function +# angle_radian = math.radians(angle) + +# # Obtain the rotation matrix using cv2.getRotationMatrix2D() +# M = cv2.getRotationMatrix2D((cx, cy), angle_radian, scale) + +# # Apply the rotation matrix to the four corners of the bounding box +# corners = np.array([[bbox[0], bbox[1]], +# [bbox[0] + bbox[2], bbox[1]], +# [bbox[0] + bbox[2], bbox[1] + bbox[3]], +# [bbox[0], bbox[1] + bbox[3]]], dtype=np.float32) +# rotated_corners = cv2.transform(np.array([corners]), M)[0] + +# # Compute the bounding box of the rotated corners +# x = int(np.min(rotated_corners[:, 0])) +# y = int(np.min(rotated_corners[:, 1])) +# w = int(np.max(rotated_corners[:, 0]) - np.min(rotated_corners[:, 0])) +# h = int(np.max(rotated_corners[:, 1]) - np.min(rotated_corners[:, 1])) +# rotated_bbox = [x, y, w, h] + +# return rotated_bbox + +# def rotate_bbox(bbox: List[int], angle: float, old_shape: Tuple[int, int]) -> List[int]: +# # https://medium.com/@pokomaru/image-and-bounding-box-rotation-using-opencv-python-2def6c39453 +# bbox_ = [bbox[0], bbox[1], bbox[2], bbox[1], bbox[2], bbox[3], bbox[0], bbox[3]] +# h, w = old_shape +# cx, cy = (int(w / 2), int(h / 2)) + +# bbox_tuple = [ +# (bbox_[0], bbox_[1]), +# (bbox_[2], bbox_[3]), +# (bbox_[4], bbox_[5]), +# (bbox_[6], bbox_[7]), +# ] # put x and y coordinates in tuples, we will iterate through the tuples and perform rotation + +# rotated_bbox = [] + +# for i, coord in enumerate(bbox_tuple): +# M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0) +# cos, sin = abs(M[0, 0]), abs(M[0, 1]) +# newW = int((h * sin) + (w * cos)) +# newH = int((h * cos) + (w * sin)) +# M[0, 2] += (newW / 2) - cx +# M[1, 2] += (newH / 2) - cy +# v = [coord[0], coord[1], 1] +# adjusted_coord = np.dot(M, v) +# rotated_bbox.insert(i, (adjusted_coord[0], adjusted_coord[1])) +# result = [int(x) for t in rotated_bbox for x in t] +# return [result[i] for i in [0, 1, 2, -1]] # reformat to xyxy + + +# def deskew(image: np.ndarray) -> Tuple[np.ndarray, float]: +# grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) +# angle = 0. +# try: +# angle = determine_skew(grayscale) +# except Exception: +# pass +# rotated = rotate(image, angle, (0, 0, 0)) if angle else image +# return rotated, angle + + +# def jdeskew(image: np.ndarray) -> Tuple[np.ndarray, float]: +# angle = 0. +# try: +# angle = get_angle(image) +# except Exception: +# pass +# # TODO: change resize = True and scale the bounding box +# rotated = jrotate(image, angle, resize=False) if angle else image +# return rotated, angle +# def deskew() + +class ImageReader: + """ + accept anything, return numpy array image + """ + supported_ext = [".png", ".jpg", ".jpeg", ".pdf", ".gif"] + + @staticmethod + def validate_img_path(img_path: str) -> None: + if not os.path.exists(img_path): + raise FileNotFoundError(img_path) + if os.path.isdir(img_path): + raise IsADirectoryError(img_path) + if not Path(img_path).suffix.lower() in ImageReader.supported_ext: + raise NotImplementedError("Not supported extension at {}".format(img_path)) + + @overload + @staticmethod + def read(img: Union[str, np.ndarray, Image.Image]) -> np.ndarray: ... + + @overload + @staticmethod + def read(img: List[Union[str, np.ndarray, Image.Image]]) -> List[np.ndarray]: ... + + @overload + @staticmethod + def read(img: str) -> List[np.ndarray]: ... # for pdf or directory + + @staticmethod + def read(img): + if isinstance(img, list): + return ImageReader.from_list(img) + elif isinstance(img, str) and os.path.isdir(img): + return ImageReader.from_dir(img) + elif isinstance(img, str) and img.endswith(".pdf"): + return ImageReader.from_pdf(img) + else: + return ImageReader._read(img) + + @staticmethod + def from_dir(dir_path: str) -> List[np.ndarray]: + if os.path.isdir(dir_path): + image_files = glob.glob(os.path.join(dir_path, "*")) + return ImageReader.from_list(image_files) + else: + raise NotADirectoryError(dir_path) + + @staticmethod + def from_str(img_path: str) -> np.ndarray: + ImageReader.validate_img_path(img_path) + return ImageReader.from_PIL(Image.open(img_path)) + + @staticmethod + def from_np(img_array: np.ndarray) -> np.ndarray: + return img_array + + @staticmethod + def from_PIL(img_pil: Image.Image, transpose=True) -> np.ndarray: + # if img_pil.is_animated: + # raise NotImplementedError("Only static images are supported, animated image found") + if transpose: + img_pil = ImageOps.exif_transpose(img_pil) + if img_pil.mode != "RGB": + img_pil = img_pil.convert("RGB") + + return np.array(img_pil) + + @staticmethod + def from_list(img_list: List[Union[str, np.ndarray, Image.Image]]) -> List[np.ndarray]: + limgs = list() + for img_path in img_list: + try: + if isinstance(img_path, str): + ImageReader.validate_img_path(img_path) + limgs.append(ImageReader._read(img_path)) + except (FileNotFoundError, NotImplementedError, IsADirectoryError) as e: + print("[ERROR]: ", e) + print("[INFO]: Skipping image {}".format(img_path)) + return limgs + + @staticmethod + def from_pdf(pdf_path: str, start_page: int = 0, end_page: int = 0) -> List[np.ndarray]: + pdf_file = convert_from_path(pdf_path) + if end_page is not None: + end_page = min(len(pdf_file), end_page + 1) + limgs = [np.array(pdf_page) for pdf_page in pdf_file[start_page:end_page]] + return limgs + + @staticmethod + def _read(img: Union[str, np.ndarray, Image.Image]) -> np.ndarray: + if isinstance(img, str): + return ImageReader.from_str(img) + elif isinstance(img, Image.Image): + return ImageReader.from_PIL(img) + elif isinstance(img, np.ndarray): + return ImageReader.from_np(img) + else: + raise ValueError("Invalid img argument type: ", type(img)) + + +def get_name(file_path, ext: bool = True): + file_path_ = os.path.basename(file_path) + return file_path_ if ext else os.path.splitext(file_path_)[0] + + +def construct_file_path(dir, file_path, ext=''): + ''' + args: + dir: /path/to/dir + file_path /example_path/to/file.txt + ext = '.json' + return + /path/to/dir/file.json + ''' + return os.path.join( + dir, get_name(file_path, + True)) if ext == '' else os.path.join( + dir, get_name(file_path, + False)) + ext + + +def chunks(lst: list, n: int) -> Generator: + """ + Yield successive n-sized chunks from lst. + https://stackoverflow.com/questions/312443/how-do-i-split-a-list-into-equally-sized-chunks + """ + for i in range(0, len(lst), n): + yield lst[i:i + n] + + +def read_ocr_result_from_txt(file_path: str) -> Tuple[list, list]: + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + boxes, words = [], [] + for line in lines: + if line == "": + continue + x1, y1, x2, y2, text = line.split("\t") + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + + +def get_xyxywh_base_on_format(bbox, format): + if format == "xywh": + x1, y1, w, h = bbox[0], bbox[1], bbox[2], bbox[3] + x2, y2 = x1 + w, y1 + h + elif format == "xyxy": + x1, y1, x2, y2 = bbox + w, h = x2 - x1, y2 - y1 + else: + raise NotImplementedError("Invalid format {}".format(format)) + return (x1, y1, x2, y2, w, h) + + +def get_dynamic_params_for_bbox_of_label(text, x1, y1, w, h, img_h, img_w, font, font_scale_offset=1): + font_scale_factor = img_h / (img_w + img_h) * font_scale_offset + font_scale = w / (w + h) * font_scale_factor # adjust font scale by width height + thickness = int(font_scale_factor) + 1 + (text_width, text_height) = cv2.getTextSize(text, font, fontScale=font_scale, thickness=thickness)[0] + text_offset_x = x1 + text_offset_y = y1 - thickness + box_coords = ((text_offset_x, text_offset_y + 1), (text_offset_x + text_width - 2, text_offset_y - text_height - 2)) + return (font_scale, thickness, text_height, box_coords) + + +def visualize_bbox_and_label( + img, bboxes, texts, bbox_color=(200, 180, 60), + text_color=(0, 0, 0), + format="xyxy", is_vnese=False, draw_text=True): + ori_img_type = type(img) + if is_vnese: + img = Image.fromarray(img) if ori_img_type is np.ndarray else img + draw = ImageDraw.Draw(img) + img_w, img_h = img.size + font_pil_str = "fonts/arial.ttf" + font_cv2 = cv2.FONT_HERSHEY_SIMPLEX + else: + img_h, img_w = img.shape[0], img.shape[1] + font_cv2 = cv2.FONT_HERSHEY_SIMPLEX + for i in range(len(bboxes)): + text = texts[i] # text = "{}: {:.0f}%".format(LABELS[classIDs[i]], confidences[i]*100) + x1, y1, x2, y2, w, h = get_xyxywh_base_on_format(bboxes[i], format) + font_scale, thickness, text_height, box_coords = get_dynamic_params_for_bbox_of_label( + text, x1, y1, w, h, img_h, img_w, font=font_cv2) + if is_vnese: + font_pil = ImageFont.truetype(font_pil_str, size=text_height) # type: ignore + fdraw_text = draw.text # type: ignore + fdraw_bbox = draw.rectangle # type: ignore + # Pil use different coordinate => y = y+thickness = y-thickness + 2*thickness + arg_text = ((box_coords[0][0], box_coords[1][1]), text) + kwarg_text = {"font": font_pil, "fill": text_color, "width": thickness} + arg_rec = ((x1, y1, x2, y2),) + kwarg_rec = {"outline": bbox_color, "width": thickness} + arg_rec_text = ((box_coords[0], box_coords[1]),) + kwarg_rec_text = {"fill": bbox_color, "width": thickness} + else: + # cv2.rectangle(img, box_coords[0], box_coords[1], color, cv2.FILLED) + # cv2.putText(img, text, (text_offset_x, text_offset_y), font, fontScale=font_scale, color=(50, 0,0), thickness=thickness) + # cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness) + fdraw_text = cv2.putText + fdraw_bbox = cv2.rectangle + arg_text = (img, text, box_coords[0]) + kwarg_text = {"fontFace": font_cv2, "fontScale": font_scale, "color": text_color, "thickness": thickness} + arg_rec = (img, (x1, y1), (x2, y2)) + kwarg_rec = {"color": bbox_color, "thickness": thickness} + arg_rec_text = (img, box_coords[0], box_coords[1]) + kwarg_rec_text = {"color": bbox_color, "thickness": cv2.FILLED} + # draw a bounding box rectangle and label on the img + fdraw_bbox(*arg_rec, **kwarg_rec) # type: ignore + if draw_text: + fdraw_bbox(*arg_rec_text, **kwarg_rec_text) # type: ignore + fdraw_text(*arg_text, **kwarg_text) # type: ignore # text have to put in front of rec_text + return np.array(img) if ori_img_type is np.ndarray and is_vnese else img diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/word_formation.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/word_formation.py new file mode 100644 index 0000000..3e64b97 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/externals/basic_ocr/src/word_formation.py @@ -0,0 +1,903 @@ +from builtins import dict +from .dto import Word, Line, WordGroup, Box +from .utils import find_maximum_without_outliers +import numpy as np +from typing import Optional, List, Tuple, Union + +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ +### WORDS TO LINES ALGORITHMS FROM MMOCR AND TESSERACT ############################################################################################################################################################################### +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ + +DEGREE_TO_RADIAN_COEF = np.pi / 180 +MAX_INT = int(2e10 + 9) +MIN_INT = -MAX_INT + + +def is_on_same_line(box_a, box_b, min_y_overlap_ratio=0.8): + """Check if two boxes are on the same line by their y-axis coordinates. + + Two boxes are on the same line if they overlap vertically, and the length + of the overlapping line segment is greater than min_y_overlap_ratio * the + height of either of the boxes. + + Args: + box_a (list), box_b (list): Two bounding boxes to be checked + min_y_overlap_ratio (float): The minimum vertical overlapping ratio + allowed for boxes in the same line + + Returns: + The bool flag indicating if they are on the same line + """ + a_y_min = np.min(box_a[1::2]) + b_y_min = np.min(box_b[1::2]) + a_y_max = np.max(box_a[1::2]) + b_y_max = np.max(box_b[1::2]) + + # Make sure that box a is always the box above another + if a_y_min > b_y_min: + a_y_min, b_y_min = b_y_min, a_y_min + a_y_max, b_y_max = b_y_max, a_y_max + + if b_y_min <= a_y_max: + if min_y_overlap_ratio is not None: + sorted_y = sorted([b_y_min, b_y_max, a_y_max]) + overlap = sorted_y[1] - sorted_y[0] + min_a_overlap = (a_y_max - a_y_min) * min_y_overlap_ratio + min_b_overlap = (b_y_max - b_y_min) * min_y_overlap_ratio + return overlap >= min_a_overlap or \ + overlap >= min_b_overlap + else: + return True + return False + + +def merge_bboxes_to_group(bboxes_group, x_sorted_boxes): + merged_bboxes = [] + for box_group in bboxes_group: + merged_box = {} + merged_box['text'] = ' '.join( + [x_sorted_boxes[idx]['text'] for idx in box_group]) + x_min, y_min = float('inf'), float('inf') + x_max, y_max = float('-inf'), float('-inf') + for idx in box_group: + x_max = max(np.max(x_sorted_boxes[idx]['box'][::2]), x_max) + x_min = min(np.min(x_sorted_boxes[idx]['box'][::2]), x_min) + y_max = max(np.max(x_sorted_boxes[idx]['box'][1::2]), y_max) + y_min = min(np.min(x_sorted_boxes[idx]['box'][1::2]), y_min) + merged_box['box'] = [ + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ] + merged_box['list_words'] = [x_sorted_boxes[idx]['word'] + for idx in box_group] + merged_bboxes.append(merged_box) + return merged_bboxes + + +def stitch_boxes_into_lines(boxes, max_x_dist=10, min_y_overlap_ratio=0.3): + """Stitch fragmented boxes of words into lines. + + Note: part of its logic is inspired by @Johndirr + (https://github.com/faustomorales/keras-ocr/issues/22) + + Args: + boxes (list): List of ocr results to be stitched + max_x_dist (int): The maximum horizontal distance between the closest + edges of neighboring boxes in the same line + min_y_overlap_ratio (float): The minimum vertical overlapping ratio + allowed for any pairs of neighboring boxes in the same line + + Returns: + merged_boxes(List[dict]): List of merged boxes and texts + """ + + if len(boxes) <= 1: + if len(boxes) == 1: + boxes[0]["list_words"] = [boxes[0]["word"]] + return boxes + + # merged_groups = [] + merged_lines = [] + + # sort groups based on the x_min coordinate of boxes + x_sorted_boxes = sorted(boxes, key=lambda x: np.min(x['box'][::2])) + # store indexes of boxes which are already parts of other lines + skip_idxs = set() + + i = 0 + # locate lines of boxes starting from the leftmost one + for i in range(len(x_sorted_boxes)): + if i in skip_idxs: + continue + # the rightmost box in the current line + rightmost_box_idx = i + line = [rightmost_box_idx] + for j in range(i + 1, len(x_sorted_boxes)): + if j in skip_idxs: + continue + if is_on_same_line(x_sorted_boxes[rightmost_box_idx]['box'], + x_sorted_boxes[j]['box'], min_y_overlap_ratio): + line.append(j) + skip_idxs.add(j) + rightmost_box_idx = j + + # split line into lines if the distance between two neighboring + # sub-lines' is greater than max_x_dist + # groups = [] + # line_idx = 0 + # groups.append([line[0]]) + # for k in range(1, len(line)): + # curr_box = x_sorted_boxes[line[k]] + # prev_box = x_sorted_boxes[line[k - 1]] + # dist = np.min(curr_box['box'][::2]) - np.max(prev_box['box'][::2]) + # if dist > max_x_dist: + # line_idx += 1 + # groups.append([]) + # groups[line_idx].append(line[k]) + + # # Get merged boxes + merged_line = merge_bboxes_to_group([line], x_sorted_boxes) + merged_lines.extend(merged_line) + # merged_group = merge_bboxes_to_group(groups,x_sorted_boxes) + # merged_groups.extend(merged_group) + + merged_lines = sorted(merged_lines, key=lambda x: np.min(x['box'][1::2])) + # merged_groups = sorted(merged_groups, key=lambda x: np.min(x['box'][1::2])) + return merged_lines # , merged_groups + +# REFERENCE +# https://vigneshgig.medium.com/bounding-box-sorting-algorithm-for-text-detection-and-object-detection-from-left-to-right-and-top-cf2c523c8a85 +# https://huggingface.co/spaces/tomofi/MMOCR/blame/main/mmocr/utils/box_util.py + + +def words_to_lines_mmocr(words: List[Word], *args) -> Tuple[List[Line], Optional[int]]: + bboxes = [{"box": [w.bbox[0], w.bbox[1], w.bbox[2], w.bbox[1], w.bbox[2], w.bbox[3], w.bbox[0], w.bbox[3]], + "text":w._text, "word":w} for w in words] + merged_lines = stitch_boxes_into_lines(bboxes) + merged_groups = merged_lines # TODO: fix code to return both word group and line + lwords_groups = [WordGroup(list_words=merged_box["list_words"], + text=merged_box["text"], + boundingbox=[merged_box["box"][i] for i in [0, 1, 2, -1]]) + for merged_box in merged_groups] + + llines = [Line(text=word_group._text, list_word_groups=[word_group], boundingbox=word_group._bbox_obj) + for word_group in lwords_groups] + + return llines, None # same format with the origin words_to_lines + # lines = [Line() for merged] + + +# def most_overlapping_row(rows, top, bottom, y_shift): +# max_overlap = -1 +# max_overlap_idx = -1 +# for i, row in enumerate(rows): +# row_top, row_bottom = row +# overlap = min(top + y_shift, row_top) - max(bottom + y_shift, row_bottom) +# if overlap > max_overlap: +# max_overlap = overlap +# max_overlap_idx = i +# return max_overlap_idx +def most_overlapping_row(rows, row_words, bottom, top, y_shift, max_row_size, y_overlap_threshold=0.5): + max_overlap = -1 + max_overlap_idx = -1 + overlapping_rows = [] + + for i, row in enumerate(rows): + row_bottom, row_top = row + overlap = min(bottom - y_shift[i], row_bottom) - \ + max(top - y_shift[i], row_top) + + if overlap > max_overlap: + max_overlap = overlap + max_overlap_idx = i + + # if at least overlap 1 pixel and not (overlap too much and overlap too little) + if (row_top <= bottom and row_bottom >= top) and not (bottom - top - max_overlap > max_row_size * y_overlap_threshold) and not (max_overlap < max_row_size * y_overlap_threshold): + overlapping_rows.append(i) + + # Merge overlapping rows if necessary + if len(overlapping_rows) > 1: + merge_bottom = max(rows[i][0] for i in overlapping_rows) + merge_top = min(rows[i][1] for i in overlapping_rows) + + if merge_bottom - merge_top <= max_row_size: + # Merge rows + merged_row = (merge_bottom, merge_top) + merged_words = [] + # Remove other overlapping rows + + for row_idx in overlapping_rows[:0:-1]: # [1,2,3] -> 3,2 + merged_words.extend(row_words[row_idx]) + del rows[row_idx] + del row_words[row_idx] + + rows[overlapping_rows[0]] = merged_row + row_words[overlapping_rows[0]].extend(merged_words[::-1]) + max_overlap_idx = overlapping_rows[0] + + if bottom - top - max_overlap > max_row_size * y_overlap_threshold and max_overlap < max_row_size * y_overlap_threshold: + max_overlap_idx = -1 + return max_overlap_idx + + +def stitch_boxes_into_lines_tesseract(words: list[Word], max_running_y_shift: int, + gradient: float, y_overlap_threshold: float) -> Tuple[list[list[Word]], float]: + sorted_words = sorted(words, key=lambda x: x.bbox[0]) + rows = [] + row_words = [] + max_row_size = find_maximum_without_outliers([word.height for word in sorted_words]) + running_y_shift = [] + for _i, word in enumerate(sorted_words): + bbox, _text = word.bbox, word._text + _x1, y1, _x2, y2 = bbox + bottom, top = y2, y1 + max_row_size = max(max_row_size, bottom - top) + overlap_row_idx = most_overlapping_row( + rows, row_words, bottom, top, running_y_shift, max_row_size, y_overlap_threshold) + + if overlap_row_idx == -1: # No overlapping row found + new_row = (bottom, top) + rows.append(new_row) + row_words.append([word]) + running_y_shift.append(0) + else: # Overlapping row found + row_bottom, row_top = rows[overlap_row_idx] + new_bottom = max(row_bottom, bottom) + new_top = min(row_top, top) + rows[overlap_row_idx] = (new_bottom, new_top) + row_words[overlap_row_idx].append(word) + new_shift = (top + bottom) / 2 - (row_top + row_bottom) / 2 + running_y_shift[overlap_row_idx] = min( + gradient * running_y_shift[overlap_row_idx] + (1 - gradient) * new_shift, max_running_y_shift) # update and clamp + + # Sort rows and row_texts based on the top y-coordinate + sorted_rows_data = sorted(zip(rows, row_words), key=lambda x: x[0][1]) + _sorted_rows_idx, sorted_row_words = zip(*sorted_rows_data) + # /_|<- the perpendicular line of the horizontal line and the skew line of the page + page_skew_dist = sum(running_y_shift) / len(running_y_shift) + return sorted_row_words, page_skew_dist + + +def construct_word_groups_tesseract(sorted_row_words: list[list[Word]], + max_x_dist: int, page_skew_dist: float) -> list[list[list[Word]]]: + # approximate page_skew_angle by page_skew_dist + corrected_max_x_dist = max_x_dist * abs(np.cos(page_skew_dist * DEGREE_TO_RADIAN_COEF)) + constructed_row_word_groups = [] + for row_words in sorted_row_words: + lword_groups = [] + line_idx = 0 + lword_groups.append([row_words[0]]) + for k in range(1, len(row_words)): + curr_box = row_words[k].bbox + prev_box = row_words[k - 1].bbox + dist = curr_box[0] - prev_box[2] + if dist > corrected_max_x_dist: + line_idx += 1 + lword_groups.append([]) + lword_groups[line_idx].append(row_words[k]) + constructed_row_word_groups.append(lword_groups) + return constructed_row_word_groups + + +def group_bbox_and_text(lwords: Union[list[Word], list[WordGroup]]) -> tuple[Box, tuple[str, float]]: + text = ' '.join([word._text for word in lwords]) + x_min, y_min = MAX_INT, MAX_INT + x_max, y_max = MIN_INT, MIN_INT + conf_det = 0 + conf_cls = 0 + for word in lwords: + x_max = int(max(np.max(word.bbox[::2]), x_max)) + x_min = int(min(np.min(word.bbox[::2]), x_min)) + y_max = int(max(np.max(word.bbox[1::2]), y_max)) + y_min = int(min(np.min(word.bbox[1::2]), y_min)) + conf_det += word._conf_det + conf_cls += word._conf_cls + bbox = Box(x_min, y_min, x_max, y_max, conf=conf_det / len(lwords)) + return bbox, (text, conf_cls / len(lwords)) + + +def words_to_lines_tesseract(words: List[Word], + page_width: int, max_running_y_shift_degree: int, gradient: float, max_x_dist: int, + y_overlap_threshold: float) -> Tuple[List[Line], + Optional[float]]: + max_running_y_shift = page_width * np.tan(max_running_y_shift_degree * DEGREE_TO_RADIAN_COEF) + sorted_row_words, page_skew_dist = stitch_boxes_into_lines_tesseract( + words, max_running_y_shift, gradient, y_overlap_threshold) + constructed_row_word_groups = construct_word_groups_tesseract( + sorted_row_words, max_x_dist, page_skew_dist) + llines = [] + for row in constructed_row_word_groups: + lwords_row = [] + lword_groups = [] + for word_group in row: + bbox_word_group, text_word_group = group_bbox_and_text(word_group) + lwords_row.extend(word_group) + lword_groups.append( + WordGroup( + list_words=word_group, text=text_word_group[0], + conf_cls=text_word_group[1], + boundingbox=bbox_word_group)) + bbox_line, text_line = group_bbox_and_text(lwords_row) + llines.append( + Line( + list_word_groups=lword_groups, text=text_line[0], + boundingbox=bbox_line, conf_cls=text_line[1])) + return llines, page_skew_dist + + + + +### WORDS TO WORDGROUPS ######################################################################################################################################################################################################################### + + +def merge_overlapping_word_groups( + rows: list[list[int]], + row_words: list[list[Word]], + overlapping_rows: list[int], + max_row_size: int) -> bool: + # Merge found overlapping rows if necessary + merge_top = max(rows[i][1] for i in overlapping_rows) + merge_bottom = min(rows[i][3] for i in overlapping_rows) + merge_left = min(rows[i][0] for i in overlapping_rows) + merge_right = max(rows[i][2] for i in overlapping_rows) + + if merge_top - merge_bottom <= max_row_size: + # Merge rows + merged_row = [merge_left, merge_top, merge_right, merge_bottom] + merged_words = [] + # Remove other overlapping rows + + for row_idx in overlapping_rows[:0:-1]: # [1,2,3] -> 3,2 + merged_words.extend(row_words[row_idx]) + del rows[row_idx] + del row_words[row_idx] + + rows[overlapping_rows[0]] = merged_row + row_words[overlapping_rows[0]].extend(merged_words[::-1]) + return True + return False + + +def most_overlapping_word_groups( + rows, row_words, curr_word_bbox, y_shift, max_row_size, y_overlap_threshold, max_x_dist): + max_overlap = -1 + max_overlap_idx = -1 + overlapping_rows = [] + left, top, right, bottom = curr_word_bbox + for i, row in enumerate(rows): + row_left, row_top, row_right, row_bottom = row + top_shift = top - y_shift[i] + bottom_shift = bottom - y_shift[i] + + # find the most overlapping row + overlap = min(bottom_shift, row_bottom) - max(top_shift, row_top) + if overlap > max_overlap and min(right - row_left, left - row_right) < max_x_dist: + max_overlap = overlap + max_overlap_idx = i + + # exclusive process to handle cases where there are multiple satisfying overlapping rows. For example some rows are not initially overlapping but as the appended words constantly get skewer, there is a change that the end of 1 row would reạch the beginning other row + # if (row_top <= bottom and row_bottom >= top) and not (bottom - top - max_overlap > max_row_size * y_overlap_threshold) and not (max_overlap < max_row_size * y_overlap_threshold): + if (row_top <= bottom_shift and row_bottom >= top_shift) \ + and min(right - row_left, left - row_right) < max_x_dist \ + and not (bottom - top - overlap > max_row_size * y_overlap_threshold) \ + and not (overlap < max_row_size * y_overlap_threshold): + # explain: + # (row_top <= bottom_shift and row_bottom >= top_shift) -> overlap at least 1 pixel + # not (bottom - top - overlap > max_row_size * y_overlap_threshold) -> curr_word is not too big too overlap (to exclude figures containing words) + # not (overlap < max_row_size * y_overlap_threshold) -> overlap too little should not be merged + # min(right - row_left, row_right - left) < max_x_dist -> either the curr_word is close enough to left or right of the curr_row + overlapping_rows.append(i) + + if len(overlapping_rows) > 1 and merge_overlapping_word_groups(rows, row_words, overlapping_rows, max_row_size): + max_overlap_idx = overlapping_rows[0] + if bottom - top - max_overlap > max_row_size * y_overlap_threshold and max_overlap < max_row_size * y_overlap_threshold: + max_overlap_idx = -1 + return max_overlap_idx + + +def update_overlapping_word_group_bbox(rows: list[list[int]], overlap_row_idx: int, curr_word_bbox: list[int]) -> None: + left, top, right, bottom = curr_word_bbox + row_left, row_top, row_right, row_bottom = rows[overlap_row_idx] + new_bottom = max(row_bottom, bottom) + new_top = min(row_top, top) + new_left = min(row_left, left) + new_right = max(row_right, right) + rows[overlap_row_idx] = [new_left, new_top, new_right, new_bottom] + + +def update_word_group_running_y_shift( + running_y_shift: list[float], + overlap_row_idx: int, curr_row_bbox: list[int], + curr_word_bbox: list[int], + gradient: float, max_running_y_shift: float) -> None: + _, top, _, bottom = curr_word_bbox + _, row_top, _, row_bottom = curr_row_bbox + new_shift = (top + bottom) / 2 - (row_top + row_bottom) / 2 + running_y_shift[overlap_row_idx] = min( + gradient * running_y_shift[overlap_row_idx] + (1 - gradient) * new_shift, max_running_y_shift) # update and clamp + + +def stitch_boxes_into_word_groups_tesseract(words: list[Word], + max_running_y_shift: int, gradient: float, y_overlap_threshold: float, + max_x_dist: int) -> Tuple[list[WordGroup], float]: + sorted_words = sorted(words, key=lambda x: x.bbox[0]) + rows = [] + row_words = [] + max_row_size = sorted_words[0].height + running_y_shift = [] + for word in sorted_words: + bbox: list[int] = word.bbox + max_row_size = max(max_row_size, bbox[3] - bbox[1]) + if bbox[-1] < 200 and word.text == "Nguyễn": + print("DEBUGING") + overlap_row_idx = most_overlapping_word_groups( + rows, row_words, bbox, running_y_shift, max_row_size, y_overlap_threshold, max_x_dist) + if overlap_row_idx == -1: # No overlapping row found + rows.append(bbox) # new row + row_words.append([word]) # new row_word + running_y_shift.append(0) + else: # Overlapping row found + # row_bottom, row_top = rows[overlap_row_idx] + update_overlapping_word_group_bbox(rows, overlap_row_idx, bbox) + row_words[overlap_row_idx].append(word) # update row_words + update_word_group_running_y_shift( + running_y_shift, overlap_row_idx, rows[overlap_row_idx], + bbox, gradient, max_running_y_shift) + + # Sort rows and row_texts based on the top y-coordinate + sorted_rows_data = sorted(zip(rows, row_words), key=lambda x: x[0][1]) + _sorted_rows_idx, sorted_row_words = zip(*sorted_rows_data) + lword_groups = [] + for word_group in sorted_row_words: + bbox_word_group, text_word_group = group_bbox_and_text(word_group) + lword_groups.append( + WordGroup( + list_words=word_group, text=text_word_group[0], + conf_cls=text_word_group[1], + boundingbox=bbox_word_group)) + # /_|<- the perpendicular line of the horizontal line and the skew line of the page + page_skew_dist = sum(running_y_shift) / len(running_y_shift) + + return lword_groups, page_skew_dist + + +def is_on_same_line_mmocr_tesseract(box_a: list[int], box_b: list[int], min_y_overlap_ratio: float) -> bool: + a_y_min = box_a[1] + b_y_min = box_b[1] + a_y_max = box_a[3] + b_y_max = box_b[3] + + # Make sure that box a is always the box above another + if a_y_min > b_y_min: + a_y_min, b_y_min = b_y_min, a_y_min + a_y_max, b_y_max = b_y_max, a_y_max + + if b_y_min <= a_y_max: + if min_y_overlap_ratio is not None: + sorted_y = sorted([b_y_min, b_y_max, a_y_max]) + overlap = sorted_y[1] - sorted_y[0] + min_a_overlap = (a_y_max - a_y_min) * min_y_overlap_ratio + min_b_overlap = (b_y_max - b_y_min) * min_y_overlap_ratio + return overlap >= min_a_overlap or \ + overlap >= min_b_overlap + else: + return True + return False + + +def stitch_word_groups_into_lines_mmocr_tesseract( + lword_groups: list[WordGroup], + min_y_overlap_ratio: float) -> list[Line]: + merged_lines = [] + + # sort groups based on the x_min coordinate of boxes + # store indexes of boxes which are already parts of other lines + sorted_word_groups = sorted(lword_groups, key=lambda x: x.bbox[0]) + skip_idxs = set() + + i = 0 + # locate lines of boxes starting from the leftmost one + for i in range(len(sorted_word_groups)): + if i in skip_idxs: + continue + # the rightmost box in the current line + rightmost_box_idx = i + line = [rightmost_box_idx] + for j in range(i + 1, len(sorted_word_groups)): + if j in skip_idxs: + continue + if is_on_same_line_mmocr_tesseract(sorted_word_groups[rightmost_box_idx].bbox, + sorted_word_groups[j].bbox, min_y_overlap_ratio): + line.append(j) + skip_idxs.add(j) + rightmost_box_idx = j + + lword_groups_in_line = [sorted_word_groups[k] for k in line] + bbox_line, text_line = group_bbox_and_text(lword_groups_in_line) + merged_lines.append( + Line( + list_word_groups=lword_groups_in_line, text=text_line[0], + conf_cls=text_line[1], + boundingbox=bbox_line)) + merged_lines = sorted(merged_lines, key=lambda x: x.bbox[1]) + return merged_lines + + +def words_formation_mmocr_tesseract(words: List[Word], page_width: int, word_formation_mode: str, max_running_y_shift_degree: int, gradient: float, + max_x_dist: int, y_overlap_threshold: float) -> Tuple[Union[List[WordGroup], list[Line]], + Optional[float]]: + if len(words) == 0: + return [], 0 + max_running_y_shift = page_width * np.tan(max_running_y_shift_degree * DEGREE_TO_RADIAN_COEF) + lword_groups, page_skew_dist = stitch_boxes_into_word_groups_tesseract( + words, max_running_y_shift, gradient, y_overlap_threshold, max_x_dist) + if word_formation_mode == "word_group": + return lword_groups, page_skew_dist + elif word_formation_mode == "line": + llines = stitch_word_groups_into_lines_mmocr_tesseract(lword_groups, y_overlap_threshold) + return llines, page_skew_dist + else: + raise NotImplementedError("Word formation mode not supported: {}".format(word_formation_mode)) + +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ +### END WORDS TO LINES ALGORITHMS FROM MMOCR AND TESSERACT ############################################################################################################################################################################### +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ + +# MIN_IOU_HEIGHT = 0.7 +# MIN_WIDTH_LINE_RATIO = 0.05 + + +# def resize_to_original( +# boundingbox, scale +# ): # resize coordinates to match size of original image +# left, top, right, bottom = boundingbox +# left *= scale[1] +# right *= scale[1] +# top *= scale[0] +# bottom *= scale[0] +# return [left, top, right, bottom] + + +# def check_iomin(word: Word, word_group: Word_group): +# min_height = min( +# word.boundingbox[3] - word.boundingbox[1], +# word_group.boundingbox[3] - word_group.boundingbox[1], +# ) +# intersect = min(word.boundingbox[3], word_group.boundingbox[3]) - max( +# word.boundingbox[1], word_group.boundingbox[1] +# ) +# if intersect / min_height > 0.7: +# return True +# return False + + +# def prepare_line(words): +# lines = [] +# visited = [False] * len(words) +# for id_word, word in enumerate(words): +# if word.invalid_size() == 0: +# continue +# new_line = True +# for i in range(len(lines)): +# if ( +# lines[i].in_same_line(word) and not visited[id_word] +# ): # check if word is in the same line with lines[i] +# lines[i].merge_word(word) +# new_line = False +# visited[id_word] = True + +# if new_line == True: +# new_line = Line() +# new_line.merge_word(word) +# lines.append(new_line) + +# # print(len(lines)) +# # sort line from top to bottom according top coordinate +# lines.sort(key=lambda x: x.boundingbox[1]) +# return lines + + +# def __create_word_group(word, word_group_id): +# new_word_group_ = Word_group() +# new_word_group_.list_words = list() +# new_word_group_.word_group_id = word_group_id +# new_word_group_.add_word(word) + +# return new_word_group_ + + +# def __sort_line(line): +# line.list_word_groups.sort( +# key=lambda x: x.boundingbox[0] +# ) # sort word in lines from left to right + +# return line + + +# def __merge_text_for_line(line): +# line.text = "" +# for word in line.list_word_groups: +# line.text += " " + word.text + +# return line + + +# def __update_list_word_groups(line, word_group_id, word_id, line_width): + +# old_list_word_group = line.list_word_groups +# list_word_groups = [] + +# inital_word_group = __create_word_group( +# old_list_word_group[0], word_group_id) +# old_list_word_group[0].word_id = word_id +# list_word_groups.append(inital_word_group) +# word_group_id += 1 +# word_id += 1 + +# for word in old_list_word_group[1:]: +# check_word_group = True +# word.word_id = word_id +# word_id += 1 + +# if ( +# (not list_word_groups[-1].text.endswith(":")) +# and ( +# (word.boundingbox[0] - list_word_groups[-1].boundingbox[2]) +# / line_width +# < MIN_WIDTH_LINE_RATIO +# ) +# and check_iomin(word, list_word_groups[-1]) +# ): +# list_word_groups[-1].add_word(word) +# check_word_group = False + +# if check_word_group: +# new_word_group = __create_word_group(word, word_group_id) +# list_word_groups.append(new_word_group) +# word_group_id += 1 +# line.list_word_groups = list_word_groups +# return line, word_group_id, word_id + + +# def construct_word_groups_in_each_line(lines): +# line_id = 0 +# word_group_id = 0 +# word_id = 0 +# for i in range(len(lines)): +# if len(lines[i].list_word_groups) == 0: +# continue + +# # left, top ,right, bottom +# line_width = lines[i].boundingbox[2] - \ +# lines[i].boundingbox[0] # right - left +# line_width = 1 # TODO: to remove +# lines[i] = __sort_line(lines[i]) + +# # update text for lines after sorting +# lines[i] = __merge_text_for_line(lines[i]) + +# lines[i], word_group_id, word_id = __update_list_word_groups( +# lines[i], +# word_group_id, +# word_id, +# line_width) +# lines[i].update_line_id(line_id) +# line_id += 1 +# return lines + + +# def words_to_lines(words, check_special_lines=True): # words is list of Word instance +# # sort word by top +# words.sort(key=lambda x: (x.boundingbox[1], x.boundingbox[0])) +# # words.sort(key=lambda x: (sum(x.bbox))) +# number_of_word = len(words) +# # print(number_of_word) +# # sort list words to list lines, which have not contained word_group yet +# lines = prepare_line(words) + +# # construct word_groups in each line +# lines = construct_word_groups_in_each_line(lines) +# return lines, number_of_word + + +# def near(word_group1: Word_group, word_group2: Word_group): +# min_height = min( +# word_group1.boundingbox[3] - word_group1.boundingbox[1], +# word_group2.boundingbox[3] - word_group2.boundingbox[1], +# ) +# overlap = min(word_group1.boundingbox[3], word_group2.boundingbox[3]) - max( +# word_group1.boundingbox[1], word_group2.boundingbox[1] +# ) + +# if overlap > 0: +# return True +# if abs(overlap / min_height) < 1.5: +# print("near enough", abs(overlap / min_height), overlap, min_height) +# return True +# return False + + +# def calculate_iou_and_near(wg1: Word_group, wg2: Word_group): +# min_height = min( +# wg1.boundingbox[3] - +# wg1.boundingbox[1], wg2.boundingbox[3] - wg2.boundingbox[1] +# ) +# overlap = min(wg1.boundingbox[3], wg2.boundingbox[3]) - max( +# wg1.boundingbox[1], wg2.boundingbox[1] +# ) +# iou = overlap / min_height +# distance = min( +# abs(wg1.boundingbox[0] - wg2.boundingbox[2]), +# abs(wg1.boundingbox[2] - wg2.boundingbox[0]), +# ) +# if iou > 0.7 and distance < 0.5 * (wg1.boundingboxp[2] - wg1.boundingbox[0]): +# return True +# return False + + +# def construct_word_groups_to_kie_label(list_word_groups: list): +# kie_dict = dict() +# for wg in list_word_groups: +# if wg.kie_label == "other": +# continue +# if wg.kie_label not in kie_dict: +# kie_dict[wg.kie_label] = [wg] +# else: +# kie_dict[wg.kie_label].append(wg) + +# new_dict = dict() +# for key, value in kie_dict.items(): +# if len(value) == 1: +# new_dict[key] = value +# continue + +# value.sort(key=lambda x: x.boundingbox[1]) +# new_dict[key] = value +# return new_dict + + +# def invoice_construct_word_groups_to_kie_label(list_word_groups: list): +# kie_dict = dict() + +# for wg in list_word_groups: +# if wg.kie_label == "other": +# continue +# if wg.kie_label not in kie_dict: +# kie_dict[wg.kie_label] = [wg] +# else: +# kie_dict[wg.kie_label].append(wg) + +# return kie_dict + + +# def postprocess_total_value(kie_dict): +# if "total_in_words_value" not in kie_dict: +# return kie_dict + +# for k, value in kie_dict.items(): +# if k == "total_in_words_value": +# continue +# l = [] +# for v in value: +# if v.boundingbox[3] <= kie_dict["total_in_words_value"][0].boundingbox[3]: +# l.append(v) + +# if len(l) != 0: +# kie_dict[k] = l + +# return kie_dict + + +# def postprocess_tax_code_value(kie_dict): +# if "buyer_tax_code_value" in kie_dict or "seller_tax_code_value" not in kie_dict: +# return kie_dict + +# kie_dict["buyer_tax_code_value"] = [] +# for v in kie_dict["seller_tax_code_value"]: +# if "buyer_name_key" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_key"][0]) +# ): +# kie_dict["buyer_tax_code_value"].append(v) +# continue + +# if "buyer_name_value" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_value"][0]) +# ): +# kie_dict["buyer_tax_code_value"].append(v) +# continue + +# if "buyer_address_value" in kie_dict and near( +# kie_dict["buyer_address_value"][0], v +# ): +# kie_dict["buyer_tax_code_value"].append(v) +# return kie_dict + + +# def postprocess_tax_code_key(kie_dict): +# if "buyer_tax_code_key" in kie_dict or "seller_tax_code_key" not in kie_dict: +# return kie_dict +# kie_dict["buyer_tax_code_key"] = [] +# for v in kie_dict["seller_tax_code_key"]: +# if "buyer_name_key" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_key"][0]) +# ): +# kie_dict["buyer_tax_code_key"].append(v) +# continue + +# if "buyer_name_value" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_value"][0]) +# ): +# kie_dict["buyer_tax_code_key"].append(v) +# continue + +# if "buyer_address_value" in kie_dict and near( +# kie_dict["buyer_address_value"][0], v +# ): +# kie_dict["buyer_tax_code_key"].append(v) + +# return kie_dict + + +# def invoice_postprocess(kie_dict: dict): +# # all keys or values which are below total_in_words_value will be thrown away +# kie_dict = postprocess_total_value(kie_dict) +# kie_dict = postprocess_tax_code_value(kie_dict) +# kie_dict = postprocess_tax_code_key(kie_dict) +# return kie_dict + + +# def throw_overlapping_words(list_words): +# new_list = [list_words[0]] +# for word in list_words: +# overlap = False +# area = (word.boundingbox[2] - word.boundingbox[0]) * ( +# word.boundingbox[3] - word.boundingbox[1] +# ) +# for word2 in new_list: +# area2 = (word2.boundingbox[2] - word2.boundingbox[0]) * ( +# word2.boundingbox[3] - word2.boundingbox[1] +# ) +# xmin_intersect = max(word.boundingbox[0], word2.boundingbox[0]) +# xmax_intersect = min(word.boundingbox[2], word2.boundingbox[2]) +# ymin_intersect = max(word.boundingbox[1], word2.boundingbox[1]) +# ymax_intersect = min(word.boundingbox[3], word2.boundingbox[3]) +# if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: +# continue + +# area_intersect = (xmax_intersect - xmin_intersect) * ( +# ymax_intersect - ymin_intersect +# ) +# if area_intersect / area > 0.7 or area_intersect / area2 > 0.7: +# overlap = True +# if overlap == False: +# new_list.append(word) +# return new_list + + +# def check_iou(box1: Word, box2: Box, threshold=0.9): +# area1 = (box1.boundingbox[2] - box1.boundingbox[0]) * ( +# box1.boundingbox[3] - box1.boundingbox[1] +# ) +# area2 = (box2.xmax - box2.xmin) * (box2.ymax - box2.ymin) +# xmin_intersect = max(box1.boundingbox[0], box2.xmin) +# ymin_intersect = max(box1.boundingbox[1], box2.ymin) +# xmax_intersect = min(box1.boundingbox[2], box2.xmax) +# ymax_intersect = min(box1.boundingbox[3], box2.ymax) +# if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: +# area_intersect = 0 +# else: +# area_intersect = (xmax_intersect - xmin_intersect) * ( +# ymax_intersect - ymin_intersect +# ) +# union = area1 + area2 - area_intersect +# iou = area_intersect / union +# if iou > threshold: +# return True +# return False diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/main.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/main.py new file mode 100644 index 0000000..db5f3b6 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/main.py @@ -0,0 +1,147 @@ +import os +import glob +import cv2 +import json +import argparse +import numpy as np +from tqdm import tqdm +from PIL import Image +from datetime import datetime +from sdsvkvu.sources.kvu import KVUEngine +from sdsvkvu.sources.utils import export_kvu_outputs, export_sbt_outputs, draw_kvu_outputs +from sdsvkvu.utils.utils import create_dir, write_to_json, pdf2img +from sdsvkvu.utils.query.vat import export_kvu_for_VAT_invoice, merged_kvu_for_VAT_invoice_for_multi_pages +from sdsvkvu.utils.query.sbt import export_kvu_for_SDSAP, merged_kvu_for_SDSAP_for_multi_pages +from sdsvkvu.utils.query.vtb import export_kvu_for_vietin, merged_kvu_for_vietin_for_multi_pages +from sdsvkvu.utils.query.all import export_kvu_for_all, merged_kvu_for_all_for_multi_pages +from sdsvkvu.utils.query.manulife import export_kvu_for_manulife, merged_kvu_for_manulife_for_multi_pages +from sdsvkvu.utils.query.sbt_v2 import export_kvu_for_SBT, merged_kvu_for_SBT_for_multi_pages + + +def get_args(): + args = argparse.ArgumentParser(description='Main file') + args.add_argument('--img_dir', type=str, required=True, + help='path to input image/directory file') + args.add_argument('--save_dir', type=str, required=True, + help='path to save directory') + args.add_argument('--doc_type', type=str, default="vat", + help='type of document') + args.add_argument('--export_img', type=bool, default=False, + help='export image of output visualization') + args.add_argument('--kvu_params', type=str, required=False, default="") + return args.parse_args() + + +def load_engine(kwargs) -> KVUEngine: + print('[INFO] Loading Key-Value Understanding model ...') + if not isinstance(kwargs, dict): + kwargs = json.loads(kwargs) if kwargs else {} + engine = KVUEngine(**kwargs) + print("[INFO] Loaded model") + print("[INFO] KVU engine settings: \n", engine._settings) + return engine + + +def process_img(img_path: str, save_dir: str, engine: KVUEngine, export_all: bool, option: str) -> dict: + assert (engine._settings.mode == 4 and option == "sbt_v2") \ + or (engine._settings.mode != 4 and option != "sbt_v2"), \ + "[ERROR] Mode (4) has just supported option \"sbt_v2\"" + + print("="*5, os.path.basename(img_path)) + create_dir(save_dir) + fname, img_ext = os.path.splitext(os.path.basename(img_path)) + out_ext = ".json" + image, lbbox, lwords, pr_class_words, pr_relations = engine.predict(img_path) + + if len(lbbox) != 1: + raise ValueError( + f"Not support to predict each separated window: {len(lbbox)}" + ) + + for i in range(len(lbbox)): + if engine._settings.mode in range(4): + raw_outputs = export_kvu_outputs(lwords[i], lbbox[i], pr_class_words[i], pr_relations[i], engine._settings.class_names) + elif engine._settings.mode == 4: + raw_outputs = export_sbt_outputs(lwords[i], lbbox[i], pr_class_words[i], pr_relations[i], engine._settings.class_names) + + if export_all: + save_path = os.path.join(save_dir, 'kvu_results') + create_dir(save_path) + write_to_json(os.path.join(save_path, fname + out_ext), raw_outputs) + # image = Image.open(img_path) + image = np.array(image) + image = draw_kvu_outputs(image, lbbox[i], pr_class_words[i], pr_relations[i], class_names=engine._settings.class_names) + cv2.imwrite(os.path.join(save_path, fname + img_ext), image) + + + if option == "vat": + outputs = export_kvu_for_VAT_invoice(raw_outputs) + elif option == "sbt": + outputs = export_kvu_for_SDSAP(raw_outputs) + elif option == "vtb": + outputs = export_kvu_for_vietin(raw_outputs) + elif option == "manulife": + outputs = export_kvu_for_manulife(raw_outputs) + elif option == "sbt_v2": + outputs = export_kvu_for_SBT(raw_outputs) + else: + outputs = export_kvu_for_all(raw_outputs) + write_to_json(os.path.join(save_dir, fname + out_ext), outputs) + return outputs + + +def process_pdf(pdf_path: str, save_dir: str, engine: KVUEngine, export_all: bool, option: str, n_pages: int = -1) -> dict: + out_ext = ".json" + fname, pdf_ext = os.path.splitext(os.path.basename(pdf_path)) + img_dirname = '_'.join([os.path.basename(os.path.dirname(pdf_path)), fname]) + img_save_dir = os.path.join(save_dir, img_dirname) + create_dir(img_save_dir) + list_img_files = pdf2img(pdf_path, img_save_dir, n_pages=n_pages, return_fname=True) + outputs = [] + for img_path in list_img_files: + print("=====", os.path.basename(img_path)) + _outputs = process_img(img_path, img_save_dir, engine, export_all=export_all, option=option) + outputs.append(_outputs) + if option == "vat": + outputs = merged_kvu_for_VAT_invoice_for_multi_pages(outputs) + elif option == "sbt": + outputs = merged_kvu_for_SDSAP_for_multi_pages(outputs) + elif option == "vtb": + outputs = merged_kvu_for_vietin_for_multi_pages(outputs) + elif option == "manulife": + outputs = merged_kvu_for_manulife_for_multi_pages(outputs) + elif option == "sbt_v2": + outputs = merged_kvu_for_SBT_for_multi_pages(outputs) + else: + outputs = merged_kvu_for_all_for_multi_pages(outputs) + write_to_json(os.path.join(save_dir, fname + out_ext), outputs) + return outputs + + +def process_dir(dir_path: str, save_dir: str, engine: KVUEngine, export_all: bool, option: str, dir_level: int = 0) -> None: + list_images = [] + for ext in ['JPG', 'PNG', 'jpeg', 'jpg', 'png', 'pdf']: + list_images += glob.glob(os.path.join(dir_path, f"{'*/'*dir_level}*.{ext}")) + print('No. images:', len(list_images)) + for file_path in tqdm(list_images): + if os.path.splitext(file_path)[1] == ".pdf": + outputs = process_pdf(file_path, save_dir, engine, export_all=export_all, option=option, n_pages=-1) + else: + outputs = process_img(file_path, save_dir, engine, export_all=export_all, option=option) + + +def Predictor_KVU(img: str, save_dir: str, engine: KVUEngine) -> dict: + curr_datetime = datetime.now().strftime('%Y-%m-%d %H-%M-%S') + image_path = "/home/thucpd/thucpd/PV2-2023/tmp_image/{}.jpg".format(curr_datetime) + cv2.imwrite(image_path, img) + vat_outputs = process_img(image_path, save_dir, engine, export_all=False, option="vat") + return vat_outputs + + +if __name__ == "__main__": + args = get_args() + engine = load_engine(args.kvu_params) + # vat_outputs = process_img(args.img_dir, args.save_dir, engine, export_all=True, option="vat") + # vat_outputs = process_pdf(args.img_dir, args.save_dir, engine, export_all=True, option="vat") + process_dir(args.img_dir, args.save_dir, engine, export_all=args.export_img, option=args.doc_type) + print('[INFO] Done') diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/__init__.py new file mode 100644 index 0000000..d4e48aa --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/__init__.py @@ -0,0 +1,45 @@ + +import os +import torch +from sdsvkvu.model.kvu_model import KVUModel +from sdsvkvu.model.combined_model import ComKVUModel +from sdsvkvu.model.document_kvu_model import DocKVUModel +from sdsvkvu.model.sbt_model import SBTModel + +def get_model(cfg): + if cfg.mode == 0 or cfg.mode == 1: + model = ComKVUModel(cfg=cfg) + elif cfg.mode == 2: + model = KVUModel(cfg=cfg) + elif cfg.mode == 3: + model = DocKVUModel(cfg=cfg) + elif cfg.mode == 4: + model = SBTModel(cfg=cfg) + else: + raise ValueError(f'[ERROR] Model mode of {cfg.mode} is not supported') + return model + +def load_checkpoint(ckpt_path, model, key_include): + assert os.path.exists(ckpt_path) == True, f"Ckpt path at {ckpt_path} not exist!" + state_dict = torch.load(ckpt_path, 'cpu')['state_dict'] + for key in list(state_dict.keys()): + if f'.{key_include}.' not in key: + del state_dict[key] + else: + state_dict[key[4:].replace(key_include + '.', "")] = state_dict[key] # remove net.something. + del state_dict[key] + model.load_state_dict(state_dict, strict=True) + print(f"Load checkpoint at {ckpt_path}") + return model + +def load_model_weight(net, pretrained_model_file): + pretrained_model_state_dict = torch.load(pretrained_model_file, map_location="cpu")[ + "state_dict" + ] + new_state_dict = {} + for k, v in pretrained_model_state_dict.items(): + new_k = k + if new_k.startswith("net."): + new_k = new_k[len("net.") :] + new_state_dict[new_k] = v + net.load_state_dict(new_state_dict) \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/combined_model.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/combined_model.py new file mode 100644 index 0000000..5c32797 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/combined_model.py @@ -0,0 +1,71 @@ +import os +import torch +from torch import nn + +from sdsvkvu.model.kvu_model import KVUModel +# from model import load_checkpoint + + +class ComKVUModel(KVUModel): + def __init__(self, cfg): + super().__init__(cfg) + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.finetune_only = cfg.train.finetune_only + + self._get_backbones(self.model_cfg.backbone) + self._create_head() + + # if os.path.exists(self.model_cfg.ckpt_model_file): + # self.backbone_layoutxlm = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone_layoutxlm, 'backbone_layoutxlm') + # self.itc_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.itc_layer, 'itc_layer') + # self.stc_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.stc_layer, 'stc_layer') + # self.relation_layer = load_checkpoint(self.model_cfg.ckpt_model_file, self.relation_layer, 'relation_layer') + # self.relation_layer_from_key = load_checkpoint(self.model_cfg.ckpt_model_file, self.relation_layer_from_key, 'relation_layer_from_key') + + self.loss_func = nn.CrossEntropyLoss() + + # if self.freeze: + # for name, param in self.named_parameters(): + # if 'backbone' in name: + # param.requires_grad = False + # if self.finetune_only == 'EE': + # for name, param in self.named_parameters(): + # if 'itc_layer' not in name and 'stc_layer' not in name: + # param.requires_grad = False + # if self.finetune_only == 'EL': + # for name, param in self.named_parameters(): + # if 'relation_layer' not in name or 'relation_layer_from_key' in name: + # param.requires_grad = False + # if self.finetune_only == 'ELK': + # for name, param in self.named_parameters(): + # if 'relation_layer_from_key' not in name: + # param.requires_grad = False + + + def forward(self, batch): + image = batch["image"] + input_ids_layoutxlm = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask_layoutxlm = batch["attention_mask_layoutxlm"] + + backbone_outputs_layoutxlm = self.backbone_layoutxlm( + image=image, input_ids=input_ids_layoutxlm, bbox=bbox, attention_mask=attention_mask_layoutxlm) + + last_hidden_states = backbone_outputs_layoutxlm.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(last_hidden_states, last_hidden_states).squeeze(0) + head_outputs = {"itc_outputs": itc_outputs, "stc_outputs": stc_outputs, + "el_outputs": el_outputs, "el_outputs_from_key": el_outputs_from_key} + + loss = 0.0 + if any(['labels' in key for key in batch.keys()]): + loss = self._get_loss(head_outputs, batch) + + return head_outputs, loss + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/document_kvu_model.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/document_kvu_model.py new file mode 100644 index 0000000..b278fb5 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/document_kvu_model.py @@ -0,0 +1,162 @@ +import torch +from torch import nn +from sdsvkvu.model.relation_extractor import RelationExtractor +from sdsvkvu.model.kvu_model import KVUModel +# from model import load_checkpoint + + +class DocKVUModel(KVUModel): + def __init__(self, cfg): + super().__init__(cfg) + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.train_cfg = cfg.train + self.n_classes = len(self.model_cfg.class_names) + + self._get_backbones(self.model_cfg.backbone) + + self._create_head() + + self.loss_func = nn.CrossEntropyLoss() + + def _create_head(self): + self.backbone_hidden_size = self.backbone_config.hidden_size + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + # self.n_classes = self.model_cfg.n_classes + 1 + self.repr_hiddent_size = self.backbone_hidden_size + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # Classfication Layer for whole document + # (1) Initial token classification + self.itc_layer_document = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.repr_hiddent_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + # (3) Linking token classification + self.relation_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + # (4) Linking token classification + self.relation_layer_from_key_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + self.relation_layer_from_key.apply(self._init_weight) + + self.itc_layer_document.apply(self._init_weight) + self.stc_layer_document.apply(self._init_weight) + self.relation_layer_document.apply(self._init_weight) + self.relation_layer_from_key_document.apply(self._init_weight) + + + def forward(self, batches): + head_outputs_list = [] + loss = 0.0 + for batch in batches["windows"]: + image = batch["image"] + input_ids = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask = batch["attention_mask_layoutxlm"] + + if self.freeze: + for param in self.backbone.parameters(): + param.requires_grad = False + + if self.model_cfg.backbone == 'layoutxlm': + backbone_outputs = self.backbone( + image=image, input_ids=input_ids, bbox=bbox, attention_mask=attention_mask + ) + else: + backbone_outputs = self.backbone(input_ids, attention_mask=attention_mask) + + last_hidden_states = backbone_outputs.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key(last_hidden_states, last_hidden_states).squeeze(0) + + window_repr = last_hidden_states.transpose(0, 1).contiguous() + + head_outputs = {"window_repr": window_repr, + "itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + head_outputs_list.append(head_outputs) + + batch = batches["documents"] + + document_repr = torch.cat([w['window_repr'] for w in head_outputs_list], dim=1) + document_repr = document_repr.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer_document(document_repr).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer_document(document_repr, document_repr).squeeze(0) + el_outputs = self.relation_layer_document(document_repr, document_repr).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key_document(document_repr, document_repr).squeeze(0) + + head_outputs = {"itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + return head_outputs, loss + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/kvu_model.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/kvu_model.py new file mode 100644 index 0000000..0277d1b --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/kvu_model.py @@ -0,0 +1,300 @@ +import os +import torch +from torch import nn +from pathlib import Path +from transformers import ( + LayoutLMConfig, + LayoutLMModel, + LayoutLMTokenizer, +) +from transformers import ( + LayoutLMv2Config, + LayoutLMv2Model, + LayoutLMv2FeatureExtractor, + LayoutXLMTokenizer, +) +from transformers import ( + XLMRobertaConfig, + AutoTokenizer, + XLMRobertaModel +) + +# from model import load_checkpoint +from sdsvkvu.sources.utils import merged_token_embeddings +from sdsvkvu.model.relation_extractor import RelationExtractor + + +class KVUModel(nn.Module): + def __init__(self, cfg): + super().__init__() + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.finetune_only = cfg.train.finetune_only + self.n_classes = len(self.model_cfg.class_names) + + self._get_backbones(self.model_cfg.backbone) + self._create_head() + + # if (cfg.stage == 2) and (os.path.exists(self.model_cfg.ckpt_model_file)): + # self.backbone_layoutxlm = load_checkpoint(self.model_cfg.ckpt_model_file, self.backbone_layoutxlm, 'backbone_layoutxlm') + + self._create_head() + self.loss_func = nn.CrossEntropyLoss() + + if self.freeze: + for name, param in self.named_parameters(): + if "backbone" in name: + param.requires_grad = False + + def _create_head(self): + self.backbone_hidden_size = 768 + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + # self.n_classes = self.model_cfg.n_classes + 1 + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, # 1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, # 1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (4) Linking token classification + self.relation_layer_from_key = RelationExtractor( + n_relations=1, # 1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + + # def _get_backbones(self, config_type): + # self.tokenizer_layoutxlm = LayoutXLMTokenizer.from_pretrained('microsoft/layoutxlm-base') + # self.feature_extractor = LayoutLMv2FeatureExtractor(apply_ocr=False) + # self.backbone_layoutxlm = LayoutLMv2Model.from_pretrained('microsoft/layoutxlm-base') + + def _get_backbones(self, config_type): + configs = { + "layoutlm": { + "config": LayoutLMConfig, + "tokenizer": LayoutLMTokenizer, + "backbone": LayoutLMModel, + "feature_extrator": LayoutLMv2FeatureExtractor, + }, + "layoutxlm": { + "config": LayoutLMv2Config, + "tokenizer": LayoutXLMTokenizer, + "backbone": LayoutLMv2Model, + "feature_extrator": LayoutLMv2FeatureExtractor, + }, + "xlm-roberta": { + "config": XLMRobertaConfig, + "tokenizer": AutoTokenizer, + "backbone": XLMRobertaModel, + "feature_extrator": LayoutLMv2FeatureExtractor, + }, + } + + self.backbone_config = configs[config_type]["config"].from_pretrained( + self.model_cfg.pretrained_model_path + ) + if config_type != "xlm-roberta": + self.tokenizer = configs[config_type]["tokenizer"].from_pretrained( + self.model_cfg.pretrained_model_path + ) + else: + self.tokenizer = configs[config_type]["tokenizer"].from_pretrained( + self.model_cfg.pretrained_model_path, use_fast=False + ) + self.feature_extractor = configs[config_type]["feature_extrator"]( + apply_ocr=False + ) + self.backbone = configs[config_type]["backbone"].from_pretrained( + self.model_cfg.pretrained_model_path + ) + + @staticmethod + def _init_weight(module): + init_std = 0.02 + if isinstance(module, nn.Linear): + nn.init.normal_(module.weight, 0.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + elif isinstance(module, nn.LayerNorm): + nn.init.normal_(module.weight, 1.0, init_std) + if module.bias is not None: + nn.init.constant_(module.bias, 0.0) + + def forward(self, lbatches): + windows = lbatches["windows"] + token_embeddings_windows = [] + lvalids = [] + loverlaps = [] + + for i, batch in enumerate(windows): + batch = { + k: v.cuda() for k, v in batch.items() if k not in ("img_path", "words") + } + image = batch["image"] + input_ids_layoutxlm = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask_layoutxlm = batch["attention_mask_layoutxlm"] + + backbone_outputs_layoutxlm = self.backbone_layoutxlm( + image=image, + input_ids=input_ids_layoutxlm, + bbox=bbox, + attention_mask=attention_mask_layoutxlm, + ) + + last_hidden_states_layoutxlm = backbone_outputs_layoutxlm.last_hidden_state[ + :, :512, : + ] + + lvalids.append(batch["len_valid_tokens"]) + loverlaps.append(batch["len_overlap_tokens"]) + token_embeddings_windows.append(last_hidden_states_layoutxlm) + + token_embeddings = merged_token_embeddings( + token_embeddings_windows, loverlaps, lvalids, average=False + ) + + token_embeddings = token_embeddings.transpose(0, 1).contiguous().cuda() + itc_outputs = self.itc_layer(token_embeddings).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(token_embeddings, token_embeddings).squeeze(0) + el_outputs = self.relation_layer(token_embeddings, token_embeddings).squeeze(0) + el_outputs_from_key = self.relation_layer_from_key( + token_embeddings, token_embeddings + ).squeeze(0) + head_outputs = { + "itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs, + "el_outputs_from_key": el_outputs_from_key, + "embedding_tokens": token_embeddings.transpose(0, 1) + .contiguous() + .detach() + .cpu() + .numpy(), + } + + loss = 0.0 + if any(["labels" in key for key in lbatches.keys()]): + labels = { + k: v.cuda() + for k, v in lbatches["documents"].items() + if k not in ("img_path") + } + loss = self._get_loss(head_outputs, labels) + + return head_outputs, loss + + def _get_loss(self, head_outputs, batch): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + itc_loss = self._get_itc_loss(itc_outputs, batch) + stc_loss = self._get_stc_loss(stc_outputs, batch) + el_loss = self._get_el_loss(el_outputs, batch) + el_loss_from_key = self._get_el_loss(el_outputs_from_key, batch, from_key=True) + + loss = itc_loss + stc_loss + el_loss + el_loss_from_key + + return loss + + def _get_itc_loss(self, itc_outputs, batch): + itc_mask = batch["are_box_first_tokens"].view(-1) + + itc_logits = itc_outputs.view(-1, self.model_cfg.n_classes + 1) + itc_logits = itc_logits[itc_mask] + + itc_labels = batch["itc_labels"].view(-1) + itc_labels = itc_labels[itc_mask] + + itc_loss = self.loss_func(itc_logits, itc_labels) + + return itc_loss + + def _get_stc_loss(self, stc_outputs, batch): + inv_attention_mask = 1 - batch["attention_mask_layoutxlm"] + + bsz, max_seq_length = inv_attention_mask.shape + device = inv_attention_mask.device + + invalid_token_mask = torch.cat( + [inv_attention_mask, torch.zeros([bsz, 1]).to(device)], axis=1 + ).bool() + + stc_outputs.masked_fill_(invalid_token_mask[:, None, :], -10000.0) + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + stc_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + stc_mask = batch["attention_mask_layoutxlm"].view(-1).bool() + stc_logits = stc_outputs.view(-1, max_seq_length + 1) + stc_logits = stc_logits[stc_mask] + + stc_labels = batch["stc_labels"].view(-1) + stc_labels = stc_labels[stc_mask] + + stc_loss = self.loss_func(stc_logits, stc_labels) + + return stc_loss + + def _get_el_loss(self, el_outputs, batch, from_key=False): + bsz, max_seq_length = batch["attention_mask_layoutxlm"].shape + + device = batch["attention_mask_layoutxlm"].device + + self_token_mask = ( + torch.eye(max_seq_length, max_seq_length + 1).to(device).bool() + ) + + box_first_token_mask = torch.cat( + [ + (batch["are_box_first_tokens"] == False), + torch.zeros([bsz, 1], dtype=torch.bool).to(device), + ], + axis=1, + ) + el_outputs.masked_fill_(box_first_token_mask[:, None, :], -10000.0) + el_outputs.masked_fill_(self_token_mask[None, :, :], -10000.0) + + mask = batch["are_box_first_tokens"].view(-1) + + logits = el_outputs.view(-1, max_seq_length + 1) + logits = logits[mask] + + if from_key: + el_labels = batch["el_labels_from_key"] + else: + el_labels = batch["el_labels"] + labels = el_labels.view(-1) + labels = labels[mask] + + loss = self.loss_func(logits, labels) + return loss diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/relation_extractor.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/relation_extractor.py new file mode 100644 index 0000000..40a169e --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/relation_extractor.py @@ -0,0 +1,48 @@ +import torch +from torch import nn + + +class RelationExtractor(nn.Module): + def __init__( + self, + n_relations, + backbone_hidden_size, + head_hidden_size, + head_p_dropout=0.1, + ): + super().__init__() + + self.n_relations = n_relations + self.backbone_hidden_size = backbone_hidden_size + self.head_hidden_size = head_hidden_size + self.head_p_dropout = head_p_dropout + + self.drop = nn.Dropout(head_p_dropout) + self.q_net = nn.Linear( + self.backbone_hidden_size, self.n_relations * self.head_hidden_size + ) + + self.k_net = nn.Linear( + self.backbone_hidden_size, self.n_relations * self.head_hidden_size + ) + + self.dummy_node = nn.Parameter(torch.Tensor(1, self.backbone_hidden_size)) + nn.init.normal_(self.dummy_node) + + def forward(self, h_q, h_k): + h_q = self.q_net(self.drop(h_q)) + + dummy_vec = self.dummy_node.unsqueeze(0).repeat(1, h_k.size(1), 1) + h_k = torch.cat([h_k, dummy_vec], axis=0) + h_k = self.k_net(self.drop(h_k)) + + head_q = h_q.view( + h_q.size(0), h_q.size(1), self.n_relations, self.head_hidden_size + ) + head_k = h_k.view( + h_k.size(0), h_k.size(1), self.n_relations, self.head_hidden_size + ) + + relation_score = torch.einsum("ibnd,jbnd->nbij", (head_q, head_k)) + + return relation_score diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/sbt_model.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/sbt_model.py new file mode 100644 index 0000000..736d223 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/model/sbt_model.py @@ -0,0 +1,156 @@ +import torch +from torch import nn +from transformers import LayoutLMConfig, LayoutLMModel, LayoutLMTokenizer, LayoutLMv2FeatureExtractor +from transformers import LayoutLMv2Config, LayoutLMv2Model +from sdsvkvu.model.relation_extractor import RelationExtractor +from sdsvkvu.model.kvu_model import KVUModel +# from utils import load_checkpoint + + +class SBTModel(KVUModel): + def __init__(self, cfg): + super().__init__(cfg=cfg) + + self.model_cfg = cfg.model + self.freeze = cfg.train.freeze + self.train_cfg = cfg.train + self.n_classes = len(self.model_cfg.class_names) + + self._get_backbones(self.model_cfg.backbone) + self._create_head() + + self.loss_func = nn.CrossEntropyLoss() + + + def _create_head(self): + self.backbone_hidden_size = self.backbone_config.hidden_size + self.head_hidden_size = self.model_cfg.head_hidden_size + self.head_p_dropout = self.model_cfg.head_p_dropout + # self.n_classes = self.model_cfg.n_classes + 1 + # self.relations = self.model_cfg.n_relations + self.repr_hiddent_size = self.backbone_hidden_size + + # (1) Initial token classification + self.itc_layer = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.backbone_hidden_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.backbone_hidden_size, self.n_classes), + ) + # (2) Subsequent token classification + self.stc_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # (3) Linking token classification + self.relation_layer = RelationExtractor( + n_relations=1, #1 + backbone_hidden_size=self.backbone_hidden_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + # Classfication Layer for whole document + self.itc_layer_document = nn.Sequential( + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.repr_hiddent_size), + nn.Dropout(self.head_p_dropout), + nn.Linear(self.repr_hiddent_size, self.n_classes), + ) + + self.stc_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.relation_layer_document = RelationExtractor( + n_relations=1, + backbone_hidden_size=self.repr_hiddent_size, + head_hidden_size=self.head_hidden_size, + head_p_dropout=self.head_p_dropout, + ) + + self.itc_layer.apply(self._init_weight) + self.stc_layer.apply(self._init_weight) + self.relation_layer.apply(self._init_weight) + + self.itc_layer_document.apply(self._init_weight) + self.stc_layer_document.apply(self._init_weight) + self.relation_layer_document.apply(self._init_weight) + + + + def forward(self, batches): + head_outputs_list = [] + loss = 0. + for batch in batches["windows"]: + image = batch["image"] + input_ids = batch["input_ids_layoutxlm"] + bbox = batch["bbox"] + attention_mask = batch["attention_mask_layoutxlm"] + + if self.freeze: + for param in self.backbone.parameters(): + param.requires_grad = False + + if self.model_cfg.backbone == 'layoutxlm': + backbone_outputs = self.backbone( + image=image, input_ids=input_ids, bbox=bbox, attention_mask=attention_mask + ) + else: + backbone_outputs = self.backbone(input_ids, attention_mask=attention_mask) + + last_hidden_states = backbone_outputs.last_hidden_state[:, :512, :] + last_hidden_states = last_hidden_states.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer(last_hidden_states).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer(last_hidden_states, last_hidden_states).squeeze(0) + el_outputs = self.relation_layer(last_hidden_states, last_hidden_states).squeeze(0) + + window_repr = last_hidden_states.transpose(0, 1).contiguous() + + head_outputs = {"window_repr": window_repr, + "itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs,} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + head_outputs_list.append(head_outputs) + + batch = batches["documents"] + + document_repr = torch.cat([w['window_repr'] for w in head_outputs_list], dim=1) + document_repr = document_repr.transpose(0, 1).contiguous() + + itc_outputs = self.itc_layer_document(document_repr).transpose(0, 1).contiguous() + stc_outputs = self.stc_layer_document(document_repr, document_repr).squeeze(0) + el_outputs = self.relation_layer_document(document_repr, document_repr).squeeze(0) + + head_outputs = {"itc_outputs": itc_outputs, + "stc_outputs": stc_outputs, + "el_outputs": el_outputs} + + if any(['labels' in key for key in batch.keys()]): + loss += self._get_loss(head_outputs, batch) + + return head_outputs, loss + + def _get_loss(self, head_outputs, batch): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + + itc_loss = self._get_itc_loss(itc_outputs, batch) + stc_loss = self._get_stc_loss(stc_outputs, batch) + el_loss = self._get_el_loss(el_outputs, batch, from_key=False) + + loss = itc_loss + stc_loss + el_loss + + return loss diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/predictor.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/predictor.py new file mode 100644 index 0000000..2861a31 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/predictor.py @@ -0,0 +1,225 @@ +import torch +from pathlib import Path +from omegaconf import OmegaConf + +import os +from sdsvkvu.sources.utils import parse_initial_words, parse_subsequent_words, parse_relations +from sdsvkvu.model import get_model, load_model_weight + + +class KVUPredictor: + def __init__(self, configs): + self.mode = configs.mode + self.device = configs.device + self.pretrained_model_path = configs.model.pretrained_model_path + net, cfg = self._load_model(configs.model.config, + configs.model.checkpoint) + + self.model = net + self.class_names = cfg.model.class_names + self.max_seq_length = cfg.train.max_seq_length + self.backbone_type = cfg.model.backbone + + if self.mode in (3, 4): + self.slice_interval = 0 + self.window_size = cfg.train.window_size + self.max_window_count = cfg.train.max_window_count + self.dummy_idx = self.max_seq_length * self.max_window_count + + else: + self.slice_interval = cfg.train.slice_interval + self.window_size = cfg.train.max_num_words + self.max_window_count = 1 + if self.mode == 2: + self.dummy_idx = 0 # dynamic dummy + else: + self.dummy_idx = self.max_seq_length # 512 + + + def get_process_configs(self): + _settings = { + # "tokenizer_layoutxlm": self.model.tokenizer_layoutxlm, + # "feature_extractor": self.model.feature_extractor, + "class_names": self.class_names, + "backbone_type": self.backbone_type, + "window_size": self.window_size, + "slice_interval": self.slice_interval, + "max_window_count": self.max_window_count, + "max_seq_length": self.max_seq_length, + "device": self.device, + "mode": self.mode + } + + feature_extractor = self.model.feature_extractor + if self.mode in (3, 4): + tokenizer_layoutxlm = self.model.tokenizer + else: + tokenizer_layoutxlm = self.model.tokenizer_layoutxlm + + return OmegaConf.create(_settings), tokenizer_layoutxlm, feature_extractor + + + def _load_model(self, cfg_path, ckpt_path): + cfg = OmegaConf.load(cfg_path) + + if self.pretrained_model_path is not None and os.path.exists(self.pretrained_model_path): + cfg.model.pretrained_model_path = self.pretrained_model_path + print("[INFO] Load pretrained backbone at:", cfg.model.pretrained_model_path) + + cfg.mode = self.mode + net = get_model(cfg) + load_model_weight(net, ckpt_path) + net.to(self.device) + net.eval() + return net, cfg + + def predict(self, input_sample): + if self.mode == 0: # Normal + bbox, lwords, pr_class_words, pr_relations = self.com_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 1: # Full - tokens + bbox, lwords, pr_class_words, pr_relations = self.cat_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 2: # Sliding + bbox, lwords, pr_class_words, pr_relations = [], [], [], [] + for window in input_sample['windows']: + _bbox, _lwords, _pr_class_words, _pr_relations = self.com_predict(window) + bbox.append(_bbox) + lwords.append(_lwords) + pr_class_words.append(_pr_class_words) + pr_relations.append(_pr_relations) + return bbox, lwords, pr_class_words, pr_relations + + elif self.mode == 3: # Document + bbox, lwords, pr_class_words, pr_relations = self.doc_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + + elif self.mode == 4: # SBT + bbox, lwords, pr_class_words, pr_relations = self.sbt_predict(input_sample) + return [bbox], [lwords], [pr_class_words], [pr_relations] + else: + raise ValueError(f"Not supported mode: {self.mode }") + + def doc_predict(self, input_sample): + lwords = input_sample['documents']['words'] + for idx, window in enumerate(input_sample['windows']): + input_sample['windows'][idx] = {k: v.unsqueeze(0).to(self.device) for k, v in window.items() if k not in ('words', 'n_empty_windows')} + + with torch.no_grad(): + head_outputs, _ = self.model(input_sample) + + input_sample = input_sample['documents'] + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + # input_sample = {k: v.detach().cpu() for k, v in input_sample.items()} + + bbox = input_sample['bbox'].squeeze(0) + pr_class_words, pr_relations = self.kvu_parser(input_sample, head_outputs) + + return bbox, lwords, pr_class_words, pr_relations + + + def com_predict(self, input_sample): + lwords = input_sample['words'] + input_sample = {k: v.unsqueeze(0) for k, v in input_sample.items() if k not in ('words', 'img_path')} + input_sample = {k: v.to(self.device) for k, v in input_sample.items()} + + with torch.no_grad(): + head_outputs, _ = self.model(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + input_sample = {k: v.detach().cpu() for k, v in input_sample.items()} + + + bbox = input_sample['bbox'].squeeze(0) + pr_class_words, pr_relations = self.kvu_parser(input_sample, head_outputs) + + return bbox, lwords, pr_class_words, pr_relations + + + def cat_predict(self, input_sample): + lwords = input_sample['documents']['words'] + inputs = [] + for window in input_sample['windows']: + inputs.append({k: v.unsqueeze(0).cuda() for k, v in window.items() if k not in ('words', 'img_path')}) + input_sample['windows'] = inputs + + with torch.no_grad(): + head_outputs, _ = self.model(input_sample) + + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items() if k not in ('embedding_tokens')} + + + input_sample = {k: v.unsqueeze(0) for k, v in input_sample["documents"].items()} + + bbox = input_sample['bbox'].squeeze(0) + self.dummy_idx = bbox.shape[0] + pr_class_words, pr_relations = self.kvu_parser(input_sample, head_outputs) + return bbox, lwords, pr_class_words, pr_relations + + + def kvu_parser(self, input_sample, head_outputs): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + el_outputs_from_key = head_outputs["el_outputs_from_key"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + pr_el_from_key = torch.argmax(el_outputs_from_key, -1).squeeze(0) + + box_first_token_mask = input_sample['are_box_first_tokens'].squeeze(0) + attention_mask = input_sample['attention_mask_layoutxlm'].squeeze(0) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, self.dummy_idx + ) + + pr_relations_from_header = parse_relations(pr_el_label, box_first_token_mask, self.dummy_idx) + pr_relations_from_key = parse_relations(pr_el_from_key, box_first_token_mask, self.dummy_idx) + pr_relations = pr_relations_from_header | pr_relations_from_key + + return pr_class_words, pr_relations + + + def sbt_predict(self, input_sample): + lwords = input_sample['documents']['words'] + for idx, window in enumerate(input_sample['windows']): + input_sample['windows'][idx] = {k: v.unsqueeze(0).to(self.device) for k, v in window.items() if k not in ('words', 'n_empty_windows')} + + with torch.no_grad(): + head_outputs, _ = self.model(input_sample) + + input_sample = input_sample['documents'] + head_outputs = {k: v.detach().cpu() for k, v in head_outputs.items()} + # input_sample = {k: v.detach().cpu() for k, v in input_sample.items()} + + bbox = input_sample['bbox'].squeeze(0) + pr_class_words, pr_relations = self.sbt_parser(input_sample, head_outputs) + + return bbox, lwords, pr_class_words, pr_relations + + + def sbt_parser(self, input_sample, head_outputs): + itc_outputs = head_outputs["itc_outputs"] + stc_outputs = head_outputs["stc_outputs"] + el_outputs = head_outputs["el_outputs"] + + pr_itc_label = torch.argmax(itc_outputs, -1).squeeze(0) + pr_stc_label = torch.argmax(stc_outputs, -1).squeeze(0) + pr_el_label = torch.argmax(el_outputs, -1).squeeze(0) + + box_first_token_mask = input_sample['are_box_first_tokens'].squeeze(0) + attention_mask = input_sample['attention_mask_layoutxlm'].squeeze(0) + + pr_init_words = parse_initial_words(pr_itc_label, box_first_token_mask, self.class_names) + pr_class_words = parse_subsequent_words( + pr_stc_label, attention_mask, pr_init_words, self.dummy_idx + ) + + pr_relations = parse_relations(pr_el_label, box_first_token_mask, self.dummy_idx) + + return pr_class_words, pr_relations \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/preprocess.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/preprocess.py new file mode 100644 index 0000000..c499a61 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/preprocess.py @@ -0,0 +1,479 @@ +import torch +import itertools +import numpy as np + +from sdsvkvu.sources.utils import sliding_windows + + +class KVUProcessor: + def __init__( + self, + tokenizer_layoutxlm, + feature_extractor, + backbone_type, + class_names, + slice_interval, + window_size, + max_seq_length, + mode, + **kwargs, + ): + self.mode = mode + self.class_names = class_names + self.backbone_type = backbone_type + + self.window_size = window_size + self.slice_interval = slice_interval + self.max_seq_length = max_seq_length + + self.tokenizer_layoutxlm = tokenizer_layoutxlm + self.feature_extractor = feature_extractor + + self.pad_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids( + tokenizer_layoutxlm._pad_token + ) + self.cls_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids( + tokenizer_layoutxlm._cls_token + ) + self.sep_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids( + tokenizer_layoutxlm._sep_token + ) + self.unk_token_id_layoutxlm = tokenizer_layoutxlm.convert_tokens_to_ids( + self.tokenizer_layoutxlm._unk_token + ) + + self.class_idx_dic = dict( + [(class_name, idx) for idx, class_name in enumerate(self.class_names)] + ) + + def __call__(self, lbboxes: list, lwords: list, image, width, height) -> dict: + image = torch.from_numpy( + self.feature_extractor(image)["pixel_values"][0].copy() + ) + + bbox_windows = sliding_windows(lbboxes, self.window_size, self.slice_interval) + word_windows = sliding_windows(lwords, self.window_size, self.slice_interval) + assert len(bbox_windows) == len(word_windows), \ + f"Shape of lbboxes and lwords after sliding window is not the same {len(bbox_windows)} # {len(word_windows)}" + + if self.mode == 0: # First 512 tokens + output = self.preprocess_window( + bounding_boxes=lbboxes, + words=lwords, + image_features={"image": image, "width": width, "height": height}, + max_seq_length=self.max_seq_length, + ) + elif self.mode == 1: # Get full tokens + output = {} + windows = [] + for i in range(len(bbox_windows)): + windows.append( + self.preprocess_window( + bounding_boxes=bbox_windows[i], + words=word_windows[i], + image_features={"image": image, "width": width, "height": height}, + max_seq_length=self.max_seq_length, + ) + ) + + output["windows"] = windows + elif self.mode == 2: # Sliding window + output = {} + windows = [] + output["doduments"] = self.preprocess_window( + bounding_boxes=lbboxes, + words=lwords, + image_features={"image": image, "width": width, "height": height}, + max_seq_length=2048, + ) + for i in range(len(bbox_windows)): + windows.append( + self.preprocess( + bounding_boxes=bbox_windows[i], + words=word_windows[i], + image_features={"image": image, "width": width, "height": height}, + max_seq_length=self.max_seq_length, + ) + ) + + output["windows"] = windows + else: + raise ValueError(f"Not supported mode: {self.mode }") + return output + + def preprocess_window(self, bounding_boxes, words, image_features, max_seq_length): + list_word_objects = [] + for bb, text in zip(bounding_boxes, words): + boundingBox = [ + [bb[0], bb[1]], + [bb[2], bb[1]], + [bb[2], bb[3]], + [bb[0], bb[3]], + ] + tokens = self.tokenizer_layoutxlm.convert_tokens_to_ids( + self.tokenizer_layoutxlm.tokenize(text) + ) + list_word_objects.append( + {"layoutxlm_tokens": tokens, "boundingBox": boundingBox, "text": text} + ) + + ( + bbox, + input_ids, + attention_mask, + are_box_first_tokens, + box_to_token_indices, + box2token_span_map, + lwords, + len_valid_tokens, + len_non_overlap_tokens, + len_list_tokens, + ) = self.parser_words( + list_word_objects, + self.max_seq_length, + image_features["width"], + image_features["height"], + ) + + assert len_list_tokens == len_valid_tokens + 2 + len_overlap_tokens = len_valid_tokens - len_non_overlap_tokens + + ntokens = max_seq_length if max_seq_length == 512 else len_valid_tokens + 2 + + input_ids = input_ids[:ntokens] + attention_mask = attention_mask[:ntokens] + bbox = bbox[:ntokens] + are_box_first_tokens = are_box_first_tokens[:ntokens] + + input_ids = torch.from_numpy(input_ids) + attention_mask = torch.from_numpy(attention_mask) + bbox = torch.from_numpy(bbox) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + + len_valid_tokens = torch.tensor(len_valid_tokens) + len_overlap_tokens = torch.tensor(len_overlap_tokens) + return_dict = { + "words": lwords, + "len_overlap_tokens": len_overlap_tokens, + "len_valid_tokens": len_valid_tokens, + "image": image_features["image"], + "input_ids_layoutxlm": input_ids, + "attention_mask_layoutxlm": attention_mask, + "are_box_first_tokens": are_box_first_tokens, + "bbox": bbox, + } + return return_dict + + def parser_words(self, words, max_seq_length, width, height): + list_bbs = [] + list_words = [] + list_tokens = [] + cls_bbs = [0.0] * 8 + box2token_span_map = [] + box_to_token_indices = [] + lwords = [""] * max_seq_length + + cum_token_idx = 0 + len_valid_tokens = 0 + len_non_overlap_tokens = 0 + + input_ids = np.ones(max_seq_length, dtype=int) * self.pad_token_id_layoutxlm + bbox = np.zeros((max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(max_seq_length, dtype=np.bool_) + + for word_idx, word in enumerate(words): + this_box_token_indices = [] + + tokens = word["layoutxlm_tokens"] + bb = word["boundingBox"] + text = word["text"] + + len_valid_tokens += len(tokens) + if word_idx < self.slice_interval: + len_non_overlap_tokens += len(tokens) + + if len(tokens) == 0: + tokens.append(self.unk_token_id) + + if len(list_tokens) + len(tokens) > max_seq_length - 2: + break + + box2token_span_map.append( + [len(list_tokens) + 1, len(list_tokens) + len(tokens) + 1] + ) # including st_idx + list_tokens += tokens + + # min, max clipping + for coord_idx in range(4): + bb[coord_idx][0] = max(0.0, min(bb[coord_idx][0], width)) + bb[coord_idx][1] = max(0.0, min(bb[coord_idx][1], height)) + + bb = list(itertools.chain(*bb)) + bbs = [bb for _ in range(len(tokens))] + texts = [text for _ in range(len(tokens))] + + for _ in tokens: + cum_token_idx += 1 + this_box_token_indices.append(cum_token_idx) + + list_bbs.extend(bbs) + list_words.extend(texts) #### + box_to_token_indices.append(this_box_token_indices) + + sep_bbs = [width, height] * 4 + + # For [CLS] and [SEP] + list_tokens = ( + [self.cls_token_id_layoutxlm] + + list_tokens[: max_seq_length - 2] + + [self.sep_token_id_layoutxlm] + ) + if len(list_bbs) == 0: + # When len(json_obj["words"]) == 0 (no OCR result) + list_bbs = [cls_bbs] + [sep_bbs] + else: # len(list_bbs) > 0 + list_bbs = [cls_bbs] + list_bbs[: max_seq_length - 2] + [sep_bbs] + # list_words = ['CLS'] + list_words[: max_seq_length - 2] + ['SEP'] ### + # if len(list_words) < 510: + # list_words.extend(['

    ' for _ in range(510 - len(list_words))]) + list_words = ( + [self.tokenizer_layoutxlm._cls_token] + + list_words[: max_seq_length - 2] + + [self.tokenizer_layoutxlm._sep_token] + ) + + len_list_tokens = len(list_tokens) + input_ids[:len_list_tokens] = list_tokens + attention_mask[:len_list_tokens] = 1 + + bbox[:len_list_tokens, :] = list_bbs + lwords[:len_list_tokens] = list_words + + # Normalize bbox -> 0 ~ 1 + bbox[:, [0, 2, 4, 6]] = bbox[:, [0, 2, 4, 6]] / width + bbox[:, [1, 3, 5, 7]] = bbox[:, [1, 3, 5, 7]] / height + + if self.backbone_type in ("layoutlm", "layoutxlm"): + bbox = bbox[:, [0, 1, 4, 5]] + bbox = bbox * 1000 + bbox = bbox.astype(int) + else: + assert False + + st_indices = [ + indices[0] + for indices in box_to_token_indices + if indices[0] < max_seq_length + ] + are_box_first_tokens[st_indices] = True + + return ( + bbox, + input_ids, + attention_mask, + are_box_first_tokens, + box_to_token_indices, + box2token_span_map, + lwords, + len_valid_tokens, + len_non_overlap_tokens, + len_list_tokens, + ) + + +class DocKVUProcessor(KVUProcessor): + def __init__( + self, + tokenizer_layoutxlm, + feature_extractor, + backbone_type, + class_names, + max_window_count, + slice_interval, + window_size, + max_seq_length, + mode, + **kwargs, + ): + super().__init__( + tokenizer_layoutxlm=tokenizer_layoutxlm, + feature_extractor=feature_extractor, + backbone_type=backbone_type, + class_names=class_names, + slice_interval=slice_interval, + window_size=window_size, + max_seq_length=max_seq_length, + mode=mode, + ) + + self.max_window_count = max_window_count + self.pad_token_id = self.pad_token_id_layoutxlm + self.cls_token_id = self.cls_token_id_layoutxlm + self.sep_token_id = self.sep_token_id_layoutxlm + self.unk_token_id = self.unk_token_id_layoutxlm + self.tokenizer = self.tokenizer_layoutxlm + + def __call__(self, lbboxes: list, lwords: list, images, width, height) -> dict: + image_features = torch.from_numpy( + self.feature_extractor(images)["pixel_values"][0].copy() + ) + output = self.preprocess_document( + bounding_boxes=lbboxes, + words=lwords, + image_features={"image": image_features, "width": width, "height": height}, + max_seq_length=self.max_seq_length, + ) + return output + + def preprocess_document(self, bounding_boxes, words, image_features, max_seq_length): + n_words = len(words) + output_dicts = {"windows": [], "documents": []} + n_empty_windows = 0 + + for i in range(self.max_window_count): + input_ids = np.ones(max_seq_length, dtype=int) * self.pad_token_id + bbox = np.zeros((max_seq_length, 8), dtype=np.float32) + attention_mask = np.zeros(max_seq_length, dtype=int) + are_box_first_tokens = np.zeros(max_seq_length, dtype=np.bool_) + + if n_words == 0: + n_empty_windows += 1 + output_dicts["windows"].append( + { + "image": image_features["image"], + "input_ids_layoutxlm": torch.from_numpy(input_ids), + "bbox": torch.from_numpy(bbox), + "words": [], + "attention_mask_layoutxlm": torch.from_numpy(attention_mask), + "are_box_first_tokens": torch.from_numpy(are_box_first_tokens), + } + ) + continue + + start_word_idx = i * self.window_size + stop_word_idx = min(n_words, (i + 1) * self.window_size) + + if start_word_idx >= stop_word_idx: + n_empty_windows += 1 + output_dicts["windows"].append(output_dicts["windows"][-1]) + continue + + list_word_objects = [] + for bb, text in zip( + bounding_boxes[start_word_idx:stop_word_idx], + words[start_word_idx:stop_word_idx], + ): + boundingBox = [ + [bb[0], bb[1]], + [bb[2], bb[1]], + [bb[2], bb[3]], + [bb[0], bb[3]], + ] + tokens = self.tokenizer_layoutxlm.convert_tokens_to_ids( + self.tokenizer_layoutxlm.tokenize(text) + ) + list_word_objects.append( + { + "layoutxlm_tokens": tokens, + "boundingBox": boundingBox, + "text": text, + } + ) + + ( + bbox, + input_ids, + attention_mask, + are_box_first_tokens, + box_to_token_indices, + box2token_span_map, + lwords, + len_valid_tokens, + len_non_overlap_tokens, + len_list_layoutxlm_tokens, + ) = self.parser_words( + list_word_objects, + self.max_seq_length, + image_features["width"], + image_features["height"], + ) + + input_ids = torch.from_numpy(input_ids) + bbox = torch.from_numpy(bbox) + attention_mask = torch.from_numpy(attention_mask) + are_box_first_tokens = torch.from_numpy(are_box_first_tokens) + + return_dict = { + "bbox": bbox, + "words": lwords, + "image": image_features["image"], + "input_ids_layoutxlm": input_ids, + "attention_mask_layoutxlm": attention_mask, + "are_box_first_tokens": are_box_first_tokens, + } + output_dicts["windows"].append(return_dict) + + attention_mask = torch.cat( + [o["attention_mask_layoutxlm"] for o in output_dicts["windows"]] + ) + are_box_first_tokens = torch.cat( + [o["are_box_first_tokens"] for o in output_dicts["windows"]] + ) + if n_empty_windows > 0: + attention_mask[ + self.max_seq_length * (self.max_window_count - n_empty_windows) : + ] = torch.from_numpy( + np.zeros(self.max_seq_length * n_empty_windows, dtype=int) + ) + are_box_first_tokens[ + self.max_seq_length * (self.max_window_count - n_empty_windows) : + ] = torch.from_numpy( + np.zeros(self.max_seq_length * n_empty_windows, dtype=np.bool_) + ) + bbox = torch.cat([o["bbox"] for o in output_dicts["windows"]]) + words = [] + for o in output_dicts["windows"]: + words.extend(o["words"]) + + return_dict = { + "bbox": bbox, + "words": words, + "attention_mask_layoutxlm": attention_mask, + "are_box_first_tokens": are_box_first_tokens, + "n_empty_windows": n_empty_windows, + } + output_dicts["documents"] = return_dict + + return output_dicts + + +class SBTProcessor(DocKVUProcessor): + def __init__( + self, + tokenizer_layoutxlm, + feature_extractor, + backbone_type, + class_names, + max_window_count, + slice_interval, + window_size, + max_seq_length, + mode, + **kwargs, + ): + super().__init__( + tokenizer_layoutxlm, + feature_extractor, + backbone_type, + class_names, + max_window_count, + slice_interval, + window_size, + max_seq_length, + mode, + **kwargs, + ) + + def __call__(self, lbboxes: list, lwords: list, images, width, height) -> dict: + return super().__call__(lbboxes, lwords, images, width, height) \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/run_ocr.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/run_ocr.py new file mode 100644 index 0000000..8bce812 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/modules/run_ocr.py @@ -0,0 +1,25 @@ +import numpy as np +from pathlib import Path +from typing import Union, Tuple, List +from sdsvkvu.externals.basic_ocr.src.ocr import OcrEngine + + +def load_ocr_engine(opt) -> OcrEngine: + print("[INFO] Loading engine...") + engine = OcrEngine(**opt) + print("[INFO] Engine loaded") + return engine + + +def process_img(img: Union[str, np.ndarray], engine: OcrEngine) -> List: # For OCR integrated deskew using paddle + page = engine(img) + bboxes = [] + texts = [] + for word_segment in page._word_segments: + for word in word_segment.list_words: + bboxes.append(word.bbox[:]) + texts.append(word.text) + + image = page.deskewed_image if page.deskewed_image is not None else page.image + return bboxes, texts, image + \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/settings.yml b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/settings.yml new file mode 100644 index 0000000..685f9d3 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/settings.yml @@ -0,0 +1,21 @@ +device: "cuda:0" # "cuda:0" +mode: 4 # best option to infer +model: + pretrained_model_path: /mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu/microsoft/layoutxlm-base # default: "" + config: /home/sds/tuanlv/02-KVU/03-KVU_sbt/experiments/key_value_understanding_for_sbt-20231121-085847/base.yaml + checkpoint: /home/sds/tuanlv/02-KVU/03-KVU_sbt/experiments/key_value_understanding_for_sbt-20231121-085847/checkpoints/best_model.pth +ocr_engine: + detector: + # version: /home/sds/datnt/mmdetection/wild_receipt_finetune_weights_c_lite.pth + version: /mnt/hdd4T/OCR/datnt/mmdetection/logs/textdet-baseline-Nov3-wildreceiptv4-sdsapv1-mcocr-ssreceipt1_Imei/epoch_100_params.pth + rotator_version: /home/sds/datnt/mmdetection/logs/textdet-with-rotate-20230317/best_bbox_mAP_epoch_30_lite.pth + recognizer: + version: satrn-lite-general-pretrain-20230106 + deskew: + enable: True + text_detector: + config: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/config/det.yaml + weight: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_PP-OCRv3_det_infer + text_cls: + config: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/config/cls.yaml + weight: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_ppocr_mobile_v2.0_cls_infer diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/kvu.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/kvu.py new file mode 100644 index 0000000..1b560b0 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/kvu.py @@ -0,0 +1,73 @@ +import imagesize +from PIL import Image +from pathlib import Path +from omegaconf import OmegaConf + +from sdsvkvu.modules.predictor import KVUPredictor +from sdsvkvu.modules.preprocess import KVUProcessor, DocKVUProcessor, SBTProcessor +from sdsvkvu.modules.run_ocr import load_ocr_engine, process_img +from sdsvkvu.utils.utils import post_process_basic_ocr +from sdsvkvu.sources.utils import revert_scale_bbox, Timer + +DEFAULT_SETTING_PATH = str(Path(__file__).parents[1]) + "/settings.yml" + + +class KVUEngine: + def __init__(self, setting_file: str = DEFAULT_SETTING_PATH, ocr_engine=None, **kwargs) -> None: + configs = OmegaConf.load(setting_file) + for key, param in kwargs.items(): # overwrite default settings by keyword arguments + if key not in configs: + raise ValueError("Invalid setting found in KVUEngine: ", key) + if isinstance(param, dict): + for k, v in param.items(): + if k not in configs[key]: + raise ValueError("Invalid setting found in KVUEngine: ", key, k) + configs[key][k] = v + else: + configs[key] = param + + self.predictor = KVUPredictor(configs) + self._settings, tokenizer_layoutxlm, feature_extractor = self.predictor.get_process_configs() + mode = self._settings.mode + if mode in (0, 1, 2): + self.processor = KVUProcessor(tokenizer_layoutxlm=tokenizer_layoutxlm, + feature_extractor=feature_extractor, + **self._settings) + elif mode == 3: + self.processor = DocKVUProcessor(tokenizer_layoutxlm=tokenizer_layoutxlm, + feature_extractor=feature_extractor, + **self._settings) + elif mode == 4: + self.processor = SBTProcessor(tokenizer_layoutxlm=tokenizer_layoutxlm, + feature_extractor=feature_extractor, + **self._settings) + else: + raise ValueError(f'[ERROR] Inferencing mode of {mode} is not supported') + + if ocr_engine is None: + print("[INFO] Load internal OCR Engine") + configs.ocr_engine.device = configs.device + self.ocr_engine = load_ocr_engine(configs.ocr_engine) + else: + print("[INFO] Load external OCR Engine") + self.ocr_engine = ocr_engine + + def predict(self, img_path): + lbboxes, lwords, image = process_img(img_path, self.ocr_engine) + lwords = post_process_basic_ocr(lwords) + + if len(lbboxes) == 0: + print("[WARNING] Empty document") + return image, [[]], [[]], [[]], [[]] + + height, width, _ = image.shape + image = Image.fromarray(image) + + inputs = self.processor(lbboxes, lwords, image, width=width, height=height) + + with Timer("kvu"): + lbbox, lwords, pr_class_words, pr_relations = self.predictor.predict(inputs) + + for i in range(len(lbbox)): + lbbox[i] = [revert_scale_bbox(bb, width=width, height=height) for bb in lbbox[i]] + return image, lbbox, lwords, pr_class_words, pr_relations \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/utils.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/utils.py new file mode 100644 index 0000000..53bc12e --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/sources/utils.py @@ -0,0 +1,610 @@ +import os +import cv2 +import copy +import time +import torch +import math +import numpy as np +from typing import Callable +from sdsvkvu.utils.post_processing import get_string, get_string_by_deduplicate_bbox, get_string_with_word2line + +# def get_colormap(): +# return { +# 'others': (0, 0, 255), # others: red +# 'title': (0, 255, 255), # title: yellow +# 'key': (255, 0, 0), # key: blue +# 'value': (0, 255, 0), # value: green +# 'header': (233, 197, 15), # header +# 'group': (0, 128, 128), # group +# 'relation': (0, 0, 255)# (128, 128, 128), # relation +# } + + +class Timer: + def __init__(self, name: str) -> None: + self.name = name + + def __enter__(self): + self.start_time = time.perf_counter() + return self + + def __exit__(self, func: Callable, *args): + self.end_time = time.perf_counter() + self.elapsed_time = self.end_time - self.start_time + print(f"[INFO]: {self.name} took : {self.elapsed_time:.6f} seconds") + +def get_colormap(): + return { + "others": (0, 0, 255), # others: red + "title": (0, 255, 255), # title: yellow + "key": (255, 0, 0), # key: blue + "value": (0, 255, 0), # value: green + "header": (233, 197, 15), # header + "group": (0, 128, 128), # group + "relation": (0, 0, 255), # (128, 128, 128), # relation + + # "others": (187, 125, 250), # pink + "seller": (183, 50, 255), # bold pink + "date_key": (128, 51, 115), # orange + "date_value": (55, 250, 250), # yellow + "product_name": (245, 61, 61), # blue + "product_code": (233, 197, 17), # header + "quantity": (102, 255, 102), # green + "sn_key": (179, 134, 89), + "sn_value": (51, 153, 204), + "invoice_number_key": (40, 90, 144), + "invoice_number_value": (162, 239, 204), + "sold_key": (74, 180, 150), + "sold_value": (14, 184, 53), + "voucher": (39, 86, 103), + "website": (207, 19, 85), + "hotline": (153, 224, 56), + # "group": (0, 128, 128), # brown + # "relation": (0, 0, 255), # (128, 128, 128), # red + } + +def convert_image(image): + exif = image._getexif() + orientation = None + if exif is not None: + orientation = exif.get(0x0112) + # Convert the PIL image to OpenCV format + image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + # Rotate the image in OpenCV if necessary + if orientation == 3: + image = cv2.rotate(image, cv2.ROTATE_180) + elif orientation == 6: + image = cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE) + elif orientation == 8: + image = cv2.rotate(image, cv2.ROTATE_90_COUNTERCLOCKWISE) + else: + image = np.asarray(image) + + if len(image.shape) == 2: + image = np.repeat(image[:, :, np.newaxis], 3, axis=2) + assert len(image.shape) == 3 + + return image, orientation + +def visualize(image, bbox, pr_class_words, pr_relations, color_map, labels=['others', 'title', 'key', 'value', 'header'], thickness=1): + # image, orientation = convert_image(image) + + # if orientation is not None and orientation == 6: + # width, height, _ = image.shape + # else: + # height, width, _ = image.shape + + if len(pr_class_words) > 0: + id2label = {k: labels[k] for k in range(len(labels))} + for lb, groups in enumerate(pr_class_words): + if lb == 0: + continue + for group_id, group in enumerate(groups): + for i, word_id in enumerate(group): + # x0, y0, x1, y1 = revert_scale_bbox(bbox[word_id], width, height) + x0, y0, x1, y1 = bbox[word_id] + cv2.rectangle(image, (x0, y0), (x1, y1), color=color_map[id2label[lb]], thickness=thickness) + + if i == 0: + x_center0, y_center0 = int((x0+x1)/2), int((y0+y1)/2) + else: + x_center1, y_center1 = int((x0+x1)/2), int((y0+y1)/2) + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['group'], thickness=thickness) + x_center0, y_center0 = x_center1, y_center1 + + if len(pr_relations) > 0: + for pair in pr_relations: + # xyxy0 = revert_scale_bbox(bbox[pair[0]], width, height) + # xyxy1 = revert_scale_bbox(bbox[pair[1]], width, height) + xyxy0 = bbox[pair[0]] + xyxy1 = bbox[pair[1]] + + x_center0, y_center0 = int((xyxy0[0] + xyxy0[2])/2), int((xyxy0[1] + xyxy0[3])/2) + x_center1, y_center1 = int((xyxy1[0] + xyxy1[2])/2), int((xyxy1[1] + xyxy1[3])/2) + + cv2.line(image, (x_center0, y_center0), (x_center1, y_center1), color=color_map['relation'], thickness=thickness) + + return image + +def revert_scale_bbox(box, width, height): + return [ + int((box[0] / 1000) * width), + int((box[1] / 1000) * height), + int((box[2] / 1000) * width), + int((box[3] / 1000) * height) + ] + + +def draw_kvu_outputs(image: np.ndarray, bbox: list, pr_class_words: list, pr_relations: list, class_names: list = ['others', 'title', 'key', 'value', 'header'], thickness: int = 1): + color_map = get_colormap() + image = visualize(image, bbox, pr_class_words, pr_relations, color_map, class_names, thickness) + if (image.shape[2] == 2): + image = cv2.cvtColor(image, cv2.COLOR_BGR5652BGR) + return cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + + +def sliding_windows(elements: list, window_size: int, slice_interval: int) -> list: + element_windows = [] + + if len(elements) > window_size: + max_step = math.ceil((len(elements) - window_size)/slice_interval) + + for i in range(0, max_step + 1): + if (i*slice_interval+window_size) >= len(elements): + _window = copy.deepcopy(elements[i*slice_interval:]) + else: + _window = copy.deepcopy(elements[i*slice_interval: i*slice_interval+window_size]) + element_windows.append(_window) + return element_windows + else: + return [elements] + + +def merged_token_embeddings(lpatches: list, loverlaps:list, lvalids: list, average: bool) -> torch.tensor: + start_pos = 1 + end_pos = start_pos + lvalids[0] + embedding_tokens = copy.deepcopy(lpatches[0][:, start_pos:end_pos, ...]) + cls_token = copy.deepcopy(lpatches[0][:, :1, ...]) + sep_token = copy.deepcopy(lpatches[0][:, -1:, ...]) + + for i in range(1, len(lpatches)): + start_pos = 1 + end_pos = start_pos + lvalids[i] + + overlap_gap = copy.deepcopy(loverlaps[i-1]) + window = copy.deepcopy(lpatches[i][:, start_pos:end_pos, ...]) + + if overlap_gap != 0: + prev_overlap = copy.deepcopy(embedding_tokens[:, -overlap_gap:, ...]) + curr_overlap = copy.deepcopy(window[:, :overlap_gap, ...]) + assert prev_overlap.shape == curr_overlap.shape, f"{prev_overlap.shape} # {curr_overlap.shape} with overlap: {overlap_gap}" + + if average: + avg_overlap = ( + prev_overlap + curr_overlap + ) / 2. + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], avg_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens[:, :-overlap_gap, ...], curr_overlap, window[:, overlap_gap:, ...]], dim=1 + ) + else: + embedding_tokens = torch.cat( + [embedding_tokens, window], dim=1 + ) + return torch.cat([cls_token, embedding_tokens, sep_token], dim=1) + + +def parse_initial_words(itc_label, box_first_token_mask, class_names): + itc_label_np = itc_label.cpu().numpy() + box_first_token_mask_np = box_first_token_mask.cpu().numpy() + + outputs = [[] for _ in range(len(class_names))] + + for token_idx, label in enumerate(itc_label_np): + if box_first_token_mask_np[token_idx] and label != 0: + outputs[label].append(token_idx) + + return outputs + + +def parse_subsequent_words(stc_label, attention_mask, init_words, dummy_idx): + max_connections = 50 + + valid_stc_label = stc_label * attention_mask.bool() + valid_stc_label = valid_stc_label.cpu().numpy() + stc_label_np = stc_label.cpu().numpy() + + valid_token_indices = np.where( + (valid_stc_label != dummy_idx) * (valid_stc_label != 0) + ) + + next_token_idx_dict = {} + for token_idx in valid_token_indices[0]: + next_token_idx_dict[stc_label_np[token_idx]] = token_idx + + outputs = [] + for init_token_indices in init_words: + sub_outputs = [] + for init_token_idx in init_token_indices: + cur_token_indices = [init_token_idx] + for _ in range(max_connections): + if cur_token_indices[-1] in next_token_idx_dict: + if ( + next_token_idx_dict[cur_token_indices[-1]] + not in init_token_indices + ): + cur_token_indices.append( + next_token_idx_dict[cur_token_indices[-1]] + ) + else: + break + else: + break + sub_outputs.append(tuple(cur_token_indices)) + + outputs.append(sub_outputs) + + return outputs + +def parse_relations(el_label, box_first_token_mask, dummy_idx): + valid_el_labels = el_label * box_first_token_mask + valid_el_labels = valid_el_labels.cpu().numpy() + el_label_np = el_label.cpu().numpy() + + max_token = box_first_token_mask.shape[0] - 1 + + valid_token_indices = np.where( + ((valid_el_labels != dummy_idx) * (valid_el_labels != 0)) ### + ) + + link_map_tuples = [] + for token_idx in valid_token_indices[0]: + link_map_tuples.append((el_label_np[token_idx], token_idx)) + + return set(link_map_tuples) + + +def get_pairs(json: list, rel_from: str, rel_to: str) -> dict: + outputs = {} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] in (rel_from, rel_to): + is_rel[element['class']]['status'] = 1 + is_rel[element['class']]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + outputs[is_rel[rel_to]['value']['group_id']] = [is_rel[rel_from]['value']['group_id'], is_rel[rel_to]['value']['group_id']] + return outputs + +def get_table_relations(json: list, header_key_pairs: dict, rel_from="key", rel_to="value") -> dict: + list_keys = list(header_key_pairs.keys()) + relations = {k: [] for k in list_keys} + for pair in json: + is_rel = {rel_from: {'status': 0}, rel_to: {'status': 0}} + for element in pair: + if element['class'] == rel_from and element['group_id'] in list_keys: + is_rel[rel_from]['status'] = 1 + is_rel[rel_from]['value'] = element + if element['class'] == rel_to: + is_rel[rel_to]['status'] = 1 + is_rel[rel_to]['value'] = element + if all([v['status'] == 1 for _, v in is_rel.items()]): + relations[is_rel[rel_from]['value']['group_id']].append(is_rel[rel_to]['value']['group_id']) + return relations + +def get_key2values_relations(key_value_pairs: dict): + triple_linkings = {} + for value_group_id, key_value_pair in key_value_pairs.items(): + key_group_id = key_value_pair[0] + if key_group_id not in list(triple_linkings.keys()): + triple_linkings[key_group_id] = [] + triple_linkings[key_group_id].append(value_group_id) + return triple_linkings + + +def get_wordgroup_bbox(lbbox: list, lword_ids: list) -> list: + points = [lbbox[i] for i in lword_ids] + x_min, y_min = min(points, key=lambda x: x[0])[0], min(points, key=lambda x: x[1])[1] + x_max, y_max = max(points, key=lambda x: x[2])[2], max(points, key=lambda x: x[3])[3] + return [x_min, y_min, x_max, y_max] + + +def merged_token_to_wordgroup(class_words: list, lwords: list, lbbox: list, labels: list) -> dict: + word_groups = {} + id2class = {i: labels[i] for i in range(len(labels))} + for class_id, lwgroups_in_class in enumerate(class_words): + for ltokens_in_wgroup in lwgroups_in_class: + group_id = ltokens_in_wgroup[0] + ltokens_to_ltexts = [lwords[token] for token in ltokens_in_wgroup] + ltokens_to_lbboxes = [lbbox[token] for token in ltokens_in_wgroup] + # text_string = get_string(ltokens_to_ltexts) + text_string = get_string_by_deduplicate_bbox(ltokens_to_ltexts, ltokens_to_lbboxes) + # text_string = get_string_with_word2line(ltokens_to_ltexts, ltokens_to_lbboxes) + group_bbox = get_wordgroup_bbox(lbbox, ltokens_in_wgroup) + word_groups[group_id] = { + 'group_id': group_id, + 'text': text_string, + 'class': id2class[class_id], + 'tokens': ltokens_in_wgroup, + 'bbox': group_bbox + } + return word_groups + +def verify_linking_id(word_groups: dict, linking_id: int) -> int: + if linking_id not in list(word_groups): + for wg_id, _word_group in word_groups.items(): + if linking_id in _word_group['tokens']: + return wg_id + return linking_id + +def matched_wordgroup_relations(word_groups:dict, lrelations: list) -> list: + outputs = [] + for pair in lrelations: + wg_from = verify_linking_id(word_groups, pair[0]) + wg_to = verify_linking_id(word_groups, pair[1]) + try: + outputs.append([word_groups[wg_from], word_groups[wg_to]]) + except Exception as e: + print('Not valid pair:', wg_from, wg_to) + return outputs + + +def get_single_entity(word_groups: dict, lrelations: list, labels: list) -> list: + # single_entity = {'title': [], 'key': [], 'value': [], 'header': []} + single_entity = {lb: [] for lb in labels} + list_linked_ids = [] + for pair in lrelations: + list_linked_ids.extend(pair) + + for word_group_id, word_group in word_groups.items(): + if word_group_id not in list_linked_ids: + single_entity[word_group['class']].append(word_group) + return single_entity + + +def export_kvu_outputs(lwords, lbbox, class_words, lrelations, labels=['others', 'title', 'key', 'value', 'header']): + word_groups = merged_token_to_wordgroup(class_words, lwords, lbbox, labels) + linking_pairs = matched_wordgroup_relations(word_groups, lrelations) + + header_key = get_pairs(linking_pairs, rel_from='header', rel_to='key') # => {key_group_id: [header_group_id, key_group_id]} + header_value = get_pairs(linking_pairs, rel_from='header', rel_to='value') # => {value_group_id: [header_group_id, value_group_id]} + key_value = get_pairs(linking_pairs, rel_from='key', rel_to='value') # => {value_group_id: [key_group_id, value_group_id]} + + single_entity = get_single_entity(word_groups, lrelations, labels=labels) + + # table_relations = get_table_relations(linking_pairs, header_key) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + key2values_relations = get_key2values_relations(key_value) # => {key_group_id: [value_group_id1, value_groupid2, ...]} + + triplet_pairs = [] + single_pairs = [] + table = [] + # print('key2values_relations', key2values_relations) + for key_group_id, list_value_group_ids in key2values_relations.items(): + if len(list_value_group_ids) == 0: continue + elif len(list_value_group_ids) == 1: + value_group_id = list_value_group_ids[0] + single_pairs.append({word_groups[key_group_id]['text']: { + 'id': value_group_id, + 'class': "value", + 'text': word_groups[value_group_id]['text'], + 'bbox': word_groups[value_group_id]['bbox'], + "key_bbox": word_groups[key_group_id]["bbox"], + }}) + else: + item = [] + for value_group_id in list_value_group_ids: + if value_group_id not in header_value.keys(): + header_name_for_value = "non-header" + else: + header_group_id = header_value[value_group_id][0] + header_name_for_value = word_groups[header_group_id]['text'] + item.append({ + 'id': value_group_id, + 'class': 'value', + 'header': header_name_for_value, + 'text': word_groups[value_group_id]['text'], + 'bbox': word_groups[value_group_id]['bbox'], + "key_bbox": word_groups[key_group_id]["bbox"], + "header_bbox": word_groups[header_group_id]["bbox"] + if header_group_id != -1 else [0, 0, 0, 0], + }) + if key_group_id not in list(header_key.keys()): + triplet_pairs.append({ + word_groups[key_group_id]['text']: item + }) + else: + header_group_id = header_key[key_group_id][0] + header_name_for_key = word_groups[header_group_id]['text'] + item.append({ + 'id': key_group_id, + 'class': 'key', + 'header': header_name_for_key, + 'text': word_groups[key_group_id]['text'], + 'bbox': word_groups[key_group_id]['bbox'], + "key_bbox": word_groups[key_group_id]["bbox"], + "header_bbox": word_groups[header_group_id]["bbox"], + }) + table.append({key_group_id: item}) + + + # Add entity without linking + single_entity_dict = {} + for class_name, single_items in single_entity.items(): + single_entity_dict[class_name] = [] + for single_item in single_items: + single_entity_dict[class_name].append({ + 'text': single_item['text'], + 'id': single_item['group_id'], + 'class': class_name, + 'bbox': single_item['bbox'] + }) + + + if len(table) > 0: + table = sorted(table, key=lambda x: list(x.keys())[0]) + table = [v for item in table for k, v in item.items()] + + outputs = {} + outputs['title'] = sorted( + single_entity_dict["title"], key=lambda x: x["id"] + ) + outputs['key'] = sorted( + single_entity_dict["key"], key=lambda x: x["id"] + ) + outputs['value'] = sorted( + single_entity_dict["value"], key=lambda x: x["id"] + ) + outputs['single'] = sorted(single_pairs, key=lambda x: int(float(list(x.values())[0]['id']))) + outputs['triplet'] = triplet_pairs + outputs['table'] = table + return outputs + + + +def export_sbt_outputs( + lwords, + lbboxes, + class_words, + lrelations, + labels, +): + word_groups = merged_token_to_wordgroup(class_words, lwords, lbboxes, labels) + linking_pairs = matched_wordgroup_relations(word_groups, lrelations) + + date_key_value_pairs = get_pairs( + linking_pairs, rel_from="date_key", rel_to="date_value" + ) # => {date_value_group_id: [date_key_group_id, date_value_group_id]} + # product_name_code_pairs = get_pairs( + # linking_pairs, rel_to="product_name", rel_from="product_code" + # ) # => {product_name_group_id: [product_code_group_id, product_name_group_id]} + # product_name_quantity_pairs = get_pairs( + # linking_pairs, rel_to="product_name", rel_from="quantity" + # ) # => {product_name_group_id: [quantity_group_id, product_name_group_id]} + serial_key_value_pairs = get_pairs( + linking_pairs, rel_from="sn_key", rel_to="sn_value" + ) # => {sn_value_group_id: [sn_key_group_id, sn_value_group_id]} + + sold_key_value_pairs = get_pairs( + linking_pairs, rel_from="sold_key", rel_to="sold_value" + ) # => {sold_value_group_id: [sold_key_group_id, sold_value_group_id]} + + single_entity = get_single_entity(word_groups, lrelations, labels=labels) + + date_value = [] + sold_value = [] + serial_imei = [] + table = [] + # print('key2values_relations', key2values_relations) + date_relations = get_key2values_relations(date_key_value_pairs) + for key_group_id, list_value_group_id in date_relations.items(): + for value_group_id in list_value_group_id: + date_value.append( + { + "text": word_groups[value_group_id]["text"], + "id": value_group_id, + "class": "date_value", + "bbox": word_groups[value_group_id]["bbox"], + "key_bbox": word_groups[key_group_id]["bbox"], + "raw_key_name": word_groups[key_group_id]["text"], + } + ) + + sold_relations = get_key2values_relations(sold_key_value_pairs) + for key_group_id, list_value_group_id in sold_relations.items(): + for value_group_id in list_value_group_id: + sold_value.append( + { + "text": word_groups[value_group_id]["text"], + "id": value_group_id, + "class": "sold_value", + "bbox": word_groups[value_group_id]["bbox"], + "key_bbox": word_groups[key_group_id]["bbox"], + "raw_key_name": word_groups[key_group_id]["text"], + } + ) + + + serial_relations = get_key2values_relations(serial_key_value_pairs) + for key_group_id, list_value_group_id in serial_relations.items(): + for value_group_id in list_value_group_id: + serial_imei.append( + { + "text": word_groups[value_group_id]["text"], + "id": value_group_id, + "class": "sn_value", + "bbox": word_groups[value_group_id]["bbox"], + "key_bbox": word_groups[key_group_id]["bbox"], + "raw_key_name": word_groups[key_group_id]["text"], + } + ) + + + single_entity_dict = {} + for class_name, single_items in single_entity.items(): + single_entity_dict[class_name] = [] + for single_item in single_items: + single_entity_dict[class_name].append( + { + "text": single_item["text"], + "id": single_item["group_id"], + "class": class_name, + "bbox": single_item["bbox"], + } + ) + + # list_product_name_group_ids = set( + # list(product_name_code_pairs.keys()) + # + list(product_name_quantity_pairs.keys()) + # + [x["id"] for x in single_entity_dict["product_name"]] + # ) + # for product_name_group_id in list_product_name_group_ids: + # item = {"productname": [], "modelnumber": [], "qty": []} + # item["productname"].append( + # { + # "text": word_groups[product_name_group_id]["text"], + # "id": product_name_group_id, + # "class": "product_name", + # "bbox": word_groups[product_name_group_id]["bbox"], + # } + # ) + # if product_name_group_id in product_name_code_pairs: + # product_code_group_id = product_name_code_pairs[product_name_group_id][0] + # item["modelnumber"].append( + # { + # "text": word_groups[product_code_group_id]["text"], + # "id": product_code_group_id, + # "class": "product_code", + # "bbox": word_groups[product_code_group_id]["bbox"], + # } + # ) + # if product_name_group_id in product_name_quantity_pairs: + # quantity_group_id = product_name_quantity_pairs[product_name_group_id][0] + # item["qty"].append( + # { + # "text": word_groups[quantity_group_id]["text"], + # "id": quantity_group_id, + # "class": "quantity", + # "bbox": word_groups[quantity_group_id]["bbox"], + # } + # ) + # table.append(item) + + # if len(table) > 0: + # table = sorted(table, key=lambda x: x["productname"][0]["id"]) + + if len(serial_imei) > 0: + serial_imei = sorted(serial_imei, key=lambda x: x["id"]) + + outputs = {} + outputs["seller"] = single_entity_dict["seller"] + outputs["voucher"] = single_entity_dict["voucher"] + outputs["website"] = single_entity_dict["website"] + outputs["hotline"] = single_entity_dict["hotline"] + outputs["sold_value"] = sold_value + single_entity_dict["sold_key"] + single_entity_dict["sold_value"] + outputs["date_value"] = date_value + single_entity_dict["date_value"] + single_entity_dict["date_key"] + outputs["serial_imei"] = serial_imei + single_entity_dict["sn_value"] + single_entity_dict["sn_key"] + # outputs["table"] = table + return outputs diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/list_retailers.txt b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/list_retailers.txt new file mode 100644 index 0000000..dbce48a --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/list_retailers.txt @@ -0,0 +1,167 @@ +1 FUSION TELECOM +3 Mobile +A STAR MOBILE TRADING +A2Z +ACES MOBILE +ARS DIGITAL WORLD +AV Intelligence +AZ Telecommunication +Active Electronics +Addon Systems +Alpha Telecom +Amazon.sg +Arrow Communication +Audio Ace Electronics +Audio House +BEST DIGITAL LIFESTYLE +BLAZING HANDPHONE SHOP +Best Denki +Best Denki - Great World +Best Denki - Ngee Ann +Best Denki - Online shop +Best Denki - Vivo +C. K. Tang +CALLMOBILE +Hachi +Challenger +Challenger Corporate +Circles.Life +Courts +Courts Heeren +Courts Megastore +Cyber Jip +DV Tech +Easytone +Everjoint Electrical +GIANT MOBILE +GRAPES COMMUNICATION +Gadget Affair +Gain City +Gain City Best-Electric +Gain City - Sungei Kadut +Gain City - Marina Square +Gain City - Sungei Kadut +Garphil Enterprise +Goh Ah Bee +HI MOBILE +Han's Communication +Handphone Shop +Harvey Norman +Harvey Norman - Millenia Walk +Harvey Norman - Pertama Merchandising +Harvey Norman - Millenia Walk +Harvey Norman - Northpoint +Hi-Life +I.COMM MOBILE PLUS +ING Mobile +IT Mobile  +IT TALENT TRADING +Ingram Micro +Isetan +Ivan Mobile & Jewelleries +J2 MOBILE STORE +KASIA Mobile +Kong Tai +Kris Shop +Lazada +Lazada - Samsung Brand Store +Lion City Company +Lucky Store +M1 Exclusive Partners +M1 Shop +MAGNA MOBILE +MEGA TELESHOP +MELA SHOPPE +MG MOBILE COMMUNICATION +MOBILE SQUARE +MOBILE X +MOBILEHUB SERVICE +MOBILERELATION 1 +MOBY +MOHAMED MUSTAFA & SAMSUDDIN +MY MOBILE HOUSE +Magnify +Mega Discount Store +My Mobile +My Mobile House +MyRepublic +NAIN INTERNATIONAL TRADING +NARANJAN ELECTRONICS +NTUC +Naranjan Int Mobile +New Sound Electrical Dept +One Dream Telecom +One2Free Mobile +Onephone Online +PHONEVIBES +POD CONTACT TELECOMMUNICATIONS +PROVIDER STORES +Parisilk Electronics & Computers +Planet Telecoms +Pod Contact +Poorvika (TV) +Popular Book +Provider Stores +RED WHITE MOBILE +REMO COMM +Red White Mobile +Rigel Telecom +SK Mobile +SMART PLAY TRADING +SMART TECH MOBILE +SOLULAR PLUS +SONIC CONNECTION ENTERPRISES +SPRINT - CASS (HQ) +SUMMER TELECOM +Lazada Samsung Brand Store +Shopee Samsung Brand Store +Lazada Samsung Certified Store +Shopee Samsung Certified Store +Samsung Brand Store +Samsung Customer Service Plaza Singapura +Samsung EDU Store +Samsung EPP Store +Samsung Experience Store +Samsung Experience Store - 313 Somerset +Samsung Experience Store - Bedok Mall +Samsung Experience Store - Bugis Junction +Samsung Experience Store - Causeway Point +Samsung Experience Store - ION +Samsung Experience Store - Jurong Point +Samsung Experience Store - Nex +Samsung Experience Store - Northpoint City +Samsung Experience Store - Tampines Mall +Samsung Experience Store - VivoCity +Samsung Experience Store - Westgate +Samsung Experience Store - delivery +Samsung Experience Store - pickup +Samsung Online Shop +Samsung Online Shop +Singtel Exclusive Retailers +Singtel Shop +Sprint-Cass +StarHub Exclusive Partners +StarHub Shop +T2 Electronics +TANGS +Takashimaya +Takashimaya Singapore +Telemobile +Telestation Infocomm +Trends n Trendies +U MOBILE SHOP +U-First Mobile +UNITED MOBILE SERVICES +UWIN - CHEERS COMMUNICATIONS +UWIN COMMUNICATION +Univercell +Urban Republic +VMCS Pte Ltd +VV MOBILE +Vi Mobile +WEN MOBILE TRADING +YOSHI MOBILE +ZYM Official Store +Zalora +i.Comm Mobile +iShopChangi diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/manulife.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/manulife.py new file mode 100644 index 0000000..6d172ae --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/manulife.py @@ -0,0 +1,69 @@ +key_dict = { + "Document type": ["documenttype", "loaichungtu"], + "Document name": ["documentname", "tenchungtu"], + "Patient Name": ["patientname", "tenbenhnhan"], + "Date of Birth/Year of birth": [ + "dateofbirth", + "yearofbirth", + "ngaythangnamsinh", + "namsinh", + ], + "Age": ["age", "tuoi"], + "Gender": ["gender", "gioitinh"], + "Social insurance card No.": ["socialinsurancecardno", "sothebhyt"], + "Medical service provider name": ["medicalserviceprovidername", "tencosoyte"], + "Department name": ["departmentname", "tenkhoadieutri"], + "Diagnosis description": ["diagnosisdescription", "motachandoan"], + "Diagnosis code": ["diagnosiscode", "machandoan"], + "Admission date": ["admissiondate", "ngaynhapvien"], + "Discharge date": ["dischargedate", "ngayxuatvien"], + "Treatment method": ["treatmentmethod", "phuongphapdieutri"], + "Treatment date": ["treatmentdate", "ngaydieutri", "ngaykham"], + "Follow up treatment date": ["followuptreatmentdate", "ngaytaikham"], + # "Name of precribed medicine": [], + # "Quantity of prescribed medicine": [], + # "Dosage for each medicine": [] + "Medical expense": ["Medical expense", "chiphiyte"], + "Invoice No.": ["invoiceno", "sohoadon"], +} + +title_dict = { + "Chứng từ y tế": [ + "giayravien", + "giaychungnhanphauthuat", + "cachthucphauthuat", + "phauthuat", + "tomtathosobenhan", + "donthuoc", + "toathuoc", + "donbosung", + "ketquaconghuongtu" + "ketqua", + "phieuchidinh", + "phieudangkykham", + "giayhenkhamlai", + "phieukhambenh", + "phieukhambenhvaovien", + "phieuxetnghiem", + "phieuketquaxetnghiem", + "phieuchidinhxetnghiem", + "ketquasieuam", + "phieuchidinhxetnghiem" + ], + "Chứng từ thanh toán": [ + "hoadon", + "hoadongiatrigiatang", + "hoadongiatrigiatangchuyendoituhoadondientu", + "bangkechiphibaohiem", + "bienlaithutien", + "bangkechiphidieutrinoitru" + ], +} + +def get_dict(type: str): + if type == "key": + return key_dict + elif type == "title": + return title_dict + else: + raise ValueError(f"[ERROR] Dictionary type of {type} is not supported") \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt.py new file mode 100644 index 0000000..edb33f8 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt.py @@ -0,0 +1,32 @@ +header_dict = { + 'productname': ['description', 'productdescription', 'articledescription', 'descriptionofgood', 'itemdescription', + 'brandmodel', 'itemdepartment', 'departmentbrand', 'department', 'certificateno', + 'product', 'modelname', 'paticulars', 'device', 'items', 'itemno'], + 'modelnumber': ['serialno', 'serial', 'articles', 'simimeiserial', 'article', 'articlenumber', 'articleidmaterialcode', + 'itemcode', 'code', 'mcode', 'productcode', 'model', 'product', 'imeiccid', 'transaction'], + 'qty': ['quantity', 'invoicequantity'] +} + +key_dict = { + 'purchase_date': ['date', 'purchasedate', 'datetime', 'orderdate', 'orderdatetime', 'invoicedate', 'dateredeemed', 'issuedate', 'billingdocdate'], + 'retailername': ['retailer', 'retailername', 'ownedoperatedby'], + 'serial_number': ['serialnumber', 'serialno'], + 'imei_number': ['imeiesim', 'imeislot1', 'imeislot2', 'imei', 'imei1', 'imei2'] +} + +extra_dict = { + 'serial_number': ['sn'], + 'imei_number': ['imel', 'imed'], + 'modelnumber': ['sku', 'sn', 'imei'], + 'qty': ['qty'] +} + +def get_dict(type: str): + if type == "key": + return key_dict + elif type == "header": + return header_dict + elif type == "extra": + return extra_dict + else: + raise ValueError(f'[ERROR] Dictionary type of {type} is not supported') \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt_v2.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt_v2.py new file mode 100644 index 0000000..ad3141e --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/sbt_v2.py @@ -0,0 +1,116 @@ +import os +from pathlib import Path + +cur_dir = str(Path(__file__).parents[0]) +from sdsvkvu.utils.utils import read_txt +from sdsvkvu.utils.post_processing import preprocessing + + +header_dict = { + "productname": [ + "description", + "productdescription", + "articledescription", + "descriptionofgood", + "itemdescription", + "brandmodel", + "itemdepartment", + "departmentbrand", + "department", + "certificateno", + "product", + "modelname", + "paticulars", + "device", + "items", + "itemno", + ], + "modelnumber": [ + "serialno", + "serial", + "articles", + "simimeiserial", + "article", + "articlenumber", + "articleidmaterialcode", + "itemcode", + "code", + "mcode", + "productcode", + "model", + "product", + "imeiccid", + "transaction", + ], + "qty": ["quantity", "invoicequantity"], +} + +date_dict = { + "purchase_date": [ + "date", + "purchasedate", + "datetime", + "orderdate", + "orderdatetime", + "invoicedate", + "dateredeemed", + "issuedate", + "billingdocdate", + "placedon", + "transactiondatetime", + "creationdate", + "ordertime", + "dateofissue", + ] +} + +imei_dict = { + "serial_number": ["serialnumber", "serialno"], + "imei_number": ["imeiesim", "imeislot1", "imeislot2", "imei", "imei1", "imei2"], +} + +sold_dict = { + "sold_by_party": ["soldtoparty"], + "sold_by": ["soldby"] +} + +extra_dict = { + "serial_number": ["sn"], + "imei_number": ["imel", "imed"], + "modelnumber": ["sku", "sn", "imei"], + "qty": ["qty"], +} + +seller_mapping = { + "Samsung Experience Store": ["G-FORCE NETWORK PTE LTD", "eSmart Mobile", "eSmart Mobile Pte Ltd"], + "Samsung Online Store": ["SAMSUNG ELECTRONICS SINGAPORE PTE LTD"], + "Samsung Brand Store": ["SAMSUNG OFFICIAL STORE"], + "Harvey Norman": ["PERTAMA MERCHANDISING PTE LTD"], + "Shopee": ["shopee mall"], + "Lazada": ["laz mall", "lazmall"], + "LTD": ["limited"], + "PTE": ["private"], +} + + +seller_list = read_txt(os.path.join(cur_dir, "list_retailers.txt")) +seller_dict = {seller.upper(): [preprocessing(seller)] for seller in seller_list} + + +def get_dict(type: str): + if type == "date": + return date_dict + elif type == "imei": + return imei_dict + elif type == "sold_by": + return sold_dict + elif type == "header": + return header_dict + elif type == "extra": + return extra_dict + elif type == "seller": + return seller_dict + elif type == "seller_mapping": + return seller_mapping + else: + raise ValueError(f"[ERROR] Dictionary type of {type} is not supported") diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vat.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vat.py new file mode 100644 index 0000000..8945200 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vat.py @@ -0,0 +1,69 @@ +DKVU2XML = { + "Ký hiệu mẫu hóa đơn": "form_no", + "Ký hiệu hóa đơn": "serial_no", + "Số hóa đơn": "invoice_no", + "Ngày, tháng, năm lập hóa đơn": "issue_date", + "Tên người bán": "seller_name", + "Mã số thuế người bán": "seller_tax_code", + "Thuế suất": "tax_rate", + "Thuế GTGT đủ điều kiện khấu trừ thuế": "VAT_input_amount", + "Mặt hàng": "item", + "Đơn vị tính": "unit", + "Số lượng": "quantity", + "Đơn giá": "unit_price", + "Doanh số mua chưa có thuế": "amount" +} + +key_dict = { + 'Ký hiệu mẫu hóa đơn': ['mausoformno', 'mauso'], + 'Ký hiệu hóa đơn': ['kyhieuserialno', 'kyhieuserial', 'kyhieu'], + 'Số hóa đơn': ['soinvoiceno', 'invoiceno'], + 'Ngày, tháng, năm lập hóa đơn': [], + 'Tên người bán': ['donvibanseller', 'donvibanhangsalesunit', 'donvibanhangseller', 'kyboisignedby'], + 'Mã số thuế người bán': ['masothuetaxcode', 'maxsothuetaxcodenumber', 'masothue'], + 'Thuế suất': ['thuesuatgtgttaxrate', 'thuesuatgtgt'], + 'Thuế GTGT đủ điều kiện khấu trừ thuế': ['tienthuegtgtvatamount', 'tienthuegtgt'], + # 'Ghi chú': [], + # 'Ngày': ['ngayday', 'ngay', 'day'], + # 'Tháng': ['thangmonth', 'thang', 'month'], + # 'Năm': ['namyear', 'nam', 'year'] +} + +header_dict = { + 'Mặt hàng': ['tenhanghoa,dichvu', 'danhmuc,dichvu', 'dichvusudung', 'tenquycachhanghoa','description', 'descriptionofgood', 'itemdescription'], + 'Đơn vị tính': ['donvitinh', 'dvtunit'], + 'Số lượng': ['soluong', 'quantity', 'invoicequantity', 'soluongquantity'], + 'Đơn giá': ['dongia', 'dongiaprice'], + 'Doanh số mua chưa có thuế': ['thanhtien', 'thanhtientruocthuegtgt', 'tienchuathue'], +} + +extra_dict = { + 'Số hóa đơn': ['sono', 'so'], + 'Mã số thuế người bán': ['mst'], + 'Tên người bán': ['kyboi'], + 'Ngày, tháng, năm lập hóa đơn': ['kyngay', 'kyngaydate'], + 'Mặt hàng': ['tenhanghoa','sanpham'], + 'Số lượng': ['sl', 'qty'], + 'Đơn vị tính': ['dvt'], +} + +date_dict = { + 'day': ['ngayday', 'ngaydate', 'ngay', 'day'], + 'month': ['thangmonth', 'thang', 'month'], + 'year': ['namyear', 'nam', 'year'] +} + +def get_dict(type: str): + if type == "key": + return key_dict + elif type == "header": + return header_dict + elif type == "extra": + return extra_dict + elif type == "date": + return date_dict + elif type == "kvu2xml": + return DKVU2XML + else: + raise ValueError(f'[ERROR] Dictionary type of {type} is not supported') + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vtb.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vtb.py new file mode 100644 index 0000000..a4dd15b --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/dictionary/vtb.py @@ -0,0 +1,33 @@ +key_dict = { + "number": ["kemtheoquyetdinhso", "quyetdinhso"], + "title": [], + "date": [], + "signee": ['botruong', 'thutruong', 'giamdoc', 'phogiamdoc', 'chunhiem', 'phochunhiem', + 'hieutruong', 'vientruong', 'thuky', 'chutich', 'phochutich', 'bithu', 'chutoa', + 'daidien', 'truongban', 'tongcuctruong', 'photongcuctruong', 'cuctruong', 'cucpho', + 'thuky', 'chanhthanhtra', 'thutruongdonvi', 'thutuong', + 'kiemtoanvien', 'canbokekhai'], + "sender": ['kinhgui'], + "receiver": ['noinhan', 'noigui'] + } + +extra_dict = { + "number": ['so'], + # "sender": ['dien'] +} + +date_dict = { + "day": ['ngayday', 'ngaydate', 'ngay', 'day'], + "month": ['thangmonth', 'thang', 'month'], + "year": ['namyear', 'nam', 'year'] +} + +def get_dict(type: str): + if type == "key": + return key_dict + elif type == "extra": + return extra_dict + elif type == "date": + return date_dict + else: + raise ValueError(f'[ERROR] Dictionary type of {type} is not supported') \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/post_processing.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/post_processing.py new file mode 100644 index 0000000..14b6bec --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/post_processing.py @@ -0,0 +1,362 @@ +import re +import nltk +import string +import tldextract +from dateutil import parser +from datetime import datetime +# nltk.download('words') +try: + nltk.data.find("corpora/words") +except LookupError: + nltk.download('words') +words = set(nltk.corpus.words.words()) + +from sdsvkvu.utils.word2line import Word, words_to_lines + +s1 = u'ÀÁÂÃÈÉÊÌÍÒÓÔÕÙÚÝàáâãèéêìíòóôõùúýĂăĐđĨĩŨũƠơƯưẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹ' +s0 = u'AAAAEEEIIOOOOUUYaaaaeeeiioooouuyAaDdIiUuOoUuAaAaAaAaAaAaAaAaAaAaAaAaEeEeEeEeEeEeEeEeIiIiOoOoOoOoOoOoOoOoOoOoOoOoUuUuUuUuUuUuUuYyYyYyYy' + +# def clean_text(text): +# return re.sub(r"[^A-Za-z(),!?\'\`]", " ", text) + +def get_string(lwords: list): + unique_list = [] + for item in lwords: + if item.isdigit() and len(item) == 1: + unique_list.append(item) + elif item not in unique_list: + unique_list.append(item) + return ' '.join(unique_list) + +def get_string_by_deduplicate_bbox(lwords: list, lbboxes: list): + unique_list = [] + prev_bbox = [-1, -1, -1, -1] + for word, bbox in zip(lwords, lbboxes): + if bbox != prev_bbox: + unique_list.append(word) + prev_bbox = bbox + return ' '.join(unique_list) + +def get_string_with_word2line(lwords: list, lbboxes: list): + list_words = [] + unique_list = [] + list_sorted_words = [] + + prev_bbox = [-1, -1, -1, -1] + for word, bbox in zip(lwords, lbboxes): + if bbox != prev_bbox: + prev_bbox = bbox + list_words.append(Word(image=None, text=word, conf_cls=-1, bndbox=bbox, conf_detect=-1)) + unique_list.append(word) + + llines = words_to_lines(list_words)[0] + + for line in llines: + for _word_group in line.list_word_groups: + for _word in _word_group.list_words: + list_sorted_words.append(_word.text) + + string_from_model = ' '.join(unique_list) + string_after_word2line = ' '.join(list_sorted_words) + + # if string_from_model != string_after_word2line: + # print("[Warning] Word group from model is different with word2line module") + # print("Model: ", ' '.join(unique_list)) + # print("Word2line: ", ' '.join(list_sorted_words)) + + return string_after_word2line + +def date_regexing(inp_str): + patterns = { + 'ngay': r"ngày\d+", + 'thang': r"tháng\d+", + 'nam': r"năm\d+" + } + inp_str = inp_str.replace(" ", "").lower() + outputs = {k: '' for k in patterns} + for key, pattern in patterns.items(): + matches = re.findall(pattern, inp_str) + if len(matches) > 0: + element = set([match[len(key):] for match in matches]) + outputs[key] = list(element)[0] + return outputs['ngay'], outputs['thang'], outputs['nam'] + + +def parse_date1(date_str): + # remove space + date_str = re.sub(r"[\[\]\{\}\(\)\.\,]", " ", date_str) + date_str = re.sub(r"/\s+", "/", date_str) + date_str = re.sub(r"-\s+", "-", date_str) + + is_parser_error = False + try: + date_obj = parser.parse(date_str, fuzzy=True) + year_str = str(date_obj.year) + day_str = str(date_obj.day) + # date_formated = date_obj.strftime("%d/%m/%Y") + date_formated = date_obj.strftime("%Y-%m-%d") + except Exception as err: + # date_str = sorted(date_str.split(" "), key=lambda x: len(x), reverse=True)[0] + # date_str, is_match = date_regexing(date_str) + is_match = False + if is_match: + date_formated = date_str + is_parser_error = False + return date_formated, is_parser_error + else: + print(f"Error parse date: err = {err}, date = {date_str}") + date_formated = date_str + is_parser_error = True + return date_formated, is_parser_error + + if len(normalize_number(date_str)) == 6: + year_str = year_str[-2:] + try: + year_index = date_str.index(str(year_str)) + day_index = date_str.index(str(day_str)) + if year_index > day_index: + date_obj = parser.parse(date_str, fuzzy=True, dayfirst=True) + + # date_formated = date_obj.strftime("%d/%m/%Y") + date_formated = date_obj.strftime("%Y-%m-%d") + except Exception as err: + print(f"Error check dayfirst: err = {err}, date = {date_str}") + + return date_formated, is_parser_error + + +def parse_date(date_str): + # remove space + date_str = re.sub(r"[\[\]\{\}\(\)\.\,]", " ", date_str) + date_str = re.sub(r"/\s+", "/", date_str) + date_str = re.sub(r"-\s+", "-", date_str) + date_str = re.sub(r"\-+", "-", date_str) + date_str = date_str.lower().replace("0ct", "oct") + + is_parser_error = False + try: + date_obj = parser.parse(date_str, fuzzy=True) + except Exception as err: + print(f"1.Error parse date: err = {err}, date = {date_str}") + try: + date_str = sorted(date_str.split(" "), key=lambda x: len(x), reverse=True)[0] + date_obj = parser.parse(date_str, fuzzy=True) + except Exception as err: + print(f"2.Error parse date: err = {err}, date = {date_str}") + is_parser_error = True + return [date_str], is_parser_error + + year_str = int(date_obj.year) + month_str = int(date_obj.month) + day_str = int(date_obj.day) + + current_year = int(datetime.now().year) + if year_str > current_year or year_str < 2010: # invalid year + date_obj = date_obj.replace(year=current_year) + + formated_date = date_obj.strftime("%Y-%m-%d") + revert_formated_date = date_obj.strftime("%Y-%d-%m") + + if any(txt in date_str for txt in ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']): + return [formated_date], is_parser_error + if month_str <= 12 and day_str <= 12: + return [formated_date, revert_formated_date], is_parser_error + return [formated_date], is_parser_error + + + +def normalize_imei(imei): + imei = imei.replace(" ", "") + imei = imei.split("/")[0] + return imei + +def normalize_seller(seller): + # if isinstance(seller, str): + # seller = seller + return seller + +def normalize_website(website): + if isinstance(website, str): + # website = website.lower().replace("www.", "").replace("ww.", "").replace(".com", "") + website = website.lower() + website = website.split(".com")[0] + website = tldextract.extract(website).domain + return website + +def normalize_hotline(hotline): + if isinstance(hotline, str): + hotline = hotline.lower().replace("hotline", "") + return hotline + +def normalize_voucher(voucher): + if isinstance(voucher, str): + voucher = voucher.lower().replace("voucher", "") + return voucher + + + +def normalize_number( + text_str: str, reserve_dot=False, reserve_plus=False, reserve_minus=False +): + """ + Normalize a string of numbers by removing non-numeric characters + + """ + assert isinstance(text_str, str), "input must be str" + reserver_chars = "" + if reserve_dot: + reserver_chars += ".," + if reserve_plus: + reserver_chars += "+" + if reserve_minus: + reserver_chars += "-" + regex_fomula = "[^0-9{}]".format(reserver_chars) + normalized_text_str = re.sub(r"{}".format(regex_fomula), "", text_str) + return normalized_text_str + +def remove_bullet_points_and_punctuation(text): + # Remove bullet points (e.g., • or -) + text = re.sub(r'^\s*[\•\-\*]\s*', '', text, flags=re.MULTILINE) + text = text.strip() + # # Remove end-of-sentence punctuation (e.g., ., !, ?) + # text = re.sub(r'[.!?]', '', text) + if len(text) > 0 and text[0] in (',', '.', ':', ';', '?', '!'): + text = text[1:] + if len(text) > 0 and text[-1] in (',', '.', ':', ';', '?', '!'): + text = text[:-1] + return text.strip() + +def split_key_value_by_colon(key: str, value: str) -> list: + key_string = key if key is not None else "" + value_string = value if value is not None else "" + text_string = key_string + " " + value_string + elements = text_string.split(":") + if len(elements) > 1 and not bool(re.search(r'\d', elements[0])): + return elements[0], text_string[len(elements[0])+1 :].strip() + return key, value + + +def is_string_in_range(s): + try: + num = int(s) + return 0 <= num <= 9 + except ValueError: + return False + +def remove_english_words(text): + _word = [w.lower() for w in nltk.wordpunct_tokenize(text) if w.lower() not in words] + return ' '.join(_word) + +def remove_punctuation(text): + return text.translate(str.maketrans(" ", " ", string.punctuation)) + +def remove_accents(input_str, s0, s1): + s = '' + # print input_str.encode('utf-8') + for c in input_str: + if c in s1: + s += s0[s1.index(c)] + else: + s += c + return s + +def remove_spaces(text): + return text.replace(' ', '') + +def preprocessing(text: str): + # text = remove_english_words(text) if table else text + text = remove_punctuation(text) + text = remove_accents(text, s0, s1) + text = remove_spaces(text) + return text.lower() + +def longestCommonSubsequence(text1: str, text2: str) -> int: + # https://leetcode.com/problems/longest-common-subsequence/discuss/351689/JavaPython-3-Two-DP-codes-of-O(mn)-and-O(min(m-n))-spaces-w-picture-and-analysis + dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)] + for i, c in enumerate(text1): + for j, d in enumerate(text2): + dp[i + 1][j + 1] = 1 + \ + dp[i][j] if c == d else max(dp[i][j + 1], dp[i + 1][j]) + return dp[-1][-1] + + +def longest_common_subsequence_with_idx(X, Y): + """ + This implementation uses dynamic programming to calculate the length of the LCS, and uses a path array to keep track of the characters in the LCS. + The longest_common_subsequence function takes two strings as input, and returns a tuple with three values: + the length of the LCS, + the index of the first character of the LCS in the first string, + and the index of the last character of the LCS in the first string. + """ + m, n = len(X), len(Y) + L = [[0 for i in range(n + 1)] for j in range(m + 1)] + + # Following steps build L[m+1][n+1] in bottom up fashion. Note + # that L[i][j] contains length of LCS of X[0..i-1] and Y[0..j-1] + right_idx = 0 + max_lcs = 0 + for i in range(m + 1): + for j in range(n + 1): + if i == 0 or j == 0: + L[i][j] = 0 + elif X[i - 1] == Y[j - 1]: + L[i][j] = L[i - 1][j - 1] + 1 + if L[i][j] > max_lcs: + max_lcs = L[i][j] + right_idx = i + else: + L[i][j] = max(L[i - 1][j], L[i][j - 1]) + + # Create a string variable to store the lcs string + lcs = L[i][j] + # Start from the right-most-bottom-most corner and + # one by one store characters in lcs[] + i = m + j = n + # right_idx = 0 + while i > 0 and j > 0: + # If current character in X[] and Y are same, then + # current character is part of LCS + if X[i - 1] == Y[j - 1]: + + i -= 1 + j -= 1 + # If not same, then find the larger of two and + # go in the direction of larger value + elif L[i - 1][j] > L[i][j - 1]: + # right_idx = i if not right_idx else right_idx #the first change in L should be the right index of the lcs + i -= 1 + else: + j -= 1 + return lcs, i, max(i + lcs, right_idx) + + + +def longest_common_substring(X, Y): + m = len(X) + n = len(Y) + + # Create a 2D array to store the lengths of common substrings + dp = [[0] * (n + 1) for _ in range(m + 1)] + + # Variables to store the length of the longest common substring + max_length = 0 + end_index = 0 + + # Build the dp array bottom-up + for i in range(1, m + 1): + for j in range(1, n + 1): + if X[i - 1] == Y[j - 1]: + dp[i][j] = dp[i - 1][j - 1] + 1 + + # Update the length and ending index of the common substring + if dp[i][j] > max_length: + max_length = dp[i][j] + end_index = i - 1 + else: + dp[i][j] = 0 + + # The longest common substring is X[end_index - max_length + 1:end_index + 1] + + return len(X[end_index - max_length + 1: end_index + 1]) + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/__init__.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/all.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/all.py new file mode 100644 index 0000000..c1a909a --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/all.py @@ -0,0 +1,75 @@ +from sdsvkvu.utils.post_processing import split_key_value_by_colon, remove_bullet_points_and_punctuation + + +def normalize_kvu_output(raw_outputs: dict) -> dict: + outputs = {} + for key, values in raw_outputs.items(): + if key == "table": + table = [] + for row in values: + item = {} + for k, v in row.items(): + k = remove_bullet_points_and_punctuation(k) + if v is not None and len(v) > 0: + v = remove_bullet_points_and_punctuation(v) + item[k] = v + table.append(item) + outputs[key] = table + else: + key = remove_bullet_points_and_punctuation(key) + if isinstance(values, list): + values = [remove_bullet_points_and_punctuation(v) for v in values] + elif values is not None and len(values) > 0: + values = remove_bullet_points_and_punctuation(values) + outputs[key] = values + return outputs + + +def export_kvu_for_all(raw_outputs: dict) -> dict: + outputs = {} + # Title + outputs["title"] = ( + raw_outputs["title"][0]["text"] if len(raw_outputs["title"]) > 0 else None + ) + + # Pairs of key-value + for pair in raw_outputs["single"]: + for key, values in pair.items(): + # outputs[key] = values["text"] + elements = split_key_value_by_colon(key, values["text"]) + outputs[elements[0]] = elements[1] + + # Only key fields + for key in raw_outputs["key"]: + # outputs[key["text"]] = None + elements = split_key_value_by_colon(key["text"], None) + outputs[elements[0]] = elements[1] + + # Triplet data + for triplet in raw_outputs["triplet"]: + for key, list_value in triplet.items(): + outputs[key] = [value["text"] for value in list_value] + + # Table data + table = [] + for row in raw_outputs["table"]: + item = {} + for cell in row: + item[cell["header"]] = cell["text"] + table.append(item) + outputs["table"] = table + outputs = normalize_kvu_output(outputs) + return outputs + + +def merged_kvu_for_all_for_multi_pages(loutputs: list) -> dict: + merged_outputs = {} + table = [] + for outputs in loutputs: + for key, value in outputs.items(): + if key == "table": + table.append(value) + else: + merged_outputs[key] = value + merged_outputs['table'] = table + return merged_outputs \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/manulife.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/manulife.py new file mode 100644 index 0000000..543b89b --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/manulife.py @@ -0,0 +1,133 @@ +from sdsvkvu.utils.dictionary.manulife import get_dict +from sdsvkvu.utils.post_processing import ( + split_key_value_by_colon, + remove_bullet_points_and_punctuation, + longestCommonSubsequence, + preprocessing + ) + + + +def manulife_key_matching(text: str, threshold: float, dict_type: str): + dictionary = get_dict(type=dict_type) + processed_text = preprocessing(text) + + for key, candidates in dictionary.items(): + + if any([txt == processed_text for txt in candidates]): + return True, key, 5 * (1 + len(processed_text)), processed_text + + if any([txt in processed_text for txt in candidates]): + return True, key, 5, processed_text + + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + if len(v) == 0: + continue + scores[k] = max( + [ + longestCommonSubsequence(processed_text, key) / len(key) + for key in dictionary[k] + ] + ) + key, score = max(scores.items(), key=lambda x: x[1]) + return score > threshold, key if score > threshold else text, score, processed_text + +def normalize_kvu_output_for_manulife(raw_outputs: dict) -> dict: + outputs = {} + for key, values in raw_outputs.items(): + if key == "tables" and len(values) > 0: + table_list = [] + for table in values: + headers, data = [], [] + headers = [ + remove_bullet_points_and_punctuation(header).upper() + for header in table["headers"] + ] + for row in table["data"]: + item = [] + for k, v in row.items(): + if v is not None and len(v) > 0: + item.append(remove_bullet_points_and_punctuation(v)) + else: + item.append(v) + data.append(item) + table_list.append({"headers": headers, "data": data}) + outputs[key] = table_list + else: + key = remove_bullet_points_and_punctuation(key).capitalize() + if isinstance(values, list): + values = [remove_bullet_points_and_punctuation(v) for v in values] + elif values is not None and len(values) > 0: + values = remove_bullet_points_and_punctuation(values) + outputs[key] = values + return outputs + +def export_kvu_for_manulife(raw_outputs: dict) -> dict: + outputs = {} + # Title + title_list = [] + for title in raw_outputs["title"]: + is_match, title_name, score, proceessed_text = manulife_key_matching(title["text"], threshold=0.6, dict_type="title") + title_list.append({ + 'documment_type': title_name if is_match else "", + 'content': title['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': title['id'] + }) + + if len(title_list) > 0: + selected_element = max(title_list, key=lambda x: x['lcs_score']) + outputs["title"] = f"({selected_element['documment_type']}) {selected_element['content']}" + else: + outputs["title"] = None + + # Pairs of key-value + for pair in raw_outputs["single"]: + for key, values in pair.items(): + # outputs[key] = values["text"] + elements = split_key_value_by_colon(key, values["text"]) + outputs[elements[0]] = elements[1] + + # Only key fields + for key in raw_outputs["key"]: + # outputs[key["text"]] = None + elements = split_key_value_by_colon(key["text"], None) + outputs[elements[0]] = elements[1] + + # Triplet data + for triplet in raw_outputs["triplet"]: + for key, list_value in triplet.items(): + outputs[key] = [value["text"] for value in list_value] + + # Table data + table = [] + header_list = {cell['header']: cell['header_bbox'] for row in raw_outputs['table'] for cell in row} + if header_list: + header_list = dict(sorted(header_list.items(), key=lambda x: int(x[1][0]))) + # print("Header_list:", header_list.keys()) + + for row in raw_outputs["table"]: + item = {header: None for header in list(header_list.keys())} + for cell in row: + item[cell["header"]] = cell["text"] + table.append(item) + outputs["tables"] = [{"headers": list(header_list.keys()), "data": table}] + else: + outputs["tables"] = [] + outputs = normalize_kvu_output_for_manulife(outputs) + return outputs + + +def merged_kvu_for_manulife_for_multi_pages(loutputs: list) -> dict: + merged_outputs = {} + table = [] + for outputs in loutputs: + for key, value in outputs.items(): + if key == "tables": + table.append(value) + else: + merged_outputs[key] = value + merged_outputs['tables'] = table + return merged_outputs \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt.py new file mode 100644 index 0000000..bc7a5e4 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt.py @@ -0,0 +1,186 @@ +from sdsvkvu.utils.post_processing import longestCommonSubsequence, preprocessing, is_string_in_range +from sdsvkvu.utils.dictionary.sbt import get_dict + +# For SBT project +def sbt_key_matching(text: str, threshold: float, dict_type: str): + dictionary = get_dict(type=dict_type) + processed_text = preprocessing(text) + + # Step 1: Exactly matching + extra_dict = get_dict("extra") + for key, candidates in dictionary.items(): + candidates = candidates + extra_dict[key] if key in extra_dict.keys() else candidates + + if any([processed_text == txt for txt in candidates]): + return key, 10, processed_text + + # Step 2: LCS score + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score >= threshold else text, score, processed_text + +def get_sbt_table_info(outputs): + table = [] + for single_item in outputs['table']: + item = {k: [] for k in get_dict("header").keys()} + for cell in single_item: + header_name, score, proceessed_text = sbt_key_matching(cell['header'], threshold=0.8, dict_type="header") + # print(f"{cell['header']} ==> {proceessed_text} ==> {header_name} : {score} - {cell['text']}") + is_header_valid = False + if header_name in list(item.keys()): + if header_name != "productname": + is_header_valid = True + elif cell['class'] == 'key': # Header with name of itemno as productname only when key + is_header_valid = True + _, _, proceessed_text = sbt_key_matching(cell['text'], threshold=0.8, dict_type="header") + if any([txt in proceessed_text for txt in ["originalreceipt", "homeclubvoucher", "ippuob"]]): + # print(proceessed_text) + is_header_valid = False + else: + is_header_valid = False + + if is_header_valid: + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + table.append(item) + return table + +def get_sbt_triplet_info(outputs): + triplet_pairs = [] + for single_item in outputs['triplet']: + item = {k: [] for k in get_dict("header").keys()} + is_item_valid = 0 + for key_name, list_value in single_item.items(): + for value in list_value: + if value['header'] == "non-header": + continue + header_name, score, proceessed_text = sbt_key_matching(value['header'], threshold=0.8, dict_type="header") + if header_name in list(item.keys()): + is_item_valid = 1 + item[header_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + if is_item_valid == 1: + for header_name, value in item.items(): + if len(value) == 0: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + item['productname'] = key_name + # triplet_pairs.append({key_name: new_item}) + triplet_pairs.append(item) + + # else: ## Triplet => key as productname + # item['productname'] = key_name + # for value in list_value: + # # print(value) + # if is_string_in_range(value['text']): + # item['qty'] = value['text'] + # triplet_pairs.append(item) + return triplet_pairs + + +def get_sbt_info(outputs): + single_pairs = {k: [] for k in get_dict("key").keys()} + for pair in outputs['single']: + for key_name, value in pair.items(): + key_name, score, proceessed_text = sbt_key_matching(key_name, threshold=0.8, dict_type="key") + # print(f"{key} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'] + }) + + ### Get single_pair of serial_number if it predict as a table (Product Information) + is_product_info = False + for table_row in outputs['table']: + pair = {"key": None, 'value': None} + for cell in table_row: + _, _, proceessed_text = sbt_key_matching(cell['header'], threshold=0.8, dict_type="key") + if any(txt in proceessed_text for txt in ['product', 'information', 'productinformation']): + is_product_info = True + if cell['class'] in pair: + pair[cell['class']] = cell + + if all(v is not None for k, v in pair.items()) and is_product_info == True: + key_name, score, proceessed_text = sbt_key_matching(pair['key']['text'], threshold=0.8, dict_type="key") + # print(f"{pair['key']['text']} ==> {proceessed_text} ==> {key_name} : {score} - {pair['value']['text']}") + + if key_name in list(single_pairs): + single_pairs[key_name].append({ + 'content': pair['value']['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['value']['id'] + }) + ### block_end + + ap_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if len(list_potential_value) == 0: continue + if key_name == "imei_number": + # print('list_potential_value', list_potential_value) + ap_outputs[key_name] = [] + for v in list_potential_value: + imei = v['content'].replace(' ', '') + if imei.isdigit() and len(imei) > 5: # imei is number and have more 5 digits + ap_outputs[key_name].append(imei) + else: + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + ap_outputs[key_name] = selected_value['content'] + + return ap_outputs + +def export_kvu_for_SDSAP(outputs): + # List of items in table + table = get_sbt_table_info(outputs) + triplet_pairs = get_sbt_triplet_info(outputs) + table = table + triplet_pairs + + ap_outputs = get_sbt_info(outputs) + + ap_outputs['table'] = table + return ap_outputs + +def merged_kvu_for_SDSAP_for_multi_pages(lvat_outputs: list): + merged_outputs = {k: [] for k in get_dict("key").keys()} + merged_outputs['table'] = [] + for outputs in lvat_outputs: + for key_name, value in outputs.items(): + if key_name == "table": + merged_outputs[key_name].extend(value) + else: + merged_outputs[key_name].append(value) + + for key, value in merged_outputs.items(): + if key == "table": + continue + if len(value) == 0: + merged_outputs[key] = None + else: + merged_outputs[key] = value[0] + + return merged_outputs \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt_v2.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt_v2.py new file mode 100644 index 0000000..31d3002 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/sbt_v2.py @@ -0,0 +1,320 @@ +import re +from sdsvkvu.utils.post_processing import ( + longestCommonSubsequence, + longest_common_substring, + preprocessing, + parse_date, + split_key_value_by_colon, + normalize_imei, + normalize_website, + normalize_hotline, + normalize_seller, + normalize_voucher +) +from sdsvkvu.utils.dictionary.sbt_v2 import get_dict + +def post_process_date(list_dates): + if len(list_dates) == 0: + return None + selected_value = max(list_dates, key=lambda x: x["lcs_score"]) # Get max lsc score + if not isinstance(selected_value["content"], str): + is_parser_error = True + date_formated = None + else: + date_formated, is_parser_error = parse_date(selected_value["content"]) + return date_formated + + + +def post_process_serial(list_serials): + if len(list_serials) == 0: + return None + selected_value = max( + list_serials, key=lambda x: x["lcs_score"] + ) # Get max lsc score + return selected_value["content"].strip() + + +def post_process_imei(list_imeis): + imeis = [] + for v in list_imeis: + if not isinstance(v["content"], str): + continue + imei = v["content"].replace(" ", "") + if imei.isdigit() and len(imei) > 5: # imei is number and have more 5 digits + imeis.append({ + "content": imei, + "token_id": v['token_id'] + }) + + if len(imeis) > 0: + return sorted(imeis, key=lambda x: int(x["token_id"]))[0]['content'].strip() + return None + + +def post_process_qty(inp_str: str) -> str: + pattern = r"\d" + match = re.search(pattern, inp_str) + if match: + return match.group() + return inp_str + + +def post_process_seller(list_sellers): + seller_mapping = get_dict(type="seller_mapping") + vote_list = {} + for seller in list_sellers: + seller_name = seller['content'] + if seller_name not in vote_list: + vote_list[seller_name] = 0 + + vote_list[seller_name] += seller['lcs_score'] + + if len(vote_list) > 0: + selected_value = max( + vote_list, key=lambda x: vote_list[x] + ) # Get major voting + + for norm_seller, candidates in seller_mapping.items(): + if any(preprocessing(txt) == preprocessing(selected_value) for txt in candidates): + selected_value = norm_seller + break + + selected_value = selected_value.lower() + for txt in candidates: + txt = txt.lower() + if txt in selected_value: + selected_value = selected_value.replace(txt, norm_seller) + + return selected_value.strip().title() + return None + +def post_process_subsidiary(list_subsidiaries): + if len(list_subsidiaries) > 0: + selected_value = max( + list_subsidiaries, key=lambda x: x["lcs_score"] + ) # Get max lsc score + return selected_value["content"] + return None + +def sbt_key_matching(text: str, threshold: float, dict_type: str): + dictionary = get_dict(type=dict_type) + processed_text = preprocessing(text) + + scores = {k: 0.0 for k in dictionary} + # Step 1: LCS score + for k, v in dictionary.items(): + score1 = max([ + longestCommonSubsequence(processed_text, key) / + max(len(key), len(processed_text)) + for key in dictionary[k]]) + + score2 = max([ + longest_common_substring(processed_text, key) / + max(len(key), len(processed_text)) + for key in dictionary[k]]) + + scores[k] = score1 if score1 > score2 else score2 + + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score >= threshold else text, score, processed_text + + +def get_date_value(list_dates): + date_outputs = [] + for date_obj in list_dates: + if "raw_key_name" in date_obj: + date_key, date_value = split_key_value_by_colon(date_obj['raw_key_name'], date_obj['text']) + else: + date_key, date_value = split_key_value_by_colon( + date_obj['text'] if date_obj['class'] == "date_key" else None, + date_obj['text'] if date_obj['class'] == "date_value" else None + ) + # print(f"======{date_key} : {date_value}") + + if date_key is None and date_obj['class'] == "date_value": + date_value = date_obj['text'] + proceessed_text, score = "", len(date_value) if isinstance(date_value, str) else 0 + else: + key_name, score, proceessed_text = sbt_key_matching( + date_key, threshold=0.8, dict_type="date" + ) + # print(f"{date_key} ==> {proceessed_text} ==> {key_name} : {score} - {date_value}") + date_outputs.append( + { + "content": date_value, + "processed_key_name": proceessed_text, + "lcs_score": score, + "token_id": date_obj["id"], + } + ) + return date_outputs + +def get_serial_imei(list_sn): + sn_outputs = {"serial_number": [], "imei_number": []} + for sn_obj in list_sn: + if "raw_key_name" in sn_obj: + sn_key, sn_value = split_key_value_by_colon(sn_obj['raw_key_name'], sn_obj['text']) + else: + sn_key, sn_value = split_key_value_by_colon( + sn_obj['text'] if sn_obj['class'] == "sn_key" else None, + sn_obj['text'] if sn_obj['class'] == "sn_value" else None + ) + # print(f"====== {sn_key} : {sn_value}") + + if sn_key is None and sn_obj['class'] == "sn_value": + sn_value = sn_obj['text'] + key_name, proceessed_text, score = None, "", 0.8 + else: + key_name, score, proceessed_text = sbt_key_matching( + sn_key, threshold=0.8, dict_type="imei" + ) + # print(f"{sn_key} ==> {proceessed_text} ==> {key_name} : {score} - {sn_value}") + + value = { + "content": sn_value, + "processed_key_name": proceessed_text, + "lcs_score": score, + "token_id": sn_obj["id"], + } + + if key_name is None: + if normalize_imei(sn_value).isdigit(): + sn_outputs['imei_number'].append(value) + else: + sn_outputs['serial_number'].append(value) + elif key_name in ['imei_number', 'serial_number']: + sn_outputs[key_name].append(value) + return sn_outputs + + +def get_product_info(list_items): + table = [] + for row in list_items: + item = {} + for key, value in row.items(): + item[key] = None + if len(value) > 0: + if key == "qty": + item[key] = post_process_qty(value[0]["text"]) + else: + item[key] = value[0]["text"] + table.append(item) + return table + + + +def get_seller(outputs): # Post processing to combine seller and extra information (voucher, hotline, website) + seller_outputs = [] + voucher_info = [] + + for key_field in ["seller", "website", "hotline", "voucher", "sold_by"]: + threshold = 0.7 + func_name = f"normalize_{key_field}" + for potential_seller in outputs[key_field]: + seller_name, score, processed_text = sbt_key_matching(eval(func_name)(potential_seller["text"]), threshold=threshold, dict_type="seller") + print(f"{potential_seller['text']} ==> {processed_text} ==> {seller_name} : {score}") + + if key_field in ("voucher"): + voucher_info.append(potential_seller['text']) + + seller_outputs.append( + { + "content": seller_name, + "raw_seller_name": potential_seller["text"], + "processed_seller_name": processed_text, + "lcs_score": score, + "info": key_field + } + ) + + for voucher in voucher_info: + for i in range(len(seller_outputs)): + if voucher.lower() not in seller_outputs[i]['content'].lower(): + seller_outputs[i]['content'] = f"{voucher} {seller_outputs[i]['content']}" + + return seller_outputs + + +def get_subsidiary(list_subsidiaries): + subsidiary_outputs = [] + sold_by_info = [] + for sold_obj in list_subsidiaries: + if "raw_key_name" in sold_obj: + sold_key, sold_value = split_key_value_by_colon(sold_obj['raw_key_name'], sold_obj['text']) + else: + sold_key, sold_value = split_key_value_by_colon( + sold_obj['text'] if sold_obj['class'] == "sold_key" else None, + sold_obj['text'] if sold_obj['class'] == "sold_value" else None + ) + # print(f"======{sold_key} : {sold_value}") + + + if sold_key is None and sold_obj['class'] == "sold_value": + sold_value = sold_obj['text'] + key_name, proceessed_text, score = "unknown", "", 0.8 + else: + key_name, score, proceessed_text = sbt_key_matching( + sold_key, threshold=0.8, dict_type="sold_by" + ) + # print(f"{sold_key} ==> {proceessed_text} ==> {key_name} : {score} - {sold_value}") + + if key_name == "sold_by": + sold_by_info.append(sold_obj) + else: + subsidiary_outputs.append( + { + "content": sold_value, + "processed_key_name": proceessed_text, + "lcs_score": score, + "token_id": sold_obj["id"], + } + ) + return subsidiary_outputs, sold_by_info + + +def export_kvu_for_SBT(outputs): + # sold to party + list_subsidiaries, sold_by_info = get_subsidiary(outputs['sold_value']) + # seller + outputs['sold_by'] = sold_by_info + list_sellers = get_seller(outputs) + # date + list_dates = get_date_value(outputs["date_value"]) + # serial_number or imei + list_serial_imei = get_serial_imei(outputs["serial_imei"]) + + serial_number = post_process_serial(list_serial_imei["serial_number"]) + imei_number = post_process_imei(list_serial_imei["imei_number"]) + # table + # list_items = get_product_info(outputs["table"]) + + ap_outputs = {} + ap_outputs["retailername"] = post_process_seller(list_sellers) + ap_outputs["sold_to_party"] = post_process_subsidiary(list_subsidiaries) + ap_outputs["purchase_date"] = post_process_date(list_dates) + ap_outputs["imei_number"] = imei_number if imei_number is not None else serial_number + # ap_outputs["table"] = list_items + + return ap_outputs + + +def merged_kvu_for_SBT_for_multi_pages(lvat_outputs: list): + merged_outputs = {k: [] for k in get_dict("key").keys()} + merged_outputs['table'] = [] + for outputs in lvat_outputs: + for key_name, value in outputs.items(): + if key_name == "table": + merged_outputs[key_name].extend(value) + else: + merged_outputs[key_name].append(value) + + for key, value in merged_outputs.items(): + if key == "table": + continue + if len(value) == 0: + merged_outputs[key] = None + else: + merged_outputs[key] = value[0] + + return merged_outputs \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vat.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vat.py new file mode 100644 index 0000000..424ec35 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vat.py @@ -0,0 +1,237 @@ +import re +from sdsvkvu.utils.post_processing import longestCommonSubsequence, preprocessing +from sdsvkvu.utils.dictionary.vat import get_dict + + +# For FI-VAT project +def vat_key_replacing(vat_outputs: dict) -> dict: + outputs = {} + DKVU2XML = get_dict("kvu2xml") + for key, value in vat_outputs.items(): + if key != "table": + outputs[DKVU2XML[key]] = value + else: + list_items = [] + for item in value: + list_items.append({ + DKVU2XML[item_key]: item_value for item_key, item_value in item.items() + }) + outputs['table'] = list_items + return outputs + + +def vat_key_matching(text: str, threshold: float, dict_type: str): + dictionary = get_dict(dict_type) + processed_text = preprocessing(text) + + # Step 1: Exactly matching + date_dict = get_dict("date") + for time_key, candidates in date_dict.items(): + if any([processed_text == txt for txt in candidates]): + return "Ngày, tháng, năm lập hóa đơn", 5, time_key + + extra_dict = get_dict("extra") + for key, candidates in dictionary.items(): + candidates = candidates + extra_dict[key] if key in extra_dict.keys() else candidates + + if key == 'Tên người bán' and processed_text == "kyboi": + return key, 8, processed_text + + if any([processed_text == txt for txt in candidates]): + return key, 10, processed_text + + # Step 2: LCS score + scores = {k: 0.0 for k in dictionary} + for k, v in dictionary.items(): + if k in ("Ngày, tháng, năm lập hóa đơn"): continue + scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score > threshold else text, score, processed_text + + +def normalize_number_format(s: str) -> float: + s = s.replace(' ', '').replace('O', '0').replace('o', '0') + if s.endswith(",00") or s.endswith(".00"): + s = s[:-3] + if all([delimiter in s for delimiter in [',', '.']]): + s = s.replace('.', '').split(',') + remain_value = s[1].split('0')[0] + return int(s[0]) + int(remain_value) * 1 / (10**len(remain_value)) + else: + s = s.replace(',', '').replace('.', '') + return int(s) + + +def post_process_item(item: dict) -> dict: + check_keys = ['Số lượng', 'Đơn giá', 'Doanh số mua chưa có thuế'] + mis_key = [] + for key in check_keys: + if item[key] in (None, '0'): + mis_key.append(key) + if len(mis_key) == 1: + try: + if mis_key[0] == check_keys[0] and normalize_number_format(item[check_keys[1]]) != 0: + item[mis_key[0]] = round(normalize_number_format(item[check_keys[2]]) / normalize_number_format(item[check_keys[1]])).__str__() + elif mis_key[0] == check_keys[1] and normalize_number_format(item[check_keys[0]]) != 0: + item[mis_key[0]] = (normalize_number_format(item[check_keys[2]]) / normalize_number_format(item[check_keys[0]])).__str__() + elif mis_key[0] == check_keys[2]: + item[mis_key[0]] = (normalize_number_format(item[check_keys[0]]) * normalize_number_format(item[check_keys[1]])).__str__() + except Exception as e: + print("Cannot post process this item with error:", e) + return item + + +def get_vat_table_info(outputs): + table = [] + for single_item in outputs['table']: + item = {k: [] for k in get_dict("header").keys()} + for cell in single_item: + header_name, score, proceessed_text = vat_key_matching(cell['header'], threshold=0.75, dict_type="header") + if header_name in list(item.keys()): + # item[header_name] = value['text'] + item[header_name].append({ + 'content': cell['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': cell['id'] + }) + + for header_name, value in item.items(): + if len(value) == 0: + if header_name in ("Số lượng", "Doanh số mua chưa có thuế"): + item[header_name] = '0' + else: + item[header_name] = None + continue + item[header_name] = max(value, key=lambda x: x['lcs_score'])['content'] # Get max lsc score + + item = post_process_item(item) + + if item["Mặt hàng"] == None: + continue + table.append(item) + return table + +def get_vat_info(outputs): + # VAT Information + single_pairs = {k: [] for k in get_dict("key").keys()} + for pair in outputs['single']: + for raw_key_name, value in pair.items(): + key_name, score, proceessed_text = vat_key_matching(raw_key_name, threshold=0.8, dict_type="key") + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value['id'], + }) + + for triplet in outputs['triplet']: + for key, value_list in triplet.items(): + if len(value_list) == 1: + key_name, score, proceessed_text = vat_key_matching(key, threshold=0.8, dict_type="key") + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value_list[0]['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': value_list[0]['id'] + }) + + for pair in value_list: + key_name, score, proceessed_text = vat_key_matching(pair['header'], threshold=0.8, dict_type="key") + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': pair['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'] + }) + + for table_row in outputs['table']: + for pair in table_row: + key_name, score, proceessed_text = vat_key_matching(pair['header'], threshold=0.8, dict_type="key") + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': pair['text'], + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'] + }) + + return single_pairs + + +def post_process_tax_code(tax_code_raw: str): + if len(tax_code_raw.replace(' ', '')) not in (10, 13): # to remove the first/last number dupicated + tax_code_raw = tax_code_raw.split(' ') + tax_code_raw = sorted(tax_code_raw, key=lambda x: len(x), reverse=True)[0] + return tax_code_raw.replace(' ', '') + + +def merge_vat_info(single_pairs): + vat_outputs = {k: None for k in list(single_pairs)} + for key_name, list_potential_value in single_pairs.items(): + if key_name in ("Ngày, tháng, năm lập hóa đơn"): + if len(list_potential_value) == 1: + vat_outputs[key_name] = list_potential_value[0]['content'] + else: + date_time = {'day': 'dd', 'month': 'mm', 'year': 'yyyy'} + for value in list_potential_value: + date_time[value['processed_key_name']] = re.sub("[^0-9]", "", value['content']) + vat_outputs[key_name] = f"{date_time['day']}/{date_time['month']}/{date_time['year']}" + else: + if len(list_potential_value) == 0: continue + if key_name in ("Mã số thuế người bán"): + selected_value = min(list_potential_value, key=lambda x: x['token_id']) # Get first tax code + vat_outputs[key_name] = post_process_tax_code(selected_value['content']) + + else: + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + vat_outputs[key_name] = selected_value['content'] + return vat_outputs + + +def export_kvu_for_VAT_invoice(outputs): + vat_outputs = {} + # List of items in table + table = get_vat_table_info(outputs) + # VAT Information + single_pairs = get_vat_info(outputs) + vat_outputs = merge_vat_info(single_pairs) + # Combine VAT information and table + vat_outputs['table'] = table + return vat_outputs + + +def merged_kvu_for_VAT_invoice_for_multi_pages(lvat_outputs: list): + merged_outputs = {k: [] for k in get_dict("key").keys()} + merged_outputs['table'] = [] + for outputs in lvat_outputs: + for key_name, value in outputs.items(): + if key_name == "table": + merged_outputs[key_name].extend(value) + else: + if value == None or value == "dd/mm/yyyy": + # print(key_name, value) + continue + merged_outputs[key_name].append(value) + + for key, value in merged_outputs.items(): + if key == "table": + continue + if len(value) == 0: + merged_outputs[key] = None + else: + merged_outputs[key] = value[0] + + return merged_outputs + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vtb.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vtb.py new file mode 100644 index 0000000..14694af --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/query/vtb.py @@ -0,0 +1,153 @@ +import re +from sdsvkvu.utils.post_processing import preprocessing, date_regexing, remove_bullet_points_and_punctuation +from sdsvkvu.utils.dictionary.vtb import get_dict + +# For Vietin Bank project +def vietin_key_matching(text: str, threshold: float, dict_type: str): + dictionary = get_dict(type=dict_type) + processed_text = preprocessing(text) + + # Step 1: Exactly matching + date_dict = get_dict("date") + for time_key, candidates in date_dict.items(): + if any([txt in processed_text for txt in candidates]): + return "date", 5, time_key + + extra_dict = get_dict("extra") + for key, candidates in dictionary.items(): + candidates = candidates + extra_dict[key] if key in extra_dict.keys() else candidates + + if processed_text[-4:] == "dien": # EX: Bộ trưởng Bộ GTVT điện: A, B, C + return "sender", 15, processed_text + + if any([txt in processed_text for txt in candidates]): + return key, 10, processed_text + + # Step 2: LCS score + scores = {k: 0.0 for k in dictionary} + ## Disable temporarily + # for k, v in dictionary.items(): + # if k in ("date", "title", "number", 'signee', 'sender', 'receiver'): continue + # scores[k] = max([longestCommonSubsequence(processed_text, key)/len(key) for key in dictionary[k]]) + key, score = max(scores.items(), key=lambda x: x[1]) + return key if score > threshold else text, score, processed_text + + +def get_vietin_info(outputs): + # Vietin Information + single_pairs = {k: [] for k in get_dict(type="key").keys()} + for pair in outputs['single']: + for raw_key_name, value in pair.items(): + key_name, score, proceessed_text = vietin_key_matching(raw_key_name, threshold=0.8, dict_type="key") + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value['text']}") + + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': value['text'], + 'processed_key_name': proceessed_text, + 'raw_key_name': raw_key_name, + 'lcs_score': score, + 'token_id': value['id'], + 'single_entity': False + }) + + + for single_item in outputs['key'] + outputs['value']: + key_name, score, proceessed_text = vietin_key_matching(single_item['text'], threshold=0.8, dict_type="key") + # print(f"{single_item['text']} ==> {proceessed_text} ==> {key_name} : {score} - {single_item['text']}") + + # if key_name not in ('number', 'date'): continue + if key_name in list(single_pairs.keys()): + single_pairs[key_name].append({ + 'content': single_item['text'], + 'processed_key_name': proceessed_text, + 'raw_key_name': single_item['text'], + 'lcs_score': score, + 'token_id': single_item['id'], + 'single_entity': True + }) + + + # Sender and receiver are usually in triplet + for triplet in outputs['triplet']: + for raw_key_name, value_list in triplet.items(): + key_name, score, proceessed_text = vietin_key_matching(raw_key_name, threshold=0.8, dict_type="key") + # print(f"{raw_key_name} ==> {proceessed_text} ==> {key_name} : {score} - {value_list[0]['text']}") + + if key_name in list(single_pairs.keys()): + for pair in value_list: + single_pairs[key_name].append({ + 'content': pair['text'], + 'raw_key_name': raw_key_name, + 'processed_key_name': proceessed_text, + 'lcs_score': score, + 'token_id': pair['id'], + 'single_entity': False + }) + return single_pairs + +def post_process_vietin_info(single_pairs): + vietin_outputs = {k: None for k in get_dict(type="key").keys()} + for key_name, list_potential_value in single_pairs.items(): + if key_name in ("date"): + if len(list_potential_value) == 1: + check_string = list_potential_value[0]['content'].replace(" ", "") + if check_string.replace('/', '').isdigit(): + vietin_outputs[key_name] = check_string + else: + # date_time = {'day': 'dd', 'month': 'mm', 'year': 'yyyy'} + # if len(list_potential_value) == 3: + # for value in list_potential_value: + # date_time[value['processed_key_name']] = re.sub("[^0-9]", "", value['content']) + # vietin_outputs[key_name] = f"{date_time['day']}/{date_time['month']}/{date_time['year']}" + # else: + list_potential_value = sorted(list_potential_value, key=lambda x: x['token_id'], reverse=False) + full_string = ' '.join([v['raw_key_name'] + v['content'] for v in list_potential_value]) + d, m, y = date_regexing(full_string) + vietin_outputs[key_name] = f"{d}/{m}/{y}" + # print(full_string) + # print(d, m, y) + elif key_name in ("receiver", "sender"): + list_potential_value = sorted(list_potential_value, key=lambda x: x['token_id'], reverse=False) + vietin_outputs[key_name] = [remove_bullet_points_and_punctuation(value['content']) for value in list_potential_value] + elif key_name in ("signee"): + list_potential_value = sorted(list_potential_value, key=lambda x: x['token_id'], reverse=False) + vietin_outputs[key_name] = [f"{value['content']} - {value['raw_key_name']}" for value in list_potential_value if value['single_entity'] == False] + else: + if len(list_potential_value) == 0: continue + selected_value = max(list_potential_value, key=lambda x: x['lcs_score']) # Get max lsc score + vietin_outputs[key_name] = selected_value['content'] + if key_name in ("number"): + number = re.sub("[^0-9]", "", selected_value['raw_key_name']) + start_idx = selected_value['content'].find(number) + if start_idx != -1: + vietin_outputs[key_name] = selected_value['content'].replace(" ", "")[start_idx:] + else: + vietin_outputs[key_name] = number + selected_value['content'].replace(" ", "") + + return vietin_outputs + +def export_kvu_for_vietin(outputs): + single_pairs = get_vietin_info(outputs) + vietin_outputs = post_process_vietin_info(single_pairs) + vietin_outputs['title'] = [title['text'] for title in outputs["title"]] + return vietin_outputs + +def merged_kvu_for_vietin_for_multi_pages(lvietin_outputs: list): + merged_outputs = {k: [] for k in get_dict("key").keys()} + for outputs in lvietin_outputs: + for key_name, value in outputs.items(): + if value == None or value == "dd/mm/yyyy": + # print(key_name, value) + continue + merged_outputs[key_name].append(value) + + for key, value in merged_outputs.items(): + if len(value) == 0: + merged_outputs[key] = None + elif key == "receiver": + merged_outputs[key] = value[-1] + elif key in ("number", "title", "date", "signee", "sender"): + merged_outputs[key] = value[0] + + return merged_outputs \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/utils.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/utils.py new file mode 100644 index 0000000..5019adb --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/utils.py @@ -0,0 +1,129 @@ +import os +import json +import glob +from tqdm import tqdm +from pdf2image import convert_from_path +from dicttoxml import dicttoxml + + +def create_dir(save_dir=''): + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + # else: + # print("DIR already existed.") + # print('Save dir : {}'.format(save_dir)) + +def convert_pdf2img(pdf_dir, save_dir): + pdf_files = glob.glob(f'{pdf_dir}/*.pdf') + print('No. pdf files:', len(pdf_files)) + print(pdf_files) + + for file in tqdm(pdf_files): + pdf2img(file, save_dir, n_pages=-1, return_fname=False) + # pages = convert_from_path(file, 500) + # for i, page in enumerate(pages): + # page.save(os.path.join(save_dir, os.path.basename(file).replace('.pdf', f'_{i}.jpg')), 'JPEG') + print('Done!!!') + +def pdf2img(pdf_path, save_dir, n_pages=-1, return_fname=False): + file_names = [] + pages = convert_from_path(pdf_path) + if n_pages != -1: + pages = pages[:n_pages] + for i, page in enumerate(pages): + _save_path = os.path.join(save_dir, os.path.basename(pdf_path).replace('.pdf', f'_{i}.jpg')) + page.save(_save_path, 'JPEG') + file_names.append(_save_path) + if return_fname: + return file_names + +def xyxy2xywh(bbox): + return [ + float(bbox[0]), + float(bbox[1]), + float(bbox[2]) - float(bbox[0]), + float(bbox[3]) - float(bbox[1]), + ] + +def write_to_json(file_path, content): + with open(file_path, mode='w', encoding='utf8') as f: + json.dump(content, f, ensure_ascii=False) + + +def read_json(file_path): + with open(file_path, 'r') as f: + return json.load(f) + +def read_xml(file_path): + with open(file_path, 'r') as xml_file: + return xml_file.read() + +def write_to_xml(file_path, content): + with open(file_path, mode="w", encoding='utf8') as f: + f.write(content) + +def write_to_xml_from_dict(file_path, content): + xml = dicttoxml(content) + xml = content + xml_decode = xml.decode() + + with open(file_path, mode="w") as f: + f.write(xml_decode) + +def read_txt(ocr_path): + with open(ocr_path, "r") as f: + lines = f.read().splitlines() + return lines + +def load_ocr_result(ocr_path): + with open(ocr_path, 'r') as f: + lines = f.read().splitlines() + + preds = [] + for line in lines: + preds.append(line.split('\t')) + return preds + +def post_process_basic_ocr(lwords: list) -> list: + pp_lwords = [] + for word in lwords: + pp_lwords.append(word.replace("✪", " ")) + return pp_lwords + +def read_ocr_result_from_txt(file_path: str): + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + + boxes, words = [], [] + for line in lines: + if line == "": + continue + word_info = line.split("\t") + if len(word_info) == 6: + x1, y1, x2, y2, text, _ = word_info + elif len(word_info) == 5: + x1, y1, x2, y2, text = word_info + + x1, y1, x2, y2 = int(float(x1)), int(float(y1)), int(float(x2)), int(float(y2)) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + + + + + + + + + + + + + + + diff --git a/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/word2line.py b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/word2line.py new file mode 100644 index 0000000..d8380ef --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/sdsvkvu/utils/word2line.py @@ -0,0 +1,226 @@ +class Word(): + def __init__(self, text="",image=None, conf_detect=0.0, conf_cls=0.0, bndbox = [-1,-1,-1,-1], kie_label =""): + self.type = "word" + self.text =text + self.image = image + self.conf_detect = conf_detect + self.conf_cls = conf_cls + self.boundingbox = bndbox # [left, top,right,bot] coordinate of top-left and bottom-right point + self.word_id = 0 # id of word + self.word_group_id = 0 # id of word_group which instance belongs to + self.line_id = 0 #id of line which instance belongs to + self.paragraph_id = 0 #id of line which instance belongs to + self.kie_label = kie_label + def invalid_size(self): + return (self.boundingbox[2] - self.boundingbox[0]) * (self.boundingbox[3] - self.boundingbox[1]) > 0 + def is_special_word(self): + left, top, right, bottom = self.boundingbox + width, height = right - left, bottom - top + text = self.text + + if text is None: + return True + + # if len(text) > 7: + # return True + if len(text) >= 7: + no_digits = sum(c.isdigit() for c in text) + return no_digits / len(text) >= 0.3 + + return False + +class Word_group(): + def __init__(self): + self.type = "word_group" + self.list_words = [] # dict of word instances + self.word_group_id = 0 # word group id + self.line_id = 0 #id of line which instance belongs to + self.paragraph_id = 0# id of paragraph which instance belongs to + self.text ="" + self.boundingbox = [-1,-1,-1,-1] + self.kie_label ="" + def add_word(self, word:Word): #add a word instance to the word_group + if word.text != "✪": + for w in self.list_words: + if word.word_id == w.word_id: + print("Word id collision") + return False + word.word_group_id = self.word_group_id # + word.line_id = self.line_id + word.paragraph_id = self.paragraph_id + self.list_words.append(word) + self.text += ' '+ word.text + if self.boundingbox == [-1,-1,-1,-1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3])] + return True + else: + return False + + def update_word_group_id(self, new_word_group_id): + self.word_group_id = new_word_group_id + for i in range(len(self.list_words)): + self.list_words[i].word_group_id = new_word_group_id + + def update_kie_label(self): + list_kie_label = [word.kie_label for word in self.list_words] + dict_kie = dict() + for label in list_kie_label: + if label not in dict_kie: + dict_kie[label]=1 + else: + dict_kie[label]+=1 + total = len(list(dict_kie.values())) + max_value = max(list(dict_kie.values())) + list_keys = list(dict_kie.keys()) + list_values = list(dict_kie.values()) + self.kie_label = list_keys[list_values.index(max_value)] + +class Line(): + def __init__(self): + self.type = "line" + self.list_word_groups = [] # list of Word_group instances in the line + self.line_id = 0 #id of line in the paragraph + self.paragraph_id = 0 # id of paragraph which instance belongs to + self.text = "" + self.boundingbox = [-1,-1,-1,-1] + def add_group(self, word_group:Word_group): # add a word_group instance + if word_group.list_words is not None: + for wg in self.list_word_groups: + if word_group.word_group_id == wg.word_group_id: + print("Word_group id collision") + return False + + self.list_word_groups.append(word_group) + self.text += word_group.text + word_group.paragraph_id = self.paragraph_id + word_group.line_id = self.line_id + + for i in range(len(word_group.list_words)): + word_group.list_words[i].paragraph_id = self.paragraph_id #set paragraph_id for word + word_group.list_words[i].line_id = self.line_id #set line_id for word + return True + return False + def update_line_id(self, new_line_id): + self.line_id = new_line_id + for i in range(len(self.list_word_groups)): + self.list_word_groups[i].line_id = new_line_id + for j in range(len(self.list_word_groups[i].list_words)): + self.list_word_groups[i].list_words[j].line_id = new_line_id + + + def merge_word(self, word): # word can be a Word instance or a Word_group instance + if word.text != "✪": + if self.boundingbox == [-1,-1,-1,-1]: + self.boundingbox = word.boundingbox + else: + self.boundingbox = [min(self.boundingbox[0], word.boundingbox[0]), + min(self.boundingbox[1], word.boundingbox[1]), + max(self.boundingbox[2], word.boundingbox[2]), + max(self.boundingbox[3], word.boundingbox[3])] + self.list_word_groups.append(word) + self.text += ' ' + word.text + return True + return False + + + def in_same_line(self, input_line, thresh=0.7): + # calculate iou in vertical direction + left1, top1, right1, bottom1 = self.boundingbox + left2, top2, right2, bottom2 = input_line.boundingbox + + sorted_vals = sorted([top1, bottom1, top2, bottom2]) + intersection = sorted_vals[2] - sorted_vals[1] + union = sorted_vals[3]-sorted_vals[0] + min_height = min(bottom1-top1, bottom2-top2) + if min_height==0: + return False + ratio = intersection / min_height + height1, height2 = top1-bottom1, top2-bottom2 + ratio_height = float(max(height1, height2))/float(min(height1, height2)) + # height_diff = (float(top1-bottom1))/(float(top2-bottom2)) + + + if (top1 in range(top2, bottom2) or top2 in range(top1, bottom1)) and ratio >= thresh and (ratio_height<2): + return True + return False + +def check_iomin(word:Word, word_group:Word_group): + min_height = min(word.boundingbox[3]-word.boundingbox[1],word_group.boundingbox[3]-word_group.boundingbox[1]) + intersect = min(word.boundingbox[3],word_group.boundingbox[3]) - max(word.boundingbox[1],word_group.boundingbox[1]) + if intersect/min_height > 0.7: + return True + return False + +def words_to_lines(words, check_special_lines=True): #words is list of Word instance + #sort word by top + words.sort(key = lambda x: (x.boundingbox[1], x.boundingbox[0])) + number_of_word = len(words) + #sort list words to list lines, which have not contained word_group yet + lines = [] + for i, word in enumerate(words): + if word.invalid_size()==0: + continue + new_line = True + for i in range(len(lines)): + if lines[i].in_same_line(word): #check if word is in the same line with lines[i] + lines[i].merge_word(word) + new_line = False + + if new_line ==True: + new_line = Line() + new_line.merge_word(word) + lines.append(new_line) + + # print(len(lines)) + #sort line from top to bottom according top coordinate + lines.sort(key = lambda x: x.boundingbox[1]) + + #construct word_groups in each line + line_id = 0 + word_group_id =0 + word_id = 0 + for i in range(len(lines)): + if len(lines[i].list_word_groups)==0: + continue + #left, top ,right, bottom + line_width = lines[i].boundingbox[2] - lines[i].boundingbox[0] # right - left + # print("line_width",line_width) + lines[i].list_word_groups.sort(key = lambda x: x.boundingbox[0]) #sort word in lines from left to right + + #update text for lines after sorting + lines[i].text ="" + for word in lines[i].list_word_groups: + lines[i].text += " "+word.text + + list_word_groups=[] + inital_word_group = Word_group() + inital_word_group.word_group_id= word_group_id + word_group_id +=1 + lines[i].list_word_groups[0].word_id=word_id + inital_word_group.add_word(lines[i].list_word_groups[0]) + word_id+=1 + list_word_groups.append(inital_word_group) + for word in lines[i].list_word_groups[1:]: #iterate through each word object in list_word_groups (has not been construted to word_group yet) + check_word_group= True + #set id for each word in each line + word.word_id = word_id + word_id+=1 + if (not list_word_groups[-1].text.endswith(':')) and ((word.boundingbox[0]-list_word_groups[-1].boundingbox[2])/line_width <0.05) and check_iomin(word, list_word_groups[-1]): + list_word_groups[-1].add_word(word) + check_word_group=False + if check_word_group ==True: + new_word_group = Word_group() + new_word_group.word_group_id= word_group_id + word_group_id +=1 + new_word_group.add_word(word) + list_word_groups.append(new_word_group) + lines[i].list_word_groups = list_word_groups + # set id for lines from top to bottom + lines[i].update_line_id(line_id) + line_id +=1 + return lines, number_of_word \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/setup.cfg b/cope2n-ai-fi/modules/_sdsvkvu/setup.cfg new file mode 100644 index 0000000..ce66352 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/setup.cfg @@ -0,0 +1,49 @@ +[tool:pytest] +norecursedirs = + .git + dist + build +addopts = + --strict + --doctest-modules + --durations=0 + +[coverage:report] +exclude_lines = + pragma: no-cover + pass + +[flake8] +max-line-length = 120 +exclude = .tox,*.egg,build,temp +select = E,W,F +doctests = True +verbose = 2 +# https://pep8.readthedocs.io/en/latest/intro.html#error-codes +format = pylint +# see: https://www.flake8rules.com/ +ignore = + E731 # Do not assign a lambda expression, use a def + W504 # Line break occurred after a binary operator + F401 # Module imported but unused + F841 # Local variable name is assigned to but never used + W605 # Invalid escape sequence 'x' + +# setup.cfg or tox.ini +[check-manifest] +ignore = + *.yml + .github + .github/* + +[metadata] +license_file = LICENSE +description-file = README.md +author = tuanlv +author_email = lv.tuan3@samsung.com +# long_description = file:README.md +# long_description_content_type = text/markdown +[options] +packages = find: +python_requires = >=3.9 +include_package_data = True \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/setup.py b/cope2n-ai-fi/modules/_sdsvkvu/setup.py new file mode 100644 index 0000000..8fde133 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/setup.py @@ -0,0 +1,181 @@ +import os +import os.path as osp +import shutil +import sys +import warnings +from setuptools import find_packages, setup + + +version_file = "sdsvkvu/utils/version.py" +is_windows = sys.platform == 'win32' + +def readme(): + with open('README.md', encoding='utf-8') as f: + content = f.read() + return content + +def add_mim_extention(): + """Add extra files that are required to support MIM into the package. + + These files will be added by creating a symlink to the originals if the + package is installed in `editable` mode (e.g. pip install -e .), or by + copying from the originals otherwise. + """ + + # parse installment mode + if 'develop' in sys.argv: + # installed by `pip install -e .` + mode = 'symlink' + elif 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: + # installed by `pip install .` + # or create source distribution by `python setup.py sdist` + mode = 'copy' + else: + return + + filenames = ['tools', 'configs', 'model-index.yml'] + repo_path = osp.dirname(__file__) + mim_path = osp.join(repo_path, 'mmocr', '.mim') + os.makedirs(mim_path, exist_ok=True) + + for filename in filenames: + if osp.exists(filename): + src_path = osp.join(repo_path, filename) + tar_path = osp.join(mim_path, filename) + + if osp.isfile(tar_path) or osp.islink(tar_path): + os.remove(tar_path) + elif osp.isdir(tar_path): + shutil.rmtree(tar_path) + + if mode == 'symlink': + src_relpath = osp.relpath(src_path, osp.dirname(tar_path)) + try: + os.symlink(src_relpath, tar_path) + except OSError: + # Creating a symbolic link on windows may raise an + # `OSError: [WinError 1314]` due to privilege. If + # the error happens, the src file will be copied + mode = 'copy' + warnings.warn( + f'Failed to create a symbolic link for {src_relpath}, ' + f'and it will be copied to {tar_path}') + else: + continue + + if mode == 'copy': + if osp.isfile(src_path): + shutil.copyfile(src_path, tar_path) + elif osp.isdir(src_path): + shutil.copytree(src_path, tar_path) + else: + warnings.warn(f'Cannot copy file {src_path}.') + else: + raise ValueError(f'Invalid mode {mode}') + + +def get_version(): + with open(version_file, 'r') as f: + exec(compile(f.read(), version_file, 'exec')) + import sys + + # return short version for sdist + if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: + return locals()['short_version'] + else: + return locals()['__version__'] + + +def parse_requirements(fname='requirements.txt', with_version=True): + """Parse the package dependencies listed in a requirements file but strip + specific version information. + + Args: + fname (str): Path to requirements file. + with_version (bool, default=False): If True, include version specs. + Returns: + info (list[str]): List of requirements items. + CommandLine: + python -c "import setup; print(setup.parse_requirements())" + """ + import re + import sys + from os.path import exists + require_fpath = fname + + def parse_line(line): + """Parse information from a line in a requirements text file.""" + if line.startswith('-r '): + # Allow specifying requirements in other files + target = line.split(' ')[1] + for info in parse_require_file(target): + yield info + else: + info = {'line': line} + if line.startswith('-e '): + info['package'] = line.split('#egg=')[1] + else: + # Remove versioning from the package + pat = '(' + '|'.join(['>=', '==', '>']) + ')' + parts = re.split(pat, line, maxsplit=1) + parts = [p.strip() for p in parts] + + info['package'] = parts[0] + if len(parts) > 1: + op, rest = parts[1:] + if ';' in rest: + # Handle platform specific dependencies + # http://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies + version, platform_deps = map(str.strip, + rest.split(';')) + info['platform_deps'] = platform_deps + else: + version = rest # NOQA + info['version'] = (op, version) + yield info + + def parse_require_file(fpath): + with open(fpath, 'r') as f: + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + for info in parse_line(line): + yield info + + def gen_packages_items(): + if exists(require_fpath): + for info in parse_require_file(require_fpath): + parts = [info['package']] + if with_version and 'version' in info: + parts.extend(info['version']) + if not sys.version.startswith('3.4'): + # apparently package_deps are broken in 3.4 + platform_deps = info.get('platform_deps') + if platform_deps is not None: + parts.append(';' + platform_deps) + item = ''.join(parts) + yield item + + packages = list(gen_packages_items()) + return packages + +if __name__ == '__main__': + setup( + name='sdsvkvu', + # version=get_version(), + version="0.0.1", + description='SDSV OCR Team: Key-value understanding', + long_description=readme(), + long_description_content_type='text/markdown', + packages=find_packages(), # exclude=('configs', 'tools', 'demo') + include_package_data=True, + url='https://github.com/open-mmlab/mmocr', + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.9', + ], + license='Apache License 2.0', + install_requires=parse_requirements('requirements.txt'), + zip_safe=False) \ No newline at end of file diff --git a/cope2n-ai-fi/modules/_sdsvkvu/test.py b/cope2n-ai-fi/modules/_sdsvkvu/test.py new file mode 100644 index 0000000..5be2845 --- /dev/null +++ b/cope2n-ai-fi/modules/_sdsvkvu/test.py @@ -0,0 +1,18 @@ +import os +from sdsvkvu import load_engine, process_img, process_pdf, process_dir +from sdsvkvu.modules.run_ocr import load_ocr_engine +os.environ["CUDA_VISIBLE_DEVICES"]="1" +# os.environ["NLTK_DATA"]="/mnt/ssd1T/tuanlv/02-KVU/sdsvkvu/nltk_data" + +if __name__ == "__main__": + # ocr_engine = load_ocr_engine({"device": "cuda:0"}) + kwargs = {"device": "cuda:0", "ocr_engine": None} + img_dir = "/mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu/visualize/test_manulife" + save_dir = "/mnt/hdd4T/OCR/tuanlv/02-KVU/sdsvkvu/visualize/test_manulife" + engine = load_engine(kwargs) + # option: "vat" for vat invoice outputs, "sbt": sbt invoice outputs, else for raw outputs + # outputs = process_img(img_dir, save_dir, engine, export_all=False, option="vat") + # outputs = process_pdf(img_dir, save_dir, engine, export_all=True, option="vat") + process_dir(img_dir, save_dir, engine, export_all=True, option="manulife") + # process_dir(img_dir, save_dir, engine, export_all=True, option="") + diff --git a/cope2n-ai-fi/modules/ocr_engine/.gitignore b/cope2n-ai-fi/modules/ocr_engine/.gitignore new file mode 100644 index 0000000..1250e95 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +visualize/ +results/ +*.jpeg +*.jpg +*.png diff --git a/cope2n-ai-fi/modules/ocr_engine/README.md b/cope2n-ai-fi/modules/ocr_engine/README.md new file mode 100644 index 0000000..ca1b349 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/README.md @@ -0,0 +1,47 @@ +# OCR Engine + +OCR Engine is a Python package that combines text detection and recognition models from [mmdet](https://github.com/open-mmlab/mmdetection) and [mmocr](https://github.com/open-mmlab/mmocr) to perform Optical Character Recognition (OCR) on various inputs. The package currently supports three types of input: a single image, a recursive directory, or a csv file. + +## Installation + +To install OCR Engine, clone the repository and install the required packages: + +```bash +git clone git@github.com:mrlasdt/ocr-engine.git +cd ocr-engine +pip install -r requirements.txt + +``` + + +## Usage + +To use OCR Engine, simply run the `ocr_engine.py` script with the desired input type and input path. For example, to perform OCR on a single image: + +```css +python ocr_engine.py --input_type image --input_path /path/to/image.jpg +``` + +To perform OCR on a recursive directory: + +```css +python ocr_engine.py --input_type directory --input_path /path/to/directory/ + +``` + +To perform OCR on a csv file: + + +``` +python ocr_engine.py --input_type csv --input_path /path/to/file.csv +``` + +OCR Engine will automatically detect and recognize text in the input and output the results in a CSV file named `ocr_results.csv`. + +## Contributing + +If you would like to contribute to OCR Engine, please fork the repository and submit a pull request. We welcome contributions of all types, including bug fixes, new features, and documentation improvements. + +## License + +OCR Engine is released under the [MIT License](https://opensource.org/licenses/MIT). See the LICENSE file for more information. diff --git a/cope2n-ai-fi/modules/ocr_engine/TODO.todo b/cope2n-ai-fi/modules/ocr_engine/TODO.todo new file mode 100644 index 0000000..34df095 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/TODO.todo @@ -0,0 +1,10 @@ +☐ refactor argument parser of run.py +☐ add timer level, logging level and write_mode to argumments +☐ add paddleocr deskew to the code +☐ fix the deskew code to resize the image only for detecting the angle, we want to feed the original size image to the text detection pipeline so that the bounding boxes would be mapped back to the original size +☐ ocr engine import took too long +☐ add word level to write_mode +☐ add word group and line +change max_x_dist from pixel to percentage of box width +☐ visualization: adjust fontsize dynamically + diff --git a/cope2n-ai-fi/modules/ocr_engine/__init__.py b/cope2n-ai-fi/modules/ocr_engine/__init__.py new file mode 100644 index 0000000..433d5ff --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/__init__.py @@ -0,0 +1,19 @@ +# # Define package-level variables +# __version__ = '0.0' + +import os +import sys +from pathlib import Path +cur_dir = str(Path(__file__).parents[0]) +sys.path.append(cur_dir) +sys.path.append(os.path.join(cur_dir, "externals")) + +# Import modules +from .run import load_engine +from .src.ocr import OcrEngine +# from .src.word_formation import words_to_lines +from .src.word_formation import words_to_lines_tesseract as words_to_lines +from .src.utils import ImageReader, read_ocr_result_from_txt +from .src.dto import Word, Line, Page, Document, Box +# Expose package contents +__all__ = ["OcrEngine", "Box", "Word", "Line", "Page", "Document", "words_to_lines", "ImageReader", "read_ocr_result_from_txt"] diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/.gitignore b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/.gitignore new file mode 100644 index 0000000..e56dbb2 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/.gitignore @@ -0,0 +1,9 @@ +output* +*.pyc +*.jpg +check +weights/ +workdirs/ +__pycache__* +test_hungbnt.py +libs* \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/README.md b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/README.md new file mode 100644 index 0000000..d1b9637 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/README.md @@ -0,0 +1,29 @@ +

    +

    Dewarp

    +

    + +***Feature*** +- Align document + + +## I. Setup +***Dependencies*** +- Python: 3.8 +- Torch: 1.10.2 +- CUDA: 11.6 +- transformers: 4.28.1 +### 1. Install PaddlePaddle +``` +python -m pip install paddlepaddle-gpu==2.4.2.post116 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +``` + +### 2. Install sdsv_dewarp +``` +pip install -v -e . +``` + + +## II. Test +``` +python test.py --input samples --out demo/outputs --device 'cuda' +``` diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml new file mode 100644 index 0000000..4c4cfdc --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml @@ -0,0 +1,3 @@ +model_dir: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_ppocr_mobile_v2.0_cls_infer +gpu_mem: 3000 +max_batch_size: 32 \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml new file mode 100644 index 0000000..f218ef1 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml @@ -0,0 +1,8 @@ +model_dir: /mnt/hdd4T/OCR/tuanlv/01-BasicOCR/ocr-engine-deskew/externals/sdsv_dewarp/weights/ch_PP-OCRv3_det_infer +gpu_mem: 3000 +det_limit_side_len: 1560 +det_limit_type: max +det_db_unclip_ratio: 1.85 +det_db_thresh: 0.3 +det_db_box_thresh: 0.5 +det_db_score_mode: fast \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/requirements.txt b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/requirements.txt new file mode 100644 index 0000000..768fde9 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/requirements.txt @@ -0,0 +1,7 @@ + +paddleocr>=2.0.1 +opencv-contrib-python +opencv-python +numpy +gdown==3.13.0 +imgaug==0.4.0 diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO new file mode 100644 index 0000000..634708d --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/PKG-INFO @@ -0,0 +1,45 @@ +Metadata-Version: 2.1 +Name: sdsv-dewarp +Version: 1.0.0 +Summary: Dewarp document +Home-page: +License: Apache License 2.0 +Classifier: Development Status :: 4 - Beta +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Description-Content-Type: text/markdown + +

    +

    Dewarp

    +

    + +***Feature*** +- Align document + + +## I. Setup +***Dependencies*** +- Python: 3.8 +- Torch: 1.10.2 +- CUDA: 11.6 +- transformers: 4.28.1 +### 1. Install PaddlePaddle +``` +python -m pip install paddlepaddle-gpu==2.4.2.post116 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html +``` + +### 2. Install sdsv_dewarp +``` +pip install -v -e . +``` + + +## II. Test +``` +python test.py --input samples --out demo/outputs --device 'cuda' +``` diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt new file mode 100644 index 0000000..953a123 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/SOURCES.txt @@ -0,0 +1,15 @@ +README.md +setup.py +sdsv_dewarp/__init__.py +sdsv_dewarp/api.py +sdsv_dewarp/config.py +sdsv_dewarp/factory.py +sdsv_dewarp/models.py +sdsv_dewarp/utils.py +sdsv_dewarp/version.py +sdsv_dewarp.egg-info/PKG-INFO +sdsv_dewarp.egg-info/SOURCES.txt +sdsv_dewarp.egg-info/dependency_links.txt +sdsv_dewarp.egg-info/not-zip-safe +sdsv_dewarp.egg-info/requires.txt +sdsv_dewarp.egg-info/top_level.txt \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt new file mode 100644 index 0000000..89816c8 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/requires.txt @@ -0,0 +1,6 @@ +paddleocr>=2.0.1 +opencv-contrib-python +opencv-python +numpy +gdown==3.13.0 +imgaug==0.4.0 diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt new file mode 100644 index 0000000..a5ce4e8 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp.egg-info/top_level.txt @@ -0,0 +1 @@ +sdsv_dewarp diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/__init__.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/api.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/api.py new file mode 100644 index 0000000..d71ddc5 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/api.py @@ -0,0 +1,200 @@ +import math +import numpy as np +from typing import List +import cv2 +import collections +import logging +import imgaug.augmenters as iaa +from imgaug.augmentables.polys import Polygon, PolygonsOnImage + +from sdsv_dewarp.models import PaddleTextClassifier, PaddleTextDetector +from sdsv_dewarp.config import Cfg +from .utils import * + + +MIN_LONG_EDGE = 40**2 +NUMBER_BOX_FOR_ALIGNMENT = 200 +MAX_ANGLE = 180 +MIN_ANGLE = 1 +MIN_NUM_BOX_TEXT = 3 +CROP_SIZE = 3000 + +logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) + + +class AlignImage: + """Rotate image to 0 degree + Args: + text_detector (deepmodel): Text detection model + text_cls (deepmodel): Text classification model (0 or 180) + + Return: + is_blank (bool): Blank image when haven't boxes text + image_align: Image after alignment + angle_align: Degree of angle alignment + """ + + def __init__(self, text_detector: dict, text_cls: dict, device: str = 'cpu'): + self.text_detector = None + self.text_cls = None + self.use_gpu = True if device != 'cpu' else False + + self._init_model(text_detector, text_cls) + + def _init_model(self, text_detector, text_cls): + det_config = Cfg.load_config_from_file(text_detector['config']) + det_config['model_dir'] = text_detector['weight'] + cls_config = Cfg.load_config_from_file(text_cls['config']) + cls_config['model_dir'] = text_cls['weight'] + + self.text_detector = PaddleTextDetector(config=det_config, use_gpu=self.use_gpu) + self.text_cls = PaddleTextClassifier(config=cls_config, use_gpu=self.use_gpu) + + def _cal_width(self, poly_box): + """Calculate width of a polygon [[x1, y1], [x2, y2], [x3, y3], [x4, y4]]""" + tl, tr, br, bl = poly_box + edge_s, edge_l = distance(tl, tr), distance(tr, br) + + return max(edge_s, edge_l) + + def _get_most_frequent(self, values): + values = np.array(values) + # create the histogram + hist, bins = np.histogram(values, bins=np.arange(0, 181, 10)) + + # get the index of the most frequent angle + index = np.argmax(hist) + + # get the most frequent angle + most_frequent_angle = (bins[index] + bins[index + 1]) / 2 + + return most_frequent_angle + + def _cal_angle(self, poly_box): + """Calculate the angle between two point""" + a = poly_box[0] + b = poly_box[1] + c = poly_box[2] + + # Get the longer edge + if distance(a, b) >= distance(b, c): + x, y = a, b + else: + x, y = b, c + + angle = math.degrees(math.atan2(-(y[1] - x[1]), y[0] - x[0])) + + if angle < 0: + angle = 180 - abs(angle) + + return angle + + def _reject_outliers(self, data, m=5.0): + """Remove noise angle""" + list_index = np.arange(len(data)) + d = np.abs(data - np.median(data)) + mdev = np.median(d) + s = d / (mdev if mdev else 1.0) + + return list_index[s < m], data[s < m] + + def __call__(self, image): + """image (np.ndarray): BGR image""" + + # Crop center image to increase speed of text detection + + image_resized = crop_image(image, crop_size=CROP_SIZE).copy() if max(image.shape) > CROP_SIZE else image.copy() + poly_box_texts = self.text_detector(image_resized) + + # draw_img = vis_ocr( + # image_resized, + # poly_box_texts, + # ) + # cv2.imwrite("draw_img.jpg", draw_img) + + is_blank = False + + # Check image is blank + if len(poly_box_texts) <= MIN_NUM_BOX_TEXT: + is_blank = True + return image, is_blank, 0 + + # # Crop document + # poly_np = np.array(poly_box_texts) + # min_x = poly_box_texts[:, 0].min() + # max_x = poly_box_texts[:, 2].max() + # min_y = poly_box_texts[:, 1].min() + # max_y = poly_box_texts[:, 3].max() + + # Filter small poly + poly_box_areas = [ + [self._cal_width(poly_box), id] + for id, poly_box in enumerate(poly_box_texts) + ] + + poly_box_areas = sorted(poly_box_areas)[-NUMBER_BOX_FOR_ALIGNMENT:] + poly_box_areas = [poly_box_texts[id[1]] for id in poly_box_areas] + + # Calculate angle + list_angle = [self._cal_angle(poly_box) for poly_box in poly_box_areas] + list_angle = [angle if angle >= MIN_ANGLE else 180 for angle in list_angle] + + # LOGGER.info(f"List angle before reject outlier: {list_angle}") + list_angle = np.array(list_angle) + list_index, list_angle = self._reject_outliers(list_angle) + # LOGGER.info(f"List angle after reject outlier: {list_angle}") + + if len(list_angle): + + frequent_angle = self._get_most_frequent(list_angle) + list_angle = [angle for angle in list_angle if abs(angle - frequent_angle) <= 45] + # LOGGER.info(f"List angle after reject angle: {list_angle}") + angle = np.mean(list_angle) + else: + angle = 0 + + # LOGGER.info(f"Avg angle: {angle}") + + # Reuse poly boxes detected by text detection + polys_org = PolygonsOnImage( + [Polygon(poly_box_areas[index]) for index in list_index], + shape=image_resized.shape, + ) + seq_augment = iaa.Sequential([iaa.Rotate(angle, fit_output=True, order=3)]) + + # Rotate image by degree + if angle >= MIN_ANGLE and angle <= MAX_ANGLE: + image_resized, polys_aug = seq_augment( + image=image_resized, polygons=polys_org + ) + else: + angle = 0 + image_resized, polys_aug = image_resized, polys_org + + # cv2.imwrite("image_resized.jpg", image_resized) + + # Classify image 0 or 180 degree + list_poly = [poly.coords for poly in polys_aug] + + image_crop_list = [ + dewarp_by_polygon(image_resized, poly)[0] for poly in list_poly + ] + + cls_res = self.text_cls(image_crop_list) + cls_labels = [cls_[0] for cls_ in cls_res[1]] + # LOGGER.info(f"Angle lines: {cls_labels}") + counter = collections.Counter(cls_labels) + + angle_align = angle + if counter["0"] <= counter["180"]: + aug = iaa.Rotate(angle + 180, fit_output=True, order=3) + angle_align = angle + 180 + else: + aug = iaa.Rotate(angle, fit_output=True, order=3) + + # Rotate the image by degree + image = aug.augment_image(image) + + return image, is_blank, angle_align + # return image diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/config.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/config.py new file mode 100644 index 0000000..204c2c0 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/config.py @@ -0,0 +1,41 @@ +import yaml +import pprint +import os +import json + + +def load_from_yaml(fname): + with open(fname, encoding='utf-8') as f: + base_config = yaml.safe_load(f) + return base_config + +def load_from_json(fname): + with open(fname, "r", encoding='utf-8') as f: + base_config = json.load(f) + return base_config + +class Cfg(dict): + def __init__(self, config_dict): + super(Cfg, self).__init__(**config_dict) + self.__dict__ = self + + @staticmethod + def load_config_from_file(fname, download_base=False): + if not os.path.exists(fname): + raise FileNotFoundError("Not found config at {}".format(fname)) + if fname.endswith(".yaml") or fname.endswith(".yml"): + return Cfg(load_from_yaml(fname)) + elif fname.endswith(".json"): + return Cfg(load_from_json(fname)) + else: + raise Exception(f"{fname} not supported") + + + def save(self, fname): + with open(fname, 'w', encoding='utf-8') as outfile: + yaml.dump(dict(self), outfile, default_flow_style=False, allow_unicode=True) + + # @property + def pretty_text(self): + return pprint.PrettyPrinter().pprint(self) + diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/factory.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/factory.py new file mode 100644 index 0000000..65e4bbd --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/factory.py @@ -0,0 +1,75 @@ +import os +import shutil +import hashlib +import warnings + +def sha256sum(filename): + h = hashlib.sha256() + b = bytearray(128*1024) + mv = memoryview(b) + with open(filename, 'rb', buffering=0) as f: + for n in iter(lambda : f.readinto(mv), 0): + h.update(mv[:n]) + return h.hexdigest() + + +online_model_factory = { + 'yolox-s-general-text-pretrain-20221226': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/62j266xm8r.pth', + 'hash': '89bff792685af454d0cfea5d6d673be6914d614e4c2044e786da6eddf36f8b50'}, + 'yolox-s-checkbox-20220726': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/1647d7eys7.pth', + 'hash': '7c1e188b7375dcf0b7b9d317675ebd92a86fdc29363558002249867249ee10f8'}, + 'yolox-s-idcard-5c-20221027': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/jr0egad3ix.pth', + 'hash': '73a7772594c1f6d3f6d6a98b6d6e4097af5026864e3bd50531ad9e635ae795a7'}, + 'yolox-s-handwritten-text-line-20230228': { + 'url': 'https://github.com/moewiee/satrn-model-factory/raw/main/rb07rtwmgi.pth', + 'hash': 'a31d1bf8fc880479d2e11463dad0b4081952a13e553a02919109b634a1190ef1'} +} + +__hub_available_versions__ = online_model_factory.keys() + +def _get_from_hub(file_path, version, version_url): + os.system(f'wget -O {file_path} {version_url}') + assert os.path.exists(file_path), \ + 'wget failed while trying to retrieve from hub.' + downloaded_hash = sha256sum(file_path) + if downloaded_hash != online_model_factory[version]['hash']: + os.remove(file_path) + raise ValueError('sha256 hash doesnt match for version retrieved from hub.') + +def _get(version): + use_online = version in __hub_available_versions__ + + if not use_online and not os.path.exists(version): + raise ValueError(f'Model version {version} not found online and not found local.') + + hub_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'hub') + if not os.path.exists(hub_path): + os.makedirs(hub_path) + if use_online: + version_url = online_model_factory[version]['url'] + file_path = os.path.join(hub_path, os.path.basename(version_url)) + else: + file_path = os.path.join(hub_path, os.path.basename(version)) + + if not os.path.exists(file_path): + if use_online: + _get_from_hub(file_path, version, version_url) + else: + shutil.copy2(version, file_path) + else: + if use_online: + downloaded_hash = sha256sum(file_path) + if downloaded_hash != online_model_factory[version]['hash']: + os.remove(file_path) + warnings.warn('existing hub version sha256 hash doesnt match, now re-download from hub.') + _get_from_hub(file_path, version, version_url) + else: + if sha256sum(file_path) != sha256sum(version): + os.remove(file_path) + warnings.warn('existing local version sha256 hash doesnt match, now replace with new local version.') + shutil.copy2(version, file_path) + + return \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/models.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/models.py new file mode 100644 index 0000000..64cb88c --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/models.py @@ -0,0 +1,73 @@ + +from paddleocr.tools.infer.predict_det import TextDetector +from paddleocr.tools.infer.predict_cls import TextClassifier +from paddleocr.paddleocr import parse_args +from sdsv_dewarp.config import Cfg + +class PaddleTextDetector(object): + def __init__( + self, + # config_path: str, + config: dict, + use_gpu=False + ): + # config = Cfg.load_config_from_file(config_path) + + self.args = parse_args(mMain=False) + self.args.__dict__.update( + det_model_dir=config['model_dir'], + gpu_mem=config['gpu_mem'], + use_gpu=use_gpu, + use_zero_copy_run=True, + max_batch_size=1, + det_limit_side_len=config['det_limit_side_len'], #960 + det_limit_type=config['det_limit_type'], #'max' + det_db_unclip_ratio=config['det_db_unclip_ratio'], + det_db_thresh=config['det_db_thresh'], + det_db_box_thresh=config['det_db_box_thresh'], + det_db_score_mode=config['det_db_score_mode'], + ) + self.text_detector = TextDetector(self.args) + + def __call__(self, image): + """ + + Args: + image (np.ndarray): BGR images + + Returns: + np.ndarray: numpy array of poly boxes - shape 4x2 + """ + dt_boxes, time_infer = self.text_detector(image) + return dt_boxes + + +class PaddleTextClassifier(object): + def __init__( + self, + # config_path: str, + config: str, + use_gpu=False + ): + # config = Cfg.load_config_from_file(config_path) + + self.args = parse_args(mMain=False) + self.args.__dict__.update( + cls_model_dir=config['model_dir'], + gpu_mem=config['gpu_mem'], + use_gpu=use_gpu, + use_zero_copy_run=True, + cls_batch_num=config['max_batch_size'], + ) + self.text_classifier = TextClassifier(self.args) + + def __call__(self, images): + """ + Args: + images (np.ndarray): list of BGR images + + Returns: + img_list, cls_res, elapse : cls_res format = (label, conf) + """ + out= self.text_classifier(images) + return out \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/utils.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/utils.py new file mode 100644 index 0000000..ae02cc1 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/utils.py @@ -0,0 +1,212 @@ +import math +import cv2 +import numpy as np +from PIL import Image, ImageDraw, ImageFont +import random + + +def distance(p1, p2): + """Calculate Euclid distance""" + x1, y1 = p1 + x2, y2 = p2 + dist = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) + + return dist + + +def crop_image(image, crop_size=1280): + """Crop center image""" + h, w = image.shape[:2] + x_center, y_center = w // 2, h // 2 + half_size = crop_size // 2 + + xmin, ymin = x_center - half_size, y_center - half_size + xmax, ymax = x_center + half_size, y_center + half_size + + xmin = max(xmin, 0) + ymin = max(ymin, 0) + xmax = min(xmax, w) + ymax = min(ymax, h) + + return image[ymin:ymax, xmin:xmax] + + +def _closest_point(corners, A): + """Find closest A in corrers point""" + distances = [distance(A, p) for p in corners] + return corners[np.argmin(distances)] + + +def _re_order_corners(image_size, corners) -> list: + """Order by corners by clockwise angle""" + h, w = image_size + tl = _closest_point(corners, (0, 0)) + tr = _closest_point(corners, (w, 0)) + br = _closest_point(corners, (w, h)) + bl = _closest_point(corners, (0, h)) + + return [tl, tr, br, bl] + + +def _validate_corner(corners, ratio_thres=0.5, epsilon=1e-3) -> bool: + """Check corners is valid + Invalid: 3 points, duplicate points, .... + """ + c_tl, c_tr, c_br, c_bl = corners + e_top = distance(c_tl, c_tr) + e_right = distance(c_tr, c_br) + e_bottom = distance(c_br, c_bl) + e_left = distance(c_bl, c_tl) + + min_tb = min(e_top, e_bottom) + max_tb = max(e_top, e_bottom) + min_lr = min(e_left, e_right) + max_lr = max(e_left, e_right) + + # Nếu các điểm trùng nhau thì độ dài các cạnh sẽ bằng 0 + if min(max_tb, max_lr) < epsilon: + return False + + ratio = min(min_tb / max_tb, min_lr / max_lr) + if ratio < ratio_thres: + return False + + return True + + +def dewarp_by_polygon( + image, corners, need_validate=False, need_reorder=True, trace_trans=None +): + """Crop and dewarp from 4 corners of images + + Args: + image (np.array) + corners (list): Ex : [(3347, 512), (3379, 2427), (638, 2524), (647, 495)] + need_validate (bool, optional): validate 4 points. Defaults to False. + need_reorder (bool, optional): validate 4 points. Defaults to True. + + Returns: + dewarped: image after dewarp + corners: location of 4 corners after reorder + """ + h, w = image.shape[:2] + + if need_reorder: + corners = _re_order_corners((h, w), corners) + + dewarped = image + + if need_validate: + validate = _validate_corner(corners) + else: + validate = True + + if validate: + # perform dewarp + target_w = int( + max(distance(corners[0], corners[1]), distance(corners[2], corners[3])) + ) + target_h = int( + max(distance(corners[0], corners[3]), distance(corners[1], corners[2])) + ) + target_corners = [ + [0, 0], + [target_w, 0], + [target_w, target_h], + [0, target_h], + ] + + pts1 = np.float32(corners) + pts2 = np.float32(target_corners) + transform_matrix = cv2.getPerspectiveTransform(pts1, pts2) + + dewarped = cv2.warpPerspective(image, transform_matrix, (target_w, target_h)) + if trace_trans is not None: + trace_trans["dewarp_method"]["polygon"][ + "transform_matrix" + ] = transform_matrix + + return (dewarped, corners, trace_trans) + + +def vis_ocr(image, boxes, txts=[], scores=None, drop_score=0.5): + """ + Args: + image (np.ndarray / PIL): BGR image or PIL image + boxes (list / np.ndarray): list of polygon boxes + txts (list): list of text labels + scores (list, optional): probality. Defaults to None. + drop_score (float, optional): . Defaults to 0.5. + font_path (str, optional): Path of font. Defaults to "test/fonts/latin.ttf". + Returns: + np.ndarray: BGR image + """ + + if len(txts) == 0: + txts = [""] * len(boxes) + + if isinstance(image, np.ndarray): + image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + if isinstance(boxes, list): + boxes = np.array(boxes) + + h, w = image.height, image.width + img_left = image.copy() + img_right = Image.new("RGB", (w, h), (255, 255, 255)) + draw_left = ImageDraw.Draw(img_left) + draw_right = ImageDraw.Draw(img_right) + for idx, (box, txt) in enumerate(zip(boxes, txts)): + if scores is not None and scores[idx] < drop_score: + continue + color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + draw_left.polygon( + [ + box[0][0], + box[0][1], + box[1][0], + box[1][1], + box[2][0], + box[2][1], + box[3][0], + box[3][1], + ], + fill=color, + ) + draw_right.polygon( + [ + box[0][0], + box[0][1], + box[1][0], + box[1][1], + box[2][0], + box[2][1], + box[3][0], + box[3][1], + ], + outline=color, + ) + box_height = math.sqrt( + (box[0][0] - box[3][0]) ** 2 + (box[0][1] - box[3][1]) ** 2 + ) + box_width = math.sqrt( + (box[0][0] - box[1][0]) ** 2 + (box[0][1] - box[1][1]) ** 2 + ) + if box_height > 2 * box_width: + font_size = max(int(box_width * 0.9), 10) + font = ImageFont.load_default() + cur_y = box[0][1] + for c in txt: + char_size = font.getsize(c) + draw_right.text((box[0][0] + 3, cur_y), c, fill=(0, 0, 0), font=font) + cur_y += char_size[1] + else: + font_size = max(int(box_height * 0.8), 10) + font = ImageFont.load_default() + draw_right.text([box[0][0], box[0][1]], txt, fill=(0, 0, 0), font=font) + img_left = Image.blend(image, img_left, 0.5) + + img_show = Image.new("RGB", (w * 2, h), (255, 255, 255)) + img_show.paste(img_left, (0, 0, w, h)) + img_show.paste(img_right, (w, 0, w * 2, h)) + img_show = cv2.cvtColor(np.array(img_show), cv2.COLOR_RGB2BGR) + return img_show diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/version.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/version.py new file mode 100644 index 0000000..a1570ac --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/sdsv_dewarp/version.py @@ -0,0 +1 @@ +__version__="1.0.0" \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/setup.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/setup.py new file mode 100644 index 0000000..7887e8f --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/setup.py @@ -0,0 +1,187 @@ +import os +import os.path as osp +import shutil +import sys +import warnings +from setuptools import find_packages, setup + + +def readme(): + with open('README.md', encoding='utf-8') as f: + content = f.read() + return content + + +version_file = 'sdsv_dewarp/version.py' +is_windows = sys.platform == 'win32' + + +def add_mim_extention(): + """Add extra files that are required to support MIM into the package. + + These files will be added by creating a symlink to the originals if the + package is installed in `editable` mode (e.g. pip install -e .), or by + copying from the originals otherwise. + """ + + # parse installment mode + if 'develop' in sys.argv: + # installed by `pip install -e .` + mode = 'symlink' + elif 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: + # installed by `pip install .` + # or create source distribution by `python setup.py sdist` + mode = 'copy' + else: + return + + filenames = ['tools', 'configs', 'model-index.yml'] + repo_path = osp.dirname(__file__) + mim_path = osp.join(repo_path, 'mmocr', '.mim') + os.makedirs(mim_path, exist_ok=True) + + for filename in filenames: + if osp.exists(filename): + src_path = osp.join(repo_path, filename) + tar_path = osp.join(mim_path, filename) + + if osp.isfile(tar_path) or osp.islink(tar_path): + os.remove(tar_path) + elif osp.isdir(tar_path): + shutil.rmtree(tar_path) + + if mode == 'symlink': + src_relpath = osp.relpath(src_path, osp.dirname(tar_path)) + try: + os.symlink(src_relpath, tar_path) + except OSError: + # Creating a symbolic link on windows may raise an + # `OSError: [WinError 1314]` due to privilege. If + # the error happens, the src file will be copied + mode = 'copy' + warnings.warn( + f'Failed to create a symbolic link for {src_relpath}, ' + f'and it will be copied to {tar_path}') + else: + continue + + if mode == 'copy': + if osp.isfile(src_path): + shutil.copyfile(src_path, tar_path) + elif osp.isdir(src_path): + shutil.copytree(src_path, tar_path) + else: + warnings.warn(f'Cannot copy file {src_path}.') + else: + raise ValueError(f'Invalid mode {mode}') + + +def get_version(): + with open(version_file, 'r') as f: + exec(compile(f.read(), version_file, 'exec')) + import sys + + # return short version for sdist + if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv: + return locals()['short_version'] + else: + return locals()['__version__'] + + +def parse_requirements(fname='requirements.txt', with_version=True): + """Parse the package dependencies listed in a requirements file but strip + specific version information. + + Args: + fname (str): Path to requirements file. + with_version (bool, default=False): If True, include version specs. + Returns: + info (list[str]): List of requirements items. + CommandLine: + python -c "import setup; print(setup.parse_requirements())" + """ + import re + import sys + from os.path import exists + require_fpath = fname + + def parse_line(line): + """Parse information from a line in a requirements text file.""" + if line.startswith('-r '): + # Allow specifying requirements in other files + target = line.split(' ')[1] + for info in parse_require_file(target): + yield info + else: + info = {'line': line} + if line.startswith('-e '): + info['package'] = line.split('#egg=')[1] + else: + # Remove versioning from the package + pat = '(' + '|'.join(['>=', '==', '>']) + ')' + parts = re.split(pat, line, maxsplit=1) + parts = [p.strip() for p in parts] + + info['package'] = parts[0] + if len(parts) > 1: + op, rest = parts[1:] + if ';' in rest: + # Handle platform specific dependencies + # http://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies + version, platform_deps = map(str.strip, + rest.split(';')) + info['platform_deps'] = platform_deps + else: + version = rest # NOQA + info['version'] = (op, version) + yield info + + def parse_require_file(fpath): + with open(fpath, 'r') as f: + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + for info in parse_line(line): + yield info + + def gen_packages_items(): + if exists(require_fpath): + for info in parse_require_file(require_fpath): + parts = [info['package']] + if with_version and 'version' in info: + parts.extend(info['version']) + if not sys.version.startswith('3.4'): + # apparently package_deps are broken in 3.4 + platform_deps = info.get('platform_deps') + if platform_deps is not None: + parts.append(';' + platform_deps) + item = ''.join(parts) + yield item + + packages = list(gen_packages_items()) + return packages + + +if __name__ == '__main__': + setup( + name='sdsv_dewarp', + version=get_version(), + description='Dewarp document', + long_description=readme(), + long_description_content_type='text/markdown', + packages=find_packages(exclude=('configs', 'tools', 'demo')), + include_package_data=True, + url='', + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + ], + license='Apache License 2.0', + install_requires=parse_requirements('requirements.txt'), + zip_safe=False) diff --git a/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/test.py b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/test.py new file mode 100644 index 0000000..4bfdbc7 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/test.py @@ -0,0 +1,47 @@ +from sdsv_dewarp.api import AlignImage +import cv2 +import glob +import os +import tqdm +import time +import argparse + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--input") + parser.add_argument("--out") + parser.add_argument("--device", type=str, default="cuda:1") + + args = parser.parse_args() + model = AlignImage(device=args.device) + + + img_dir = args.input + out_dir = args.out + if not os.path.exists(out_dir): + os.makedirs(out_dir) + + img_paths = glob.glob(img_dir + "/*") + + times = [] + for img_path in tqdm.tqdm(img_paths): + t1 = time.time() + img = cv2.imread(img_path) + if img is None: + print(img_path) + continue + + aligned_img, is_blank, angle_align = model(img) + + times.append(time.time() - t1) + + if not is_blank: + cv2.imwrite(os.path.join(out_dir, os.path.basename(img_path)), aligned_img) + else: + cv2.imwrite(os.path.join(out_dir, os.path.basename(img_path)), img) + + + times = times[1:] + print("Avg time: ", sum(times) / len(times)) \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/requirements.txt b/cope2n-ai-fi/modules/ocr_engine/requirements.txt new file mode 100644 index 0000000..7bfd9c5 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/requirements.txt @@ -0,0 +1,82 @@ +addict==2.4.0 +asttokens==2.2.1 +autopep8==1.6.0 +backcall==0.2.0 +backports.functools-lru-cache==1.6.4 +brotlipy==0.7.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==2.0.4 +click==8.1.3 +colorama==0.4.6 +cryptography==39.0.1 +debugpy==1.5.1 +decorator==5.1.1 +docopt==0.6.2 +entrypoints==0.4 +executing==1.2.0 +flit_core==3.6.0 +idna==3.4 +importlib-metadata==6.0.0 +ipykernel==6.15.0 +ipython==8.11.0 +jedi==0.18.2 +jupyter-client==7.0.6 +jupyter_core==4.12.0 +Markdown==3.4.1 +markdown-it-py==2.2.0 +matplotlib-inline==0.1.6 +mdurl==0.1.2 +mkl-fft==1.3.6 +mkl-random==1.2.2 +mkl-service==2.4.0 +mmcv-full==1.7.1 +model-index==0.1.11 +nest-asyncio==1.5.6 +numpy==1.24.3 +opencv-python==4.7.0.72 +openmim==0.3.6 +ordered-set==4.1.0 +packaging==23.0 +pandas==1.5.3 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.4.0 +pip==22.3.1 +pipdeptree==2.5.2 +prompt-toolkit==3.0.38 +psutil==5.9.0 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pycodestyle==2.10.0 +pycparser==2.21 +Pygments==2.14.0 +pyOpenSSL==23.0.0 +PySocks==1.7.1 +python-dateutil==2.8.2 +pytz==2022.7.1 +PyYAML==6.0 +pyzmq==19.0.2 +requests==2.28.1 +rich==13.3.1 +sdsvtd==0.1.1 +sdsvtr==0.0.5 +setuptools==65.6.3 +Shapely==1.8.4 +six==1.16.0 +stack-data==0.6.2 +tabulate==0.9.0 +toml==0.10.2 +torch==1.13.1 +torchvision==0.14.1 +tornado==6.1 +tqdm==4.65.0 +traitlets==5.9.0 +typing_extensions==4.4.0 +urllib3==1.26.14 +wcwidth==0.2.6 +wheel==0.38.4 +yapf==0.32.0 +yarg==0.1.9 +zipp==3.15.0 diff --git a/cope2n-ai-fi/modules/ocr_engine/run.py b/cope2n-ai-fi/modules/ocr_engine/run.py new file mode 100644 index 0000000..3b19879 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/run.py @@ -0,0 +1,200 @@ +""" +see scripts/run_ocr.sh to run +""" +# from pathlib import Path # add parent path to run debugger +# import sys +# FILE = Path(__file__).absolute() +# sys.path.append(FILE.parents[2].as_posix()) + + +from src.utils import construct_file_path, ImageReader +from src.dto import Line +from src.ocr import OcrEngine +import argparse +import tqdm +import pandas as pd +from pathlib import Path +import json +import os +import numpy as np +from typing import Union, Tuple, List, Optional +from collections import defaultdict + +current_dir = os.getcwd() + + +def get_args(): + parser = argparse.ArgumentParser() + # parser image + parser.add_argument( + "--image", + type=str, + required=True, + help="path to input image/directory/csv file", + ) + parser.add_argument( + "--save_dir", type=str, required=True, help="path to save directory" + ) + parser.add_argument( + "--include", type=str, nargs="+", default=[], help="files/folders to include" + ) + parser.add_argument( + "--exclude", type=str, nargs="+", default=[], help="files/folders to exclude" + ) + parser.add_argument( + "--base_dir", + type=str, + required=False, + default=current_dir, + help="used when --image and --save_dir are relative paths to a base directory, default to current directory", + ) + parser.add_argument( + "--export_csv", + type=str, + required=False, + default="", + help="used when --image is a directory. If set, a csv file contains image_path, ocr_path and label will be exported to save_dir.", + ) + parser.add_argument( + "--export_img", + type=bool, + required=False, + default=False, + help="whether to save the visualize img", + ) + parser.add_argument("--ocr_kwargs", type=str, required=False, default="") + opt = parser.parse_args() + return opt + + +def load_engine(opt) -> OcrEngine: + print("[INFO] Loading engine...") + kw = json.loads(opt.ocr_kwargs) if opt.ocr_kwargs else {} + engine = OcrEngine(**kw) + print("[INFO] Engine loaded") + return engine + + +def convert_relative_path_to_positive_path(tgt_dir: Path, base_dir: Path) -> Path: + return tgt_dir if tgt_dir.is_absolute() else base_dir.joinpath(tgt_dir) + + +def get_paths_from_opt(opt) -> Tuple[Path, Path]: + # BC\ kiem\ tra\ y\ te -> BC kiem tra y te + img_path = opt.image.replace("\\ ", " ").strip() + save_dir = opt.save_dir.replace("\\ ", " ").strip() + base_dir = opt.base_dir.replace("\\ ", " ").strip() + input_image = convert_relative_path_to_positive_path(Path(img_path), Path(base_dir)) + save_dir = convert_relative_path_to_positive_path(Path(save_dir), Path(base_dir)) + if not save_dir.exists(): + save_dir.mkdir() + print("[INFO]: Creating folder ", save_dir) + return input_image, save_dir + + +def process_img( + img: Union[str, np.ndarray], + save_dir_or_path: str, + engine: OcrEngine, + export_img: bool, + save_path_deskew: Optional[str] = None, +) -> None: + save_dir_or_path = Path(save_dir_or_path) + if isinstance(img, np.ndarray): + if save_dir_or_path.is_dir(): + raise ValueError("numpy array input require a save path, not a save dir") + page = engine(img) + save_path = ( + str(save_dir_or_path.joinpath(Path(img).stem + ".txt")) + if save_dir_or_path.is_dir() + else str(save_dir_or_path) + ) + page.write_to_file("word", save_path) + if export_img: + page.save_img( + save_path.replace(".txt", ".jpg"), + is_vnese=True, + save_path_deskew=save_path_deskew, + ) + + +def process_dir( + dir_path: str, + save_dir: str, + engine: OcrEngine, + export_img: bool, + lexcludes: List[str] = [], + lincludes: List[str] = [], + ddata=defaultdict(list), +) -> None: + pdir_path = Path(dir_path) + print(pdir_path) + # save_dir_sub = Path(construct_file_path(save_dir, dir_path, ext="")) + psave_dir = Path(save_dir) + psave_dir.mkdir(exist_ok=True) + for img_path in (pbar := tqdm.tqdm(pdir_path.iterdir())): + pbar.set_description(f"Processing {pdir_path}") + if (lincludes and img_path.name not in lincludes) or ( + img_path.name in lexcludes + ): + continue # only process desired files/foders + if img_path.is_dir(): + psave_dir_sub = psave_dir.joinpath(img_path.stem) + process_dir(img_path, str(psave_dir_sub), engine, ddata) + elif img_path.suffix.lower() in ImageReader.supported_ext: + simg_path = str(img_path) + # try: + img = ( + ImageReader.read(simg_path) + if img_path.suffix != ".pdf" + else ImageReader.read(simg_path)[0] + ) + save_path = str(Path(psave_dir).joinpath(img_path.stem + ".txt")) + save_path_deskew = str( + Path(psave_dir).joinpath(img_path.stem + "_deskewed.jpg") + ) + process_img(img, save_path, engine, export_img, save_path_deskew) + # except Exception as e: + # print('[ERROR]: ', e, ' at ', simg_path) + # continue + ddata["img_path"].append(simg_path) + ddata["ocr_path"].append(save_path) + if Path(save_path_deskew).exists(): + ddata["save_path_deskew"].append(save_path) + ddata["label"].append(pdir_path.stem) + # ddata.update({"img_path": img_path, "save_path": save_path, "label": dir_path.stem}) + return ddata + + +def process_csv(csv_path: str, engine: OcrEngine) -> None: + df = pd.read_csv(csv_path) + if not "image_path" in df.columns or not "ocr_path" in df.columns: + raise AssertionError("Cannot fing image_path in df headers") + for row in df.iterrows(): + process_img(row.image_path, row.ocr_path, engine) + + +if __name__ == "__main__": + opt = get_args() + engine = load_engine(opt) + print("[INFO]: OCR engine settings:", engine.settings) + img, save_dir = get_paths_from_opt(opt) + + lskip_dir = [] + if img.is_dir(): + ddata = process_dir( + img, save_dir, engine, opt.export_img, opt.exclude, opt.include + ) + if opt.export_csv: + pd.DataFrame.from_dict(ddata).to_csv( + Path(save_dir).joinpath(opt.export_csv) + ) + elif img.suffix in ImageReader.supported_ext: + process_img(str(img), save_dir, engine, opt.export_img) + elif img.suffix == ".csv": + print( + "[WARNING]: Running with csv file will ignore the save_dir argument. Instead, the ocr_path in the csv would be used" + ) + process_csv(img, engine) + else: + raise NotImplementedError("[ERROR]: Unsupported file {}".format(img)) diff --git a/cope2n-ai-fi/modules/ocr_engine/scripts/run_deskew.sh b/cope2n-ai-fi/modules/ocr_engine/scripts/run_deskew.sh new file mode 100644 index 0000000..34ecd8c --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/scripts/run_deskew.sh @@ -0,0 +1,9 @@ +export CUDA_VISIBLE_DEVICES=1 +# export PATH=/usr/local/cuda-11.6/bin${PATH:+:${PATH}} +# export LD_LIBRARY_PATH=/usr/local/cuda-11.6/lib64\ {LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}} +# export CUDA_HOME=/usr/local/cuda-11.6 +# export PATH=/usr/local/cuda-11.6/bin:$PATH +# export CPATH=/usr/local/cuda-11.6/include:$CPATH +# export LIBRARY_PATH=/usr/local/cuda-11.6/lib64:$LIBRARY_PATH +# export LD_LIBRARY_PATH=/usr/local/cuda-11.6/lib64:/usr/local/cuda-11.6/extras/CUPTI/lib64:$LD_LIBRARY_PATH +python test/test_deskew_dir.py \ No newline at end of file diff --git a/cope2n-ai-fi/modules/ocr_engine/scripts/run_ocr.sh b/cope2n-ai-fi/modules/ocr_engine/scripts/run_ocr.sh new file mode 100644 index 0000000..8ade432 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/scripts/run_ocr.sh @@ -0,0 +1,49 @@ + + +#bash scripts/run_ocr.sh -i /mnt/hdd2T/AICR/Projects/2023/FWD/Forms/PDFs/ -o /mnt/ssd1T/hungbnt/DocumentClassification/results/ocr -e out.csv -k "{\"device\":\"cuda:1\"}" -p True -n Passport 'So\ HK' +#bash scripts/run_ocr.sh -i '/mnt/hdd2T/AICR/Projects/2023/FWD/Forms/PDFs/So\ HK' -o /mnt/ssd1T/hungbnt/DocumentClassification/results/ocr -e out.csv -k "{\"device\":\"cuda:1\"}" -p True +#-n and -x do not accept multiple argument currently + + +# bash scripts/run_ocr.sh -i /mnt/hdd4T/OCR/hoangdc/End_to_end/ICDAR2013/data/images_receipt_5images/ -o visualize/ -e out.csv -k "{\"device\":\"cuda:1\"}" -p True + +export PYTHONWARNINGS="ignore" + +while getopts i:o:b:e:p:k:n:x: flag +do + case "${flag}" in + i) img=${OPTARG};; + o) out_dir=${OPTARG};; + b) base_dir=${OPTARG};; + e) export_csv=${OPTARG};; + p) export_img=${OPTARG};; + k) ocr_kwargs=${OPTARG};; + n) include=("${OPTARG[@]}");; + x) exclude=("${OPTARG[@]}");; + esac +done + +cmd="python run.py \ + --image $img \ + --save_dir $out_dir \ + --export_csv $export_csv \ + --export_img $export_img \ + --ocr_kwargs $ocr_kwargs" + +if [ ${#include[@]} -gt 0 ]; then + cmd+=" --include" + for item in "${include[@]}"; do + cmd+=" $item" + done +fi + +if [ ${#exclude[@]} -gt 0 ]; then + cmd+=" --exclude" + for item in "${exclude[@]}"; do + cmd+=" $item" + done +fi + + +echo $cmd +exec $cmd diff --git a/cope2n-ai-fi/modules/ocr_engine/settings.yml b/cope2n-ai-fi/modules/ocr_engine/settings.yml new file mode 100644 index 0000000..055e16f --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/settings.yml @@ -0,0 +1,36 @@ +device: &device cuda:0 +max_img_size: [1920,1920] #text det default size: 1280x1280 #[] = originla size, TODO: fix the deskew code to resize the image only for detecting the angle, we want to feed the original size image to the text detection pipeline so that the bounding boxes would be mapped back to the original size +extend_bbox: [0, 0.0, 0.0, 0.0] # left, top, right, bottom +batch_size: 1 #1 means batch_mode = False +detector: + # version: /mnt/hdd2T/datnt/datnt_from_ssd1T/mmdetection/wild_receipt_finetune_weights_c_lite.pth + version: /workspace/cope2n-ai-fi/weights/models/sdsap_sbt/ocr_engine/sdsvtd/epoch_100_params.pth + auto_rotate: True + rotator_version: /workspace/cope2n-ai-fi/weights/models/sdsap_sbt/ocr_engine/sdsvtd/best_bbox_mAP_epoch_30_lite.pth + device: *device + +recognizer: + # version: satrn-lite-general-pretrain-20230106 + version: /workspace/cope2n-ai-fi/weights/models/sdsvtr/hub/jxqhbem4to.pth + max_seq_len_overwrite: 24 #default = 12 + return_confident: True + device: *device +#extend the bbox to avoid losing accent mark in vietnames, if using ocr for only english, disable it + +deskew: + enable: True + text_detector: + config: /workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/det.yaml + weight: /workspace/cope2n-ai-fi/weights/models/sdsap_sbt/ocr_engine/sdsv_dewarp/ch_PP-OCRv3_det_infer + text_cls: + config: /workspace/cope2n-ai-fi/modules/ocr_engine/externals/sdsv_dewarp/config/cls.yaml + weight: /workspace/cope2n-ai-fi/weights/models/sdsap_sbt/ocr_engine/sdsv_dewarp/ch_ppocr_mobile_v2.0_cls_infer + device: *device + + +words_to_lines: + gradient: 0.6 + max_x_dist: 20 + max_running_y_shift_degree: 10 #degrees + y_overlap_threshold: 0.5 + word_formation_mode: line diff --git a/cope2n-ai-fi/modules/ocr_engine/src/dto.py b/cope2n-ai-fi/modules/ocr_engine/src/dto.py new file mode 100644 index 0000000..8dae901 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/src/dto.py @@ -0,0 +1,534 @@ +import numpy as np +from typing import Optional, List, Union +import cv2 +from PIL import Image +from pathlib import Path +from .utils import visualize_bbox_and_label + + +class Box: + def __init__( + self, x1: int, y1: int, x2: int, y2: int, conf: float = -1.0, label: str = "" + ): + self._x1 = x1 + self._y1 = y1 + self._x2 = x2 + self._y2 = y2 + self._conf = conf + self._label = label + + def __repr__(self) -> str: + return str(self.bbox) + + def __str__(self) -> str: + return str(self.bbox) + + def get(self, return_confidence=False) -> Union[list[int], list[Union[float, int]]]: + return self.bbox if not return_confidence else self.xyxyc + + def __getitem__(self, key): + return self.bbox[key] + + @property + def width(self): + return max(self._x2 - self._x1, -1) + + @property + def height(self): + return max(self._y2 - self._y1, -1) + + @property + def bbox(self) -> list[int]: + return [self._x1, self._y1, self._x2, self._y2] + + @bbox.setter + def bbox(self, bbox_: list[int]): + self._x1, self._y1, self._x2, self._y2 = bbox_ + + @property + def xyxyc(self) -> list[Union[float, int]]: + return [self._x1, self._y1, self._x2, self._y2, self._conf] + + @staticmethod + def normalize_bbox(bbox: list[int]) -> list[int]: + return [int(b) for b in bbox] + + def to_int(self): + self._x1, self._y1, self._x2, self._y2 = self.normalize_bbox( + [self._x1, self._y1, self._x2, self._y2] + ) + return self + + @staticmethod + def clamp_bbox_by_img_wh(bbox: list, width: int, height: int) -> list[int]: + x1, y1, x2, y2 = bbox + x1 = min(max(0, x1), width) + x2 = min(max(0, x2), width) + y1 = min(max(0, y1), height) + y2 = min(max(0, y2), height) + return [x1, y1, x2, y2] + + def clamp_by_img_wh(self, width: int, height: int): + self._x1, self._y1, self._x2, self._y2 = self.clamp_bbox_by_img_wh( + [self._x1, self._y1, self._x2, self._y2], width, height + ) + return self + + @staticmethod + def extend_bbox(bbox: list, margin: list): # -> Self (python3.11) + margin_l, margin_t, margin_r, margin_b = margin + l, t, r, b = bbox # left, top, right, bottom + t = t - (b - t) * margin_t + b = b + (b - t) * margin_b + l = l - (r - l) * margin_l + r = r + (r - l) * margin_r + return [l, t, r, b] + + def get_extend_bbox(self, margin: list): + extended_bbox = self.extend_bbox(self.bbox, margin) + return Box(*extended_bbox, label=self._label) + + @staticmethod + def bbox_is_valid(bbox: list[int]) -> bool: + if bbox == [-1, -1, -1, -1]: + raise ValueError("Empty bounding box found") + l, t, r, b = bbox # left, top, right, bottom + return True if (b - t) * (r - l) > 0 else False + + def is_valid(self) -> bool: + return self.bbox_is_valid(self.bbox) + + @staticmethod + def crop_img_by_bbox(img: np.ndarray, bbox: list) -> np.ndarray: + l, t, r, b = bbox + return img[t:b, l:r] + + def crop_img(self, img: np.ndarray) -> np.ndarray: + return self.crop_img_by_bbox(img, self.bbox) + + +class Word: + def __init__( + self, + image=None, + text="", + conf_cls=-1.0, + bbox_obj: Box = Box(-1, -1, -1, -1), + conf_detect=-1.0, + kie_label="", + ): + # self.type = "word" + self._text = text + self._image = image + self._conf_det = conf_detect + self._conf_cls = conf_cls + # [left, top,right,bot] coordinate of top-left and bottom-right point + self._bbox_obj = bbox_obj + # self.word_id = 0 # id of word + # self.word_group_id = 0 # id of word_group which instance belongs to + # self.line_id = 0 # id of line which instance belongs to + # self.paragraph_id = 0 # id of line which instance belongs to + self._kie_label = kie_label + + @property + def bbox(self) -> list[int]: + return self._bbox_obj.bbox + + @property + def text(self) -> str: + return self._text + + @property + def height(self): + return self._bbox_obj.height + + @property + def width(self): + return self._bbox_obj.width + + def __repr__(self) -> str: + return self._text + + def __str__(self) -> str: + return self._text + + def is_valid(self) -> bool: + return self._bbox_obj.is_valid() + + # def is_special_word(self): + # if not self._text: + # raise ValueError("Cannot validatie size of empty bounding box") + + # # if len(text) > 7: + # # return True + # if len(self._text) >= 7: + # no_digits = sum(c.isdigit() for c in text) + # return no_digits / len(text) >= 0.3 + + # return False + + +class WordGroup: + def __init__( + self, + list_words: List[Word] = list(), + text: str = "", + boundingbox: Box = Box(-1, -1, -1, -1), + conf_cls: float = -1, + conf_det: float = -1, + ): + # self.type = "word_group" + self._list_words = list_words # dict of word instances + # self.word_group_id = 0 # word group id + # self.line_id = 0 # id of line which instance belongs to + # self.paragraph_id = 0 # id of paragraph which instance belongs to + self._text = text + self._bbox_obj = boundingbox + self._kie_label = "" + self._conf_cls = conf_cls + self._conf_det = conf_det + + @property + def bbox(self) -> list[int]: + return self._bbox_obj.bbox + + @property + def text(self) -> str: + return self._text + + @property + def list_words(self) -> list[Word]: + return self._list_words + + def __repr__(self) -> str: + return self._text + + def __str__(self) -> str: + return self._text + + # def add_word(self, word: Word): # add a word instance to the word_group + # if word._text != "✪": + # for w in self._list_words: + # if word.word_id == w.word_id: + # print("Word id collision") + # return False + # word.word_group_id = self.word_group_id # + # word.line_id = self.line_id + # word.paragraph_id = self.paragraph_id + # self._list_words.append(word) + # self._text += " " + word._text + # if self.bbox_obj == [-1, -1, -1, -1]: + # self.bbox_obj = word._bbox_obj + # else: + # self.bbox_obj = [ + # min(self.bbox_obj[0], word._bbox_obj[0]), + # min(self.bbox_obj[1], word._bbox_obj[1]), + # max(self.bbox_obj[2], word._bbox_obj[2]), + # max(self.bbox_obj[3], word._bbox_obj[3]), + # ] + # return True + # else: + # return False + + # def update_word_group_id(self, new_word_group_id): + # self.word_group_id = new_word_group_id + # for i in range(len(self._list_words)): + # self._list_words[i].word_group_id = new_word_group_id + + # def update_kie_label(self): + # list_kie_label = [word._kie_label for word in self._list_words] + # dict_kie = dict() + # for label in list_kie_label: + # if label not in dict_kie: + # dict_kie[label] = 1 + # else: + # dict_kie[label] += 1 + # total = len(list(dict_kie.values())) + # max_value = max(list(dict_kie.values())) + # list_keys = list(dict_kie.keys()) + # list_values = list(dict_kie.values()) + # self.kie_label = list_keys[list_values.index(max_value)] + + # def update_text(self): # update text after changing positions of words in list word + # text = "" + # for word in self._list_words: + # text += " " + word._text + # self._text = text + + +class Line: + def __init__( + self, + list_word_groups: List[WordGroup] = [], + text: str = "", + boundingbox: Box = Box(-1, -1, -1, -1), + conf_cls: float = -1, + conf_det: float = -1, + ): + # self.type = "line" + self._list_word_groups = ( + list_word_groups # list of Word_group instances in the line + ) + # self.line_id = 0 # id of line in the paragraph + # self.paragraph_id = 0 # id of paragraph which instance belongs to + self._text = text + self._bbox_obj = boundingbox + self._conf_cls = conf_cls + self._conf_det = conf_det + + @property + def bbox(self) -> list[int]: + return self._bbox_obj.bbox + + @property + def text(self) -> str: + return self._text + + @property + def list_word_groups(self) -> List[WordGroup]: + return self._list_word_groups + + @property + def list_words(self) -> list[Word]: + return [ + word + for word_group in self._list_word_groups + for word in word_group.list_words + ] + + def __repr__(self) -> str: + return self._text + + def __str__(self) -> str: + return self._text + + # def add_group(self, word_group: WordGroup): # add a word_group instance + # if word_group._list_words is not None: + # for wg in self.list_word_groups: + # if word_group.word_group_id == wg.word_group_id: + # print("Word_group id collision") + # return False + + # self.list_word_groups.append(word_group) + # self.text += word_group._text + # word_group.paragraph_id = self.paragraph_id + # word_group.line_id = self.line_id + + # for i in range(len(word_group._list_words)): + # word_group._list_words[ + # i + # ].paragraph_id = self.paragraph_id # set paragraph_id for word + # word_group._list_words[i].line_id = self.line_id # set line_id for word + # return True + # return False + + # def update_line_id(self, new_line_id): + # self.line_id = new_line_id + # for i in range(len(self.list_word_groups)): + # self.list_word_groups[i].line_id = new_line_id + # for j in range(len(self.list_word_groups[i]._list_words)): + # self.list_word_groups[i]._list_words[j].line_id = new_line_id + + # def merge_word(self, word): # word can be a Word instance or a Word_group instance + # if word.text != "✪": + # if self.boundingbox == [-1, -1, -1, -1]: + # self.boundingbox = word.boundingbox + # else: + # self.boundingbox = [ + # min(self.boundingbox[0], word.boundingbox[0]), + # min(self.boundingbox[1], word.boundingbox[1]), + # max(self.boundingbox[2], word.boundingbox[2]), + # max(self.boundingbox[3], word.boundingbox[3]), + # ] + # self.list_word_groups.append(word) + # self.text += " " + word.text + # return True + # return False + + # def __cal_ratio(self, top1, bottom1, top2, bottom2): + # sorted_vals = sorted([top1, bottom1, top2, bottom2]) + # intersection = sorted_vals[2] - sorted_vals[1] + # min_height = min(bottom1 - top1, bottom2 - top2) + # if min_height == 0: + # return -1 + # ratio = intersection / min_height + # return ratio + + # def __cal_ratio_height(self, top1, bottom1, top2, bottom2): + + # height1, height2 = top1 - bottom1, top2 - bottom2 + # ratio_height = float(max(height1, height2)) / float(min(height1, height2)) + # return ratio_height + + # def in_same_line(self, input_line, thresh=0.7): + # # calculate iou in vertical direction + # _, top1, _, bottom1 = self.boundingbox + # _, top2, _, bottom2 = input_line.boundingbox + + # ratio = self.__cal_ratio(top1, bottom1, top2, bottom2) + # ratio_height = self.__cal_ratio_height(top1, bottom1, top2, bottom2) + + # if ( + # (top2 <= top1 <= bottom2) or (top1 <= top2 <= bottom1) + # and ratio >= thresh + # and (ratio_height < 2) + # ): + # return True + # return False + + +# class Paragraph: +# def __init__(self, id=0, lines=None): +# self.list_lines = lines if lines is not None else [] # list of all lines in the paragraph +# self.paragraph_id = id # index of paragraph in the ist of paragraph +# self.text = "" +# self.boundingbox = [-1, -1, -1, -1] + +# @property +# def bbox(self): +# return self.boundingbox + +# def __repr__(self) -> str: +# return self.text + +# def __str__(self) -> str: +# return self.text + +# def add_line(self, line: Line): # add a line instance +# if line.list_word_groups is not None: +# for l in self.list_lines: +# if line.line_id == l.line_id: +# print("Line id collision") +# return False +# for i in range(len(line.list_word_groups)): +# line.list_word_groups[ +# i +# ].paragraph_id = ( +# self.paragraph_id +# ) # set paragraph id for every word group in line +# for j in range(len(line.list_word_groups[i]._list_words)): +# line.list_word_groups[i]._list_words[ +# j +# ].paragraph_id = ( +# self.paragraph_id +# ) # set paragraph id for every word in word groups +# line.paragraph_id = self.paragraph_id # set paragraph id for line +# self.list_lines.append(line) # add line to paragraph +# self.text += " " + line.text +# return True +# else: +# return False + +# def update_paragraph_id( +# self, new_paragraph_id +# ): # update new paragraph_id for all lines, word_groups, words inside paragraph +# self.paragraph_id = new_paragraph_id +# for i in range(len(self.list_lines)): +# self.list_lines[ +# i +# ].paragraph_id = new_paragraph_id # set new paragraph_id for line +# for j in range(len(self.list_lines[i].list_word_groups)): +# self.list_lines[i].list_word_groups[ +# j +# ].paragraph_id = new_paragraph_id # set new paragraph_id for word_group +# for k in range(len(self.list_lines[i].list_word_groups[j].list_words)): +# self.list_lines[i].list_word_groups[j].list_words[ +# k +# ].paragraph_id = new_paragraph_id # set new paragraph id for word +# return True + + +class Page: + def __init__( + self, + word_segments: Union[List[WordGroup], List[Line]], + image: np.ndarray, + deskewed_image: Optional[np.ndarray] = None, + ) -> None: + self._word_segments = word_segments + self._image = image + self._deskewed_image = deskewed_image + self._drawed_image: Optional[np.ndarray] = None + + @property + def word_segments(self): + return self._word_segments + + @property + def list_words(self) -> list[Word]: + return [ + word + for word_segment in self._word_segments + for word in word_segment.list_words + ] + + @property + def image(self): + return self._image + + @property + def PIL_image(self): + return Image.fromarray(self._image) + + @property + def drawed_image(self): + return self._drawed_image + + @property + def deskewed_image(self): + return self._deskewed_image + + def visualize_bbox_and_label(self, **kwargs: dict): + if self._drawed_image is not None: + return self._drawed_image + bboxes = list() + texts = list() + for word in self.list_words: + bboxes.append([int(float(b)) for b in word.bbox]) + texts.append(word._text) + img = visualize_bbox_and_label( + self._deskewed_image if self._deskewed_image is not None else self._image, + bboxes, + texts, + **kwargs + ) + self._drawed_image = img + return self._drawed_image + + def save_img(self, save_path: str, **kwargs: dict) -> None: + save_path_deskew = kwargs.pop("save_path_deskew", Path(save_path).with_stem(Path(save_path).stem + "_deskewed").as_posix()) + if self._deskewed_image is not None: + # save_path_deskew: str = kwargs.pop("save_path_deskew", Path(save_path).with_stem(Path(save_path).stem + "_deskewed").as_posix()) # type: ignore + cv2.imwrite(save_path_deskew, self._deskewed_image) + + img = self.visualize_bbox_and_label(**kwargs) + cv2.imwrite(save_path, img) + + + def write_to_file(self, mode: str, save_path: str) -> None: + f = open(save_path, "w+", encoding="utf-8") + for word_segment in self._word_segments: + if mode == "segment": + xmin, ymin, xmax, ymax = word_segment.bbox + f.write( + "{}\t{}\t{}\t{}\t{}\n".format( + xmin, ymin, xmax, ymax, word_segment._text + ) + ) + elif mode == "word": + for word in word_segment.list_words: + # xmin, ymin, xmax, ymax = word.bbox + xmin, ymin, xmax, ymax = [int(float(b)) for b in word.bbox] + f.write( + "{}\t{}\t{}\t{}\t{}\n".format( + xmin, ymin, xmax, ymax, word._text + ) + ) + else: + raise NotImplementedError("Unknown mode: {}".format(mode)) + f.close() + + +class Document: + def __init__(self, lpages: List[Page]) -> None: + self.lpages = lpages diff --git a/cope2n-ai-fi/modules/ocr_engine/src/ocr.py b/cope2n-ai-fi/modules/ocr_engine/src/ocr.py new file mode 100644 index 0000000..280d0b2 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/src/ocr.py @@ -0,0 +1,258 @@ +from typing import Union, overload, List, Optional, Tuple +from PIL import Image +import torch +import numpy as np +import yaml +from pathlib import Path +import mmcv +from sdsvtd import StandaloneYOLOXRunner +from sdsvtr import StandaloneSATRNRunner +from sdsv_dewarp.api import AlignImage + +from .utils import ImageReader, chunks, Timer, post_process_recog # rotate_bbox + +# from .utils import jdeskew as deskew +# from externals.deskew.sdsv_dewarp import pdeskew as deskew +# from .utils import deskew +from .dto import Word, Line, Page, Document, Box, WordGroup + +# from .word_formation import words_to_lines as words_to_lines +# from .word_formation import wo rds_to_lines_mmocr as words_to_lines +from .word_formation import words_formation_mmocr_tesseract as word_formation + +DEFAULT_SETTING_PATH = str(Path(__file__).parents[1]) + "/settings.yml" + + +class OcrEngine: + def __init__(self, settings_file: str = DEFAULT_SETTING_PATH, **kwargs): + """Warper of text detection and text recognition + :param settings_file: path to default setting file + :param kwargs: keyword arguments to overwrite the default settings file + """ + with open(settings_file) as f: + # use safe_load instead load + self._settings = yaml.safe_load(f) + self._update_configs(kwargs) + + self._ensure_device() + self._detector = StandaloneYOLOXRunner(**self._settings["detector"]) + self._recognizer = StandaloneSATRNRunner(**self._settings["recognizer"]) + self._deskewer = self._load_deskewer() + + def _update_configs(self, params): + for key, para in params.items(): # overwrite default settings by keyword arguments + if key not in self._settings: + raise ValueError("Invalid setting found in OcrEngine: ", k) + if key == "device": + self._settings[key] = para + self._settings["detector"][key] = para + self._settings["recognizer"][key] = para + self._settings["deskew"][key] = para + else: + for k, v in para.items(): + if isinstance(v, dict): + for sub_key, sub_value in v.items(): + self._settings[key][k][sub_key] = sub_value + else: + self._settings[key][k] = v + + def _load_deskewer(self) -> Optional[AlignImage]: + if self._settings["deskew"]["enable"]: + deskewer = AlignImage( + **{k: v for k, v in self._settings["deskew"].items() if k != "enable"} + ) + print( + "[WARNING]: Deskew is enabled. The bounding boxes prediction may not be aligned with the original image. In case of using these predictions for pseudo-label, turn on save_deskewed option and use the saved deskewed images instead for further proceed." + ) + return deskewer + return None + + def _ensure_device(self): + if "cuda" in self._settings["device"]: + if not torch.cuda.is_available(): + print("[WARNING]: CUDA is not available, running with cpu instead") + self._settings["device"] = "cpu" + + @property + def version(self): + return { + "detector": self._settings["detector"], + "recognizer": self._settings["recognizer"], + } + + @property + def settings(self): + return self._settings + + # @staticmethod + # def xyxyc_to_xyxy_c(xyxyc: np.ndarray) -> Tuple[List[list], list]: + # ''' + # convert sdsvtd yoloX detection output to list of bboxes and list of confidences + # @param xyxyc: array of shape (n, 5) + # ''' + # xyxy = xyxyc[:, :4].tolist() + # confs = xyxyc[:, 4].tolist() + # return xyxy, confs + # -> Tuple[np.ndarray, List[Box]]: + + def preprocess(self, img: np.ndarray) -> tuple[np.ndarray, bool, float]: + img_ = img.copy() + if self._settings["max_img_size"]: + img_ = mmcv.imrescale( + img, + tuple(self._settings["max_img_size"]), + return_scale=False, + interpolation="bilinear", + backend="cv2", + ) + is_blank = False + if self._deskewer: + with Timer("deskew"): + img_, is_blank, angle = self._deskewer(img_) + return img, is_blank, angle # replace img_ to img + # for i, bbox in enumerate(bboxes): + # rotated_bbox = rotate_bbox(bbox, angle, img.shape[:2]) + # bboxes[i].bbox = rotated_bbox + return img, is_blank, 0 + + def run_detect( + self, img: np.ndarray, return_raw: bool = False + ) -> Tuple[np.ndarray, Union[List[Box], List[list]]]: + """ + run text detection and return list of xyxyc if return_confidence is True, otherwise return a list of xyxy + """ + pred_det = self._detector(img) + if self._settings["detector"]["auto_rotate"]: + img, pred_det = pred_det + pred_det = pred_det[0] # only image at a time + return ( + (img, pred_det.tolist()) + if return_raw + else (img, [Box(*xyxyc) for xyxyc in pred_det.tolist()]) + ) + + def run_recog( + self, imgs: List[np.ndarray] + ) -> Union[List[str], List[Tuple[str, float]]]: + if len(imgs) == 0: + return list() + pred_rec = self._recognizer(imgs) + return [ + (post_process_recog(word), conf) + for word, conf in zip(pred_rec[0], pred_rec[1]) + ] + + def read_img(self, img: str) -> np.ndarray: + return ImageReader.read(img) + + def get_cropped_imgs( + self, img: np.ndarray, bboxes: Union[List[Box], List[list]] + ) -> Tuple[List[np.ndarray], List[bool]]: + """ + img: np image + bboxes: list of xyxy + """ + lcropped_imgs = list() + mask = list() + for bbox in bboxes: + bbox = Box(*bbox) if isinstance(bbox, list) else bbox + bbox = bbox.get_extend_bbox(self._settings["extend_bbox"]) + + bbox.clamp_by_img_wh(img.shape[1], img.shape[0]) + bbox.to_int() + if not bbox.is_valid(): + mask.append(False) + continue + cropped_img = bbox.crop_img(img) + lcropped_imgs.append(cropped_img) + mask.append(True) + return lcropped_imgs, mask + + def read_page( + self, img: np.ndarray, bboxes: Union[List[Box], List[list]] + ) -> Union[List[WordGroup], List[Line]]: + if len(bboxes) == 0: # no bbox found + return list() + with Timer("cropped imgs"): + lcropped_imgs, mask = self.get_cropped_imgs(img, bboxes) + with Timer("recog"): + # batch_mode for efficiency + pred_recs = self.run_recog(lcropped_imgs) + with Timer("construct words"): + lwords = list() + for i in range(len(pred_recs)): + if not mask[i]: + continue + text, conf_rec = pred_recs[i][0], pred_recs[i][1] + bbox = Box(*bboxes[i]) if isinstance(bboxes[i], list) else bboxes[i] + lwords.append( + Word( + image=img, + text=text, + conf_cls=conf_rec, + bbox_obj=bbox, + conf_detect=bbox._conf, + ) + ) + with Timer("word formation"): + return word_formation( + lwords, img.shape[1], **self._settings["words_to_lines"] + )[0] + + # https://stackoverflow.com/questions/48127642/incompatible-types-in-assignment-on-union + + @overload + def __call__(self, img: Union[str, np.ndarray, Image.Image]) -> Page: + ... + + @overload + def __call__(self, img: List[Union[str, np.ndarray, Image.Image]]) -> Document: + ... + + def __call__(self, img): # type: ignore #ignoring type before implementing batch_mode + """ + Accept an image or list of them, return ocr result as a page or document + """ + with Timer("read image"): + img = ImageReader.read(img) + if self._settings["batch_size"] == 1: + if isinstance(img, list): + if len(img) == 1: + img = img[0] # in case input type is a 1 page pdf + else: + raise AssertionError( + "list input can only be used with batch_mode enabled" + ) + img_deskewed, is_blank, angle = self.preprocess(img) + + if is_blank: + print( + "[WARNING]: Blank image detected" + ) # TODO: should we stop the execution here? + with Timer("detect"): + img_deskewed, bboxes = self.run_detect(img_deskewed) + with Timer("read_page"): + lsegments = self.read_page(img_deskewed, bboxes) + return Page(lsegments, img, img_deskewed if angle != 0 else None) + else: + # lpages = [] + # # chunks to reduce memory footprint + # for imgs in chunks(img, self._batch_size): + # # pred_dets = self._detector(imgs) + # # TEMP: use list comprehension because sdsvtd do not support batch mode of text detection + # img = self.preprocess(img) + # img, bboxes = self.run_detect(img) + # for img_, bboxes_ in zip(imgs, bboxes): + # llines = self.read_page(img, bboxes_) + # page = Page(llines, img) + # lpages.append(page) + # return Document(lpages) + raise NotImplementedError("Batch mode is currently not supported") + + +if __name__ == "__main__": + img_path = "/mnt/ssd1T/hungbnt/Cello/data/PH/Sea7/Sea_7_1.jpg" + engine = OcrEngine(device="cuda:0") + # https://stackoverflow.com/questions/66435480/overload-following-optional-argument + page = engine(img_path) # type: ignore + print(page._word_segments) diff --git a/cope2n-ai-fi/modules/ocr_engine/src/utils.py b/cope2n-ai-fi/modules/ocr_engine/src/utils.py new file mode 100644 index 0000000..3c4332e --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/src/utils.py @@ -0,0 +1,369 @@ +from PIL import ImageFont, ImageDraw, Image, ImageOps +# import matplotlib.pyplot as plt +import numpy as np +import cv2 +import os +import time +from typing import Generator, Union, List, overload, Tuple, Callable +import glob +import math +from pathlib import Path +from pdf2image import convert_from_path +# from deskew import determine_skew +# from jdeskew.estimator import get_angle +# from jdeskew.utility import rotate as jrotate + + +def post_process_recog(text: str) -> str: + text = text.replace("✪", " ") + return text + + +def find_maximum_without_outliers(lst: list[int], threshold: float = 1.): + ''' + To find the maximum number in a list while excluding its outlier values, you can follow these steps: + Determine the range within which you consider values as outliers. This can be based on a specific threshold or a statistical measure such as the interquartile range (IQR). + Iterate through the list and filter out the outlier values based on the defined range. Keep track of the non-outlier values. + Find the maximum value among the non-outlier values. + ''' + # Calculate the lower and upper boundaries for outliers + q1 = np.percentile(lst, 25) + q3 = np.percentile(lst, 75) + iqr = q3 - q1 + lower_bound = q1 - threshold * iqr + upper_bound = q3 + threshold * iqr + + # Filter out outlier values + non_outliers = [x for x in lst if lower_bound <= x <= upper_bound] + + # Find the maximum value among non-outliers + max_value = max(non_outliers) + + return max_value + + +class Timer: + def __init__(self, name: str) -> None: + self.name = name + + def __enter__(self): + self.start_time = time.perf_counter() + return self + + def __exit__(self, func: Callable, *args): + self.end_time = time.perf_counter() + self.elapsed_time = self.end_time - self.start_time + print(f"[INFO]: {self.name} took : {self.elapsed_time:.6f} seconds") + + +# def rotate( +# image: np.ndarray, angle: float, background: Union[int, Tuple[int, int, int]] +# ) -> np.ndarray: +# old_width, old_height = image.shape[:2] +# angle_radian = math.radians(angle) +# width = abs(np.sin(angle_radian) * old_height) + abs(np.cos(angle_radian) * old_width) +# height = abs(np.sin(angle_radian) * old_width) + abs(np.cos(angle_radian) * old_height) +# image_center = tuple(np.array(image.shape[1::-1]) / 2) +# rot_mat = cv2.getRotationMatrix2D(image_center, angle, 1.0) +# rot_mat[1, 2] += (width - old_width) / 2 +# rot_mat[0, 2] += (height - old_height) / 2 +# return cv2.warpAffine(image, rot_mat, (int(round(height)), int(round(width))), borderValue=background) + + +# def rotate_bbox(bbox: list, angle: float) -> list: +# # Compute the center point of the bounding box +# cx = bbox[0] + bbox[2] / 2 +# cy = bbox[1] + bbox[3] / 2 + +# # Define the scale factor for the rotated bounding box +# scale = 1.0 # following the deskew and jdeskew function +# angle_radian = math.radians(angle) + +# # Obtain the rotation matrix using cv2.getRotationMatrix2D() +# M = cv2.getRotationMatrix2D((cx, cy), angle_radian, scale) + +# # Apply the rotation matrix to the four corners of the bounding box +# corners = np.array([[bbox[0], bbox[1]], +# [bbox[0] + bbox[2], bbox[1]], +# [bbox[0] + bbox[2], bbox[1] + bbox[3]], +# [bbox[0], bbox[1] + bbox[3]]], dtype=np.float32) +# rotated_corners = cv2.transform(np.array([corners]), M)[0] + +# # Compute the bounding box of the rotated corners +# x = int(np.min(rotated_corners[:, 0])) +# y = int(np.min(rotated_corners[:, 1])) +# w = int(np.max(rotated_corners[:, 0]) - np.min(rotated_corners[:, 0])) +# h = int(np.max(rotated_corners[:, 1]) - np.min(rotated_corners[:, 1])) +# rotated_bbox = [x, y, w, h] + +# return rotated_bbox + +# def rotate_bbox(bbox: List[int], angle: float, old_shape: Tuple[int, int]) -> List[int]: +# # https://medium.com/@pokomaru/image-and-bounding-box-rotation-using-opencv-python-2def6c39453 +# bbox_ = [bbox[0], bbox[1], bbox[2], bbox[1], bbox[2], bbox[3], bbox[0], bbox[3]] +# h, w = old_shape +# cx, cy = (int(w / 2), int(h / 2)) + +# bbox_tuple = [ +# (bbox_[0], bbox_[1]), +# (bbox_[2], bbox_[3]), +# (bbox_[4], bbox_[5]), +# (bbox_[6], bbox_[7]), +# ] # put x and y coordinates in tuples, we will iterate through the tuples and perform rotation + +# rotated_bbox = [] + +# for i, coord in enumerate(bbox_tuple): +# M = cv2.getRotationMatrix2D((cx, cy), angle, 1.0) +# cos, sin = abs(M[0, 0]), abs(M[0, 1]) +# newW = int((h * sin) + (w * cos)) +# newH = int((h * cos) + (w * sin)) +# M[0, 2] += (newW / 2) - cx +# M[1, 2] += (newH / 2) - cy +# v = [coord[0], coord[1], 1] +# adjusted_coord = np.dot(M, v) +# rotated_bbox.insert(i, (adjusted_coord[0], adjusted_coord[1])) +# result = [int(x) for t in rotated_bbox for x in t] +# return [result[i] for i in [0, 1, 2, -1]] # reformat to xyxy + + +# def deskew(image: np.ndarray) -> Tuple[np.ndarray, float]: +# grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) +# angle = 0. +# try: +# angle = determine_skew(grayscale) +# except Exception: +# pass +# rotated = rotate(image, angle, (0, 0, 0)) if angle else image +# return rotated, angle + + +# def jdeskew(image: np.ndarray) -> Tuple[np.ndarray, float]: +# angle = 0. +# try: +# angle = get_angle(image) +# except Exception: +# pass +# # TODO: change resize = True and scale the bounding box +# rotated = jrotate(image, angle, resize=False) if angle else image +# return rotated, angle +# def deskew() + +class ImageReader: + """ + accept anything, return numpy array image + """ + supported_ext = [".png", ".jpg", ".jpeg", ".pdf", ".gif"] + + @staticmethod + def validate_img_path(img_path: str) -> None: + if not os.path.exists(img_path): + raise FileNotFoundError(img_path) + if os.path.isdir(img_path): + raise IsADirectoryError(img_path) + if not Path(img_path).suffix.lower() in ImageReader.supported_ext: + raise NotImplementedError("Not supported extension at {}".format(img_path)) + + @overload + @staticmethod + def read(img: Union[str, np.ndarray, Image.Image]) -> np.ndarray: ... + + @overload + @staticmethod + def read(img: List[Union[str, np.ndarray, Image.Image]]) -> List[np.ndarray]: ... + + @overload + @staticmethod + def read(img: str) -> List[np.ndarray]: ... # for pdf or directory + + @staticmethod + def read(img): + if isinstance(img, list): + return ImageReader.from_list(img) + elif isinstance(img, str) and os.path.isdir(img): + return ImageReader.from_dir(img) + elif isinstance(img, str) and img.endswith(".pdf"): + return ImageReader.from_pdf(img) + else: + return ImageReader._read(img) + + @staticmethod + def from_dir(dir_path: str) -> List[np.ndarray]: + if os.path.isdir(dir_path): + image_files = glob.glob(os.path.join(dir_path, "*")) + return ImageReader.from_list(image_files) + else: + raise NotADirectoryError(dir_path) + + @staticmethod + def from_str(img_path: str) -> np.ndarray: + ImageReader.validate_img_path(img_path) + return ImageReader.from_PIL(Image.open(img_path)) + + @staticmethod + def from_np(img_array: np.ndarray) -> np.ndarray: + return img_array + + @staticmethod + def from_PIL(img_pil: Image.Image, transpose=True) -> np.ndarray: + # if img_pil.is_animated: + # raise NotImplementedError("Only static images are supported, animated image found") + if transpose: + img_pil = ImageOps.exif_transpose(img_pil) + if img_pil.mode != "RGB": + img_pil = img_pil.convert("RGB") + + return np.array(img_pil) + + @staticmethod + def from_list(img_list: List[Union[str, np.ndarray, Image.Image]]) -> List[np.ndarray]: + limgs = list() + for img_path in img_list: + try: + if isinstance(img_path, str): + ImageReader.validate_img_path(img_path) + limgs.append(ImageReader._read(img_path)) + except (FileNotFoundError, NotImplementedError, IsADirectoryError) as e: + print("[ERROR]: ", e) + print("[INFO]: Skipping image {}".format(img_path)) + return limgs + + @staticmethod + def from_pdf(pdf_path: str, start_page: int = 0, end_page: int = 0) -> List[np.ndarray]: + pdf_file = convert_from_path(pdf_path) + if end_page is not None: + end_page = min(len(pdf_file), end_page + 1) + limgs = [np.array(pdf_page) for pdf_page in pdf_file[start_page:end_page]] + return limgs + + @staticmethod + def _read(img: Union[str, np.ndarray, Image.Image]) -> np.ndarray: + if isinstance(img, str): + return ImageReader.from_str(img) + elif isinstance(img, Image.Image): + return ImageReader.from_PIL(img) + elif isinstance(img, np.ndarray): + return ImageReader.from_np(img) + else: + raise ValueError("Invalid img argument type: ", type(img)) + + +def get_name(file_path, ext: bool = True): + file_path_ = os.path.basename(file_path) + return file_path_ if ext else os.path.splitext(file_path_)[0] + + +def construct_file_path(dir, file_path, ext=''): + ''' + args: + dir: /path/to/dir + file_path /example_path/to/file.txt + ext = '.json' + return + /path/to/dir/file.json + ''' + return os.path.join( + dir, get_name(file_path, + True)) if ext == '' else os.path.join( + dir, get_name(file_path, + False)) + ext + + +def chunks(lst: list, n: int) -> Generator: + """ + Yield successive n-sized chunks from lst. + https://stackoverflow.com/questions/312443/how-do-i-split-a-list-into-equally-sized-chunks + """ + for i in range(0, len(lst), n): + yield lst[i:i + n] + + +def read_ocr_result_from_txt(file_path: str) -> Tuple[list, list]: + ''' + return list of bounding boxes, list of words + ''' + with open(file_path, 'r') as f: + lines = f.read().splitlines() + boxes, words = [], [] + for line in lines: + if line == "": + continue + x1, y1, x2, y2, text = line.split("\t") + x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2) + if text and text != " ": + words.append(text) + boxes.append((x1, y1, x2, y2)) + return boxes, words + + +def get_xyxywh_base_on_format(bbox, format): + if format == "xywh": + x1, y1, w, h = bbox[0], bbox[1], bbox[2], bbox[3] + x2, y2 = x1 + w, y1 + h + elif format == "xyxy": + x1, y1, x2, y2 = bbox + w, h = x2 - x1, y2 - y1 + else: + raise NotImplementedError("Invalid format {}".format(format)) + return (x1, y1, x2, y2, w, h) + + +def get_dynamic_params_for_bbox_of_label(text, x1, y1, w, h, img_h, img_w, font, font_scale_offset=1): + font_scale_factor = img_h / (img_w + img_h) * font_scale_offset + font_scale = w / (w + h) * font_scale_factor # adjust font scale by width height + thickness = int(font_scale_factor) + 1 + (text_width, text_height) = cv2.getTextSize(text, font, fontScale=font_scale, thickness=thickness)[0] + text_offset_x = x1 + text_offset_y = y1 - thickness + box_coords = ((text_offset_x, text_offset_y + 1), (text_offset_x + text_width - 2, text_offset_y - text_height - 2)) + return (font_scale, thickness, text_height, box_coords) + + +def visualize_bbox_and_label( + img, bboxes, texts, bbox_color=(200, 180, 60), + text_color=(0, 0, 0), + format="xyxy", is_vnese=False, draw_text=True): + ori_img_type = type(img) + if is_vnese: + img = Image.fromarray(img) if ori_img_type is np.ndarray else img + draw = ImageDraw.Draw(img) + img_w, img_h = img.size + font_pil_str = "fonts/arial.ttf" + font_cv2 = cv2.FONT_HERSHEY_SIMPLEX + else: + img_h, img_w = img.shape[0], img.shape[1] + font_cv2 = cv2.FONT_HERSHEY_SIMPLEX + for i in range(len(bboxes)): + text = texts[i] # text = "{}: {:.0f}%".format(LABELS[classIDs[i]], confidences[i]*100) + x1, y1, x2, y2, w, h = get_xyxywh_base_on_format(bboxes[i], format) + font_scale, thickness, text_height, box_coords = get_dynamic_params_for_bbox_of_label( + text, x1, y1, w, h, img_h, img_w, font=font_cv2) + if is_vnese: + font_pil = ImageFont.truetype(font_pil_str, size=text_height) # type: ignore + fdraw_text = draw.text # type: ignore + fdraw_bbox = draw.rectangle # type: ignore + # Pil use different coordinate => y = y+thickness = y-thickness + 2*thickness + arg_text = ((box_coords[0][0], box_coords[1][1]), text) + kwarg_text = {"font": font_pil, "fill": text_color, "width": thickness} + arg_rec = ((x1, y1, x2, y2),) + kwarg_rec = {"outline": bbox_color, "width": thickness} + arg_rec_text = ((box_coords[0], box_coords[1]),) + kwarg_rec_text = {"fill": bbox_color, "width": thickness} + else: + # cv2.rectangle(img, box_coords[0], box_coords[1], color, cv2.FILLED) + # cv2.putText(img, text, (text_offset_x, text_offset_y), font, fontScale=font_scale, color=(50, 0,0), thickness=thickness) + # cv2.rectangle(img, (x1, y1), (x2, y2), color, thickness) + fdraw_text = cv2.putText + fdraw_bbox = cv2.rectangle + arg_text = (img, text, box_coords[0]) + kwarg_text = {"fontFace": font_cv2, "fontScale": font_scale, "color": text_color, "thickness": thickness} + arg_rec = (img, (x1, y1), (x2, y2)) + kwarg_rec = {"color": bbox_color, "thickness": thickness} + arg_rec_text = (img, box_coords[0], box_coords[1]) + kwarg_rec_text = {"color": bbox_color, "thickness": cv2.FILLED} + # draw a bounding box rectangle and label on the img + fdraw_bbox(*arg_rec, **kwarg_rec) # type: ignore + if draw_text: + fdraw_bbox(*arg_rec_text, **kwarg_rec_text) # type: ignore + fdraw_text(*arg_text, **kwarg_text) # type: ignore # text have to put in front of rec_text + return np.array(img) if ori_img_type is np.ndarray and is_vnese else img diff --git a/cope2n-ai-fi/modules/ocr_engine/src/word_formation.py b/cope2n-ai-fi/modules/ocr_engine/src/word_formation.py new file mode 100644 index 0000000..3e64b97 --- /dev/null +++ b/cope2n-ai-fi/modules/ocr_engine/src/word_formation.py @@ -0,0 +1,903 @@ +from builtins import dict +from .dto import Word, Line, WordGroup, Box +from .utils import find_maximum_without_outliers +import numpy as np +from typing import Optional, List, Tuple, Union + +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ +### WORDS TO LINES ALGORITHMS FROM MMOCR AND TESSERACT ############################################################################################################################################################################### +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ + +DEGREE_TO_RADIAN_COEF = np.pi / 180 +MAX_INT = int(2e10 + 9) +MIN_INT = -MAX_INT + + +def is_on_same_line(box_a, box_b, min_y_overlap_ratio=0.8): + """Check if two boxes are on the same line by their y-axis coordinates. + + Two boxes are on the same line if they overlap vertically, and the length + of the overlapping line segment is greater than min_y_overlap_ratio * the + height of either of the boxes. + + Args: + box_a (list), box_b (list): Two bounding boxes to be checked + min_y_overlap_ratio (float): The minimum vertical overlapping ratio + allowed for boxes in the same line + + Returns: + The bool flag indicating if they are on the same line + """ + a_y_min = np.min(box_a[1::2]) + b_y_min = np.min(box_b[1::2]) + a_y_max = np.max(box_a[1::2]) + b_y_max = np.max(box_b[1::2]) + + # Make sure that box a is always the box above another + if a_y_min > b_y_min: + a_y_min, b_y_min = b_y_min, a_y_min + a_y_max, b_y_max = b_y_max, a_y_max + + if b_y_min <= a_y_max: + if min_y_overlap_ratio is not None: + sorted_y = sorted([b_y_min, b_y_max, a_y_max]) + overlap = sorted_y[1] - sorted_y[0] + min_a_overlap = (a_y_max - a_y_min) * min_y_overlap_ratio + min_b_overlap = (b_y_max - b_y_min) * min_y_overlap_ratio + return overlap >= min_a_overlap or \ + overlap >= min_b_overlap + else: + return True + return False + + +def merge_bboxes_to_group(bboxes_group, x_sorted_boxes): + merged_bboxes = [] + for box_group in bboxes_group: + merged_box = {} + merged_box['text'] = ' '.join( + [x_sorted_boxes[idx]['text'] for idx in box_group]) + x_min, y_min = float('inf'), float('inf') + x_max, y_max = float('-inf'), float('-inf') + for idx in box_group: + x_max = max(np.max(x_sorted_boxes[idx]['box'][::2]), x_max) + x_min = min(np.min(x_sorted_boxes[idx]['box'][::2]), x_min) + y_max = max(np.max(x_sorted_boxes[idx]['box'][1::2]), y_max) + y_min = min(np.min(x_sorted_boxes[idx]['box'][1::2]), y_min) + merged_box['box'] = [ + x_min, y_min, x_max, y_min, x_max, y_max, x_min, y_max + ] + merged_box['list_words'] = [x_sorted_boxes[idx]['word'] + for idx in box_group] + merged_bboxes.append(merged_box) + return merged_bboxes + + +def stitch_boxes_into_lines(boxes, max_x_dist=10, min_y_overlap_ratio=0.3): + """Stitch fragmented boxes of words into lines. + + Note: part of its logic is inspired by @Johndirr + (https://github.com/faustomorales/keras-ocr/issues/22) + + Args: + boxes (list): List of ocr results to be stitched + max_x_dist (int): The maximum horizontal distance between the closest + edges of neighboring boxes in the same line + min_y_overlap_ratio (float): The minimum vertical overlapping ratio + allowed for any pairs of neighboring boxes in the same line + + Returns: + merged_boxes(List[dict]): List of merged boxes and texts + """ + + if len(boxes) <= 1: + if len(boxes) == 1: + boxes[0]["list_words"] = [boxes[0]["word"]] + return boxes + + # merged_groups = [] + merged_lines = [] + + # sort groups based on the x_min coordinate of boxes + x_sorted_boxes = sorted(boxes, key=lambda x: np.min(x['box'][::2])) + # store indexes of boxes which are already parts of other lines + skip_idxs = set() + + i = 0 + # locate lines of boxes starting from the leftmost one + for i in range(len(x_sorted_boxes)): + if i in skip_idxs: + continue + # the rightmost box in the current line + rightmost_box_idx = i + line = [rightmost_box_idx] + for j in range(i + 1, len(x_sorted_boxes)): + if j in skip_idxs: + continue + if is_on_same_line(x_sorted_boxes[rightmost_box_idx]['box'], + x_sorted_boxes[j]['box'], min_y_overlap_ratio): + line.append(j) + skip_idxs.add(j) + rightmost_box_idx = j + + # split line into lines if the distance between two neighboring + # sub-lines' is greater than max_x_dist + # groups = [] + # line_idx = 0 + # groups.append([line[0]]) + # for k in range(1, len(line)): + # curr_box = x_sorted_boxes[line[k]] + # prev_box = x_sorted_boxes[line[k - 1]] + # dist = np.min(curr_box['box'][::2]) - np.max(prev_box['box'][::2]) + # if dist > max_x_dist: + # line_idx += 1 + # groups.append([]) + # groups[line_idx].append(line[k]) + + # # Get merged boxes + merged_line = merge_bboxes_to_group([line], x_sorted_boxes) + merged_lines.extend(merged_line) + # merged_group = merge_bboxes_to_group(groups,x_sorted_boxes) + # merged_groups.extend(merged_group) + + merged_lines = sorted(merged_lines, key=lambda x: np.min(x['box'][1::2])) + # merged_groups = sorted(merged_groups, key=lambda x: np.min(x['box'][1::2])) + return merged_lines # , merged_groups + +# REFERENCE +# https://vigneshgig.medium.com/bounding-box-sorting-algorithm-for-text-detection-and-object-detection-from-left-to-right-and-top-cf2c523c8a85 +# https://huggingface.co/spaces/tomofi/MMOCR/blame/main/mmocr/utils/box_util.py + + +def words_to_lines_mmocr(words: List[Word], *args) -> Tuple[List[Line], Optional[int]]: + bboxes = [{"box": [w.bbox[0], w.bbox[1], w.bbox[2], w.bbox[1], w.bbox[2], w.bbox[3], w.bbox[0], w.bbox[3]], + "text":w._text, "word":w} for w in words] + merged_lines = stitch_boxes_into_lines(bboxes) + merged_groups = merged_lines # TODO: fix code to return both word group and line + lwords_groups = [WordGroup(list_words=merged_box["list_words"], + text=merged_box["text"], + boundingbox=[merged_box["box"][i] for i in [0, 1, 2, -1]]) + for merged_box in merged_groups] + + llines = [Line(text=word_group._text, list_word_groups=[word_group], boundingbox=word_group._bbox_obj) + for word_group in lwords_groups] + + return llines, None # same format with the origin words_to_lines + # lines = [Line() for merged] + + +# def most_overlapping_row(rows, top, bottom, y_shift): +# max_overlap = -1 +# max_overlap_idx = -1 +# for i, row in enumerate(rows): +# row_top, row_bottom = row +# overlap = min(top + y_shift, row_top) - max(bottom + y_shift, row_bottom) +# if overlap > max_overlap: +# max_overlap = overlap +# max_overlap_idx = i +# return max_overlap_idx +def most_overlapping_row(rows, row_words, bottom, top, y_shift, max_row_size, y_overlap_threshold=0.5): + max_overlap = -1 + max_overlap_idx = -1 + overlapping_rows = [] + + for i, row in enumerate(rows): + row_bottom, row_top = row + overlap = min(bottom - y_shift[i], row_bottom) - \ + max(top - y_shift[i], row_top) + + if overlap > max_overlap: + max_overlap = overlap + max_overlap_idx = i + + # if at least overlap 1 pixel and not (overlap too much and overlap too little) + if (row_top <= bottom and row_bottom >= top) and not (bottom - top - max_overlap > max_row_size * y_overlap_threshold) and not (max_overlap < max_row_size * y_overlap_threshold): + overlapping_rows.append(i) + + # Merge overlapping rows if necessary + if len(overlapping_rows) > 1: + merge_bottom = max(rows[i][0] for i in overlapping_rows) + merge_top = min(rows[i][1] for i in overlapping_rows) + + if merge_bottom - merge_top <= max_row_size: + # Merge rows + merged_row = (merge_bottom, merge_top) + merged_words = [] + # Remove other overlapping rows + + for row_idx in overlapping_rows[:0:-1]: # [1,2,3] -> 3,2 + merged_words.extend(row_words[row_idx]) + del rows[row_idx] + del row_words[row_idx] + + rows[overlapping_rows[0]] = merged_row + row_words[overlapping_rows[0]].extend(merged_words[::-1]) + max_overlap_idx = overlapping_rows[0] + + if bottom - top - max_overlap > max_row_size * y_overlap_threshold and max_overlap < max_row_size * y_overlap_threshold: + max_overlap_idx = -1 + return max_overlap_idx + + +def stitch_boxes_into_lines_tesseract(words: list[Word], max_running_y_shift: int, + gradient: float, y_overlap_threshold: float) -> Tuple[list[list[Word]], float]: + sorted_words = sorted(words, key=lambda x: x.bbox[0]) + rows = [] + row_words = [] + max_row_size = find_maximum_without_outliers([word.height for word in sorted_words]) + running_y_shift = [] + for _i, word in enumerate(sorted_words): + bbox, _text = word.bbox, word._text + _x1, y1, _x2, y2 = bbox + bottom, top = y2, y1 + max_row_size = max(max_row_size, bottom - top) + overlap_row_idx = most_overlapping_row( + rows, row_words, bottom, top, running_y_shift, max_row_size, y_overlap_threshold) + + if overlap_row_idx == -1: # No overlapping row found + new_row = (bottom, top) + rows.append(new_row) + row_words.append([word]) + running_y_shift.append(0) + else: # Overlapping row found + row_bottom, row_top = rows[overlap_row_idx] + new_bottom = max(row_bottom, bottom) + new_top = min(row_top, top) + rows[overlap_row_idx] = (new_bottom, new_top) + row_words[overlap_row_idx].append(word) + new_shift = (top + bottom) / 2 - (row_top + row_bottom) / 2 + running_y_shift[overlap_row_idx] = min( + gradient * running_y_shift[overlap_row_idx] + (1 - gradient) * new_shift, max_running_y_shift) # update and clamp + + # Sort rows and row_texts based on the top y-coordinate + sorted_rows_data = sorted(zip(rows, row_words), key=lambda x: x[0][1]) + _sorted_rows_idx, sorted_row_words = zip(*sorted_rows_data) + # /_|<- the perpendicular line of the horizontal line and the skew line of the page + page_skew_dist = sum(running_y_shift) / len(running_y_shift) + return sorted_row_words, page_skew_dist + + +def construct_word_groups_tesseract(sorted_row_words: list[list[Word]], + max_x_dist: int, page_skew_dist: float) -> list[list[list[Word]]]: + # approximate page_skew_angle by page_skew_dist + corrected_max_x_dist = max_x_dist * abs(np.cos(page_skew_dist * DEGREE_TO_RADIAN_COEF)) + constructed_row_word_groups = [] + for row_words in sorted_row_words: + lword_groups = [] + line_idx = 0 + lword_groups.append([row_words[0]]) + for k in range(1, len(row_words)): + curr_box = row_words[k].bbox + prev_box = row_words[k - 1].bbox + dist = curr_box[0] - prev_box[2] + if dist > corrected_max_x_dist: + line_idx += 1 + lword_groups.append([]) + lword_groups[line_idx].append(row_words[k]) + constructed_row_word_groups.append(lword_groups) + return constructed_row_word_groups + + +def group_bbox_and_text(lwords: Union[list[Word], list[WordGroup]]) -> tuple[Box, tuple[str, float]]: + text = ' '.join([word._text for word in lwords]) + x_min, y_min = MAX_INT, MAX_INT + x_max, y_max = MIN_INT, MIN_INT + conf_det = 0 + conf_cls = 0 + for word in lwords: + x_max = int(max(np.max(word.bbox[::2]), x_max)) + x_min = int(min(np.min(word.bbox[::2]), x_min)) + y_max = int(max(np.max(word.bbox[1::2]), y_max)) + y_min = int(min(np.min(word.bbox[1::2]), y_min)) + conf_det += word._conf_det + conf_cls += word._conf_cls + bbox = Box(x_min, y_min, x_max, y_max, conf=conf_det / len(lwords)) + return bbox, (text, conf_cls / len(lwords)) + + +def words_to_lines_tesseract(words: List[Word], + page_width: int, max_running_y_shift_degree: int, gradient: float, max_x_dist: int, + y_overlap_threshold: float) -> Tuple[List[Line], + Optional[float]]: + max_running_y_shift = page_width * np.tan(max_running_y_shift_degree * DEGREE_TO_RADIAN_COEF) + sorted_row_words, page_skew_dist = stitch_boxes_into_lines_tesseract( + words, max_running_y_shift, gradient, y_overlap_threshold) + constructed_row_word_groups = construct_word_groups_tesseract( + sorted_row_words, max_x_dist, page_skew_dist) + llines = [] + for row in constructed_row_word_groups: + lwords_row = [] + lword_groups = [] + for word_group in row: + bbox_word_group, text_word_group = group_bbox_and_text(word_group) + lwords_row.extend(word_group) + lword_groups.append( + WordGroup( + list_words=word_group, text=text_word_group[0], + conf_cls=text_word_group[1], + boundingbox=bbox_word_group)) + bbox_line, text_line = group_bbox_and_text(lwords_row) + llines.append( + Line( + list_word_groups=lword_groups, text=text_line[0], + boundingbox=bbox_line, conf_cls=text_line[1])) + return llines, page_skew_dist + + + + +### WORDS TO WORDGROUPS ######################################################################################################################################################################################################################### + + +def merge_overlapping_word_groups( + rows: list[list[int]], + row_words: list[list[Word]], + overlapping_rows: list[int], + max_row_size: int) -> bool: + # Merge found overlapping rows if necessary + merge_top = max(rows[i][1] for i in overlapping_rows) + merge_bottom = min(rows[i][3] for i in overlapping_rows) + merge_left = min(rows[i][0] for i in overlapping_rows) + merge_right = max(rows[i][2] for i in overlapping_rows) + + if merge_top - merge_bottom <= max_row_size: + # Merge rows + merged_row = [merge_left, merge_top, merge_right, merge_bottom] + merged_words = [] + # Remove other overlapping rows + + for row_idx in overlapping_rows[:0:-1]: # [1,2,3] -> 3,2 + merged_words.extend(row_words[row_idx]) + del rows[row_idx] + del row_words[row_idx] + + rows[overlapping_rows[0]] = merged_row + row_words[overlapping_rows[0]].extend(merged_words[::-1]) + return True + return False + + +def most_overlapping_word_groups( + rows, row_words, curr_word_bbox, y_shift, max_row_size, y_overlap_threshold, max_x_dist): + max_overlap = -1 + max_overlap_idx = -1 + overlapping_rows = [] + left, top, right, bottom = curr_word_bbox + for i, row in enumerate(rows): + row_left, row_top, row_right, row_bottom = row + top_shift = top - y_shift[i] + bottom_shift = bottom - y_shift[i] + + # find the most overlapping row + overlap = min(bottom_shift, row_bottom) - max(top_shift, row_top) + if overlap > max_overlap and min(right - row_left, left - row_right) < max_x_dist: + max_overlap = overlap + max_overlap_idx = i + + # exclusive process to handle cases where there are multiple satisfying overlapping rows. For example some rows are not initially overlapping but as the appended words constantly get skewer, there is a change that the end of 1 row would reạch the beginning other row + # if (row_top <= bottom and row_bottom >= top) and not (bottom - top - max_overlap > max_row_size * y_overlap_threshold) and not (max_overlap < max_row_size * y_overlap_threshold): + if (row_top <= bottom_shift and row_bottom >= top_shift) \ + and min(right - row_left, left - row_right) < max_x_dist \ + and not (bottom - top - overlap > max_row_size * y_overlap_threshold) \ + and not (overlap < max_row_size * y_overlap_threshold): + # explain: + # (row_top <= bottom_shift and row_bottom >= top_shift) -> overlap at least 1 pixel + # not (bottom - top - overlap > max_row_size * y_overlap_threshold) -> curr_word is not too big too overlap (to exclude figures containing words) + # not (overlap < max_row_size * y_overlap_threshold) -> overlap too little should not be merged + # min(right - row_left, row_right - left) < max_x_dist -> either the curr_word is close enough to left or right of the curr_row + overlapping_rows.append(i) + + if len(overlapping_rows) > 1 and merge_overlapping_word_groups(rows, row_words, overlapping_rows, max_row_size): + max_overlap_idx = overlapping_rows[0] + if bottom - top - max_overlap > max_row_size * y_overlap_threshold and max_overlap < max_row_size * y_overlap_threshold: + max_overlap_idx = -1 + return max_overlap_idx + + +def update_overlapping_word_group_bbox(rows: list[list[int]], overlap_row_idx: int, curr_word_bbox: list[int]) -> None: + left, top, right, bottom = curr_word_bbox + row_left, row_top, row_right, row_bottom = rows[overlap_row_idx] + new_bottom = max(row_bottom, bottom) + new_top = min(row_top, top) + new_left = min(row_left, left) + new_right = max(row_right, right) + rows[overlap_row_idx] = [new_left, new_top, new_right, new_bottom] + + +def update_word_group_running_y_shift( + running_y_shift: list[float], + overlap_row_idx: int, curr_row_bbox: list[int], + curr_word_bbox: list[int], + gradient: float, max_running_y_shift: float) -> None: + _, top, _, bottom = curr_word_bbox + _, row_top, _, row_bottom = curr_row_bbox + new_shift = (top + bottom) / 2 - (row_top + row_bottom) / 2 + running_y_shift[overlap_row_idx] = min( + gradient * running_y_shift[overlap_row_idx] + (1 - gradient) * new_shift, max_running_y_shift) # update and clamp + + +def stitch_boxes_into_word_groups_tesseract(words: list[Word], + max_running_y_shift: int, gradient: float, y_overlap_threshold: float, + max_x_dist: int) -> Tuple[list[WordGroup], float]: + sorted_words = sorted(words, key=lambda x: x.bbox[0]) + rows = [] + row_words = [] + max_row_size = sorted_words[0].height + running_y_shift = [] + for word in sorted_words: + bbox: list[int] = word.bbox + max_row_size = max(max_row_size, bbox[3] - bbox[1]) + if bbox[-1] < 200 and word.text == "Nguyễn": + print("DEBUGING") + overlap_row_idx = most_overlapping_word_groups( + rows, row_words, bbox, running_y_shift, max_row_size, y_overlap_threshold, max_x_dist) + if overlap_row_idx == -1: # No overlapping row found + rows.append(bbox) # new row + row_words.append([word]) # new row_word + running_y_shift.append(0) + else: # Overlapping row found + # row_bottom, row_top = rows[overlap_row_idx] + update_overlapping_word_group_bbox(rows, overlap_row_idx, bbox) + row_words[overlap_row_idx].append(word) # update row_words + update_word_group_running_y_shift( + running_y_shift, overlap_row_idx, rows[overlap_row_idx], + bbox, gradient, max_running_y_shift) + + # Sort rows and row_texts based on the top y-coordinate + sorted_rows_data = sorted(zip(rows, row_words), key=lambda x: x[0][1]) + _sorted_rows_idx, sorted_row_words = zip(*sorted_rows_data) + lword_groups = [] + for word_group in sorted_row_words: + bbox_word_group, text_word_group = group_bbox_and_text(word_group) + lword_groups.append( + WordGroup( + list_words=word_group, text=text_word_group[0], + conf_cls=text_word_group[1], + boundingbox=bbox_word_group)) + # /_|<- the perpendicular line of the horizontal line and the skew line of the page + page_skew_dist = sum(running_y_shift) / len(running_y_shift) + + return lword_groups, page_skew_dist + + +def is_on_same_line_mmocr_tesseract(box_a: list[int], box_b: list[int], min_y_overlap_ratio: float) -> bool: + a_y_min = box_a[1] + b_y_min = box_b[1] + a_y_max = box_a[3] + b_y_max = box_b[3] + + # Make sure that box a is always the box above another + if a_y_min > b_y_min: + a_y_min, b_y_min = b_y_min, a_y_min + a_y_max, b_y_max = b_y_max, a_y_max + + if b_y_min <= a_y_max: + if min_y_overlap_ratio is not None: + sorted_y = sorted([b_y_min, b_y_max, a_y_max]) + overlap = sorted_y[1] - sorted_y[0] + min_a_overlap = (a_y_max - a_y_min) * min_y_overlap_ratio + min_b_overlap = (b_y_max - b_y_min) * min_y_overlap_ratio + return overlap >= min_a_overlap or \ + overlap >= min_b_overlap + else: + return True + return False + + +def stitch_word_groups_into_lines_mmocr_tesseract( + lword_groups: list[WordGroup], + min_y_overlap_ratio: float) -> list[Line]: + merged_lines = [] + + # sort groups based on the x_min coordinate of boxes + # store indexes of boxes which are already parts of other lines + sorted_word_groups = sorted(lword_groups, key=lambda x: x.bbox[0]) + skip_idxs = set() + + i = 0 + # locate lines of boxes starting from the leftmost one + for i in range(len(sorted_word_groups)): + if i in skip_idxs: + continue + # the rightmost box in the current line + rightmost_box_idx = i + line = [rightmost_box_idx] + for j in range(i + 1, len(sorted_word_groups)): + if j in skip_idxs: + continue + if is_on_same_line_mmocr_tesseract(sorted_word_groups[rightmost_box_idx].bbox, + sorted_word_groups[j].bbox, min_y_overlap_ratio): + line.append(j) + skip_idxs.add(j) + rightmost_box_idx = j + + lword_groups_in_line = [sorted_word_groups[k] for k in line] + bbox_line, text_line = group_bbox_and_text(lword_groups_in_line) + merged_lines.append( + Line( + list_word_groups=lword_groups_in_line, text=text_line[0], + conf_cls=text_line[1], + boundingbox=bbox_line)) + merged_lines = sorted(merged_lines, key=lambda x: x.bbox[1]) + return merged_lines + + +def words_formation_mmocr_tesseract(words: List[Word], page_width: int, word_formation_mode: str, max_running_y_shift_degree: int, gradient: float, + max_x_dist: int, y_overlap_threshold: float) -> Tuple[Union[List[WordGroup], list[Line]], + Optional[float]]: + if len(words) == 0: + return [], 0 + max_running_y_shift = page_width * np.tan(max_running_y_shift_degree * DEGREE_TO_RADIAN_COEF) + lword_groups, page_skew_dist = stitch_boxes_into_word_groups_tesseract( + words, max_running_y_shift, gradient, y_overlap_threshold, max_x_dist) + if word_formation_mode == "word_group": + return lword_groups, page_skew_dist + elif word_formation_mode == "line": + llines = stitch_word_groups_into_lines_mmocr_tesseract(lword_groups, y_overlap_threshold) + return llines, page_skew_dist + else: + raise NotImplementedError("Word formation mode not supported: {}".format(word_formation_mode)) + +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ +### END WORDS TO LINES ALGORITHMS FROM MMOCR AND TESSERACT ############################################################################################################################################################################### +############################################################################################################################################################################################################################ +############################################################################################################################################################################################################################ + +# MIN_IOU_HEIGHT = 0.7 +# MIN_WIDTH_LINE_RATIO = 0.05 + + +# def resize_to_original( +# boundingbox, scale +# ): # resize coordinates to match size of original image +# left, top, right, bottom = boundingbox +# left *= scale[1] +# right *= scale[1] +# top *= scale[0] +# bottom *= scale[0] +# return [left, top, right, bottom] + + +# def check_iomin(word: Word, word_group: Word_group): +# min_height = min( +# word.boundingbox[3] - word.boundingbox[1], +# word_group.boundingbox[3] - word_group.boundingbox[1], +# ) +# intersect = min(word.boundingbox[3], word_group.boundingbox[3]) - max( +# word.boundingbox[1], word_group.boundingbox[1] +# ) +# if intersect / min_height > 0.7: +# return True +# return False + + +# def prepare_line(words): +# lines = [] +# visited = [False] * len(words) +# for id_word, word in enumerate(words): +# if word.invalid_size() == 0: +# continue +# new_line = True +# for i in range(len(lines)): +# if ( +# lines[i].in_same_line(word) and not visited[id_word] +# ): # check if word is in the same line with lines[i] +# lines[i].merge_word(word) +# new_line = False +# visited[id_word] = True + +# if new_line == True: +# new_line = Line() +# new_line.merge_word(word) +# lines.append(new_line) + +# # print(len(lines)) +# # sort line from top to bottom according top coordinate +# lines.sort(key=lambda x: x.boundingbox[1]) +# return lines + + +# def __create_word_group(word, word_group_id): +# new_word_group_ = Word_group() +# new_word_group_.list_words = list() +# new_word_group_.word_group_id = word_group_id +# new_word_group_.add_word(word) + +# return new_word_group_ + + +# def __sort_line(line): +# line.list_word_groups.sort( +# key=lambda x: x.boundingbox[0] +# ) # sort word in lines from left to right + +# return line + + +# def __merge_text_for_line(line): +# line.text = "" +# for word in line.list_word_groups: +# line.text += " " + word.text + +# return line + + +# def __update_list_word_groups(line, word_group_id, word_id, line_width): + +# old_list_word_group = line.list_word_groups +# list_word_groups = [] + +# inital_word_group = __create_word_group( +# old_list_word_group[0], word_group_id) +# old_list_word_group[0].word_id = word_id +# list_word_groups.append(inital_word_group) +# word_group_id += 1 +# word_id += 1 + +# for word in old_list_word_group[1:]: +# check_word_group = True +# word.word_id = word_id +# word_id += 1 + +# if ( +# (not list_word_groups[-1].text.endswith(":")) +# and ( +# (word.boundingbox[0] - list_word_groups[-1].boundingbox[2]) +# / line_width +# < MIN_WIDTH_LINE_RATIO +# ) +# and check_iomin(word, list_word_groups[-1]) +# ): +# list_word_groups[-1].add_word(word) +# check_word_group = False + +# if check_word_group: +# new_word_group = __create_word_group(word, word_group_id) +# list_word_groups.append(new_word_group) +# word_group_id += 1 +# line.list_word_groups = list_word_groups +# return line, word_group_id, word_id + + +# def construct_word_groups_in_each_line(lines): +# line_id = 0 +# word_group_id = 0 +# word_id = 0 +# for i in range(len(lines)): +# if len(lines[i].list_word_groups) == 0: +# continue + +# # left, top ,right, bottom +# line_width = lines[i].boundingbox[2] - \ +# lines[i].boundingbox[0] # right - left +# line_width = 1 # TODO: to remove +# lines[i] = __sort_line(lines[i]) + +# # update text for lines after sorting +# lines[i] = __merge_text_for_line(lines[i]) + +# lines[i], word_group_id, word_id = __update_list_word_groups( +# lines[i], +# word_group_id, +# word_id, +# line_width) +# lines[i].update_line_id(line_id) +# line_id += 1 +# return lines + + +# def words_to_lines(words, check_special_lines=True): # words is list of Word instance +# # sort word by top +# words.sort(key=lambda x: (x.boundingbox[1], x.boundingbox[0])) +# # words.sort(key=lambda x: (sum(x.bbox))) +# number_of_word = len(words) +# # print(number_of_word) +# # sort list words to list lines, which have not contained word_group yet +# lines = prepare_line(words) + +# # construct word_groups in each line +# lines = construct_word_groups_in_each_line(lines) +# return lines, number_of_word + + +# def near(word_group1: Word_group, word_group2: Word_group): +# min_height = min( +# word_group1.boundingbox[3] - word_group1.boundingbox[1], +# word_group2.boundingbox[3] - word_group2.boundingbox[1], +# ) +# overlap = min(word_group1.boundingbox[3], word_group2.boundingbox[3]) - max( +# word_group1.boundingbox[1], word_group2.boundingbox[1] +# ) + +# if overlap > 0: +# return True +# if abs(overlap / min_height) < 1.5: +# print("near enough", abs(overlap / min_height), overlap, min_height) +# return True +# return False + + +# def calculate_iou_and_near(wg1: Word_group, wg2: Word_group): +# min_height = min( +# wg1.boundingbox[3] - +# wg1.boundingbox[1], wg2.boundingbox[3] - wg2.boundingbox[1] +# ) +# overlap = min(wg1.boundingbox[3], wg2.boundingbox[3]) - max( +# wg1.boundingbox[1], wg2.boundingbox[1] +# ) +# iou = overlap / min_height +# distance = min( +# abs(wg1.boundingbox[0] - wg2.boundingbox[2]), +# abs(wg1.boundingbox[2] - wg2.boundingbox[0]), +# ) +# if iou > 0.7 and distance < 0.5 * (wg1.boundingboxp[2] - wg1.boundingbox[0]): +# return True +# return False + + +# def construct_word_groups_to_kie_label(list_word_groups: list): +# kie_dict = dict() +# for wg in list_word_groups: +# if wg.kie_label == "other": +# continue +# if wg.kie_label not in kie_dict: +# kie_dict[wg.kie_label] = [wg] +# else: +# kie_dict[wg.kie_label].append(wg) + +# new_dict = dict() +# for key, value in kie_dict.items(): +# if len(value) == 1: +# new_dict[key] = value +# continue + +# value.sort(key=lambda x: x.boundingbox[1]) +# new_dict[key] = value +# return new_dict + + +# def invoice_construct_word_groups_to_kie_label(list_word_groups: list): +# kie_dict = dict() + +# for wg in list_word_groups: +# if wg.kie_label == "other": +# continue +# if wg.kie_label not in kie_dict: +# kie_dict[wg.kie_label] = [wg] +# else: +# kie_dict[wg.kie_label].append(wg) + +# return kie_dict + + +# def postprocess_total_value(kie_dict): +# if "total_in_words_value" not in kie_dict: +# return kie_dict + +# for k, value in kie_dict.items(): +# if k == "total_in_words_value": +# continue +# l = [] +# for v in value: +# if v.boundingbox[3] <= kie_dict["total_in_words_value"][0].boundingbox[3]: +# l.append(v) + +# if len(l) != 0: +# kie_dict[k] = l + +# return kie_dict + + +# def postprocess_tax_code_value(kie_dict): +# if "buyer_tax_code_value" in kie_dict or "seller_tax_code_value" not in kie_dict: +# return kie_dict + +# kie_dict["buyer_tax_code_value"] = [] +# for v in kie_dict["seller_tax_code_value"]: +# if "buyer_name_key" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_key"][0]) +# ): +# kie_dict["buyer_tax_code_value"].append(v) +# continue + +# if "buyer_name_value" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_value"][0]) +# ): +# kie_dict["buyer_tax_code_value"].append(v) +# continue + +# if "buyer_address_value" in kie_dict and near( +# kie_dict["buyer_address_value"][0], v +# ): +# kie_dict["buyer_tax_code_value"].append(v) +# return kie_dict + + +# def postprocess_tax_code_key(kie_dict): +# if "buyer_tax_code_key" in kie_dict or "seller_tax_code_key" not in kie_dict: +# return kie_dict +# kie_dict["buyer_tax_code_key"] = [] +# for v in kie_dict["seller_tax_code_key"]: +# if "buyer_name_key" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_key"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_key"][0]) +# ): +# kie_dict["buyer_tax_code_key"].append(v) +# continue + +# if "buyer_name_value" in kie_dict and ( +# v.boundingbox[3] > kie_dict["buyer_name_value"][0].boundingbox[3] +# or near(v, kie_dict["buyer_name_value"][0]) +# ): +# kie_dict["buyer_tax_code_key"].append(v) +# continue + +# if "buyer_address_value" in kie_dict and near( +# kie_dict["buyer_address_value"][0], v +# ): +# kie_dict["buyer_tax_code_key"].append(v) + +# return kie_dict + + +# def invoice_postprocess(kie_dict: dict): +# # all keys or values which are below total_in_words_value will be thrown away +# kie_dict = postprocess_total_value(kie_dict) +# kie_dict = postprocess_tax_code_value(kie_dict) +# kie_dict = postprocess_tax_code_key(kie_dict) +# return kie_dict + + +# def throw_overlapping_words(list_words): +# new_list = [list_words[0]] +# for word in list_words: +# overlap = False +# area = (word.boundingbox[2] - word.boundingbox[0]) * ( +# word.boundingbox[3] - word.boundingbox[1] +# ) +# for word2 in new_list: +# area2 = (word2.boundingbox[2] - word2.boundingbox[0]) * ( +# word2.boundingbox[3] - word2.boundingbox[1] +# ) +# xmin_intersect = max(word.boundingbox[0], word2.boundingbox[0]) +# xmax_intersect = min(word.boundingbox[2], word2.boundingbox[2]) +# ymin_intersect = max(word.boundingbox[1], word2.boundingbox[1]) +# ymax_intersect = min(word.boundingbox[3], word2.boundingbox[3]) +# if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: +# continue + +# area_intersect = (xmax_intersect - xmin_intersect) * ( +# ymax_intersect - ymin_intersect +# ) +# if area_intersect / area > 0.7 or area_intersect / area2 > 0.7: +# overlap = True +# if overlap == False: +# new_list.append(word) +# return new_list + + +# def check_iou(box1: Word, box2: Box, threshold=0.9): +# area1 = (box1.boundingbox[2] - box1.boundingbox[0]) * ( +# box1.boundingbox[3] - box1.boundingbox[1] +# ) +# area2 = (box2.xmax - box2.xmin) * (box2.ymax - box2.ymin) +# xmin_intersect = max(box1.boundingbox[0], box2.xmin) +# ymin_intersect = max(box1.boundingbox[1], box2.ymin) +# xmax_intersect = min(box1.boundingbox[2], box2.xmax) +# ymax_intersect = min(box1.boundingbox[3], box2.ymax) +# if xmax_intersect < xmin_intersect or ymax_intersect < ymin_intersect: +# area_intersect = 0 +# else: +# area_intersect = (xmax_intersect - xmin_intersect) * ( +# ymax_intersect - ymin_intersect +# ) +# union = area1 + area2 - area_intersect +# iou = area_intersect / union +# if iou > threshold: +# return True +# return False diff --git a/cope2n-ai-fi/modules/sdsvkvu b/cope2n-ai-fi/modules/sdsvkvu new file mode 160000 index 0000000..b93fa59 --- /dev/null +++ b/cope2n-ai-fi/modules/sdsvkvu @@ -0,0 +1 @@ +Subproject commit b93fa59908b3329074a475aaf3a6333b937f34e7 diff --git a/cope2n-ai-fi/requirements.txt b/cope2n-ai-fi/requirements.txt new file mode 100755 index 0000000..d62a544 --- /dev/null +++ b/cope2n-ai-fi/requirements.txt @@ -0,0 +1,10 @@ +django-environ + +sdsv_dewarp +sdsvtd +sdsvtr +sdsvkie +sdsvkvu + +pymupdf +easydict \ No newline at end of file diff --git a/cope2n-ai-fi/run.sh b/cope2n-ai-fi/run.sh new file mode 100755 index 0000000..14df2ea --- /dev/null +++ b/cope2n-ai-fi/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# cd /cope2n-ai-fi/sdsvkie +# pip3 install -v -e . +# cd /cope2n-ai-fi/sdsvtd +# pip3 install -v -e . +# cd /cope2n-ai-fi/sdsvtr +# pip3 install -v -e . +# cd /cope2n-ai-fi +bash -c "celery -A celery_worker.worker_fi worker --loglevel=INFO --pool=solo" \ No newline at end of file

    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