From 7fa635e0bead2782f379bfb2160f64609339dbb7 Mon Sep 17 00:00:00 2001 From: chflame163 Date: Wed, 28 Aug 2024 20:05:24 +0800 Subject: [PATCH] commit EVF-SAMUltra node --- README.MD | 26 +- README_CN.MD | 26 + image/evf_sam_ultra_example.jpg | Bin 0 -> 446585 bytes image/evf_sam_ultra_node.jpg | Bin 0 -> 127637 bytes install_requirements.bat | 8 +- install_requirements_aki.bat | 6 +- py/evf-sam/__init__.py | 0 py/evf-sam/inference.py | 145 +++ .../EfficientSAM/efficient_sam/__init__.py | 7 + .../efficient_sam/build_efficient_sam.py | 22 + .../efficient_sam/efficient_sam.py | 306 ++++++ .../efficient_sam/efficient_sam_decoder.py | 318 ++++++ .../efficient_sam/efficient_sam_encoder.py | 257 +++++ .../model/EfficientSAM/efficient_sam/mlp.py | 29 + .../efficient_sam/two_way_transformer.py | 266 +++++ py/evf-sam/model/configuration_evf.py | 113 ++ py/evf-sam/model/evf_effisam.py | 313 ++++++ py/evf-sam/model/evf_sam.py | 303 ++++++ py/evf-sam/model/evf_sam2.py | 341 ++++++ py/evf-sam/model/evf_sam2_video.py | 321 ++++++ py/evf-sam/model/segment_anything/__init__.py | 10 + .../automatic_mask_generator.py | 372 +++++++ .../model/segment_anything/build_sam.py | 108 ++ .../segment_anything/modeling/__init__.py | 11 + .../model/segment_anything/modeling/common.py | 43 + .../modeling/image_encoder.py | 426 ++++++++ .../segment_anything/modeling/mask_decoder.py | 191 ++++ .../modeling/prompt_encoder.py | 238 +++++ .../model/segment_anything/modeling/sam.py | 184 ++++ .../segment_anything/modeling/transformer.py | 242 +++++ .../model/segment_anything/predictor.py | 284 +++++ .../model/segment_anything/utils/__init__.py | 5 + .../model/segment_anything/utils/amg.py | 346 ++++++ .../model/segment_anything/utils/onnx.py | 157 +++ .../segment_anything/utils/transforms.py | 113 ++ .../model/segment_anything_2/sam2/__init__.py | 10 + .../sam2/automatic_mask_generator.py | 434 ++++++++ .../segment_anything_2/sam2/build_sam.py | 90 ++ .../sam2/csrc/connected_components.cu | 289 +++++ .../sam2/modeling/__init__.py | 5 + .../sam2/modeling/backbones/__init__.py | 5 + .../sam2/modeling/backbones/hieradet.py | 295 ++++++ .../sam2/modeling/backbones/image_encoder.py | 133 +++ .../sam2/modeling/backbones/utils.py | 95 ++ .../sam2/modeling/memory_attention.py | 169 +++ .../sam2/modeling/memory_encoder.py | 182 ++++ .../sam2/modeling/position_encoding.py | 216 ++++ .../sam2/modeling/sam/__init__.py | 5 + .../sam2/modeling/sam/mask_decoder.py | 295 ++++++ .../sam2/modeling/sam/prompt_encoder.py | 241 +++++ .../sam2/modeling/sam/transformer.py | 330 ++++++ .../sam2/modeling/sam2_base.py | 833 +++++++++++++++ .../sam2/modeling/sam2_utils.py | 149 +++ .../sam2/sam2_image_predictor.py | 446 ++++++++ .../sam2/sam2_video_predictor.py | 984 ++++++++++++++++++ .../segment_anything_2/sam2/utils/__init__.py | 5 + .../segment_anything_2/sam2/utils/amg.py | 348 +++++++ .../segment_anything_2/sam2/utils/misc.py | 238 +++++ .../sam2/utils/transforms.py | 99 ++ .../sam2_configs/__init__.py | 5 + .../sam2_configs/sam2_hiera_b+.yaml | 113 ++ .../sam2_configs/sam2_hiera_l.yaml | 117 +++ .../sam2_configs/sam2_hiera_s.yaml | 116 +++ .../sam2_configs/sam2_hiera_t.yaml | 118 +++ py/evf-sam/model/segment_anything_2/setup.py | 29 + py/evf-sam/model/unilm/beit3/README.md | 191 ++++ py/evf-sam/model/unilm/beit3/datasets.py | 847 +++++++++++++++ .../unilm/beit3/engine_for_finetuning.py | 598 +++++++++++ .../get_started/get_started_for_captioning.md | 176 ++++ .../get_started_for_image_classification.md | 138 +++ .../get_started/get_started_for_nlvr2.md | 136 +++ .../get_started/get_started_for_retrieval.md | 161 +++ .../get_started/get_started_for_vqav2.md | 144 +++ py/evf-sam/model/unilm/beit3/glossary.py | 190 ++++ .../model/unilm/beit3/modeling_finetune.py | 386 +++++++ .../model/unilm/beit3/modeling_utils.py | 76 ++ py/evf-sam/model/unilm/beit3/optim_factory.py | 128 +++ py/evf-sam/model/unilm/beit3/randaug.py | 340 ++++++ py/evf-sam/model/unilm/beit3/requirements.txt | 22 + .../model/unilm/beit3/run_beit3_finetuning.py | 448 ++++++++ py/evf-sam/model/unilm/beit3/utils.py | 913 ++++++++++++++++ py/evf-sam/utils/ade20k_classes.json | 30 + py/evf-sam/utils/aug.py | 117 +++ py/evf-sam/utils/data_processing.py | 90 ++ py/evf-sam/utils/dataset.py | 386 +++++++ py/evf-sam/utils/grefcoco.py | 193 ++++ py/evf-sam/utils/grefer.py | 352 +++++++ py/evf-sam/utils/refer.py | 391 +++++++ py/evf-sam/utils/refer_seg_dataset.py | 254 +++++ py/evf-sam/utils/sem_seg_dataset.py | 290 ++++++ py/evf-sam/utils/utils.py | 127 +++ py/evf_sam_ultra.py | 112 ++ pyproject.toml | 4 +- repair_dependency_list.txt | 4 +- requirements.txt | 8 +- whl/hydra_core-1.3.2-py3-none-any.whl | Bin 0 -> 154547 bytes 96 files changed, 19174 insertions(+), 9 deletions(-) create mode 100644 image/evf_sam_ultra_example.jpg create mode 100644 image/evf_sam_ultra_node.jpg create mode 100644 py/evf-sam/__init__.py create mode 100644 py/evf-sam/inference.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/__init__.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/build_efficient_sam.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_decoder.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_encoder.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/mlp.py create mode 100644 py/evf-sam/model/EfficientSAM/efficient_sam/two_way_transformer.py create mode 100644 py/evf-sam/model/configuration_evf.py create mode 100644 py/evf-sam/model/evf_effisam.py create mode 100644 py/evf-sam/model/evf_sam.py create mode 100644 py/evf-sam/model/evf_sam2.py create mode 100644 py/evf-sam/model/evf_sam2_video.py create mode 100644 py/evf-sam/model/segment_anything/__init__.py create mode 100644 py/evf-sam/model/segment_anything/automatic_mask_generator.py create mode 100644 py/evf-sam/model/segment_anything/build_sam.py create mode 100644 py/evf-sam/model/segment_anything/modeling/__init__.py create mode 100644 py/evf-sam/model/segment_anything/modeling/common.py create mode 100644 py/evf-sam/model/segment_anything/modeling/image_encoder.py create mode 100644 py/evf-sam/model/segment_anything/modeling/mask_decoder.py create mode 100644 py/evf-sam/model/segment_anything/modeling/prompt_encoder.py create mode 100644 py/evf-sam/model/segment_anything/modeling/sam.py create mode 100644 py/evf-sam/model/segment_anything/modeling/transformer.py create mode 100644 py/evf-sam/model/segment_anything/predictor.py create mode 100644 py/evf-sam/model/segment_anything/utils/__init__.py create mode 100644 py/evf-sam/model/segment_anything/utils/amg.py create mode 100644 py/evf-sam/model/segment_anything/utils/onnx.py create mode 100644 py/evf-sam/model/segment_anything/utils/transforms.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/__init__.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/automatic_mask_generator.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/build_sam.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/csrc/connected_components.cu create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/__init__.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/__init__.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/hieradet.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/image_encoder.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/utils.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/memory_attention.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/memory_encoder.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/position_encoding.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/sam/__init__.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/sam/mask_decoder.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/sam/prompt_encoder.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/sam/transformer.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_base.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_utils.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/sam2_image_predictor.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/sam2_video_predictor.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/utils/__init__.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/utils/amg.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/utils/misc.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2/utils/transforms.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2_configs/__init__.py create mode 100644 py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_b+.yaml create mode 100644 py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_l.yaml create mode 100644 py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_s.yaml create mode 100644 py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_t.yaml create mode 100644 py/evf-sam/model/segment_anything_2/setup.py create mode 100644 py/evf-sam/model/unilm/beit3/README.md create mode 100644 py/evf-sam/model/unilm/beit3/datasets.py create mode 100644 py/evf-sam/model/unilm/beit3/engine_for_finetuning.py create mode 100644 py/evf-sam/model/unilm/beit3/get_started/get_started_for_captioning.md create mode 100644 py/evf-sam/model/unilm/beit3/get_started/get_started_for_image_classification.md create mode 100644 py/evf-sam/model/unilm/beit3/get_started/get_started_for_nlvr2.md create mode 100644 py/evf-sam/model/unilm/beit3/get_started/get_started_for_retrieval.md create mode 100644 py/evf-sam/model/unilm/beit3/get_started/get_started_for_vqav2.md create mode 100644 py/evf-sam/model/unilm/beit3/glossary.py create mode 100644 py/evf-sam/model/unilm/beit3/modeling_finetune.py create mode 100644 py/evf-sam/model/unilm/beit3/modeling_utils.py create mode 100644 py/evf-sam/model/unilm/beit3/optim_factory.py create mode 100644 py/evf-sam/model/unilm/beit3/randaug.py create mode 100644 py/evf-sam/model/unilm/beit3/requirements.txt create mode 100644 py/evf-sam/model/unilm/beit3/run_beit3_finetuning.py create mode 100644 py/evf-sam/model/unilm/beit3/utils.py create mode 100644 py/evf-sam/utils/ade20k_classes.json create mode 100644 py/evf-sam/utils/aug.py create mode 100644 py/evf-sam/utils/data_processing.py create mode 100644 py/evf-sam/utils/dataset.py create mode 100644 py/evf-sam/utils/grefcoco.py create mode 100644 py/evf-sam/utils/grefer.py create mode 100644 py/evf-sam/utils/refer.py create mode 100644 py/evf-sam/utils/refer_seg_dataset.py create mode 100644 py/evf-sam/utils/sem_seg_dataset.py create mode 100644 py/evf-sam/utils/utils.py create mode 100644 py/evf_sam_ultra.py create mode 100644 whl/hydra_core-1.3.2-py3-none-any.whl diff --git a/README.MD b/README.MD index 9cf63f5..2daf2b6 100644 --- a/README.MD +++ b/README.MD @@ -102,6 +102,8 @@ When this error has occurred, please check the network environment. ## Update **If the dependency package error after updating, please reinstall the relevant dependency packages.
+* Commit [EVF-SAMUltra](#EVFSAMUltra) node, it is implementation of [EVF-SAM](https://github.com/hustvl/EVF-SAM) in ComfyUI. Please download model files from [BaiduNetdisk](https://pan.baidu.com/s/1EvaxgKcCxUpMbYKzLnEx9w?pwd=69bn) or [huggingface/EVF-SAM2](https://huggingface.co/YxZhang/evf-sam2/tree/main), [huggingface/EVF-SAM](https://huggingface.co/YxZhang/evf-sam/tree/main) to ```ComfyUI/models/EVF-SAM``` folder(save the models in their respective subdirectories). +Due to the introduction of new dependencies package, after the plugin upgrade, please execute ```install_dequirements.bat``` to install. * Commit [ImageTaggerSave](#ImageTaggerSave) and [ImageAutoCropV3](#ImageAutoCropV3) nodes. Used to implement the automatic trimming and marking workflow for the training set (the workflow ```image_tagger_save.json``` is located in the workflow directory). * Commit [CheckMaskV2](#CheckMaskV2) node, Added the ```simple``` method to detect masks more quickly. * Commit [ImageReel](#ImageReel) and [ImageReelComposite](#ImageReelComposite) nodes to composite multiple images on a canvas. @@ -1510,7 +1512,7 @@ Output: * y: The y-coordinate of the top left corner position. -## Ultra节点组 +## Ultra Nodes ![image](image/ultra_nodes.jpg) Nodes that use ultra fine edge masking processing methods, the latest version of nodes includes: SegmentAnythingUltraV2, RmBgUltraV2, BiRefNetUltra, PersonMaskUltraV2, SegformerB2ClothesUltra and MaskEdgeUltraDetailV2. There are three edge processing methods for these nodes: @@ -1566,6 +1568,28 @@ On the basis of SegmentAnythingUltra, the following changes have been made: * device: Set whether the VitMatte to use cuda. * max_megapixels: Set the maximum size for VitMate operations. +### EVF-SAMUltra +This node is implementation of [EVF-SAM](https://github.com/hustvl/EVF-SAM) in ComfyUI. +*Please download model files from [BaiduNetdisk](https://pan.baidu.com/s/1EvaxgKcCxUpMbYKzLnEx9w?pwd=69bn) or [huggingface/EVF-SAM2](https://huggingface.co/YxZhang/evf-sam2/tree/main), [huggingface/EVF-SAM](https://huggingface.co/YxZhang/evf-sam/tree/main) to ```ComfyUI/models/EVF-SAM``` folder(save the models in their respective subdirectories). +![image](image/evf_sam_ultra_example.jpg) + +Node Options: +![image](image/evf_sam_ultra_node.jpg) + +* image: The input image. +* model: Select the model. Currently, there are options for evf-sam2 and evf sam. +* presicion: Model accuracy can be selected from fp16, bf16, and fp32. +* load_in_bit: Load the model with positional accuracy. You can choose from 16, 8, and 4. +* pormpt: Prompt words used for segmentation. +* detail_method: Edge processing methods. provides VITMatte, VITMatte(local), PyMatting, GuidedFilter. If the model has been downloaded after the first use of VITMatte, you can use VITMatte (local) afterwards. +* detail_erode: Mask the erosion range inward from the edge. the larger the value, the larger the range of inward repair. +* detail_dilate: The edge of the mask expands outward. the larger the value, the wider the range of outward repair. +* black_point: Edge black sampling threshold. +* white_point: Edge white sampling threshold. +* process_detail: Set to false here will skip edge processing to save runtime. +* device: Set whether the VitMatte to use cuda. +* max_megapixels: Set the maximum size for VitMate operations. + ### Florence2Ultra Using the segmentation function of the Florence2 model, while also having ultra-high edge details. The code for this node section is from [spacepxl/ComfyUI-Florence-2](https://github.com/spacepxl/ComfyUI-Florence-2), thanks to the original author. diff --git a/README_CN.MD b/README_CN.MD index ab70dc1..2a69be5 100644 --- a/README_CN.MD +++ b/README_CN.MD @@ -103,6 +103,8 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git ## 更新说明 **如果本插件更新后出现依赖包错误,请重新安装相关依赖包。 +* 添加 [EVF-SAMUltra](#EVFSAMUltra) 节点,是[EVF-SAM](https://github.com/hustvl/EVF-SAM)在ComfyUI中的实现。请从[百度网盘](https://pan.baidu.com/s/1EvaxgKcCxUpMbYKzLnEx9w?pwd=69bn) 或者 [huggingface/EVF-SAM2](https://huggingface.co/YxZhang/evf-sam2/tree/main), [huggingface/EVF-SAM](https://huggingface.co/YxZhang/evf-sam/tree/main) 下载全部模型文件并复制到```ComfyUI/models/EVF-SAM```文件夹(请将模型保存在各自子目录中)。 +由于引入了新的依赖,插件升级后请执行```install_requirements.bat```安装新的依赖包。 * 添加 [ImageTaggerSave](#ImageTaggerSave) 和 [ImageAutoCropV3](#ImageAutoCropV3) 节点,用于实现训练集自动裁切打标工作流(工作流```image_tagger_save_example.json```在workflow目录中)。 * 添加 [CheckMaskV2](#CheckMaskV2) 节点,增加了```simple```方法以更快速检测遮罩。 * 添加 [ImageReel ](#ImageReel) 和 [ImageReelComposit](#ImageReelComposit) 节点,可将多张图片显示在一起。 @@ -1543,6 +1545,30 @@ SegmentAnythingUltra的V2升级版,增加了VITMatte边缘处理方法。 * device: 设置是否使用cuda。 * max_megapixels: 设置vitmatte运算的最大尺寸。 +### EVF-SAMUltra +本节点是[EVF-SAM](https://github.com/hustvl/EVF-SAM)在ComfyUI中的实现。 +*请从[百度网盘](https://pan.baidu.com/s/1EvaxgKcCxUpMbYKzLnEx9w?pwd=69bn) 或者 [huggingface/EVF-SAM2](https://huggingface.co/YxZhang/evf-sam2/tree/main), [huggingface/EVF-SAM](https://huggingface.co/YxZhang/evf-sam/tree/main) 下载全部模型文件并复制到```ComfyUI/models/EVF-SAM```文件夹(请将模型保存在各自子目录中)。 + +![image](image/evf_sam_ultra_example.jpg) + +节点选项说明: +![image](image/evf_sam_ultra_node.jpg) + +* image: 图片输入。 +* model: 选择模型。目前有 evf-sam2 和 evf-sam 可选。 +* presicion: 模型精度,可选择fp16, bf16 和 fp32。 +* load_in_bit: 按位精度加载模型。可选择16,8 和 4。 +* pormpt: 用于分割的提示词。 +* detail_method: 边缘处理方法。提供了VITMatte, VITMatte(local), PyMatting, GuidedFilter。如果首次使用VITMatte后模型已经下载,之后可以使用VITMatte(local)。 +* detail_erode: 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。 +* detail_dilate: 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。 +* black_point: 边缘黑色采样阈值。 +* white_point: 边缘黑色采样阈值。 +* process_detail: 此处设为False将跳过边缘处理以节省运行时间。 +* device: 设置是否使用cuda。 +* max_megapixels: 设置vitmatte运算的最大尺寸。 + + ### Florence2Ultra 使用 Florence2 模型的分割功能,同时具有超高的边缘细节。 本节点部分的代码来自[spacepxl/ComfyUI-Florence-2](https://github.com/spacepxl/ComfyUI-Florence-2),感谢原作者。 diff --git a/image/evf_sam_ultra_example.jpg b/image/evf_sam_ultra_example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e16012b5e1d7e674691b1184dec3d7a96ef35763 GIT binary patch literal 446585 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%0A@rrTE}Ey%JROXlUSg9 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 ejuH# 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≦ zZ+x2T{Za%&paKA8zWJJl$*d4=DPf@Y;Bb9n^*uGS!>EPaVZq)NM%Jg4yzdMY_h#Wj zr~uiTN0dE{0?S^nT_gzL&0kv%rNji1F}PKY-DKCAouX`32IY27DfFNEkdl&8Z+^19 zA$tlE2In+q`rce|_hS2Cji?Nzm)Oj}sh|j6qsb_dQ^fDj?=rElPf4W%4GC`A_3#hU5F>U-g)%uPinQf`djN zK<*SI1tH>yyYP$yuNc8wOJw#({8Kp@suh(oo_pb2NiW>tV5HsnzU_!-YOZ@dKa?fZewG>W>Oc$o%N#L zH*Uv7o;f|>ykTHHz`8(ioBA0Gm__BJ$I58?(9XlLZ$g*5wJ&sM^{PmS=kp&A6pIFb zY~rD4k*`&xd8XzFAn~gchjr^8vU4hvT7ygHr)eiwJS-FX47#6|Omy-X|Ga~o{N#U{ z>@Qm}B(^0=^FnwoKytD{W&ZehqIP?J!~}IXd4sS?a9?il>?eVyk8~fP6Pv?mk)G%@ z{A3Fvs&jBI)3!s3wXG44zrkPT_}mK-EqY`j`jz>~&8e*@-Q%3INRvhO!v&H$C*F}b z&v%br<>fhg2`ah}a(~9|QE~j{bttvLQum#-7_6rnIf#r}-{YW#!KStJ)DUc$sXh z)VW`fX2@i86ov5QG&?}6yT6SnK2{Z_=%jm<`{N`Yz^q!dl%RsgylK$Ow8>P7nUYlD zdaY*?Qz)UQlREcfCFo4x^>({#DlCze5U&}c2R~D~T z3@bcoJ<^eXm!-crHcBt`y-7}MXp!^aH^!tg?nrD+skG%|V;AC9wGR@{1XlW;QtC|K z%?1<>@fW!%u>Rgdy9P5J$WU9@3C9%LMpKs+BIA#^(OPH zVmy_0TS14xS%YOF&Mmy%VRJ=27E+!*R>RT9z|zhOpKm)%)w_I3ueq@^mvMD%Vjm}o z4}!g1HoFQlJa>2K8PLH_KE^S*S5EL3mU|rs`6KUQLqp-*3^^FGA*b?ScE^9&6;13h z0gaAV^0wrQpontgWdThN&$`EkAC;O&zXN}aDbRiY#n8|*7Uj7=8gCcvFI2L44BOjC zRF$)@I(Aw3fwT0eYbX5)#Ur7l@7VjqC+2Kvqt;^&Y_Gqs+GLEyxNec3vsXs+EV*$A z@6N#CNgn*i(F-HR%_0Hv@bsHeO}5h?ZEX7mj`w@P-Mmyn&PHtact^0aTJXjld%O|k zm1zf3-3Xg1t(+Y_oZdH?c`SE_)Ry`B?v2Cg;~U+2+K@@+G%ht?lv?lnDH4yKKM=yT?}*z9Fbq%1&PJ-Cj8@~FLilLj-~taabsZGnN_H3I)L zaHFZTH$Pl2hE%XMi2J)qOO@h%vQ|b*+B)X`k%j9 z^&6abeD5Flod6d&|v<>pqe8)nE9A~uM&%I1|db;~l=^2><Zw% zOEXSOX|X;fZHsUt@SY#_RF1F}zHHVl2KS%*bnmUdNmkEicsNiNTwS^t#| z5lo6Z!p0j~Sj^2oVdRs~)j?9Bt(Tm|-#cN)gCi=L$yJ>q>R&cn^h8XGak!$i1hzYu zJ>;|9*vs2(3ME>eUHLU_O*w_P`ttBu^4275 z(qB@qI@4ifiRk;qDiv6PIx}|eu)NaU&ibGNw!JIrv*}t)7d8LvOo8TcU(K^KC94bz zePc$?oeXvLPiExXz&A!Rtq;J_lP_nJu2rbmTTRNRowD9%XAp-2FU0O7-z)yXl&zsG z&X8GSRKRGZ${wUoGx}coRlf`FSYlyYYgb6Z#4|6~sEnNDZ(H8;Kh;lezWG{r?G^R* zdp>d+xjZ+~q)`ax>%2sKOYWy#)UlI^ht)Lg!@fF1@jBsp)MZ1+b8~_8j{7vwo<;N$ zTFu|9A|Q3XnveTc+90rm&S7O1e$^rGv~k_F?6rX8a{V!qbjc&GV>>fQh6QM=)LE?@Bz~Y6;9^~QI~B44_s`;yFiPtsP9pI-p)LQ z=CV^TJ4ky%=TQbY&!oxBMCEsAV$hk{mL)ghtDQC;vJdjwQC4^nhXr!5Q^!YoZ=n8hwwa%Y3F?c@r@UrXVofngSM=?ExfP}aOL&srr z_y0%z^>0_*c+Xn|^c(0E#+uP(7PCbBJ7NA*W)rUfi}bWXMbD;w_n45*1}NQN)Y4>N zQTe^2_Y4^p2~?JW`@_rG{Ji9voySws@9wJG)dq77c}C5zGrFs|VI!kvL{eVg)!@#~ zwB24s{M%DkV}K_g6}DFke@Cg3d4|O+W_cH}+d$?)aNj_uhm}4_lap`L0-*vpizJb{ z8vk3?-_LIUNlUR;=@+JZ{3l5=(jueY7V}7YZ@A(mG2g=Wgw7DIvhuNGUncRjb{Q0x zQ0MlK#{~z!0^;%Sx;RIKS*V(vy88J3t$!T$E4ZxyhY3KZ0+>u6_vK$qAD5-c}!sH~Vl?!q30#U@1O@7VtHQ9orXvdasyaJuGOu zL*)VcJ9+R$M0Az0eEfocW)%KO9^f^X!}}Aa4-4Y0O8TI`Cs3W+Bt&T=7hnA|g96drVN&6<>-x8=q}Rq+ECisPufKJ&50v1Y0JABO zpM&olXhc(9ZUDZLfzA+oANf7<$H1t*0G`zX!tu&o(U1joz4!7LGcflccl*VhlK1gX z0>zpB+FbY@3;KU2&wEN~;4TLcvoNvH;9pbSRXb)L79n6B9MV!&=%RykPw{SC$H~3S z@ZVGA|I$;xGE|HB9#u~dl=3X)KeOadnBYzUdF=;Gz`v)uKmN`BGuKJQt9MX59RxO0 zprO#`K**r9-rM|g1KsdHJP_raz)mH(4VTv=fUH9OV%M*meT86&T!unBmGHa#cb$Jo z7J!{5T7{oS7aM7~8W*3an;y^ps4EG6_Q>|dH&4z11*7CjH4Xg<&$pgc+4D7)ePDcS zXIz*2=Lh{9?KGZJ3FGEAuiwAWb6mDhfvpf;D-@qbP$j-f?B}g<)ZdznHA&mS?Sj5J z&+q5$n;olsAZ}T>IUaEeL9rQnFT=jWGHp0KEv@45Fj|ylsYt6R+iM;K*?A~Y(5j2L z@|Pa_4wg(^o>fu5GGR}Ibqt^}+}&rA_H@$%igesF>bC-+SWFaC)Hf92MNy%NR=%>u_cMj1cxCyWK1AFdHfCFAWtcE_IIf6{aw>7mLTfhE z#8<#baq6wT-annITfxcfm`WBV#^LJGV#!zV4g(q66AfxdREo68s+MG#db0e%#FC(n{X&wE#l zqyLlSyyl!rfrQBSd=sg#nlGZ%W~^J}SyOH}EM=olFw=!zY{uMW)_m{Y zk3WDgMyw3RzM6Wx`1xmntJRP-58CvVJ%w(EK1nP=zi+04Dlrn8J;bLegdg5lLh-iu zuu^d8=4{ggN{uKSF^MgQ5mLQ)j)0uQ`Ud7fh~-|bB6+&+*aDkhzP|B{qftC zB}1%dODkAV1Yux|MXg*$iO)!6$NS*@c9(rO%xipwRc^Ao^Jp>C?jiSE4vDa@<{mQ6 zvixF4_WnL7J~bvNmXLT0Gj<}~OE1SB@Vcn7H-tz%={BrY`%AE07j_cWs7)OUc3Qxf z#)O8nSi7%%e_{Dm=af(G&#&%f$^1yAxPJwB7?y#0Y^ z5g#IuX&ndwBu-)xpuowE5AEkhsbe|7Wx;~Uh#rH-e)UA_xvj9BP{w!GB4H z?(NGTz0fn^yx;D3GV9bR;4)$K5ozS?0mm^Y#WAv8GqB(ct)z+|!mwoK z*tXKnl&ETxHIXG+XMA!wP=_#bSWs7Q#8aiFNpmeWaj>l+Xx5bpEpYM3`U#YJ-|SGl zLg3M@M)!E!dD73ER+_|V+8+?G88lq)ASzytJ84!nKqey#P0O)MG&Lzy&hkt1I<1pL zxN$FZOfFmTG4Q7Sh4o2exp0!H(e>lVYUIoVJV8mS0`|z2 zZKBmPJ~RrPb`yLJH1bzF&$I&^9{B5waY%B^OWRhl)S1 z*e782x(Ze9&0Q)yLYAkiRd_*5j%sFCSLBqa8E)XW71c5wQakKFjAJU&U(v(oR6Q)Mh1fWsb^M( zR%IE;+=RPoP(LBN>E@F+(duhfV_|P!_Biq)U1!S1d1j9-YXP}oaK?nh?WaV60iK8Z zKWMd|F$E}3V=Hj5&mVK%US@Nk?`H~*krdyL9~Sc9ch=8$G9oy^MA-ydTliu-bi^w# zSWCqXVTlry8l7V&n%wbMJ;mU4&emHue$qdq_@2&#$l$+6ONyp??JnmMob=pnF0hZ} z>PF7q%l8CL-p_KYydP~3?`fjD3da_%6fGuTBV234RjVU(8;1F8t9I2U2a(ynki)tW z-$KogDAnv{)QN5i$7R0rdU&OwS9qm*%f`Ho?B2c6=UZwrVEFdP(_HwQlw(sgY!O3v z$G7>KBkShdo=IiOZ#8O)+y{0G`-{}ZZfuHXjCBX;woaf@6oLhLDEqdtleICs0~ z>L28LR98>CxLP?T_;Q9;NZUL&8(=R5@6WY+(eD z(JZXQ7+1=w9;8;BKB8fO$wTHcN`})4R-XWaLh3T5Mw!0Cz)Cf_>^zIF3T{2{NTg5io#yGz^cA+8d2Vjfb4gu~$SS+Miz8~p_{AYLaFF}IIlw1aNn4^- zJikW~_;C=KutR~5B4(Jth-Sq^_a@Gq;$=g7mVBqM><(u1YOhwinlC>J81z5N5%pX3 zFiHIE)2Q}cj+PZKncV)HU@jV+ zu49NVV*17_E)ihB`&fC${e{8f3!=G->lX}oQg#gMv@@>&TD-TmxP&V-OJv~y0$?ET zryr>+ek*`3Fy{zu=;Zzv!`!cKssW6ETHXXpcmI>QkNqRi!R7nuz2j4_IRIRQ4q#L% zE_c-(fbj5wu~{STPnHo$?%e&P1( z^f&*n)qblWf%$uXVeNxRFG1G$3R|ajt|QS#h^Rn7#+;JuSu~4yH-5Z}myIrjVsvKu zu2%>|n!$LMARBQDn<1U3!LO=;-6B`FT9}KVchjVdA2Ig?1@k9Yal93btnL?!LkSdP zM{o985wX)VGr-l=>hA_t!YR|#M8(cxtZCXhdRx7)Kkv+uWXd^q-l@cxTTXat(I(QR zZW=^!h$+n%n-WE5< zu|OQx3?C0}H{G1F9=fjcV^&3)_I@BGsO}g)oqLCX09K?QAJ75z5!dhqg`aEv9UJK zDOn91vLO&9$Vgr|MsG;08_#FY^HbLnLBsgT^_>aw4yvdvs#N$ldRZ;!{3?^Rtgv<0 zn5liS$h3J7SY`3RL)mm(TY1QwdXdMG{<=DL2p18x)z_^D{5Z_Jdi$rorwE3aXM2={lxH6@OZFZv03^Ho`H z?CK=*`~8&B&GJnvUPV2ps&>tFbzO9yrMv8>^F9Z4*ETsgJaNH?g(-uf8I!{f>Y?f% zs>m#g!P#toMA0Cn80Dd*m{6f2Hu@p%+X>u(e_+$zE*;L6;_=4KT8-x8oLl!A+e**l zbE`ut<&arvY3lY5kyapwwj1qON3YfNh4Yi_Q~^A3I!T685GVWG1n2=O#HIjMpYaHu zH+_psUE~RUf8HIeYgRR0_{7t){r8OaNXWQLFuI$=K3rEKW>cWpd3Hg%+bV`2L4foY zFLB#Gz{V$_$)R4olU!wi-*8%Yr{DDigk#GfudXGXBQ88#gA`PRTet2j=ZkL`cT%3( zwI`d@Q%x7RcJjqZWoKD;*PrMt6waJWSA%iKaXJ#rAMTw%^Bs_)(pQ4R24!*0EwMQ~ zRlCKSD&iI>WR}ID+}n&C4I>&O0}Z=(Yweho1qSXCIL7MZ)kXPMbftNo`pMfGS+wh? zPBTAr)G`lwL>mIPDj(*O5P3jn3G%YeGD9pLacY;$hgYbVt!lTb$uDikp;hK6cx@Rs zX|Nj}RZK9gJe`!DPq$2tZ+yeGJt0oG*68QSQ%?a4pH>x@tGs^S>@!(?25)r2J^`3s ztZK!J;|BiRM!uHFbVC`LAfm?@fM zD*u3fI-5}r=|=i~yNhZi7Y=xyPW+A-8o;=_dP|J{d+pfyvuFKp*6lcWdhrYqw&bLE zRh*<~fhZTOyy{VniAk;dgDX~U2Sk;b5|!;*s>F?ZI{{ub6-~_C&jUOTYUi1sx*xkX zpcqiJvSZS!vWq${{WmIcF(h+P8dnn_7e%Un3R zLti?S@wRVpjdE|(y*(B)?H0IJXOREUMJ@uoSbaMfnUgap{3@BMek;SXnv+USHWSWq zO=mV(PT6o@T-U&(5=n-#osnc2KqqWGTrA8Dtk7&5ZJWo8;x+Hl68;KA2#u>+E(R!X|yG5CQVr)*DKR^Wn7JloASZU|j$)Xqwww zSU&ea2KTi*1eJdoEm8PpdL_@>aiEG06Et-we0_hl>^#F*Ik@yK3tP|iD|tZ$Hy!kM zl7tQ?>|&QjqPolV+ZKEyOy1fq5N&exNaxJ+Me?EA58}$cT|bD6zqvkHutO&pG_5@g z(B8!qC8fj+reE*s5Ki9aScs3(K2A4(c57h^l8a~X1^4d2mu@Tk1|$SKy;?+G60A8# zcn90e(ZbU*a#gJHbm-*Pz|My?2Q>KIE$v$dl;A#wN(>LIC_4GI@xh(}FU~IiqVwEb zoe2|LxSC0d%xb+O{6`wc7t-)+mE(ubqwi<$x{vUGO;?*pJRlI zqTjk}yem}YL=JSI2nf3a12&V2iuo3o688f*3BvC`!))zVGZ*b%_s-5p9B9SrE69GA zFw^{Vf9}R;bHI%*3=;E9;C|z#=D6brYnYOP(F(eaz`;)*-&zO~y=T&56P+oY*fcm-R{ zTAfnqoc`)C5@}%V=3ygjJs^}vF)fKe8%8gjD#s2lg^P0%fzNM$Gr_kiux>byG=q4( zf*p@KCdstAPB;o;J41SQMaiN>eHX#aAS0RYdb$h5^|ivA^+KiK z7AZ2Sk<$%6pjup0dUL--HmZG&NVT9+>MyI}DEvW1M)yPh12Mc46^yCyf0UTTbK@2) zVm=OMrAY^s+ETp|rFu@cE+M=R|3G!K8+hx2{wDN}d*>|PDS+Ildz<5HFJ+s6GH`@j zsq!|@od^lN2s*EeW6BASFQz-c!D`Hcl}!4)(^GJ5jK@FH!l=rx_JHC|#JSQ&1MLsE zbm72XSp347tBSJ+Zc~tx0MleW+Ezz{Z$_E(b|N{tnn=$y(CiUSr87_y8N)yQ8)g0f zSl2h%{!I@5x0C)K>pH-{{(od$kBh>xc_iVyp;I0^9~E>fGKy*D%x5~3frf$c%hR1U za(h?1s6a7M5^P!|gPl*k>*MGsq8KX{FY|7TSZ%+3K!;3JSc2>;bDvtU#7(|+{X-IS zO~DI2;IX}v^L0Rb8|muLy{CmsV^9lxpGADxjQ!GAi@6|uRD9j68ckbb6yEG3QIzKG z@vc2ovohJisUPrw587I_>*Jm zxE0KTJ+5P6S5!|DVPzma4Zpud&|{OZ`M{ArR;+o7P*)kW;D~altz2{#?XLDfJl@;t z2snp{ZgnsZbwADTQH;qIyV91{(8Xd9FZhP5*W_nj>9)7oN=kLn2u&EPf8G^9T&-tC z$2!yCenpEcSI4+6O@g zuA7~RNh)-c*r|)==?aW;>*LA|8|DRXkX*yZh&s}SRj`k^Ro>_dZj_$ph{Xg!5%Ond zQi~s@hZC#4KnnPk3kg}1(E>d5L?khPA+@$SmOH6=QQ`)j9Ars_EoU+iFLmZA%{cuB zo(z&YZ)}(8bn5Q((Z$s+J5=+ivwt~-UC&n))Y59{O00`x4C}|zITe*|E6Ti+IFBB$ zv;()(hUcRsj$2^14%QO6JBZ@6ks$7K>M-DX+uKp!u?^`uMZP&gs_KW{^lYlRb%@X$ zrJk8t7K|y%l)HUGzD<3|yf%@QxkbS1_F0SDgto#Wm4lZqGB3LAc%!&zrMeZ7UwE@S zbc0nzuesHQy|G+J+-ov3qGrTLy7Jm3mw&$|}5PC!iE=7L6dkLE_x!BU7 zo^_gCPyIyM?U3%Y73y!Po9122Zrz3o)CI ztuGsvS=F9e&ZacTiF)wCrGw0DhHCGJevb7Kv8pN`{uUznmJ;b|l` z4D^f+{QrZruZ(Kr>)xfMKn16`wph{PE~R*JcUp>uV8NXhTA)aPP=ZTw*Wj+fg1fs1 zmjZ>}^e6B8|8Vd9aPL~P)?}TT%;cI zzxiuQ{oU2OwVkv#gU6GScj|SEG|MLLNoN)X(U2{9Bjx2oNfZR-#I#4DT5GN4wS4Du zxgxwymSIyuC6jo(AD*?ZXcRO6-Yu0?_yE-Ehx%lz4ZEAiUZPEghrR`;mpRr5rj$cT z32|hCi;ThQ6^)dq^SmrS2608ELR2DZ)szw?hBZBx^_FYhRPjdPa@=@tzvgk>!7hjz zyUev}4&8TV%f{WJXYk6hjxXAILAQT7IVbt!th@3 z{-!O6U)5vh?_I7WhLJRD$-er@IBLV4b>B);VUSPKHH1WE1j9bYxb`=JL-u5+N7}++ z%T!Y%I}OTh?#7aOm~?GddU|HJ8Lw**^q--rojwJd^zMx=igHf!R+?yw^(?7ndDbv#g;X9rCxK2YyzIch9@1aNMP3IDe>lp z$~x_;x78)%YO>A|q|uKUV$w&dfws1T<@IHZ!vF@4-)I-4aitetO@kK!ZzXzyyg1jv zGKM_kBx1FdN1;b#py;6ic*|J3IB%rr>E~>$lr=12MJx&13qP*4+mfvXj_=c9aLrD9 zOKk07JMHrWDg!Dz9Bc;z>`%r+wnj-nYSKD-8g>otB|~qbjKE#9{K#{so-24-V-7u# z{+TmKjDliXl*h*((n5WyCjR|^;EP%5#z3DM?i{FkYom{)!sKS9UBfOX{N*elV*ru% zQIK2^HJpJ$_=_%KRbmA*j$(}iZLXfKyxDAk)}5r9OehJ-y(peby$mhgI2jsR%{Izso(j;kYg*EXk@(KfX#Z&DfmZ5WR)}Hb@d>~3+p8> zt&UC9_br@F^T0)q8Z)28Xf+3JC1CSdjqt}--KxJN5c+#a7cCxr zAeE|AP83FV+St>pWCzG)qne@@XIwp{v;XEJi(}ebo&aTQfaZ zTCHMr)-)$uFe~{U&!l$0wx8hI$7{D1Fw$pk(kSAU1RR@eEKddpNh!)uxl{V{9sgV8gJmZ^_hr_%f!~G^$c1Ksk$)pPVHJy(?n!nf}TNqHp+N z&H_g|{3<5Xq%@ycV}%WxO)7aC_xwH2B^#{IS{WiRsHiKkCZVo6?x#p?BYeLNgtZ)-kOW zSzc=owmEXPrV#Xqb5`3sJ=}=cRoZaN3<+It;U2+?tP*;Ij1G!1;Y*5jZdcyAV{i#P zk-pZF?u>Uq+T(S(u=q-2q<<{3TvrRGDYGtCHjNuO%7val4|)UzuN{u2TNe!Mc^Y@Q z&)YZ&ajW{XkhGd?d;MCBQv8*1l3GTjV{Rn0c{KnUHhOUvE*FJ4_X;+gz7kV7 zten)>E#BJt)Bb$m!0!~;s@1Ui1IerMl@r${X6oRbiQb!$L!0@Y#bWwh7mES$4bXem zT`g4mjYRgdV*7lwaGKcKql_b#X0_QTlN4cDX9OP(b5A`Sy(l8u`kFsBt>)-aEK*Pi z5}8}jO{uDHtJOce(SlS^N8j5s=?yE7SH&Q(LU9N9=Z=TskOE|{Rtsw-3|>wV(ug*M zm9B|+Z+1T+b?ra&cIilVT!MQcBo|0F4ckPxr4VnVF7+#R+&TK`zkbiYL(_m0( z%volq^}H-_8;^fI$=Af3ay2=pFjN|y;znPvk9mBmNs_qoW^UH18eR;S6dLkLsIfVd zQ@K2t^^dbZZki-z^BWQaJdcDj0ky=Oyo@aoFukN#1%-fxPO4A#sR_L-`k5bZrQaLu z#dw2oj$gYhJFYrr(Mfj>-elPD%Qy{r>S#n#RXuKB4a8lB-eV_NkL;HAQFX(HN{xgC4j^s$@?Kk5E*Aj)?(dT;-DAab9{^)E7?>42K~_UcUE$@|qtrMQKp zJI`#ayKYqXGwlYZ(AZ5Im>8KCOr9}BUJF&XzTu?bGc_TiF@N+bOH75Ec zkZ>y|`3Ov15NR4hV{;OE`%6*(7}Gg+8#l-3k9)*=KG6e*9QlbIcH3na6eNjwQkh=$ zQcP{42y%_MR7#-)n{$(dAeZn^^g6ujlI`ju(sR3jxlc}$Um_waC6Q`C-4`9Rw3it^ z2Ibr78@*@9r4Sr_h2DAk`w6^%julh!nlTUoqdah5@gi-z?&Eup&SXewR=4?-6WXGc0bqr~sN*SlBav?jgbQ@Adw3 z2vySM`RDErf{Rfr0x_ry{?{~=*`a@#t#3tIF1M&_xQ1T-Fi2v`PI&v8N9@)k@89Zi zA-*`($cd@1CO=uXi}LWs`sY9p_Kf^93-fODxykSwfxoK%dS~W0S~dC{B1G&r8k2nb zRLDQJz52`U|7pO@hh!2pQ2YFJ<~NhiqGAAraJH%be)jGWKTT zaS>+u=2-Y>nE$%ef4kl| zB35GE3Ev~P3;qwTGgiea@@gDjq5GGsP5Eb7u|Qai7j>J57!J|sCQjd#-Kvdf_P;Cf zaPVB-0lk4jinM}IFiR;th)%$qe<0T1I}u2c_IZlhIa8r^D9|cD}Vh)8}EY!rhO-H)H+Q!DDk>D)}Qa1e`d+`3=V{nh@SRsdLJTg9WFsh@w45vtP|5NuTsLBdD zc%C(5m@4X^z$G`2Y5v|nm1WeOI1CU3XJYp$>g0Qf9j@kA&b=$q7;`$uw~>Gp4J2J5EynT=D70?81qalh%hpN8P(VALHvjJ~4?fBAR&5~8YK6s)!)xW_)As8c1HEh(3Y9rmucRr35f zszLh68&t9U^B$1^{_a-&f4f75%#BfZGK=|G^n&>B(LndlpnyX1AJlIT(Iy1`ZPjx) zYDCqFc`XG#g2lbT8J2MR)%|b3aNT&VlQ$pQAUXPEB0M8vSwtK0Ei3v8#ebr7?Xmr5 zA`7-o`Y@Rz*GP$2{%DokdF25JW6mvt^#1ya{1rob)B%S&{CSjU;32htLQH>$of7m< z5o0k;7j(+-V8`#N>ji&5N{kU+KF#MR2Z{;Dh|}06H@cF)zC;yKuRU7Q#6x*@HYO#0 zdAhU9!4ujEQRHG6e|>#InG6zE(1ue);_TONeDRfCxJapQ7iS>Ko#f~e)S*1H>p18E zxZR(51-j?2n?nizE)VBO;~Eh>K9EXn3Q!CD>z{zJ!Jh7z;ct64Iucf-u%iVZeBC9< zymn++qey zuUm`E0gt;z(q()}u?6bH!m)tDW-tSi8w+%t5jXwD!|e9B?-tzxk+?b1m=075#u&GB^7Zy>2KW>qQ63vSy@yo(1*~r8mg!ak$Tvbd%APVXGtl&i2%Nqu}Q#` z4P&R+WJujto;yRc{OP@8ip4@}uSYn>m0}O4n;eE-EO0S4pWldgC)}(*u&1UjAJJ0S zI)s#F*j5L@Lc74?iMW&3%l#g*(+yN=0?Zw5oVjru^@CW0ATjBXUz1E({Gh$7w8hl; z$7A!%IEg3Ao?emh-{sgxzk}`9IvF{%42r0-i71C1Y7GzP2*xoXUhj5!#A$8P3C*QhnFJ5QIKo zO8cENjwh+;ZUGi#q`ShO!Ydrihoc+RhCOupZgcO1SOWy4QgcuYhQVBv;)fyQ5NkxQ z{;$lETg5ZVn`vg}Lc8bPCGKyq`wV%Ee2^F~-R+BxRbS+Fnp1tejcmkT?S$ih^uE;4 zOxGC0x0F2Wq4NJaUVg<=f^L?aGhri7tQdAw6E0nt*8-Pf=hBQMOjW+8nl9 zZF4|NnP?J1*6iM>JYQz8=o_PNKl2wD(`716yY+^BqzGlok68mmzzuF@g{rsDK8yC6 za@e=3udOoZiGZjT8B{3Y@3G+{x@q(VDclhHS`XPDO{!jS-o>d>Z~D#)L2@PtR1Pf6 zMfk+HJ2FJ7Ml>yv3t354RKD3_JCEWEAR(XWFi6;6Zrs=QRxvf6OIlBxM*(vkAPKM< z7~CY?^_KQmaj|j5IWw*Z6v|48z_;@laJATg9}~i;SHZ$pR6RaD{^=1V?uqnsK)z^~ zXR5UrFz*3#@cj9;+WVg%QJ+*(y+}$m)$Pz86?C5v&2t*%g)rEU++XqzMRoIqc0Zjn zbz6LLTroLd!q@Ydjr1=D#8NQx_r>2iq(S>&o}MuCZoBxM>&97IK3D(VE#e-58*43t z@CL_vO4l+poWGAJ0bjlfely`DhCI}&L>Sp{h z23ivp9@&||6b#M+Xi!RF`5T>?tfdpGf4F(gk^7?b6EG0DAcP_H-XK{6zqN&Ry;JmB ze0gSO)Gs~u@G~3RcBSnCEGx{`fq$jf^i>HTsKMp~x>y1=Q_W%Do4sdWvbu)4Y6n%m z?OF0DzW0N^UGLhACck@k6}pkU1@m(KmC^!{_%?kadp*@(vrlcM;u0(@e;bP>fA8F# zlR5CoL2OVoSz70LCvTk>n0AiXS9K(w`>^>4eNw9?HAXB_FN%1;q~!UqRg-Kf;6Sup zbN|8&cfLE-^eA{Y#|!ke@h9hxZ>~UTr|%QvaxYW30`E+Q9Wz&_NQ+PAtyQbdf%n0? zn;pb69~KV2$AlPgXm8GSWXe;mzh-87!>{ydNv`n|xn)c2oP4!LOW@LK2RCY!9yP{ky3j(3 zE4#Mg&rY0O!PbTXuiqCbgtRz@*xlO93?wxjx3qgFIT9CBYUxo~{eC+uv zsTbTf=R|K`-cMeCHWG{bG_Kt!*rVFFT{X&^qimpDXzydW2F`cwtTZ9ikQSHBm}Cj^nl8JHEucg}4+HSV4^ zifhCeTSkGeKAz4A4(?bw{BT!|ToFQ#4ef;mHHHuqeUm?kL>IQG$>slsU6;+-1c@*s#Y-olO@04pAP;VjY?4vF#|wNf^B4z* zGHiAU+1~Ngz1C-Q@Cxs~z{J;m_H;LLLFI--Q@6pC=6FSnWa-1@sEFPcLo&kpw#4*Y zL{jfAOj*&xnx0bGS=Sne_^=_hg4EUiHL&%@ZhFcyfM$9I-9A2#9ALR6cGS!1qhV@% zIgbE^U+G*6nF!nTgedF3w0kwEXrSU)xU8k3sJ}DMwWfbO#-E%(3I4Fz>3n??6=MqK zvIB=bdxY_$;YW9Jn}&7Hb@DpHsolQ*G{l4v+`_?VgDls4Z?7RXE29joHZdNUwmYQ0T)SsZFH zH{pFJ%FdAKIzJ#}d?|3>^_nrLWBkZyVAk|!UEdg974N|4nCueKqz)XVf{b;gv>El&>& znqlP|JUCX4jBUd3m!xYg(Ow3CDhlU3xDF}8SIVkK2t_5-vFsZ98?A5x>(lHCCzeKg zB=DKSgbNSw^CW;ogo>t-Qx$MiLCL>yVQ3=$vemJB|3)Hjw1=Xd6CPJq)A`mMVu%|4 zz96~PX2fjskPB^-S7Pd6Uv@;;DYlWsi%l%=OSpy{fJ5XEJYWYn`;Zs)(o{b8svhjp zaG|jBoJPNRna>MJE3ChTK!ngdPRH3kaBU>{!qBbX488I8u$|DZj_9?Fb-8QzyY3y3 zA=n9*6GBPFO)6WJfJOPMOyEop-Lg4ojZ}|h^+ATGA95wX_=mNSbH{R2ss*2}!djhj z^O|MRRW(Mnf`fmfHLWKuE<%n{($>N@wI1>4C=&Bm1{&Vvp+hbBnrE+IBzHio(j`Ru z`miI4Oh13_U6223K-xTt$}zUTLh_Y83V}^FJ;$#1E@5|GtHgj^lc<2E93c5-5Qdli zb&jk0412bs(aDO=;%xL|xI{6#>!C|lTyo0PK2>#fa&0=|`uMpQ zgu}DO;3TzW_`V@$Yg{~OTf?@%N9jj&twA2bbI*=sObAv}v-qR&k@sN5d%@;e*5llW z<4yapNiA`p&DzwLE|;@gJk8w%O7i8>c-9|GxITx1_%!f6%Zd*-dU$y$22R^4cfUxX z$y!jBS^c8Z5GVyOhY~?Dnp=o{3bK)KI!3rsi>L&?CJX9!1%^=D)r7fMpEM^mjNvk1 z505To9RKVl+a5BoNK9%#-?FzPjh<*P>~#^DwJeGMS(@k5thHcI>{?ASEw6@aZaW&o zXt)BOb}wj|4ABmHC{OtcM18u}yPKQfMK68k0-4CWU4`w{y*B8( z^v_1FH2;~IC*+!=7HvR#dSy?T%s51^L@WN9zN0(Rj<4hzB$(4#uLOQM`HV`u!r(|u z))NhQF!7mvkUN_9SerQlyY?sDpGiCkwU`%;m?*x@$h)4mQp2e;6$XsbmSHvXnm=s| zmSMSx$tm#2m8cRQBL39XjV@*T*CX=tNdJCXx*p?K^Y~*DXkTROHW}t3oq&S(z+{wE z#{T>CiKDc*;hKeljo`w>Es;iPYl_k zR55Rmq5-+y9h-?@H00eU6u zL-VLxKNi=N+a5YuQ#(T+tW+(xByd2f}=8e#A%BaBBSaAsvU|Q;MQgSAjXQ z>aF~^inoXl{3+|vViwATm5!NJJ(HbNd@3n{6#Ok6#R z4=HL{gYpuDJtV{Qby@;~Drz+rNk0yN^V{6rc>;CR@Dh-Mr~~ClaP)~PK@|1;ZVBJz zHz2JdvD-Z1h`NAc(#SSCcV!m9oAz zjJXmu0Wr6vg@@4gf?}lFR;d_6K}(F8?LSrL_68z5%!nCzEr9$>-qk76hJu8ObZCNc zvw&;yT>&2)PUf^{#MBtBRd87?mmZRSDfJyw^H_Bi^|9k=>?(5bBf`h8L|W)OYq2@p z{7}!dH2Y|E&Ty>>U2*6ZFS(NHRmHLbuCNmuv?GZTE;Q7~$pjnK(3<~P@O*3Cvsu$< zcbOaD-Urc9qO|a|8LZBZ=3&6@z<~<1B{a(G!OaE%7SP~gnOXhm`cjJu(PRckGxiwp z9*R$S0K^smb`{Ppq0~S48B1vfu(2sSpZC!|OcVT;SUBy74PqB515A1XE5+~uYpF!L z&oEY%1cbWkYz=`b_e31IbxKqbiA^4RLQtyO;8yMEC+HwUb2~YnT1FloyY}wTLW?5n&9lRQsnQX z2h&)@<=Ees)LP`~eKK}IoyrG(tR09qb@v#?bQtu)scd|QyIXL)IVd>s2>))t5v8Rb^ zz(YQ^B_@AF{#xoHOS2*~#`GPZ$OT&oq-Jcd1nK>;KH2j={SguNAdpA+L|oxI!F0S8 zbRcDfD#hR$?5UzpCz|-oH|WAcenS23uZl6oa*4zJ&t_tk;k+->2_^#nls+}!MPKkZ zLIq)V*E#4>;gry=4-x3;xLw4xD7G<~|Kby4K&zs_wQltmKLU zt}`yiU%H2(t7YJD8b@YsAHkSa)~ACTw{0}Em|&bU)~9Xj_VIi0=T^>V?1_1tCYu}M zkVa1E+P?a!ZE(uIuMUs1cIjnOGe{o zc3&}0+s&=>YB1Et@2Qxz z={En^m|jGmb#QwfSeYc)S(3aSlks_>CFW9yPwT5|Ko|(afwHPoRi#pB3-YS%HPai9 z^a9zjy;DQsm)>IQ6n38HwSBcF=^7?GiO!*2ze0zd*O4=Fi`YqLFwp*iu0+jb8M~00 zp3>0iHd2UY*~wy>wGm02JFn1ob6e7(qQElaqp-A1lZ&R*(5NQ^zwgCk#KBQVDE>%! zZ4_RnJ%>m-b7RkQG1+aWiir7y-Uf|5FK7@KKf4`E&T0oUPgx*WX&k|PN4ZpN{lSVA z5fpD)r#_OMxgg0h%WU-@Z1tQ((>e8GMlL)&6~oK1MsK2IBD}htFmfCY#KY;H;dHmx^DK zNOHBgs#maWt+g_4z)jQQ`PUT@$+7!5J{5U!MvE#_mUNaRdM)0FXt5)gAh0*W!$^&& z$->2`@*F!$s)n%A}eH)CkLl)XT!Ba7=zvz;xB zfO{R6IcAw%h1{88j4tikw-6c(1uK+l3-lAH&&_PoNsyz(BoboQR4KA^0MIrechVb0 z*`-QU4YWAZQgc3`V|FGSJ$0bAU9tHAQ|_Wsg5U znz!Cg^@q5E4uTe58%mAF$#$A?tjUWcojoH5WyaNuLs|rymoBAi#RzdN^OzrE5g29M zc9(i+PQDYUpwED<26T+qJeB)7=R1-;HBL={B&az(kSnO> z4#C1%@QQvSXv8EKoFAEH@Wp|%heEFVoSC^D5Vl-y9%8^Ra(FXz^z-dG z8wWo>mwCY(+(CT37$WR4-kPDi4-Hy+V_OsgG1N$un0d*M>UFl~HAbH&3AsM0ZHX3i z-jcZ1a@5$Q&9^9xb*l`>dz-YH?b4uU0)exyJd6zc3@_6vikDAJw`IQuzo;Ew&R?Yz z+4J1{h_gaTkKT3@^u1WvPbF8UNeizehw;p@GF?_9nD3btD5c}<+oOnZv6=={NPj*W zbn-rpGb$n0Ty}L*qJjCB`9p$tk`oSJw-S?(y+_#Qg=C)l<>(2f;@SOA>=QhD*Q8>Q z00xL6vwy(5jq|0;%orNS!jtzN!P~H(y5`5f(c+VNNF;|jg{^XPV<6MjU58*UEU%?T zuVY^U_t3>@?yMhQe{_HD9^z5TZmzLFRstwKkdZ0=6ibZMLM8tg7MMm23 zqG$R~*4~b7=HQY1V+}y z%D(;^ewHQ1L^jucN0yxlA6dNk6u*`ck3F-PR3EW3_E33*QO#3ZKUTxxFru)}wld)V z8E}}S*0L4~pk#-LZ{brWPRDG&SbIDF12&adg3Ujcc5$xyyy zPH1l@=?-93=rgCl?#L&=^S<~Hi%zg^b7nqsz#pSsj~uW@0sHPWqGctdu!YqZ?>-`U zPYkxb!%d4>=avlTw6C(N{UW!#yfD8b_&}pCbfXS|)(?KT6?K)m^?9|pr2(${(@74! zBT0;)0wVv$f&aGhs!wqetBw>epCiR|VXjbX;3i;Th{yz1)0)1|b&W^0F2821r9Ndr zsbN&uo(?>vorRn?VzL!>B=N)bAx|Uar0s&ghOqazC{W(39}!1UC6q!F?%$_~ubgjB z%%P+t`b~%+etd2TnY(s%_$o`mnGm_n?hf%PlO?KQq=}2N{+y9tCypt{_(36Z@{~ll zX~h?ghkVME&Nm+}cQ5L1B)C`Me0-R`4DeoxVfoRbIlgXWLTAZn;nWMqxvsSNQt^|B zA>f|Hbv7u;ceHg^E7#9dF)BP6CNm%-#bJR&Y1O-IIN{h(DX!G=mw!w_cSptg5wb## zsrjc*088b~6&vY#5?p6X#V*n_$2x}zJ3|oYygu!F!#6QwLWITv)!uTHwR)>DYWy`M z2z5X&M7le#jPcC$b=1UW0DlrNQ=0la?v*eD34wG8*S>kF=>3Owu$r4eBR|;PawUyTulrW+E7ZI^l#tAWnb=u z3R`ber@s(KLYR8tR!<(`SCdlh#{401*enmYmBK>lM)s?F=$s2Ok(Jnu_CZ5=`)N&> zp#(!y=5KuImmH}ps(EKbvBhVUPDye_Zx*%Nf2F*HqUZ&O*no^%Qi4( zcm9-UCsw694Kd}6)d%DjrSLRWd{Sa?aJ9@x7ynv@Qrym>JI7T|5Sh=lXV5X`MA4sN ztb6aVD{>md*t)zohNra&xN%42AdJ|}RSa^CjWR_imvJ;qn)03+bGpGiM6SliDkAdp z;|=vHAVOV(pV@(mP7Y;;xX-1Jr$ce$E?YzdeuhxC- zzfIyiNKR+aXwKfkRW){qL+X~i{7|g+4JNDi`8CNi3)sa_Zf3{FQO}CDD}sDCdUIQ{ zu~o_ii*z1}6$(`w(JNMC@h|0fjbs2?3=KnW5=Ix~L|wGWt4DC$F-0Y!kG@<8a`R>S zqz5V1RZ}=NSb}(QoE{DHP0zniI*nGqwAaW~I9kZ4wgOe1MFjZzk-x6uMT^9H#nT{F z=o+W&<*n3a^VK~>QEV~?ndV&wo*+#M8Os;&y%PEEU8 zt-oTS#Ze&A3#HJDeppv61mvIxt{@-db-;IQ&Kd^T$LGvw*) zxp2s_M7k@;sm=^=h#E!aq-MY-kd92-aX++%MSrU=d2U(2jPjH4CEbQC-vg;|zLHNi zb`3K1rcU;kQ+qcoWgluX$ANhCyyJbvu*Ib$3Jv3`3lzISd@go~s zDvpWVoO~WfWKT9awJpWonn8+_L)vM-Q4q?7a>}6bPuE4=3Zks}$K_Sn3Td!zC-J^t zSp?;re)M@C1S^mkT;yvI&vyk?XUAhA2tYT%=NmJZnmgM{{1>mWzkD!%5(9q$gJd#Q zG;bU}7PlG>EhS;wIc4=G-Rx^&L8S$7xL(k>+KFT>>mPBUmPlRRH|kRbn|g&RgLaZ- zf4X4XT(GUWY`9p8k_TVq z?=N59YZT>64bx;uIirFk1}Gv)y8jE)3)K?(Z?s$n+Fu7pXZ-FH)YEcvxLtE{nQm*W z`T6RFW!cF-;#qhZ z6ZvgIlbpT)Z$RS&^5Dg6L>1Zv{^J*zEeSB*@N?>KEKX^HkkDxz!_`vmQEhS=mFPN5 z;n@eJ&lE%&5C)G%EdraHU}6HQ@ssHq$TM&EwLw;=3HPU_g4H-3E;s1eLki zhTv+U+nCmpuZX5WfFJ}IZUdX{l-ClU(WxJT>edh9*z=2ZMI?Ddlce#DAg7;x*b{$g zc%qVyuMY&$%5g#sm~n!0vh+I{+z4dZsEoEB8WhSu49b6r1N7tLj~f}&UU-oWHZX)w zYV&UiNePqIahHP7zfA%}Yae>B>$VefSWJ}bc5C)-x)^AoIay>-1#Jf1*H~Z9rEZO02+xARZ`Y+6mA#!U+^-4KPin; zdGFu}RfH%wnICJ~{~l)c*KLUYf~67mh&33ugmtxC&`nWizjh3M z83BZ#7-8Hg!h#1!ekuT*1~=+}k!yuelh_r`gFZm%iKw8dwf(%vCFOU+3Rnn@@CApD zERJ|-LQ<`jcmE<#jT500d4N#SDJYrxK(XOq%&4;^r?6Fh@A!eupH-!?ya~zT)+@AS z%(JgWZH6n7SFgAkT8dEZ{CbG6mzLh3b6C-{=z6)VVdJ0uq6;j{dXPNMz+gUiF}o2L zQ2?nM(`u(r+uo}vHe-b6KbX>Ug{`D;2U5)4v3(SJS;~cEIiBPWUob`ca0OgKmdyB|yg zdMtLZ`V+n}s!P9y5CNhI+>1-j2-yA)u!bvUtDH~^ma));{(f&@4YOri*ZOwURbugS zjtfnU)4==P3b-1XTUyk~yD4)_#cRQ8eVR+wXR^&3@m`%;-bbD+H?kY6ne2evr}+gt z`MEW+if?b=!SRzx2@x+!#Euo{z#=(|`*EwD^`2i}j*k16CWmk2s?vIXJND6yIIlM! zOHG`F&isO|qh4&Yo8QR`OyaFcdjbTc>QG5j#v8RP398Qg890Y7l{PnuVh@`p030EF zQ|5U=+gkQF64aYM19BHpsn}$yrM5suMixH~77(SUWy+p5^pIjq_Hi^(B2HW+mEf;} zFc5Mz&Y@0gl8AwG+A(#=;Q$VLdaD%1wHErdWD986+z1OgBkTzzs0kSZ1_r=N?JQO@O zFgz35s+xtegN(Xf|IC~+;(Jo4_&1{v8QYAKY;6W7J;ya zs0F{1GA%slmsE)J_W#jMxEQ&MaM^O@v;hDO7vJIu((#~Zho}h1PGz$8ikbyO(zTsU zlOLKIgq7Q$md-m9BqVFetM6C<6|v|p@!t$u$1F7Ve7%+jFypPqW-e*g)*bS>McKKt zD{4G5Pl9UINoy3M`8q6q&+{>-U7eZAx)?os+>|KDx$kUVfGhQADRzlP~Ek7V*yz8&=fZDIoJguFv0; z3Za%b4K+({OK#Fs#?Yjjua{WN&Zl z31yUIQF%!!o65|TU{$uwSf5Vtd14q_yLo{F-*X3YoYg^y?|pF^sa>sh!qU(ArizW) zfJ=6dB|)!wrlA15g|3^kHq&kC7hvnR9Y}fG5Zh>xQzv1X%%>YGt+S;S2=geSc48Tn zW3TeY;!_ONJMU%Md3nNB`m5cvMgsZln{X?h$b&lB;60XHpVo^Kv+w-tFp>><@0*~Q zG`c9VQ=1OGhWLGj0ha>H{T)kR>D%IG_ROApU%SlhG$cv;TM#{5;f|mATx7>%4t!OogEnk+{>DU6^*d*N>@GlKhF?Y4|MoXrYlaoKqwrF4K6vj3`)skCmpu zdgkcV#9LIX={-tgIf+(&uIL#3TC(O-qIs8wIXQE}WAIRJC;;A8P5CQ;JS2X6NI%YpuxAp_yQXDw6@u|?)6ZV!`RG|H=ZACzIrTncOdU@!Cw6b$WnAS; zD>`sgRdblnLb-GpS2tSf#kj5}LW!E!#a{}7CCc9O#)4N;;eEz)7@>*HT0EhSmMkwC zVklY|-bxP*FwC5s-rWS|rJhKqRgG&^XF|Y8vHHWaESnEELqId{ZXv61LeXdQj>kGW z-I!wyN#urCT2{^<+{^e`2M)r2?WQpc^Rfm};MhREZ&zusiHrF>pI=TN#62TJEJyWk5- z*nQs!uyH=ympeX{;7oR{F&#|&#!^JFyb{c#{F+hqbt~>fy zt=-;nYc=dT{UlFyB~4%cWB__z2MOc-WbP`joR7~Pctriiv&@Q|9KKBzFwBb=p zsof~HSko^$_LS|I$9%O(OFZYa-GcOn)D&tZo@M)lGTfnat~86j<8#qleP)kQ8hJMC zTAWlr^0s3ef9;y5*YQkWeqvITLB0mC-v8q1nZ!Vvua)(=-MJ)37M|{?=11~m8Qao` z(=1&$7mq+pSDnKGUKlbZhws9o)r1@42JV|1S7Mf%$*nEOpx&JQ zL5`#JaYe*tK*#qcH`jj2E&7!vyH?$_AD2zcdoFEdlV7RjS!T#(KANwH8t^c&wP zF0H;XNxB&nx;C4&)$RTrXVD;iA;UbfuKA-_*4cN2%$d_};#qn>t+um8ZD%fO@zUsp z3uAs2sg@k&5H{hHwclvs(mv2y*5=2une!Lo;)R&iaoSE2uiCbS4khQxr}wS#XiJ|U zZ4!UIkW!|pQW~bAog6!VpS6F~$e8|QX@{Dsa;nG8c*(_OSTdW_C*CAA!9~?YU8|f= zB>3fu;hq*zC@Sde`Yxzwpsld*hQZU}8RR@!N9}|hPD4N&QBr5gL1B}4l6XfYA}obnwkGV**QBT3Z7AA))@oMU?`@VUZRpjmW!730I%{UDJKofgD>RBMY%&QOno>2IHYa%W5_n{-ntBsingkN?q}7^MZ@)s7 z-rwxHyjLq}WD1&S5vi+&6B(cSzrT_D1L)enqnwZnFscoCy*Ijd{gU)Aj*^6gQ(VoM zZJM0%FDOLOA3x#mJT1&W=~quJ&ATiT6>p-|!O9KhD>=pflbyDnIM;OV{!U+HCFqM+ z_Qn6pQP6jFw(vtGuSjLRfj1C+|Khb6t~2OAnPuxqeNESO-eZ&c90GZ~T<@27U~yX3 z>0vCY)0_NVyJBMIwYmfwj-{0^6=%@6-s~Uxyk?nsCSCEx5T@#1JJ!q$_5?XfC!W1=EOV zF$}t01y`HonUa<}B|}11(mSi&rfn*=qt>F}@RNUiBjXwz^ZC#ZOm96T-QOR^nI;|X zCv=dV5r23GpE64>%HTm`jDQJ4Q2B+a@YYbNvWWBKh?5<#n7liWG{KOFX978ycs(xj zu5jrCEc3P`eK>_cX8k-@jvFPwk-KXkm9b-3$%t6m9OQzYs6`dIu%@~SujAO1NI8#` z3f-x&gp^JK%QEaDgN+0zGBSZPC$Y0^be|(qvmH31g)9^XyrhcU$J3%gRy0=IsABc4MV7nxOyIUYU%Zvcj$s- zrTB*g;Nmi1_om7Qfo5a`QLJIxmBkd!?dgLbM*T!ewa@sVhE3XV`;v}8pHDSfPp<)T zrpGt!wBIGfK4Ij(eI{41SnpJwB;7;$%r6Y3DLx&9gB8y1uSRxi4{$GCqD(&X|BWUp zF+>^dtmExGmSeBWT%6xsHfPQhyr#zkCAqf2ok9yP$8;P{EUJrX+koB)?dpyjeW;t~ zb}d)=y0fTg;j4Xu^99!>WoLokIj)q`{iDhUsoukdAUBhKW~J&HQZr8wBR@{vBuZ_8 zO&kgd7Akz{<--&SJK?paWA>SiO+mwDK*I#Hj1R@0e9dj?^JpA_V@$at+y&-wh@ZzD z0XQ+eSXRxq&QeTb2ZC!;;ojIjO}oZx$)&vhEO=$j^S*%xyEZJ##5w|52LFq_w+w2l z?e|7$DbN;aDTU&+#U*HhODV;@I7y(mCP;9t+(?n&uEi~(Sa4{I7b}+F?(P&?xZm`C z_CD`(X7-uc=fj!ve%LelaILJBl`CsjuJxCn)C4Gr(9u`e4;u+3`);&)_Gj&54v5%6 z5TG-RT0E2r&j2Ek$W6#ZMn?-b2g|GtZ4bLsbcCKR6i5U9vig#Im7%Ga$I~;>rQp?} zPrZk<1A>MfO5eX6eL?63u?6(h4>&%3lFq;|C3Or+DTCVK3bEe_w%E@&^qiJ3$o{9NFsT z_JH;GEd=k}15@j}3&r%Vn*Cr@dJ#_`kK)e+76L}se1mvgOPZ$1%S!hOLOvyO3Y^)S z(j`gzS5Jfs-b!Wo2r(n-X{c99O((N>FF^6BVGOhv;YP(qU+G|#zAi0+CMt-^yemKO zV{1p9Wx=?=l`Z=NS_;KK$T!`|Xl?VF?#UU6<2pQGVKfqO%B zv+(E!?3g4C6+qq8luZDDW61O-j2nJW-bT^wldt?UG>3X|W_+Cb6wSdCuCnNjhY!)T z;{P?S09Uq$@(?up7_(fLap2_Zrnb=dDYj*Yy>l=VdlUUf63}RMJXdF7d%Afx+K#V= z3seH!DrA6NMLyEAR2hU4g_}4GV5fbpfVT>pNjiV_v%$5V?$mXjCH0sAQ=$(N&;3ki zmaOPE^#)jv`QLH~|p`|xhmn3H6j!2M#f{%?Yx-RLY*mQ)740VWk5 zC21wq#H_hU08&%;0FO>1`%l{KzqsY}9s|CSLfj0)uZ#bKMf)!jUlEOTjYoltjLHM6 zj+ukv7|VZEh7hT@kg)BSTX!h?)o7_6G3FI@fuUR{eHsO2^ia8-8fN)v?LIoqki?K^7$h3j0|wJ%G~NcLKkb1>5WrOAMxl)j9av9st4;^t|NA9 z8WU%6cfSl98HF^WS4FxT-@9YkT*qw{zpRxB8$_1^exj?3wd%fxYBHD|=!-pdCst)G z3&0HYSR})=3ts8C9GpHejj~gXdD^D@!r7FxeqtP&=Ijact1)&MUNcE zZEeQz=@mwK@*WcVW;M+0tq7s0k>%~pJ=f)}8L)fHYeAuU^ag@MX0Viy?9?*nE-7JBY8C!d+S|S#ydy~ zPt&xxs}^6*i?cf5c-)F)SsAJmEH zG6;9^OgjFm)%z?SQ+V7Yo+@k&<7W%4g_ARcy}q&|7fQ{SHBIf9 zpH5(1HN=%UbMfBH>hsICdziGd! z7Wvka_%~TH6en10ba-@ll`trV+H=cs?mW567I09?AGa%|SLGHrJr(aRzmd+`1Hw52 z-g#)dw68ERL(A3!XtpC5L>wy`LM6Py}YMSWjh#uGoY(km9lLN5T+Q?AKOBlC~G zi6<9Dtr!bTMnn=Bs%rG1bfI)21W7BR>+0zvloJ37SN6uY4?W1lJUucqfRWImUxDX4 z;Jxkn{!Hg)OnSvQvBp>>-{%3bhw|`Z9fGJ7x=(We6Q`pgf)S3G!w|*nK4z(cM#H8R zPwF}YPsP1xv`2I(4Jg`n5&PLQUR%eTj1IYi7;infI4cu109z3Hm9DyFmveu7{Y5Uf;e&fWb zOc_YW?{=)($6Y!R5O5l~!l5Cs)EiXIYt7cV7H=UJ&9$xk<;lYq#Lz z3i)yF{Di)#gPwHBWQG3_@00(y;uF&O-M6?@lY!Y``c*+7-P4JGD`^W>tz{Il`fe-# z*NY^T&{D=4zUc6;K=_^&vl(8|!vB%2v`9d~0h0>vpGr3opH=_dpOuuXNdlzZ&dd}^ zS+y?sssN4$|NfCIcz);i$PSVHbG*F2&Hqg>|HB^y??26{?@vbyHiMVtmdpOPvAyZj zKQ0Lc*DTnFrvLaQ96-`pMchAriMy4J5vV6?$BROMu0+g3dFLWPGcOeg>zOBvD*J$a zl+1!3%PK{Owv4>8`&_ITuU|97-SR9?7ECUX7)mLxe_t`S6zSjV3&SOKkS65tzl!cp zt}-huNPLK(JJ55t3f~+vnW?FMZCUo=shiJ4|NGs?-jY0v93KxdVDmxTX%8ebwg@kX*=@3t^iLF@BDIdkTrw-2LAAINa2iE+93Qd z*hONO+40^!9WyICtg)flZJuV{cnWz!@p2*{UhH;{bx_h~gJF{tiZ-I;{oQ%hwS|6N+SK#$(mor7x8;g$U4wyW z6G-5B(Q8)HTmrH1Dp&eCgOAGz8jwSCAZkFsYWA(<`0sGcMbyZ=(t>rnR2s+5ti;#ar?MIzn-2db8%vq=ui%OzKZMsysnX_lW7Q zyg`FruWWc97+J2d*1u$Z!a1~82 zMgboPpdRMo%buREG$XRdyzgK%|4JWpO^~@7Bqv~QwU}&PU=3vWG35~r$68w?Z?c%K z-|ANCRtSn70?2Gv%1Vx9Tdil%ak5!`Je+1RcIZxL(AQ7(dX|7X-+Z1JJl)!CBHQY4 z_v-!SZMyXuhf0RY>}9N`j7$Kbbd7WDXYrVy(`$XUX@?%EV&M*3^xL>4lL6E~wXkQk z8T6B~S*bgBI=@X%N*=7{`T zSvGNXo{x0a=hj7nWt{3$H@c{cdYSD~2~9H_{~6*i{#(^#HStRth*_mqj+vpqFCHQY zgZ7SvwvSe_&K=s4vC-kcnSv39Jj~|z@EsQ`w2^naKxtge(G`w6)Quums3tFi84U11 z=KI3<40{XgC%jDJ%{Cr+Q_%5oN-xa$L#PW}nyVH~Ph3Z{#>l?v)2~ljrG@PGitmN@ zu8q6*c|{hY)g*}Ep8>?B~#6rAk5r6V5s&$(8m5AwEjVbk-ExBxVx`dhW215@IKkgfqKxOBpSOt*XZs( zu)HsYw2~f=^p%dzGpROkE?=#0$SksLVZC1J^s!lN=3r_RUhpm~K}0Hzv7Wup-?6mQ z^!W)82VII6(c!C~L9S;rw!G1);wp~Q!KbMN)`)&ukXS#$?P4O%lR`!Ja^X-@XrlG`dZCp=2?!0#F_MSV{uBt1{WX}U|n;kSX zCb?LjSCLEE=T6I+i~^rHu8ZBuwkrpnBk9&)?}dm0UyY}7KEkAD3$H_XxjsE?I49n# zi*X?uayl8KK|ArbGV_{iQju%0AWt(2NCBXxwQ#2rRK;J)dajeg$xYigjUY?*+L7#{ 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%0D?RhZe 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 literal 0 HcmV?d00001 diff --git a/install_requirements.bat b/install_requirements.bat index d304e90..4e1043d 100644 --- a/install_requirements.bat +++ b/install_requirements.bat @@ -2,15 +2,21 @@ set "python_exec=..\..\..\python_embeded\python.exe" set "repair_dependency_txt=%~dp0\repair_dependency_list.txt" +set "requirements_txt=%~dp0\requirements.txt" echo Installing with ComfyUI Portable echo . echo Install whl... %python_exec% -s -m pip install ./whl/docopt-0.6.2-py2.py3-none-any.whl +%python_exec% -s -m pip install ./whl/hydra_core-1.3.2-py3-none-any.whl echo . echo Install requirement.txt... -%python_exec% -s -m pip install -r ./requirements.txt + +for /f "delims=" %%i in (%requirements_txt%) do ( + %python_exec% -s -m pip install "%%i" + ) + echo . echo Fixing Dependency Package... diff --git a/install_requirements_aki.bat b/install_requirements_aki.bat index 33027c6..09bc678 100644 --- a/install_requirements_aki.bat +++ b/install_requirements_aki.bat @@ -2,15 +2,19 @@ set "python_exec=..\..\python\python.exe" set "repair_dependency_txt=%~dp0\repair_dependency_list.txt" +set "requirements_txt=%~dp0\requirements.txt" echo Installing with ComfyUI Portable echo . echo Install whl... %python_exec% -s -m pip install ./whl/docopt-0.6.2-py2.py3-none-any.whl +%python_exec% -s -m pip install ./whl/hydra_core-1.3.2-py3-none-any.whl echo . echo Install requirement.txt... -%python_exec% -s -m pip install -r ./requirements.txt +for /f "delims=" %%i in (%requirements_txt%) do ( + %python_exec% -s -m pip install "%%i" + ) echo . echo Fixing Dependency Package... diff --git a/py/evf-sam/__init__.py b/py/evf-sam/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/py/evf-sam/inference.py b/py/evf-sam/inference.py new file mode 100644 index 0000000..794963e --- /dev/null +++ b/py/evf-sam/inference.py @@ -0,0 +1,145 @@ +import os +import sys +from PIL import Image +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from torchvision import transforms +from torchvision.transforms.functional import InterpolationMode +from transformers import AutoTokenizer, BitsAndBytesConfig +from model.segment_anything.utils.transforms import ResizeLongestSide + +def sam_preprocess( + x: np.ndarray, + pixel_mean=torch.Tensor([123.675, 116.28, 103.53]).view(-1, 1, 1), + pixel_std=torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1), + img_size=1024, + model_type="ori") -> torch.Tensor: + ''' + preprocess of Segment Anything Model, including scaling, normalization and padding. + preprocess differs between SAM and Effi-SAM, where Effi-SAM use no padding. + input: ndarray + output: torch.Tensor + ''' + assert img_size==1024, \ + "both SAM and Effi-SAM receive images of size 1024^2, don't change this setting unless you're sure that your employed model works well with another size." + x = ResizeLongestSide(img_size).apply_image(x) + resize_shape = x.shape[:2] + x = torch.from_numpy(x).permute(2,0,1).contiguous() + + # Normalize colors + x = (x - pixel_mean) / pixel_std + if model_type=="effi" or model_type=="sam2": + x = F.interpolate(x.unsqueeze(0), (img_size, img_size), mode="bilinear").squeeze(0) + else: + # Pad + h, w = x.shape[-2:] + padh = img_size - h + padw = img_size - w + x = F.pad(x, (0, padw, 0, padh)) + return x, resize_shape + +def beit3_preprocess(x: np.ndarray, img_size=224) -> torch.Tensor: + ''' + preprocess for BEIT-3 model. + input: ndarray + output: torch.Tensor + ''' + beit_preprocess = transforms.Compose([ + transforms.ToTensor(), + transforms.Resize((img_size, img_size), interpolation=InterpolationMode.BICUBIC), + transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) + ]) + return beit_preprocess(x) + +def init_models(model_path:str, model_type:str, precision:str, load_in_bit:int=16): + tokenizer = AutoTokenizer.from_pretrained( + model_path, + padding_side="right", + use_fast=False, + ) + + torch_dtype = torch.float32 + if precision == "bf16": + torch_dtype = torch.bfloat16 + elif precision == "fp16": + torch_dtype = torch.half + + kwargs = {"torch_dtype": torch_dtype} + + if load_in_bit==4: + kwargs.update( + { + "torch_dtype": torch.half, + "quantization_config": BitsAndBytesConfig( + llm_int8_skip_modules=["visual_model"], + load_in_4bit=True, + bnb_4bit_compute_dtype=torch.float16, + bnb_4bit_use_double_quant=True, + bnb_4bit_quant_type="nf4", + ), + } + ) + elif load_in_bit==8: + kwargs.update( + { + "torch_dtype": torch.half, + "quantization_config": BitsAndBytesConfig( + llm_int8_skip_modules=["visual_model"], + load_in_8bit=True, + ), + } + ) + + if model_type=="ori": + from model.evf_sam import EvfSamModel + model = EvfSamModel.from_pretrained( + model_path, low_cpu_mem_usage=True, **kwargs + ) + elif model_type=="effi": + from model.evf_effisam import EvfEffiSamModel + model = EvfEffiSamModel.from_pretrained( + model_path, low_cpu_mem_usage=True, **kwargs + ) + elif model_type=="sam2": + from model.evf_sam2 import EvfSam2Model + model = EvfSam2Model.from_pretrained( + model_path, low_cpu_mem_usage=True, **kwargs + ) + + if load_in_bit > 8 and torch.cuda.is_available(): + model = model.cuda() + model.eval() + + return tokenizer, model + +def evf_sam_main(model_path:str, model_type:str, precision:str, load_in_bit:int, image:Image, prompt:str, ): + + image_size = 224 + # initialize model and tokenizer + tokenizer, model = init_models(model_path, model_type, precision, load_in_bit) + + # preprocess + image_np = np.asarray(image) + image_np = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB) + original_size_list = [image_np.shape[:2]] + image_beit = beit3_preprocess(image_np, image_size).to(dtype=model.dtype, device=model.device) + image_sam, resize_shape = sam_preprocess(image_np, model_type=model_type) + image_sam = image_sam.to(dtype=model.dtype, device=model.device) + input_ids = tokenizer(prompt, return_tensors="pt")["input_ids"].to(device=model.device) + + # infer + pred_mask = model.inference( + image_sam.unsqueeze(0), + image_beit.unsqueeze(0), + input_ids, + resize_list=[resize_shape], + original_size_list=original_size_list, + ) + + pred_mask = pred_mask.detach().cpu().numpy()[0] + pred_mask = (pred_mask > 0).astype(np.uint8) * 255 + out_put_image = Image.fromarray(pred_mask.squeeze(), mode="L") + + return out_put_image \ No newline at end of file diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/__init__.py b/py/evf-sam/model/EfficientSAM/efficient_sam/__init__.py new file mode 100644 index 0000000..22a2d29 --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +from .build_efficient_sam import ( + build_efficient_sam_vitt, + build_efficient_sam_vits, +) diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/build_efficient_sam.py b/py/evf-sam/model/EfficientSAM/efficient_sam/build_efficient_sam.py new file mode 100644 index 0000000..b5a030d --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/build_efficient_sam.py @@ -0,0 +1,22 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from .efficient_sam import build_efficient_sam + +def build_efficient_sam_vitt(checkpoint=None): + return build_efficient_sam( + encoder_patch_embed_dim=192, + encoder_num_heads=3, + checkpoint=checkpoint, + ).eval() + + +def build_efficient_sam_vits(checkpoint=None): + return build_efficient_sam( + encoder_patch_embed_dim=384, + encoder_num_heads=6, + checkpoint=checkpoint, + ).eval() diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam.py b/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam.py new file mode 100644 index 0000000..a4ad17d --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam.py @@ -0,0 +1,306 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from typing import Any, List, Tuple, Type + +import torch +import torch.nn.functional as F + +from torch import nn, Tensor + +from .efficient_sam_decoder import MaskDecoder, PromptEncoder +from .efficient_sam_encoder import ImageEncoderViT +from .two_way_transformer import TwoWayAttentionBlock, TwoWayTransformer + +class EfficientSam(nn.Module): + mask_threshold: float = 0.0 + image_format: str = "RGB" + + def __init__( + self, + image_encoder: ImageEncoderViT, + prompt_encoder: PromptEncoder, + decoder_max_num_input_points: int, + mask_decoder: MaskDecoder, + pixel_mean: List[float] = [0.485, 0.456, 0.406], + pixel_std: List[float] = [0.229, 0.224, 0.225], + ) -> None: + """ + SAM predicts object masks from an image and input prompts. + + Arguments: + image_encoder (ImageEncoderViT): The backbone used to encode the + image into image embeddings that allow for efficient mask prediction. + prompt_encoder (PromptEncoder): Encodes various types of input prompts. + mask_decoder (MaskDecoder): Predicts masks from the image embeddings + and encoded prompts. + pixel_mean (list(float)): Mean values for normalizing pixels in the input image. + pixel_std (list(float)): Std values for normalizing pixels in the input image. + """ + super().__init__() + self.image_encoder = image_encoder + self.prompt_encoder = prompt_encoder + self.decoder_max_num_input_points = decoder_max_num_input_points + self.mask_decoder = mask_decoder + self.register_buffer( + "pixel_mean", torch.Tensor(pixel_mean).view(1, 3, 1, 1), False + ) + self.register_buffer( + "pixel_std", torch.Tensor(pixel_std).view(1, 3, 1, 1), False + ) + + @torch.jit.export + def predict_masks( + self, + image_embeddings: torch.Tensor, + batched_points: torch.Tensor, + batched_point_labels: torch.Tensor, + multimask_output: bool, + input_h: int, + input_w: int, + output_h: int = -1, + output_w: int = -1, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Predicts masks given image embeddings and prompts. This only runs the decoder. + + Arguments: + image_embeddings: A tensor of shape [B, C, H, W] or [B*max_num_queries, C, H, W] + batched_points: A tensor of shape [B, max_num_queries, num_pts, 2] + batched_point_labels: A tensor of shape [B, max_num_queries, num_pts] + Returns: + A tuple of two tensors: + low_res_mask: A tensor of shape [B, max_num_queries, 256, 256] of predicted masks + iou_predictions: A tensor of shape [B, max_num_queries] of estimated IOU scores + """ + + batch_size, max_num_queries, num_pts, _ = batched_points.shape + num_pts = batched_points.shape[2] + rescaled_batched_points = self.get_rescaled_pts(batched_points, input_h, input_w) + + if num_pts > self.decoder_max_num_input_points: + rescaled_batched_points = rescaled_batched_points[ + :, :, : self.decoder_max_num_input_points, : + ] + batched_point_labels = batched_point_labels[ + :, :, : self.decoder_max_num_input_points + ] + elif num_pts < self.decoder_max_num_input_points: + rescaled_batched_points = F.pad( + rescaled_batched_points, + (0, 0, 0, self.decoder_max_num_input_points - num_pts), + value=-1.0, + ) + batched_point_labels = F.pad( + batched_point_labels, + (0, self.decoder_max_num_input_points - num_pts), + value=-1.0, + ) + + sparse_embeddings = self.prompt_encoder( + rescaled_batched_points.reshape( + batch_size * max_num_queries, self.decoder_max_num_input_points, 2 + ), + batched_point_labels.reshape( + batch_size * max_num_queries, self.decoder_max_num_input_points + ), + ) + + sparse_embeddings = sparse_embeddings.view( + batch_size, + max_num_queries, + sparse_embeddings.shape[1], + sparse_embeddings.shape[2], + ) + low_res_masks, iou_predictions = self.mask_decoder( + image_embeddings, + self.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + multimask_output=multimask_output, + ) + _, num_predictions, low_res_size, _ = low_res_masks.shape + + if output_w > 0 and output_h > 0: + output_masks = F.interpolate( + low_res_masks, (output_h, output_w), mode="bicubic" + ) + output_masks = torch.reshape( + output_masks, + (batch_size, max_num_queries, num_predictions, output_h, output_w), + ) + else: + output_masks = torch.reshape( + low_res_masks, + ( + batch_size, + max_num_queries, + num_predictions, + low_res_size, + low_res_size, + ), + ) + iou_predictions = torch.reshape( + iou_predictions, (batch_size, max_num_queries, num_predictions) + ) + return output_masks, iou_predictions + + def get_rescaled_pts(self, batched_points: torch.Tensor, input_h: int, input_w: int): + return torch.stack( + [ + torch.where( + batched_points[..., 0] >= 0, + batched_points[..., 0] * self.image_encoder.img_size / input_w, + -1.0, + ), + torch.where( + batched_points[..., 1] >= 0, + batched_points[..., 1] * self.image_encoder.img_size / input_h, + -1.0, + ), + ], + dim=-1, + ) + + @torch.jit.export + def get_image_embeddings(self, batched_images) -> torch.Tensor: + """ + Predicts masks end-to-end from provided images and prompts. + If prompts are not known in advance, using SamPredictor is + recommended over calling the model directly. + + Arguments: + batched_images: A tensor of shape [B, 3, H, W] + Returns: + List of image embeddings each of of shape [B, C(i), H(i), W(i)]. + The last embedding corresponds to the final layer. + """ + batched_images = self.preprocess(batched_images) + return self.image_encoder(batched_images) + + def forward( + self, + batched_images: torch.Tensor, + batched_points: torch.Tensor, + batched_point_labels: torch.Tensor, + scale_to_original_image_size: bool = True, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Predicts masks end-to-end from provided images and prompts. + If prompts are not known in advance, using SamPredictor is + recommended over calling the model directly. + + Arguments: + batched_images: A tensor of shape [B, 3, H, W] + batched_points: A tensor of shape [B, num_queries, max_num_pts, 2] + batched_point_labels: A tensor of shape [B, num_queries, max_num_pts] + + Returns: + A list tuples of two tensors where the ith element is by considering the first i+1 points. + low_res_mask: A tensor of shape [B, 256, 256] of predicted masks + iou_predictions: A tensor of shape [B, max_num_queries] of estimated IOU scores + """ + batch_size, _, input_h, input_w = batched_images.shape + image_embeddings = self.get_image_embeddings(batched_images) + return self.predict_masks( + image_embeddings, + batched_points, + batched_point_labels, + multimask_output=True, + input_h=input_h, + input_w=input_w, + output_h=input_h if scale_to_original_image_size else -1, + output_w=input_w if scale_to_original_image_size else -1, + ) + + def preprocess(self, x: torch.Tensor) -> torch.Tensor: + """Normalize pixel values and pad to a square input.""" + if ( + x.shape[2] != self.image_encoder.img_size + or x.shape[3] != self.image_encoder.img_size + ): + x = F.interpolate( + x, + (self.image_encoder.img_size, self.image_encoder.img_size), + mode="bilinear", + ) + return (x - self.pixel_mean) / self.pixel_std + + +def build_efficient_sam(encoder_patch_embed_dim, encoder_num_heads, checkpoint=None): + img_size = 1024 + encoder_patch_size = 16 + encoder_depth = 12 + encoder_mlp_ratio = 4.0 + encoder_neck_dims = [256, 256] + decoder_max_num_input_points = 6 + decoder_transformer_depth = 2 + decoder_transformer_mlp_dim = 2048 + decoder_num_heads = 8 + decoder_upscaling_layer_dims = [64, 32] + num_multimask_outputs = 3 + iou_head_depth = 3 + iou_head_hidden_dim = 256 + activation = "gelu" + normalization_type = "layer_norm" + normalize_before_activation = False + + assert activation == "relu" or activation == "gelu" + if activation == "relu": + activation_fn = nn.ReLU + else: + activation_fn = nn.GELU + + image_encoder = ImageEncoderViT( + img_size=img_size, + patch_size=encoder_patch_size, + in_chans=3, + patch_embed_dim=encoder_patch_embed_dim, + normalization_type=normalization_type, + depth=encoder_depth, + num_heads=encoder_num_heads, + mlp_ratio=encoder_mlp_ratio, + neck_dims=encoder_neck_dims, + act_layer=activation_fn, + ) + + image_embedding_size = image_encoder.image_embedding_size + encoder_transformer_output_dim = image_encoder.transformer_output_dim + + sam = EfficientSam( + image_encoder=image_encoder, + prompt_encoder=PromptEncoder( + embed_dim=encoder_transformer_output_dim, + image_embedding_size=(image_embedding_size, image_embedding_size), + input_image_size=(img_size, img_size), + ), + decoder_max_num_input_points=decoder_max_num_input_points, + mask_decoder=MaskDecoder( + transformer_dim=encoder_transformer_output_dim, + transformer=TwoWayTransformer( + depth=decoder_transformer_depth, + embedding_dim=encoder_transformer_output_dim, + num_heads=decoder_num_heads, + mlp_dim=decoder_transformer_mlp_dim, + activation=activation_fn, + normalize_before_activation=normalize_before_activation, + ), + num_multimask_outputs=num_multimask_outputs, + activation=activation_fn, + normalization_type=normalization_type, + normalize_before_activation=normalize_before_activation, + iou_head_depth=iou_head_depth - 1, + iou_head_hidden_dim=iou_head_hidden_dim, + upscaling_layer_dims=decoder_upscaling_layer_dims, + ), + pixel_mean=[0.485, 0.456, 0.406], + pixel_std=[0.229, 0.224, 0.225], + ) + if checkpoint is not None: + with open(checkpoint, "rb") as f: + state_dict = torch.load(f, map_location="cpu") + sam.load_state_dict(state_dict["model"]) + return sam diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_decoder.py b/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_decoder.py new file mode 100644 index 0000000..909605d --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_decoder.py @@ -0,0 +1,318 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import List, Tuple, Type + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .mlp import MLPBlock + + +class PromptEncoder(nn.Module): + def __init__( + self, + embed_dim: int, + image_embedding_size: Tuple[int, int], + input_image_size: Tuple[int, int], + ) -> None: + """ + Encodes prompts for input to SAM's mask decoder. + + Arguments: + embed_dim (int): The prompts' embedding dimension + image_embedding_size (tuple(int, int)): The spatial size of the + image embedding, as (H, W). + input_image_size (int): The padded size of the image as input + to the image encoder, as (H, W). + """ + super().__init__() + self.embed_dim = embed_dim + self.input_image_size = input_image_size + self.image_embedding_size = image_embedding_size + self.pe_layer = PositionEmbeddingRandom(embed_dim // 2) + self.invalid_points = nn.Embedding(1, embed_dim) + self.point_embeddings = nn.Embedding(1, embed_dim) + self.bbox_top_left_embeddings = nn.Embedding(1, embed_dim) + self.bbox_bottom_right_embeddings = nn.Embedding(1, embed_dim) + + def get_dense_pe(self) -> torch.Tensor: + """ + Returns the positional encoding used to encode point prompts, + applied to a dense set of points the shape of the image encoding. + + Returns: + torch.Tensor: Positional encoding with shape + 1x(embed_dim)x(embedding_h)x(embedding_w) + """ + return self.pe_layer(self.image_embedding_size).unsqueeze(0) + + def _embed_points( + self, + points: torch.Tensor, + labels: torch.Tensor, + ) -> torch.Tensor: + """Embeds point prompts.""" + + points = points + 0.5 # Shift to center of pixel + point_embedding = self.pe_layer.forward_with_coords( + points, self.input_image_size + ) + invalid_label_ids = torch.eq(labels, -1)[:,:,None] + point_label_ids = torch.eq(labels, 1)[:,:,None] + topleft_label_ids = torch.eq(labels, 2)[:,:,None] + bottomright_label_ids = torch.eq(labels, 3)[:,:,None] + point_embedding = point_embedding + self.invalid_points.weight[:,None,:] * invalid_label_ids + point_embedding = point_embedding + self.point_embeddings.weight[:,None,:] * point_label_ids + point_embedding = point_embedding + self.bbox_top_left_embeddings.weight[:,None,:] * topleft_label_ids + point_embedding = point_embedding + self.bbox_bottom_right_embeddings.weight[:,None,:] * bottomright_label_ids + return point_embedding + + def forward( + self, + coords, + labels, + ) -> torch.Tensor: + """ + Embeds different types of prompts, returning both sparse and dense + embeddings. + + Arguments: + points: A tensor of shape [B, 2] + labels: An integer tensor of shape [B] where each element is 1,2 or 3. + + Returns: + torch.Tensor: sparse embeddings for the points and boxes, with shape + BxNx(embed_dim), where N is determined by the number of input points + and boxes. + """ + return self._embed_points(coords, labels) + + +class PositionEmbeddingRandom(nn.Module): + """ + Positional encoding using random spatial frequencies. + """ + + def __init__(self, num_pos_feats: int) -> None: + super().__init__() + self.register_buffer( + "positional_encoding_gaussian_matrix", torch.randn((2, num_pos_feats)) + ) + + def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor: + """Positionally encode points that are normalized to [0,1].""" + # assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape + coords = 2 * coords - 1 + coords = coords @ self.positional_encoding_gaussian_matrix + coords = 2 * np.pi * coords + # outputs d_1 x ... x d_n x C shape + return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1) + + def forward(self, size: Tuple[int, int]) -> torch.Tensor: + """Generate positional encoding for a grid of the specified size.""" + h, w = size + device = self.positional_encoding_gaussian_matrix.device + grid = torch.ones([h, w], device=device, dtype=self.positional_encoding_gaussian_matrix.dtype) + y_embed = grid.cumsum(dim=0) - 0.5 + x_embed = grid.cumsum(dim=1) - 0.5 + y_embed = y_embed / h + x_embed = x_embed / w + + pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1)) + return pe.permute(2, 0, 1) # C x H x W + + def forward_with_coords( + self, coords_input: torch.Tensor, image_size: Tuple[int, int] + ) -> torch.Tensor: + """Positionally encode points that are not normalized to [0,1].""" + coords = coords_input.clone() + coords[:, :, 0] = coords[:, :, 0] / image_size[1] + coords[:, :, 1] = coords[:, :, 1] / image_size[0] + # remove to(float) here, don't know why original implementation add this + return self._pe_encoding(coords) # B x N x C + + +class MaskDecoder(nn.Module): + def __init__( + self, + *, + transformer_dim: int, + transformer: nn.Module, + num_multimask_outputs: int, + activation: Type[nn.Module], + normalization_type: str, + normalize_before_activation: bool, + iou_head_depth: int, + iou_head_hidden_dim: int, + upscaling_layer_dims: List[int], + ) -> None: + """ + Predicts masks given an image and prompt embeddings, using a + transformer architecture. + + Arguments: + transformer_dim (int): the channel dimension of the transformer + transformer (nn.Module): the transformer used to predict masks + num_multimask_outputs (int): the number of masks to predict + when disambiguating masks + activation (nn.Module): the type of activation to use when + upscaling masks + iou_head_depth (int): the depth of the MLP used to predict + mask quality + iou_head_hidden_dim (int): the hidden dimension of the MLP + used to predict mask quality + """ + super().__init__() + self.transformer_dim = transformer_dim + self.transformer = transformer + + self.num_multimask_outputs = num_multimask_outputs + + self.iou_token = nn.Embedding(1, transformer_dim) + if num_multimask_outputs > 1: + self.num_mask_tokens = num_multimask_outputs + 1 + else: + self.num_mask_tokens = 1 + self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim) + output_dim_after_upscaling = transformer_dim + + self.final_output_upscaling_layers = nn.ModuleList([]) + for idx, layer_dims in enumerate(upscaling_layer_dims): + self.final_output_upscaling_layers.append( + nn.Sequential( + nn.ConvTranspose2d( + output_dim_after_upscaling, + layer_dims, + kernel_size=2, + stride=2, + ), + nn.GroupNorm(1, layer_dims) + if idx < len(upscaling_layer_dims) - 1 + else nn.Identity(), + activation(), + ) + ) + output_dim_after_upscaling = layer_dims + + self.output_hypernetworks_mlps = nn.ModuleList( + [ + MLPBlock( + input_dim=transformer_dim, + hidden_dim=transformer_dim, + output_dim=output_dim_after_upscaling, + num_layers=2, + act=activation, + ) + for i in range(self.num_mask_tokens) + ] + ) + + self.iou_prediction_head = MLPBlock( + input_dim=transformer_dim, + hidden_dim=iou_head_hidden_dim, + output_dim=self.num_mask_tokens, + num_layers=iou_head_depth, + act=activation, + ) + + def forward( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + multimask_output: bool, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Predict masks given image and prompt embeddings. + + Arguments: + image_embeddings: A tensor of shape [B, C, H, W] or [B*max_num_queries, C, H, W] + image_pe (torch.Tensor): positional encoding with the shape of image_embeddings (the batch dimension is broadcastable). + sparse_prompt_embeddings (torch.Tensor): the embeddings of the points and boxes + multimask_output (bool): Whether to return multiple masks or a single + mask. + + Returns: + torch.Tensor: batched predicted masks + torch.Tensor: batched predictions of mask quality + """ + + ( + batch_size, + max_num_queries, + sparse_embed_dim_1, + sparse_embed_dim_2, + ) = sparse_prompt_embeddings.shape + + ( + _, + image_embed_dim_c, + image_embed_dim_h, + image_embed_dim_w, + ) = image_embeddings.shape + + # Tile the image embedding for all queries. + image_embeddings_tiled = torch.tile( + image_embeddings[:, None, :, :, :], [1, max_num_queries, 1, 1, 1] + ).view( + batch_size * max_num_queries, + image_embed_dim_c, + image_embed_dim_h, + image_embed_dim_w, + ) + sparse_prompt_embeddings = sparse_prompt_embeddings.reshape( + batch_size * max_num_queries, sparse_embed_dim_1, sparse_embed_dim_2 + ) + masks, iou_pred = self.predict_masks( + image_embeddings=image_embeddings_tiled, + image_pe=image_pe, + sparse_prompt_embeddings=sparse_prompt_embeddings, + ) + + if multimask_output and self.num_multimask_outputs > 1: + return masks[:, 1:, :], iou_pred[:, 1:] + else: + return masks[:, :1, :], iou_pred[:, :1] + + def predict_masks( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Predicts masks. See 'forward' for more details.""" + # Concatenate output tokens + output_tokens = torch.cat( + [self.iou_token.weight, self.mask_tokens.weight], dim=0 + ) + output_tokens = output_tokens.unsqueeze(0).expand( + sparse_prompt_embeddings.size(0), -1, -1 + ) + tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1) + # Expand per-image data in batch direction to be per-mask + pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) + b, c, h, w = image_embeddings.shape + hs, src = self.transformer(image_embeddings, pos_src, tokens) + iou_token_out = hs[:, 0, :] + mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :] + + # Upscale mask embeddings and predict masks using the mask tokens + upscaled_embedding = src.transpose(1, 2).view(b, c, h, w) + + for upscaling_layer in self.final_output_upscaling_layers: + upscaled_embedding = upscaling_layer(upscaled_embedding) + hyper_in_list: List[torch.Tensor] = [] + for i, output_hypernetworks_mlp in enumerate(self.output_hypernetworks_mlps): + hyper_in_list.append(output_hypernetworks_mlp(mask_tokens_out[:, i, :])) + hyper_in = torch.stack(hyper_in_list, dim=1) + b, c, h, w = upscaled_embedding.shape + masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w) + # Generate mask quality predictions + iou_pred = self.iou_prediction_head(iou_token_out) + return masks, iou_pred diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_encoder.py b/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_encoder.py new file mode 100644 index 0000000..73fd7ac --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/efficient_sam_encoder.py @@ -0,0 +1,257 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from typing import List, Optional, Tuple, Type + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class LayerNorm2d(nn.Module): + def __init__(self, num_channels: int, eps: float = 1e-6) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(num_channels)) + self.bias = nn.Parameter(torch.zeros(num_channels)) + self.eps = eps + + def forward(self, x: torch.Tensor) -> torch.Tensor: + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + x = self.weight[:, None, None] * x + self.bias[:, None, None] + return x + + +class PatchEmbed(nn.Module): + """2D Image to Patch Embedding""" + + def __init__( + self, + img_size, + patch_size, + in_chans, + embed_dim, + ): + super().__init__() + self.proj = nn.Conv2d( + in_chans, + embed_dim, + kernel_size=(patch_size, patch_size), + stride=(patch_size, patch_size), + bias=True, + ) + + def forward(self, x): + B, C, H, W = x.shape + x = self.proj(x) + return x + + +class Attention(nn.Module): + def __init__( + self, + dim, + num_heads, + qkv_bias, + qk_scale=None, + ): + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = qk_scale or head_dim**-0.5 + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.proj = nn.Linear(dim, dim) + + def forward(self, x): + B, N, C = x.shape + qkv = ( + self.qkv(x) + .reshape(B, N, 3, self.num_heads, C // self.num_heads) + .permute(2, 0, 3, 1, 4) + ) + q, k, v = ( + qkv[0], + qkv[1], + qkv[2], + ) + attn = (q @ k.transpose(-2, -1)) * self.scale + attn = attn.softmax(dim=-1) + x = (attn @ v).transpose(1, 2).reshape(B, N, C) + x = self.proj(x) + return x + + +class Mlp(nn.Module): + def __init__( + self, + in_features, + hidden_features=None, + out_features=None, + act_layer=nn.GELU, + ): + super().__init__() + out_features = out_features or in_features + hidden_features = hidden_features or in_features + self.fc1 = nn.Linear(in_features, hidden_features) + self.act = act_layer() + self.fc2 = nn.Linear(hidden_features, out_features) + + def forward(self, x): + x = self.fc1(x) + x = self.act(x) + x = self.fc2(x) + return x + + +class Block(nn.Module): + def __init__( + self, + dim, + num_heads, + mlp_ratio=4.0, + qkv_bias=False, + qk_scale=None, + act_layer=nn.GELU, + ): + super().__init__() + self.norm1 = nn.LayerNorm(dim, eps=1e-6) + self.attn = Attention( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + qk_scale=qk_scale, + ) + self.norm2 = nn.LayerNorm(dim, eps=1e-6) + mlp_hidden_dim = int(dim * mlp_ratio) + self.mlp = Mlp( + in_features=dim, + hidden_features=mlp_hidden_dim, + act_layer=act_layer, + ) + + def forward(self, x): + x = x + self.attn(self.norm1(x)) + x = x + self.mlp(self.norm2(x)) + return x + + +@torch.jit.export +def get_abs_pos( + abs_pos: torch.Tensor, has_cls_token: bool, hw: List[int] +) -> torch.Tensor: + """ + Calculate absolute positional embeddings. If needed, resize embeddings and remove cls_token + dimension for the original embeddings. + Args: + abs_pos (Tensor): absolute positional embeddings with (1, num_position, C). + has_cls_token (bool): If true, has 1 embedding in abs_pos for cls token. + hw (Tuple): size of input image tokens. + + Returns: + Absolute positional embeddings after processing with shape (1, H, W, C) + """ + h = hw[0] + w = hw[1] + if has_cls_token: + abs_pos = abs_pos[:, 1:] + xy_num = abs_pos.shape[1] + size = int(math.sqrt(xy_num)) + assert size * size == xy_num + + if size != h or size != w: + new_abs_pos = F.interpolate( + abs_pos.reshape(1, size, size, -1).permute(0, 3, 1, 2), + size=(h, w), + mode="bicubic", + align_corners=False, + ) + return new_abs_pos.permute(0, 2, 3, 1) + else: + return abs_pos.reshape(1, h, w, -1) + + +# Image encoder for efficient SAM. +class ImageEncoderViT(nn.Module): + def __init__( + self, + img_size: int, + patch_size: int, + in_chans: int, + patch_embed_dim: int, + normalization_type: str, + depth: int, + num_heads: int, + mlp_ratio: float, + neck_dims: List[int], + act_layer: Type[nn.Module], + ) -> None: + """ + Args: + img_size (int): Input image size. + patch_size (int): Patch size. + in_chans (int): Number of input image channels. + patch_embed_dim (int): Patch embedding dimension. + depth (int): Depth of ViT. + num_heads (int): Number of attention heads in each ViT block. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + act_layer (nn.Module): Activation layer. + """ + super().__init__() + + self.img_size = img_size + self.image_embedding_size = img_size // ((patch_size if patch_size > 0 else 1)) + self.transformer_output_dim = ([patch_embed_dim] + neck_dims)[-1] + self.pretrain_use_cls_token = True + pretrain_img_size = 224 + self.patch_embed = PatchEmbed(img_size, patch_size, in_chans, patch_embed_dim) + # Initialize absolute positional embedding with pretrain image size. + num_patches = (pretrain_img_size // patch_size) * ( + pretrain_img_size // patch_size + ) + num_positions = num_patches + 1 + self.pos_embed = nn.Parameter(torch.zeros(1, num_positions, patch_embed_dim)) + self.blocks = nn.ModuleList() + for i in range(depth): + vit_block = Block(patch_embed_dim, num_heads, mlp_ratio, True) + self.blocks.append(vit_block) + self.neck = nn.Sequential( + nn.Conv2d( + patch_embed_dim, + neck_dims[0], + kernel_size=1, + bias=False, + ), + LayerNorm2d(neck_dims[0]), + nn.Conv2d( + neck_dims[0], + neck_dims[0], + kernel_size=3, + padding=1, + bias=False, + ), + LayerNorm2d(neck_dims[0]), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + assert ( + x.shape[2] == self.img_size and x.shape[3] == self.img_size + ), "input image size must match self.img_size" + x = self.patch_embed(x) + # B C H W -> B H W C + x = x.permute(0, 2, 3, 1) + x = x + get_abs_pos( + self.pos_embed, self.pretrain_use_cls_token, [x.shape[1], x.shape[2]] + ) + num_patches = x.shape[1] + assert x.shape[2] == num_patches + x = x.reshape(x.shape[0], num_patches * num_patches, x.shape[3]) + for blk in self.blocks: + x = blk(x) + x = x.reshape(x.shape[0], num_patches, num_patches, x.shape[2]) + x = self.neck(x.permute(0, 3, 1, 2)) + return x diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/mlp.py b/py/evf-sam/model/EfficientSAM/efficient_sam/mlp.py new file mode 100644 index 0000000..b3be8db --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/mlp.py @@ -0,0 +1,29 @@ +from typing import Type + +from torch import nn + + +# Lightly adapted from +# https://github.com/facebookresearch/MaskFormer/blob/main/mask_former/modeling/transformer/transformer_predictor.py # noqa +class MLPBlock(nn.Module): + def __init__( + self, + input_dim: int, + hidden_dim: int, + output_dim: int, + num_layers: int, + act: Type[nn.Module], + ) -> None: + super().__init__() + self.num_layers = num_layers + h = [hidden_dim] * (num_layers - 1) + self.layers = nn.ModuleList( + nn.Sequential(nn.Linear(n, k), act()) + for n, k in zip([input_dim] + h, [hidden_dim] * num_layers) + ) + self.fc = nn.Linear(hidden_dim, output_dim) + + def forward(self, x): + for layer in self.layers: + x = layer(x) + return self.fc(x) diff --git a/py/evf-sam/model/EfficientSAM/efficient_sam/two_way_transformer.py b/py/evf-sam/model/EfficientSAM/efficient_sam/two_way_transformer.py new file mode 100644 index 0000000..b06e528 --- /dev/null +++ b/py/evf-sam/model/EfficientSAM/efficient_sam/two_way_transformer.py @@ -0,0 +1,266 @@ +import math +from typing import Tuple, Type +import torch +from torch import nn, Tensor +from .mlp import MLPBlock + + + + +class TwoWayTransformer(nn.Module): + def __init__( + self, + depth: int, + embedding_dim: int, + num_heads: int, + mlp_dim: int, + activation: Type[nn.Module], + normalize_before_activation: bool, + attention_downsample_rate: int = 2, + ) -> None: + """ + A transformer decoder that attends to an input image using + queries whose positional embedding is supplied. + + Args: + depth (int): number of layers in the transformer + embedding_dim (int): the channel dimension for the input embeddings + num_heads (int): the number of heads for multihead attention. Must + divide embedding_dim + mlp_dim (int): the channel dimension internal to the MLP block + activation (nn.Module): the activation to use in the MLP block + """ + super().__init__() + self.depth = depth + self.embedding_dim = embedding_dim + self.num_heads = num_heads + self.mlp_dim = mlp_dim + self.layers = nn.ModuleList() + + for i in range(depth): + curr_layer = TwoWayAttentionBlock( + embedding_dim=embedding_dim, + num_heads=num_heads, + mlp_dim=mlp_dim, + activation=activation, + normalize_before_activation=normalize_before_activation, + attention_downsample_rate=attention_downsample_rate, + skip_first_layer_pe=(i == 0), + ) + self.layers.append(curr_layer) + + self.final_attn_token_to_image = AttentionForTwoWayAttentionBlock( + embedding_dim, + num_heads, + downsample_rate=attention_downsample_rate, + ) + self.norm_final_attn = nn.LayerNorm(embedding_dim) + + def forward( + self, + image_embedding: Tensor, + image_pe: Tensor, + point_embedding: Tensor, + ) -> Tuple[Tensor, Tensor]: + """ + Args: + image_embedding (torch.Tensor): image to attend to. Should be shape + B x embedding_dim x h x w for any h and w. + image_pe (torch.Tensor): the positional encoding to add to the image. Must + have the same shape as image_embedding. + point_embedding (torch.Tensor): the embedding to add to the query points. + Must have shape B x N_points x embedding_dim for any N_points. + + Returns: + torch.Tensor: the processed point_embedding + torch.Tensor: the processed image_embedding + """ + + # BxCxHxW -> BxHWxC == B x N_image_tokens x C + bs, c, h, w = image_embedding.shape + image_embedding = image_embedding.flatten(2).permute(0, 2, 1) + image_pe = image_pe.flatten(2).permute(0, 2, 1) + + # Prepare queries + queries = point_embedding + keys = image_embedding + + # Apply transformer blocks and final layernorm + for idx, layer in enumerate(self.layers): + queries, keys = layer( + queries=queries, + keys=keys, + query_pe=point_embedding, + key_pe=image_pe, + ) + + # Apply the final attention layer from the points to the image + q = queries + point_embedding + k = keys + image_pe + attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys) + queries = queries + attn_out + queries = self.norm_final_attn(queries) + return queries, keys + + +class TwoWayAttentionBlock(nn.Module): + def __init__( + self, + embedding_dim: int, + num_heads: int, + mlp_dim: int, + activation: Type[nn.Module], + normalize_before_activation: bool, + attention_downsample_rate: int = 2, + skip_first_layer_pe: bool = False, + ) -> None: + """ + A transformer block with four layers: (1) self-attention of sparse + inputs, (2) cross attention of sparse inputs to dense inputs, (3) mlp + block on sparse inputs, and (4) cross attention of dense inputs to sparse + inputs. + + Arguments: + embedding_dim (int): the channel dimension of the embeddings + num_heads (int): the number of heads in the attention layers + mlp_dim (int): the hidden dimension of the mlp block + activation (nn.Module): the activation of the mlp block + skip_first_layer_pe (bool): skip the PE on the first layer + """ + super().__init__() + self.self_attn = AttentionForTwoWayAttentionBlock(embedding_dim, num_heads) + self.norm1 = nn.LayerNorm(embedding_dim) + + self.cross_attn_token_to_image = AttentionForTwoWayAttentionBlock( + embedding_dim, + num_heads, + downsample_rate=attention_downsample_rate, + ) + self.norm2 = nn.LayerNorm(embedding_dim) + + self.mlp = MLPBlock( + embedding_dim, + mlp_dim, + embedding_dim, + 1, + activation, + ) + + self.norm3 = nn.LayerNorm(embedding_dim) + + self.norm4 = nn.LayerNorm(embedding_dim) + self.cross_attn_image_to_token = AttentionForTwoWayAttentionBlock( + embedding_dim, + num_heads, + downsample_rate=attention_downsample_rate, + ) + + self.skip_first_layer_pe = skip_first_layer_pe + + def forward( + self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor + ) -> Tuple[Tensor, Tensor]: + # Self attention block + if not self.skip_first_layer_pe: + queries = queries + query_pe + attn_out = self.self_attn(q=queries, k=queries, v=queries) + queries = queries + attn_out + queries = self.norm1(queries) + + # Cross attention block, tokens attending to image embedding + q = queries + query_pe + k = keys + key_pe + attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys) + queries = queries + attn_out + queries = self.norm2(queries) + + # MLP block + mlp_out = self.mlp(queries) + queries = queries + mlp_out + queries = self.norm3(queries) + + # Cross attention block, image embedding attending to tokens + q = queries + query_pe + k = keys + key_pe + attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries) + keys = keys + attn_out + keys = self.norm4(keys) + + return queries, keys + + +class AttentionForTwoWayAttentionBlock(nn.Module): + """ + An attention layer that allows for downscaling the size of the embedding + after projection to queries, keys, and values. + """ + + def __init__( + self, + embedding_dim: int, + num_heads: int, + downsample_rate: int = 1, + ) -> None: + super().__init__() + self.embedding_dim = embedding_dim + self.internal_dim = embedding_dim // downsample_rate + self.num_heads = num_heads + assert ( + self.internal_dim % num_heads == 0 + ), "num_heads must divide embedding_dim." + self.c_per_head = self.internal_dim / num_heads + self.inv_sqrt_c_per_head = 1.0 / math.sqrt(self.c_per_head) + + self.q_proj = nn.Linear(embedding_dim, self.internal_dim) + self.k_proj = nn.Linear(embedding_dim, self.internal_dim) + self.v_proj = nn.Linear(embedding_dim, self.internal_dim) + self.out_proj = nn.Linear(self.internal_dim, embedding_dim) + self._reset_parameters() + + def _reset_parameters(self) -> None: + # The fan_out is incorrect, but matches pytorch's initialization + # for which qkv is a single 3*embedding_dim x embedding_dim matrix + fan_in = self.embedding_dim + fan_out = 3 * self.internal_dim + # Xavier uniform with our custom fan_out + bnd = math.sqrt(6 / (fan_in + fan_out)) + nn.init.uniform_(self.q_proj.weight, -bnd, bnd) + nn.init.uniform_(self.k_proj.weight, -bnd, bnd) + nn.init.uniform_(self.v_proj.weight, -bnd, bnd) + # out_proj.weight is left with default initialization, like pytorch attention + nn.init.zeros_(self.q_proj.bias) + nn.init.zeros_(self.k_proj.bias) + nn.init.zeros_(self.v_proj.bias) + nn.init.zeros_(self.out_proj.bias) + + def _separate_heads(self, x: Tensor, num_heads: int) -> Tensor: + b, n, c = x.shape + x = x.reshape(b, n, num_heads, c // num_heads) + return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head + + def _recombine_heads(self, x: Tensor) -> Tensor: + b, n_heads, n_tokens, c_per_head = x.shape + x = x.transpose(1, 2) + return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C + + def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor: + # Input projections + q = self.q_proj(q) + k = self.k_proj(k) + v = self.v_proj(v) + + # Separate into heads + q = self._separate_heads(q, self.num_heads) + k = self._separate_heads(k, self.num_heads) + v = self._separate_heads(v, self.num_heads) + + # Attention + _, _, _, c_per_head = q.shape + attn = q @ k.permute(0, 1, 3, 2) # B x N_heads x N_tokens x N_tokens + attn = attn * self.inv_sqrt_c_per_head + attn = torch.softmax(attn, dim=-1) + # Get output + out = attn @ v + out = self._recombine_heads(out) + out = self.out_proj(out) + return out diff --git a/py/evf-sam/model/configuration_evf.py b/py/evf-sam/model/configuration_evf.py new file mode 100644 index 0000000..fc1383f --- /dev/null +++ b/py/evf-sam/model/configuration_evf.py @@ -0,0 +1,113 @@ +# coding=utf-8 +# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved. +# +# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX +# and OPT implementations in this library. It has been modified from its +# original forms to accommodate minor architectural differences compared +# to GPT-NeoX and OPT used by the Meta AI team that trained the model. +# +# 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. +""" Evf model configuration""" + +from transformers.configuration_utils import PretrainedConfig +from transformers.utils import logging + +logger = logging.get_logger(__name__) + +EVF_PRETRAINED_CONFIG_ARCHIVE_MAP = {} + + +class EvfConfig(PretrainedConfig): + r""" + This is the configuration class to store the configuration of a [`EvfSam`]. + + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the + documentation from [`PretrainedConfig`] for more information. + + Args: + hidden_size (`int`, *optional*, defaults to 4096): + Dimension of the hidden representations. + pretraining_tp (`int`, *optional*, defaults to `1`): + Experimental feature. Tensor parallelism rank used during pretraining. Please refer to [this + document](https://huggingface.co/docs/transformers/parallelism) to understand more about it. This value is + necessary to ensure exact reproducibility of the pretraining results. Please refer to [this + issue](https://github.com/pytorch/pytorch/issues/76232). + rope_scaling (`Dict`, *optional*): + Dictionary containing the scaling configuration for the RoPE embeddings. Currently supports three scaling + strategies: linear and dynamic. Their scaling factor must be an float greater than 1. The expected format + is `{"type": strategy name, "factor": scaling factor}`. When using this flag, don't update + `max_position_embeddings` to the expected new maximum. See the following thread for more information on how + these scaling strategies behave: + https://www.reddit.com/r/LocalLLaMA/comments/14mrgpr/dynamically_scaled_rope_further_increases/. This is an + experimental feature, subject to breaking API changes in future versions. + + Example: + + ```python + + >>> configuration = EvfConfig() + >>> model = EvfSam(configuration) + + >>> # Accessing the model configuration + >>> configuration = model.config + ```""" + model_type = "evf" + keys_to_ignore_at_inference = ["past_key_values"] + + def __init__( + self, + hidden_size=768, + pad_token_id=1, + bos_token_id=0, + eos_token_id=2, + pretraining_tp=1, + tie_word_embeddings=False, + rope_scaling=None, + out_dim=256, + **kwargs, + ): + self.hidden_size = hidden_size + self.out_dim = out_dim + + # self.pretraining_tp = pretraining_tp + # self.rope_scaling = rope_scaling + # self._rope_scaling_validation() + + super().__init__( + pad_token_id=pad_token_id, + bos_token_id=bos_token_id, + eos_token_id=eos_token_id, + tie_word_embeddings=tie_word_embeddings, + **kwargs, + ) + + def _rope_scaling_validation(self): + """ + Validate the `rope_scaling` configuration. + """ + if self.rope_scaling is None: + return + + if not isinstance(self.rope_scaling, dict) or len(self.rope_scaling) != 2: + raise ValueError( + "`rope_scaling` must be a dictionary with with two fields, `name` and `factor`, " + f"got {self.rope_scaling}" + ) + rope_scaling_type = self.rope_scaling.get("type", None) + rope_scaling_factor = self.rope_scaling.get("factor", None) + if rope_scaling_type is None or rope_scaling_type not in ["linear", "dynamic"]: + raise ValueError( + f"`rope_scaling`'s name field must be one of ['linear', 'dynamic'], got {rope_scaling_type}" + ) + if rope_scaling_factor is None or not isinstance(rope_scaling_factor, float) or rope_scaling_factor <= 1.0: + raise ValueError(f"`rope_scaling`'s factor field must be an float > 1, got {rope_scaling_factor}") diff --git a/py/evf-sam/model/evf_effisam.py b/py/evf-sam/model/evf_effisam.py new file mode 100644 index 0000000..9820624 --- /dev/null +++ b/py/evf-sam/model/evf_effisam.py @@ -0,0 +1,313 @@ +from typing import List, Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers import PreTrainedModel, AutoConfig, AutoModelForCausalLM +from .EfficientSAM.efficient_sam.build_efficient_sam import build_efficient_sam_vits, build_efficient_sam_vitt +from .unilm.beit3.modeling_utils import BEiT3Wrapper, _get_base_config, _get_large_config +from .configuration_evf import EvfConfig + + +def dice_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, + scale=1000, # 100000.0, + eps=1e-6, +): + """ + Compute the DICE loss, similar to generalized IOU for masks + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + """ + inputs = inputs.sigmoid() + inputs = inputs.flatten(1, 2) + targets = targets.flatten(1, 2) + numerator = 2 * (inputs / scale * targets).sum(-1) + denominator = (inputs / scale).sum(-1) + (targets / scale).sum(-1) + loss = 1 - (numerator + eps) / (denominator + eps) + loss = loss.sum() / (num_masks + 1e-8) + return loss + + +def sigmoid_ce_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, +): + """ + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + Returns: + Loss tensor + """ + loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") + loss = loss.flatten(1, 2).mean(1).sum() / (num_masks + 1e-8) + return loss + + + +class EvfEffiSamModel(PreTrainedModel): + config_class = EvfConfig + def __init__( + self, + config, + **kwargs + ): + super(EvfEffiSamModel, self).__init__(config) + + self.config = config + self.vision_pretrained = kwargs.get("vision_pretrained", None) + self.encoder_pretrained = kwargs.get("encoder_pretrained", None) + self.dice_loss_weight = kwargs.get("dice_loss_weight", None) + self.bce_loss_weight = kwargs.get("bce_loss_weight", None) + self.train_mask_decoder = kwargs.get("train_mask_decoder", False) + self.initialize_evf_modules(config) + + + def initialize_evf_modules(self, config): + # EffiSAM + if config.sam_scale=="tiny": + self.visual_model = build_efficient_sam_vitt(self.vision_pretrained) + elif config.sam_scale=="small": + # vits scale, or without pretrained weight (self.vision_pretrained=None) + self.visual_model = build_efficient_sam_vits(self.vision_pretrained) + else: + raise NotImplementedError + + for param in self.visual_model.parameters(): + param.requires_grad = False + if self.train_mask_decoder: + self.visual_model.mask_decoder.train() + for param in self.visual_model.mask_decoder.parameters(): + param.requires_grad = True + + # beit-3 + if self.config.mm_extractor_scale == "base": + beit_config = _get_base_config() + elif self.config.mm_extractor_scale == "large": + beit_config = _get_large_config() + else: + raise AttributeError(f"model config should contain key 'mm_extractor_scale', with value 'base' or 'large'.") + + self.mm_extractor = BEiT3Wrapper(beit_config) + if self.encoder_pretrained is not None: + beit_state_dict = torch.load(self.encoder_pretrained)["model"] + self.mm_extractor.load_state_dict( + beit_state_dict, + strict=False + ) + + for param in self.mm_extractor.parameters(): + param.requires_grad = True + + # Projection layer + in_dim = config.hidden_size + assert in_dim==beit_config.encoder_embed_dim, \ + f"projection layer dim {in_dim} mismatch with mm_extractor dim {beit_config.encoder_embed_dim}" + out_dim = config.out_dim + text_fc = [ + nn.Linear(in_dim, in_dim), + nn.ReLU(), + nn.Linear(in_dim, out_dim) + ] + self.text_hidden_fcs = nn.ModuleList([nn.Sequential(*text_fc)]) + self.text_hidden_fcs.train() + for param in self.text_hidden_fcs.parameters(): + param.requires_grad = True + + def get_visual_embs(self, pixel_values: torch.Tensor): + with torch.no_grad(): + image_embeddings_list = [] + for i in range(pixel_values.shape[0]): + torch.cuda.empty_cache() + image_embeddings = self.visual_model.image_encoder( + pixel_values[i].unsqueeze(0) + ) + image_embeddings_list.append(image_embeddings) + torch.cuda.empty_cache() + image_embeddings = torch.cat(image_embeddings_list, 0) + return image_embeddings + + def forward( + self, + images: torch.Tensor, + images_evf: torch.Tensor, + input_ids: torch.Tensor, + attention_masks: torch.Tensor, + offset: torch.Tensor, + masks_list: List[torch.Tensor], + label_list: List[torch.Tensor], + resize_list: List[tuple], + inference: bool = False, + **kwargs, + ): + image_embeddings = self.get_visual_embs(images) + batch_size = image_embeddings.shape[0] + assert batch_size == len(offset) - 1 + + images_evf_list = [] + for i in range(len(offset) - 1): + start_i, end_i = offset[i], offset[i + 1] + images_evf_i = ( + images_evf[i] + .unsqueeze(0) + .expand(end_i - start_i, -1, -1, -1) + .contiguous() + ) + images_evf_list.append(images_evf_i) + images_evf = torch.cat(images_evf_list, dim=0) + + multimask_output = False + output = self.mm_extractor.beit3( + visual_tokens=images_evf, + textual_tokens=input_ids, + text_padding_position=~attention_masks + ) + + feat = output["encoder_out"][:, :1, ...] + + feat = self.text_hidden_fcs[0](feat) + feat = torch.split(feat, [offset[i+1] - offset[i] for i in range(len(offset)-1)]) + + pred_masks = [] + for i in range(len(feat)): + sparse_embeddings = feat[i].unsqueeze(0) + sparse_embeddings = sparse_embeddings.to(feat[i].dtype) + low_res_masks, iou_predictions = self.visual_model.mask_decoder( + image_embeddings=image_embeddings[i].unsqueeze(0), + image_pe=self.visual_model.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + multimask_output=multimask_output, + ) + + if multimask_output: + sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1) + + pred_mask = self.postprocess_masks( + low_res_masks[:, :1], + input_size=resize_list[i], + original_size=label_list[i].shape, + ) + pred_masks.append(pred_mask[:, 0]) + + gt_masks = masks_list + + if inference: + return { + "pred_masks": pred_masks, + "gt_masks": gt_masks, + } + + mask_bce_loss = 0 + mask_dice_loss = 0 + num_masks = 0 + for batch_idx in range(len(pred_masks)): + gt_mask = gt_masks[batch_idx] + pred_mask = pred_masks[batch_idx] + + assert ( + gt_mask.shape[0] == pred_mask.shape[0] + ), "gt_mask.shape: {}, pred_mask.shape: {}".format( + gt_mask.shape, pred_mask.shape + ) + mask_bce_loss += ( + sigmoid_ce_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + * gt_mask.shape[0] + ) + mask_dice_loss += ( + dice_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + * gt_mask.shape[0] + ) + num_masks += gt_mask.shape[0] + + mask_bce_loss = self.bce_loss_weight * mask_bce_loss / (num_masks + 1e-8) + mask_dice_loss = self.dice_loss_weight * mask_dice_loss / (num_masks + 1e-8) + mask_loss = mask_bce_loss + mask_dice_loss + + loss = mask_loss + + return { + "loss": loss, + "mask_bce_loss": mask_bce_loss, + "mask_dice_loss": mask_dice_loss, + "mask_loss": mask_loss, + } + + def postprocess_masks( + self, + masks: torch.Tensor, + input_size: Tuple[int, ...], + original_size: Tuple[int, ...], + ) -> torch.Tensor: + """ + pre-process of Effi-SAM is different from SAM, where there is no padding, + so cropping is not needed in post-process. + """ + + dtype = masks.dtype + + # masks = F.interpolate( + # masks.float(), + # (1024, 1024), + # mode="bilinear", + # align_corners=False, + # ) + # masks = masks.to(dtype) + # masks = masks[..., : input_size[0], : input_size[1]] + + masks = F.interpolate( + masks, original_size, mode="bilinear", align_corners=False + ) + masks = masks.to(dtype) + return masks + + def inference( + self, + images, + images_evf, + input_ids, + resize_list, + original_size_list, + multimask_output=False, + ): + with torch.no_grad(): + image_embeddings = self.visual_model.image_encoder(images) + + output = self.mm_extractor.beit3(visual_tokens=images_evf, textual_tokens=input_ids, text_padding_position=torch.zeros_like(input_ids)) + + feat = output["encoder_out"][:, :1, ...] + feat = self.text_hidden_fcs[0](feat) + sparse_embeddings = feat.unsqueeze(0) + sparse_embeddings = sparse_embeddings.to(feat.dtype) + low_res_masks, iou_predictions = self.visual_model.mask_decoder( + image_embeddings=image_embeddings, + image_pe=self.visual_model.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + multimask_output=multimask_output, + ) + if multimask_output: + sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1) + + pred_mask = self.postprocess_masks( + low_res_masks[:, :1], + input_size=resize_list[0], + original_size=original_size_list[0], + ) + + return pred_mask[:, 0] + + +AutoConfig.register("evf", EvfConfig) +AutoModelForCausalLM.register(EvfConfig, EvfEffiSamModel) diff --git a/py/evf-sam/model/evf_sam.py b/py/evf-sam/model/evf_sam.py new file mode 100644 index 0000000..a0ec88e --- /dev/null +++ b/py/evf-sam/model/evf_sam.py @@ -0,0 +1,303 @@ +from typing import List + +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers import PreTrainedModel, AutoConfig, AutoModelForCausalLM +from .segment_anything import build_sam_vit_h +from .unilm.beit3.modeling_utils import BEiT3Wrapper, _get_base_config, _get_large_config +from .configuration_evf import EvfConfig + +def dice_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, + scale=1000, # 100000.0, + eps=1e-6, +): + """ + Compute the DICE loss, similar to generalized IOU for masks + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + """ + inputs = inputs.sigmoid() + inputs = inputs.flatten(1, 2) + targets = targets.flatten(1, 2) + numerator = 2 * (inputs / scale * targets).sum(-1) + denominator = (inputs / scale).sum(-1) + (targets / scale).sum(-1) + loss = 1 - (numerator + eps) / (denominator + eps) + loss = loss.sum() / (num_masks + 1e-8) + return loss + + +def sigmoid_ce_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, +): + """ + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + Returns: + Loss tensor + """ + loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") + loss = loss.flatten(1, 2).mean(1).sum() / (num_masks + 1e-8) + return loss + + + +class EvfSamModel(PreTrainedModel): + config_class = EvfConfig + def __init__( + self, + config, + **kwargs + ): + super(EvfSamModel, self).__init__(config) + + self.config = config + self.vision_pretrained = kwargs.get("vision_pretrained", None) + self.encoder_pretrained = kwargs.get("encoder_pretrained", None) + self.dice_loss_weight = kwargs.get("dice_loss_weight", None) + self.bce_loss_weight = kwargs.get("bce_loss_weight", None) + self.train_mask_decoder = kwargs.get("train_mask_decoder", False) + self.train_prompt_encoder = kwargs.get("train_prompt_encoder", False) + self.initialize_evf_modules(config) + + + def initialize_evf_modules(self, config): + # SAM + if config.sam_scale=="huge": + self.visual_model = build_sam_vit_h(self.vision_pretrained) + else: + raise NotImplementedError + + for param in self.visual_model.parameters(): + param.requires_grad = False + if self.train_mask_decoder: + self.visual_model.mask_decoder.train() + for param in self.visual_model.mask_decoder.parameters(): + param.requires_grad = True + if self.train_prompt_encoder: + self.visual_model.prompt_encoder.no_mask_embed.requires_grad_(True) + + # beit-3 + if self.config.mm_extractor_scale == "base": + beit_config = _get_base_config() + elif self.config.mm_extractor_scale == "large": + beit_config = _get_large_config() + else: + raise AttributeError(f"model config should contain key 'mm_extractor_scale', with value 'base' or 'large'.") + + self.mm_extractor = BEiT3Wrapper(beit_config) + if self.encoder_pretrained is not None: + beit_state_dict = torch.load(self.encoder_pretrained)["model"] + self.mm_extractor.load_state_dict( + beit_state_dict, + strict=False + ) + + for param in self.mm_extractor.parameters(): + param.requires_grad = True + + # Projection layer + in_dim = config.hidden_size + assert in_dim==beit_config.encoder_embed_dim, \ + f"projection layer dim {in_dim} mismatch with mm_extractor dim {beit_config.encoder_embed_dim}" + out_dim = config.out_dim + text_fc = [ + nn.Linear(in_dim, in_dim), + nn.ReLU(), + nn.Linear(in_dim, out_dim) + ] + self.text_hidden_fcs = nn.ModuleList([nn.Sequential(*text_fc)]) + self.text_hidden_fcs.train() + for param in self.text_hidden_fcs.parameters(): + param.requires_grad = True + + def get_visual_embs(self, pixel_values: torch.FloatTensor): + with torch.no_grad(): + image_embeddings_list = [] + for i in range(pixel_values.shape[0]): + torch.cuda.empty_cache() + image_embeddings = self.visual_model.image_encoder( + pixel_values[i].unsqueeze(0) + ) + image_embeddings_list.append(image_embeddings) + torch.cuda.empty_cache() + image_embeddings = torch.cat(image_embeddings_list, 0) + return image_embeddings + + def forward( + self, + images: torch.FloatTensor, + images_evf: torch.FloatTensor, + input_ids: torch.LongTensor, + attention_masks: torch.LongTensor, + offset: torch.LongTensor, + masks_list: List[torch.FloatTensor], + label_list: List[torch.Tensor], + resize_list: List[tuple], + inference: bool = False, + **kwargs, + ): + image_embeddings = self.get_visual_embs(images) + batch_size = image_embeddings.shape[0] + assert batch_size == len(offset) - 1 + + images_evf_list = [] + for i in range(len(offset) - 1): + start_i, end_i = offset[i], offset[i + 1] + images_evf_i = ( + images_evf[i] + .unsqueeze(0) + .expand(end_i - start_i, -1, -1, -1) + .contiguous() + ) + images_evf_list.append(images_evf_i) + images_evf = torch.cat(images_evf_list, dim=0) + + multimask_output = False + output = self.mm_extractor.beit3( + visual_tokens=images_evf, + textual_tokens=input_ids, + text_padding_position=~attention_masks + ) + + feat = output["encoder_out"][:, :1, ...] + + feat = self.text_hidden_fcs[0](feat) + feat = torch.split(feat, [offset[i+1] - offset[i] for i in range(len(offset)-1)]) + + pred_masks = [] + for i in range(len(feat)): + ( + sparse_embeddings, + dense_embeddings, + ) = self.visual_model.prompt_encoder( + points=None, + boxes=None, + masks=None, + text_embeds=feat[i], + ) + sparse_embeddings = sparse_embeddings.to(feat[i].dtype) + low_res_masks, iou_predictions = self.visual_model.mask_decoder( + image_embeddings=image_embeddings[i].unsqueeze(0), + image_pe=self.visual_model.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + ) + + if multimask_output: + sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1)[:, :1] + + pred_mask = self.visual_model.postprocess_masks( + low_res_masks, + input_size=resize_list[i], + original_size=label_list[i].shape, + ) + pred_masks.append(pred_mask[:, 0]) + + gt_masks = masks_list + + if inference: + return { + "pred_masks": pred_masks, + "gt_masks": gt_masks, + } + + mask_bce_loss = 0 + mask_dice_loss = 0 + num_masks = 0 + for batch_idx in range(len(pred_masks)): + gt_mask = gt_masks[batch_idx] + pred_mask = pred_masks[batch_idx] + + assert ( + gt_mask.shape[0] == pred_mask.shape[0] + ), "gt_mask.shape: {}, pred_mask.shape: {}".format( + gt_mask.shape, pred_mask.shape + ) + mask_bce_loss += ( + sigmoid_ce_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + * gt_mask.shape[0] + ) + mask_dice_loss += ( + dice_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + * gt_mask.shape[0] + ) + num_masks += gt_mask.shape[0] + + mask_bce_loss = self.bce_loss_weight * mask_bce_loss / (num_masks + 1e-8) + mask_dice_loss = self.dice_loss_weight * mask_dice_loss / (num_masks + 1e-8) + mask_loss = mask_bce_loss + mask_dice_loss + + loss = mask_loss + + return { + "loss": loss, + "mask_bce_loss": mask_bce_loss, + "mask_dice_loss": mask_dice_loss, + "mask_loss": mask_loss, + } + + def inference( + self, + images, + images_evf, + input_ids, + resize_list, + original_size_list, + multimask_output=False, + ): + with torch.no_grad(): + image_embeddings = self.visual_model.image_encoder(images) + multimask_output = multimask_output + + output = self.mm_extractor.beit3(visual_tokens=images_evf, textual_tokens=input_ids, text_padding_position=torch.zeros_like(input_ids)) + + feat = output["encoder_out"][:, :1, ...] + feat = self.text_hidden_fcs[0](feat) + ( + sparse_embeddings, + dense_embeddings, + ) = self.visual_model.prompt_encoder( + points=None, + boxes=None, + masks=None, + text_embeds=feat, + ) + sparse_embeddings = sparse_embeddings.to(feat.dtype) + low_res_masks, iou_predictions = self.visual_model.mask_decoder( + image_embeddings=image_embeddings, + image_pe=self.visual_model.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + ) + if multimask_output: + sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1)[:, :1] + + pred_mask = self.visual_model.postprocess_masks( + low_res_masks, + input_size=resize_list[0], + original_size=original_size_list[0], + ) + + return pred_mask[:, 0] + + +AutoConfig.register("evf", EvfConfig) +AutoModelForCausalLM.register(EvfConfig, EvfSamModel) \ No newline at end of file diff --git a/py/evf-sam/model/evf_sam2.py b/py/evf-sam/model/evf_sam2.py new file mode 100644 index 0000000..93aa295 --- /dev/null +++ b/py/evf-sam/model/evf_sam2.py @@ -0,0 +1,341 @@ +from typing import List +import os +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers import PreTrainedModel, AutoConfig, AutoModelForCausalLM +from .segment_anything_2.sam2.build_sam import build_sam2 +from .unilm.beit3.modeling_utils import BEiT3Wrapper, _get_base_config, _get_large_config +from .configuration_evf import EvfConfig +from .segment_anything_2.sam2.utils.misc import load_video_frames +from collections import OrderedDict + + +def dice_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, + scale=1000, # 100000.0, + eps=1e-6, +): + """ + Compute the DICE loss, similar to generalized IOU for masks + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + """ + inputs = inputs.sigmoid() + inputs = inputs.flatten(1, 2) + targets = targets.flatten(1, 2) + numerator = 2 * (inputs / scale * targets).sum(-1) + denominator = (inputs / scale).sum(-1) + (targets / scale).sum(-1) + loss = 1 - (numerator + eps) / (denominator + eps) + loss = loss.sum() / (num_masks + 1e-8) + return loss + + +def sigmoid_ce_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, +): + """ + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + Returns: + Loss tensor + """ + loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") + loss = loss.flatten(1, 2).mean(1).sum() / (num_masks + 1e-8) + return loss + +class EvfSam2Model(PreTrainedModel): + config_class = EvfConfig + def __init__( + self, + config, + **kwargs + ): + super(EvfSam2Model, self).__init__(config) + + self.config = config + self.vision_pretrained = kwargs.get("vision_pretrained", None) + self.encoder_pretrained = kwargs.get("encoder_pretrained", None) + self.dice_loss_weight = kwargs.get("dice_loss_weight", None) + self.bce_loss_weight = kwargs.get("bce_loss_weight", None) + self.train_mask_decoder = kwargs.get("train_mask_decoder", False) + self.train_prompt_encoder = kwargs.get("train_prompt_encoder", False) + self.initialize_evf_modules(config) + self._bb_feat_sizes = [ + (256, 256), + (128, 128), + (64, 64), + ] + + def initialize_evf_modules(self, config): + # SAM + if config.sam_scale=="large": + self.visual_model = build_sam2("sam2_hiera_l.yaml", self.vision_pretrained, device=None) + elif config.sam_scale=="tiny": + self.visual_model = build_sam2("sam2_hiera_t.yaml", self.vision_pretrained, device=None) + else: + raise NotImplementedError + + for param in self.visual_model.parameters(): + param.requires_grad = False + if self.train_mask_decoder: + self.visual_model.sam_mask_decoder.train() + for param in self.visual_model.sam_mask_decoder.parameters(): + param.requires_grad = True + if self.train_prompt_encoder: + self.visual_model.sam_prompt_encoder.no_mask_embed.requires_grad_(True) + + # beit-3 + if self.config.mm_extractor_scale == "base": + beit_config = _get_base_config() + elif self.config.mm_extractor_scale == "large": + beit_config = _get_large_config() + else: + raise AttributeError(f"model config should contain key 'mm_extractor_scale', with value 'base' or 'large'.") + + self.mm_extractor = BEiT3Wrapper(beit_config) + if self.encoder_pretrained is not None: + beit_state_dict = torch.load(self.encoder_pretrained)["model"] + self.mm_extractor.load_state_dict( + beit_state_dict, + strict=False + ) + + for param in self.mm_extractor.parameters(): + param.requires_grad = True + + # Projection layer + in_dim = config.hidden_size + assert in_dim==beit_config.encoder_embed_dim, \ + f"projection layer dim {in_dim} mismatch with mm_extractor dim {beit_config.encoder_embed_dim}" + out_dim = config.out_dim + text_fc = [ + nn.Linear(in_dim, in_dim), + nn.ReLU(), + nn.Linear(in_dim, out_dim) + ] + self.text_hidden_fcs = nn.ModuleList([nn.Sequential(*text_fc)]) + self.text_hidden_fcs.train() + for param in self.text_hidden_fcs.parameters(): + param.requires_grad = True + + def postprocess_masks(self, masks: torch.Tensor, orig_hw) -> torch.Tensor: + """ + Perform PostProcessing on output masks. + """ + masks = masks.float() + masks = F.interpolate(masks, orig_hw, mode="bilinear", align_corners=False) + return masks + + def forward( + self, + images: torch.FloatTensor, + images_evf: torch.FloatTensor, + input_ids: torch.LongTensor, + attention_masks: torch.LongTensor, + offset: torch.LongTensor, + masks_list: List[torch.FloatTensor], + label_list: List[torch.Tensor], + resize_list: List[tuple], + inference: bool = False, + **kwargs, + ): + # image_embeddings = self.get_visual_embs(images) + backbone_out = self.visual_model.forward_image(images) + # dict_keys(['vision_features', 'vision_pos_enc', 'backbone_fpn']) + _, image_embeddings, _, _ = self.visual_model._prepare_backbone_features(backbone_out) + image_embeddings = [_.to(images.dtype) for _ in image_embeddings] + batch_size = images.shape[0] + if self.visual_model.directly_add_no_mem_embed: + image_embeddings[-1] = image_embeddings[-1] + self.visual_model.no_mem_embed + + feats = [ + feat.permute(1, 2, 0).view(batch_size, -1, *feat_size) + for feat, feat_size in zip(image_embeddings[::-1], self._bb_feat_sizes[::-1]) + ][::-1] + _features = {"image_embed": feats[-1], "high_res_feats": feats[:-1]} + + + assert batch_size == len(offset) - 1 + + images_evf_list = [] + for i in range(len(offset) - 1): + start_i, end_i = offset[i], offset[i + 1] + images_evf_i = ( + images_evf[i] + .unsqueeze(0) + .expand(end_i - start_i, -1, -1, -1) + .contiguous() + ) + images_evf_list.append(images_evf_i) + images_evf = torch.cat(images_evf_list, dim=0) + + multimask_output = False + output = self.mm_extractor.beit3( + visual_tokens=images_evf, + textual_tokens=input_ids, + text_padding_position=~attention_masks + ) + + feat = output["encoder_out"][:, :1, ...] + + feat = self.text_hidden_fcs[0](feat) + feat = torch.split(feat, [offset[i+1] - offset[i] for i in range(len(offset)-1)]) + + pred_masks = [] + + for i in range(len(feat)): + ( + sparse_embeddings, + dense_embeddings, + ) = self.visual_model.sam_prompt_encoder( + points=None, + boxes=None, + masks=None, + text_embeds=feat[i], + ) + sparse_embeddings = sparse_embeddings.to(feat[i].dtype) + high_res_features = [ + feat_level[i].unsqueeze(0) + for feat_level in _features["high_res_feats"] + ] + low_res_masks, iou_predictions, _, _ = self.visual_model.sam_mask_decoder( + image_embeddings=_features["image_embed"][i].unsqueeze(0), + image_pe=self.visual_model.sam_prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + repeat_image = True, + high_res_features=high_res_features, + ) + + if multimask_output: + sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1)[:, :1] + + pred_mask = self.postprocess_masks( + low_res_masks, + orig_hw=label_list[i].shape, + ) + pred_masks.append(pred_mask[:, 0]) + + gt_masks = masks_list + + if inference: + return { + "pred_masks": pred_masks, + "gt_masks": gt_masks, + } + + mask_bce_loss = 0 + mask_dice_loss = 0 + num_masks = 0 + for batch_idx in range(len(pred_masks)): + gt_mask = gt_masks[batch_idx] + pred_mask = pred_masks[batch_idx] + + assert ( + gt_mask.shape[0] == pred_mask.shape[0] + ), "gt_mask.shape: {}, pred_mask.shape: {}".format( + gt_mask.shape, pred_mask.shape + ) + mask_bce_loss += ( + sigmoid_ce_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + * gt_mask.shape[0] + ) + mask_dice_loss += ( + dice_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + * gt_mask.shape[0] + ) + num_masks += gt_mask.shape[0] + + mask_bce_loss = self.bce_loss_weight * mask_bce_loss / (num_masks + 1e-8) + mask_dice_loss = self.dice_loss_weight * mask_dice_loss / (num_masks + 1e-8) + mask_loss = mask_bce_loss + mask_dice_loss + + loss = mask_loss + + return { + "loss": loss, + "mask_bce_loss": mask_bce_loss, + "mask_dice_loss": mask_dice_loss, + "mask_loss": mask_loss, + } + + def inference( + self, + images, + images_evf, + input_ids, + resize_list, + original_size_list, + multimask_output=False, + ): + with torch.no_grad(): + backbone_out = self.visual_model.forward_image(images) + # dict_keys(['vision_features', 'vision_pos_enc', 'backbone_fpn']) + _, image_embeddings, _, _ = self.visual_model._prepare_backbone_features(backbone_out) + image_embeddings = [_.to(images.dtype) for _ in image_embeddings] + batch_size = images.shape[0] + if self.visual_model.directly_add_no_mem_embed: + image_embeddings[-1] = image_embeddings[-1] + self.visual_model.no_mem_embed + + feats = [ + feat.permute(1, 2, 0).view(batch_size, -1, *feat_size) + for feat, feat_size in zip(image_embeddings[::-1], self._bb_feat_sizes[::-1]) + ][::-1] + _features = {"image_embed": feats[-1], "high_res_feats": feats[:-1]} + + + multimask_output = multimask_output + + output = self.mm_extractor.beit3(visual_tokens=images_evf, textual_tokens=input_ids, text_padding_position=torch.zeros_like(input_ids)) + + feat = output["encoder_out"][:, :1, ...] + feat = self.text_hidden_fcs[0](feat) + ( + sparse_embeddings, + dense_embeddings, + ) = self.visual_model.sam_prompt_encoder( + points=None, + boxes=None, + masks=None, + text_embeds=feat, + ) + high_res_features = _features["high_res_feats"] + sparse_embeddings = sparse_embeddings.to(feat.dtype) + low_res_masks, iou_predictions, _, _ = self.visual_model.sam_mask_decoder( + image_embeddings=_features["image_embed"], + image_pe=self.visual_model.sam_prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + repeat_image = True, + high_res_features=high_res_features, + ) + if multimask_output: + sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1)[:, :1] + + pred_mask = self.postprocess_masks( + low_res_masks, + orig_hw=original_size_list[0], + ) + + return pred_mask[:, 0] + + +AutoConfig.register("evf", EvfConfig) +AutoModelForCausalLM.register(EvfConfig, EvfSam2Model) \ No newline at end of file diff --git a/py/evf-sam/model/evf_sam2_video.py b/py/evf-sam/model/evf_sam2_video.py new file mode 100644 index 0000000..ef2499a --- /dev/null +++ b/py/evf-sam/model/evf_sam2_video.py @@ -0,0 +1,321 @@ +from typing import List +import os +import torch +import torch.nn as nn +import torch.nn.functional as F +from transformers import PreTrainedModel, AutoConfig, AutoModelForCausalLM +from .segment_anything_2.sam2.build_sam import build_sam2, build_sam2_video_predictor +from .unilm.beit3.modeling_utils import BEiT3Wrapper, _get_base_config, _get_large_config +from .configuration_evf import EvfConfig +from .segment_anything_2.sam2.utils.misc import load_video_frames +from collections import OrderedDict + + + +def dice_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, + scale=1000, # 100000.0, + eps=1e-6, +): + """ + Compute the DICE loss, similar to generalized IOU for masks + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + """ + inputs = inputs.sigmoid() + inputs = inputs.flatten(1, 2) + targets = targets.flatten(1, 2) + numerator = 2 * (inputs / scale * targets).sum(-1) + denominator = (inputs / scale).sum(-1) + (targets / scale).sum(-1) + loss = 1 - (numerator + eps) / (denominator + eps) + loss = loss.sum() / (num_masks + 1e-8) + return loss + + +def sigmoid_ce_loss( + inputs: torch.Tensor, + targets: torch.Tensor, + num_masks: float, +): + """ + Args: + inputs: A float tensor of arbitrary shape. + The predictions for each example. + targets: A float tensor with the same shape as inputs. Stores the binary + classification label for each element in inputs + (0 for the negative class and 1 for the positive class). + Returns: + Loss tensor + """ + loss = F.binary_cross_entropy_with_logits(inputs, targets, reduction="none") + loss = loss.flatten(1, 2).mean(1).sum() / (num_masks + 1e-8) + return loss + +class EvfSam2Model(PreTrainedModel): + config_class = EvfConfig + def __init__( + self, + config, + **kwargs + ): + super(EvfSam2Model, self).__init__(config) + + self.config = config + self.vision_pretrained = kwargs.get("vision_pretrained", None) + self.encoder_pretrained = kwargs.get("encoder_pretrained", None) + self.dice_loss_weight = kwargs.get("dice_loss_weight", None) + self.bce_loss_weight = kwargs.get("bce_loss_weight", None) + self.train_mask_decoder = kwargs.get("train_mask_decoder", False) + self.train_prompt_encoder = kwargs.get("train_prompt_encoder", False) + self.initialize_evf_modules(config) + self._bb_feat_sizes = [ + (256, 256), + (128, 128), + (64, 64), + ] + + def initialize_evf_modules(self, config): + # SAM + if config.sam_scale=="large": + self.visual_model = build_sam2_video_predictor("sam2_hiera_l.yaml", self.vision_pretrained, device=None) + elif config.sam_scale=="tiny": + self.visual_model = build_sam2_video_predictor("sam2_hiera_t.yaml", self.vision_pretrained, device=None) + else: + raise NotImplementedError + + for param in self.visual_model.parameters(): + param.requires_grad = False + if self.train_mask_decoder: + self.visual_model.sam_mask_decoder.train() + for param in self.visual_model.sam_mask_decoder.parameters(): + param.requires_grad = True + if self.train_prompt_encoder: + self.visual_model.sam_prompt_encoder.no_mask_embed.requires_grad_(True) + + # beit-3 + if self.config.mm_extractor_scale == "base": + beit_config = _get_base_config() + elif self.config.mm_extractor_scale == "large": + beit_config = _get_large_config() + else: + raise AttributeError(f"model config should contain key 'mm_extractor_scale', with value 'base' or 'large'.") + + self.mm_extractor = BEiT3Wrapper(beit_config) + if self.encoder_pretrained is not None: + beit_state_dict = torch.load(self.encoder_pretrained)["model"] + self.mm_extractor.load_state_dict( + beit_state_dict, + strict=False + ) + + for param in self.mm_extractor.parameters(): + param.requires_grad = True + + # Projection layer + in_dim = config.hidden_size + assert in_dim==beit_config.encoder_embed_dim, \ + f"projection layer dim {in_dim} mismatch with mm_extractor dim {beit_config.encoder_embed_dim}" + out_dim = config.out_dim + text_fc = [ + nn.Linear(in_dim, in_dim), + nn.ReLU(), + nn.Linear(in_dim, out_dim) + ] + self.text_hidden_fcs = nn.ModuleList([nn.Sequential(*text_fc)]) + self.text_hidden_fcs.train() + for param in self.text_hidden_fcs.parameters(): + param.requires_grad = True + + + def postprocess_masks(self, masks: torch.Tensor, orig_hw) -> torch.Tensor: + """ + Perform PostProcessing on output masks. + """ + masks = masks.float() + masks = F.interpolate(masks, orig_hw, mode="bilinear", align_corners=False) + return masks + + # def forward( + # self, + # images: torch.FloatTensor, + # images_evf: torch.FloatTensor, + # input_ids: torch.LongTensor, + # attention_masks: torch.LongTensor, + # offset: torch.LongTensor, + # masks_list: List[torch.FloatTensor], + # label_list: List[torch.Tensor], + # resize_list: List[tuple], + # inference: bool = False, + # **kwargs, + # ): + # # image_embeddings = self.get_visual_embs(images) + # backbone_out = self.visual_model.forward_image(images) + # # dict_keys(['vision_features', 'vision_pos_enc', 'backbone_fpn']) + # _, image_embeddings, _, _ = self.visual_model._prepare_backbone_features(backbone_out) + # image_embeddings = [_.to(images.dtype) for _ in image_embeddings] + # batch_size = images.shape[0] + # if self.visual_model.directly_add_no_mem_embed: + # image_embeddings[-1] = image_embeddings[-1] + self.visual_model.no_mem_embed + + # feats = [ + # feat.permute(1, 2, 0).view(batch_size, -1, *feat_size) + # for feat, feat_size in zip(image_embeddings[::-1], self._bb_feat_sizes[::-1]) + # ][::-1] + # _features = {"image_embed": feats[-1], "high_res_feats": feats[:-1]} + + + # assert batch_size == len(offset) - 1 + + # images_evf_list = [] + # for i in range(len(offset) - 1): + # start_i, end_i = offset[i], offset[i + 1] + # images_evf_i = ( + # images_evf[i] + # .unsqueeze(0) + # .expand(end_i - start_i, -1, -1, -1) + # .contiguous() + # ) + # images_evf_list.append(images_evf_i) + # images_evf = torch.cat(images_evf_list, dim=0) + + # multimask_output = False + # output = self.mm_extractor.beit3( + # visual_tokens=images_evf, + # textual_tokens=input_ids, + # text_padding_position=~attention_masks + # ) + + # feat = output["encoder_out"][:, :1, ...] + + # feat = self.text_hidden_fcs[0](feat) + # feat = torch.split(feat, [offset[i+1] - offset[i] for i in range(len(offset)-1)]) + + # pred_masks = [] + + # for i in range(len(feat)): + # ( + # sparse_embeddings, + # dense_embeddings, + # ) = self.visual_model.sam_prompt_encoder( + # points=None, + # boxes=None, + # masks=None, + # text_embeds=feat[i], + # ) + # sparse_embeddings = sparse_embeddings.to(feat[i].dtype) + # high_res_features = [ + # feat_level[i].unsqueeze(0) + # for feat_level in _features["high_res_feats"] + # ] + # low_res_masks, iou_predictions, _, _ = self.visual_model.sam_mask_decoder( + # image_embeddings=_features["image_embed"][i].unsqueeze(0), + # image_pe=self.visual_model.sam_prompt_encoder.get_dense_pe(), + # sparse_prompt_embeddings=sparse_embeddings, + # dense_prompt_embeddings=dense_embeddings, + # multimask_output=multimask_output, + # repeat_image = True, + # high_res_features=high_res_features, + # ) + + # if multimask_output: + # sorted_ids = torch.argsort(iou_predictions, dim=-1, descending=True) + # low_res_masks = torch.take_along_dim(low_res_masks, sorted_ids[..., None, None], dim=1)[:, :1] + + # pred_mask = self.postprocess_masks( + # low_res_masks, + # orig_hw=label_list[i].shape, + # ) + # pred_masks.append(pred_mask[:, 0]) + + # gt_masks = masks_list + + # if inference: + # return { + # "pred_masks": pred_masks, + # "gt_masks": gt_masks, + # } + + # mask_bce_loss = 0 + # mask_dice_loss = 0 + # num_masks = 0 + # for batch_idx in range(len(pred_masks)): + # gt_mask = gt_masks[batch_idx] + # pred_mask = pred_masks[batch_idx] + + # assert ( + # gt_mask.shape[0] == pred_mask.shape[0] + # ), "gt_mask.shape: {}, pred_mask.shape: {}".format( + # gt_mask.shape, pred_mask.shape + # ) + # mask_bce_loss += ( + # sigmoid_ce_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + # * gt_mask.shape[0] + # ) + # mask_dice_loss += ( + # dice_loss(pred_mask, gt_mask, num_masks=gt_mask.shape[0]) + # * gt_mask.shape[0] + # ) + # num_masks += gt_mask.shape[0] + + # mask_bce_loss = self.bce_loss_weight * mask_bce_loss / (num_masks + 1e-8) + # mask_dice_loss = self.dice_loss_weight * mask_dice_loss / (num_masks + 1e-8) + # mask_loss = mask_bce_loss + mask_dice_loss + + # loss = mask_loss + + # return { + # "loss": loss, + # "mask_bce_loss": mask_bce_loss, + # "mask_dice_loss": mask_dice_loss, + # "mask_loss": mask_loss, + # } + + def inference( + self, + video_path, + images_evf, + input_ids, + # original_size_list, + multimask_output=False, + ): + predictor = self.visual_model + inference_state = predictor.init_state(video_path=video_path) + predictor.reset_state(inference_state) + + + multimask_output = multimask_output + + output = self.mm_extractor.beit3(visual_tokens=images_evf, textual_tokens=input_ids, text_padding_position=torch.zeros_like(input_ids)) + + feat = output["encoder_out"][:, :1, ...] + feat = self.text_hidden_fcs[0](feat) + + ann_frame_idx = 0 # the frame index we interact with + ann_obj_id = 1 # give a unique id to each object we interact with (it can be any integers) + + _, out_obj_ids, out_mask_logits = predictor.add_new_text( + inference_state=inference_state, + frame_idx=ann_frame_idx, + obj_id=ann_obj_id, + text=feat + ) + + # run propagation throughout the video and collect the results in a dict + video_segments = {} # video_segments contains the per-frame segmentation results + for out_frame_idx, out_obj_ids, out_mask_logits in predictor.propagate_in_video(inference_state): + video_segments[out_frame_idx] = { + out_obj_id: (out_mask_logits[i] > 0.0).cpu().numpy() + for i, out_obj_id in enumerate(out_obj_ids) + } + + return video_segments + + +AutoConfig.register("evf", EvfConfig) +AutoModelForCausalLM.register(EvfConfig, EvfSam2Model) \ No newline at end of file diff --git a/py/evf-sam/model/segment_anything/__init__.py b/py/evf-sam/model/segment_anything/__init__.py new file mode 100644 index 0000000..e66218b --- /dev/null +++ b/py/evf-sam/model/segment_anything/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from .automatic_mask_generator import SamAutomaticMaskGenerator +from .build_sam import (build_sam, build_sam_vit_b, build_sam_vit_h, + build_sam_vit_l, sam_model_registry) +from .predictor import SamPredictor diff --git a/py/evf-sam/model/segment_anything/automatic_mask_generator.py b/py/evf-sam/model/segment_anything/automatic_mask_generator.py new file mode 100644 index 0000000..aa4bc4f --- /dev/null +++ b/py/evf-sam/model/segment_anything/automatic_mask_generator.py @@ -0,0 +1,372 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import torch +from torchvision.ops.boxes import batched_nms, box_area # type: ignore + +from .modeling import Sam +from .predictor import SamPredictor +from .utils.amg import (MaskData, area_from_rle, batch_iterator, + batched_mask_to_box, box_xyxy_to_xywh, + build_all_layer_point_grids, calculate_stability_score, + coco_encode_rle, generate_crop_boxes, + is_box_near_crop_edge, mask_to_rle_pytorch, + remove_small_regions, rle_to_mask, uncrop_boxes_xyxy, + uncrop_masks, uncrop_points) + + +class SamAutomaticMaskGenerator: + def __init__( + self, + model: Sam, + points_per_side: Optional[int] = 32, + points_per_batch: int = 64, + pred_iou_thresh: float = 0.88, + stability_score_thresh: float = 0.95, + stability_score_offset: float = 1.0, + box_nms_thresh: float = 0.7, + crop_n_layers: int = 0, + crop_nms_thresh: float = 0.7, + crop_overlap_ratio: float = 512 / 1500, + crop_n_points_downscale_factor: int = 1, + point_grids: Optional[List[np.ndarray]] = None, + min_mask_region_area: int = 0, + output_mode: str = "binary_mask", + ) -> None: + """ + Using a SAM model, generates masks for the entire image. + Generates a grid of point prompts over the image, then filters + low quality and duplicate masks. The default settings are chosen + for SAM with a ViT-H backbone. + + Arguments: + model (Sam): The SAM model to use for mask prediction. + points_per_side (int or None): The number of points to be sampled + along one side of the image. The total number of points is + points_per_side**2. If None, 'point_grids' must provide explicit + point sampling. + points_per_batch (int): Sets the number of points run simultaneously + by the model. Higher numbers may be faster but use more GPU memory. + pred_iou_thresh (float): A filtering threshold in [0,1], using the + model's predicted mask quality. + stability_score_thresh (float): A filtering threshold in [0,1], using + the stability of the mask under changes to the cutoff used to binarize + the model's mask predictions. + stability_score_offset (float): The amount to shift the cutoff when + calculated the stability score. + box_nms_thresh (float): The box IoU cutoff used by non-maximal + suppression to filter duplicate masks. + crop_n_layers (int): If >0, mask prediction will be run again on + crops of the image. Sets the number of layers to run, where each + layer has 2**i_layer number of image crops. + crop_nms_thresh (float): The box IoU cutoff used by non-maximal + suppression to filter duplicate masks between different crops. + crop_overlap_ratio (float): Sets the degree to which crops overlap. + In the first crop layer, crops will overlap by this fraction of + the image length. Later layers with more crops scale down this overlap. + crop_n_points_downscale_factor (int): The number of points-per-side + sampled in layer n is scaled down by crop_n_points_downscale_factor**n. + point_grids (list(np.ndarray) or None): A list over explicit grids + of points used for sampling, normalized to [0,1]. The nth grid in the + list is used in the nth crop layer. Exclusive with points_per_side. + min_mask_region_area (int): If >0, postprocessing will be applied + to remove disconnected regions and holes in masks with area smaller + than min_mask_region_area. Requires opencv. + output_mode (str): The form masks are returned in. Can be 'binary_mask', + 'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools. + For large resolutions, 'binary_mask' may consume large amounts of + memory. + """ + + assert (points_per_side is None) != ( + point_grids is None + ), "Exactly one of points_per_side or point_grid must be provided." + if points_per_side is not None: + self.point_grids = build_all_layer_point_grids( + points_per_side, + crop_n_layers, + crop_n_points_downscale_factor, + ) + elif point_grids is not None: + self.point_grids = point_grids + else: + raise ValueError("Can't have both points_per_side and point_grid be None.") + + assert output_mode in [ + "binary_mask", + "uncompressed_rle", + "coco_rle", + ], f"Unknown output_mode {output_mode}." + if output_mode == "coco_rle": + from pycocotools import \ + mask as mask_utils # type: ignore # noqa: F401 + + if min_mask_region_area > 0: + import cv2 # type: ignore # noqa: F401 + + self.predictor = SamPredictor(model) + self.points_per_batch = points_per_batch + self.pred_iou_thresh = pred_iou_thresh + self.stability_score_thresh = stability_score_thresh + self.stability_score_offset = stability_score_offset + self.box_nms_thresh = box_nms_thresh + self.crop_n_layers = crop_n_layers + self.crop_nms_thresh = crop_nms_thresh + self.crop_overlap_ratio = crop_overlap_ratio + self.crop_n_points_downscale_factor = crop_n_points_downscale_factor + self.min_mask_region_area = min_mask_region_area + self.output_mode = output_mode + + @torch.no_grad() + def generate(self, image: np.ndarray) -> List[Dict[str, Any]]: + """ + Generates masks for the given image. + + Arguments: + image (np.ndarray): The image to generate masks for, in HWC uint8 format. + + Returns: + list(dict(str, any)): A list over records for masks. Each record is + a dict containing the following keys: + segmentation (dict(str, any) or np.ndarray): The mask. If + output_mode='binary_mask', is an array of shape HW. Otherwise, + is a dictionary containing the RLE. + bbox (list(float)): The box around the mask, in XYWH format. + area (int): The area in pixels of the mask. + predicted_iou (float): The model's own prediction of the mask's + quality. This is filtered by the pred_iou_thresh parameter. + point_coords (list(list(float))): The point coordinates input + to the model to generate this mask. + stability_score (float): A measure of the mask's quality. This + is filtered on using the stability_score_thresh parameter. + crop_box (list(float)): The crop of the image used to generate + the mask, given in XYWH format. + """ + + # Generate masks + mask_data = self._generate_masks(image) + + # Filter small disconnected regions and holes in masks + if self.min_mask_region_area > 0: + mask_data = self.postprocess_small_regions( + mask_data, + self.min_mask_region_area, + max(self.box_nms_thresh, self.crop_nms_thresh), + ) + + # Encode masks + if self.output_mode == "coco_rle": + mask_data["segmentations"] = [ + coco_encode_rle(rle) for rle in mask_data["rles"] + ] + elif self.output_mode == "binary_mask": + mask_data["segmentations"] = [rle_to_mask(rle) for rle in mask_data["rles"]] + else: + mask_data["segmentations"] = mask_data["rles"] + + # Write mask records + curr_anns = [] + for idx in range(len(mask_data["segmentations"])): + ann = { + "segmentation": mask_data["segmentations"][idx], + "area": area_from_rle(mask_data["rles"][idx]), + "bbox": box_xyxy_to_xywh(mask_data["boxes"][idx]).tolist(), + "predicted_iou": mask_data["iou_preds"][idx].item(), + "point_coords": [mask_data["points"][idx].tolist()], + "stability_score": mask_data["stability_score"][idx].item(), + "crop_box": box_xyxy_to_xywh(mask_data["crop_boxes"][idx]).tolist(), + } + curr_anns.append(ann) + + return curr_anns + + def _generate_masks(self, image: np.ndarray) -> MaskData: + orig_size = image.shape[:2] + crop_boxes, layer_idxs = generate_crop_boxes( + orig_size, self.crop_n_layers, self.crop_overlap_ratio + ) + + # Iterate over image crops + data = MaskData() + for crop_box, layer_idx in zip(crop_boxes, layer_idxs): + crop_data = self._process_crop(image, crop_box, layer_idx, orig_size) + data.cat(crop_data) + + # Remove duplicate masks between crops + if len(crop_boxes) > 1: + # Prefer masks from smaller crops + scores = 1 / box_area(data["crop_boxes"]) + scores = scores.to(data["boxes"].device) + keep_by_nms = batched_nms( + data["boxes"].float(), + scores, + torch.zeros_like(data["boxes"][:, 0]), # categories + iou_threshold=self.crop_nms_thresh, + ) + data.filter(keep_by_nms) + + data.to_numpy() + return data + + def _process_crop( + self, + image: np.ndarray, + crop_box: List[int], + crop_layer_idx: int, + orig_size: Tuple[int, ...], + ) -> MaskData: + # Crop the image and calculate embeddings + x0, y0, x1, y1 = crop_box + cropped_im = image[y0:y1, x0:x1, :] + cropped_im_size = cropped_im.shape[:2] + self.predictor.set_image(cropped_im) + + # Get points for this crop + points_scale = np.array(cropped_im_size)[None, ::-1] + points_for_image = self.point_grids[crop_layer_idx] * points_scale + + # Generate masks for this crop in batches + data = MaskData() + for (points,) in batch_iterator(self.points_per_batch, points_for_image): + batch_data = self._process_batch( + points, cropped_im_size, crop_box, orig_size + ) + data.cat(batch_data) + del batch_data + self.predictor.reset_image() + + # Remove duplicates within this crop. + keep_by_nms = batched_nms( + data["boxes"].float(), + data["iou_preds"], + torch.zeros_like(data["boxes"][:, 0]), # categories + iou_threshold=self.box_nms_thresh, + ) + data.filter(keep_by_nms) + + # Return to the original image frame + data["boxes"] = uncrop_boxes_xyxy(data["boxes"], crop_box) + data["points"] = uncrop_points(data["points"], crop_box) + data["crop_boxes"] = torch.tensor([crop_box for _ in range(len(data["rles"]))]) + + return data + + def _process_batch( + self, + points: np.ndarray, + im_size: Tuple[int, ...], + crop_box: List[int], + orig_size: Tuple[int, ...], + ) -> MaskData: + orig_h, orig_w = orig_size + + # Run model on this batch + transformed_points = self.predictor.transform.apply_coords(points, im_size) + in_points = torch.as_tensor(transformed_points, device=self.predictor.device) + in_labels = torch.ones( + in_points.shape[0], dtype=torch.int, device=in_points.device + ) + masks, iou_preds, _ = self.predictor.predict_torch( + in_points[:, None, :], + in_labels[:, None], + multimask_output=True, + return_logits=True, + ) + + # Serialize predictions and store in MaskData + data = MaskData( + masks=masks.flatten(0, 1), + iou_preds=iou_preds.flatten(0, 1), + points=torch.as_tensor(points.repeat(masks.shape[1], axis=0)), + ) + del masks + + # Filter by predicted IoU + if self.pred_iou_thresh > 0.0: + keep_mask = data["iou_preds"] > self.pred_iou_thresh + data.filter(keep_mask) + + # Calculate stability score + data["stability_score"] = calculate_stability_score( + data["masks"], + self.predictor.model.mask_threshold, + self.stability_score_offset, + ) + if self.stability_score_thresh > 0.0: + keep_mask = data["stability_score"] >= self.stability_score_thresh + data.filter(keep_mask) + + # Threshold masks and calculate boxes + data["masks"] = data["masks"] > self.predictor.model.mask_threshold + data["boxes"] = batched_mask_to_box(data["masks"]) + + # Filter boxes that touch crop boundaries + keep_mask = ~is_box_near_crop_edge( + data["boxes"], crop_box, [0, 0, orig_w, orig_h] + ) + if not torch.all(keep_mask): + data.filter(keep_mask) + + # Compress to RLE + data["masks"] = uncrop_masks(data["masks"], crop_box, orig_h, orig_w) + data["rles"] = mask_to_rle_pytorch(data["masks"]) + del data["masks"] + + return data + + @staticmethod + def postprocess_small_regions( + mask_data: MaskData, min_area: int, nms_thresh: float + ) -> MaskData: + """ + Removes small disconnected regions and holes in masks, then reruns + box NMS to remove any new duplicates. + + Edits mask_data in place. + + Requires open-cv as a dependency. + """ + if len(mask_data["rles"]) == 0: + return mask_data + + # Filter small disconnected regions and holes + new_masks = [] + scores = [] + for rle in mask_data["rles"]: + mask = rle_to_mask(rle) + + mask, changed = remove_small_regions(mask, min_area, mode="holes") + unchanged = not changed + mask, changed = remove_small_regions(mask, min_area, mode="islands") + unchanged = unchanged and not changed + + new_masks.append(torch.as_tensor(mask).unsqueeze(0)) + # Give score=0 to changed masks and score=1 to unchanged masks + # so NMS will prefer ones that didn't need postprocessing + scores.append(float(unchanged)) + + # Recalculate boxes and remove any new duplicates + masks = torch.cat(new_masks, dim=0) + boxes = batched_mask_to_box(masks) + keep_by_nms = batched_nms( + boxes.float(), + torch.as_tensor(scores), + torch.zeros_like(boxes[:, 0]), # categories + iou_threshold=nms_thresh, + ) + + # Only recalculate RLEs for masks that have changed + for i_mask in keep_by_nms: + if scores[i_mask] == 0.0: + mask_torch = masks[i_mask].unsqueeze(0) + mask_data["rles"][i_mask] = mask_to_rle_pytorch(mask_torch)[0] + mask_data["boxes"][i_mask] = boxes[i_mask] # update res directly + mask_data.filter(keep_by_nms) + + return mask_data diff --git a/py/evf-sam/model/segment_anything/build_sam.py b/py/evf-sam/model/segment_anything/build_sam.py new file mode 100644 index 0000000..788d25a --- /dev/null +++ b/py/evf-sam/model/segment_anything/build_sam.py @@ -0,0 +1,108 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from functools import partial + +import torch + +from .modeling import (ImageEncoderViT, MaskDecoder, PromptEncoder, Sam, + TwoWayTransformer) + + +def build_sam_vit_h(checkpoint=None): + return _build_sam( + encoder_embed_dim=1280, + encoder_depth=32, + encoder_num_heads=16, + encoder_global_attn_indexes=[7, 15, 23, 31], + checkpoint=checkpoint, + ) + + +build_sam = build_sam_vit_h + + +def build_sam_vit_l(checkpoint=None): + return _build_sam( + encoder_embed_dim=1024, + encoder_depth=24, + encoder_num_heads=16, + encoder_global_attn_indexes=[5, 11, 17, 23], + checkpoint=checkpoint, + ) + + +def build_sam_vit_b(checkpoint=None): + return _build_sam( + encoder_embed_dim=768, + encoder_depth=12, + encoder_num_heads=12, + encoder_global_attn_indexes=[2, 5, 8, 11], + checkpoint=checkpoint, + ) + + +sam_model_registry = { + "default": build_sam_vit_h, + "vit_h": build_sam_vit_h, + "vit_l": build_sam_vit_l, + "vit_b": build_sam_vit_b, +} + + +def _build_sam( + encoder_embed_dim, + encoder_depth, + encoder_num_heads, + encoder_global_attn_indexes, + checkpoint=None, +): + prompt_embed_dim = 256 + image_size = 1024 + vit_patch_size = 16 + image_embedding_size = image_size // vit_patch_size + sam = Sam( + image_encoder=ImageEncoderViT( + depth=encoder_depth, + embed_dim=encoder_embed_dim, + img_size=image_size, + mlp_ratio=4, + norm_layer=partial(torch.nn.LayerNorm, eps=1e-6), + num_heads=encoder_num_heads, + patch_size=vit_patch_size, + qkv_bias=True, + use_rel_pos=True, + global_attn_indexes=encoder_global_attn_indexes, + window_size=14, + out_chans=prompt_embed_dim, + ), + prompt_encoder=PromptEncoder( + embed_dim=prompt_embed_dim, + image_embedding_size=(image_embedding_size, image_embedding_size), + input_image_size=(image_size, image_size), + mask_in_chans=16, + ), + mask_decoder=MaskDecoder( + num_multimask_outputs=3, + transformer=TwoWayTransformer( + depth=2, + embedding_dim=prompt_embed_dim, + mlp_dim=2048, + num_heads=8, + ), + transformer_dim=prompt_embed_dim, + iou_head_depth=3, + iou_head_hidden_dim=256, + ), + pixel_mean=[123.675, 116.28, 103.53], + pixel_std=[58.395, 57.12, 57.375], + ) + sam.eval() + if checkpoint is not None: + with open(checkpoint, "rb") as f: + state_dict = torch.load(f) + sam.load_state_dict(state_dict, strict=False) + return sam diff --git a/py/evf-sam/model/segment_anything/modeling/__init__.py b/py/evf-sam/model/segment_anything/modeling/__init__.py new file mode 100644 index 0000000..088af38 --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from .image_encoder import ImageEncoderViT +from .mask_decoder import MaskDecoder +from .prompt_encoder import PromptEncoder +from .sam import Sam +from .transformer import TwoWayTransformer diff --git a/py/evf-sam/model/segment_anything/modeling/common.py b/py/evf-sam/model/segment_anything/modeling/common.py new file mode 100644 index 0000000..e872781 --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/common.py @@ -0,0 +1,43 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Type + +import torch +import torch.nn as nn + + +class MLPBlock(nn.Module): + def __init__( + self, + embedding_dim: int, + mlp_dim: int, + act: Type[nn.Module] = nn.GELU, + ) -> None: + super().__init__() + self.lin1 = nn.Linear(embedding_dim, mlp_dim) + self.lin2 = nn.Linear(mlp_dim, embedding_dim) + self.act = act() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.lin2(self.act(self.lin1(x))) + + +# From https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/batch_norm.py # noqa +# Itself from https://github.com/facebookresearch/ConvNeXt/blob/d1fa8f6fef0a165b27399986cc2bdacc92777e40/models/convnext.py#L119 # noqa +class LayerNorm2d(nn.Module): + def __init__(self, num_channels: int, eps: float = 1e-6) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(num_channels)) + self.bias = nn.Parameter(torch.zeros(num_channels)) + self.eps = eps + + def forward(self, x: torch.Tensor) -> torch.Tensor: + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + x = self.weight[:, None, None] * x + self.bias[:, None, None] + return x diff --git a/py/evf-sam/model/segment_anything/modeling/image_encoder.py b/py/evf-sam/model/segment_anything/modeling/image_encoder.py new file mode 100644 index 0000000..b472a3d --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/image_encoder.py @@ -0,0 +1,426 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Optional, Tuple, Type + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from .common import LayerNorm2d, MLPBlock + + +# This class and its supporting functions below lightly adapted from the ViTDet backbone available at: https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/vit.py # noqa +class ImageEncoderViT(nn.Module): + def __init__( + self, + img_size: int = 1024, + patch_size: int = 16, + in_chans: int = 3, + embed_dim: int = 768, + depth: int = 12, + num_heads: int = 12, + mlp_ratio: float = 4.0, + out_chans: int = 256, + qkv_bias: bool = True, + norm_layer: Type[nn.Module] = nn.LayerNorm, + act_layer: Type[nn.Module] = nn.GELU, + use_abs_pos: bool = True, + use_rel_pos: bool = False, + rel_pos_zero_init: bool = True, + window_size: int = 0, + global_attn_indexes: Tuple[int, ...] = (), + ) -> None: + """ + Args: + img_size (int): Input image size. + patch_size (int): Patch size. + in_chans (int): Number of input image channels. + embed_dim (int): Patch embedding dimension. + depth (int): Depth of ViT. + num_heads (int): Number of attention heads in each ViT block. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + qkv_bias (bool): If True, add a learnable bias to query, key, value. + norm_layer (nn.Module): Normalization layer. + act_layer (nn.Module): Activation layer. + use_abs_pos (bool): If True, use absolute positional embeddings. + use_rel_pos (bool): If True, add relative positional embeddings to the attention map. + rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. + window_size (int): Window size for window attention blocks. + global_attn_indexes (list): Indexes for blocks using global attention. + """ + super().__init__() + self.img_size = img_size + self.embed_dim = embed_dim + self.out_chans = out_chans + + self.patch_embed = PatchEmbed( + kernel_size=(patch_size, patch_size), + stride=(patch_size, patch_size), + in_chans=in_chans, + embed_dim=embed_dim, + ) + + self.pos_embed: Optional[nn.Parameter] = None + if use_abs_pos: + # Initialize absolute positional embedding with pretrain image size. + self.pos_embed = nn.Parameter( + torch.zeros( + 1, img_size // patch_size, img_size // patch_size, embed_dim + ) + ) + + self.blocks = nn.ModuleList() + for i in range(depth): + block = Block( + dim=embed_dim, + num_heads=num_heads, + mlp_ratio=mlp_ratio, + qkv_bias=qkv_bias, + norm_layer=norm_layer, + act_layer=act_layer, + use_rel_pos=use_rel_pos, + rel_pos_zero_init=rel_pos_zero_init, + window_size=window_size if i not in global_attn_indexes else 0, + input_size=(img_size // patch_size, img_size // patch_size), + ) + self.blocks.append(block) + + self.neck = nn.Sequential( + nn.Conv2d( + embed_dim, + out_chans, + kernel_size=1, + bias=False, + ), + LayerNorm2d(out_chans), + nn.Conv2d( + out_chans, + out_chans, + kernel_size=3, + padding=1, + bias=False, + ), + LayerNorm2d(out_chans), + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.patch_embed(x) + if self.pos_embed is not None: + x = x + self.pos_embed + + for blk in self.blocks: + x = blk(x) + + dtype = x.dtype + if dtype == torch.float16: # prevent overflow + with torch.autocast(device_type="cuda", dtype=torch.float32): + x = self.neck(x.permute(0, 3, 1, 2)) + x = x.to(dtype) + else: + x = self.neck(x.permute(0, 3, 1, 2)) + return x + + +class Block(nn.Module): + """Transformer blocks with support of window attention and residual propagation blocks""" + + def __init__( + self, + dim: int, + num_heads: int, + mlp_ratio: float = 4.0, + qkv_bias: bool = True, + norm_layer: Type[nn.Module] = nn.LayerNorm, + act_layer: Type[nn.Module] = nn.GELU, + use_rel_pos: bool = False, + rel_pos_zero_init: bool = True, + window_size: int = 0, + input_size: Optional[Tuple[int, int]] = None, + ) -> None: + """ + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads in each ViT block. + mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. + qkv_bias (bool): If True, add a learnable bias to query, key, value. + norm_layer (nn.Module): Normalization layer. + act_layer (nn.Module): Activation layer. + use_rel_pos (bool): If True, add relative positional embeddings to the attention map. + rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. + window_size (int): Window size for window attention blocks. If it equals 0, then + use global attention. + input_size (tuple(int, int) or None): Input resolution for calculating the relative + positional parameter size. + """ + super().__init__() + self.norm1 = norm_layer(dim) + self.attn = Attention( + dim, + num_heads=num_heads, + qkv_bias=qkv_bias, + use_rel_pos=use_rel_pos, + rel_pos_zero_init=rel_pos_zero_init, + input_size=input_size if window_size == 0 else (window_size, window_size), + ) + + self.norm2 = norm_layer(dim) + self.mlp = MLPBlock( + embedding_dim=dim, mlp_dim=int(dim * mlp_ratio), act=act_layer + ) + + self.window_size = window_size + + def forward(self, x: torch.Tensor) -> torch.Tensor: + shortcut = x + x = self.norm1(x) + # Window partition + if self.window_size > 0: + H, W = x.shape[1], x.shape[2] + x, pad_hw = window_partition(x, self.window_size) + + x = self.attn(x) + # Reverse window partition + if self.window_size > 0: + x = window_unpartition(x, self.window_size, pad_hw, (H, W)) + + x = shortcut + x + x = x + self.mlp(self.norm2(x)) + + return x + + +class Attention(nn.Module): + """Multi-head Attention block with relative position embeddings.""" + + def __init__( + self, + dim: int, + num_heads: int = 8, + qkv_bias: bool = True, + use_rel_pos: bool = False, + rel_pos_zero_init: bool = True, + input_size: Optional[Tuple[int, int]] = None, + ) -> None: + """ + Args: + dim (int): Number of input channels. + num_heads (int): Number of attention heads. + qkv_bias (bool): If True, add a learnable bias to query, key, value. + rel_pos (bool): If True, add relative positional embeddings to the attention map. + rel_pos_zero_init (bool): If True, zero initialize relative positional parameters. + input_size (tuple(int, int) or None): Input resolution for calculating the relative + positional parameter size. + """ + super().__init__() + self.num_heads = num_heads + head_dim = dim // num_heads + self.scale = head_dim**-0.5 + + self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias) + self.proj = nn.Linear(dim, dim) + + self.use_rel_pos = use_rel_pos + if self.use_rel_pos: + assert ( + input_size is not None + ), "Input size must be provided if using relative positional encoding." + # initialize relative positional embeddings + self.rel_pos_h = nn.Parameter(torch.zeros(2 * input_size[0] - 1, head_dim)) + self.rel_pos_w = nn.Parameter(torch.zeros(2 * input_size[1] - 1, head_dim)) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + B, H, W, _ = x.shape + # qkv with shape (3, B, nHead, H * W, C) + qkv = ( + self.qkv(x).reshape(B, H * W, 3, self.num_heads, -1).permute(2, 0, 3, 1, 4) + ) + # q, k, v with shape (B * nHead, H * W, C) + q, k, v = qkv.reshape(3, B * self.num_heads, H * W, -1).unbind(0) + + attn = (q * self.scale) @ k.transpose(-2, -1) + + if self.use_rel_pos: + attn = add_decomposed_rel_pos( + attn, q, self.rel_pos_h, self.rel_pos_w, (H, W), (H, W) + ) + + attn = attn.softmax(dim=-1) + x = ( + (attn @ v) + .view(B, self.num_heads, H, W, -1) + .permute(0, 2, 3, 1, 4) + .reshape(B, H, W, -1) + ) + x = self.proj(x) + + return x + + +def window_partition( + x: torch.Tensor, window_size: int +) -> Tuple[torch.Tensor, Tuple[int, int]]: + """ + Partition into non-overlapping windows with padding if needed. + Args: + x (tensor): input tokens with [B, H, W, C]. + window_size (int): window size. + + Returns: + windows: windows after partition with [B * num_windows, window_size, window_size, C]. + (Hp, Wp): padded height and width before partition + """ + B, H, W, C = x.shape + + pad_h = (window_size - H % window_size) % window_size + pad_w = (window_size - W % window_size) % window_size + if pad_h > 0 or pad_w > 0: + x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h)) + Hp, Wp = H + pad_h, W + pad_w + + x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C) + windows = ( + x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + ) + return windows, (Hp, Wp) + + +def window_unpartition( + windows: torch.Tensor, + window_size: int, + pad_hw: Tuple[int, int], + hw: Tuple[int, int], +) -> torch.Tensor: + """ + Window unpartition into original sequences and removing padding. + Args: + windows (tensor): input tokens with [B * num_windows, window_size, window_size, C]. + window_size (int): window size. + pad_hw (Tuple): padded height and width (Hp, Wp). + hw (Tuple): original height and width (H, W) before padding. + + Returns: + x: unpartitioned sequences with [B, H, W, C]. + """ + Hp, Wp = pad_hw + H, W = hw + B = windows.shape[0] // (Hp * Wp // window_size // window_size) + x = windows.view( + B, Hp // window_size, Wp // window_size, window_size, window_size, -1 + ) + x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1) + + if Hp > H or Wp > W: + x = x[:, :H, :W, :].contiguous() + return x + + +def get_rel_pos(q_size: int, k_size: int, rel_pos: torch.Tensor) -> torch.Tensor: + """ + Get relative positional embeddings according to the relative positions of + query and key sizes. + Args: + q_size (int): size of query q. + k_size (int): size of key k. + rel_pos (Tensor): relative position embeddings (L, C). + + Returns: + Extracted positional embeddings according to relative positions. + """ + max_rel_dist = int(2 * max(q_size, k_size) - 1) + # Interpolate rel pos if needed. + if rel_pos.shape[0] != max_rel_dist: + # Interpolate rel pos. + rel_pos_resized = F.interpolate( + rel_pos.reshape(1, rel_pos.shape[0], -1).permute(0, 2, 1), + size=max_rel_dist, + mode="linear", + ) + rel_pos_resized = rel_pos_resized.reshape(-1, max_rel_dist).permute(1, 0) + else: + rel_pos_resized = rel_pos + + # Scale the coords with short length if shapes for q and k are different. + q_coords = torch.arange(q_size)[:, None] * max(k_size / q_size, 1.0) + k_coords = torch.arange(k_size)[None, :] * max(q_size / k_size, 1.0) + relative_coords = (q_coords - k_coords) + (k_size - 1) * max(q_size / k_size, 1.0) + + return rel_pos_resized[relative_coords.long()] + + +def add_decomposed_rel_pos( + attn: torch.Tensor, + q: torch.Tensor, + rel_pos_h: torch.Tensor, + rel_pos_w: torch.Tensor, + q_size: Tuple[int, int], + k_size: Tuple[int, int], +) -> torch.Tensor: + """ + Calculate decomposed Relative Positional Embeddings from :paper:`mvitv2`. + https://github.com/facebookresearch/mvit/blob/19786631e330df9f3622e5402b4a419a263a2c80/mvit/models/attention.py # noqa B950 + Args: + attn (Tensor): attention map. + q (Tensor): query q in the attention layer with shape (B, q_h * q_w, C). + rel_pos_h (Tensor): relative position embeddings (Lh, C) for height axis. + rel_pos_w (Tensor): relative position embeddings (Lw, C) for width axis. + q_size (Tuple): spatial sequence size of query q with (q_h, q_w). + k_size (Tuple): spatial sequence size of key k with (k_h, k_w). + + Returns: + attn (Tensor): attention map with added relative positional embeddings. + """ + q_h, q_w = q_size + k_h, k_w = k_size + Rh = get_rel_pos(q_h, k_h, rel_pos_h) + Rw = get_rel_pos(q_w, k_w, rel_pos_w) + + B, _, dim = q.shape + r_q = q.reshape(B, q_h, q_w, dim) + rel_h = torch.einsum("bhwc,hkc->bhwk", r_q, Rh) + rel_w = torch.einsum("bhwc,wkc->bhwk", r_q, Rw) + + attn = ( + attn.view(B, q_h, q_w, k_h, k_w) + + rel_h[:, :, :, :, None] + + rel_w[:, :, :, None, :] + ).view(B, q_h * q_w, k_h * k_w) + + return attn + + +class PatchEmbed(nn.Module): + """ + Image to Patch Embedding. + """ + + def __init__( + self, + kernel_size: Tuple[int, int] = (16, 16), + stride: Tuple[int, int] = (16, 16), + padding: Tuple[int, int] = (0, 0), + in_chans: int = 3, + embed_dim: int = 768, + ) -> None: + """ + Args: + kernel_size (Tuple): kernel size of the projection layer. + stride (Tuple): stride of the projection layer. + padding (Tuple): padding size of the projection layer. + in_chans (int): Number of input image channels. + embed_dim (int): Patch embedding dimension. + """ + super().__init__() + + self.proj = nn.Conv2d( + in_chans, embed_dim, kernel_size=kernel_size, stride=stride, padding=padding + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.proj(x) + # B C H W -> B H W C + x = x.permute(0, 2, 3, 1) + return x diff --git a/py/evf-sam/model/segment_anything/modeling/mask_decoder.py b/py/evf-sam/model/segment_anything/modeling/mask_decoder.py new file mode 100644 index 0000000..fb104ea --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/mask_decoder.py @@ -0,0 +1,191 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import List, Tuple, Type + +import torch +from torch import nn +from torch.nn import functional as F + +from .common import LayerNorm2d + + +class MaskDecoder(nn.Module): + def __init__( + self, + *, + transformer_dim: int, + transformer: nn.Module, + num_multimask_outputs: int = 3, + activation: Type[nn.Module] = nn.GELU, + iou_head_depth: int = 3, + iou_head_hidden_dim: int = 256, + ) -> None: + """ + Predicts masks given an image and prompt embeddings, using a + transformer architecture. + + Arguments: + transformer_dim (int): the channel dimension of the transformer + transformer (nn.Module): the transformer used to predict masks + num_multimask_outputs (int): the number of masks to predict + when disambiguating masks + activation (nn.Module): the type of activation to use when + upscaling masks + iou_head_depth (int): the depth of the MLP used to predict + mask quality + iou_head_hidden_dim (int): the hidden dimension of the MLP + used to predict mask quality + """ + super().__init__() + self.transformer_dim = transformer_dim + self.transformer = transformer + + self.num_multimask_outputs = num_multimask_outputs + + self.iou_token = nn.Embedding(1, transformer_dim) + self.num_mask_tokens = num_multimask_outputs + 1 + self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim) + + self.output_upscaling = nn.Sequential( + nn.ConvTranspose2d( + transformer_dim, transformer_dim // 4, kernel_size=2, stride=2 + ), + LayerNorm2d(transformer_dim // 4), + activation(), + nn.ConvTranspose2d( + transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2 + ), + activation(), + ) + self.output_hypernetworks_mlps = nn.ModuleList( + [ + MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) + for i in range(self.num_mask_tokens) + ] + ) + + self.iou_prediction_head = MLP( + transformer_dim, iou_head_hidden_dim, self.num_mask_tokens, iou_head_depth + ) + + def forward( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + dense_prompt_embeddings: torch.Tensor, + multimask_output: bool, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Predict masks given image and prompt embeddings. + + Arguments: + image_embeddings (torch.Tensor): the embeddings from the image encoder + image_pe (torch.Tensor): positional encoding with the shape of image_embeddings + sparse_prompt_embeddings (torch.Tensor): the embeddings of the points and boxes + dense_prompt_embeddings (torch.Tensor): the embeddings of the mask inputs + multimask_output (bool): Whether to return multiple masks or a single + mask. + + Returns: + torch.Tensor: batched predicted masks + torch.Tensor: batched predictions of mask quality + """ + masks, iou_pred = self.predict_masks( + image_embeddings=image_embeddings, + image_pe=image_pe, + sparse_prompt_embeddings=sparse_prompt_embeddings, + dense_prompt_embeddings=dense_prompt_embeddings, + ) + + # Select the correct mask or masks for output + if multimask_output: + mask_slice = slice(1, None) + else: + mask_slice = slice(0, 1) + masks = masks[:, mask_slice, :, :] + iou_pred = iou_pred[:, mask_slice] + + # Prepare output + return masks, iou_pred + + def predict_masks( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + dense_prompt_embeddings: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Predicts masks. See 'forward' for more details.""" + # Concatenate output tokens + output_tokens = torch.cat( + [self.iou_token.weight, self.mask_tokens.weight], dim=0 + ) + output_tokens = output_tokens.unsqueeze(0).expand( + sparse_prompt_embeddings.size(0), -1, -1 + ) + + tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1) + + # image_embeddings: [1, C, H, W], tokens: [B, N, C] + # dense_prompt_embeddings: [B, C, H, W] + # Expand per-image data in batch direction to be per-mask + src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0) + src = src + dense_prompt_embeddings + pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) + b, c, h, w = src.shape + + # Run the transformer + hs, src = self.transformer(src, pos_src, tokens) + iou_token_out = hs[:, 0, :] + mask_tokens_out = hs[:, 1 : (1 + self.num_mask_tokens), :] + + # Upscale mask embeddings and predict masks using the mask tokens + src = src.transpose(1, 2).view(b, c, h, w) + upscaled_embedding = self.output_upscaling(src) + hyper_in_list: List[torch.Tensor] = [] + for i in range(self.num_mask_tokens): + hyper_in_list.append( + self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) + ) + hyper_in = torch.stack(hyper_in_list, dim=1) + b, c, h, w = upscaled_embedding.shape + masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view( + b, self.num_mask_tokens, h, w + ) + + # Generate mask quality predictions + iou_pred = self.iou_prediction_head(iou_token_out) + + return masks, iou_pred + + +# Lightly adapted from +# https://github.com/facebookresearch/MaskFormer/blob/main/mask_former/modeling/transformer/transformer_predictor.py # noqa +class MLP(nn.Module): + def __init__( + self, + input_dim: int, + hidden_dim: int, + output_dim: int, + num_layers: int, + sigmoid_output: bool = False, + ) -> None: + super().__init__() + self.num_layers = num_layers + h = [hidden_dim] * (num_layers - 1) + self.layers = nn.ModuleList( + nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim]) + ) + self.sigmoid_output = sigmoid_output + + def forward(self, x): + for i, layer in enumerate(self.layers): + x = F.relu(layer(x)) if i < self.num_layers - 1 else layer(x) + if self.sigmoid_output: + x = F.sigmoid(x) + return x diff --git a/py/evf-sam/model/segment_anything/modeling/prompt_encoder.py b/py/evf-sam/model/segment_anything/modeling/prompt_encoder.py new file mode 100644 index 0000000..16bc3a4 --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/prompt_encoder.py @@ -0,0 +1,238 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Any, Optional, Tuple, Type + +import numpy as np +import torch +from torch import nn + +from .common import LayerNorm2d + + +class PromptEncoder(nn.Module): + def __init__( + self, + embed_dim: int, + image_embedding_size: Tuple[int, int], + input_image_size: Tuple[int, int], + mask_in_chans: int, + activation: Type[nn.Module] = nn.GELU, + ) -> None: + """ + Encodes prompts for input to SAM's mask decoder. + + Arguments: + embed_dim (int): The prompts' embedding dimension + image_embedding_size (tuple(int, int)): The spatial size of the + image embedding, as (H, W). + input_image_size (int): The padded size of the image as input + to the image encoder, as (H, W). + mask_in_chans (int): The number of hidden channels used for + encoding input masks. + activation (nn.Module): The activation to use when encoding + input masks. + """ + super().__init__() + self.embed_dim = embed_dim + self.input_image_size = input_image_size + self.image_embedding_size = image_embedding_size + self.pe_layer = PositionEmbeddingRandom(embed_dim // 2) + + self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners + point_embeddings = [ + nn.Embedding(1, embed_dim) for i in range(self.num_point_embeddings) + ] + self.point_embeddings = nn.ModuleList(point_embeddings) + self.not_a_point_embed = nn.Embedding(1, embed_dim) + + self.mask_input_size = ( + 4 * image_embedding_size[0], + 4 * image_embedding_size[1], + ) + self.mask_downscaling = nn.Sequential( + nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2), + LayerNorm2d(mask_in_chans // 4), + activation(), + nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2), + LayerNorm2d(mask_in_chans), + activation(), + nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1), + ) + self.no_mask_embed = nn.Embedding(1, embed_dim) + + def get_dense_pe(self) -> torch.Tensor: + """ + Returns the positional encoding used to encode point prompts, + applied to a dense set of points the shape of the image encoding. + + Returns: + torch.Tensor: Positional encoding with shape + 1x(embed_dim)x(embedding_h)x(embedding_w) + """ + return self.pe_layer(self.image_embedding_size).unsqueeze(0) + + def _embed_points( + self, + points: torch.Tensor, + labels: torch.Tensor, + pad: bool, + ) -> torch.Tensor: + """Embeds point prompts.""" + points = points + 0.5 # Shift to center of pixel + if pad: + padding_point = torch.zeros((points.shape[0], 1, 2), device=points.device) + padding_label = -torch.ones((labels.shape[0], 1), device=labels.device) + points = torch.cat([points, padding_point], dim=1) + labels = torch.cat([labels, padding_label], dim=1) + point_embedding = self.pe_layer.forward_with_coords( + points, self.input_image_size + ) + point_embedding[labels == -1] = 0.0 + point_embedding[labels == -1] += self.not_a_point_embed.weight + point_embedding[labels == 0] += self.point_embeddings[0].weight + point_embedding[labels == 1] += self.point_embeddings[1].weight + return point_embedding + + def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor: + """Embeds box prompts.""" + boxes = boxes + 0.5 # Shift to center of pixel + coords = boxes.reshape(-1, 2, 2) + corner_embedding = self.pe_layer.forward_with_coords( + coords, self.input_image_size + ) + corner_embedding[:, 0, :] += self.point_embeddings[2].weight + corner_embedding[:, 1, :] += self.point_embeddings[3].weight + return corner_embedding + + def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor: + """Embeds mask inputs.""" + mask_embedding = self.mask_downscaling(masks) + return mask_embedding + + def _get_batch_size( + self, + points: Optional[Tuple[torch.Tensor, torch.Tensor]], + boxes: Optional[torch.Tensor], + masks: Optional[torch.Tensor], + text_embeds: Optional[torch.Tensor], + ) -> int: + """ + Gets the batch size of the output given the batch size of the input prompts. + """ + if points is not None: + return points[0].shape[0] + elif boxes is not None: + return boxes.shape[0] + elif masks is not None: + return masks.shape[0] + elif text_embeds is not None: + return text_embeds.shape[0] + else: + return 1 + + def _get_device(self) -> torch.device: + return self.point_embeddings[0].weight.device + + def forward( + self, + points: Optional[Tuple[torch.Tensor, torch.Tensor]], + boxes: Optional[torch.Tensor], + masks: Optional[torch.Tensor], + text_embeds: Optional[torch.Tensor], + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Embeds different types of prompts, returning both sparse and dense + embeddings. + + Arguments: + points (tuple(torch.Tensor, torch.Tensor) or none): point coordinates + and labels to embed. + boxes (torch.Tensor or none): boxes to embed + masks (torch.Tensor or none): masks to embed + + Returns: + torch.Tensor: sparse embeddings for the points and boxes, with shape + BxNx(embed_dim), where N is determined by the number of input points + and boxes. + torch.Tensor: dense embeddings for the masks, in the shape + Bx(embed_dim)x(embed_H)x(embed_W) + """ + bs = self._get_batch_size(points, boxes, masks, text_embeds) + sparse_embeddings = torch.empty( + (bs, 0, self.embed_dim), device=self._get_device() + ) + if points is not None: + coords, labels = points + point_embeddings = self._embed_points(coords, labels, pad=(boxes is None)) + sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1) + if boxes is not None: + box_embeddings = self._embed_boxes(boxes) + sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1) + + if text_embeds is not None: + sparse_embeddings = torch.cat([sparse_embeddings, text_embeds], dim=1) + + if masks is not None: + dense_embeddings = self._embed_masks(masks) + else: + dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand( + bs, -1, self.image_embedding_size[0], self.image_embedding_size[1] + ) + + return sparse_embeddings, dense_embeddings + + +class PositionEmbeddingRandom(nn.Module): + """ + Positional encoding using random spatial frequencies. + """ + + def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None: + super().__init__() + if scale is None or scale <= 0.0: + scale = 1.0 + self.register_buffer( + "positional_encoding_gaussian_matrix", + scale * torch.randn((2, num_pos_feats)), + ) + + def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor: + """Positionally encode points that are normalized to [0,1].""" + # assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape + coords = 2 * coords - 1 + + if coords.dtype != self.positional_encoding_gaussian_matrix.dtype: + coords = coords.to(self.positional_encoding_gaussian_matrix.dtype) + + coords = coords @ self.positional_encoding_gaussian_matrix + coords = 2 * np.pi * coords + # outputs d_1 x ... x d_n x C shape + return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1) + + def forward(self, size: Tuple[int, int]) -> torch.Tensor: + """Generate positional encoding for a grid of the specified size.""" + h, w = size + device: Any = self.positional_encoding_gaussian_matrix.device + grid = torch.ones( + (h, w), device=device, dtype=self.positional_encoding_gaussian_matrix.dtype + ) + y_embed = grid.cumsum(dim=0) - 0.5 + x_embed = grid.cumsum(dim=1) - 0.5 + y_embed = y_embed / h + x_embed = x_embed / w + + pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1)) + return pe.permute(2, 0, 1) # C x H x W + + def forward_with_coords( + self, coords_input: torch.Tensor, image_size: Tuple[int, int] + ) -> torch.Tensor: + """Positionally encode points that are not normalized to [0,1].""" + coords = coords_input.clone() + coords[:, :, 0] = coords[:, :, 0] / image_size[1] + coords[:, :, 1] = coords[:, :, 1] / image_size[0] + return self._pe_encoding(coords.to(torch.float)) # B x N x C diff --git a/py/evf-sam/model/segment_anything/modeling/sam.py b/py/evf-sam/model/segment_anything/modeling/sam.py new file mode 100644 index 0000000..f1d82ca --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/sam.py @@ -0,0 +1,184 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Any, Dict, List, Tuple + +import torch +from torch import nn +from torch.nn import functional as F + +from .image_encoder import ImageEncoderViT +from .mask_decoder import MaskDecoder +from .prompt_encoder import PromptEncoder + + +class Sam(nn.Module): + mask_threshold: float = 0.0 + image_format: str = "RGB" + + def __init__( + self, + image_encoder: ImageEncoderViT, + prompt_encoder: PromptEncoder, + mask_decoder: MaskDecoder, + pixel_mean: List[float] = [123.675, 116.28, 103.53], + pixel_std: List[float] = [58.395, 57.12, 57.375], + ) -> None: + """ + SAM predicts object masks from an image and input prompts. + + Arguments: + image_encoder (ImageEncoderViT): The backbone used to encode the + image into image embeddings that allow for efficient mask prediction. + prompt_encoder (PromptEncoder): Encodes various types of input prompts. + mask_decoder (MaskDecoder): Predicts masks from the image embeddings + and encoded prompts. + pixel_mean (list(float)): Mean values for normalizing pixels in the input image. + pixel_std (list(float)): Std values for normalizing pixels in the input image. + """ + super().__init__() + self.image_encoder = image_encoder + self.prompt_encoder = prompt_encoder + self.mask_decoder = mask_decoder + self.register_buffer( + "pixel_mean", torch.Tensor(pixel_mean).view(-1, 1, 1), False + ) + self.register_buffer("pixel_std", torch.Tensor(pixel_std).view(-1, 1, 1), False) + + @property + def device(self) -> Any: + return self.pixel_mean.device + + @torch.no_grad() + def forward( + self, + batched_input: List[Dict[str, Any]], + multimask_output: bool, + ) -> List[Dict[str, torch.Tensor]]: + """ + Predicts masks end-to-end from provided images and prompts. + If prompts are not known in advance, using SamPredictor is + recommended over calling the model directly. + + Arguments: + batched_input (list(dict)): A list over input images, each a + dictionary with the following keys. A prompt key can be + excluded if it is not present. + 'image': The image as a torch tensor in 3xHxW format, + already transformed for input to the model. + 'original_size': (tuple(int, int)) The original size of + the image before transformation, as (H, W). + 'point_coords': (torch.Tensor) Batched point prompts for + this image, with shape BxNx2. Already transformed to the + input frame of the model. + 'point_labels': (torch.Tensor) Batched labels for point prompts, + with shape BxN. + 'boxes': (torch.Tensor) Batched box inputs, with shape Bx4. + Already transformed to the input frame of the model. + 'mask_inputs': (torch.Tensor) Batched mask inputs to the model, + in the form Bx1xHxW. + multimask_output (bool): Whether the model should predict multiple + disambiguating masks, or return a single mask. + + Returns: + (list(dict)): A list over input images, where each element is + as dictionary with the following keys. + 'masks': (torch.Tensor) Batched binary mask predictions, + with shape BxCxHxW, where B is the number of input prompts, + C is determined by multimask_output, and (H, W) is the + original size of the image. + 'iou_predictions': (torch.Tensor) The model's predictions + of mask quality, in shape BxC. + 'low_res_logits': (torch.Tensor) Low resolution logits with + shape BxCxHxW, where H=W=256. Can be passed as mask input + to subsequent iterations of prediction. + """ + input_images = torch.stack( + [self.preprocess(x["image"]) for x in batched_input], dim=0 + ) + image_embeddings = self.image_encoder(input_images) + + outputs = [] + for image_record, curr_embedding in zip(batched_input, image_embeddings): + if "point_coords" in image_record: + points = (image_record["point_coords"], image_record["point_labels"]) + else: + points = None + sparse_embeddings, dense_embeddings = self.prompt_encoder( + points=points, + boxes=image_record.get("boxes", None), + masks=image_record.get("mask_inputs", None), + ) + low_res_masks, iou_predictions = self.mask_decoder( + image_embeddings=curr_embedding.unsqueeze(0), + image_pe=self.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + ) + masks = self.postprocess_masks( + low_res_masks, + input_size=image_record["image"].shape[-2:], + original_size=image_record["original_size"], + ) + masks = masks > self.mask_threshold + outputs.append( + { + "masks": masks, + "iou_predictions": iou_predictions, + "low_res_logits": low_res_masks, + } + ) + return outputs + + def postprocess_masks( + self, + masks: torch.Tensor, + input_size: Tuple[int, ...], + original_size: Tuple[int, ...], + ) -> torch.Tensor: + """ + Remove padding and upscale masks to the original image size. + + Arguments: + masks (torch.Tensor): Batched masks from the mask_decoder, + in BxCxHxW format. + input_size (tuple(int, int)): The size of the image input to the + model, in (H, W) format. Used to remove padding. + original_size (tuple(int, int)): The original size of the image + before resizing for input to the model, in (H, W) format. + + Returns: + (torch.Tensor): Batched masks in BxCxHxW format, where (H, W) + is given by original_size. + """ + + dtype = masks.dtype + + masks = F.interpolate( + masks.float(), + (self.image_encoder.img_size, self.image_encoder.img_size), + mode="bilinear", + align_corners=False, + ) + # masks = masks.to(dtype) + masks = masks[..., : input_size[0], : input_size[1]] + masks = F.interpolate( + masks, original_size, mode="bilinear", align_corners=False + ) + return masks + + def preprocess(self, x: torch.Tensor) -> torch.Tensor: + """Normalize pixel values and pad to a square input.""" + # Normalize colors + x = (x - self.pixel_mean) / self.pixel_std + + # Pad + h, w = x.shape[-2:] + padh = self.image_encoder.img_size - h + padw = self.image_encoder.img_size - w + x = F.pad(x, (0, padw, 0, padh)) + return x diff --git a/py/evf-sam/model/segment_anything/modeling/transformer.py b/py/evf-sam/model/segment_anything/modeling/transformer.py new file mode 100644 index 0000000..8c511e4 --- /dev/null +++ b/py/evf-sam/model/segment_anything/modeling/transformer.py @@ -0,0 +1,242 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from typing import Tuple, Type + +import torch +from torch import Tensor, nn + +from .common import MLPBlock + + +class TwoWayTransformer(nn.Module): + def __init__( + self, + depth: int, + embedding_dim: int, + num_heads: int, + mlp_dim: int, + activation: Type[nn.Module] = nn.ReLU, + attention_downsample_rate: int = 2, + ) -> None: + """ + A transformer decoder that attends to an input image using + queries whose positional embedding is supplied. + + Args: + depth (int): number of layers in the transformer + embedding_dim (int): the channel dimension for the input embeddings + num_heads (int): the number of heads for multihead attention. Must + divide embedding_dim + mlp_dim (int): the channel dimension internal to the MLP block + activation (nn.Module): the activation to use in the MLP block + """ + super().__init__() + self.depth = depth + self.embedding_dim = embedding_dim + self.num_heads = num_heads + self.mlp_dim = mlp_dim + self.layers = nn.ModuleList() + + for i in range(depth): + self.layers.append( + TwoWayAttentionBlock( + embedding_dim=embedding_dim, + num_heads=num_heads, + mlp_dim=mlp_dim, + activation=activation, + attention_downsample_rate=attention_downsample_rate, + skip_first_layer_pe=(i == 0), + ) + ) + + self.final_attn_token_to_image = Attention( + embedding_dim, num_heads, downsample_rate=attention_downsample_rate + ) + self.norm_final_attn = nn.LayerNorm(embedding_dim) + + def forward( + self, + image_embedding: Tensor, + image_pe: Tensor, + point_embedding: Tensor, + ) -> Tuple[Tensor, Tensor]: + """ + Args: + image_embedding (torch.Tensor): image to attend to. Should be shape + B x embedding_dim x h x w for any h and w. + image_pe (torch.Tensor): the positional encoding to add to the image. Must + have the same shape as image_embedding. + point_embedding (torch.Tensor): the embedding to add to the query points. + Must have shape B x N_points x embedding_dim for any N_points. + + Returns: + torch.Tensor: the processed point_embedding + torch.Tensor: the processed image_embedding + """ + # BxCxHxW -> BxHWxC == B x N_image_tokens x C + bs, c, h, w = image_embedding.shape + image_embedding = image_embedding.flatten(2).permute(0, 2, 1) + image_pe = image_pe.flatten(2).permute(0, 2, 1) + + # Prepare queries + queries = point_embedding + keys = image_embedding + + # Apply transformer blocks and final layernorm + for layer in self.layers: + queries, keys = layer( + queries=queries, + keys=keys, + query_pe=point_embedding, + key_pe=image_pe, + ) + + # Apply the final attention layer from the points to the image + q = queries + point_embedding + k = keys + image_pe + attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys) + queries = queries + attn_out + queries = self.norm_final_attn(queries) + + return queries, keys + + +class TwoWayAttentionBlock(nn.Module): + def __init__( + self, + embedding_dim: int, + num_heads: int, + mlp_dim: int = 2048, + activation: Type[nn.Module] = nn.ReLU, + attention_downsample_rate: int = 2, + skip_first_layer_pe: bool = False, + ) -> None: + """ + A transformer block with four layers: (1) self-attention of sparse + inputs, (2) cross attention of sparse inputs to dense inputs, (3) mlp + block on sparse inputs, and (4) cross attention of dense inputs to sparse + inputs. + + Arguments: + embedding_dim (int): the channel dimension of the embeddings + num_heads (int): the number of heads in the attention layers + mlp_dim (int): the hidden dimension of the mlp block + activation (nn.Module): the activation of the mlp block + skip_first_layer_pe (bool): skip the PE on the first layer + """ + super().__init__() + self.self_attn = Attention(embedding_dim, num_heads) + self.norm1 = nn.LayerNorm(embedding_dim) + + self.cross_attn_token_to_image = Attention( + embedding_dim, num_heads, downsample_rate=attention_downsample_rate + ) + self.norm2 = nn.LayerNorm(embedding_dim) + + self.mlp = MLPBlock(embedding_dim, mlp_dim, activation) + self.norm3 = nn.LayerNorm(embedding_dim) + + self.norm4 = nn.LayerNorm(embedding_dim) + self.cross_attn_image_to_token = Attention( + embedding_dim, num_heads, downsample_rate=attention_downsample_rate + ) + + self.skip_first_layer_pe = skip_first_layer_pe + + def forward( + self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor + ) -> Tuple[Tensor, Tensor]: + # Self attention block + if self.skip_first_layer_pe: + queries = self.self_attn(q=queries, k=queries, v=queries) + else: + q = queries + query_pe + attn_out = self.self_attn(q=q, k=q, v=queries) + queries = queries + attn_out + queries = self.norm1(queries) + + # Cross attention block, tokens attending to image embedding + q = queries + query_pe + k = keys + key_pe + attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys) + queries = queries + attn_out + queries = self.norm2(queries) + + # MLP block + mlp_out = self.mlp(queries) + queries = queries + mlp_out + queries = self.norm3(queries) + + # Cross attention block, image embedding attending to tokens + q = queries + query_pe + k = keys + key_pe + attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries) + keys = keys + attn_out + keys = self.norm4(keys) + + return queries, keys + + +class Attention(nn.Module): + """ + An attention layer that allows for downscaling the size of the embedding + after projection to queries, keys, and values. + """ + + def __init__( + self, + embedding_dim: int, + num_heads: int, + downsample_rate: int = 1, + ) -> None: + super().__init__() + self.embedding_dim = embedding_dim + self.internal_dim = embedding_dim // downsample_rate + self.num_heads = num_heads + assert ( + self.internal_dim % num_heads == 0 + ), "num_heads must divide embedding_dim." + + self.q_proj = nn.Linear(embedding_dim, self.internal_dim) + self.k_proj = nn.Linear(embedding_dim, self.internal_dim) + self.v_proj = nn.Linear(embedding_dim, self.internal_dim) + self.out_proj = nn.Linear(self.internal_dim, embedding_dim) + + def _separate_heads(self, x: Tensor, num_heads: int) -> Tensor: + b, n, c = x.shape + x = x.reshape(b, n, num_heads, c // num_heads) + return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head + + def _recombine_heads(self, x: Tensor) -> Tensor: + b, n_heads, n_tokens, c_per_head = x.shape + x = x.transpose(1, 2) + return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C + + def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor: + # Input projections + q = self.q_proj(q) + k = self.k_proj(k) + v = self.v_proj(v) + + # Separate into heads + q = self._separate_heads(q, self.num_heads) + k = self._separate_heads(k, self.num_heads) + v = self._separate_heads(v, self.num_heads) + + # Attention + _, _, _, c_per_head = q.shape + attn = q @ k.permute(0, 1, 3, 2) # B x N_heads x N_tokens x N_tokens + attn = attn / math.sqrt(c_per_head) + attn = torch.softmax(attn, dim=-1) + + # Get output + out = attn @ v + out = self._recombine_heads(out) + out = self.out_proj(out) + + return out diff --git a/py/evf-sam/model/segment_anything/predictor.py b/py/evf-sam/model/segment_anything/predictor.py new file mode 100644 index 0000000..bf52d81 --- /dev/null +++ b/py/evf-sam/model/segment_anything/predictor.py @@ -0,0 +1,284 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Optional, Tuple + +import numpy as np +import torch + +from .modeling import Sam +from .utils.transforms import ResizeLongestSide + + +class SamPredictor: + def __init__( + self, + sam_model: Sam, + ) -> None: + """ + Uses SAM to calculate the image embedding for an image, and then + allow repeated, efficient mask prediction given prompts. + + Arguments: + sam_model (Sam): The model to use for mask prediction. + """ + super().__init__() + self.model = sam_model + self.transform = ResizeLongestSide(sam_model.image_encoder.img_size) + self.reset_image() + + def set_image( + self, + image: np.ndarray, + image_format: str = "RGB", + ) -> None: + """ + Calculates the image embeddings for the provided image, allowing + masks to be predicted with the 'predict' method. + + Arguments: + image (np.ndarray): The image for calculating masks. Expects an + image in HWC uint8 format, with pixel values in [0, 255]. + image_format (str): The color format of the image, in ['RGB', 'BGR']. + """ + assert image_format in [ + "RGB", + "BGR", + ], f"image_format must be in ['RGB', 'BGR'], is {image_format}." + if image_format != self.model.image_format: + image = image[..., ::-1] + + # Transform the image to the form expected by the model + input_image = self.transform.apply_image(image) + input_image_torch = torch.as_tensor(input_image, device=self.device) + input_image_torch = input_image_torch.permute(2, 0, 1).contiguous()[ + None, :, :, : + ] + + self.set_torch_image(input_image_torch, image.shape[:2]) + + @torch.no_grad() + def set_torch_image( + self, + transformed_image: torch.Tensor, + original_image_size: Tuple[int, ...], + ) -> None: + """ + Calculates the image embeddings for the provided image, allowing + masks to be predicted with the 'predict' method. Expects the input + image to be already transformed to the format expected by the model. + + Arguments: + transformed_image (torch.Tensor): The input image, with shape + 1x3xHxW, which has been transformed with ResizeLongestSide. + original_image_size (tuple(int, int)): The size of the image + before transformation, in (H, W) format. + """ + assert ( + len(transformed_image.shape) == 4 + and transformed_image.shape[1] == 3 + and max(*transformed_image.shape[2:]) == self.model.image_encoder.img_size + ), f"set_torch_image input must be BCHW with long side {self.model.image_encoder.img_size}." + self.reset_image() + + self.original_size = original_image_size + self.input_size = tuple(transformed_image.shape[-2:]) + input_image = self.model.preprocess(transformed_image) + self.features = self.model.image_encoder(input_image) + self.is_image_set = True + + def predict( + self, + point_coords: Optional[np.ndarray] = None, + point_labels: Optional[np.ndarray] = None, + box: Optional[np.ndarray] = None, + mask_input: Optional[np.ndarray] = None, + multimask_output: bool = True, + return_logits: bool = False, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Predict masks for the given input prompts, using the currently set image. + + Arguments: + point_coords (np.ndarray or None): A Nx2 array of point prompts to the + model. Each point is in (X,Y) in pixels. + point_labels (np.ndarray or None): A length N array of labels for the + point prompts. 1 indicates a foreground point and 0 indicates a + background point. + box (np.ndarray or None): A length 4 array given a box prompt to the + model, in XYXY format. + mask_input (np.ndarray): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form 1xHxW, where + for SAM, H=W=256. + multimask_output (bool): If true, the model will return three masks. + For ambiguous input prompts (such as a single click), this will often + produce better masks than a single prediction. If only a single + mask is needed, the model's predicted quality score can be used + to select the best mask. For non-ambiguous prompts, such as multiple + input prompts, multimask_output=False can give better results. + return_logits (bool): If true, returns un-thresholded masks logits + instead of a binary mask. + + Returns: + (np.ndarray): The output masks in CxHxW format, where C is the + number of masks, and (H, W) is the original image size. + (np.ndarray): An array of length C containing the model's + predictions for the quality of each mask. + (np.ndarray): An array of shape CxHxW, where C is the number + of masks and H=W=256. These low resolution logits can be passed to + a subsequent iteration as mask input. + """ + if not self.is_image_set: + raise RuntimeError( + "An image must be set with .set_image(...) before mask prediction." + ) + + # Transform input prompts + coords_torch, labels_torch, box_torch, mask_input_torch = None, None, None, None + if point_coords is not None: + assert ( + point_labels is not None + ), "point_labels must be supplied if point_coords is supplied." + point_coords = self.transform.apply_coords(point_coords, self.original_size) + coords_torch = torch.as_tensor( + point_coords, dtype=torch.float, device=self.device + ) + labels_torch = torch.as_tensor( + point_labels, dtype=torch.int, device=self.device + ) + coords_torch, labels_torch = coords_torch[None, :, :], labels_torch[None, :] + if box is not None: + box = self.transform.apply_boxes(box, self.original_size) + box_torch = torch.as_tensor(box, dtype=torch.float, device=self.device) + box_torch = box_torch[None, :] + if mask_input is not None: + mask_input_torch = torch.as_tensor( + mask_input, dtype=torch.float, device=self.device + ) + mask_input_torch = mask_input_torch[None, :, :, :] + + masks, iou_predictions, low_res_masks = self.predict_torch( + coords_torch, + labels_torch, + box_torch, + mask_input_torch, + multimask_output, + return_logits=return_logits, + ) + + masks_np = masks[0].detach().cpu().numpy() + iou_predictions_np = iou_predictions[0].detach().cpu().numpy() + low_res_masks_np = low_res_masks[0].detach().cpu().numpy() + return masks_np, iou_predictions_np, low_res_masks_np + + @torch.no_grad() + def predict_torch( + self, + point_coords: Optional[torch.Tensor], + point_labels: Optional[torch.Tensor], + boxes: Optional[torch.Tensor] = None, + mask_input: Optional[torch.Tensor] = None, + multimask_output: bool = True, + return_logits: bool = False, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Predict masks for the given input prompts, using the currently set image. + Input prompts are batched torch tensors and are expected to already be + transformed to the input frame using ResizeLongestSide. + + Arguments: + point_coords (torch.Tensor or None): A BxNx2 array of point prompts to the + model. Each point is in (X,Y) in pixels. + point_labels (torch.Tensor or None): A BxN array of labels for the + point prompts. 1 indicates a foreground point and 0 indicates a + background point. + boxes (np.ndarray or None): A Bx4 array given a box prompt to the + model, in XYXY format. + mask_input (np.ndarray): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form Bx1xHxW, where + for SAM, H=W=256. Masks returned by a previous iteration of the + predict method do not need further transformation. + multimask_output (bool): If true, the model will return three masks. + For ambiguous input prompts (such as a single click), this will often + produce better masks than a single prediction. If only a single + mask is needed, the model's predicted quality score can be used + to select the best mask. For non-ambiguous prompts, such as multiple + input prompts, multimask_output=False can give better results. + return_logits (bool): If true, returns un-thresholded masks logits + instead of a binary mask. + + Returns: + (torch.Tensor): The output masks in BxCxHxW format, where C is the + number of masks, and (H, W) is the original image size. + (torch.Tensor): An array of shape BxC containing the model's + predictions for the quality of each mask. + (torch.Tensor): An array of shape BxCxHxW, where C is the number + of masks and H=W=256. These low res logits can be passed to + a subsequent iteration as mask input. + """ + if not self.is_image_set: + raise RuntimeError( + "An image must be set with .set_image(...) before mask prediction." + ) + + if point_coords is not None: + points = (point_coords, point_labels) + else: + points = None + + # Embed prompts + sparse_embeddings, dense_embeddings = self.model.prompt_encoder( + points=points, + boxes=boxes, + masks=mask_input, + ) + + # Predict masks + low_res_masks, iou_predictions = self.model.mask_decoder( + image_embeddings=self.features, + image_pe=self.model.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + ) + + # Upscale the masks to the original image resolution + masks = self.model.postprocess_masks( + low_res_masks, self.input_size, self.original_size + ) + + if not return_logits: + masks = masks > self.model.mask_threshold + + return masks, iou_predictions, low_res_masks + + def get_image_embedding(self) -> torch.Tensor: + """ + Returns the image embeddings for the currently set image, with + shape 1xCxHxW, where C is the embedding dimension and (H,W) are + the embedding spatial dimension of SAM (typically C=256, H=W=64). + """ + if not self.is_image_set: + raise RuntimeError( + "An image must be set with .set_image(...) to generate an embedding." + ) + assert ( + self.features is not None + ), "Features must exist if an image has been set." + return self.features + + @property + def device(self) -> torch.device: + return self.model.device + + def reset_image(self) -> None: + """Resets the currently set image.""" + self.is_image_set = False + self.features = None + self.orig_h = None + self.orig_w = None + self.input_h = None + self.input_w = None diff --git a/py/evf-sam/model/segment_anything/utils/__init__.py b/py/evf-sam/model/segment_anything/utils/__init__.py new file mode 100644 index 0000000..5277f46 --- /dev/null +++ b/py/evf-sam/model/segment_anything/utils/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. diff --git a/py/evf-sam/model/segment_anything/utils/amg.py b/py/evf-sam/model/segment_anything/utils/amg.py new file mode 100644 index 0000000..5c3bc5d --- /dev/null +++ b/py/evf-sam/model/segment_anything/utils/amg.py @@ -0,0 +1,346 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from copy import deepcopy +from itertools import product +from typing import Any, Dict, Generator, ItemsView, List, Tuple + +import numpy as np +import torch + + +class MaskData: + """ + A structure for storing masks and their related data in batched format. + Implements basic filtering and concatenation. + """ + + def __init__(self, **kwargs) -> None: + for v in kwargs.values(): + assert isinstance( + v, (list, np.ndarray, torch.Tensor) + ), "MaskData only supports list, numpy arrays, and torch tensors." + self._stats = dict(**kwargs) + + def __setitem__(self, key: str, item: Any) -> None: + assert isinstance( + item, (list, np.ndarray, torch.Tensor) + ), "MaskData only supports list, numpy arrays, and torch tensors." + self._stats[key] = item + + def __delitem__(self, key: str) -> None: + del self._stats[key] + + def __getitem__(self, key: str) -> Any: + return self._stats[key] + + def items(self) -> ItemsView[str, Any]: + return self._stats.items() + + def filter(self, keep: torch.Tensor) -> None: + for k, v in self._stats.items(): + if v is None: + self._stats[k] = None + elif isinstance(v, torch.Tensor): + self._stats[k] = v[torch.as_tensor(keep, device=v.device)] + elif isinstance(v, np.ndarray): + self._stats[k] = v[keep.detach().cpu().numpy()] + elif isinstance(v, list) and keep.dtype == torch.bool: + self._stats[k] = [a for i, a in enumerate(v) if keep[i]] + elif isinstance(v, list): + self._stats[k] = [v[i] for i in keep] + else: + raise TypeError(f"MaskData key {k} has an unsupported type {type(v)}.") + + def cat(self, new_stats: "MaskData") -> None: + for k, v in new_stats.items(): + if k not in self._stats or self._stats[k] is None: + self._stats[k] = deepcopy(v) + elif isinstance(v, torch.Tensor): + self._stats[k] = torch.cat([self._stats[k], v], dim=0) + elif isinstance(v, np.ndarray): + self._stats[k] = np.concatenate([self._stats[k], v], axis=0) + elif isinstance(v, list): + self._stats[k] = self._stats[k] + deepcopy(v) + else: + raise TypeError(f"MaskData key {k} has an unsupported type {type(v)}.") + + def to_numpy(self) -> None: + for k, v in self._stats.items(): + if isinstance(v, torch.Tensor): + self._stats[k] = v.detach().cpu().numpy() + + +def is_box_near_crop_edge( + boxes: torch.Tensor, crop_box: List[int], orig_box: List[int], atol: float = 20.0 +) -> torch.Tensor: + """Filter masks at the edge of a crop, but not at the edge of the original image.""" + crop_box_torch = torch.as_tensor(crop_box, dtype=torch.float, device=boxes.device) + orig_box_torch = torch.as_tensor(orig_box, dtype=torch.float, device=boxes.device) + boxes = uncrop_boxes_xyxy(boxes, crop_box).float() + near_crop_edge = torch.isclose(boxes, crop_box_torch[None, :], atol=atol, rtol=0) + near_image_edge = torch.isclose(boxes, orig_box_torch[None, :], atol=atol, rtol=0) + near_crop_edge = torch.logical_and(near_crop_edge, ~near_image_edge) + return torch.any(near_crop_edge, dim=1) + + +def box_xyxy_to_xywh(box_xyxy: torch.Tensor) -> torch.Tensor: + box_xywh = deepcopy(box_xyxy) + box_xywh[2] = box_xywh[2] - box_xywh[0] + box_xywh[3] = box_xywh[3] - box_xywh[1] + return box_xywh + + +def batch_iterator(batch_size: int, *args) -> Generator[List[Any], None, None]: + assert len(args) > 0 and all( + len(a) == len(args[0]) for a in args + ), "Batched iteration must have inputs of all the same size." + n_batches = len(args[0]) // batch_size + int(len(args[0]) % batch_size != 0) + for b in range(n_batches): + yield [arg[b * batch_size : (b + 1) * batch_size] for arg in args] + + +def mask_to_rle_pytorch(tensor: torch.Tensor) -> List[Dict[str, Any]]: + """ + Encodes masks to an uncompressed RLE, in the format expected by + pycoco tools. + """ + # Put in fortran order and flatten h,w + b, h, w = tensor.shape + tensor = tensor.permute(0, 2, 1).flatten(1) + + # Compute change indices + diff = tensor[:, 1:] ^ tensor[:, :-1] + change_indices = diff.nonzero() + + # Encode run length + out = [] + for i in range(b): + cur_idxs = change_indices[change_indices[:, 0] == i, 1] + cur_idxs = torch.cat( + [ + torch.tensor([0], dtype=cur_idxs.dtype, device=cur_idxs.device), + cur_idxs + 1, + torch.tensor([h * w], dtype=cur_idxs.dtype, device=cur_idxs.device), + ] + ) + btw_idxs = cur_idxs[1:] - cur_idxs[:-1] + counts = [] if tensor[i, 0] == 0 else [0] + counts.extend(btw_idxs.detach().cpu().tolist()) + out.append({"size": [h, w], "counts": counts}) + return out + + +def rle_to_mask(rle: Dict[str, Any]) -> np.ndarray: + """Compute a binary mask from an uncompressed RLE.""" + h, w = rle["size"] + mask = np.empty(h * w, dtype=bool) + idx = 0 + parity = False + for count in rle["counts"]: + mask[idx : idx + count] = parity + idx += count + parity ^= True + mask = mask.reshape(w, h) + return mask.transpose() # Put in C order + + +def area_from_rle(rle: Dict[str, Any]) -> int: + return sum(rle["counts"][1::2]) + + +def calculate_stability_score( + masks: torch.Tensor, mask_threshold: float, threshold_offset: float +) -> torch.Tensor: + """ + Computes the stability score for a batch of masks. The stability + score is the IoU between the binary masks obtained by thresholding + the predicted mask logits at high and low values. + """ + # One mask is always contained inside the other. + # Save memory by preventing unnecessary cast to torch.int64 + intersections = ( + (masks > (mask_threshold + threshold_offset)) + .sum(-1, dtype=torch.int16) + .sum(-1, dtype=torch.int32) + ) + unions = ( + (masks > (mask_threshold - threshold_offset)) + .sum(-1, dtype=torch.int16) + .sum(-1, dtype=torch.int32) + ) + return intersections / unions + + +def build_point_grid(n_per_side: int) -> np.ndarray: + """Generates a 2D grid of points evenly spaced in [0,1]x[0,1].""" + offset = 1 / (2 * n_per_side) + points_one_side = np.linspace(offset, 1 - offset, n_per_side) + points_x = np.tile(points_one_side[None, :], (n_per_side, 1)) + points_y = np.tile(points_one_side[:, None], (1, n_per_side)) + points = np.stack([points_x, points_y], axis=-1).reshape(-1, 2) + return points + + +def build_all_layer_point_grids( + n_per_side: int, n_layers: int, scale_per_layer: int +) -> List[np.ndarray]: + """Generates point grids for all crop layers.""" + points_by_layer = [] + for i in range(n_layers + 1): + n_points = int(n_per_side / (scale_per_layer**i)) + points_by_layer.append(build_point_grid(n_points)) + return points_by_layer + + +def generate_crop_boxes( + im_size: Tuple[int, ...], n_layers: int, overlap_ratio: float +) -> Tuple[List[List[int]], List[int]]: + """ + Generates a list of crop boxes of different sizes. Each layer + has (2**i)**2 boxes for the ith layer. + """ + crop_boxes, layer_idxs = [], [] + im_h, im_w = im_size + short_side = min(im_h, im_w) + + # Original image + crop_boxes.append([0, 0, im_w, im_h]) + layer_idxs.append(0) + + def crop_len(orig_len, n_crops, overlap): + return int(math.ceil((overlap * (n_crops - 1) + orig_len) / n_crops)) + + for i_layer in range(n_layers): + n_crops_per_side = 2 ** (i_layer + 1) + overlap = int(overlap_ratio * short_side * (2 / n_crops_per_side)) + + crop_w = crop_len(im_w, n_crops_per_side, overlap) + crop_h = crop_len(im_h, n_crops_per_side, overlap) + + crop_box_x0 = [int((crop_w - overlap) * i) for i in range(n_crops_per_side)] + crop_box_y0 = [int((crop_h - overlap) * i) for i in range(n_crops_per_side)] + + # Crops in XYWH format + for x0, y0 in product(crop_box_x0, crop_box_y0): + box = [x0, y0, min(x0 + crop_w, im_w), min(y0 + crop_h, im_h)] + crop_boxes.append(box) + layer_idxs.append(i_layer + 1) + + return crop_boxes, layer_idxs + + +def uncrop_boxes_xyxy(boxes: torch.Tensor, crop_box: List[int]) -> torch.Tensor: + x0, y0, _, _ = crop_box + offset = torch.tensor([[x0, y0, x0, y0]], device=boxes.device) + # Check if boxes has a channel dimension + if len(boxes.shape) == 3: + offset = offset.unsqueeze(1) + return boxes + offset + + +def uncrop_points(points: torch.Tensor, crop_box: List[int]) -> torch.Tensor: + x0, y0, _, _ = crop_box + offset = torch.tensor([[x0, y0]], device=points.device) + # Check if points has a channel dimension + if len(points.shape) == 3: + offset = offset.unsqueeze(1) + return points + offset + + +def uncrop_masks( + masks: torch.Tensor, crop_box: List[int], orig_h: int, orig_w: int +) -> torch.Tensor: + x0, y0, x1, y1 = crop_box + if x0 == 0 and y0 == 0 and x1 == orig_w and y1 == orig_h: + return masks + # Coordinate transform masks + pad_x, pad_y = orig_w - (x1 - x0), orig_h - (y1 - y0) + pad = (x0, pad_x - x0, y0, pad_y - y0) + return torch.nn.functional.pad(masks, pad, value=0) + + +def remove_small_regions( + mask: np.ndarray, area_thresh: float, mode: str +) -> Tuple[np.ndarray, bool]: + """ + Removes small disconnected regions and holes in a mask. Returns the + mask and an indicator of if the mask has been modified. + """ + import cv2 # type: ignore + + assert mode in ["holes", "islands"] + correct_holes = mode == "holes" + working_mask = (correct_holes ^ mask).astype(np.uint8) + n_labels, regions, stats, _ = cv2.connectedComponentsWithStats(working_mask, 8) + sizes = stats[:, -1][1:] # Row 0 is background label + small_regions = [i + 1 for i, s in enumerate(sizes) if s < area_thresh] + if len(small_regions) == 0: + return mask, False + fill_labels = [0] + small_regions + if not correct_holes: + fill_labels = [i for i in range(n_labels) if i not in fill_labels] + # If every region is below threshold, keep largest + if len(fill_labels) == 0: + fill_labels = [int(np.argmax(sizes)) + 1] + mask = np.isin(regions, fill_labels) + return mask, True + + +def coco_encode_rle(uncompressed_rle: Dict[str, Any]) -> Dict[str, Any]: + from pycocotools import mask as mask_utils # type: ignore + + h, w = uncompressed_rle["size"] + rle = mask_utils.frPyObjects(uncompressed_rle, h, w) + rle["counts"] = rle["counts"].decode("utf-8") # Necessary to serialize with json + return rle + + +def batched_mask_to_box(masks: torch.Tensor) -> torch.Tensor: + """ + Calculates boxes in XYXY format around masks. Return [0,0,0,0] for + an empty mask. For input shape C1xC2x...xHxW, the output shape is C1xC2x...x4. + """ + # torch.max below raises an error on empty inputs, just skip in this case + if torch.numel(masks) == 0: + return torch.zeros(*masks.shape[:-2], 4, device=masks.device) + + # Normalize shape to CxHxW + shape = masks.shape + h, w = shape[-2:] + if len(shape) > 2: + masks = masks.flatten(0, -3) + else: + masks = masks.unsqueeze(0) + + # Get top and bottom edges + in_height, _ = torch.max(masks, dim=-1) + in_height_coords = in_height * torch.arange(h, device=in_height.device)[None, :] + bottom_edges, _ = torch.max(in_height_coords, dim=-1) + in_height_coords = in_height_coords + h * (~in_height) + top_edges, _ = torch.min(in_height_coords, dim=-1) + + # Get left and right edges + in_width, _ = torch.max(masks, dim=-2) + in_width_coords = in_width * torch.arange(w, device=in_width.device)[None, :] + right_edges, _ = torch.max(in_width_coords, dim=-1) + in_width_coords = in_width_coords + w * (~in_width) + left_edges, _ = torch.min(in_width_coords, dim=-1) + + # If the mask is empty the right edge will be to the left of the left edge. + # Replace these boxes with [0, 0, 0, 0] + empty_filter = (right_edges < left_edges) | (bottom_edges < top_edges) + out = torch.stack([left_edges, top_edges, right_edges, bottom_edges], dim=-1) + out = out * (~empty_filter).unsqueeze(-1) + + # Return to original shape + if len(shape) > 2: + out = out.reshape(*shape[:-2], 4) + else: + out = out[0] + + return out diff --git a/py/evf-sam/model/segment_anything/utils/onnx.py b/py/evf-sam/model/segment_anything/utils/onnx.py new file mode 100644 index 0000000..3521208 --- /dev/null +++ b/py/evf-sam/model/segment_anything/utils/onnx.py @@ -0,0 +1,157 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Tuple + +import torch +import torch.nn as nn +from torch.nn import functional as F + +from ..modeling import Sam +from .amg import calculate_stability_score + + +class SamOnnxModel(nn.Module): + """ + This model should not be called directly, but is used in ONNX export. + It combines the prompt encoder, mask decoder, and mask postprocessing of Sam, + with some functions modified to enable model tracing. Also supports extra + options controlling what information. See the ONNX export script for details. + """ + + def __init__( + self, + model: Sam, + return_single_mask: bool, + use_stability_score: bool = False, + return_extra_metrics: bool = False, + ) -> None: + super().__init__() + self.mask_decoder = model.mask_decoder + self.model = model + self.img_size = model.image_encoder.img_size + self.return_single_mask = return_single_mask + self.use_stability_score = use_stability_score + self.stability_score_offset = 1.0 + self.return_extra_metrics = return_extra_metrics + + @staticmethod + def resize_longest_image_size( + input_image_size: torch.Tensor, longest_side: int + ) -> torch.Tensor: + input_image_size = input_image_size.to(torch.float32) + scale = longest_side / torch.max(input_image_size) + transformed_size = scale * input_image_size + transformed_size = torch.floor(transformed_size + 0.5).to(torch.int64) + return transformed_size + + def _embed_points( + self, point_coords: torch.Tensor, point_labels: torch.Tensor + ) -> torch.Tensor: + point_coords = point_coords + 0.5 + point_coords = point_coords / self.img_size + point_embedding = self.model.prompt_encoder.pe_layer._pe_encoding(point_coords) + point_labels = point_labels.unsqueeze(-1).expand_as(point_embedding) + + point_embedding = point_embedding * (point_labels != -1) + point_embedding = ( + point_embedding + + self.model.prompt_encoder.not_a_point_embed.weight * (point_labels == -1) + ) + + for i in range(self.model.prompt_encoder.num_point_embeddings): + point_embedding = ( + point_embedding + + self.model.prompt_encoder.point_embeddings[i].weight + * (point_labels == i) + ) + + return point_embedding + + def _embed_masks( + self, input_mask: torch.Tensor, has_mask_input: torch.Tensor + ) -> torch.Tensor: + mask_embedding = has_mask_input * self.model.prompt_encoder.mask_downscaling( + input_mask + ) + mask_embedding = mask_embedding + ( + 1 - has_mask_input + ) * self.model.prompt_encoder.no_mask_embed.weight.reshape(1, -1, 1, 1) + return mask_embedding + + def mask_postprocessing( + self, masks: torch.Tensor, orig_im_size: torch.Tensor + ) -> torch.Tensor: + masks = F.interpolate( + masks, + size=(self.img_size, self.img_size), + mode="bilinear", + align_corners=False, + ) + + prepadded_size = self.resize_longest_image_size(orig_im_size, self.img_size).to( + torch.int64 + ) + masks = masks[..., : prepadded_size[0], : prepadded_size[1]] # type: ignore + + orig_im_size = orig_im_size.to(torch.int64) + h, w = orig_im_size[0], orig_im_size[1] + masks = F.interpolate(masks, size=(h, w), mode="bilinear", align_corners=False) + return masks + + def select_masks( + self, masks: torch.Tensor, iou_preds: torch.Tensor, num_points: int + ) -> Tuple[torch.Tensor, torch.Tensor]: + # Determine if we should return the multiclick mask or not from the number of points. + # The reweighting is used to avoid control flow. + score_reweight = torch.tensor( + [[1000] + [0] * (self.model.mask_decoder.num_mask_tokens - 1)] + ).to(iou_preds.device) + score = iou_preds + (num_points - 2.5) * score_reweight + best_idx = torch.argmax(score, dim=1) + masks = masks[torch.arange(masks.shape[0]), best_idx, :, :].unsqueeze(1) + iou_preds = iou_preds[torch.arange(masks.shape[0]), best_idx].unsqueeze(1) + + return masks, iou_preds + + @torch.no_grad() + def forward( + self, + image_embeddings: torch.Tensor, + point_coords: torch.Tensor, + point_labels: torch.Tensor, + mask_input: torch.Tensor, + has_mask_input: torch.Tensor, + orig_im_size: torch.Tensor, + ): + sparse_embedding = self._embed_points(point_coords, point_labels) + dense_embedding = self._embed_masks(mask_input, has_mask_input) + + masks, scores = self.model.mask_decoder.predict_masks( + image_embeddings=image_embeddings, + image_pe=self.model.prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embedding, + dense_prompt_embeddings=dense_embedding, + ) + + if self.use_stability_score: + scores = calculate_stability_score( + masks, self.model.mask_threshold, self.stability_score_offset + ) + + if self.return_single_mask: + masks, scores = self.select_masks(masks, scores, point_coords.shape[1]) + + upscaled_masks = self.mask_postprocessing(masks, orig_im_size) + + if self.return_extra_metrics: + stability_scores = calculate_stability_score( + upscaled_masks, self.model.mask_threshold, self.stability_score_offset + ) + areas = (upscaled_masks > self.model.mask_threshold).sum(-1).sum(-1) + return upscaled_masks, scores, stability_scores, areas, masks + + return upscaled_masks, scores, masks diff --git a/py/evf-sam/model/segment_anything/utils/transforms.py b/py/evf-sam/model/segment_anything/utils/transforms.py new file mode 100644 index 0000000..4232d84 --- /dev/null +++ b/py/evf-sam/model/segment_anything/utils/transforms.py @@ -0,0 +1,113 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from copy import deepcopy +from typing import Tuple + +import numpy as np +import torch +from torch.nn import functional as F +from torchvision.transforms.functional import resize # type: ignore +from torchvision.transforms.functional import to_pil_image + + +class ResizeLongestSide: + """ + Resizes images to the longest side 'target_length', as well as provides + methods for resizing coordinates and boxes. Provides methods for + transforming both numpy array and batched torch tensors. + """ + + def __init__(self, target_length: int) -> None: + self.target_length = target_length + + def apply_image(self, image: np.ndarray) -> np.ndarray: + """ + Expects a numpy array with shape HxWxC in uint8 format. + """ + target_size = self.get_preprocess_shape( + image.shape[0], image.shape[1], self.target_length + ) + return np.array(resize(to_pil_image(image), target_size)) + + def apply_coords( + self, coords: np.ndarray, original_size: Tuple[int, ...] + ) -> np.ndarray: + """ + Expects a numpy array of length 2 in the final dimension. Requires the + original image size in (H, W) format. + """ + old_h, old_w = original_size + new_h, new_w = self.get_preprocess_shape( + original_size[0], original_size[1], self.target_length + ) + coords = deepcopy(coords).astype(float) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + def apply_boxes( + self, boxes: np.ndarray, original_size: Tuple[int, ...] + ) -> np.ndarray: + """ + Expects a numpy array shape Bx4. Requires the original image size + in (H, W) format. + """ + boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size) + return boxes.reshape(-1, 4) + + def apply_image_torch(self, image: torch.Tensor) -> torch.Tensor: + """ + Expects batched images with shape BxCxHxW and float format. This + transformation may not exactly match apply_image. apply_image is + the transformation expected by the model. + """ + # Expects an image in BCHW format. May not exactly match apply_image. + target_size = self.get_preprocess_shape( + image.shape[0], image.shape[1], self.target_length + ) + return F.interpolate( + image, target_size, mode="bilinear", align_corners=False, antialias=True + ) + + def apply_coords_torch( + self, coords: torch.Tensor, original_size: Tuple[int, ...] + ) -> torch.Tensor: + """ + Expects a torch tensor with length 2 in the last dimension. Requires the + original image size in (H, W) format. + """ + old_h, old_w = original_size + new_h, new_w = self.get_preprocess_shape( + original_size[0], original_size[1], self.target_length + ) + coords = deepcopy(coords).to(torch.float) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + def apply_boxes_torch( + self, boxes: torch.Tensor, original_size: Tuple[int, ...] + ) -> torch.Tensor: + """ + Expects a torch tensor with shape Bx4. Requires the original image + size in (H, W) format. + """ + boxes = self.apply_coords_torch(boxes.reshape(-1, 2, 2), original_size) + return boxes.reshape(-1, 4) + + @staticmethod + def get_preprocess_shape( + oldh: int, oldw: int, long_side_length: int + ) -> Tuple[int, int]: + """ + Compute the output size given input size and target long side length. + """ + scale = long_side_length * 1.0 / max(oldh, oldw) + newh, neww = oldh * scale, oldw * scale + neww = int(neww + 0.5) + newh = int(newh + 0.5) + return (newh, neww) diff --git a/py/evf-sam/model/segment_anything_2/sam2/__init__.py b/py/evf-sam/model/segment_anything_2/sam2/__init__.py new file mode 100644 index 0000000..6211a5b --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + + +from hydra import initialize_config_module + +initialize_config_module("model/segment_anything_2/sam2_configs", version_base="1.2") diff --git a/py/evf-sam/model/segment_anything_2/sam2/automatic_mask_generator.py b/py/evf-sam/model/segment_anything_2/sam2/automatic_mask_generator.py new file mode 100644 index 0000000..91b76f1 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/automatic_mask_generator.py @@ -0,0 +1,434 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +# Adapted from https://github.com/facebookresearch/segment-anything/blob/main/segment_anything/automatic_mask_generator.py +from typing import Any, Dict, List, Optional, Tuple + +import numpy as np +import torch +from torchvision.ops.boxes import batched_nms, box_area # type: ignore + +from model.segment_anything_2.sam2.modeling.sam2_base import SAM2Base +from model.segment_anything_2.sam2.sam2_image_predictor import SAM2ImagePredictor +from model.segment_anything_2.sam2.utils.amg import ( + area_from_rle, + batch_iterator, + batched_mask_to_box, + box_xyxy_to_xywh, + build_all_layer_point_grids, + calculate_stability_score, + coco_encode_rle, + generate_crop_boxes, + is_box_near_crop_edge, + mask_to_rle_pytorch, + MaskData, + remove_small_regions, + rle_to_mask, + uncrop_boxes_xyxy, + uncrop_masks, + uncrop_points, +) + + +class SAM2AutomaticMaskGenerator: + def __init__( + self, + model: SAM2Base, + points_per_side: Optional[int] = 32, + points_per_batch: int = 64, + pred_iou_thresh: float = 0.8, + stability_score_thresh: float = 0.95, + stability_score_offset: float = 1.0, + mask_threshold: float = 0.0, + box_nms_thresh: float = 0.7, + crop_n_layers: int = 0, + crop_nms_thresh: float = 0.7, + crop_overlap_ratio: float = 512 / 1500, + crop_n_points_downscale_factor: int = 1, + point_grids: Optional[List[np.ndarray]] = None, + min_mask_region_area: int = 0, + output_mode: str = "binary_mask", + use_m2m: bool = False, + multimask_output: bool = True, + ) -> None: + """ + Using a SAM 2 model, generates masks for the entire image. + Generates a grid of point prompts over the image, then filters + low quality and duplicate masks. The default settings are chosen + for SAM 2 with a HieraL backbone. + + Arguments: + model (Sam): The SAM 2 model to use for mask prediction. + points_per_side (int or None): The number of points to be sampled + along one side of the image. The total number of points is + points_per_side**2. If None, 'point_grids' must provide explicit + point sampling. + points_per_batch (int): Sets the number of points run simultaneously + by the model. Higher numbers may be faster but use more GPU memory. + pred_iou_thresh (float): A filtering threshold in [0,1], using the + model's predicted mask quality. + stability_score_thresh (float): A filtering threshold in [0,1], using + the stability of the mask under changes to the cutoff used to binarize + the model's mask predictions. + stability_score_offset (float): The amount to shift the cutoff when + calculated the stability score. + mask_threshold (float): Threshold for binarizing the mask logits + box_nms_thresh (float): The box IoU cutoff used by non-maximal + suppression to filter duplicate masks. + crop_n_layers (int): If >0, mask prediction will be run again on + crops of the image. Sets the number of layers to run, where each + layer has 2**i_layer number of image crops. + crop_nms_thresh (float): The box IoU cutoff used by non-maximal + suppression to filter duplicate masks between different crops. + crop_overlap_ratio (float): Sets the degree to which crops overlap. + In the first crop layer, crops will overlap by this fraction of + the image length. Later layers with more crops scale down this overlap. + crop_n_points_downscale_factor (int): The number of points-per-side + sampled in layer n is scaled down by crop_n_points_downscale_factor**n. + point_grids (list(np.ndarray) or None): A list over explicit grids + of points used for sampling, normalized to [0,1]. The nth grid in the + list is used in the nth crop layer. Exclusive with points_per_side. + min_mask_region_area (int): If >0, postprocessing will be applied + to remove disconnected regions and holes in masks with area smaller + than min_mask_region_area. Requires opencv. + output_mode (str): The form masks are returned in. Can be 'binary_mask', + 'uncompressed_rle', or 'coco_rle'. 'coco_rle' requires pycocotools. + For large resolutions, 'binary_mask' may consume large amounts of + memory. + use_m2m (bool): Whether to add a one step refinement using previous mask predictions. + multimask_output (bool): Whether to output multimask at each point of the grid. + """ + + assert (points_per_side is None) != ( + point_grids is None + ), "Exactly one of points_per_side or point_grid must be provided." + if points_per_side is not None: + self.point_grids = build_all_layer_point_grids( + points_per_side, + crop_n_layers, + crop_n_points_downscale_factor, + ) + elif point_grids is not None: + self.point_grids = point_grids + else: + raise ValueError("Can't have both points_per_side and point_grid be None.") + + assert output_mode in [ + "binary_mask", + "uncompressed_rle", + "coco_rle", + ], f"Unknown output_mode {output_mode}." + if output_mode == "coco_rle": + try: + from pycocotools import mask as mask_utils # type: ignore # noqa: F401 + except ImportError as e: + print("Please install pycocotools") + raise e + + self.predictor = SAM2ImagePredictor( + model, + max_hole_area=min_mask_region_area, + max_sprinkle_area=min_mask_region_area, + ) + self.points_per_batch = points_per_batch + self.pred_iou_thresh = pred_iou_thresh + self.stability_score_thresh = stability_score_thresh + self.stability_score_offset = stability_score_offset + self.mask_threshold = mask_threshold + self.box_nms_thresh = box_nms_thresh + self.crop_n_layers = crop_n_layers + self.crop_nms_thresh = crop_nms_thresh + self.crop_overlap_ratio = crop_overlap_ratio + self.crop_n_points_downscale_factor = crop_n_points_downscale_factor + self.min_mask_region_area = min_mask_region_area + self.output_mode = output_mode + self.use_m2m = use_m2m + self.multimask_output = multimask_output + + @torch.no_grad() + def generate(self, image: np.ndarray) -> List[Dict[str, Any]]: + """ + Generates masks for the given image. + + Arguments: + image (np.ndarray): The image to generate masks for, in HWC uint8 format. + + Returns: + list(dict(str, any)): A list over records for masks. Each record is + a dict containing the following keys: + segmentation (dict(str, any) or np.ndarray): The mask. If + output_mode='binary_mask', is an array of shape HW. Otherwise, + is a dictionary containing the RLE. + bbox (list(float)): The box around the mask, in XYWH format. + area (int): The area in pixels of the mask. + predicted_iou (float): The model's own prediction of the mask's + quality. This is filtered by the pred_iou_thresh parameter. + point_coords (list(list(float))): The point coordinates input + to the model to generate this mask. + stability_score (float): A measure of the mask's quality. This + is filtered on using the stability_score_thresh parameter. + crop_box (list(float)): The crop of the image used to generate + the mask, given in XYWH format. + """ + + # Generate masks + mask_data = self._generate_masks(image) + + # Encode masks + if self.output_mode == "coco_rle": + mask_data["segmentations"] = [ + coco_encode_rle(rle) for rle in mask_data["rles"] + ] + elif self.output_mode == "binary_mask": + mask_data["segmentations"] = [rle_to_mask(rle) for rle in mask_data["rles"]] + else: + mask_data["segmentations"] = mask_data["rles"] + + # Write mask records + curr_anns = [] + for idx in range(len(mask_data["segmentations"])): + ann = { + "segmentation": mask_data["segmentations"][idx], + "area": area_from_rle(mask_data["rles"][idx]), + "bbox": box_xyxy_to_xywh(mask_data["boxes"][idx]).tolist(), + "predicted_iou": mask_data["iou_preds"][idx].item(), + "point_coords": [mask_data["points"][idx].tolist()], + "stability_score": mask_data["stability_score"][idx].item(), + "crop_box": box_xyxy_to_xywh(mask_data["crop_boxes"][idx]).tolist(), + } + curr_anns.append(ann) + + return curr_anns + + def _generate_masks(self, image: np.ndarray) -> MaskData: + orig_size = image.shape[:2] + crop_boxes, layer_idxs = generate_crop_boxes( + orig_size, self.crop_n_layers, self.crop_overlap_ratio + ) + + # Iterate over image crops + data = MaskData() + for crop_box, layer_idx in zip(crop_boxes, layer_idxs): + crop_data = self._process_crop(image, crop_box, layer_idx, orig_size) + data.cat(crop_data) + + # Remove duplicate masks between crops + if len(crop_boxes) > 1: + # Prefer masks from smaller crops + scores = 1 / box_area(data["crop_boxes"]) + scores = scores.to(data["boxes"].device) + keep_by_nms = batched_nms( + data["boxes"].float(), + scores, + torch.zeros_like(data["boxes"][:, 0]), # categories + iou_threshold=self.crop_nms_thresh, + ) + data.filter(keep_by_nms) + data.to_numpy() + return data + + def _process_crop( + self, + image: np.ndarray, + crop_box: List[int], + crop_layer_idx: int, + orig_size: Tuple[int, ...], + ) -> MaskData: + # Crop the image and calculate embeddings + x0, y0, x1, y1 = crop_box + cropped_im = image[y0:y1, x0:x1, :] + cropped_im_size = cropped_im.shape[:2] + self.predictor.set_image(cropped_im) + + # Get points for this crop + points_scale = np.array(cropped_im_size)[None, ::-1] + points_for_image = self.point_grids[crop_layer_idx] * points_scale + + # Generate masks for this crop in batches + data = MaskData() + for (points,) in batch_iterator(self.points_per_batch, points_for_image): + batch_data = self._process_batch( + points, cropped_im_size, crop_box, orig_size, normalize=True + ) + data.cat(batch_data) + del batch_data + self.predictor.reset_predictor() + + # Remove duplicates within this crop. + keep_by_nms = batched_nms( + data["boxes"].float(), + data["iou_preds"], + torch.zeros_like(data["boxes"][:, 0]), # categories + iou_threshold=self.box_nms_thresh, + ) + data.filter(keep_by_nms) + + # Return to the original image frame + data["boxes"] = uncrop_boxes_xyxy(data["boxes"], crop_box) + data["points"] = uncrop_points(data["points"], crop_box) + data["crop_boxes"] = torch.tensor([crop_box for _ in range(len(data["rles"]))]) + + return data + + def _process_batch( + self, + points: np.ndarray, + im_size: Tuple[int, ...], + crop_box: List[int], + orig_size: Tuple[int, ...], + normalize=False, + ) -> MaskData: + orig_h, orig_w = orig_size + + # Run model on this batch + points = torch.as_tensor(points, device=self.predictor.device) + in_points = self.predictor._transforms.transform_coords( + points, normalize=normalize, orig_hw=im_size + ) + in_labels = torch.ones( + in_points.shape[0], dtype=torch.int, device=in_points.device + ) + masks, iou_preds, low_res_masks = self.predictor._predict( + in_points[:, None, :], + in_labels[:, None], + multimask_output=self.multimask_output, + return_logits=True, + ) + + # Serialize predictions and store in MaskData + data = MaskData( + masks=masks.flatten(0, 1), + iou_preds=iou_preds.flatten(0, 1), + points=points.repeat_interleave(masks.shape[1], dim=0), + low_res_masks=low_res_masks.flatten(0, 1), + ) + del masks + + if not self.use_m2m: + # Filter by predicted IoU + if self.pred_iou_thresh > 0.0: + keep_mask = data["iou_preds"] > self.pred_iou_thresh + data.filter(keep_mask) + + # Calculate and filter by stability score + data["stability_score"] = calculate_stability_score( + data["masks"], self.mask_threshold, self.stability_score_offset + ) + if self.stability_score_thresh > 0.0: + keep_mask = data["stability_score"] >= self.stability_score_thresh + data.filter(keep_mask) + else: + # One step refinement using previous mask predictions + in_points = self.predictor._transforms.transform_coords( + data["points"], normalize=normalize, orig_hw=im_size + ) + labels = torch.ones( + in_points.shape[0], dtype=torch.int, device=in_points.device + ) + masks, ious = self.refine_with_m2m( + in_points, labels, data["low_res_masks"], self.points_per_batch + ) + data["masks"] = masks.squeeze(1) + data["iou_preds"] = ious.squeeze(1) + + if self.pred_iou_thresh > 0.0: + keep_mask = data["iou_preds"] > self.pred_iou_thresh + data.filter(keep_mask) + + data["stability_score"] = calculate_stability_score( + data["masks"], self.mask_threshold, self.stability_score_offset + ) + if self.stability_score_thresh > 0.0: + keep_mask = data["stability_score"] >= self.stability_score_thresh + data.filter(keep_mask) + + # Threshold masks and calculate boxes + data["masks"] = data["masks"] > self.mask_threshold + data["boxes"] = batched_mask_to_box(data["masks"]) + + # Filter boxes that touch crop boundaries + keep_mask = ~is_box_near_crop_edge( + data["boxes"], crop_box, [0, 0, orig_w, orig_h] + ) + if not torch.all(keep_mask): + data.filter(keep_mask) + + # Compress to RLE + data["masks"] = uncrop_masks(data["masks"], crop_box, orig_h, orig_w) + data["rles"] = mask_to_rle_pytorch(data["masks"]) + del data["masks"] + + return data + + @staticmethod + def postprocess_small_regions( + mask_data: MaskData, min_area: int, nms_thresh: float + ) -> MaskData: + """ + Removes small disconnected regions and holes in masks, then reruns + box NMS to remove any new duplicates. + + Edits mask_data in place. + + Requires open-cv as a dependency. + """ + if len(mask_data["rles"]) == 0: + return mask_data + + # Filter small disconnected regions and holes + new_masks = [] + scores = [] + for rle in mask_data["rles"]: + mask = rle_to_mask(rle) + + mask, changed = remove_small_regions(mask, min_area, mode="holes") + unchanged = not changed + mask, changed = remove_small_regions(mask, min_area, mode="islands") + unchanged = unchanged and not changed + + new_masks.append(torch.as_tensor(mask).unsqueeze(0)) + # Give score=0 to changed masks and score=1 to unchanged masks + # so NMS will prefer ones that didn't need postprocessing + scores.append(float(unchanged)) + + # Recalculate boxes and remove any new duplicates + masks = torch.cat(new_masks, dim=0) + boxes = batched_mask_to_box(masks) + keep_by_nms = batched_nms( + boxes.float(), + torch.as_tensor(scores), + torch.zeros_like(boxes[:, 0]), # categories + iou_threshold=nms_thresh, + ) + + # Only recalculate RLEs for masks that have changed + for i_mask in keep_by_nms: + if scores[i_mask] == 0.0: + mask_torch = masks[i_mask].unsqueeze(0) + mask_data["rles"][i_mask] = mask_to_rle_pytorch(mask_torch)[0] + mask_data["boxes"][i_mask] = boxes[i_mask] # update res directly + mask_data.filter(keep_by_nms) + + return mask_data + + def refine_with_m2m(self, points, point_labels, low_res_masks, points_per_batch): + new_masks = [] + new_iou_preds = [] + + for cur_points, cur_point_labels, low_res_mask in batch_iterator( + points_per_batch, points, point_labels, low_res_masks + ): + best_masks, best_iou_preds, _ = self.predictor._predict( + cur_points[:, None, :], + cur_point_labels[:, None], + mask_input=low_res_mask[:, None, :], + multimask_output=False, + return_logits=True, + ) + new_masks.append(best_masks) + new_iou_preds.append(best_iou_preds) + masks = torch.cat(new_masks, dim=0) + return masks, torch.cat(new_iou_preds, dim=0) diff --git a/py/evf-sam/model/segment_anything_2/sam2/build_sam.py b/py/evf-sam/model/segment_anything_2/sam2/build_sam.py new file mode 100644 index 0000000..c0cfe12 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/build_sam.py @@ -0,0 +1,90 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import logging + +import torch +from hydra import compose +from hydra.utils import instantiate +from omegaconf import OmegaConf + +def build_sam2( + config_file, + ckpt_path=None, + device="cuda", + mode="eval", + hydra_overrides_extra=[], + apply_postprocessing=True, +): + + if apply_postprocessing: + hydra_overrides_extra = hydra_overrides_extra.copy() + hydra_overrides_extra += [ + # dynamically fall back to multi-mask if the single mask is not stable + "++model.sam_mask_decoder_extra_args.dynamic_multimask_via_stability=true", + "++model.sam_mask_decoder_extra_args.dynamic_multimask_stability_delta=0.05", + "++model.sam_mask_decoder_extra_args.dynamic_multimask_stability_thresh=0.98", + ] + # Read config and init model + cfg = compose(config_name=config_file, overrides=hydra_overrides_extra) + OmegaConf.resolve(cfg) + model = instantiate(cfg.model, _recursive_=True) + _load_checkpoint(model, ckpt_path) + if device: + model = model.to(device) + if mode == "eval": + model.eval() + return model + + +def build_sam2_video_predictor( + config_file, + ckpt_path=None, + device="cuda", + mode="eval", + hydra_overrides_extra=[], + apply_postprocessing=True, +): + hydra_overrides = [ + "++model._target_=model.segment_anything_2.sam2.sam2_video_predictor.SAM2VideoPredictor", + ] + if apply_postprocessing: + hydra_overrides_extra = hydra_overrides_extra.copy() + hydra_overrides_extra += [ + # dynamically fall back to multi-mask if the single mask is not stable + "++model.sam_mask_decoder_extra_args.dynamic_multimask_via_stability=true", + "++model.sam_mask_decoder_extra_args.dynamic_multimask_stability_delta=0.05", + "++model.sam_mask_decoder_extra_args.dynamic_multimask_stability_thresh=0.98", + # the sigmoid mask logits on interacted frames with clicks in the memory encoder so that the encoded masks are exactly as what users see from clicking + "++model.binarize_mask_from_pts_for_mem_enc=true", + # fill small holes in the low-res masks up to `fill_hole_area` (before resizing them to the original video resolution) + "++model.fill_hole_area=8", + ] + hydra_overrides.extend(hydra_overrides_extra) + + # Read config and init model + cfg = compose(config_name=config_file, overrides=hydra_overrides) + OmegaConf.resolve(cfg) + model = instantiate(cfg.model, _recursive_=True) + _load_checkpoint(model, ckpt_path) + if device: + model = model.to(device) + if mode == "eval": + model.eval() + return model + + +def _load_checkpoint(model, ckpt_path): + if ckpt_path is not None: + sd = torch.load(ckpt_path, map_location="cpu")["model"] + missing_keys, unexpected_keys = model.load_state_dict(sd) + if missing_keys: + logging.error(missing_keys) + raise RuntimeError() + if unexpected_keys: + logging.error(unexpected_keys) + raise RuntimeError() + logging.info("Loaded checkpoint sucessfully") diff --git a/py/evf-sam/model/segment_anything_2/sam2/csrc/connected_components.cu b/py/evf-sam/model/segment_anything_2/sam2/csrc/connected_components.cu new file mode 100644 index 0000000..eb83231 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/csrc/connected_components.cu @@ -0,0 +1,289 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. +// All rights reserved. + +// This source code is licensed under the license found in the +// LICENSE file in the root directory of this source tree. + +// adapted from https://github.com/zsef123/Connected_components_PyTorch +// with license found in the LICENSE_cctorch file in the root directory. +#include +#include +#include +#include +#include +#include + +// 2d +#define BLOCK_ROWS 16 +#define BLOCK_COLS 16 + +namespace cc2d { + +template +__device__ __forceinline__ unsigned char hasBit(T bitmap, unsigned char pos) { + return (bitmap >> pos) & 1; +} + +__device__ int32_t find(const int32_t* s_buf, int32_t n) { + while (s_buf[n] != n) + n = s_buf[n]; + return n; +} + +__device__ int32_t find_n_compress(int32_t* s_buf, int32_t n) { + const int32_t id = n; + while (s_buf[n] != n) { + n = s_buf[n]; + s_buf[id] = n; + } + return n; +} + +__device__ void union_(int32_t* s_buf, int32_t a, int32_t b) { + bool done; + do { + a = find(s_buf, a); + b = find(s_buf, b); + + if (a < b) { + int32_t old = atomicMin(s_buf + b, a); + done = (old == b); + b = old; + } else if (b < a) { + int32_t old = atomicMin(s_buf + a, b); + done = (old == a); + a = old; + } else + done = true; + + } while (!done); +} + +__global__ void +init_labeling(int32_t* label, const uint32_t W, const uint32_t H) { + const uint32_t row = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const uint32_t col = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const uint32_t idx = row * W + col; + + if (row < H && col < W) + label[idx] = idx; +} + +__global__ void +merge(uint8_t* img, int32_t* label, const uint32_t W, const uint32_t H) { + const uint32_t row = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const uint32_t col = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const uint32_t idx = row * W + col; + + if (row >= H || col >= W) + return; + + uint32_t P = 0; + + if (img[idx]) + P |= 0x777; + if (row + 1 < H && img[idx + W]) + P |= 0x777 << 4; + if (col + 1 < W && img[idx + 1]) + P |= 0x777 << 1; + + if (col == 0) + P &= 0xEEEE; + if (col + 1 >= W) + P &= 0x3333; + else if (col + 2 >= W) + P &= 0x7777; + + if (row == 0) + P &= 0xFFF0; + if (row + 1 >= H) + P &= 0xFF; + + if (P > 0) { + // If need check about top-left pixel(if flag the first bit) and hit the + // top-left pixel + if (hasBit(P, 0) && img[idx - W - 1]) { + union_(label, idx, idx - 2 * W - 2); // top left block + } + + if ((hasBit(P, 1) && img[idx - W]) || (hasBit(P, 2) && img[idx - W + 1])) + union_(label, idx, idx - 2 * W); // top bottom block + + if (hasBit(P, 3) && img[idx + 2 - W]) + union_(label, idx, idx - 2 * W + 2); // top right block + + if ((hasBit(P, 4) && img[idx - 1]) || (hasBit(P, 8) && img[idx + W - 1])) + union_(label, idx, idx - 2); // just left block + } +} + +__global__ void compression(int32_t* label, const int32_t W, const int32_t H) { + const uint32_t row = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const uint32_t col = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const uint32_t idx = row * W + col; + + if (row < H && col < W) + find_n_compress(label, idx); +} + +__global__ void final_labeling( + const uint8_t* img, + int32_t* label, + const int32_t W, + const int32_t H) { + const uint32_t row = (blockIdx.y * blockDim.y + threadIdx.y) * 2; + const uint32_t col = (blockIdx.x * blockDim.x + threadIdx.x) * 2; + const uint32_t idx = row * W + col; + + if (row >= H || col >= W) + return; + + int32_t y = label[idx] + 1; + + if (img[idx]) + label[idx] = y; + else + label[idx] = 0; + + if (col + 1 < W) { + if (img[idx + 1]) + label[idx + 1] = y; + else + label[idx + 1] = 0; + + if (row + 1 < H) { + if (img[idx + W + 1]) + label[idx + W + 1] = y; + else + label[idx + W + 1] = 0; + } + } + + if (row + 1 < H) { + if (img[idx + W]) + label[idx + W] = y; + else + label[idx + W] = 0; + } +} + +__global__ void init_counting( + const int32_t* label, + int32_t* count_init, + const int32_t W, + const int32_t H) { + const uint32_t row = (blockIdx.y * blockDim.y + threadIdx.y); + const uint32_t col = (blockIdx.x * blockDim.x + threadIdx.x); + const uint32_t idx = row * W + col; + + if (row >= H || col >= W) + return; + + int32_t y = label[idx]; + if (y > 0) { + int32_t count_idx = y - 1; + atomicAdd(count_init + count_idx, 1); + } +} + +__global__ void final_counting( + const int32_t* label, + const int32_t* count_init, + int32_t* count_final, + const int32_t W, + const int32_t H) { + const uint32_t row = (blockIdx.y * blockDim.y + threadIdx.y); + const uint32_t col = (blockIdx.x * blockDim.x + threadIdx.x); + const uint32_t idx = row * W + col; + + if (row >= H || col >= W) + return; + + int32_t y = label[idx]; + if (y > 0) { + int32_t count_idx = y - 1; + count_final[idx] = count_init[count_idx]; + } else { + count_final[idx] = 0; + } +} + +} // namespace cc2d + +std::vector get_connected_componnets( + const torch::Tensor& inputs) { + AT_ASSERTM(inputs.is_cuda(), "inputs must be a CUDA tensor"); + AT_ASSERTM(inputs.ndimension() == 4, "inputs must be [N, 1, H, W] shape"); + AT_ASSERTM( + inputs.scalar_type() == torch::kUInt8, "inputs must be a uint8 type"); + + const uint32_t N = inputs.size(0); + const uint32_t C = inputs.size(1); + const uint32_t H = inputs.size(2); + const uint32_t W = inputs.size(3); + + AT_ASSERTM(C == 1, "inputs must be [N, 1, H, W] shape"); + AT_ASSERTM((H % 2) == 0, "height must be a even number"); + AT_ASSERTM((W % 2) == 0, "width must be a even number"); + + // label must be uint32_t + auto label_options = + torch::TensorOptions().dtype(torch::kInt32).device(inputs.device()); + torch::Tensor labels = torch::zeros({N, C, H, W}, label_options); + torch::Tensor counts_init = torch::zeros({N, C, H, W}, label_options); + torch::Tensor counts_final = torch::zeros({N, C, H, W}, label_options); + + dim3 grid = dim3( + ((W + 1) / 2 + BLOCK_COLS - 1) / BLOCK_COLS, + ((H + 1) / 2 + BLOCK_ROWS - 1) / BLOCK_ROWS); + dim3 block = dim3(BLOCK_COLS, BLOCK_ROWS); + dim3 grid_count = + dim3((W + BLOCK_COLS) / BLOCK_COLS, (H + BLOCK_ROWS) / BLOCK_ROWS); + dim3 block_count = dim3(BLOCK_COLS, BLOCK_ROWS); + cudaStream_t stream = at::cuda::getCurrentCUDAStream(); + + for (int n = 0; n < N; n++) { + uint32_t offset = n * H * W; + + cc2d::init_labeling<<>>( + labels.data_ptr() + offset, W, H); + cc2d::merge<<>>( + inputs.data_ptr() + offset, + labels.data_ptr() + offset, + W, + H); + cc2d::compression<<>>( + labels.data_ptr() + offset, W, H); + cc2d::final_labeling<<>>( + inputs.data_ptr() + offset, + labels.data_ptr() + offset, + W, + H); + + // get the counting of each pixel + cc2d::init_counting<<>>( + labels.data_ptr() + offset, + counts_init.data_ptr() + offset, + W, + H); + cc2d::final_counting<<>>( + labels.data_ptr() + offset, + counts_init.data_ptr() + offset, + counts_final.data_ptr() + offset, + W, + H); + } + + // returned values are [labels, counts] + std::vector outputs; + outputs.push_back(labels); + outputs.push_back(counts_final); + return outputs; +} + +PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { + m.def( + "get_connected_componnets", + &get_connected_componnets, + "get_connected_componnets"); +} diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/__init__.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/__init__.py new file mode 100644 index 0000000..5277f46 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/__init__.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/__init__.py new file mode 100644 index 0000000..5277f46 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/hieradet.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/hieradet.py new file mode 100644 index 0000000..407598e --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/hieradet.py @@ -0,0 +1,295 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from functools import partial +from typing import List, Tuple, Union + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from model.segment_anything_2.sam2.modeling.backbones.utils import ( + PatchEmbed, + window_partition, + window_unpartition, +) + +from model.segment_anything_2.sam2.modeling.sam2_utils import DropPath, MLP + + +def do_pool(x: torch.Tensor, pool: nn.Module, norm: nn.Module = None) -> torch.Tensor: + if pool is None: + return x + # (B, H, W, C) -> (B, C, H, W) + x = x.permute(0, 3, 1, 2) + x = pool(x) + # (B, C, H', W') -> (B, H', W', C) + x = x.permute(0, 2, 3, 1) + if norm: + x = norm(x) + + return x + + +class MultiScaleAttention(nn.Module): + def __init__( + self, + dim: int, + dim_out: int, + num_heads: int, + q_pool: nn.Module = None, + ): + super().__init__() + + self.dim = dim + self.dim_out = dim_out + + self.num_heads = num_heads + head_dim = dim_out // num_heads + self.scale = head_dim**-0.5 + + self.q_pool = q_pool + self.qkv = nn.Linear(dim, dim_out * 3) + self.proj = nn.Linear(dim_out, dim_out) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + B, H, W, _ = x.shape + # qkv with shape (B, H * W, 3, nHead, C) + qkv = self.qkv(x).reshape(B, H * W, 3, self.num_heads, -1) + # q, k, v with shape (B, H * W, nheads, C) + q, k, v = torch.unbind(qkv, 2) + + # Q pooling (for downsample at stage changes) + if self.q_pool: + q = do_pool(q.reshape(B, H, W, -1), self.q_pool) + H, W = q.shape[1:3] # downsampled shape + q = q.reshape(B, H * W, self.num_heads, -1) + + # Torch's SDPA expects [B, nheads, H*W, C] so we transpose + x = F.scaled_dot_product_attention( + q.transpose(1, 2), + k.transpose(1, 2), + v.transpose(1, 2), + ) + # Transpose back + x = x.transpose(1, 2) + x = x.reshape(B, H, W, -1) + + x = self.proj(x) + + return x + + +class MultiScaleBlock(nn.Module): + def __init__( + self, + dim: int, + dim_out: int, + num_heads: int, + mlp_ratio: float = 4.0, + drop_path: float = 0.0, + norm_layer: Union[nn.Module, str] = "LayerNorm", + q_stride: Tuple[int, int] = None, + act_layer: nn.Module = nn.GELU, + window_size: int = 0, + ): + super().__init__() + + if isinstance(norm_layer, str): + norm_layer = partial(getattr(nn, norm_layer), eps=1e-6) + + self.dim = dim + self.dim_out = dim_out + self.norm1 = norm_layer(dim) + + self.window_size = window_size + + self.pool, self.q_stride = None, q_stride + if self.q_stride: + self.pool = nn.MaxPool2d( + kernel_size=q_stride, stride=q_stride, ceil_mode=False + ) + + self.attn = MultiScaleAttention( + dim, + dim_out, + num_heads=num_heads, + q_pool=self.pool, + ) + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + + self.norm2 = norm_layer(dim_out) + self.mlp = MLP( + dim_out, + int(dim_out * mlp_ratio), + dim_out, + num_layers=2, + activation=act_layer, + ) + + if dim != dim_out: + self.proj = nn.Linear(dim, dim_out) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + shortcut = x # B, H, W, C + x = self.norm1(x) + + # Skip connection + if self.dim != self.dim_out: + shortcut = do_pool(self.proj(x), self.pool) + + # Window partition + window_size = self.window_size + if window_size > 0: + H, W = x.shape[1], x.shape[2] + x, pad_hw = window_partition(x, window_size) + + # Window Attention + Q Pooling (if stage change) + x = self.attn(x) + if self.q_stride: + # Shapes have changed due to Q pooling + window_size = self.window_size // self.q_stride[0] + H, W = shortcut.shape[1:3] + + pad_h = (window_size - H % window_size) % window_size + pad_w = (window_size - W % window_size) % window_size + pad_hw = (H + pad_h, W + pad_w) + + # Reverse window partition + if self.window_size > 0: + x = window_unpartition(x, window_size, pad_hw, (H, W)) + + x = shortcut + self.drop_path(x) + # MLP + x = x + self.drop_path(self.mlp(self.norm2(x))) + return x + + +class Hiera(nn.Module): + """ + Reference: https://arxiv.org/abs/2306.00989 + """ + + def __init__( + self, + embed_dim: int = 96, # initial embed dim + num_heads: int = 1, # initial number of heads + drop_path_rate: float = 0.0, # stochastic depth + q_pool: int = 3, # number of q_pool stages + q_stride: Tuple[int, int] = (2, 2), # downsample stride bet. stages + stages: Tuple[int, ...] = (2, 3, 16, 3), # blocks per stage + dim_mul: float = 2.0, # dim_mul factor at stage shift + head_mul: float = 2.0, # head_mul factor at stage shift + window_pos_embed_bkg_spatial_size: Tuple[int, int] = (14, 14), + # window size per stage, when not using global att. + window_spec: Tuple[int, ...] = ( + 8, + 4, + 14, + 7, + ), + # global attn in these blocks + global_att_blocks: Tuple[int, ...] = ( + 12, + 16, + 20, + ), + return_interm_layers=True, # return feats from every stage + ): + super().__init__() + + assert len(stages) == len(window_spec) + self.window_spec = window_spec + + depth = sum(stages) + self.q_stride = q_stride + self.stage_ends = [sum(stages[:i]) - 1 for i in range(1, len(stages) + 1)] + assert 0 <= q_pool <= len(self.stage_ends[:-1]) + self.q_pool_blocks = [x + 1 for x in self.stage_ends[:-1]][:q_pool] + self.return_interm_layers = return_interm_layers + + self.patch_embed = PatchEmbed( + embed_dim=embed_dim, + ) + # Which blocks have global att? + self.global_att_blocks = global_att_blocks + + # Windowed positional embedding (https://arxiv.org/abs/2311.05613) + self.window_pos_embed_bkg_spatial_size = window_pos_embed_bkg_spatial_size + self.pos_embed = nn.Parameter( + torch.zeros(1, embed_dim, *self.window_pos_embed_bkg_spatial_size) + ) + self.pos_embed_window = nn.Parameter( + torch.zeros(1, embed_dim, self.window_spec[0], self.window_spec[0]) + ) + + dpr = [ + x.item() for x in torch.linspace(0, drop_path_rate, depth) + ] # stochastic depth decay rule + + cur_stage = 1 + self.blocks = nn.ModuleList() + + for i in range(depth): + dim_out = embed_dim + # lags by a block, so first block of + # next stage uses an initial window size + # of previous stage and final window size of current stage + window_size = self.window_spec[cur_stage - 1] + + if self.global_att_blocks is not None: + window_size = 0 if i in self.global_att_blocks else window_size + + if i - 1 in self.stage_ends: + dim_out = int(embed_dim * dim_mul) + num_heads = int(num_heads * head_mul) + cur_stage += 1 + + block = MultiScaleBlock( + dim=embed_dim, + dim_out=dim_out, + num_heads=num_heads, + drop_path=dpr[i], + q_stride=self.q_stride if i in self.q_pool_blocks else None, + window_size=window_size, + ) + + embed_dim = dim_out + self.blocks.append(block) + + self.channel_list = ( + [self.blocks[i].dim_out for i in self.stage_ends[::-1]] + if return_interm_layers + else [self.blocks[-1].dim_out] + ) + + def _get_pos_embed(self, hw: Tuple[int, int]) -> torch.Tensor: + h, w = hw + window_embed = self.pos_embed_window + pos_embed = F.interpolate(self.pos_embed, size=(h, w), mode="bicubic") + pos_embed = pos_embed + window_embed.tile( + [x // y for x, y in zip(pos_embed.shape, window_embed.shape)] + ) + pos_embed = pos_embed.permute(0, 2, 3, 1) + return pos_embed + + def forward(self, x: torch.Tensor) -> List[torch.Tensor]: + x = self.patch_embed(x) + # x: (B, H, W, C) + + # Add pos embed + x = x + self._get_pos_embed(x.shape[1:3]) + + outputs = [] + for i, blk in enumerate(self.blocks): + x = blk(x) + if (i == self.stage_ends[-1]) or ( + i in self.stage_ends and self.return_interm_layers + ): + feats = x.permute(0, 3, 1, 2) + outputs.append(feats) + + return outputs diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/image_encoder.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/image_encoder.py new file mode 100644 index 0000000..5f92baf --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/image_encoder.py @@ -0,0 +1,133 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import List, Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class ImageEncoder(nn.Module): + def __init__( + self, + trunk: nn.Module, + neck: nn.Module, + scalp: int = 0, + ): + super().__init__() + self.trunk = trunk + self.neck = neck + self.scalp = scalp + assert ( + self.trunk.channel_list == self.neck.backbone_channel_list + ), f"Channel dims of trunk and neck do not match. Trunk: {self.trunk.channel_list}, neck: {self.neck.backbone_channel_list}" + + def forward(self, sample: torch.Tensor): + # Forward through backbone + features, pos = self.neck(self.trunk(sample)) + if self.scalp > 0: + # Discard the lowest resolution features + features, pos = features[: -self.scalp], pos[: -self.scalp] + + src = features[-1] + output = { + "vision_features": src, + "vision_pos_enc": pos, + "backbone_fpn": features, + } + return output + + +class FpnNeck(nn.Module): + """ + A modified variant of Feature Pyramid Network (FPN) neck + (we remove output conv and also do bicubic interpolation similar to ViT + pos embed interpolation) + """ + + def __init__( + self, + position_encoding: nn.Module, + d_model: int, + backbone_channel_list: List[int], + kernel_size: int = 1, + stride: int = 1, + padding: int = 0, + fpn_interp_model: str = "bilinear", + fuse_type: str = "sum", + fpn_top_down_levels: Optional[List[int]] = None, + ): + """Initialize the neck + :param trunk: the backbone + :param position_encoding: the positional encoding to use + :param d_model: the dimension of the model + :param neck_norm: the normalization to use + """ + super().__init__() + self.position_encoding = position_encoding + self.convs = nn.ModuleList() + self.backbone_channel_list = backbone_channel_list + for dim in backbone_channel_list: + current = nn.Sequential() + current.add_module( + "conv", + nn.Conv2d( + in_channels=dim, + out_channels=d_model, + kernel_size=kernel_size, + stride=stride, + padding=padding, + ), + ) + + self.convs.append(current) + self.fpn_interp_model = fpn_interp_model + assert fuse_type in ["sum", "avg"] + self.fuse_type = fuse_type + + # levels to have top-down features in its outputs + # e.g. if fpn_top_down_levels is [2, 3], then only outputs of level 2 and 3 + # have top-down propagation, while outputs of level 0 and level 1 have only + # lateral features from the same backbone level. + if fpn_top_down_levels is None: + # default is to have top-down features on all levels + fpn_top_down_levels = range(len(self.convs)) + self.fpn_top_down_levels = list(fpn_top_down_levels) + + def forward(self, xs: List[torch.Tensor]): + + out = [None] * len(self.convs) + pos = [None] * len(self.convs) + assert len(xs) == len(self.convs) + # fpn forward pass + # see https://github.com/facebookresearch/detectron2/blob/main/detectron2/modeling/backbone/fpn.py + prev_features = None + # forward in top-down order (from low to high resolution) + n = len(self.convs) - 1 + for i in range(n, -1, -1): + x = xs[i] + lateral_features = self.convs[n - i](x) + if i in self.fpn_top_down_levels and prev_features is not None: + top_down_features = F.interpolate( + prev_features.to(dtype=torch.float32), + scale_factor=2.0, + mode=self.fpn_interp_model, + align_corners=( + None if self.fpn_interp_model == "nearest" else False + ), + antialias=False, + ) + prev_features = lateral_features + top_down_features + if self.fuse_type == "avg": + prev_features /= 2 + else: + prev_features = lateral_features + x_out = prev_features + out[i] = x_out + pos[i] = self.position_encoding(x_out).to(x_out.dtype) + + return out, pos diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/utils.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/utils.py new file mode 100644 index 0000000..32d55c7 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/backbones/utils.py @@ -0,0 +1,95 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +"""Some utilities for backbones, in particular for windowing""" + +from typing import Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +def window_partition(x, window_size): + """ + Partition into non-overlapping windows with padding if needed. + Args: + x (tensor): input tokens with [B, H, W, C]. + window_size (int): window size. + Returns: + windows: windows after partition with [B * num_windows, window_size, window_size, C]. + (Hp, Wp): padded height and width before partition + """ + B, H, W, C = x.shape + + pad_h = (window_size - H % window_size) % window_size + pad_w = (window_size - W % window_size) % window_size + if pad_h > 0 or pad_w > 0: + x = F.pad(x, (0, 0, 0, pad_w, 0, pad_h)) + Hp, Wp = H + pad_h, W + pad_w + + x = x.view(B, Hp // window_size, window_size, Wp // window_size, window_size, C) + windows = ( + x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C) + ) + return windows, (Hp, Wp) + + +def window_unpartition(windows, window_size, pad_hw, hw): + """ + Window unpartition into original sequences and removing padding. + Args: + x (tensor): input tokens with [B * num_windows, window_size, window_size, C]. + window_size (int): window size. + pad_hw (Tuple): padded height and width (Hp, Wp). + hw (Tuple): original height and width (H, W) before padding. + Returns: + x: unpartitioned sequences with [B, H, W, C]. + """ + Hp, Wp = pad_hw + H, W = hw + B = windows.shape[0] // (Hp * Wp // window_size // window_size) + x = windows.view( + B, Hp // window_size, Wp // window_size, window_size, window_size, -1 + ) + x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, Hp, Wp, -1) + + if Hp > H or Wp > W: + x = x[:, :H, :W, :].contiguous() + return x + + +class PatchEmbed(nn.Module): + """ + Image to Patch Embedding. + """ + + def __init__( + self, + kernel_size: Tuple[int, ...] = (7, 7), + stride: Tuple[int, ...] = (4, 4), + padding: Tuple[int, ...] = (3, 3), + in_chans: int = 3, + embed_dim: int = 768, + ): + """ + Args: + kernel_size (Tuple): kernel size of the projection layer. + stride (Tuple): stride of the projection layer. + padding (Tuple): padding size of the projection layer. + in_chans (int): Number of input image channels. + embed_dim (int): embed_dim (int): Patch embedding dimension. + """ + super().__init__() + self.proj = nn.Conv2d( + in_chans, embed_dim, kernel_size=kernel_size, stride=stride, padding=padding + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = self.proj(x) + # B C H W -> B H W C + x = x.permute(0, 2, 3, 1) + return x diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/memory_attention.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/memory_attention.py new file mode 100644 index 0000000..42f56ef --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/memory_attention.py @@ -0,0 +1,169 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Optional + +import torch +from torch import nn, Tensor + +from model.segment_anything_2.sam2.modeling.sam.transformer import RoPEAttention + +from model.segment_anything_2.sam2.modeling.sam2_utils import get_activation_fn, get_clones + + +class MemoryAttentionLayer(nn.Module): + + def __init__( + self, + activation: str, + cross_attention: nn.Module, + d_model: int, + dim_feedforward: int, + dropout: float, + pos_enc_at_attn: bool, + pos_enc_at_cross_attn_keys: bool, + pos_enc_at_cross_attn_queries: bool, + self_attention: nn.Module, + ): + super().__init__() + self.d_model = d_model + self.dim_feedforward = dim_feedforward + self.dropout_value = dropout + self.self_attn = self_attention + self.cross_attn_image = cross_attention + + # Implementation of Feedforward model + self.linear1 = nn.Linear(d_model, dim_feedforward) + self.dropout = nn.Dropout(dropout) + self.linear2 = nn.Linear(dim_feedforward, d_model) + + self.norm1 = nn.LayerNorm(d_model) + self.norm2 = nn.LayerNorm(d_model) + self.norm3 = nn.LayerNorm(d_model) + self.dropout1 = nn.Dropout(dropout) + self.dropout2 = nn.Dropout(dropout) + self.dropout3 = nn.Dropout(dropout) + + self.activation_str = activation + self.activation = get_activation_fn(activation) + + # Where to add pos enc + self.pos_enc_at_attn = pos_enc_at_attn + self.pos_enc_at_cross_attn_queries = pos_enc_at_cross_attn_queries + self.pos_enc_at_cross_attn_keys = pos_enc_at_cross_attn_keys + + def _forward_sa(self, tgt, query_pos): + # Self-Attention + tgt2 = self.norm1(tgt) + q = k = tgt2 + query_pos if self.pos_enc_at_attn else tgt2 + tgt2 = self.self_attn(q, k, v=tgt2) + tgt = tgt + self.dropout1(tgt2) + return tgt + + def _forward_ca(self, tgt, memory, query_pos, pos, num_k_exclude_rope=0): + kwds = {} + if num_k_exclude_rope > 0: + assert isinstance(self.cross_attn_image, RoPEAttention) + kwds = {"num_k_exclude_rope": num_k_exclude_rope} + + # Cross-Attention + tgt2 = self.norm2(tgt) + tgt2 = self.cross_attn_image( + q=tgt2 + query_pos if self.pos_enc_at_cross_attn_queries else tgt2, + k=memory + pos if self.pos_enc_at_cross_attn_keys else memory, + v=memory, + **kwds, + ) + tgt = tgt + self.dropout2(tgt2) + return tgt + + def forward( + self, + tgt, + memory, + pos: Optional[Tensor] = None, + query_pos: Optional[Tensor] = None, + num_k_exclude_rope: int = 0, + ) -> torch.Tensor: + + # Self-Attn, Cross-Attn + tgt = self._forward_sa(tgt, query_pos) + tgt = self._forward_ca(tgt, memory, query_pos, pos, num_k_exclude_rope) + # MLP + tgt2 = self.norm3(tgt) + tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt2)))) + tgt = tgt + self.dropout3(tgt2) + return tgt + + +class MemoryAttention(nn.Module): + def __init__( + self, + d_model: int, + pos_enc_at_input: bool, + layer: nn.Module, + num_layers: int, + batch_first: bool = True, # Do layers expect batch first input? + ): + super().__init__() + self.d_model = d_model + self.layers = get_clones(layer, num_layers) + self.num_layers = num_layers + self.norm = nn.LayerNorm(d_model) + self.pos_enc_at_input = pos_enc_at_input + self.batch_first = batch_first + + def forward( + self, + curr: torch.Tensor, # self-attention inputs + memory: torch.Tensor, # cross-attention inputs + curr_pos: Optional[Tensor] = None, # pos_enc for self-attention inputs + memory_pos: Optional[Tensor] = None, # pos_enc for cross-attention inputs + num_obj_ptr_tokens: int = 0, # number of object pointer *tokens* + ): + if isinstance(curr, list): + assert isinstance(curr_pos, list) + assert len(curr) == len(curr_pos) == 1 + curr, curr_pos = ( + curr[0], + curr_pos[0], + ) + + assert ( + curr.shape[1] == memory.shape[1] + ), "Batch size must be the same for curr and memory" + + output = curr + if self.pos_enc_at_input and curr_pos is not None: + output = output + 0.1 * curr_pos + + if self.batch_first: + # Convert to batch first + output = output.transpose(0, 1) + curr_pos = curr_pos.transpose(0, 1) + memory = memory.transpose(0, 1) + memory_pos = memory_pos.transpose(0, 1) + + for layer in self.layers: + kwds = {} + if isinstance(layer.cross_attn_image, RoPEAttention): + kwds = {"num_k_exclude_rope": num_obj_ptr_tokens} + + output = layer( + tgt=output, + memory=memory, + pos=memory_pos, + query_pos=curr_pos, + **kwds, + ) + normed_output = self.norm(output) + + if self.batch_first: + # Convert back to seq first + normed_output = normed_output.transpose(0, 1) + curr_pos = curr_pos.transpose(0, 1) + + return normed_output diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/memory_encoder.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/memory_encoder.py new file mode 100644 index 0000000..fb11cbf --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/memory_encoder.py @@ -0,0 +1,182 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from typing import Tuple + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from model.segment_anything_2.sam2.modeling.sam2_utils import DropPath, get_clones, LayerNorm2d + + +class MaskDownSampler(nn.Module): + """ + Progressively downsample a mask by total_stride, each time by stride. + Note that LayerNorm is applied per *token*, like in ViT. + + With each downsample (by a factor stride**2), channel capacity increases by the same factor. + In the end, we linearly project to embed_dim channels. + """ + + def __init__( + self, + embed_dim=256, + kernel_size=4, + stride=4, + padding=0, + total_stride=16, + activation=nn.GELU, + ): + super().__init__() + num_layers = int(math.log2(total_stride) // math.log2(stride)) + assert stride**num_layers == total_stride + self.encoder = nn.Sequential() + mask_in_chans, mask_out_chans = 1, 1 + for _ in range(num_layers): + mask_out_chans = mask_in_chans * (stride**2) + self.encoder.append( + nn.Conv2d( + mask_in_chans, + mask_out_chans, + kernel_size=kernel_size, + stride=stride, + padding=padding, + ) + ) + self.encoder.append(LayerNorm2d(mask_out_chans)) + self.encoder.append(activation()) + mask_in_chans = mask_out_chans + + self.encoder.append(nn.Conv2d(mask_out_chans, embed_dim, kernel_size=1)) + + def forward(self, x): + return self.encoder(x) + + +# Lightly adapted from ConvNext (https://github.com/facebookresearch/ConvNeXt) +class CXBlock(nn.Module): + r"""ConvNeXt Block. There are two equivalent implementations: + (1) DwConv -> LayerNorm (channels_first) -> 1x1 Conv -> GELU -> 1x1 Conv; all in (N, C, H, W) + (2) DwConv -> Permute to (N, H, W, C); LayerNorm (channels_last) -> Linear -> GELU -> Linear; Permute back + We use (2) as we find it slightly faster in PyTorch + + Args: + dim (int): Number of input channels. + drop_path (float): Stochastic depth rate. Default: 0.0 + layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6. + """ + + def __init__( + self, + dim, + kernel_size=7, + padding=3, + drop_path=0.0, + layer_scale_init_value=1e-6, + use_dwconv=True, + ): + super().__init__() + self.dwconv = nn.Conv2d( + dim, + dim, + kernel_size=kernel_size, + padding=padding, + groups=dim if use_dwconv else 1, + ) # depthwise conv + self.norm = LayerNorm2d(dim, eps=1e-6) + self.pwconv1 = nn.Linear( + dim, 4 * dim + ) # pointwise/1x1 convs, implemented with linear layers + self.act = nn.GELU() + self.pwconv2 = nn.Linear(4 * dim, dim) + # modified by ZhangYx from self.gamma to self.weight. Due to (https://github.com/facebookresearch/segment-anything-2/issues/85) + self.weight = ( + nn.Parameter(layer_scale_init_value * torch.ones((dim)), requires_grad=True) + if layer_scale_init_value > 0 + else None + ) + self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity() + + def forward(self, x): + input = x + x = self.dwconv(x) + x = self.norm(x) + x = x.permute(0, 2, 3, 1) # (N, C, H, W) -> (N, H, W, C) + x = self.pwconv1(x) + x = self.act(x) + x = self.pwconv2(x) + if self.weight is not None: + x = self.weight * x + x = x.permute(0, 3, 1, 2) # (N, H, W, C) -> (N, C, H, W) + + x = input + self.drop_path(x) + return x + + +class Fuser(nn.Module): + def __init__(self, layer, num_layers, dim=None, input_projection=False): + super().__init__() + self.proj = nn.Identity() + self.layers = get_clones(layer, num_layers) + + if input_projection: + assert dim is not None + self.proj = nn.Conv2d(dim, dim, kernel_size=1) + + def forward(self, x): + # normally x: (N, C, H, W) + x = self.proj(x) + for layer in self.layers: + x = layer(x) + return x + + +class MemoryEncoder(nn.Module): + def __init__( + self, + out_dim, + mask_downsampler, + fuser, + position_encoding, + in_dim=256, # in_dim of pix_feats + ): + super().__init__() + + self.mask_downsampler = mask_downsampler + + self.pix_feat_proj = nn.Conv2d(in_dim, in_dim, kernel_size=1) + self.fuser = fuser + self.position_encoding = position_encoding + self.out_proj = nn.Identity() + if out_dim != in_dim: + self.out_proj = nn.Conv2d(in_dim, out_dim, kernel_size=1) + + def forward( + self, + pix_feat: torch.Tensor, + masks: torch.Tensor, + skip_mask_sigmoid: bool = False, + ) -> Tuple[torch.Tensor, torch.Tensor]: + ## Process masks + # sigmoid, so that less domain shift from gt masks which are bool + if not skip_mask_sigmoid: + masks = F.sigmoid(masks) + masks = self.mask_downsampler(masks) + + ## Fuse pix_feats and downsampled masks + # in case the visual features are on CPU, cast them to CUDA + pix_feat = pix_feat.to(masks.device) + + x = self.pix_feat_proj(pix_feat) + x = x + masks + x = self.fuser(x) + x = self.out_proj(x) + + pos = self.position_encoding(x).to(x.dtype) + + return {"vision_features": x, "vision_pos_enc": [pos]} diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/position_encoding.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/position_encoding.py new file mode 100644 index 0000000..f4b57ae --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/position_encoding.py @@ -0,0 +1,216 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from typing import Any, Optional, Tuple + +import numpy as np + +import torch +from torch import nn + + +class PositionEmbeddingSine(nn.Module): + """ + This is a more standard version of the position embedding, very similar to the one + used by the Attention is all you need paper, generalized to work on images. + """ + + def __init__( + self, + num_pos_feats, + temperature: int = 10000, + normalize: bool = True, + scale: Optional[float] = None, + ): + super().__init__() + assert num_pos_feats % 2 == 0, "Expecting even model width" + self.num_pos_feats = num_pos_feats // 2 + self.temperature = temperature + self.normalize = normalize + if scale is not None and normalize is False: + raise ValueError("normalize should be True if scale is passed") + if scale is None: + scale = 2 * math.pi + self.scale = scale + + self.cache = {} + + def _encode_xy(self, x, y): + # The positions are expected to be normalized + assert len(x) == len(y) and x.ndim == y.ndim == 1 + x_embed = x * self.scale + y_embed = y * self.scale + + dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device) + dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats) + + pos_x = x_embed[:, None] / dim_t + pos_y = y_embed[:, None] / dim_t + pos_x = torch.stack( + (pos_x[:, 0::2].sin(), pos_x[:, 1::2].cos()), dim=2 + ).flatten(1) + pos_y = torch.stack( + (pos_y[:, 0::2].sin(), pos_y[:, 1::2].cos()), dim=2 + ).flatten(1) + return pos_x, pos_y + + @torch.no_grad() + def encode_boxes(self, x, y, w, h): + pos_x, pos_y = self._encode_xy(x, y) + pos = torch.cat((pos_y, pos_x, h[:, None], w[:, None]), dim=1) + return pos + + encode = encode_boxes # Backwards compatibility + + @torch.no_grad() + def encode_points(self, x, y, labels): + (bx, nx), (by, ny), (bl, nl) = x.shape, y.shape, labels.shape + assert bx == by and nx == ny and bx == bl and nx == nl + pos_x, pos_y = self._encode_xy(x.flatten(), y.flatten()) + pos_x, pos_y = pos_x.reshape(bx, nx, -1), pos_y.reshape(by, ny, -1) + pos = torch.cat((pos_y, pos_x, labels[:, :, None]), dim=2) + return pos + + @torch.no_grad() + def forward(self, x: torch.Tensor): + cache_key = (x.shape[-2], x.shape[-1]) + if cache_key in self.cache: + return self.cache[cache_key][None].repeat(x.shape[0], 1, 1, 1) + y_embed = ( + torch.arange(1, x.shape[-2] + 1, dtype=torch.float32, device=x.device) + .view(1, -1, 1) + .repeat(x.shape[0], 1, x.shape[-1]) + ) + x_embed = ( + torch.arange(1, x.shape[-1] + 1, dtype=torch.float32, device=x.device) + .view(1, 1, -1) + .repeat(x.shape[0], x.shape[-2], 1) + ) + + if self.normalize: + eps = 1e-6 + y_embed = y_embed / (y_embed[:, -1:, :] + eps) * self.scale + x_embed = x_embed / (x_embed[:, :, -1:] + eps) * self.scale + + dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device) + dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats) + + pos_x = x_embed[:, :, :, None] / dim_t + pos_y = y_embed[:, :, :, None] / dim_t + pos_x = torch.stack( + (pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4 + ).flatten(3) + pos_y = torch.stack( + (pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4 + ).flatten(3) + pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) + self.cache[cache_key] = pos[0] + return pos + + +class PositionEmbeddingRandom(nn.Module): + """ + Positional encoding using random spatial frequencies. + """ + + def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None: + super().__init__() + if scale is None or scale <= 0.0: + scale = 1.0 + self.register_buffer( + "positional_encoding_gaussian_matrix", + scale * torch.randn((2, num_pos_feats)), + ) + + def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor: + """Positionally encode points that are normalized to [0,1].""" + # assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape + coords = 2 * coords - 1 + coords = coords @ self.positional_encoding_gaussian_matrix + coords = 2 * np.pi * coords + # outputs d_1 x ... x d_n x C shape + return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1) + + def forward(self, size: Tuple[int, int]) -> torch.Tensor: + """Generate positional encoding for a grid of the specified size.""" + h, w = size + device: Any = self.positional_encoding_gaussian_matrix.device + grid = torch.ones((h, w), device=device, dtype=torch.float32) + y_embed = grid.cumsum(dim=0) - 0.5 + x_embed = grid.cumsum(dim=1) - 0.5 + y_embed = y_embed / h + x_embed = x_embed / w + + pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1)) + return pe.permute(2, 0, 1) # C x H x W + + def forward_with_coords( + self, coords_input: torch.Tensor, image_size: Tuple[int, int] + ) -> torch.Tensor: + """Positionally encode points that are not normalized to [0,1].""" + coords = coords_input.clone() + coords[:, :, 0] = coords[:, :, 0] / image_size[1] + coords[:, :, 1] = coords[:, :, 1] / image_size[0] + return self._pe_encoding(coords.to(torch.float)) # B x N x C + + +# Rotary Positional Encoding, adapted from: +# 1. https://github.com/meta-llama/codellama/blob/main/llama/model.py +# 2. https://github.com/naver-ai/rope-vit +# 3. https://github.com/lucidrains/rotary-embedding-torch + + +def init_t_xy(end_x: int, end_y: int): + t = torch.arange(end_x * end_y, dtype=torch.float32) + t_x = (t % end_x).float() + t_y = torch.div(t, end_x, rounding_mode="floor").float() + return t_x, t_y + + +def compute_axial_cis(dim: int, end_x: int, end_y: int, theta: float = 10000.0): + freqs_x = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim)) + freqs_y = 1.0 / (theta ** (torch.arange(0, dim, 4)[: (dim // 4)].float() / dim)) + + t_x, t_y = init_t_xy(end_x, end_y) + freqs_x = torch.outer(t_x, freqs_x) + freqs_y = torch.outer(t_y, freqs_y) + freqs_cis_x = torch.polar(torch.ones_like(freqs_x), freqs_x) + freqs_cis_y = torch.polar(torch.ones_like(freqs_y), freqs_y) + return torch.cat([freqs_cis_x, freqs_cis_y], dim=-1) + + +def reshape_for_broadcast(freqs_cis: torch.Tensor, x: torch.Tensor): + ndim = x.ndim + assert 0 <= 1 < ndim + assert freqs_cis.shape == (x.shape[-2], x.shape[-1]) + shape = [d if i >= ndim - 2 else 1 for i, d in enumerate(x.shape)] + return freqs_cis.view(*shape) + + +def apply_rotary_enc( + xq: torch.Tensor, + xk: torch.Tensor, + freqs_cis: torch.Tensor, + repeat_freqs_k: bool = False, +): + xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2)) + xk_ = ( + torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2)) + if xk.shape[-2] != 0 + else None + ) + freqs_cis = reshape_for_broadcast(freqs_cis, xq_) + xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3) + if xk_ is None: + # no keys to rotate, due to dropout + return xq_out.type_as(xq).to(xq.device), xk + # repeat freqs along seq_len dim to match k seq_len + if repeat_freqs_k: + r = xk_.shape[-2] // xq_.shape[-2] + freqs_cis = freqs_cis.repeat(*([1] * (freqs_cis.ndim - 2)), r, 1) + xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3) + return xq_out.type_as(xq).to(xq.device), xk_out.type_as(xk).to(xk.device) diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/__init__.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/__init__.py new file mode 100644 index 0000000..5277f46 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/mask_decoder.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/mask_decoder.py new file mode 100644 index 0000000..1992910 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/mask_decoder.py @@ -0,0 +1,295 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import List, Optional, Tuple, Type + +import torch +from torch import nn + +from model.segment_anything_2.sam2.modeling.sam2_utils import LayerNorm2d, MLP + + +class MaskDecoder(nn.Module): + def __init__( + self, + *, + transformer_dim: int, + transformer: nn.Module, + num_multimask_outputs: int = 3, + activation: Type[nn.Module] = nn.GELU, + iou_head_depth: int = 3, + iou_head_hidden_dim: int = 256, + use_high_res_features: bool = False, + iou_prediction_use_sigmoid=False, + dynamic_multimask_via_stability=False, + dynamic_multimask_stability_delta=0.05, + dynamic_multimask_stability_thresh=0.98, + pred_obj_scores: bool = False, + pred_obj_scores_mlp: bool = False, + use_multimask_token_for_obj_ptr: bool = False, + ) -> None: + """ + Predicts masks given an image and prompt embeddings, using a + transformer architecture. + + Arguments: + transformer_dim (int): the channel dimension of the transformer + transformer (nn.Module): the transformer used to predict masks + num_multimask_outputs (int): the number of masks to predict + when disambiguating masks + activation (nn.Module): the type of activation to use when + upscaling masks + iou_head_depth (int): the depth of the MLP used to predict + mask quality + iou_head_hidden_dim (int): the hidden dimension of the MLP + used to predict mask quality + """ + super().__init__() + self.transformer_dim = transformer_dim + self.transformer = transformer + + self.num_multimask_outputs = num_multimask_outputs + + self.iou_token = nn.Embedding(1, transformer_dim) + self.num_mask_tokens = num_multimask_outputs + 1 + self.mask_tokens = nn.Embedding(self.num_mask_tokens, transformer_dim) + + self.pred_obj_scores = pred_obj_scores + if self.pred_obj_scores: + self.obj_score_token = nn.Embedding(1, transformer_dim) + self.use_multimask_token_for_obj_ptr = use_multimask_token_for_obj_ptr + + self.output_upscaling = nn.Sequential( + nn.ConvTranspose2d( + transformer_dim, transformer_dim // 4, kernel_size=2, stride=2 + ), + LayerNorm2d(transformer_dim // 4), + activation(), + nn.ConvTranspose2d( + transformer_dim // 4, transformer_dim // 8, kernel_size=2, stride=2 + ), + activation(), + ) + self.use_high_res_features = use_high_res_features + if use_high_res_features: + self.conv_s0 = nn.Conv2d( + transformer_dim, transformer_dim // 8, kernel_size=1, stride=1 + ) + self.conv_s1 = nn.Conv2d( + transformer_dim, transformer_dim // 4, kernel_size=1, stride=1 + ) + + self.output_hypernetworks_mlps = nn.ModuleList( + [ + MLP(transformer_dim, transformer_dim, transformer_dim // 8, 3) + for i in range(self.num_mask_tokens) + ] + ) + + self.iou_prediction_head = MLP( + transformer_dim, + iou_head_hidden_dim, + self.num_mask_tokens, + iou_head_depth, + sigmoid_output=iou_prediction_use_sigmoid, + ) + if self.pred_obj_scores: + self.pred_obj_score_head = nn.Linear(transformer_dim, 1) + if pred_obj_scores_mlp: + self.pred_obj_score_head = MLP(transformer_dim, transformer_dim, 1, 3) + + # When outputting a single mask, optionally we can dynamically fall back to the best + # multimask output token if the single mask output token gives low stability scores. + self.dynamic_multimask_via_stability = dynamic_multimask_via_stability + self.dynamic_multimask_stability_delta = dynamic_multimask_stability_delta + self.dynamic_multimask_stability_thresh = dynamic_multimask_stability_thresh + + def forward( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + dense_prompt_embeddings: torch.Tensor, + multimask_output: bool, + repeat_image: bool, + high_res_features: Optional[List[torch.Tensor]] = None, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Predict masks given image and prompt embeddings. + + Arguments: + image_embeddings (torch.Tensor): the embeddings from the image encoder + image_pe (torch.Tensor): positional encoding with the shape of image_embeddings + sparse_prompt_embeddings (torch.Tensor): the embeddings of the points and boxes + dense_prompt_embeddings (torch.Tensor): the embeddings of the mask inputs + multimask_output (bool): Whether to return multiple masks or a single + mask. + + Returns: + torch.Tensor: batched predicted masks + torch.Tensor: batched predictions of mask quality + torch.Tensor: batched SAM token for mask output + """ + masks, iou_pred, mask_tokens_out, object_score_logits = self.predict_masks( + image_embeddings=image_embeddings, + image_pe=image_pe, + sparse_prompt_embeddings=sparse_prompt_embeddings, + dense_prompt_embeddings=dense_prompt_embeddings, + repeat_image=repeat_image, + high_res_features=high_res_features, + ) + + # Select the correct mask or masks for output + if multimask_output: + masks = masks[:, 1:, :, :] + iou_pred = iou_pred[:, 1:] + elif self.dynamic_multimask_via_stability and not self.training: + masks, iou_pred = self._dynamic_multimask_via_stability(masks, iou_pred) + else: + masks = masks[:, 0:1, :, :] + iou_pred = iou_pred[:, 0:1] + + if multimask_output and self.use_multimask_token_for_obj_ptr: + sam_tokens_out = mask_tokens_out[:, 1:] # [b, 3, c] shape + else: + # Take the mask output token. Here we *always* use the token for single mask output. + # At test time, even if we track after 1-click (and using multimask_output=True), + # we still take the single mask token here. The rationale is that we always track + # after multiple clicks during training, so the past tokens seen during training + # are always the single mask token (and we'll let it be the object-memory token). + sam_tokens_out = mask_tokens_out[:, 0:1] # [b, 1, c] shape + + # Prepare output + return masks, iou_pred, sam_tokens_out, object_score_logits + + def predict_masks( + self, + image_embeddings: torch.Tensor, + image_pe: torch.Tensor, + sparse_prompt_embeddings: torch.Tensor, + dense_prompt_embeddings: torch.Tensor, + repeat_image: bool, + high_res_features: Optional[List[torch.Tensor]] = None, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Predicts masks. See 'forward' for more details.""" + # Concatenate output tokens + s = 0 + if self.pred_obj_scores: + output_tokens = torch.cat( + [ + self.obj_score_token.weight, + self.iou_token.weight, + self.mask_tokens.weight, + ], + dim=0, + ) + s = 1 + else: + output_tokens = torch.cat( + [self.iou_token.weight, self.mask_tokens.weight], dim=0 + ) + output_tokens = output_tokens.unsqueeze(0).expand( + sparse_prompt_embeddings.size(0), -1, -1 + ) + tokens = torch.cat((output_tokens, sparse_prompt_embeddings), dim=1) + + # Expand per-image data in batch direction to be per-mask + if repeat_image: + src = torch.repeat_interleave(image_embeddings, tokens.shape[0], dim=0) + else: + assert image_embeddings.shape[0] == tokens.shape[0] + src = image_embeddings + src = src + dense_prompt_embeddings + assert ( + image_pe.size(0) == 1 + ), "image_pe should have size 1 in batch dim (from `get_dense_pe()`)" + pos_src = torch.repeat_interleave(image_pe, tokens.shape[0], dim=0) + b, c, h, w = src.shape + + # Run the transformer + hs, src = self.transformer(src, pos_src, tokens) + iou_token_out = hs[:, s, :] + mask_tokens_out = hs[:, s + 1 : (s + 1 + self.num_mask_tokens), :] + + # Upscale mask embeddings and predict masks using the mask tokens + src = src.transpose(1, 2).view(b, c, h, w) + if not self.use_high_res_features: + upscaled_embedding = self.output_upscaling(src) + else: + dc1, ln1, act1, dc2, act2 = self.output_upscaling + feat_s0, feat_s1 = high_res_features + upscaled_embedding = act1(ln1(dc1(src) + feat_s1)) + upscaled_embedding = act2(dc2(upscaled_embedding) + feat_s0) + + hyper_in_list: List[torch.Tensor] = [] + for i in range(self.num_mask_tokens): + hyper_in_list.append( + self.output_hypernetworks_mlps[i](mask_tokens_out[:, i, :]) + ) + hyper_in = torch.stack(hyper_in_list, dim=1) + b, c, h, w = upscaled_embedding.shape + masks = (hyper_in @ upscaled_embedding.view(b, c, h * w)).view(b, -1, h, w) + + # Generate mask quality predictions + iou_pred = self.iou_prediction_head(iou_token_out) + if self.pred_obj_scores: + assert s == 1 + object_score_logits = self.pred_obj_score_head(hs[:, 0, :]) + else: + # Obj scores logits - default to 10.0, i.e. assuming the object is present, sigmoid(10)=1 + object_score_logits = 10.0 * iou_pred.new_ones(iou_pred.shape[0], 1) + + return masks, iou_pred, mask_tokens_out, object_score_logits + + def _get_stability_scores(self, mask_logits): + """ + Compute stability scores of the mask logits based on the IoU between upper and + lower thresholds, similar to https://github.com/fairinternal/onevision/pull/568. + """ + mask_logits = mask_logits.flatten(-2) + stability_delta = self.dynamic_multimask_stability_delta + area_i = torch.sum(mask_logits > stability_delta, dim=-1).float() + area_u = torch.sum(mask_logits > -stability_delta, dim=-1).float() + stability_scores = torch.where(area_u > 0, area_i / area_u, 1.0) + return stability_scores + + def _dynamic_multimask_via_stability(self, all_mask_logits, all_iou_scores): + """ + When outputting a single mask, if the stability score from the current single-mask + output (based on output token 0) falls below a threshold, we instead select from + multi-mask outputs (based on output token 1~3) the mask with the highest predicted + IoU score. This is intended to ensure a valid mask for both clicking and tracking. + """ + # The best mask from multimask output tokens (1~3) + multimask_logits = all_mask_logits[:, 1:, :, :] + multimask_iou_scores = all_iou_scores[:, 1:] + best_scores_inds = torch.argmax(multimask_iou_scores, dim=-1) + batch_inds = torch.arange( + multimask_iou_scores.size(0), device=all_iou_scores.device + ) + best_multimask_logits = multimask_logits[batch_inds, best_scores_inds] + best_multimask_logits = best_multimask_logits.unsqueeze(1) + best_multimask_iou_scores = multimask_iou_scores[batch_inds, best_scores_inds] + best_multimask_iou_scores = best_multimask_iou_scores.unsqueeze(1) + + # The mask from singlemask output token 0 and its stability score + singlemask_logits = all_mask_logits[:, 0:1, :, :] + singlemask_iou_scores = all_iou_scores[:, 0:1] + stability_scores = self._get_stability_scores(singlemask_logits) + is_stable = stability_scores >= self.dynamic_multimask_stability_thresh + + # Dynamically fall back to best multimask output upon low stability scores. + mask_logits_out = torch.where( + is_stable[..., None, None].expand_as(singlemask_logits), + singlemask_logits, + best_multimask_logits, + ) + iou_scores_out = torch.where( + is_stable.expand_as(singlemask_iou_scores), + singlemask_iou_scores, + best_multimask_iou_scores, + ) + return mask_logits_out, iou_scores_out diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/prompt_encoder.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/prompt_encoder.py new file mode 100644 index 0000000..44fb6c5 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/prompt_encoder.py @@ -0,0 +1,241 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from typing import Any, Optional, Tuple, Type + +import numpy as np +import torch +from torch import nn +# from model.segment_anything_2.sam2.modeling.position_encoding import PositionEmbeddingRandom + +from model.segment_anything_2.sam2.modeling.sam2_utils import LayerNorm2d + + +class PromptEncoder(nn.Module): + def __init__( + self, + embed_dim: int, + image_embedding_size: Tuple[int, int], + input_image_size: Tuple[int, int], + mask_in_chans: int, + activation: Type[nn.Module] = nn.GELU, + ) -> None: + """ + Encodes prompts for input to SAM's mask decoder. + + Arguments: + embed_dim (int): The prompts' embedding dimension + image_embedding_size (tuple(int, int)): The spatial size of the + image embedding, as (H, W). + input_image_size (int): The padded size of the image as input + to the image encoder, as (H, W). + mask_in_chans (int): The number of hidden channels used for + encoding input masks. + activation (nn.Module): The activation to use when encoding + input masks. + """ + super().__init__() + self.embed_dim = embed_dim + self.input_image_size = input_image_size + self.image_embedding_size = image_embedding_size + self.pe_layer = PositionEmbeddingRandom(embed_dim // 2) + + self.num_point_embeddings: int = 4 # pos/neg point + 2 box corners + point_embeddings = [ + nn.Embedding(1, embed_dim) for i in range(self.num_point_embeddings) + ] + self.point_embeddings = nn.ModuleList(point_embeddings) + self.not_a_point_embed = nn.Embedding(1, embed_dim) + + self.mask_input_size = ( + 4 * image_embedding_size[0], + 4 * image_embedding_size[1], + ) + self.mask_downscaling = nn.Sequential( + nn.Conv2d(1, mask_in_chans // 4, kernel_size=2, stride=2), + LayerNorm2d(mask_in_chans // 4), + activation(), + nn.Conv2d(mask_in_chans // 4, mask_in_chans, kernel_size=2, stride=2), + LayerNorm2d(mask_in_chans), + activation(), + nn.Conv2d(mask_in_chans, embed_dim, kernel_size=1), + ) + self.no_mask_embed = nn.Embedding(1, embed_dim) + + def get_dense_pe(self) -> torch.Tensor: + """ + Returns the positional encoding used to encode point prompts, + applied to a dense set of points the shape of the image encoding. + + Returns: + torch.Tensor: Positional encoding with shape + 1x(embed_dim)x(embedding_h)x(embedding_w) + """ + return self.pe_layer(self.image_embedding_size).unsqueeze(0) + + def _embed_points( + self, + points: torch.Tensor, + labels: torch.Tensor, + pad: bool, + ) -> torch.Tensor: + """Embeds point prompts.""" + points = points + 0.5 # Shift to center of pixel + if pad: + padding_point = torch.zeros((points.shape[0], 1, 2), device=points.device) + padding_label = -torch.ones((labels.shape[0], 1), device=labels.device) + points = torch.cat([points, padding_point], dim=1) + labels = torch.cat([labels, padding_label], dim=1) + point_embedding = self.pe_layer.forward_with_coords( + points, self.input_image_size + ) + point_embedding[labels == -1] = 0.0 + point_embedding[labels == -1] += self.not_a_point_embed.weight + point_embedding[labels == 0] += self.point_embeddings[0].weight + point_embedding[labels == 1] += self.point_embeddings[1].weight + point_embedding[labels == 2] += self.point_embeddings[2].weight + point_embedding[labels == 3] += self.point_embeddings[3].weight + return point_embedding + + def _embed_boxes(self, boxes: torch.Tensor) -> torch.Tensor: + """Embeds box prompts.""" + boxes = boxes + 0.5 # Shift to center of pixel + coords = boxes.reshape(-1, 2, 2) + corner_embedding = self.pe_layer.forward_with_coords( + coords, self.input_image_size + ) + corner_embedding[:, 0, :] += self.point_embeddings[2].weight + corner_embedding[:, 1, :] += self.point_embeddings[3].weight + return corner_embedding + + def _embed_masks(self, masks: torch.Tensor) -> torch.Tensor: + """Embeds mask inputs.""" + mask_embedding = self.mask_downscaling(masks) + return mask_embedding + + def _get_batch_size( + self, + points: Optional[Tuple[torch.Tensor, torch.Tensor]], + boxes: Optional[torch.Tensor], + masks: Optional[torch.Tensor], + text_embeds: Optional[torch.Tensor], + ) -> int: + """ + Gets the batch size of the output given the batch size of the input prompts. + """ + if points is not None: + return points[0].shape[0] + elif boxes is not None: + return boxes.shape[0] + elif masks is not None: + return masks.shape[0] + elif text_embeds is not None: + return text_embeds.shape[0] + else: + return 1 + + def _get_device(self) -> torch.device: + return self.point_embeddings[0].weight.device + + def forward( + self, + points: Optional[Tuple[torch.Tensor, torch.Tensor]], + boxes: Optional[torch.Tensor], + masks: Optional[torch.Tensor], + text_embeds: Optional[torch.Tensor], + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Embeds different types of prompts, returning both sparse and dense + embeddings. + + Arguments: + points (tuple(torch.Tensor, torch.Tensor) or none): point coordinates + and labels to embed. + boxes (torch.Tensor or none): boxes to embed + masks (torch.Tensor or none): masks to embed + + Returns: + torch.Tensor: sparse embeddings for the points and boxes, with shape + BxNx(embed_dim), where N is determined by the number of input points + and boxes. + torch.Tensor: dense embeddings for the masks, in the shape + Bx(embed_dim)x(embed_H)x(embed_W) + """ + bs = self._get_batch_size(points, boxes, masks, text_embeds) + sparse_embeddings = torch.empty( + (bs, 0, self.embed_dim), device=self._get_device() + ) + if points is not None: + coords, labels = points + point_embeddings = self._embed_points(coords, labels, pad=(boxes is None)) + sparse_embeddings = torch.cat([sparse_embeddings, point_embeddings], dim=1) + if boxes is not None: + box_embeddings = self._embed_boxes(boxes) + sparse_embeddings = torch.cat([sparse_embeddings, box_embeddings], dim=1) + + if text_embeds is not None: + sparse_embeddings = torch.cat([sparse_embeddings, text_embeds], dim=1) + + if masks is not None: + dense_embeddings = self._embed_masks(masks) + else: + dense_embeddings = self.no_mask_embed.weight.reshape(1, -1, 1, 1).expand( + bs, -1, self.image_embedding_size[0], self.image_embedding_size[1] + ) + + return sparse_embeddings, dense_embeddings + + +class PositionEmbeddingRandom(nn.Module): + """ + Positional encoding using random spatial frequencies. + """ + + def __init__(self, num_pos_feats: int = 64, scale: Optional[float] = None) -> None: + super().__init__() + if scale is None or scale <= 0.0: + scale = 1.0 + self.register_buffer( + "positional_encoding_gaussian_matrix", + scale * torch.randn((2, num_pos_feats)), + ) + + def _pe_encoding(self, coords: torch.Tensor) -> torch.Tensor: + """Positionally encode points that are normalized to [0,1].""" + # assuming coords are in [0, 1]^2 square and have d_1 x ... x d_n x 2 shape + coords = 2 * coords - 1 + + if coords.dtype != self.positional_encoding_gaussian_matrix.dtype: + coords = coords.to(self.positional_encoding_gaussian_matrix.dtype) + + coords = coords @ self.positional_encoding_gaussian_matrix + coords = 2 * np.pi * coords + # outputs d_1 x ... x d_n x C shape + return torch.cat([torch.sin(coords), torch.cos(coords)], dim=-1) + + def forward(self, size: Tuple[int, int]) -> torch.Tensor: + """Generate positional encoding for a grid of the specified size.""" + h, w = size + device: Any = self.positional_encoding_gaussian_matrix.device + grid = torch.ones( + (h, w), device=device, dtype=self.positional_encoding_gaussian_matrix.dtype + ) + y_embed = grid.cumsum(dim=0) - 0.5 + x_embed = grid.cumsum(dim=1) - 0.5 + y_embed = y_embed / h + x_embed = x_embed / w + + pe = self._pe_encoding(torch.stack([x_embed, y_embed], dim=-1)) + return pe.permute(2, 0, 1) # C x H x W + + def forward_with_coords( + self, coords_input: torch.Tensor, image_size: Tuple[int, int] + ) -> torch.Tensor: + """Positionally encode points that are not normalized to [0,1].""" + coords = coords_input.clone() + coords[:, :, 0] = coords[:, :, 0] / image_size[1] + coords[:, :, 1] = coords[:, :, 1] / image_size[0] + return self._pe_encoding(coords.to(torch.float)) # B x N x C diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/transformer.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/transformer.py new file mode 100644 index 0000000..dae74f5 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam/transformer.py @@ -0,0 +1,330 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +import warnings +from functools import partial +from typing import Tuple, Type + +import torch +import torch.nn.functional as F +from torch import nn, Tensor + +from model.segment_anything_2.sam2.modeling.position_encoding import apply_rotary_enc, compute_axial_cis + +from model.segment_anything_2.sam2.modeling.sam2_utils import MLP +from model.segment_anything_2.sam2.utils.misc import get_sdpa_settings + +warnings.simplefilter(action="ignore", category=FutureWarning) +# OLD_GPU, USE_FLASH_ATTN, MATH_KERNEL_ON = get_sdpa_settings() +USE_FLASH_ATTN = False +MATH_KERNEL_ON = True +OLD_GPU = True + + +class TwoWayTransformer(nn.Module): + def __init__( + self, + depth: int, + embedding_dim: int, + num_heads: int, + mlp_dim: int, + activation: Type[nn.Module] = nn.ReLU, + attention_downsample_rate: int = 2, + ) -> None: + """ + A transformer decoder that attends to an input image using + queries whose positional embedding is supplied. + + Args: + depth (int): number of layers in the transformer + embedding_dim (int): the channel dimension for the input embeddings + num_heads (int): the number of heads for multihead attention. Must + divide embedding_dim + mlp_dim (int): the channel dimension internal to the MLP block + activation (nn.Module): the activation to use in the MLP block + """ + super().__init__() + self.depth = depth + self.embedding_dim = embedding_dim + self.num_heads = num_heads + self.mlp_dim = mlp_dim + self.layers = nn.ModuleList() + + for i in range(depth): + self.layers.append( + TwoWayAttentionBlock( + embedding_dim=embedding_dim, + num_heads=num_heads, + mlp_dim=mlp_dim, + activation=activation, + attention_downsample_rate=attention_downsample_rate, + skip_first_layer_pe=(i == 0), + ) + ) + + self.final_attn_token_to_image = Attention( + embedding_dim, num_heads, downsample_rate=attention_downsample_rate + ) + self.norm_final_attn = nn.LayerNorm(embedding_dim) + + def forward( + self, + image_embedding: Tensor, + image_pe: Tensor, + point_embedding: Tensor, + ) -> Tuple[Tensor, Tensor]: + """ + Args: + image_embedding (torch.Tensor): image to attend to. Should be shape + B x embedding_dim x h x w for any h and w. + image_pe (torch.Tensor): the positional encoding to add to the image. Must + have the same shape as image_embedding. + point_embedding (torch.Tensor): the embedding to add to the query points. + Must have shape B x N_points x embedding_dim for any N_points. + + Returns: + torch.Tensor: the processed point_embedding + torch.Tensor: the processed image_embedding + """ + # BxCxHxW -> BxHWxC == B x N_image_tokens x C + bs, c, h, w = image_embedding.shape + image_embedding = image_embedding.flatten(2).permute(0, 2, 1) + image_pe = image_pe.flatten(2).permute(0, 2, 1) + + # Prepare queries + queries = point_embedding + keys = image_embedding + + # Apply transformer blocks and final layernorm + for layer in self.layers: + queries, keys = layer( + queries=queries, + keys=keys, + query_pe=point_embedding, + key_pe=image_pe, + ) + + # Apply the final attention layer from the points to the image + q = queries + point_embedding + k = keys + image_pe + attn_out = self.final_attn_token_to_image(q=q, k=k, v=keys) + queries = queries + attn_out + queries = self.norm_final_attn(queries) + + return queries, keys + + +class TwoWayAttentionBlock(nn.Module): + def __init__( + self, + embedding_dim: int, + num_heads: int, + mlp_dim: int = 2048, + activation: Type[nn.Module] = nn.ReLU, + attention_downsample_rate: int = 2, + skip_first_layer_pe: bool = False, + ) -> None: + """ + A transformer block with four layers: (1) self-attention of sparse + inputs, (2) cross attention of sparse inputs to dense inputs, (3) mlp + block on sparse inputs, and (4) cross attention of dense inputs to sparse + inputs. + + Arguments: + embedding_dim (int): the channel dimension of the embeddings + num_heads (int): the number of heads in the attention layers + mlp_dim (int): the hidden dimension of the mlp block + activation (nn.Module): the activation of the mlp block + skip_first_layer_pe (bool): skip the PE on the first layer + """ + super().__init__() + self.self_attn = Attention(embedding_dim, num_heads) + self.norm1 = nn.LayerNorm(embedding_dim) + + self.cross_attn_token_to_image = Attention( + embedding_dim, num_heads, downsample_rate=attention_downsample_rate + ) + self.norm2 = nn.LayerNorm(embedding_dim) + + self.mlp = MLP( + embedding_dim, mlp_dim, embedding_dim, num_layers=2, activation=activation + ) + self.norm3 = nn.LayerNorm(embedding_dim) + + self.norm4 = nn.LayerNorm(embedding_dim) + self.cross_attn_image_to_token = Attention( + embedding_dim, num_heads, downsample_rate=attention_downsample_rate + ) + + self.skip_first_layer_pe = skip_first_layer_pe + + def forward( + self, queries: Tensor, keys: Tensor, query_pe: Tensor, key_pe: Tensor + ) -> Tuple[Tensor, Tensor]: + # Self attention block + if self.skip_first_layer_pe: + queries = self.self_attn(q=queries, k=queries, v=queries) + else: + q = queries + query_pe + attn_out = self.self_attn(q=q, k=q, v=queries) + queries = queries + attn_out + queries = self.norm1(queries) + + # Cross attention block, tokens attending to image embedding + q = queries + query_pe + k = keys + key_pe + attn_out = self.cross_attn_token_to_image(q=q, k=k, v=keys) + queries = queries + attn_out + queries = self.norm2(queries) + + # MLP block + mlp_out = self.mlp(queries) + queries = queries + mlp_out + queries = self.norm3(queries) + + # Cross attention block, image embedding attending to tokens + q = queries + query_pe + k = keys + key_pe + attn_out = self.cross_attn_image_to_token(q=k, k=q, v=queries) + keys = keys + attn_out + keys = self.norm4(keys) + + return queries, keys + + +class Attention(nn.Module): + """ + An attention layer that allows for downscaling the size of the embedding + after projection to queries, keys, and values. + """ + + def __init__( + self, + embedding_dim: int, + num_heads: int, + downsample_rate: int = 1, + dropout: float = 0.0, + kv_in_dim: int = None, + ) -> None: + super().__init__() + self.embedding_dim = embedding_dim + self.kv_in_dim = kv_in_dim if kv_in_dim is not None else embedding_dim + self.internal_dim = embedding_dim // downsample_rate + self.num_heads = num_heads + assert ( + self.internal_dim % num_heads == 0 + ), "num_heads must divide embedding_dim." + + self.q_proj = nn.Linear(embedding_dim, self.internal_dim) + self.k_proj = nn.Linear(self.kv_in_dim, self.internal_dim) + self.v_proj = nn.Linear(self.kv_in_dim, self.internal_dim) + self.out_proj = nn.Linear(self.internal_dim, embedding_dim) + + self.dropout_p = dropout + + def _separate_heads(self, x: Tensor, num_heads: int) -> Tensor: + b, n, c = x.shape + x = x.reshape(b, n, num_heads, c // num_heads) + return x.transpose(1, 2) # B x N_heads x N_tokens x C_per_head + + def _recombine_heads(self, x: Tensor) -> Tensor: + b, n_heads, n_tokens, c_per_head = x.shape + x = x.transpose(1, 2) + return x.reshape(b, n_tokens, n_heads * c_per_head) # B x N_tokens x C + + def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor: + # Input projections + q = self.q_proj(q) + k = self.k_proj(k) + v = self.v_proj(v) + + # Separate into heads + q = self._separate_heads(q, self.num_heads) + k = self._separate_heads(k, self.num_heads) + v = self._separate_heads(v, self.num_heads) + + dropout_p = self.dropout_p if self.training else 0.0 + # Attention + with torch.backends.cuda.sdp_kernel( + enable_flash=USE_FLASH_ATTN, + # if Flash attention kernel is off, then math kernel needs to be enabled + enable_math=(OLD_GPU and dropout_p > 0.0) or MATH_KERNEL_ON, + enable_mem_efficient=OLD_GPU, + ): + out = F.scaled_dot_product_attention(q, k, v, dropout_p=dropout_p) + + out = self._recombine_heads(out) + out = self.out_proj(out) + + return out + + +class RoPEAttention(Attention): + """Attention with rotary position encoding.""" + + def __init__( + self, + *args, + rope_theta=10000.0, + # whether to repeat q rope to match k length + # this is needed for cross-attention to memories + rope_k_repeat=False, + feat_sizes=(32, 32), # [w, h] for stride 16 feats at 512 resolution + **kwargs, + ): + super().__init__(*args, **kwargs) + + self.compute_cis = partial( + compute_axial_cis, dim=self.internal_dim // self.num_heads, theta=rope_theta + ) + freqs_cis = self.compute_cis(end_x=feat_sizes[0], end_y=feat_sizes[1]) + self.freqs_cis = freqs_cis + self.rope_k_repeat = rope_k_repeat + + def forward( + self, q: Tensor, k: Tensor, v: Tensor, num_k_exclude_rope: int = 0 + ) -> Tensor: + # Input projections + q = self.q_proj(q) + k = self.k_proj(k) + v = self.v_proj(v) + + # Separate into heads + q = self._separate_heads(q, self.num_heads) + k = self._separate_heads(k, self.num_heads) + v = self._separate_heads(v, self.num_heads) + + # Apply rotary position encoding + w = h = math.sqrt(q.shape[-2]) + self.freqs_cis = self.freqs_cis.to(q.device) + if self.freqs_cis.shape[0] != q.shape[-2]: + self.freqs_cis = self.compute_cis(end_x=w, end_y=h).to(q.device) + if q.shape[-2] != k.shape[-2]: + assert self.rope_k_repeat + + num_k_rope = k.size(-2) - num_k_exclude_rope + q, k[:, :, :num_k_rope] = apply_rotary_enc( + q, + k[:, :, :num_k_rope], + freqs_cis=self.freqs_cis, + repeat_freqs_k=self.rope_k_repeat, + ) + + dropout_p = self.dropout_p if self.training else 0.0 + # Attention + with torch.backends.cuda.sdp_kernel( + enable_flash=USE_FLASH_ATTN, + # if Flash attention kernel is off, then math kernel needs to be enabled + enable_math=(OLD_GPU and dropout_p > 0.0) or MATH_KERNEL_ON, + enable_mem_efficient=OLD_GPU, + ): + out = F.scaled_dot_product_attention(q, k, v, dropout_p=dropout_p) + + out = self._recombine_heads(out) + out = self.out_proj(out) + + return out diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_base.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_base.py new file mode 100644 index 0000000..746f2ed --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_base.py @@ -0,0 +1,833 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import torch +import torch.distributed +import torch.nn.functional as F + +from torch.nn.init import trunc_normal_ + +from model.segment_anything_2.sam2.modeling.sam.mask_decoder import MaskDecoder +from model.segment_anything_2.sam2.modeling.sam.prompt_encoder import PromptEncoder +from model.segment_anything_2.sam2.modeling.sam.transformer import TwoWayTransformer +from model.segment_anything_2.sam2.modeling.sam2_utils import get_1d_sine_pe, MLP, select_closest_cond_frames + +# a large negative value as a placeholder score for missing objects +NO_OBJ_SCORE = -1024.0 + + +class SAM2Base(torch.nn.Module): + def __init__( + self, + image_encoder, + memory_attention, + memory_encoder, + num_maskmem=7, # default 1 input frame + 6 previous frames + image_size=512, + backbone_stride=16, # stride of the image backbone output + sigmoid_scale_for_mem_enc=1.0, # scale factor for mask sigmoid prob + sigmoid_bias_for_mem_enc=0.0, # bias factor for mask sigmoid prob + # During evaluation, whether to binarize the sigmoid mask logits on interacted frames with clicks + binarize_mask_from_pts_for_mem_enc=False, + use_mask_input_as_output_without_sam=False, # on frames with mask input, whether to directly output the input mask without using a SAM prompt encoder + mask decoder + # The maximum number of conditioning frames to participate in the memory attention (-1 means no limit; if there are more conditioning frames than this limit, + # we only cross-attend to the temporally closest `max_cond_frames_in_attn` conditioning frames in the encoder when tracking each frame). This gives the model + # a temporal locality when handling a large number of annotated frames (since closer frames should be more important) and also avoids GPU OOM. + max_cond_frames_in_attn=-1, + # on the first frame, whether to directly add the no-memory embedding to the image feature + # (instead of using the transformer encoder) + directly_add_no_mem_embed=False, + # whether to use high-resolution feature maps in the SAM mask decoder + use_high_res_features_in_sam=False, + # whether to output multiple (3) masks for the first click on initial conditioning frames + multimask_output_in_sam=False, + # the minimum and maximum number of clicks to use multimask_output_in_sam (only relevant when `multimask_output_in_sam=True`; + # default is 1 for both, meaning that only the first click gives multimask output; also note that a box counts as two points) + multimask_min_pt_num=1, + multimask_max_pt_num=1, + # whether to also use multimask output for tracking (not just for the first click on initial conditioning frames; only relevant when `multimask_output_in_sam=True`) + multimask_output_for_tracking=False, + # Whether to use multimask tokens for obj ptr; Only relevant when both + # use_obj_ptrs_in_encoder=True and multimask_output_for_tracking=True + use_multimask_token_for_obj_ptr: bool = False, + # whether to use sigmoid to restrict ious prediction to [0-1] + iou_prediction_use_sigmoid=False, + # The memory bank's temporal stride during evaluation (i.e. the `r` parameter in XMem and Cutie; XMem and Cutie use r=5). + # For r>1, the (self.num_maskmem - 1) non-conditioning memory frames consist of + # (self.num_maskmem - 2) nearest frames from every r-th frames, plus the last frame. + memory_temporal_stride_for_eval=1, + # if `add_all_frames_to_correct_as_cond` is True, we also append to the conditioning frame list any frame that receives a later correction click + # if `add_all_frames_to_correct_as_cond` is False, we conditioning frame list to only use those initial conditioning frames + add_all_frames_to_correct_as_cond=False, + # whether to apply non-overlapping constraints on the object masks in the memory encoder during evaluation (to avoid/alleviate superposing masks) + non_overlap_masks_for_mem_enc=False, + # whether to cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder + use_obj_ptrs_in_encoder=False, + # the maximum number of object pointers from other frames in encoder cross attention (only relevant when `use_obj_ptrs_in_encoder=True`) + max_obj_ptrs_in_encoder=16, + # whether to add temporal positional encoding to the object pointers in the encoder (only relevant when `use_obj_ptrs_in_encoder=True`) + add_tpos_enc_to_obj_ptrs=True, + # whether to add an extra linear projection layer for the temporal positional encoding in the object pointers to avoid potential interference + # with spatial positional encoding (only relevant when both `use_obj_ptrs_in_encoder=True` and `add_tpos_enc_to_obj_ptrs=True`) + proj_tpos_enc_in_obj_ptrs=False, + # whether to only attend to object pointers in the past (before the current frame) in the encoder during evaluation + # (only relevant when `use_obj_ptrs_in_encoder=True`; this might avoid pointer information too far in the future to distract the initial tracking) + only_obj_ptrs_in_the_past_for_eval=False, + # Whether to predict if there is an object in the frame + pred_obj_scores: bool = False, + # Whether to use an MLP to predict object scores + pred_obj_scores_mlp: bool = False, + # Only relevant if pred_obj_scores=True and use_obj_ptrs_in_encoder=True; + # Whether to have a fixed no obj pointer when there is no object present + # or to use it as an additive embedding with obj_ptr produced by decoder + fixed_no_obj_ptr: bool = False, + # Soft no object, i.e. mix in no_obj_ptr softly, + # hope to make recovery easier if there is a mistake and mitigate accumulation of errors + soft_no_obj_ptr: bool = False, + use_mlp_for_obj_ptr_proj: bool = False, + # extra arguments used to construct the SAM mask decoder; if not None, it should be a dict of kwargs to be passed into `MaskDecoder` class. + sam_mask_decoder_extra_args=None, + compile_image_encoder: bool = False, + ): + super().__init__() + + # Part 1: the image backbone + self.image_encoder = image_encoder + # Use level 0, 1, 2 for high-res setting, or just level 2 for the default setting + self.use_high_res_features_in_sam = use_high_res_features_in_sam + self.num_feature_levels = 3 if use_high_res_features_in_sam else 1 + self.use_obj_ptrs_in_encoder = use_obj_ptrs_in_encoder + self.max_obj_ptrs_in_encoder = max_obj_ptrs_in_encoder + if use_obj_ptrs_in_encoder: + # A conv layer to downsample the mask prompt to stride 4 (the same stride as + # low-res SAM mask logits) and to change its scales from 0~1 to SAM logit scale, + # so that it can be fed into the SAM mask decoder to generate a pointer. + self.mask_downsample = torch.nn.Conv2d(1, 1, kernel_size=4, stride=4) + self.add_tpos_enc_to_obj_ptrs = add_tpos_enc_to_obj_ptrs + if proj_tpos_enc_in_obj_ptrs: + assert add_tpos_enc_to_obj_ptrs # these options need to be used together + self.proj_tpos_enc_in_obj_ptrs = proj_tpos_enc_in_obj_ptrs + self.only_obj_ptrs_in_the_past_for_eval = only_obj_ptrs_in_the_past_for_eval + + # Part 2: memory attention to condition current frame's visual features + # with memories (and obj ptrs) from past frames + self.memory_attention = memory_attention + self.hidden_dim = memory_attention.d_model + + # Part 3: memory encoder for the previous frame's outputs + self.memory_encoder = memory_encoder + self.mem_dim = self.hidden_dim + if hasattr(self.memory_encoder, "out_proj") and hasattr( + self.memory_encoder.out_proj, "weight" + ): + # if there is compression of memories along channel dim + self.mem_dim = self.memory_encoder.out_proj.weight.shape[0] + self.num_maskmem = num_maskmem # Number of memories accessible + # Temporal encoding of the memories + self.maskmem_tpos_enc = torch.nn.Parameter( + torch.zeros(num_maskmem, 1, 1, self.mem_dim) + ) + trunc_normal_(self.maskmem_tpos_enc, std=0.02) + # a single token to indicate no memory embedding from previous frames + self.no_mem_embed = torch.nn.Parameter(torch.zeros(1, 1, self.hidden_dim)) + self.no_mem_pos_enc = torch.nn.Parameter(torch.zeros(1, 1, self.hidden_dim)) + trunc_normal_(self.no_mem_embed, std=0.02) + trunc_normal_(self.no_mem_pos_enc, std=0.02) + self.directly_add_no_mem_embed = directly_add_no_mem_embed + # Apply sigmoid to the output raw mask logits (to turn them from + # range (-inf, +inf) to range (0, 1)) before feeding them into the memory encoder + self.sigmoid_scale_for_mem_enc = sigmoid_scale_for_mem_enc + self.sigmoid_bias_for_mem_enc = sigmoid_bias_for_mem_enc + self.binarize_mask_from_pts_for_mem_enc = binarize_mask_from_pts_for_mem_enc + self.non_overlap_masks_for_mem_enc = non_overlap_masks_for_mem_enc + self.memory_temporal_stride_for_eval = memory_temporal_stride_for_eval + # On frames with mask input, whether to directly output the input mask without + # using a SAM prompt encoder + mask decoder + self.use_mask_input_as_output_without_sam = use_mask_input_as_output_without_sam + self.multimask_output_in_sam = multimask_output_in_sam + self.multimask_min_pt_num = multimask_min_pt_num + self.multimask_max_pt_num = multimask_max_pt_num + self.multimask_output_for_tracking = multimask_output_for_tracking + self.use_multimask_token_for_obj_ptr = use_multimask_token_for_obj_ptr + self.iou_prediction_use_sigmoid = iou_prediction_use_sigmoid + + # Part 4: SAM-style prompt encoder (for both mask and point inputs) + # and SAM-style mask decoder for the final mask output + self.image_size = image_size + self.backbone_stride = backbone_stride + self.sam_mask_decoder_extra_args = sam_mask_decoder_extra_args + self.pred_obj_scores = pred_obj_scores + self.pred_obj_scores_mlp = pred_obj_scores_mlp + self.fixed_no_obj_ptr = fixed_no_obj_ptr + self.soft_no_obj_ptr = soft_no_obj_ptr + if self.fixed_no_obj_ptr: + assert self.pred_obj_scores + assert self.use_obj_ptrs_in_encoder + if self.pred_obj_scores and self.use_obj_ptrs_in_encoder: + self.no_obj_ptr = torch.nn.Parameter(torch.zeros(1, self.hidden_dim)) + trunc_normal_(self.no_obj_ptr, std=0.02) + self.use_mlp_for_obj_ptr_proj = use_mlp_for_obj_ptr_proj + + self._build_sam_heads() + self.add_all_frames_to_correct_as_cond = add_all_frames_to_correct_as_cond + self.max_cond_frames_in_attn = max_cond_frames_in_attn + + # Model compilation + if compile_image_encoder: + # Compile the forward function (not the full module) to allow loading checkpoints. + print( + "Image encoder compilation is enabled. First forward pass will be slow." + ) + self.image_encoder.forward = torch.compile( + self.image_encoder.forward, + mode="max-autotune", + fullgraph=True, + dynamic=False, + ) + + @property + def device(self): + return next(self.parameters()).device + + def forward(self, *args, **kwargs): + raise NotImplementedError( + "Please use the corresponding methods in SAM2VideoPredictor for inference." + "See notebooks/video_predictor_example.ipynb for an example." + ) + + def _build_sam_heads(self): + """Build SAM-style prompt encoder and mask decoder.""" + self.sam_prompt_embed_dim = self.hidden_dim + self.sam_image_embedding_size = self.image_size // self.backbone_stride + + # build PromptEncoder and MaskDecoder from SAM + # (their hyperparameters like `mask_in_chans=16` are from SAM code) + self.sam_prompt_encoder = PromptEncoder( + embed_dim=self.sam_prompt_embed_dim, + image_embedding_size=( + self.sam_image_embedding_size, + self.sam_image_embedding_size, + ), + input_image_size=(self.image_size, self.image_size), + mask_in_chans=16, + ) + self.sam_mask_decoder = MaskDecoder( + num_multimask_outputs=3, + transformer=TwoWayTransformer( + depth=2, + embedding_dim=self.sam_prompt_embed_dim, + mlp_dim=2048, + num_heads=8, + ), + transformer_dim=self.sam_prompt_embed_dim, + iou_head_depth=3, + iou_head_hidden_dim=256, + use_high_res_features=self.use_high_res_features_in_sam, + iou_prediction_use_sigmoid=self.iou_prediction_use_sigmoid, + pred_obj_scores=self.pred_obj_scores, + pred_obj_scores_mlp=self.pred_obj_scores_mlp, + use_multimask_token_for_obj_ptr=self.use_multimask_token_for_obj_ptr, + **(self.sam_mask_decoder_extra_args or {}), + ) + if self.use_obj_ptrs_in_encoder: + # a linear projection on SAM output tokens to turn them into object pointers + self.obj_ptr_proj = torch.nn.Linear(self.hidden_dim, self.hidden_dim) + if self.use_mlp_for_obj_ptr_proj: + self.obj_ptr_proj = MLP( + self.hidden_dim, self.hidden_dim, self.hidden_dim, 3 + ) + else: + self.obj_ptr_proj = torch.nn.Identity() + if self.proj_tpos_enc_in_obj_ptrs: + # a linear projection on temporal positional encoding in object pointers to + # avoid potential interference with spatial positional encoding + self.obj_ptr_tpos_proj = torch.nn.Linear(self.hidden_dim, self.mem_dim) + else: + self.obj_ptr_tpos_proj = torch.nn.Identity() + + def _forward_sam_heads( + self, + backbone_features, + point_inputs=None, + mask_inputs=None, + text_inputs=None, + high_res_features=None, + multimask_output=False, + ): + """ + Forward SAM prompt encoders and mask heads. + + Inputs: + - backbone_features: image features of [B, C, H, W] shape + - point_inputs: a dictionary with "point_coords" and "point_labels", where + 1) "point_coords" has [B, P, 2] shape and float32 dtype and contains the + absolute pixel-unit coordinate in (x, y) format of the P input points + 2) "point_labels" has shape [B, P] and int32 dtype, where 1 means + positive clicks, 0 means negative clicks, and -1 means padding + - mask_inputs: a mask of [B, 1, H*16, W*16] shape, float or bool, with the + same spatial size as the image. + - high_res_features: either 1) None or 2) or a list of length 2 containing + two feature maps of [B, C, 4*H, 4*W] and [B, C, 2*H, 2*W] shapes respectively, + which will be used as high-resolution feature maps for SAM decoder. + - multimask_output: if it's True, we output 3 candidate masks and their 3 + corresponding IoU estimates, and if it's False, we output only 1 mask and + its corresponding IoU estimate. + + Outputs: + - low_res_multimasks: [B, M, H*4, W*4] shape (where M = 3 if + `multimask_output=True` and M = 1 if `multimask_output=False`), the SAM + output mask logits (before sigmoid) for the low-resolution masks, with 4x + the resolution (1/4 stride) of the input backbone_features. + - high_res_multimasks: [B, M, H*16, W*16] shape (where M = 3 + if `multimask_output=True` and M = 1 if `multimask_output=False`), + upsampled from the low-resolution masks, with shape size as the image + (stride is 1 pixel). + - ious, [B, M] shape, where (where M = 3 if `multimask_output=True` and M = 1 + if `multimask_output=False`), the estimated IoU of each output mask. + - low_res_masks: [B, 1, H*4, W*4] shape, the best mask in `low_res_multimasks`. + If `multimask_output=True`, it's the mask with the highest IoU estimate. + If `multimask_output=False`, it's the same as `low_res_multimasks`. + - high_res_masks: [B, 1, H*16, W*16] shape, the best mask in `high_res_multimasks`. + If `multimask_output=True`, it's the mask with the highest IoU estimate. + If `multimask_output=False`, it's the same as `high_res_multimasks`. + - obj_ptr: [B, C] shape, the object pointer vector for the output mask, extracted + based on the output token from the SAM mask decoder. + """ + B = backbone_features.size(0) + device = backbone_features.device + assert backbone_features.size(1) == self.sam_prompt_embed_dim + assert backbone_features.size(2) == self.sam_image_embedding_size + assert backbone_features.size(3) == self.sam_image_embedding_size + + # a) Handle point prompts + if point_inputs is not None: + sam_point_coords = point_inputs["point_coords"] + sam_point_labels = point_inputs["point_labels"] + assert sam_point_coords.size(0) == B and sam_point_labels.size(0) == B + else: + # If no points are provide, pad with an empty point (with label -1) + sam_point_coords = torch.zeros(B, 1, 2, device=device) + sam_point_labels = -torch.ones(B, 1, dtype=torch.int32, device=device) + + # b) Handle mask prompts + if mask_inputs is not None: + # If mask_inputs is provided, downsize it into low-res mask input if needed + # and feed it as a dense mask prompt into the SAM mask encoder + assert len(mask_inputs.shape) == 4 and mask_inputs.shape[:2] == (B, 1) + if mask_inputs.shape[-2:] != self.sam_prompt_encoder.mask_input_size: + sam_mask_prompt = F.interpolate( + mask_inputs.float(), + size=self.sam_prompt_encoder.mask_input_size, + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + else: + sam_mask_prompt = mask_inputs + else: + # Otherwise, simply feed None (and SAM's prompt encoder will add + # a learned `no_mask_embed` to indicate no mask input in this case). + sam_mask_prompt = None + + sparse_embeddings, dense_embeddings = self.sam_prompt_encoder( + points=(sam_point_coords, sam_point_labels), + boxes=None, + masks=sam_mask_prompt, + text_embeds=text_inputs + ) + ( + low_res_multimasks, + ious, + sam_output_tokens, + object_score_logits, + ) = self.sam_mask_decoder( + image_embeddings=backbone_features, + image_pe=self.sam_prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + repeat_image=False, # the image is already batched + high_res_features=high_res_features, + ) + if self.pred_obj_scores: + is_obj_appearing = object_score_logits > 0 + + # Mask used for spatial memories is always a *hard* choice between obj and no obj, + # consistent with the actual mask prediction + low_res_multimasks = torch.where( + is_obj_appearing[:, None, None], + low_res_multimasks, + NO_OBJ_SCORE, + ) + + # convert masks from possibly bfloat16 (or float16) to float32 + # (older PyTorch versions before 2.1 don't support `interpolate` on bf16) + low_res_multimasks = low_res_multimasks.float() + high_res_multimasks = F.interpolate( + low_res_multimasks, + size=(self.image_size, self.image_size), + mode="bilinear", + align_corners=False, + ) + + sam_output_token = sam_output_tokens[:, 0] + if multimask_output: + # take the best mask prediction (with the highest IoU estimation) + best_iou_inds = torch.argmax(ious, dim=-1) + batch_inds = torch.arange(B, device=device) + low_res_masks = low_res_multimasks[batch_inds, best_iou_inds].unsqueeze(1) + high_res_masks = high_res_multimasks[batch_inds, best_iou_inds].unsqueeze(1) + if sam_output_tokens.size(1) > 1: + sam_output_token = sam_output_tokens[batch_inds, best_iou_inds] + else: + low_res_masks, high_res_masks = low_res_multimasks, high_res_multimasks + + # Extract object pointer from the SAM output token (with occlusion handling) + obj_ptr = self.obj_ptr_proj(sam_output_token) + if self.pred_obj_scores: + # Allow *soft* no obj ptr, unlike for masks + if self.soft_no_obj_ptr: + # Only hard possible with gt + assert not self.teacher_force_obj_scores_for_mem + lambda_is_obj_appearing = object_score_logits.sigmoid() + else: + lambda_is_obj_appearing = is_obj_appearing.float() + + if self.fixed_no_obj_ptr: + obj_ptr = lambda_is_obj_appearing * obj_ptr + obj_ptr = obj_ptr + (1 - lambda_is_obj_appearing) * self.no_obj_ptr + + return ( + low_res_multimasks, + high_res_multimasks, + ious, + low_res_masks, + high_res_masks, + obj_ptr, + object_score_logits, + ) + + def _use_mask_as_output(self, backbone_features, high_res_features, mask_inputs): + """ + Directly turn binary `mask_inputs` into a output mask logits without using SAM. + (same input and output shapes as in _forward_sam_heads above). + """ + # Use -10/+10 as logits for neg/pos pixels (very close to 0/1 in prob after sigmoid). + out_scale, out_bias = 20.0, -10.0 # sigmoid(-10.0)=4.5398e-05 + mask_inputs_float = mask_inputs.float() + high_res_masks = mask_inputs_float * out_scale + out_bias + low_res_masks = F.interpolate( + high_res_masks, + size=(high_res_masks.size(-2) // 4, high_res_masks.size(-1) // 4), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + # a dummy IoU prediction of all 1's under mask input + ious = mask_inputs.new_ones(mask_inputs.size(0), 1).float() + if not self.use_obj_ptrs_in_encoder: + # all zeros as a dummy object pointer (of shape [B, C]) + obj_ptr = torch.zeros( + mask_inputs.size(0), self.hidden_dim, device=mask_inputs.device + ) + else: + # produce an object pointer using the SAM decoder from the mask input + _, _, _, _, _, obj_ptr, _ = self._forward_sam_heads( + backbone_features=backbone_features, + mask_inputs=self.mask_downsample(mask_inputs_float), + high_res_features=high_res_features, + ) + # In this method, we are treating mask_input as output, e.g. using it directly to create spatial mem; + # Below, we follow the same design axiom to use mask_input to decide if obj appears or not instead of relying + # on the object_scores from the SAM decoder. + is_obj_appearing = torch.any(mask_inputs.flatten(1).float() > 0.0, dim=1) + is_obj_appearing = is_obj_appearing[..., None] + lambda_is_obj_appearing = is_obj_appearing.float() + object_score_logits = out_scale * lambda_is_obj_appearing + out_bias + if self.pred_obj_scores: + if self.fixed_no_obj_ptr: + obj_ptr = lambda_is_obj_appearing * obj_ptr + obj_ptr = obj_ptr + (1 - lambda_is_obj_appearing) * self.no_obj_ptr + + return ( + low_res_masks, + high_res_masks, + ious, + low_res_masks, + high_res_masks, + obj_ptr, + object_score_logits, + ) + + def forward_image(self, img_batch: torch.Tensor): + """Get the image feature on the input batch.""" + backbone_out = self.image_encoder(img_batch) + if self.use_high_res_features_in_sam: + # precompute projected level 0 and level 1 features in SAM decoder + # to avoid running it again on every SAM click + backbone_out["backbone_fpn"][0] = self.sam_mask_decoder.conv_s0( + backbone_out["backbone_fpn"][0] + ) + backbone_out["backbone_fpn"][1] = self.sam_mask_decoder.conv_s1( + backbone_out["backbone_fpn"][1] + ) + return backbone_out + + def _prepare_backbone_features(self, backbone_out): + """Prepare and flatten visual features.""" + backbone_out = backbone_out.copy() + assert len(backbone_out["backbone_fpn"]) == len(backbone_out["vision_pos_enc"]) + assert len(backbone_out["backbone_fpn"]) >= self.num_feature_levels + + feature_maps = backbone_out["backbone_fpn"][-self.num_feature_levels :] + vision_pos_embeds = backbone_out["vision_pos_enc"][-self.num_feature_levels :] + + feat_sizes = [(x.shape[-2], x.shape[-1]) for x in vision_pos_embeds] + # flatten NxCxHxW to HWxNxC + vision_feats = [x.flatten(2).permute(2, 0, 1) for x in feature_maps] + vision_pos_embeds = [x.flatten(2).permute(2, 0, 1) for x in vision_pos_embeds] + + return backbone_out, vision_feats, vision_pos_embeds, feat_sizes + + def _prepare_memory_conditioned_features( + self, + frame_idx, + is_init_cond_frame, + current_vision_feats, + current_vision_pos_embeds, + feat_sizes, + output_dict, + num_frames, + track_in_reverse=False, # tracking in reverse time order (for demo usage) + ): + """Fuse the current frame's visual feature map with previous memory.""" + B = current_vision_feats[-1].size(1) # batch size on this frame + C = self.hidden_dim + H, W = feat_sizes[-1] # top-level (lowest-resolution) feature size + device = current_vision_feats[-1].device + # The case of `self.num_maskmem == 0` below is primarily used for reproducing SAM on images. + # In this case, we skip the fusion with any memory. + if self.num_maskmem == 0: # Disable memory and skip fusion + pix_feat = current_vision_feats[-1].permute(1, 2, 0).view(B, C, H, W) + return pix_feat + + num_obj_ptr_tokens = 0 + # Step 1: condition the visual features of the current frame on previous memories + if not is_init_cond_frame: + # Retrieve the memories encoded with the maskmem backbone + to_cat_memory, to_cat_memory_pos_embed = [], [] + # Add conditioning frames's output first (all cond frames have t_pos=0 for + # when getting temporal positional embedding below) + assert len(output_dict["cond_frame_outputs"]) > 0 + # Select a maximum number of temporally closest cond frames for cross attention + cond_outputs = output_dict["cond_frame_outputs"] + selected_cond_outputs, unselected_cond_outputs = select_closest_cond_frames( + frame_idx, cond_outputs, self.max_cond_frames_in_attn + ) + t_pos_and_prevs = [(0, out) for out in selected_cond_outputs.values()] + # Add last (self.num_maskmem - 1) frames before current frame for non-conditioning memory + # the earliest one has t_pos=1 and the latest one has t_pos=self.num_maskmem-1 + # We also allow taking the memory frame non-consecutively (with r>1), in which case + # we take (self.num_maskmem - 2) frames among every r-th frames plus the last frame. + r = self.memory_temporal_stride_for_eval + for t_pos in range(1, self.num_maskmem): + t_rel = self.num_maskmem - t_pos # how many frames before current frame + if t_rel == 1: + # for t_rel == 1, we take the last frame (regardless of r) + if not track_in_reverse: + # the frame immediately before this frame (i.e. frame_idx - 1) + prev_frame_idx = frame_idx - t_rel + else: + # the frame immediately after this frame (i.e. frame_idx + 1) + prev_frame_idx = frame_idx + t_rel + else: + # for t_rel >= 2, we take the memory frame from every r-th frames + if not track_in_reverse: + # first find the nearest frame among every r-th frames before this frame + # for r=1, this would be (frame_idx - 2) + prev_frame_idx = ((frame_idx - 2) // r) * r + # then seek further among every r-th frames + prev_frame_idx = prev_frame_idx - (t_rel - 2) * r + else: + # first find the nearest frame among every r-th frames after this frame + # for r=1, this would be (frame_idx + 2) + prev_frame_idx = -(-(frame_idx + 2) // r) * r + # then seek further among every r-th frames + prev_frame_idx = prev_frame_idx + (t_rel - 2) * r + out = output_dict["non_cond_frame_outputs"].get(prev_frame_idx, None) + if out is None: + # If an unselected conditioning frame is among the last (self.num_maskmem - 1) + # frames, we still attend to it as if it's a non-conditioning frame. + out = unselected_cond_outputs.get(prev_frame_idx, None) + t_pos_and_prevs.append((t_pos, out)) + + for t_pos, prev in t_pos_and_prevs: + if prev is None: + continue # skip padding frames + # "maskmem_features" might have been offloaded to CPU in demo use cases, + # so we load it back to GPU (it's a no-op if it's already on GPU). + feats = prev["maskmem_features"].cuda(non_blocking=True) + to_cat_memory.append(feats.flatten(2).permute(2, 0, 1)) + # Spatial positional encoding (it might have been offloaded to CPU in eval) + maskmem_enc = prev["maskmem_pos_enc"][-1].cuda() + maskmem_enc = maskmem_enc.flatten(2).permute(2, 0, 1) + # Temporal positional encoding + maskmem_enc = ( + maskmem_enc + self.maskmem_tpos_enc[self.num_maskmem - t_pos - 1] + ) + to_cat_memory_pos_embed.append(maskmem_enc) + + # Construct the list of past object pointers + if self.use_obj_ptrs_in_encoder: + max_obj_ptrs_in_encoder = min(num_frames, self.max_obj_ptrs_in_encoder) + # First add those object pointers from selected conditioning frames + # (optionally, only include object pointers in the past during evaluation) + if not self.training and self.only_obj_ptrs_in_the_past_for_eval: + ptr_cond_outputs = { + t: out + for t, out in selected_cond_outputs.items() + if (t >= frame_idx if track_in_reverse else t <= frame_idx) + } + else: + ptr_cond_outputs = selected_cond_outputs + pos_and_ptrs = [ + # Temporal pos encoding contains how far away each pointer is from current frame + (abs(frame_idx - t), out["obj_ptr"]) + for t, out in ptr_cond_outputs.items() + ] + # Add up to (max_obj_ptrs_in_encoder - 1) non-conditioning frames before current frame + for t_diff in range(1, max_obj_ptrs_in_encoder): + t = frame_idx + t_diff if track_in_reverse else frame_idx - t_diff + if t < 0 or (num_frames is not None and t >= num_frames): + break + out = output_dict["non_cond_frame_outputs"].get( + t, unselected_cond_outputs.get(t, None) + ) + if out is not None: + pos_and_ptrs.append((t_diff, out["obj_ptr"])) + # If we have at least one object pointer, add them to the across attention + if len(pos_and_ptrs) > 0: + pos_list, ptrs_list = zip(*pos_and_ptrs) + # stack object pointers along dim=0 into [ptr_seq_len, B, C] shape + obj_ptrs = torch.stack(ptrs_list, dim=0) + # a temporal positional embedding based on how far each object pointer is from + # the current frame (sine embedding normalized by the max pointer num). + if self.add_tpos_enc_to_obj_ptrs: + t_diff_max = max_obj_ptrs_in_encoder - 1 + tpos_dim = C if self.proj_tpos_enc_in_obj_ptrs else self.mem_dim + obj_pos = torch.tensor(pos_list, device=device) + obj_pos = get_1d_sine_pe(obj_pos / t_diff_max, dim=tpos_dim) + obj_pos = self.obj_ptr_tpos_proj(obj_pos) + obj_pos = obj_pos.unsqueeze(1).expand(-1, B, self.mem_dim) + else: + obj_pos = obj_ptrs.new_zeros(len(pos_list), B, self.mem_dim) + if self.mem_dim < C: + # split a pointer into (C // self.mem_dim) tokens for self.mem_dim < C + obj_ptrs = obj_ptrs.reshape( + -1, B, C // self.mem_dim, self.mem_dim + ) + obj_ptrs = obj_ptrs.permute(0, 2, 1, 3).flatten(0, 1) + obj_pos = obj_pos.repeat_interleave(C // self.mem_dim, dim=0) + to_cat_memory.append(obj_ptrs) + to_cat_memory_pos_embed.append(obj_pos) + num_obj_ptr_tokens = obj_ptrs.shape[0] + else: + num_obj_ptr_tokens = 0 + else: + # for initial conditioning frames, encode them without using any previous memory + if self.directly_add_no_mem_embed: + # directly add no-mem embedding (instead of using the transformer encoder) + pix_feat_with_mem = current_vision_feats[-1] + self.no_mem_embed + pix_feat_with_mem = pix_feat_with_mem.permute(1, 2, 0).view(B, C, H, W) + return pix_feat_with_mem + + # Use a dummy token on the first frame (to avoid emtpy memory input to tranformer encoder) + to_cat_memory = [self.no_mem_embed.expand(1, B, self.mem_dim)] + to_cat_memory_pos_embed = [self.no_mem_pos_enc.expand(1, B, self.mem_dim)] + + # Step 2: Concatenate the memories and forward through the transformer encoder + memory = torch.cat(to_cat_memory, dim=0) + memory_pos_embed = torch.cat(to_cat_memory_pos_embed, dim=0) + + pix_feat_with_mem = self.memory_attention( + curr=current_vision_feats, + curr_pos=current_vision_pos_embeds, + memory=memory, + memory_pos=memory_pos_embed, + num_obj_ptr_tokens=num_obj_ptr_tokens, + ) + # reshape the output (HW)BC => BCHW + pix_feat_with_mem = pix_feat_with_mem.permute(1, 2, 0).view(B, C, H, W) + return pix_feat_with_mem + + def _encode_new_memory( + self, + current_vision_feats, + feat_sizes, + pred_masks_high_res, + is_mask_from_pts, + ): + """Encode the current image and its prediction into a memory feature.""" + B = current_vision_feats[-1].size(1) # batch size on this frame + C = self.hidden_dim + H, W = feat_sizes[-1] # top-level (lowest-resolution) feature size + # top-level feature, (HW)BC => BCHW + pix_feat = current_vision_feats[-1].permute(1, 2, 0).view(B, C, H, W) + if self.non_overlap_masks_for_mem_enc and not self.training: + # optionally, apply non-overlapping constraints to the masks (it's applied + # in the batch dimension and should only be used during eval, where all + # the objects come from the same video under batch size 1). + pred_masks_high_res = self._apply_non_overlapping_constraints( + pred_masks_high_res + ) + # scale the raw mask logits with a temperature before applying sigmoid + binarize = self.binarize_mask_from_pts_for_mem_enc and is_mask_from_pts + if binarize and not self.training: + mask_for_mem = (pred_masks_high_res > 0).float() + else: + # apply sigmoid on the raw mask logits to turn them into range (0, 1) + mask_for_mem = torch.sigmoid(pred_masks_high_res) + # apply scale and bias terms to the sigmoid probabilities + if self.sigmoid_scale_for_mem_enc != 1.0: + mask_for_mem = mask_for_mem * self.sigmoid_scale_for_mem_enc + if self.sigmoid_bias_for_mem_enc != 0.0: + mask_for_mem = mask_for_mem + self.sigmoid_bias_for_mem_enc + maskmem_out = self.memory_encoder( + pix_feat, mask_for_mem, skip_mask_sigmoid=True # sigmoid already applied + ) + maskmem_features = maskmem_out["vision_features"] + maskmem_pos_enc = maskmem_out["vision_pos_enc"] + + return maskmem_features, maskmem_pos_enc + + def track_step( + self, + frame_idx, + is_init_cond_frame, + current_vision_feats, + current_vision_pos_embeds, + feat_sizes, + point_inputs, + mask_inputs, + output_dict, + num_frames, + track_in_reverse=False, # tracking in reverse time order (for demo usage) + # Whether to run the memory encoder on the predicted masks. Sometimes we might want + # to skip the memory encoder with `run_mem_encoder=False`. For example, + # in demo we might call `track_step` multiple times for each user click, + # and only encode the memory when the user finalizes their clicks. And in ablation + # settings like SAM training on static images, we don't need the memory encoder. + run_mem_encoder=True, + # The previously predicted SAM mask logits (which can be fed together with new clicks in demo). + prev_sam_mask_logits=None, + text_inputs=None, + ): + current_out = {"point_inputs": point_inputs, "mask_inputs": mask_inputs} + # High-resolution feature maps for the SAM head, reshape (HW)BC => BCHW + if len(current_vision_feats) > 1: + high_res_features = [ + x.permute(1, 2, 0).view(x.size(1), x.size(2), *s) + for x, s in zip(current_vision_feats[:-1], feat_sizes[:-1]) + ] + else: + high_res_features = None + if mask_inputs is not None and self.use_mask_input_as_output_without_sam: + # When use_mask_input_as_output_without_sam=True, we directly output the mask input + # (see it as a GT mask) without using a SAM prompt encoder + mask decoder. + pix_feat = current_vision_feats[-1].permute(1, 2, 0) + pix_feat = pix_feat.view(-1, self.hidden_dim, *feat_sizes[-1]) + sam_outputs = self._use_mask_as_output( + pix_feat, high_res_features, mask_inputs + ) + else: + # fused the visual feature with previous memory features in the memory bank + pix_feat_with_mem = self._prepare_memory_conditioned_features( + frame_idx=frame_idx, + is_init_cond_frame=is_init_cond_frame, + current_vision_feats=current_vision_feats[-1:], + current_vision_pos_embeds=current_vision_pos_embeds[-1:], + feat_sizes=feat_sizes[-1:], + output_dict=output_dict, + num_frames=num_frames, + track_in_reverse=track_in_reverse, + ) + # apply SAM-style segmentation head + # here we might feed previously predicted low-res SAM mask logits into the SAM mask decoder, + # e.g. in demo where such logits come from earlier interaction instead of correction sampling + # (in this case, any `mask_inputs` shouldn't reach here as they are sent to _use_mask_as_output instead) + if prev_sam_mask_logits is not None: + assert point_inputs is not None and mask_inputs is None + mask_inputs = prev_sam_mask_logits + multimask_output = self._use_multimask(is_init_cond_frame, point_inputs) + sam_outputs = self._forward_sam_heads( + backbone_features=pix_feat_with_mem, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + high_res_features=high_res_features, + multimask_output=multimask_output, + text_inputs=text_inputs + ) + ( + _, + _, + _, + low_res_masks, + high_res_masks, + obj_ptr, + _, + ) = sam_outputs + + current_out["pred_masks"] = low_res_masks + current_out["pred_masks_high_res"] = high_res_masks + current_out["obj_ptr"] = obj_ptr + + # Finally run the memory encoder on the predicted mask to encode + # it into a new memory feature (that can be used in future frames) + if run_mem_encoder and self.num_maskmem > 0: + high_res_masks_for_mem_enc = high_res_masks + maskmem_features, maskmem_pos_enc = self._encode_new_memory( + current_vision_feats=current_vision_feats, + feat_sizes=feat_sizes, + pred_masks_high_res=high_res_masks_for_mem_enc, + is_mask_from_pts=(point_inputs is not None), + ) + current_out["maskmem_features"] = maskmem_features + current_out["maskmem_pos_enc"] = maskmem_pos_enc + else: + current_out["maskmem_features"] = None + current_out["maskmem_pos_enc"] = None + + return current_out + + def _use_multimask(self, is_init_cond_frame, point_inputs): + """Whether to use multimask output in the SAM head.""" + num_pts = 0 if point_inputs is None else point_inputs["point_labels"].size(1) + multimask_output = ( + self.multimask_output_in_sam + and (is_init_cond_frame or self.multimask_output_for_tracking) + and (self.multimask_min_pt_num <= num_pts <= self.multimask_max_pt_num) + ) + return multimask_output + + def _apply_non_overlapping_constraints(self, pred_masks): + """ + Apply non-overlapping constraints to the object scores in pred_masks. Here we + keep only the highest scoring object at each spatial location in pred_masks. + """ + batch_size = pred_masks.size(0) + if batch_size == 1: + return pred_masks + + device = pred_masks.device + # "max_obj_inds": object index of the object with the highest score at each location + max_obj_inds = torch.argmax(pred_masks, dim=0, keepdim=True) + # "batch_obj_inds": object index of each object slice (along dim 0) in `pred_masks` + batch_obj_inds = torch.arange(batch_size, device=device)[:, None, None, None] + keep = max_obj_inds == batch_obj_inds + # suppress overlapping regions' scores below -10.0 so that the foreground regions + # don't overlap (here sigmoid(-10.0)=4.5398e-05) + pred_masks = torch.where(keep, pred_masks, torch.clamp(pred_masks, max=-10.0)) + return pred_masks diff --git a/py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_utils.py b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_utils.py new file mode 100644 index 0000000..6d97059 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/modeling/sam2_utils.py @@ -0,0 +1,149 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + + +import copy + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +def select_closest_cond_frames(frame_idx, cond_frame_outputs, max_cond_frame_num): + """ + Select up to `max_cond_frame_num` conditioning frames from `cond_frame_outputs` + that are temporally closest to the current frame at `frame_idx`. Here, we take + - a) the closest conditioning frame before `frame_idx` (if any); + - b) the closest conditioning frame after `frame_idx` (if any); + - c) any other temporally closest conditioning frames until reaching a total + of `max_cond_frame_num` conditioning frames. + + Outputs: + - selected_outputs: selected items (keys & values) from `cond_frame_outputs`. + - unselected_outputs: items (keys & values) not selected in `cond_frame_outputs`. + """ + if max_cond_frame_num == -1 or len(cond_frame_outputs) <= max_cond_frame_num: + selected_outputs = cond_frame_outputs + unselected_outputs = {} + else: + assert max_cond_frame_num >= 2, "we should allow using 2+ conditioning frames" + selected_outputs = {} + + # the closest conditioning frame before `frame_idx` (if any) + idx_before = max((t for t in cond_frame_outputs if t < frame_idx), default=None) + if idx_before is not None: + selected_outputs[idx_before] = cond_frame_outputs[idx_before] + + # the closest conditioning frame after `frame_idx` (if any) + idx_after = min((t for t in cond_frame_outputs if t >= frame_idx), default=None) + if idx_after is not None: + selected_outputs[idx_after] = cond_frame_outputs[idx_after] + + # add other temporally closest conditioning frames until reaching a total + # of `max_cond_frame_num` conditioning frames. + num_remain = max_cond_frame_num - len(selected_outputs) + inds_remain = sorted( + (t for t in cond_frame_outputs if t not in selected_outputs), + key=lambda x: abs(x - frame_idx), + )[:num_remain] + selected_outputs.update((t, cond_frame_outputs[t]) for t in inds_remain) + unselected_outputs = { + t: v for t, v in cond_frame_outputs.items() if t not in selected_outputs + } + + return selected_outputs, unselected_outputs + + +def get_1d_sine_pe(pos_inds, dim, temperature=10000): + """ + Get 1D sine positional embedding as in the original Transformer paper. + """ + pe_dim = dim // 2 + dim_t = torch.arange(pe_dim, dtype=torch.float32, device=pos_inds.device) + dim_t = temperature ** (2 * (dim_t // 2) / pe_dim) + + pos_embed = pos_inds.unsqueeze(-1) / dim_t + pos_embed = torch.cat([pos_embed.sin(), pos_embed.cos()], dim=-1) + return pos_embed + + +def get_activation_fn(activation): + """Return an activation function given a string""" + if activation == "relu": + return F.relu + if activation == "gelu": + return F.gelu + if activation == "glu": + return F.glu + raise RuntimeError(f"activation should be relu/gelu, not {activation}.") + + +def get_clones(module, N): + return nn.ModuleList([copy.deepcopy(module) for i in range(N)]) + + +class DropPath(nn.Module): + # adapted from https://github.com/huggingface/pytorch-image-models/blob/main/timm/layers/drop.py + def __init__(self, drop_prob=0.0, scale_by_keep=True): + super(DropPath, self).__init__() + self.drop_prob = drop_prob + self.scale_by_keep = scale_by_keep + + def forward(self, x): + if self.drop_prob == 0.0 or not self.training: + return x + keep_prob = 1 - self.drop_prob + shape = (x.shape[0],) + (1,) * (x.ndim - 1) + random_tensor = x.new_empty(shape).bernoulli_(keep_prob) + if keep_prob > 0.0 and self.scale_by_keep: + random_tensor.div_(keep_prob) + return x * random_tensor + + +# Lightly adapted from +# https://github.com/facebookresearch/MaskFormer/blob/main/mask_former/modeling/transformer/transformer_predictor.py # noqa +class MLP(nn.Module): + def __init__( + self, + input_dim: int, + hidden_dim: int, + output_dim: int, + num_layers: int, + activation: nn.Module = nn.ReLU, + sigmoid_output: bool = False, + ) -> None: + super().__init__() + self.num_layers = num_layers + h = [hidden_dim] * (num_layers - 1) + self.layers = nn.ModuleList( + nn.Linear(n, k) for n, k in zip([input_dim] + h, h + [output_dim]) + ) + self.sigmoid_output = sigmoid_output + self.act = activation() + + def forward(self, x): + for i, layer in enumerate(self.layers): + x = self.act(layer(x)) if i < self.num_layers - 1 else layer(x) + if self.sigmoid_output: + x = F.sigmoid(x) + return x + + +# From https://github.com/facebookresearch/detectron2/blob/main/detectron2/layers/batch_norm.py # noqa +# Itself from https://github.com/facebookresearch/ConvNeXt/blob/d1fa8f6fef0a165b27399986cc2bdacc92777e40/models/convnext.py#L119 # noqa +class LayerNorm2d(nn.Module): + def __init__(self, num_channels: int, eps: float = 1e-6) -> None: + super().__init__() + self.weight = nn.Parameter(torch.ones(num_channels)) + self.bias = nn.Parameter(torch.zeros(num_channels)) + self.eps = eps + + def forward(self, x: torch.Tensor) -> torch.Tensor: + u = x.mean(1, keepdim=True) + s = (x - u).pow(2).mean(1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.eps) + x = self.weight[:, None, None] * x + self.bias[:, None, None] + return x diff --git a/py/evf-sam/model/segment_anything_2/sam2/sam2_image_predictor.py b/py/evf-sam/model/segment_anything_2/sam2/sam2_image_predictor.py new file mode 100644 index 0000000..5b7d762 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/sam2_image_predictor.py @@ -0,0 +1,446 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import logging + +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +from PIL.Image import Image + +from model.segment_anything_2.sam2.modeling.sam2_base import SAM2Base + +from model.segment_anything_2.sam2.utils.transforms import SAM2Transforms + + +class SAM2ImagePredictor: + def __init__( + self, + sam_model: SAM2Base, + mask_threshold=0.0, + max_hole_area=0.0, + max_sprinkle_area=0.0, + ) -> None: + """ + Uses SAM-2 to calculate the image embedding for an image, and then + allow repeated, efficient mask prediction given prompts. + + Arguments: + sam_model (Sam-2): The model to use for mask prediction. + mask_threshold (float): The threshold to use when converting mask logits + to binary masks. Masks are thresholded at 0 by default. + fill_hole_area (int): If fill_hole_area > 0, we fill small holes in up to + the maximum area of fill_hole_area in low_res_masks. + """ + super().__init__() + self.model = sam_model + self._transforms = SAM2Transforms( + resolution=self.model.image_size, + mask_threshold=mask_threshold, + max_hole_area=max_hole_area, + max_sprinkle_area=max_sprinkle_area, + ) + + # Predictor state + self._is_image_set = False + self._features = None + self._orig_hw = None + # Whether the predictor is set for single image or a batch of images + self._is_batch = False + + # Predictor config + self.mask_threshold = mask_threshold + + # Spatial dim for backbone feature maps + self._bb_feat_sizes = [ + (256, 256), + (128, 128), + (64, 64), + ] + + @torch.no_grad() + def set_image( + self, + image: Union[np.ndarray, Image], + ) -> None: + """ + Calculates the image embeddings for the provided image, allowing + masks to be predicted with the 'predict' method. + + Arguments: + image (np.ndarray or PIL Image): The input image to embed in RGB format. The image should be in HWC format if np.ndarray, or WHC format if PIL Image + with pixel values in [0, 255]. + image_format (str): The color format of the image, in ['RGB', 'BGR']. + """ + self.reset_predictor() + # Transform the image to the form expected by the model + if isinstance(image, np.ndarray): + logging.info("For numpy array image, we assume (HxWxC) format") + self._orig_hw = [image.shape[:2]] + elif isinstance(image, Image): + w, h = image.size + self._orig_hw = [(h, w)] + else: + raise NotImplementedError("Image format not supported") + + input_image = self._transforms(image) + input_image = input_image[None, ...].to(self.device) + + assert ( + len(input_image.shape) == 4 and input_image.shape[1] == 3 + ), f"input_image must be of size 1x3xHxW, got {input_image.shape}" + logging.info("Computing image embeddings for the provided image...") + backbone_out = self.model.forward_image(input_image) + _, vision_feats, _, _ = self.model._prepare_backbone_features(backbone_out) + # Add no_mem_embed, which is added to the lowest rest feat. map during training on videos + if self.model.directly_add_no_mem_embed: + vision_feats[-1] = vision_feats[-1] + self.model.no_mem_embed + + feats = [ + feat.permute(1, 2, 0).view(1, -1, *feat_size) + for feat, feat_size in zip(vision_feats[::-1], self._bb_feat_sizes[::-1]) + ][::-1] + self._features = {"image_embed": feats[-1], "high_res_feats": feats[:-1]} + self._is_image_set = True + logging.info("Image embeddings computed.") + + @torch.no_grad() + def set_image_batch( + self, + image_list: List[Union[np.ndarray]], + ) -> None: + """ + Calculates the image embeddings for the provided image batch, allowing + masks to be predicted with the 'predict_batch' method. + + Arguments: + image_list (List[np.ndarray]): The input images to embed in RGB format. The image should be in HWC format if np.ndarray + with pixel values in [0, 255]. + """ + self.reset_predictor() + assert isinstance(image_list, list) + self._orig_hw = [] + for image in image_list: + assert isinstance( + image, np.ndarray + ), "Images are expected to be an np.ndarray in RGB format, and of shape HWC" + self._orig_hw.append(image.shape[:2]) + # Transform the image to the form expected by the model + img_batch = self._transforms.forward_batch(image_list) + img_batch = img_batch.to(self.device) + batch_size = img_batch.shape[0] + assert ( + len(img_batch.shape) == 4 and img_batch.shape[1] == 3 + ), f"img_batch must be of size Bx3xHxW, got {img_batch.shape}" + logging.info("Computing image embeddings for the provided images...") + backbone_out = self.model.forward_image(img_batch) + _, vision_feats, _, _ = self.model._prepare_backbone_features(backbone_out) + # Add no_mem_embed, which is added to the lowest rest feat. map during training on videos + if self.model.directly_add_no_mem_embed: + vision_feats[-1] = vision_feats[-1] + self.model.no_mem_embed + + feats = [ + feat.permute(1, 2, 0).view(batch_size, -1, *feat_size) + for feat, feat_size in zip(vision_feats[::-1], self._bb_feat_sizes[::-1]) + ][::-1] + self._features = {"image_embed": feats[-1], "high_res_feats": feats[:-1]} + self._is_image_set = True + self._is_batch = True + logging.info("Image embeddings computed.") + + def predict_batch( + self, + point_coords_batch: List[np.ndarray] = None, + point_labels_batch: List[np.ndarray] = None, + box_batch: List[np.ndarray] = None, + mask_input_batch: List[np.ndarray] = None, + multimask_output: bool = True, + return_logits: bool = False, + normalize_coords=True, + ) -> Tuple[List[np.ndarray], List[np.ndarray], List[np.ndarray]]: + """This function is very similar to predict(...), however it is used for batched mode, when the model is expected to generate predictions on multiple images. + It returns a tupele of lists of masks, ious, and low_res_masks_logits. + """ + assert self._is_batch, "This function should only be used when in batched mode" + if not self._is_image_set: + raise RuntimeError( + "An image must be set with .set_image_batch(...) before mask prediction." + ) + num_images = len(self._features["image_embed"]) + all_masks = [] + all_ious = [] + all_low_res_masks = [] + for img_idx in range(num_images): + # Transform input prompts + point_coords = ( + point_coords_batch[img_idx] if point_coords_batch is not None else None + ) + point_labels = ( + point_labels_batch[img_idx] if point_labels_batch is not None else None + ) + box = box_batch[img_idx] if box_batch is not None else None + mask_input = ( + mask_input_batch[img_idx] if mask_input_batch is not None else None + ) + mask_input, unnorm_coords, labels, unnorm_box = self._prep_prompts( + point_coords, + point_labels, + box, + mask_input, + normalize_coords, + img_idx=img_idx, + ) + masks, iou_predictions, low_res_masks = self._predict( + unnorm_coords, + labels, + unnorm_box, + mask_input, + multimask_output, + return_logits=return_logits, + img_idx=img_idx, + ) + masks_np = masks.squeeze(0).float().detach().cpu().numpy() + iou_predictions_np = ( + iou_predictions.squeeze(0).float().detach().cpu().numpy() + ) + low_res_masks_np = low_res_masks.squeeze(0).float().detach().cpu().numpy() + all_masks.append(masks_np) + all_ious.append(iou_predictions_np) + all_low_res_masks.append(low_res_masks_np) + + return all_masks, all_ious, all_low_res_masks + + def predict( + self, + point_coords: Optional[np.ndarray] = None, + point_labels: Optional[np.ndarray] = None, + box: Optional[np.ndarray] = None, + mask_input: Optional[np.ndarray] = None, + multimask_output: bool = True, + return_logits: bool = False, + normalize_coords=True, + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """ + Predict masks for the given input prompts, using the currently set image. + + Arguments: + point_coords (np.ndarray or None): A Nx2 array of point prompts to the + model. Each point is in (X,Y) in pixels. + point_labels (np.ndarray or None): A length N array of labels for the + point prompts. 1 indicates a foreground point and 0 indicates a + background point. + box (np.ndarray or None): A length 4 array given a box prompt to the + model, in XYXY format. + mask_input (np.ndarray): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form 1xHxW, where + for SAM, H=W=256. + multimask_output (bool): If true, the model will return three masks. + For ambiguous input prompts (such as a single click), this will often + produce better masks than a single prediction. If only a single + mask is needed, the model's predicted quality score can be used + to select the best mask. For non-ambiguous prompts, such as multiple + input prompts, multimask_output=False can give better results. + return_logits (bool): If true, returns un-thresholded masks logits + instead of a binary mask. + normalize_coords (bool): If true, the point coordinates will be normalized to the range [0,1] and point_coords is expected to be wrt. image dimensions. + + Returns: + (np.ndarray): The output masks in CxHxW format, where C is the + number of masks, and (H, W) is the original image size. + (np.ndarray): An array of length C containing the model's + predictions for the quality of each mask. + (np.ndarray): An array of shape CxHxW, where C is the number + of masks and H=W=256. These low resolution logits can be passed to + a subsequent iteration as mask input. + """ + if not self._is_image_set: + raise RuntimeError( + "An image must be set with .set_image(...) before mask prediction." + ) + + # Transform input prompts + + mask_input, unnorm_coords, labels, unnorm_box = self._prep_prompts( + point_coords, point_labels, box, mask_input, normalize_coords + ) + + masks, iou_predictions, low_res_masks = self._predict( + unnorm_coords, + labels, + unnorm_box, + mask_input, + multimask_output, + return_logits=return_logits, + ) + + masks_np = masks.squeeze(0).float().detach().cpu().numpy() + iou_predictions_np = iou_predictions.squeeze(0).float().detach().cpu().numpy() + low_res_masks_np = low_res_masks.squeeze(0).float().detach().cpu().numpy() + return masks_np, iou_predictions_np, low_res_masks_np + + def _prep_prompts( + self, point_coords, point_labels, box, mask_logits, normalize_coords, img_idx=-1 + ): + + unnorm_coords, labels, unnorm_box, mask_input = None, None, None, None + if point_coords is not None: + assert ( + point_labels is not None + ), "point_labels must be supplied if point_coords is supplied." + point_coords = torch.as_tensor( + point_coords, dtype=torch.float, device=self.device + ) + unnorm_coords = self._transforms.transform_coords( + point_coords, normalize=normalize_coords, orig_hw=self._orig_hw[img_idx] + ) + labels = torch.as_tensor(point_labels, dtype=torch.int, device=self.device) + if len(unnorm_coords.shape) == 2: + unnorm_coords, labels = unnorm_coords[None, ...], labels[None, ...] + if box is not None: + box = torch.as_tensor(box, dtype=torch.float, device=self.device) + unnorm_box = self._transforms.transform_boxes( + box, normalize=normalize_coords, orig_hw=self._orig_hw[img_idx] + ) # Bx2x2 + if mask_logits is not None: + mask_input = torch.as_tensor( + mask_logits, dtype=torch.float, device=self.device + ) + if len(mask_input.shape) == 3: + mask_input = mask_input[None, :, :, :] + return mask_input, unnorm_coords, labels, unnorm_box + + @torch.no_grad() + def _predict( + self, + point_coords: Optional[torch.Tensor], + point_labels: Optional[torch.Tensor], + boxes: Optional[torch.Tensor] = None, + mask_input: Optional[torch.Tensor] = None, + multimask_output: bool = True, + return_logits: bool = False, + img_idx: int = -1, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Predict masks for the given input prompts, using the currently set image. + Input prompts are batched torch tensors and are expected to already be + transformed to the input frame using SAM2Transforms. + + Arguments: + point_coords (torch.Tensor or None): A BxNx2 array of point prompts to the + model. Each point is in (X,Y) in pixels. + point_labels (torch.Tensor or None): A BxN array of labels for the + point prompts. 1 indicates a foreground point and 0 indicates a + background point. + boxes (np.ndarray or None): A Bx4 array given a box prompt to the + model, in XYXY format. + mask_input (np.ndarray): A low resolution mask input to the model, typically + coming from a previous prediction iteration. Has form Bx1xHxW, where + for SAM, H=W=256. Masks returned by a previous iteration of the + predict method do not need further transformation. + multimask_output (bool): If true, the model will return three masks. + For ambiguous input prompts (such as a single click), this will often + produce better masks than a single prediction. If only a single + mask is needed, the model's predicted quality score can be used + to select the best mask. For non-ambiguous prompts, such as multiple + input prompts, multimask_output=False can give better results. + return_logits (bool): If true, returns un-thresholded masks logits + instead of a binary mask. + + Returns: + (torch.Tensor): The output masks in BxCxHxW format, where C is the + number of masks, and (H, W) is the original image size. + (torch.Tensor): An array of shape BxC containing the model's + predictions for the quality of each mask. + (torch.Tensor): An array of shape BxCxHxW, where C is the number + of masks and H=W=256. These low res logits can be passed to + a subsequent iteration as mask input. + """ + if not self._is_image_set: + raise RuntimeError( + "An image must be set with .set_image(...) before mask prediction." + ) + + if point_coords is not None: + concat_points = (point_coords, point_labels) + else: + concat_points = None + + # Embed prompts + if boxes is not None: + box_coords = boxes.reshape(-1, 2, 2) + box_labels = torch.tensor([[2, 3]], dtype=torch.int, device=boxes.device) + box_labels = box_labels.repeat(boxes.size(0), 1) + # we merge "boxes" and "points" into a single "concat_points" input (where + # boxes are added at the beginning) to sam_prompt_encoder + if concat_points is not None: + concat_coords = torch.cat([box_coords, concat_points[0]], dim=1) + concat_labels = torch.cat([box_labels, concat_points[1]], dim=1) + concat_points = (concat_coords, concat_labels) + else: + concat_points = (box_coords, box_labels) + + sparse_embeddings, dense_embeddings = self.model.sam_prompt_encoder( + points=concat_points, + boxes=None, + masks=mask_input, + ) + + # Predict masks + batched_mode = ( + concat_points is not None and concat_points[0].shape[0] > 1 + ) # multi object prediction + high_res_features = [ + feat_level[img_idx].unsqueeze(0) + for feat_level in self._features["high_res_feats"] + ] + low_res_masks, iou_predictions, _, _ = self.model.sam_mask_decoder( + image_embeddings=self._features["image_embed"][img_idx].unsqueeze(0), + image_pe=self.model.sam_prompt_encoder.get_dense_pe(), + sparse_prompt_embeddings=sparse_embeddings, + dense_prompt_embeddings=dense_embeddings, + multimask_output=multimask_output, + repeat_image=batched_mode, + high_res_features=high_res_features, + ) + + # Upscale the masks to the original image resolution + masks = self._transforms.postprocess_masks( + low_res_masks, self._orig_hw[img_idx] + ) + low_res_masks = torch.clamp(low_res_masks, -32.0, 32.0) + if not return_logits: + masks = masks > self.mask_threshold + + return masks, iou_predictions, low_res_masks + + def get_image_embedding(self) -> torch.Tensor: + """ + Returns the image embeddings for the currently set image, with + shape 1xCxHxW, where C is the embedding dimension and (H,W) are + the embedding spatial dimension of SAM (typically C=256, H=W=64). + """ + if not self._is_image_set: + raise RuntimeError( + "An image must be set with .set_image(...) to generate an embedding." + ) + assert ( + self._features is not None + ), "Features must exist if an image has been set." + return self._features["image_embed"] + + @property + def device(self) -> torch.device: + return self.model.device + + def reset_predictor(self) -> None: + """ + Resets the image embeddings and other state variables. + """ + self._is_image_set = False + self._features = None + self._orig_hw = None + self._is_batch = False diff --git a/py/evf-sam/model/segment_anything_2/sam2/sam2_video_predictor.py b/py/evf-sam/model/segment_anything_2/sam2/sam2_video_predictor.py new file mode 100644 index 0000000..be925cb --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/sam2_video_predictor.py @@ -0,0 +1,984 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from collections import OrderedDict + +import torch + +from tqdm import tqdm + +from model.segment_anything_2.sam2.modeling.sam2_base import NO_OBJ_SCORE, SAM2Base +from model.segment_anything_2.sam2.utils.misc import concat_points, fill_holes_in_mask_scores, load_video_frames + + +class SAM2VideoPredictor(SAM2Base): + """The predictor class to handle user interactions and manage inference states.""" + + def __init__( + self, + fill_hole_area=0, + # whether to apply non-overlapping constraints on the output object masks + non_overlap_masks=False, + # whether to clear non-conditioning memory of the surrounding frames (which may contain outdated information) after adding correction clicks; + # note that this would only apply to *single-object tracking* unless `clear_non_cond_mem_for_multi_obj` is also set to True) + clear_non_cond_mem_around_input=False, + # whether to also clear non-conditioning memory of the surrounding frames (only effective when `clear_non_cond_mem_around_input` is True). + clear_non_cond_mem_for_multi_obj=False, + **kwargs, + ): + super().__init__(**kwargs) + self.fill_hole_area = fill_hole_area + self.non_overlap_masks = non_overlap_masks + self.clear_non_cond_mem_around_input = clear_non_cond_mem_around_input + self.clear_non_cond_mem_for_multi_obj = clear_non_cond_mem_for_multi_obj + + @torch.inference_mode() + def init_state( + self, + video_path, + offload_video_to_cpu=False, + offload_state_to_cpu=False, + async_loading_frames=False, + ): + """Initialize a inference state.""" + images, video_height, video_width = load_video_frames( + video_path=video_path, + image_size=self.image_size, + offload_video_to_cpu=offload_video_to_cpu, + async_loading_frames=async_loading_frames, + ) + inference_state = {} + inference_state["images"] = images + inference_state["num_frames"] = len(images) + # whether to offload the video frames to CPU memory + # turning on this option saves the GPU memory with only a very small overhead + inference_state["offload_video_to_cpu"] = offload_video_to_cpu + # whether to offload the inference state to CPU memory + # turning on this option saves the GPU memory at the cost of a lower tracking fps + # (e.g. in a test case of 768x768 model, fps dropped from 27 to 24 when tracking one object + # and from 24 to 21 when tracking two objects) + inference_state["offload_state_to_cpu"] = offload_state_to_cpu + # the original video height and width, used for resizing final output scores + inference_state["video_height"] = video_height + inference_state["video_width"] = video_width + inference_state["device"] = torch.device("cuda") + if offload_state_to_cpu: + inference_state["storage_device"] = torch.device("cpu") + else: + inference_state["storage_device"] = torch.device("cuda") + # inputs on each frame + inference_state["point_inputs_per_obj"] = {} + inference_state["mask_inputs_per_obj"] = {} + # visual features on a small number of recently visited frames for quick interactions + inference_state["cached_features"] = {} + # values that don't change across frames (so we only need to hold one copy of them) + inference_state["constants"] = {} + # mapping between client-side object id and model-side object index + inference_state["obj_id_to_idx"] = OrderedDict() + inference_state["obj_idx_to_id"] = OrderedDict() + inference_state["obj_ids"] = [] + # A storage to hold the model's tracking results and states on each frame + inference_state["output_dict"] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + # Slice (view) of each object tracking results, sharing the same memory with "output_dict" + inference_state["output_dict_per_obj"] = {} + # A temporary storage to hold new outputs when user interact with a frame + # to add clicks or mask (it's merged into "output_dict" before propagation starts) + inference_state["temp_output_dict_per_obj"] = {} + # Frames that already holds consolidated outputs from click or mask inputs + # (we directly use their consolidated outputs during tracking) + inference_state["consolidated_frame_inds"] = { + "cond_frame_outputs": set(), # set containing frame indices + "non_cond_frame_outputs": set(), # set containing frame indices + } + # metadata for each tracking frame (e.g. which direction it's tracked) + inference_state["tracking_has_started"] = False + inference_state["frames_already_tracked"] = {} + # Warm up the visual backbone and cache the image feature on frame 0 + self._get_image_feature(inference_state, frame_idx=0, batch_size=1) + return inference_state + + def _obj_id_to_idx(self, inference_state, obj_id): + """Map client-side object id to model-side object index.""" + obj_idx = inference_state["obj_id_to_idx"].get(obj_id, None) + if obj_idx is not None: + return obj_idx + + # This is a new object id not sent to the server before. We only allow adding + # new objects *before* the tracking starts. + allow_new_object = not inference_state["tracking_has_started"] + if allow_new_object: + # get the next object slot + obj_idx = len(inference_state["obj_id_to_idx"]) + inference_state["obj_id_to_idx"][obj_id] = obj_idx + inference_state["obj_idx_to_id"][obj_idx] = obj_id + inference_state["obj_ids"] = list(inference_state["obj_id_to_idx"]) + # set up input and output structures for this object + inference_state["point_inputs_per_obj"][obj_idx] = {} + inference_state["mask_inputs_per_obj"][obj_idx] = {} + inference_state["output_dict_per_obj"][obj_idx] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + inference_state["temp_output_dict_per_obj"][obj_idx] = { + "cond_frame_outputs": {}, # dict containing {frame_idx: } + "non_cond_frame_outputs": {}, # dict containing {frame_idx: } + } + return obj_idx + else: + raise RuntimeError( + f"Cannot add new object id {obj_id} after tracking starts. " + f"All existing object ids: {inference_state['obj_ids']}. " + f"Please call 'reset_state' to restart from scratch." + ) + + def _obj_idx_to_id(self, inference_state, obj_idx): + """Map model-side object index to client-side object id.""" + return inference_state["obj_idx_to_id"][obj_idx] + + def _get_obj_num(self, inference_state): + """Get the total number of unique object ids received so far in this session.""" + return len(inference_state["obj_idx_to_id"]) + + @torch.inference_mode() + def add_new_points( + self, + inference_state, + frame_idx, + obj_id, + points, + labels, + clear_old_points=True, + normalize_coords=True, + ): + """Add new points to a frame.""" + obj_idx = self._obj_id_to_idx(inference_state, obj_id) + point_inputs_per_frame = inference_state["point_inputs_per_obj"][obj_idx] + mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx] + + if not isinstance(points, torch.Tensor): + points = torch.tensor(points, dtype=torch.float32) + if not isinstance(labels, torch.Tensor): + labels = torch.tensor(labels, dtype=torch.int32) + if points.dim() == 2: + points = points.unsqueeze(0) # add batch dimension + if labels.dim() == 1: + labels = labels.unsqueeze(0) # add batch dimension + if normalize_coords: + video_H = inference_state["video_height"] + video_W = inference_state["video_width"] + points = points / torch.tensor([video_W, video_H]).to(points.device) + # scale the (normalized) coordinates by the model's internal image size + points = points * self.image_size + points = points.to(inference_state["device"]) + labels = labels.to(inference_state["device"]) + + if not clear_old_points: + point_inputs = point_inputs_per_frame.get(frame_idx, None) + else: + point_inputs = None + point_inputs = concat_points(point_inputs, points, labels) + + point_inputs_per_frame[frame_idx] = point_inputs + mask_inputs_per_frame.pop(frame_idx, None) + # If this frame hasn't been tracked before, we treat it as an initial conditioning + # frame, meaning that the inputs points are to generate segments on this frame without + # using any memory from other frames, like in SAM. Otherwise (if it has been tracked), + # the input points will be used to correct the already tracked masks. + is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"] + # whether to track in reverse time order + if is_init_cond_frame: + reverse = False + else: + reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"] + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + # Add a frame to conditioning output if it's an initial conditioning frame or + # if the model sees all frames receiving clicks/mask as conditioning frames. + is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + + # Get any previously predicted mask logits on this object and feed it along with + # the new clicks into the SAM mask decoder. + prev_sam_mask_logits = None + # lookup temporary output dict first, which contains the most recent output + # (if not found, then lookup conditioning and non-conditioning frame output) + prev_out = obj_temp_output_dict[storage_key].get(frame_idx) + if prev_out is None: + prev_out = obj_output_dict["cond_frame_outputs"].get(frame_idx) + if prev_out is None: + prev_out = obj_output_dict["non_cond_frame_outputs"].get(frame_idx) + + if prev_out is not None and prev_out["pred_masks"] is not None: + prev_sam_mask_logits = prev_out["pred_masks"].cuda(non_blocking=True) + # Clamp the scale of prev_sam_mask_logits to avoid rare numerical issues. + prev_sam_mask_logits = torch.clamp(prev_sam_mask_logits, -32.0, 32.0) + current_out, _ = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=obj_output_dict, # run on the slice of a single object + frame_idx=frame_idx, + batch_size=1, # run on the slice of a single object + is_init_cond_frame=is_init_cond_frame, + point_inputs=point_inputs, + mask_inputs=None, + reverse=reverse, + # Skip the memory encoder when adding clicks or mask. We execute the memory encoder + # at the beginning of `propagate_in_video` (after user finalize their clicks). This + # allows us to enforce non-overlapping constraints on all objects before encoding + # them into memory. + run_mem_encoder=False, + prev_sam_mask_logits=prev_sam_mask_logits, + ) + # Add the output to the output dict (to be used as future memory) + obj_temp_output_dict[storage_key][frame_idx] = current_out + + # Resize the output mask to the original video resolution + obj_ids = inference_state["obj_ids"] + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=False, + consolidate_at_video_res=True, + ) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, consolidated_out["pred_masks_video_res"] + ) + return frame_idx, obj_ids, video_res_masks + + @torch.inference_mode() + def add_new_mask( + self, + inference_state, + frame_idx, + obj_id, + mask, + ): + """Add new mask to a frame.""" + obj_idx = self._obj_id_to_idx(inference_state, obj_id) + point_inputs_per_frame = inference_state["point_inputs_per_obj"][obj_idx] + mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx] + + if not isinstance(mask, torch.Tensor): + mask = torch.tensor(mask, dtype=torch.bool) + assert mask.dim() == 2 + mask_H, mask_W = mask.shape + mask_inputs_orig = mask[None, None] # add batch and channel dimension + mask_inputs_orig = mask_inputs_orig.float().to(inference_state["device"]) + + # resize the mask if it doesn't match the model's image size + if mask_H != self.image_size or mask_W != self.image_size: + mask_inputs = torch.nn.functional.interpolate( + mask_inputs_orig, + size=(self.image_size, self.image_size), + align_corners=False, + mode="bilinear", + antialias=True, # use antialias for downsampling + ) + mask_inputs = (mask_inputs >= 0.5).float() + else: + mask_inputs = mask_inputs_orig + + mask_inputs_per_frame[frame_idx] = mask_inputs + point_inputs_per_frame.pop(frame_idx, None) + # If this frame hasn't been tracked before, we treat it as an initial conditioning + # frame, meaning that the inputs points are to generate segments on this frame without + # using any memory from other frames, like in SAM. Otherwise (if it has been tracked), + # the input points will be used to correct the already tracked masks. + is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"] + # whether to track in reverse time order + if is_init_cond_frame: + reverse = False + else: + reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"] + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + # Add a frame to conditioning output if it's an initial conditioning frame or + # if the model sees all frames receiving clicks/mask as conditioning frames. + is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + + current_out, _ = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=obj_output_dict, # run on the slice of a single object + frame_idx=frame_idx, + batch_size=1, # run on the slice of a single object + is_init_cond_frame=is_init_cond_frame, + point_inputs=None, + mask_inputs=mask_inputs, + reverse=reverse, + # Skip the memory encoder when adding clicks or mask. We execute the memory encoder + # at the beginning of `propagate_in_video` (after user finalize their clicks). This + # allows us to enforce non-overlapping constraints on all objects before encoding + # them into memory. + run_mem_encoder=False, + ) + # Add the output to the output dict (to be used as future memory) + obj_temp_output_dict[storage_key][frame_idx] = current_out + + # Resize the output mask to the original video resolution + obj_ids = inference_state["obj_ids"] + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=False, + consolidate_at_video_res=True, + ) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, consolidated_out["pred_masks_video_res"] + ) + return frame_idx, obj_ids, video_res_masks + + + @torch.inference_mode() + def add_new_text( + self, + inference_state, + frame_idx, + obj_id, + text, + clear_old_points=True, + normalize_coords=True, + ): + """Add new text to a frame.""" + obj_idx = self._obj_id_to_idx(inference_state, obj_id) + point_inputs_per_frame = inference_state["point_inputs_per_obj"][obj_idx] + mask_inputs_per_frame = inference_state["mask_inputs_per_obj"][obj_idx] + + mask_inputs_per_frame.pop(frame_idx, None) + # If this frame hasn't been tracked before, we treat it as an initial conditioning + # frame, meaning that the inputs points are to generate segments on this frame without + # using any memory from other frames, like in SAM. Otherwise (if it has been tracked), + # the input points will be used to correct the already tracked masks. + is_init_cond_frame = frame_idx not in inference_state["frames_already_tracked"] + # whether to track in reverse time order + if is_init_cond_frame: + reverse = False + else: + reverse = inference_state["frames_already_tracked"][frame_idx]["reverse"] + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + # Add a frame to conditioning output if it's an initial conditioning frame or + # if the model sees all frames receiving clicks/mask as conditioning frames. + is_cond = is_init_cond_frame or self.add_all_frames_to_correct_as_cond + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + + # Get any previously predicted mask logits on this object and feed it along with + # the new clicks into the SAM mask decoder. + prev_sam_mask_logits = None + # lookup temporary output dict first, which contains the most recent output + # (if not found, then lookup conditioning and non-conditioning frame output) + prev_out = obj_temp_output_dict[storage_key].get(frame_idx) + if prev_out is None: + prev_out = obj_output_dict["cond_frame_outputs"].get(frame_idx) + if prev_out is None: + prev_out = obj_output_dict["non_cond_frame_outputs"].get(frame_idx) + + if prev_out is not None and prev_out["pred_masks"] is not None: + prev_sam_mask_logits = prev_out["pred_masks"].cuda(non_blocking=True) + # Clamp the scale of prev_sam_mask_logits to avoid rare numerical issues. + prev_sam_mask_logits = torch.clamp(prev_sam_mask_logits, -32.0, 32.0) + current_out, _ = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=obj_output_dict, # run on the slice of a single object + frame_idx=frame_idx, + batch_size=1, # run on the slice of a single object + is_init_cond_frame=is_init_cond_frame, + point_inputs=None, + mask_inputs=None, + reverse=reverse, + # Skip the memory encoder when adding clicks or mask. We execute the memory encoder + # at the beginning of `propagate_in_video` (after user finalize their clicks). This + # allows us to enforce non-overlapping constraints on all objects before encoding + # them into memory. + run_mem_encoder=False, + prev_sam_mask_logits=prev_sam_mask_logits, + text_inputs=text + ) + # Add the output to the output dict (to be used as future memory) + obj_temp_output_dict[storage_key][frame_idx] = current_out + + # Resize the output mask to the original video resolution + obj_ids = inference_state["obj_ids"] + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, + frame_idx, + is_cond=is_cond, + run_mem_encoder=False, + consolidate_at_video_res=True, + ) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, consolidated_out["pred_masks_video_res"] + ) + return frame_idx, obj_ids, video_res_masks + + + def _get_orig_video_res_output(self, inference_state, any_res_masks): + """ + Resize the object scores to the original video resolution (video_res_masks) + and apply non-overlapping constraints for final output. + """ + device = inference_state["device"] + video_H = inference_state["video_height"] + video_W = inference_state["video_width"] + any_res_masks = any_res_masks.to(device, non_blocking=True) + if any_res_masks.shape[-2:] == (video_H, video_W): + video_res_masks = any_res_masks + else: + video_res_masks = torch.nn.functional.interpolate( + any_res_masks, + size=(video_H, video_W), + mode="bilinear", + align_corners=False, + ) + if self.non_overlap_masks: + video_res_masks = self._apply_non_overlapping_constraints(video_res_masks) + return any_res_masks, video_res_masks + + def _consolidate_temp_output_across_obj( + self, + inference_state, + frame_idx, + is_cond, + run_mem_encoder, + consolidate_at_video_res=False, + ): + """ + Consolidate the per-object temporary outputs in `temp_output_dict_per_obj` on + a frame into a single output for all objects, including + 1) fill any missing objects either from `output_dict_per_obj` (if they exist in + `output_dict_per_obj` for this frame) or leave them as placeholder values + (if they don't exist in `output_dict_per_obj` for this frame); + 2) if specified, rerun memory encoder after apply non-overlapping constraints + on the object scores. + """ + batch_size = self._get_obj_num(inference_state) + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + # Optionally, we allow consolidating the temporary outputs at the original + # video resolution (to provide a better editing experience for mask prompts). + if consolidate_at_video_res: + assert not run_mem_encoder, "memory encoder cannot run at video resolution" + consolidated_H = inference_state["video_height"] + consolidated_W = inference_state["video_width"] + consolidated_mask_key = "pred_masks_video_res" + else: + consolidated_H = consolidated_W = self.image_size // 4 + consolidated_mask_key = "pred_masks" + + # Initialize `consolidated_out`. Its "maskmem_features" and "maskmem_pos_enc" + # will be added when rerunning the memory encoder after applying non-overlapping + # constraints to object scores. Its "pred_masks" are prefilled with a large + # negative value (NO_OBJ_SCORE) to represent missing objects. + consolidated_out = { + "maskmem_features": None, + "maskmem_pos_enc": None, + consolidated_mask_key: torch.full( + size=(batch_size, 1, consolidated_H, consolidated_W), + fill_value=NO_OBJ_SCORE, + dtype=torch.float32, + device=inference_state["storage_device"], + ), + "obj_ptr": torch.full( + size=(batch_size, self.hidden_dim), + fill_value=NO_OBJ_SCORE, + dtype=torch.float32, + device=inference_state["device"], + ), + } + empty_mask_ptr = None + for obj_idx in range(batch_size): + obj_temp_output_dict = inference_state["temp_output_dict_per_obj"][obj_idx] + obj_output_dict = inference_state["output_dict_per_obj"][obj_idx] + out = obj_temp_output_dict[storage_key].get(frame_idx, None) + # If the object doesn't appear in "temp_output_dict_per_obj" on this frame, + # we fall back and look up its previous output in "output_dict_per_obj". + # We look up both "cond_frame_outputs" and "non_cond_frame_outputs" in + # "output_dict_per_obj" to find a previous output for this object. + if out is None: + out = obj_output_dict["cond_frame_outputs"].get(frame_idx, None) + if out is None: + out = obj_output_dict["non_cond_frame_outputs"].get(frame_idx, None) + # If the object doesn't appear in "output_dict_per_obj" either, we skip it + # and leave its mask scores to the default scores (i.e. the NO_OBJ_SCORE + # placeholder above) and set its object pointer to be a dummy pointer. + if out is None: + # Fill in dummy object pointers for those objects without any inputs or + # tracking outcomes on this frame (only do it under `run_mem_encoder=True`, + # i.e. when we need to build the memory for tracking). + if run_mem_encoder: + if empty_mask_ptr is None: + empty_mask_ptr = self._get_empty_mask_ptr( + inference_state, frame_idx + ) + # fill object pointer with a dummy pointer (based on an empty mask) + consolidated_out["obj_ptr"][obj_idx : obj_idx + 1] = empty_mask_ptr + continue + # Add the temporary object output mask to consolidated output mask + obj_mask = out["pred_masks"] + consolidated_pred_masks = consolidated_out[consolidated_mask_key] + if obj_mask.shape[-2:] == consolidated_pred_masks.shape[-2:]: + consolidated_pred_masks[obj_idx : obj_idx + 1] = obj_mask + else: + # Resize first if temporary object mask has a different resolution + resized_obj_mask = torch.nn.functional.interpolate( + obj_mask, + size=consolidated_pred_masks.shape[-2:], + mode="bilinear", + align_corners=False, + ) + consolidated_pred_masks[obj_idx : obj_idx + 1] = resized_obj_mask + consolidated_out["obj_ptr"][obj_idx : obj_idx + 1] = out["obj_ptr"] + + # Optionally, apply non-overlapping constraints on the consolidated scores + # and rerun the memory encoder + if run_mem_encoder: + device = inference_state["device"] + high_res_masks = torch.nn.functional.interpolate( + consolidated_out["pred_masks"].to(device, non_blocking=True), + size=(self.image_size, self.image_size), + mode="bilinear", + align_corners=False, + ) + if self.non_overlap_masks_for_mem_enc: + high_res_masks = self._apply_non_overlapping_constraints(high_res_masks) + maskmem_features, maskmem_pos_enc = self._run_memory_encoder( + inference_state=inference_state, + frame_idx=frame_idx, + batch_size=batch_size, + high_res_masks=high_res_masks, + is_mask_from_pts=True, # these frames are what the user interacted with + ) + consolidated_out["maskmem_features"] = maskmem_features + consolidated_out["maskmem_pos_enc"] = maskmem_pos_enc + + return consolidated_out + + def _get_empty_mask_ptr(self, inference_state, frame_idx): + """Get a dummy object pointer based on an empty mask on the current frame.""" + # A dummy (empty) mask with a single object + batch_size = 1 + mask_inputs = torch.zeros( + (batch_size, 1, self.image_size, self.image_size), + dtype=torch.float32, + device=inference_state["device"], + ) + + # Retrieve correct image features + ( + _, + _, + current_vision_feats, + current_vision_pos_embeds, + feat_sizes, + ) = self._get_image_feature(inference_state, frame_idx, batch_size) + + # Feed the empty mask and image feature above to get a dummy object pointer + current_out = self.track_step( + frame_idx=frame_idx, + is_init_cond_frame=True, + current_vision_feats=current_vision_feats, + current_vision_pos_embeds=current_vision_pos_embeds, + feat_sizes=feat_sizes, + point_inputs=None, + mask_inputs=mask_inputs, + output_dict={}, + num_frames=inference_state["num_frames"], + track_in_reverse=False, + run_mem_encoder=False, + prev_sam_mask_logits=None, + ) + return current_out["obj_ptr"] + + @torch.inference_mode() + def propagate_in_video_preflight(self, inference_state): + """Prepare inference_state and consolidate temporary outputs before tracking.""" + # Tracking has started and we don't allow adding new objects until session is reset. + inference_state["tracking_has_started"] = True + batch_size = self._get_obj_num(inference_state) + + # Consolidate per-object temporary outputs in "temp_output_dict_per_obj" and + # add them into "output_dict". + temp_output_dict_per_obj = inference_state["temp_output_dict_per_obj"] + output_dict = inference_state["output_dict"] + # "consolidated_frame_inds" contains indices of those frames where consolidated + # temporary outputs have been added (either in this call or any previous calls + # to `propagate_in_video_preflight`). + consolidated_frame_inds = inference_state["consolidated_frame_inds"] + for is_cond in [False, True]: + # Separately consolidate conditioning and non-conditioning temp outptus + storage_key = "cond_frame_outputs" if is_cond else "non_cond_frame_outputs" + # Find all the frames that contain temporary outputs for any objects + # (these should be the frames that have just received clicks for mask inputs + # via `add_new_points` or `add_new_mask`) + temp_frame_inds = set() + for obj_temp_output_dict in temp_output_dict_per_obj.values(): + temp_frame_inds.update(obj_temp_output_dict[storage_key].keys()) + consolidated_frame_inds[storage_key].update(temp_frame_inds) + # consolidate the temprary output across all objects on this frame + for frame_idx in temp_frame_inds: + consolidated_out = self._consolidate_temp_output_across_obj( + inference_state, frame_idx, is_cond=is_cond, run_mem_encoder=True + ) + # merge them into "output_dict" and also create per-object slices + output_dict[storage_key][frame_idx] = consolidated_out + self._add_output_per_object( + inference_state, frame_idx, consolidated_out, storage_key + ) + clear_non_cond_mem = self.clear_non_cond_mem_around_input and ( + self.clear_non_cond_mem_for_multi_obj or batch_size <= 1 + ) + if clear_non_cond_mem: + # clear non-conditioning memory of the surrounding frames + self._clear_non_cond_mem_around_input(inference_state, frame_idx) + + # clear temporary outputs in `temp_output_dict_per_obj` + for obj_temp_output_dict in temp_output_dict_per_obj.values(): + obj_temp_output_dict[storage_key].clear() + + # edge case: if an output is added to "cond_frame_outputs", we remove any prior + # output on the same frame in "non_cond_frame_outputs" + for frame_idx in output_dict["cond_frame_outputs"]: + output_dict["non_cond_frame_outputs"].pop(frame_idx, None) + for obj_output_dict in inference_state["output_dict_per_obj"].values(): + for frame_idx in obj_output_dict["cond_frame_outputs"]: + obj_output_dict["non_cond_frame_outputs"].pop(frame_idx, None) + for frame_idx in consolidated_frame_inds["cond_frame_outputs"]: + assert frame_idx in output_dict["cond_frame_outputs"] + consolidated_frame_inds["non_cond_frame_outputs"].discard(frame_idx) + + # Make sure that the frame indices in "consolidated_frame_inds" are exactly those frames + # with either points or mask inputs (which should be true under a correct workflow). + # all_consolidated_frame_inds = ( + # consolidated_frame_inds["cond_frame_outputs"] + # | consolidated_frame_inds["non_cond_frame_outputs"] + # ) + # input_frames_inds = set() + # for point_inputs_per_frame in inference_state["point_inputs_per_obj"].values(): + # input_frames_inds.update(point_inputs_per_frame.keys()) + # for mask_inputs_per_frame in inference_state["mask_inputs_per_obj"].values(): + # input_frames_inds.update(mask_inputs_per_frame.keys()) + # assert all_consolidated_frame_inds == input_frames_inds + + @torch.inference_mode() + def propagate_in_video( + self, + inference_state, + start_frame_idx=None, + max_frame_num_to_track=None, + reverse=False, + ): + """Propagate the input points across frames to track in the entire video.""" + self.propagate_in_video_preflight(inference_state) + + output_dict = inference_state["output_dict"] + consolidated_frame_inds = inference_state["consolidated_frame_inds"] + obj_ids = inference_state["obj_ids"] + num_frames = inference_state["num_frames"] + batch_size = self._get_obj_num(inference_state) + if len(output_dict["cond_frame_outputs"]) == 0: + raise RuntimeError("No points are provided; please add points first") + clear_non_cond_mem = self.clear_non_cond_mem_around_input and ( + self.clear_non_cond_mem_for_multi_obj or batch_size <= 1 + ) + + # set start index, end index, and processing order + if start_frame_idx is None: + # default: start from the earliest frame with input points + start_frame_idx = min(output_dict["cond_frame_outputs"]) + if max_frame_num_to_track is None: + # default: track all the frames in the video + max_frame_num_to_track = num_frames + if reverse: + end_frame_idx = max(start_frame_idx - max_frame_num_to_track, 0) + if start_frame_idx > 0: + processing_order = range(start_frame_idx, end_frame_idx - 1, -1) + else: + processing_order = [] # skip reverse tracking if starting from frame 0 + else: + end_frame_idx = min( + start_frame_idx + max_frame_num_to_track, num_frames - 1 + ) + processing_order = range(start_frame_idx, end_frame_idx + 1) + + for frame_idx in tqdm(processing_order, desc="propagate in video"): + # We skip those frames already in consolidated outputs (these are frames + # that received input clicks or mask). Note that we cannot directly run + # batched forward on them via `_run_single_frame_inference` because the + # number of clicks on each object might be different. + if frame_idx in consolidated_frame_inds["cond_frame_outputs"]: + storage_key = "cond_frame_outputs" + current_out = output_dict[storage_key][frame_idx] + pred_masks = current_out["pred_masks"] + if clear_non_cond_mem: + # clear non-conditioning memory of the surrounding frames + self._clear_non_cond_mem_around_input(inference_state, frame_idx) + elif frame_idx in consolidated_frame_inds["non_cond_frame_outputs"]: + storage_key = "non_cond_frame_outputs" + current_out = output_dict[storage_key][frame_idx] + pred_masks = current_out["pred_masks"] + else: + storage_key = "non_cond_frame_outputs" + current_out, pred_masks = self._run_single_frame_inference( + inference_state=inference_state, + output_dict=output_dict, + frame_idx=frame_idx, + batch_size=batch_size, + is_init_cond_frame=False, + point_inputs=None, + mask_inputs=None, + reverse=reverse, + run_mem_encoder=True, + ) + output_dict[storage_key][frame_idx] = current_out + # Create slices of per-object outputs for subsequent interaction with each + # individual object after tracking. + self._add_output_per_object( + inference_state, frame_idx, current_out, storage_key + ) + inference_state["frames_already_tracked"][frame_idx] = {"reverse": reverse} + + # Resize the output mask to the original video resolution (we directly use + # the mask scores on GPU for output to avoid any CPU conversion in between) + _, video_res_masks = self._get_orig_video_res_output( + inference_state, pred_masks + ) + yield frame_idx, obj_ids, video_res_masks + + def _add_output_per_object( + self, inference_state, frame_idx, current_out, storage_key + ): + """ + Split a multi-object output into per-object output slices and add them into + `output_dict_per_obj`. The resulting slices share the same tensor storage. + """ + maskmem_features = current_out["maskmem_features"] + assert maskmem_features is None or isinstance(maskmem_features, torch.Tensor) + + maskmem_pos_enc = current_out["maskmem_pos_enc"] + assert maskmem_pos_enc is None or isinstance(maskmem_pos_enc, list) + + output_dict_per_obj = inference_state["output_dict_per_obj"] + for obj_idx, obj_output_dict in output_dict_per_obj.items(): + obj_slice = slice(obj_idx, obj_idx + 1) + obj_out = { + "maskmem_features": None, + "maskmem_pos_enc": None, + "pred_masks": current_out["pred_masks"][obj_slice], + "obj_ptr": current_out["obj_ptr"][obj_slice], + } + if maskmem_features is not None: + obj_out["maskmem_features"] = maskmem_features[obj_slice] + if maskmem_pos_enc is not None: + obj_out["maskmem_pos_enc"] = [x[obj_slice] for x in maskmem_pos_enc] + obj_output_dict[storage_key][frame_idx] = obj_out + + @torch.inference_mode() + def reset_state(self, inference_state): + """Remove all input points or mask in all frames throughout the video.""" + self._reset_tracking_results(inference_state) + # Remove all object ids + inference_state["obj_id_to_idx"].clear() + inference_state["obj_idx_to_id"].clear() + inference_state["obj_ids"].clear() + inference_state["point_inputs_per_obj"].clear() + inference_state["mask_inputs_per_obj"].clear() + inference_state["output_dict_per_obj"].clear() + inference_state["temp_output_dict_per_obj"].clear() + + def _reset_tracking_results(self, inference_state): + """Reset all tracking inputs and results across the videos.""" + for v in inference_state["point_inputs_per_obj"].values(): + v.clear() + for v in inference_state["mask_inputs_per_obj"].values(): + v.clear() + for v in inference_state["output_dict_per_obj"].values(): + v["cond_frame_outputs"].clear() + v["non_cond_frame_outputs"].clear() + for v in inference_state["temp_output_dict_per_obj"].values(): + v["cond_frame_outputs"].clear() + v["non_cond_frame_outputs"].clear() + inference_state["output_dict"]["cond_frame_outputs"].clear() + inference_state["output_dict"]["non_cond_frame_outputs"].clear() + inference_state["consolidated_frame_inds"]["cond_frame_outputs"].clear() + inference_state["consolidated_frame_inds"]["non_cond_frame_outputs"].clear() + inference_state["tracking_has_started"] = False + inference_state["frames_already_tracked"].clear() + + def _get_image_feature(self, inference_state, frame_idx, batch_size): + """Compute the image features on a given frame.""" + # Look up in the cache first + image, backbone_out = inference_state["cached_features"].get( + frame_idx, (None, None) + ) + if backbone_out is None: + # Cache miss -- we will run inference on a single image + image = inference_state["images"][frame_idx].cuda().float().unsqueeze(0) + backbone_out = self.forward_image(image) + # Cache the most recent frame's feature (for repeated interactions with + # a frame; we can use an LRU cache for more frames in the future). + inference_state["cached_features"] = {frame_idx: (image, backbone_out)} + + # expand the features to have the same dimension as the number of objects + expanded_image = image.expand(batch_size, -1, -1, -1) + expanded_backbone_out = { + "backbone_fpn": backbone_out["backbone_fpn"].copy(), + "vision_pos_enc": backbone_out["vision_pos_enc"].copy(), + } + for i, feat in enumerate(expanded_backbone_out["backbone_fpn"]): + expanded_backbone_out["backbone_fpn"][i] = feat.expand( + batch_size, -1, -1, -1 + ) + for i, pos in enumerate(expanded_backbone_out["vision_pos_enc"]): + pos = pos.expand(batch_size, -1, -1, -1) + expanded_backbone_out["vision_pos_enc"][i] = pos + + features = self._prepare_backbone_features(expanded_backbone_out) + features = (expanded_image,) + features + return features + + def _run_single_frame_inference( + self, + inference_state, + output_dict, + frame_idx, + batch_size, + is_init_cond_frame, + point_inputs, + mask_inputs, + reverse, + run_mem_encoder, + prev_sam_mask_logits=None, + text_inputs=None + ): + """Run tracking on a single frame based on current inputs and previous memory.""" + # Retrieve correct image features + ( + _, + _, + current_vision_feats, + current_vision_pos_embeds, + feat_sizes, + ) = self._get_image_feature(inference_state, frame_idx, batch_size) + + # point and mask should not appear as input simultaneously on the same frame + assert point_inputs is None or mask_inputs is None + current_out = self.track_step( + frame_idx=frame_idx, + is_init_cond_frame=is_init_cond_frame, + current_vision_feats=current_vision_feats, + current_vision_pos_embeds=current_vision_pos_embeds, + feat_sizes=feat_sizes, + point_inputs=point_inputs, + mask_inputs=mask_inputs, + output_dict=output_dict, + num_frames=inference_state["num_frames"], + track_in_reverse=reverse, + run_mem_encoder=run_mem_encoder, + prev_sam_mask_logits=prev_sam_mask_logits, + text_inputs=text_inputs + ) + + # optionally offload the output to CPU memory to save GPU space + storage_device = inference_state["storage_device"] + maskmem_features = current_out["maskmem_features"] + if maskmem_features is not None: + maskmem_features = maskmem_features.to(torch.bfloat16) + maskmem_features = maskmem_features.to(storage_device, non_blocking=True) + pred_masks_gpu = current_out["pred_masks"] + # potentially fill holes in the predicted masks + if self.fill_hole_area > 0: + pred_masks_gpu = fill_holes_in_mask_scores( + pred_masks_gpu, self.fill_hole_area + ) + pred_masks = pred_masks_gpu.to(storage_device, non_blocking=True) + # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it + maskmem_pos_enc = self._get_maskmem_pos_enc(inference_state, current_out) + # object pointer is a small tensor, so we always keep it on GPU memory for fast access + obj_ptr = current_out["obj_ptr"] + # make a compact version of this frame's output to reduce the state size + compact_current_out = { + "maskmem_features": maskmem_features, + "maskmem_pos_enc": maskmem_pos_enc, + "pred_masks": pred_masks, + "obj_ptr": obj_ptr, + } + return compact_current_out, pred_masks_gpu + + def _run_memory_encoder( + self, inference_state, frame_idx, batch_size, high_res_masks, is_mask_from_pts + ): + """ + Run the memory encoder on `high_res_masks`. This is usually after applying + non-overlapping constraints to object scores. Since their scores changed, their + memory also need to be computed again with the memory encoder. + """ + # Retrieve correct image features + _, _, current_vision_feats, _, feat_sizes = self._get_image_feature( + inference_state, frame_idx, batch_size + ) + maskmem_features, maskmem_pos_enc = self._encode_new_memory( + current_vision_feats=current_vision_feats, + feat_sizes=feat_sizes, + pred_masks_high_res=high_res_masks, + is_mask_from_pts=is_mask_from_pts, + ) + + # optionally offload the output to CPU memory to save GPU space + storage_device = inference_state["storage_device"] + maskmem_features = maskmem_features.to(torch.bfloat16) + maskmem_features = maskmem_features.to(storage_device, non_blocking=True) + # "maskmem_pos_enc" is the same across frames, so we only need to store one copy of it + maskmem_pos_enc = self._get_maskmem_pos_enc( + inference_state, {"maskmem_pos_enc": maskmem_pos_enc} + ) + return maskmem_features, maskmem_pos_enc + + def _get_maskmem_pos_enc(self, inference_state, current_out): + """ + `maskmem_pos_enc` is the same across frames and objects, so we cache it as + a constant in the inference session to reduce session storage size. + """ + model_constants = inference_state["constants"] + # "out_maskmem_pos_enc" should be either a list of tensors or None + out_maskmem_pos_enc = current_out["maskmem_pos_enc"] + if out_maskmem_pos_enc is not None: + if "maskmem_pos_enc" not in model_constants: + assert isinstance(out_maskmem_pos_enc, list) + # only take the slice for one object, since it's same across objects + maskmem_pos_enc = [x[0:1].clone() for x in out_maskmem_pos_enc] + model_constants["maskmem_pos_enc"] = maskmem_pos_enc + else: + maskmem_pos_enc = model_constants["maskmem_pos_enc"] + # expand the cached maskmem_pos_enc to the actual batch size + batch_size = out_maskmem_pos_enc[0].size(0) + expanded_maskmem_pos_enc = [ + x.expand(batch_size, -1, -1, -1) for x in maskmem_pos_enc + ] + else: + expanded_maskmem_pos_enc = None + return expanded_maskmem_pos_enc + + def _clear_non_cond_mem_around_input(self, inference_state, frame_idx): + """ + Remove the non-conditioning memory around the input frame. When users provide + correction clicks, the surrounding frames' non-conditioning memories can still + contain outdated object appearance information and could confuse the model. + + This method clears those non-conditioning memories surrounding the interacted + frame to avoid giving the model both old and new information about the object. + """ + r = self.memory_temporal_stride_for_eval + frame_idx_begin = frame_idx - r * self.num_maskmem + frame_idx_end = frame_idx + r * self.num_maskmem + output_dict = inference_state["output_dict"] + non_cond_frame_outputs = output_dict["non_cond_frame_outputs"] + for t in range(frame_idx_begin, frame_idx_end + 1): + non_cond_frame_outputs.pop(t, None) + for obj_output_dict in inference_state["output_dict_per_obj"].values(): + obj_output_dict["non_cond_frame_outputs"].pop(t, None) diff --git a/py/evf-sam/model/segment_anything_2/sam2/utils/__init__.py b/py/evf-sam/model/segment_anything_2/sam2/utils/__init__.py new file mode 100644 index 0000000..5277f46 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/utils/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. diff --git a/py/evf-sam/model/segment_anything_2/sam2/utils/amg.py b/py/evf-sam/model/segment_anything_2/sam2/utils/amg.py new file mode 100644 index 0000000..9868429 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/utils/amg.py @@ -0,0 +1,348 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import math +from copy import deepcopy +from itertools import product +from typing import Any, Dict, Generator, ItemsView, List, Tuple + +import numpy as np +import torch + +# Very lightly adapted from https://github.com/facebookresearch/segment-anything/blob/main/segment_anything/utils/amg.py + + +class MaskData: + """ + A structure for storing masks and their related data in batched format. + Implements basic filtering and concatenation. + """ + + def __init__(self, **kwargs) -> None: + for v in kwargs.values(): + assert isinstance( + v, (list, np.ndarray, torch.Tensor) + ), "MaskData only supports list, numpy arrays, and torch tensors." + self._stats = dict(**kwargs) + + def __setitem__(self, key: str, item: Any) -> None: + assert isinstance( + item, (list, np.ndarray, torch.Tensor) + ), "MaskData only supports list, numpy arrays, and torch tensors." + self._stats[key] = item + + def __delitem__(self, key: str) -> None: + del self._stats[key] + + def __getitem__(self, key: str) -> Any: + return self._stats[key] + + def items(self) -> ItemsView[str, Any]: + return self._stats.items() + + def filter(self, keep: torch.Tensor) -> None: + for k, v in self._stats.items(): + if v is None: + self._stats[k] = None + elif isinstance(v, torch.Tensor): + self._stats[k] = v[torch.as_tensor(keep, device=v.device)] + elif isinstance(v, np.ndarray): + self._stats[k] = v[keep.detach().cpu().numpy()] + elif isinstance(v, list) and keep.dtype == torch.bool: + self._stats[k] = [a for i, a in enumerate(v) if keep[i]] + elif isinstance(v, list): + self._stats[k] = [v[i] for i in keep] + else: + raise TypeError(f"MaskData key {k} has an unsupported type {type(v)}.") + + def cat(self, new_stats: "MaskData") -> None: + for k, v in new_stats.items(): + if k not in self._stats or self._stats[k] is None: + self._stats[k] = deepcopy(v) + elif isinstance(v, torch.Tensor): + self._stats[k] = torch.cat([self._stats[k], v], dim=0) + elif isinstance(v, np.ndarray): + self._stats[k] = np.concatenate([self._stats[k], v], axis=0) + elif isinstance(v, list): + self._stats[k] = self._stats[k] + deepcopy(v) + else: + raise TypeError(f"MaskData key {k} has an unsupported type {type(v)}.") + + def to_numpy(self) -> None: + for k, v in self._stats.items(): + if isinstance(v, torch.Tensor): + self._stats[k] = v.float().detach().cpu().numpy() + + +def is_box_near_crop_edge( + boxes: torch.Tensor, crop_box: List[int], orig_box: List[int], atol: float = 20.0 +) -> torch.Tensor: + """Filter masks at the edge of a crop, but not at the edge of the original image.""" + crop_box_torch = torch.as_tensor(crop_box, dtype=torch.float, device=boxes.device) + orig_box_torch = torch.as_tensor(orig_box, dtype=torch.float, device=boxes.device) + boxes = uncrop_boxes_xyxy(boxes, crop_box).float() + near_crop_edge = torch.isclose(boxes, crop_box_torch[None, :], atol=atol, rtol=0) + near_image_edge = torch.isclose(boxes, orig_box_torch[None, :], atol=atol, rtol=0) + near_crop_edge = torch.logical_and(near_crop_edge, ~near_image_edge) + return torch.any(near_crop_edge, dim=1) + + +def box_xyxy_to_xywh(box_xyxy: torch.Tensor) -> torch.Tensor: + box_xywh = deepcopy(box_xyxy) + box_xywh[2] = box_xywh[2] - box_xywh[0] + box_xywh[3] = box_xywh[3] - box_xywh[1] + return box_xywh + + +def batch_iterator(batch_size: int, *args) -> Generator[List[Any], None, None]: + assert len(args) > 0 and all( + len(a) == len(args[0]) for a in args + ), "Batched iteration must have inputs of all the same size." + n_batches = len(args[0]) // batch_size + int(len(args[0]) % batch_size != 0) + for b in range(n_batches): + yield [arg[b * batch_size : (b + 1) * batch_size] for arg in args] + + +def mask_to_rle_pytorch(tensor: torch.Tensor) -> List[Dict[str, Any]]: + """ + Encodes masks to an uncompressed RLE, in the format expected by + pycoco tools. + """ + # Put in fortran order and flatten h,w + b, h, w = tensor.shape + tensor = tensor.permute(0, 2, 1).flatten(1) + + # Compute change indices + diff = tensor[:, 1:] ^ tensor[:, :-1] + change_indices = diff.nonzero() + + # Encode run length + out = [] + for i in range(b): + cur_idxs = change_indices[change_indices[:, 0] == i, 1] + cur_idxs = torch.cat( + [ + torch.tensor([0], dtype=cur_idxs.dtype, device=cur_idxs.device), + cur_idxs + 1, + torch.tensor([h * w], dtype=cur_idxs.dtype, device=cur_idxs.device), + ] + ) + btw_idxs = cur_idxs[1:] - cur_idxs[:-1] + counts = [] if tensor[i, 0] == 0 else [0] + counts.extend(btw_idxs.detach().cpu().tolist()) + out.append({"size": [h, w], "counts": counts}) + return out + + +def rle_to_mask(rle: Dict[str, Any]) -> np.ndarray: + """Compute a binary mask from an uncompressed RLE.""" + h, w = rle["size"] + mask = np.empty(h * w, dtype=bool) + idx = 0 + parity = False + for count in rle["counts"]: + mask[idx : idx + count] = parity + idx += count + parity ^= True + mask = mask.reshape(w, h) + return mask.transpose() # Put in C order + + +def area_from_rle(rle: Dict[str, Any]) -> int: + return sum(rle["counts"][1::2]) + + +def calculate_stability_score( + masks: torch.Tensor, mask_threshold: float, threshold_offset: float +) -> torch.Tensor: + """ + Computes the stability score for a batch of masks. The stability + score is the IoU between the binary masks obtained by thresholding + the predicted mask logits at high and low values. + """ + # One mask is always contained inside the other. + # Save memory by preventing unnecessary cast to torch.int64 + intersections = ( + (masks > (mask_threshold + threshold_offset)) + .sum(-1, dtype=torch.int16) + .sum(-1, dtype=torch.int32) + ) + unions = ( + (masks > (mask_threshold - threshold_offset)) + .sum(-1, dtype=torch.int16) + .sum(-1, dtype=torch.int32) + ) + return intersections / unions + + +def build_point_grid(n_per_side: int) -> np.ndarray: + """Generates a 2D grid of points evenly spaced in [0,1]x[0,1].""" + offset = 1 / (2 * n_per_side) + points_one_side = np.linspace(offset, 1 - offset, n_per_side) + points_x = np.tile(points_one_side[None, :], (n_per_side, 1)) + points_y = np.tile(points_one_side[:, None], (1, n_per_side)) + points = np.stack([points_x, points_y], axis=-1).reshape(-1, 2) + return points + + +def build_all_layer_point_grids( + n_per_side: int, n_layers: int, scale_per_layer: int +) -> List[np.ndarray]: + """Generates point grids for all crop layers.""" + points_by_layer = [] + for i in range(n_layers + 1): + n_points = int(n_per_side / (scale_per_layer**i)) + points_by_layer.append(build_point_grid(n_points)) + return points_by_layer + + +def generate_crop_boxes( + im_size: Tuple[int, ...], n_layers: int, overlap_ratio: float +) -> Tuple[List[List[int]], List[int]]: + """ + Generates a list of crop boxes of different sizes. Each layer + has (2**i)**2 boxes for the ith layer. + """ + crop_boxes, layer_idxs = [], [] + im_h, im_w = im_size + short_side = min(im_h, im_w) + + # Original image + crop_boxes.append([0, 0, im_w, im_h]) + layer_idxs.append(0) + + def crop_len(orig_len, n_crops, overlap): + return int(math.ceil((overlap * (n_crops - 1) + orig_len) / n_crops)) + + for i_layer in range(n_layers): + n_crops_per_side = 2 ** (i_layer + 1) + overlap = int(overlap_ratio * short_side * (2 / n_crops_per_side)) + + crop_w = crop_len(im_w, n_crops_per_side, overlap) + crop_h = crop_len(im_h, n_crops_per_side, overlap) + + crop_box_x0 = [int((crop_w - overlap) * i) for i in range(n_crops_per_side)] + crop_box_y0 = [int((crop_h - overlap) * i) for i in range(n_crops_per_side)] + + # Crops in XYWH format + for x0, y0 in product(crop_box_x0, crop_box_y0): + box = [x0, y0, min(x0 + crop_w, im_w), min(y0 + crop_h, im_h)] + crop_boxes.append(box) + layer_idxs.append(i_layer + 1) + + return crop_boxes, layer_idxs + + +def uncrop_boxes_xyxy(boxes: torch.Tensor, crop_box: List[int]) -> torch.Tensor: + x0, y0, _, _ = crop_box + offset = torch.tensor([[x0, y0, x0, y0]], device=boxes.device) + # Check if boxes has a channel dimension + if len(boxes.shape) == 3: + offset = offset.unsqueeze(1) + return boxes + offset + + +def uncrop_points(points: torch.Tensor, crop_box: List[int]) -> torch.Tensor: + x0, y0, _, _ = crop_box + offset = torch.tensor([[x0, y0]], device=points.device) + # Check if points has a channel dimension + if len(points.shape) == 3: + offset = offset.unsqueeze(1) + return points + offset + + +def uncrop_masks( + masks: torch.Tensor, crop_box: List[int], orig_h: int, orig_w: int +) -> torch.Tensor: + x0, y0, x1, y1 = crop_box + if x0 == 0 and y0 == 0 and x1 == orig_w and y1 == orig_h: + return masks + # Coordinate transform masks + pad_x, pad_y = orig_w - (x1 - x0), orig_h - (y1 - y0) + pad = (x0, pad_x - x0, y0, pad_y - y0) + return torch.nn.functional.pad(masks, pad, value=0) + + +def remove_small_regions( + mask: np.ndarray, area_thresh: float, mode: str +) -> Tuple[np.ndarray, bool]: + """ + Removes small disconnected regions and holes in a mask. Returns the + mask and an indicator of if the mask has been modified. + """ + import cv2 # type: ignore + + assert mode in ["holes", "islands"] + correct_holes = mode == "holes" + working_mask = (correct_holes ^ mask).astype(np.uint8) + n_labels, regions, stats, _ = cv2.connectedComponentsWithStats(working_mask, 8) + sizes = stats[:, -1][1:] # Row 0 is background label + small_regions = [i + 1 for i, s in enumerate(sizes) if s < area_thresh] + if len(small_regions) == 0: + return mask, False + fill_labels = [0] + small_regions + if not correct_holes: + fill_labels = [i for i in range(n_labels) if i not in fill_labels] + # If every region is below threshold, keep largest + if len(fill_labels) == 0: + fill_labels = [int(np.argmax(sizes)) + 1] + mask = np.isin(regions, fill_labels) + return mask, True + + +def coco_encode_rle(uncompressed_rle: Dict[str, Any]) -> Dict[str, Any]: + from pycocotools import mask as mask_utils # type: ignore + + h, w = uncompressed_rle["size"] + rle = mask_utils.frPyObjects(uncompressed_rle, h, w) + rle["counts"] = rle["counts"].decode("utf-8") # Necessary to serialize with json + return rle + + +def batched_mask_to_box(masks: torch.Tensor) -> torch.Tensor: + """ + Calculates boxes in XYXY format around masks. Return [0,0,0,0] for + an empty mask. For input shape C1xC2x...xHxW, the output shape is C1xC2x...x4. + """ + # torch.max below raises an error on empty inputs, just skip in this case + if torch.numel(masks) == 0: + return torch.zeros(*masks.shape[:-2], 4, device=masks.device) + + # Normalize shape to CxHxW + shape = masks.shape + h, w = shape[-2:] + if len(shape) > 2: + masks = masks.flatten(0, -3) + else: + masks = masks.unsqueeze(0) + + # Get top and bottom edges + in_height, _ = torch.max(masks, dim=-1) + in_height_coords = in_height * torch.arange(h, device=in_height.device)[None, :] + bottom_edges, _ = torch.max(in_height_coords, dim=-1) + in_height_coords = in_height_coords + h * (~in_height) + top_edges, _ = torch.min(in_height_coords, dim=-1) + + # Get left and right edges + in_width, _ = torch.max(masks, dim=-2) + in_width_coords = in_width * torch.arange(w, device=in_width.device)[None, :] + right_edges, _ = torch.max(in_width_coords, dim=-1) + in_width_coords = in_width_coords + w * (~in_width) + left_edges, _ = torch.min(in_width_coords, dim=-1) + + # If the mask is empty the right edge will be to the left of the left edge. + # Replace these boxes with [0, 0, 0, 0] + empty_filter = (right_edges < left_edges) | (bottom_edges < top_edges) + out = torch.stack([left_edges, top_edges, right_edges, bottom_edges], dim=-1) + out = out * (~empty_filter).unsqueeze(-1) + + # Return to original shape + if len(shape) > 2: + out = out.reshape(*shape[:-2], 4) + else: + out = out[0] + + return out diff --git a/py/evf-sam/model/segment_anything_2/sam2/utils/misc.py b/py/evf-sam/model/segment_anything_2/sam2/utils/misc.py new file mode 100644 index 0000000..9adf6e4 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/utils/misc.py @@ -0,0 +1,238 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import os +import warnings +from threading import Thread + +import numpy as np +import torch +from PIL import Image +from tqdm import tqdm + + +def get_sdpa_settings(): + if torch.cuda.is_available(): + old_gpu = torch.cuda.get_device_properties(0).major < 7 + # only use Flash Attention on Ampere (8.0) or newer GPUs + use_flash_attn = torch.cuda.get_device_properties(0).major >= 8 + if not use_flash_attn: + warnings.warn( + "Flash Attention is disabled as it requires a GPU with Ampere (8.0) CUDA capability.", + category=UserWarning, + stacklevel=2, + ) + # keep math kernel for PyTorch versions before 2.2 (Flash Attention v2 is only + # available on PyTorch 2.2+, while Flash Attention v1 cannot handle all cases) + pytorch_version = tuple(int(v) for v in torch.__version__.split(".")[:2]) + if pytorch_version < (2, 2): + warnings.warn( + f"You are using PyTorch {torch.__version__} without Flash Attention v2 support. " + "Consider upgrading to PyTorch 2.2+ for Flash Attention v2 (which could be faster).", + category=UserWarning, + stacklevel=2, + ) + math_kernel_on = pytorch_version < (2, 2) or not use_flash_attn + else: + old_gpu = True + use_flash_attn = False + math_kernel_on = True + + return old_gpu, use_flash_attn, math_kernel_on + + +def get_connected_components(mask): + """ + Get the connected components (8-connectivity) of binary masks of shape (N, 1, H, W). + + Inputs: + - mask: A binary mask tensor of shape (N, 1, H, W), where 1 is foreground and 0 is + background. + + Outputs: + - labels: A tensor of shape (N, 1, H, W) containing the connected component labels + for foreground pixels and 0 for background pixels. + - counts: A tensor of shape (N, 1, H, W) containing the area of the connected + components for foreground pixels and 0 for background pixels. + """ + from model.segment_anything_2.sam2 import _C + + return _C.get_connected_componnets(mask.to(torch.uint8).contiguous()) + + +def mask_to_box(masks: torch.Tensor): + """ + compute bounding box given an input mask + + Inputs: + - masks: [B, 1, H, W] boxes, dtype=torch.Tensor + + Returns: + - box_coords: [B, 1, 4], contains (x, y) coordinates of top left and bottom right box corners, dtype=torch.Tensor + """ + B, _, h, w = masks.shape + device = masks.device + xs = torch.arange(w, device=device, dtype=torch.int32) + ys = torch.arange(h, device=device, dtype=torch.int32) + grid_xs, grid_ys = torch.meshgrid(xs, ys, indexing="xy") + grid_xs = grid_xs[None, None, ...].expand(B, 1, h, w) + grid_ys = grid_ys[None, None, ...].expand(B, 1, h, w) + min_xs, _ = torch.min(torch.where(masks, grid_xs, w).flatten(-2), dim=-1) + max_xs, _ = torch.max(torch.where(masks, grid_xs, -1).flatten(-2), dim=-1) + min_ys, _ = torch.min(torch.where(masks, grid_ys, h).flatten(-2), dim=-1) + max_ys, _ = torch.max(torch.where(masks, grid_ys, -1).flatten(-2), dim=-1) + bbox_coords = torch.stack((min_xs, min_ys, max_xs, max_ys), dim=-1) + + return bbox_coords + + +def _load_img_as_tensor(img_path, image_size): + img_pil = Image.open(img_path) + img_np = np.array(img_pil.convert("RGB").resize((image_size, image_size))) + if img_np.dtype == np.uint8: # np.uint8 is expected for JPEG images + img_np = img_np / 255.0 + else: + raise RuntimeError(f"Unknown image dtype: {img_np.dtype} on {img_path}") + img = torch.from_numpy(img_np).permute(2, 0, 1) + video_width, video_height = img_pil.size # the original video size + return img, video_height, video_width + + +class AsyncVideoFrameLoader: + """ + A list of video frames to be load asynchronously without blocking session start. + """ + + def __init__(self, img_paths, image_size, offload_video_to_cpu, img_mean, img_std): + self.img_paths = img_paths + self.image_size = image_size + self.offload_video_to_cpu = offload_video_to_cpu + self.img_mean = img_mean + self.img_std = img_std + # items in `self._images` will be loaded asynchronously + self.images = [None] * len(img_paths) + # catch and raise any exceptions in the async loading thread + self.exception = None + # video_height and video_width be filled when loading the first image + self.video_height = None + self.video_width = None + + # load the first frame to fill video_height and video_width and also + # to cache it (since it's most likely where the user will click) + self.__getitem__(0) + + # load the rest of frames asynchronously without blocking the session start + def _load_frames(): + try: + for n in tqdm(range(len(self.images)), desc="frame loading (JPEG)"): + self.__getitem__(n) + except Exception as e: + self.exception = e + + self.thread = Thread(target=_load_frames, daemon=True) + self.thread.start() + + def __getitem__(self, index): + if self.exception is not None: + raise RuntimeError("Failure in frame loading thread") from self.exception + + img = self.images[index] + if img is not None: + return img + + img, video_height, video_width = _load_img_as_tensor( + self.img_paths[index], self.image_size + ) + self.video_height = video_height + self.video_width = video_width + # normalize by mean and std + img -= self.img_mean + img /= self.img_std + if not self.offload_video_to_cpu: + img = img.cuda(non_blocking=True) + self.images[index] = img + return img + + def __len__(self): + return len(self.images) + + +def load_video_frames( + video_path, + image_size, + offload_video_to_cpu, + img_mean=(0.485, 0.456, 0.406), + img_std=(0.229, 0.224, 0.225), + async_loading_frames=False, +): + """ + Load the video frames from a directory of JPEG files (".jpg" format). + + The frames are resized to image_size x image_size and are loaded to GPU if + `offload_video_to_cpu` is `False` and to CPU if `offload_video_to_cpu` is `True`. + + You can load a frame asynchronously by setting `async_loading_frames` to `True`. + """ + if isinstance(video_path, str) and os.path.isdir(video_path): + jpg_folder = video_path + else: + raise NotImplementedError("Only JPEG frames are supported at this moment") + + frame_names = [ + p + for p in os.listdir(jpg_folder) + if os.path.splitext(p)[-1] in [".jpg", ".jpeg", ".JPG", ".JPEG"] + ] + frame_names.sort(key=lambda p: int(os.path.splitext(p)[0])) + num_frames = len(frame_names) + if num_frames == 0: + raise RuntimeError(f"no images found in {jpg_folder}") + img_paths = [os.path.join(jpg_folder, frame_name) for frame_name in frame_names] + img_mean = torch.tensor(img_mean, dtype=torch.float32)[:, None, None] + img_std = torch.tensor(img_std, dtype=torch.float32)[:, None, None] + + if async_loading_frames: + lazy_images = AsyncVideoFrameLoader( + img_paths, image_size, offload_video_to_cpu, img_mean, img_std + ) + return lazy_images, lazy_images.video_height, lazy_images.video_width + + images = torch.zeros(num_frames, 3, image_size, image_size, dtype=torch.float32) + for n, img_path in enumerate(tqdm(img_paths, desc="frame loading (JPEG)")): + images[n], video_height, video_width = _load_img_as_tensor(img_path, image_size) + if not offload_video_to_cpu: + images = images.cuda() + img_mean = img_mean.cuda() + img_std = img_std.cuda() + # normalize by mean and std + images -= img_mean + images /= img_std + return images, video_height, video_width + + +def fill_holes_in_mask_scores(mask, max_area): + """ + A post processor to fill small holes in mask scores with area under `max_area`. + """ + # Holes are those connected components in background with area <= self.max_area + # (background regions are those with mask scores <= 0) + assert max_area > 0, "max_area must be positive" + labels, areas = get_connected_components(mask <= 0) + is_hole = (labels > 0) & (areas <= max_area) + # We fill holes with a small positive mask score (0.1) to change them to foreground. + mask = torch.where(is_hole, 0.1, mask) + return mask + + +def concat_points(old_point_inputs, new_points, new_labels): + """Add new points and labels to previous point inputs (add at the end).""" + if old_point_inputs is None: + points, labels = new_points, new_labels + else: + points = torch.cat([old_point_inputs["point_coords"], new_points], dim=1) + labels = torch.cat([old_point_inputs["point_labels"], new_labels], dim=1) + + return {"point_coords": points, "point_labels": labels} diff --git a/py/evf-sam/model/segment_anything_2/sam2/utils/transforms.py b/py/evf-sam/model/segment_anything_2/sam2/utils/transforms.py new file mode 100644 index 0000000..082a351 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2/utils/transforms.py @@ -0,0 +1,99 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision.transforms import Normalize, Resize, ToTensor + + +class SAM2Transforms(nn.Module): + def __init__( + self, resolution, mask_threshold, max_hole_area=0.0, max_sprinkle_area=0.0 + ): + """ + Transforms for SAM2. + """ + super().__init__() + self.resolution = resolution + self.mask_threshold = mask_threshold + self.max_hole_area = max_hole_area + self.max_sprinkle_area = max_sprinkle_area + self.mean = [0.485, 0.456, 0.406] + self.std = [0.229, 0.224, 0.225] + self.to_tensor = ToTensor() + self.transforms = torch.jit.script( + nn.Sequential( + Resize((self.resolution, self.resolution)), + Normalize(self.mean, self.std), + ) + ) + + def __call__(self, x): + x = self.to_tensor(x) + return self.transforms(x) + + def forward_batch(self, img_list): + img_batch = [self.transforms(self.to_tensor(img)) for img in img_list] + img_batch = torch.stack(img_batch, dim=0) + return img_batch + + def transform_coords( + self, coords: torch.Tensor, normalize=False, orig_hw=None + ) -> torch.Tensor: + """ + Expects a torch tensor with length 2 in the last dimension. The coordinates can be in absolute image or normalized coordinates, + If the coords are in absolute image coordinates, normalize should be set to True and original image size is required. + + Returns + Un-normalized coordinates in the range of [0, 1] which is expected by the SAM2 model. + """ + if normalize: + assert orig_hw is not None + h, w = orig_hw + coords = coords.clone() + coords[..., 0] = coords[..., 0] / w + coords[..., 1] = coords[..., 1] / h + + coords = coords * self.resolution # unnormalize coords + return coords + + def transform_boxes( + self, boxes: torch.Tensor, normalize=False, orig_hw=None + ) -> torch.Tensor: + """ + Expects a tensor of shape Bx4. The coordinates can be in absolute image or normalized coordinates, + if the coords are in absolute image coordinates, normalize should be set to True and original image size is required. + """ + boxes = self.transform_coords(boxes.reshape(-1, 2, 2), normalize, orig_hw) + return boxes + + def postprocess_masks(self, masks: torch.Tensor, orig_hw) -> torch.Tensor: + """ + Perform PostProcessing on output masks. + """ + from model.segment_anything_2.sam2.utils.misc import get_connected_components + + masks = masks.float() + if self.max_hole_area > 0: + # Holes are those connected components in background with area <= self.fill_hole_area + # (background regions are those with mask scores <= self.mask_threshold) + mask_flat = masks.flatten(0, 1).unsqueeze(1) # flatten as 1-channel image + labels, areas = get_connected_components(mask_flat <= self.mask_threshold) + is_hole = (labels > 0) & (areas <= self.max_hole_area) + is_hole = is_hole.reshape_as(masks) + # We fill holes with a small positive mask score (10.0) to change them to foreground. + masks = torch.where(is_hole, self.mask_threshold + 10.0, masks) + + if self.max_sprinkle_area > 0: + labels, areas = get_connected_components(mask_flat > self.mask_threshold) + is_hole = (labels > 0) & (areas <= self.max_sprinkle_area) + is_hole = is_hole.reshape_as(masks) + # We fill holes with negative mask score (-10.0) to change them to background. + masks = torch.where(is_hole, self.mask_threshold - 10.0, masks) + + masks = F.interpolate(masks, orig_hw, mode="bilinear", align_corners=False) + return masks diff --git a/py/evf-sam/model/segment_anything_2/sam2_configs/__init__.py b/py/evf-sam/model/segment_anything_2/sam2_configs/__init__.py new file mode 100644 index 0000000..5277f46 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2_configs/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. diff --git a/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_b+.yaml b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_b+.yaml new file mode 100644 index 0000000..5ca7bfc --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_b+.yaml @@ -0,0 +1,113 @@ +# @package _global_ + +# Model +model: + _target_: model.segment_anything_2.sam2.modeling.sam2_base.SAM2Base + image_encoder: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.ImageEncoder + scalp: 1 + trunk: + _target_: model.segment_anything_2.sam2.modeling.backbones.hieradet.Hiera + embed_dim: 112 + num_heads: 2 + neck: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.FpnNeck + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 256 + normalize: true + scale: null + temperature: 10000 + d_model: 256 + backbone_channel_list: [896, 448, 224, 112] + fpn_top_down_levels: [2, 3] # output level 0 and 1 directly use the backbone features + fpn_interp_model: nearest + + memory_attention: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttention + d_model: 256 + pos_enc_at_input: true + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttentionLayer + activation: relu + dim_feedforward: 2048 + dropout: 0.1 + pos_enc_at_attn: false + self_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + d_model: 256 + pos_enc_at_cross_attn_keys: true + pos_enc_at_cross_attn_queries: false + cross_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + rope_k_repeat: True + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + kv_in_dim: 64 + num_layers: 4 + + memory_encoder: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MemoryEncoder + out_dim: 64 + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 64 + normalize: true + scale: null + temperature: 10000 + mask_downsampler: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MaskDownSampler + kernel_size: 3 + stride: 2 + padding: 1 + fuser: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.Fuser + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.CXBlock + dim: 256 + kernel_size: 7 + padding: 3 + layer_scale_init_value: 1e-6 + use_dwconv: True # depth-wise convs + num_layers: 2 + + num_maskmem: 7 + image_size: 1024 + # apply scaled sigmoid on mask logits for memory encoder, and directly feed input mask as output mask + sigmoid_scale_for_mem_enc: 20.0 + sigmoid_bias_for_mem_enc: -10.0 + use_mask_input_as_output_without_sam: true + # Memory + directly_add_no_mem_embed: true + # use high-resolution feature map in the SAM mask decoder + use_high_res_features_in_sam: true + # output 3 masks on the first click on initial conditioning frames + multimask_output_in_sam: true + # SAM heads + iou_prediction_use_sigmoid: True + # cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder + use_obj_ptrs_in_encoder: true + add_tpos_enc_to_obj_ptrs: false + only_obj_ptrs_in_the_past_for_eval: true + # object occlusion prediction + pred_obj_scores: true + pred_obj_scores_mlp: true + fixed_no_obj_ptr: true + # multimask tracking settings + multimask_output_for_tracking: true + use_multimask_token_for_obj_ptr: true + multimask_min_pt_num: 0 + multimask_max_pt_num: 1 + use_mlp_for_obj_ptr_proj: true + # Compilation flag + compile_image_encoder: False diff --git a/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_l.yaml b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_l.yaml new file mode 100644 index 0000000..293d038 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_l.yaml @@ -0,0 +1,117 @@ +# @package _global_ + +# Model +model: + _target_: model.segment_anything_2.sam2.modeling.sam2_base.SAM2Base + image_encoder: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.ImageEncoder + scalp: 1 + trunk: + _target_: model.segment_anything_2.sam2.modeling.backbones.hieradet.Hiera + embed_dim: 144 + num_heads: 2 + stages: [2, 6, 36, 4] + global_att_blocks: [23, 33, 43] + window_pos_embed_bkg_spatial_size: [7, 7] + window_spec: [8, 4, 16, 8] + neck: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.FpnNeck + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 256 + normalize: true + scale: null + temperature: 10000 + d_model: 256 + backbone_channel_list: [1152, 576, 288, 144] + fpn_top_down_levels: [2, 3] # output level 0 and 1 directly use the backbone features + fpn_interp_model: nearest + + memory_attention: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttention + d_model: 256 + pos_enc_at_input: true + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttentionLayer + activation: relu + dim_feedforward: 2048 + dropout: 0.1 + pos_enc_at_attn: false + self_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + d_model: 256 + pos_enc_at_cross_attn_keys: true + pos_enc_at_cross_attn_queries: false + cross_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + rope_k_repeat: True + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + kv_in_dim: 64 + num_layers: 4 + + memory_encoder: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MemoryEncoder + out_dim: 64 + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 64 + normalize: true + scale: null + temperature: 10000 + mask_downsampler: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MaskDownSampler + kernel_size: 3 + stride: 2 + padding: 1 + fuser: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.Fuser + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.CXBlock + dim: 256 + kernel_size: 7 + padding: 3 + layer_scale_init_value: 1e-6 + use_dwconv: True # depth-wise convs + num_layers: 2 + + num_maskmem: 7 + image_size: 1024 + # apply scaled sigmoid on mask logits for memory encoder, and directly feed input mask as output mask + sigmoid_scale_for_mem_enc: 20.0 + sigmoid_bias_for_mem_enc: -10.0 + use_mask_input_as_output_without_sam: true + # Memory + directly_add_no_mem_embed: true + # use high-resolution feature map in the SAM mask decoder + use_high_res_features_in_sam: true + # output 3 masks on the first click on initial conditioning frames + multimask_output_in_sam: true + # SAM heads + iou_prediction_use_sigmoid: True + # cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder + use_obj_ptrs_in_encoder: true + add_tpos_enc_to_obj_ptrs: false + only_obj_ptrs_in_the_past_for_eval: true + # object occlusion prediction + pred_obj_scores: true + pred_obj_scores_mlp: true + fixed_no_obj_ptr: true + # multimask tracking settings + multimask_output_for_tracking: true + use_multimask_token_for_obj_ptr: true + multimask_min_pt_num: 0 + multimask_max_pt_num: 1 + use_mlp_for_obj_ptr_proj: true + # Compilation flag + compile_image_encoder: False diff --git a/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_s.yaml b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_s.yaml new file mode 100644 index 0000000..8d4627b --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_s.yaml @@ -0,0 +1,116 @@ +# @package _global_ + +# Model +model: + _target_: model.segment_anything_2.sam2.modeling.sam2_base.SAM2Base + image_encoder: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.ImageEncoder + scalp: 1 + trunk: + _target_: model.segment_anything_2.sam2.modeling.backbones.hieradet.Hiera + embed_dim: 96 + num_heads: 1 + stages: [1, 2, 11, 2] + global_att_blocks: [7, 10, 13] + window_pos_embed_bkg_spatial_size: [7, 7] + neck: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.FpnNeck + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 256 + normalize: true + scale: null + temperature: 10000 + d_model: 256 + backbone_channel_list: [768, 384, 192, 96] + fpn_top_down_levels: [2, 3] # output level 0 and 1 directly use the backbone features + fpn_interp_model: nearest + + memory_attention: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttention + d_model: 256 + pos_enc_at_input: true + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttentionLayer + activation: relu + dim_feedforward: 2048 + dropout: 0.1 + pos_enc_at_attn: false + self_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + d_model: 256 + pos_enc_at_cross_attn_keys: true + pos_enc_at_cross_attn_queries: false + cross_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + rope_k_repeat: True + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + kv_in_dim: 64 + num_layers: 4 + + memory_encoder: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MemoryEncoder + out_dim: 64 + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 64 + normalize: true + scale: null + temperature: 10000 + mask_downsampler: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MaskDownSampler + kernel_size: 3 + stride: 2 + padding: 1 + fuser: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.Fuser + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.CXBlock + dim: 256 + kernel_size: 7 + padding: 3 + layer_scale_init_value: 1e-6 + use_dwconv: True # depth-wise convs + num_layers: 2 + + num_maskmem: 7 + image_size: 1024 + # apply scaled sigmoid on mask logits for memory encoder, and directly feed input mask as output mask + sigmoid_scale_for_mem_enc: 20.0 + sigmoid_bias_for_mem_enc: -10.0 + use_mask_input_as_output_without_sam: true + # Memory + directly_add_no_mem_embed: true + # use high-resolution feature map in the SAM mask decoder + use_high_res_features_in_sam: true + # output 3 masks on the first click on initial conditioning frames + multimask_output_in_sam: true + # SAM heads + iou_prediction_use_sigmoid: True + # cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder + use_obj_ptrs_in_encoder: true + add_tpos_enc_to_obj_ptrs: false + only_obj_ptrs_in_the_past_for_eval: true + # object occlusion prediction + pred_obj_scores: true + pred_obj_scores_mlp: true + fixed_no_obj_ptr: true + # multimask tracking settings + multimask_output_for_tracking: true + use_multimask_token_for_obj_ptr: true + multimask_min_pt_num: 0 + multimask_max_pt_num: 1 + use_mlp_for_obj_ptr_proj: true + # Compilation flag + compile_image_encoder: False diff --git a/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_t.yaml b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_t.yaml new file mode 100644 index 0000000..2664b7c --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/sam2_configs/sam2_hiera_t.yaml @@ -0,0 +1,118 @@ +# @package _global_ + +# Model +model: + _target_: model.segment_anything_2.sam2.modeling.sam2_base.SAM2Base + image_encoder: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.ImageEncoder + scalp: 1 + trunk: + _target_: model.segment_anything_2.sam2.modeling.backbones.hieradet.Hiera + embed_dim: 96 + num_heads: 1 + stages: [1, 2, 7, 2] + global_att_blocks: [5, 7, 9] + window_pos_embed_bkg_spatial_size: [7, 7] + neck: + _target_: model.segment_anything_2.sam2.modeling.backbones.image_encoder.FpnNeck + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 256 + normalize: true + scale: null + temperature: 10000 + d_model: 256 + backbone_channel_list: [768, 384, 192, 96] + fpn_top_down_levels: [2, 3] # output level 0 and 1 directly use the backbone features + fpn_interp_model: nearest + + memory_attention: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttention + d_model: 256 + pos_enc_at_input: true + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_attention.MemoryAttentionLayer + activation: relu + dim_feedforward: 2048 + dropout: 0.1 + pos_enc_at_attn: false + self_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + d_model: 256 + pos_enc_at_cross_attn_keys: true + pos_enc_at_cross_attn_queries: false + cross_attention: + _target_: model.segment_anything_2.sam2.modeling.sam.transformer.RoPEAttention + rope_theta: 10000.0 + feat_sizes: [32, 32] + rope_k_repeat: True + embedding_dim: 256 + num_heads: 1 + downsample_rate: 1 + dropout: 0.1 + kv_in_dim: 64 + num_layers: 4 + + memory_encoder: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MemoryEncoder + out_dim: 64 + position_encoding: + _target_: model.segment_anything_2.sam2.modeling.position_encoding.PositionEmbeddingSine + num_pos_feats: 64 + normalize: true + scale: null + temperature: 10000 + mask_downsampler: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.MaskDownSampler + kernel_size: 3 + stride: 2 + padding: 1 + fuser: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.Fuser + layer: + _target_: model.segment_anything_2.sam2.modeling.memory_encoder.CXBlock + dim: 256 + kernel_size: 7 + padding: 3 + layer_scale_init_value: 1e-6 + use_dwconv: True # depth-wise convs + num_layers: 2 + + num_maskmem: 7 + image_size: 1024 + # apply scaled sigmoid on mask logits for memory encoder, and directly feed input mask as output mask + # SAM decoder + sigmoid_scale_for_mem_enc: 20.0 + sigmoid_bias_for_mem_enc: -10.0 + use_mask_input_as_output_without_sam: true + # Memory + directly_add_no_mem_embed: true + # use high-resolution feature map in the SAM mask decoder + use_high_res_features_in_sam: true + # output 3 masks on the first click on initial conditioning frames + multimask_output_in_sam: true + # SAM heads + iou_prediction_use_sigmoid: True + # cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder + use_obj_ptrs_in_encoder: true + add_tpos_enc_to_obj_ptrs: false + only_obj_ptrs_in_the_past_for_eval: true + # object occlusion prediction + pred_obj_scores: true + pred_obj_scores_mlp: true + fixed_no_obj_ptr: true + # multimask tracking settings + multimask_output_for_tracking: true + use_multimask_token_for_obj_ptr: true + multimask_min_pt_num: 0 + multimask_max_pt_num: 1 + use_mlp_for_obj_ptr_proj: true + # Compilation flag + # HieraT does not currently support compilation, should always be set to False + compile_image_encoder: False diff --git a/py/evf-sam/model/segment_anything_2/setup.py b/py/evf-sam/model/segment_anything_2/setup.py new file mode 100644 index 0000000..a228990 --- /dev/null +++ b/py/evf-sam/model/segment_anything_2/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from setuptools import find_packages, setup +from torch.utils.cpp_extension import BuildExtension, CUDAExtension + +def get_extensions(): + srcs = ["sam2/csrc/connected_components.cu"] + compile_args = { + "cxx": [], + "nvcc": [ + "-DCUDA_HAS_FP16=1", + "-D__CUDA_NO_HALF_OPERATORS__", + "-D__CUDA_NO_HALF_CONVERSIONS__", + "-D__CUDA_NO_HALF2_OPERATORS__", + ], + } + ext_modules = [CUDAExtension("sam2._C", srcs, extra_compile_args=compile_args)] + return ext_modules + + +# Setup configuration +setup( + ext_modules=get_extensions(), + cmdclass={"build_ext": BuildExtension.with_options(no_python_abi_suffix=True)}, +) diff --git a/py/evf-sam/model/unilm/beit3/README.md b/py/evf-sam/model/unilm/beit3/README.md new file mode 100644 index 0000000..39c5b2e --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/README.md @@ -0,0 +1,191 @@ +# [(BEiT-3) Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks](https://arxiv.org/abs/2208.10442) + +Official PyTorch implementation and pretrained models of BEiT-3. + +The code and pretrained models of **BEiT** can be found at [here](https://github.com/microsoft/unilm/tree/master/beit). + +The code and pretrained models of **BEiT v2** can be found at [here](https://github.com/microsoft/unilm/tree/master/beit2). + +- March, 2023: release [the code and pretrained models of **BEiT-3**](https://github.com/microsoft/unilm/tree/master/beit3) +- March, 2023: [**BEiT-3**](https://arxiv.org/abs/2208.10442) was accepted by **CVPR 2023**. +- Sept 2022: release [the code and pretrained models of **BEiT v2**](https://github.com/microsoft/unilm/tree/master/beit2) +- Aug 2022: release preprint [Image as a Foreign Language: BEiT Pretraining for All Vision and Vision-Language Tasks](https://arxiv.org/abs/2208.10442) +- Aug 2022: release preprint [BEiT v2: Masked Image Modeling with Vector-Quantized Visual Tokenizers](https://arxiv.org/abs/2208.06366) +- June 2022: release preprint [VL-BEiT: Generative Vision-Language Pretraining](https://arxiv.org/abs/2206.01127) +- March, 2022: add [linear probe examples](https://github.com/microsoft/unilm/blob/master/beit/get_started_for_image_classification.md#example-linear-probe-on-imagenet) +- January, 2022: [**BEiT**](https://openreview.net/forum?id=p-BhZSz59o4) was accepted by **ICLR 2022 as Oral presentation** (54 out of 3391). +- August 2021: [**BEiT**](https://huggingface.co/transformers/master/model_doc/beit.html) is on [HuggingFace](https://github.com/huggingface/transformers) +- July 2021: BEiT-large achieves **[state-of-the-art results on ADE20K](https://paperswithcode.com/sota/semantic-segmentation-on-ade20k) (a big jump to 57.0 mIoU) for semantic segmentation**. +- July 2021: BEiT-large achieves **state-of-the-art ImageNet top-1 accuracy (88.6%) under the setting without extra data other than ImageNet-22k**. +- July 2021: release [the code and pretrained models of **BEiT**](https://github.com/microsoft/unilm/tree/master/beit) +- June 2021: release preprint [BEiT: BERT Pre-Training of Image Transformers](https://arxiv.org/abs/2106.08254) + +## Pretrained models + +We provide BEiT-3 weights pretrained on monomodal and multimodal data. Our large-size model outperforms previous large-size models across various vision-language and vision downstream tasks. The models were pretrained with 224x224 resolution. + +### Tips +- For vision-language tasks that require deep fusion, we recommend using `BEiT3-base` and `BEiT3-large`. +- For image-text retrieval or vision tasks, using `BEiT3-base-itc` and `BEiT3-large-itc` usually achieve better performance. + +### Download Checkpoints + +1. Models pretrained on ImageNet-21k images, 160 GB text documents, and web-scale image-text pairs (collected from [LAION-400M](https://laion.ai/blog/laion-400-open-dataset/), [English LAION-2B](https://laion.ai/blog/laion-5b/), [COYO-700M](https://github.com/kakaobrain/coyo-dataset), and CC15M). + - [`BEiT3-base`](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224.pth): #layer=12; hidden=768; FFN factor=4x; #head=12; patch=16x16; #parameters: 276M + - [`BEiT3-large`](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224.pth): #layer=24; hidden=1024; FFN factor=4x; #head=16; patch=16x16; #parameters: 746M + +2. Perform image-text contrastive intermediate tuning on `BEiT3-base` and `BEiT3-large`. + - [`BEiT3-base-itc`](https://github.com/addf400/files/releases/download/beit3/beit3_base_itc_patch16_224.pth): #layer=12; hidden=768; FFN factor=4x; #head=12; patch=16x16; #parameters: 222M + - [`BEiT3-large-itc`](https://github.com/addf400/files/releases/download/beit3/beit3_large_itc_patch16_224.pth): #layer=24; hidden=1024; FFN factor=4x; #head=16; patch=16x16; #parameters: 674M + +3. Add indomain image-text pairs (COCO and VG) to continue training `BEiT3-base` and `BEiT3-large` using masked data modeling. The indomain models achieve better performance on VQAv2 and NLVR2 tasks. + - [`BEiT3-base-indomain`](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224.pth): #layer=12; hidden=768; FFN factor=4x; #head=12; patch=16x16; #parameters: 276M + - [`BEiT3-large-indomain`](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224.pth): #layer=24; hidden=1024; FFN factor=4x; #head=16; patch=16x16; #parameters: 746M + +### Text Tokenizer + +[beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from transformers import XLMRobertaTokenizer +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") +``` + +### Architecture + +We use [Magneto](https://arxiv.org/abs/2210.06423) with decoupled Multiway Transformer as the backbone architecture. Magneto can have better training stability and obtain better performance across modalities (such as vision, and language). The implementation is based on the [torchscale](https://github.com/microsoft/torchscale/blob/main/torchscale/model/BEiT3.py) package. + + +## Setup + +``` +alias=`whoami | cut -d'.' -f2`; docker run -it --rm --runtime=nvidia --ipc=host --privileged -v /home/${alias}:/home/${alias} pytorch/pytorch:1.8.1-cuda11.1-cudnn8-devel bash +``` + +Clone the repo and install required packages: +``` +git clone https://github.com/microsoft/unilm.git +cd unilm/beit3 +pip install -r requirements.txt +``` + + +## Fine-tuning on ImageNet-1k (Image Classification) + +The detailed instructions can be found at [`get_started_for_image_classification.md`](get_started/get_started_for_image_classification.md). We only use vision-related parameters for image classification fine-tuning. + +| initialized checkpoint | resolution | acc@1 | acc@5 | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-----:|:-------:|-------------------| +| [beit3_base_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224.pth) | 224x224 | 85.4 | 97.6 | 87M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224_in1k.pth) | +| [beit3_base_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224.pth) | 224x224 | 85.4 | 97.6 | 87M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224_in1k.pth) | +| [beit3_large_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224.pth) | 224x224 | 87.6 | 98.3 | 305M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224_in1k.pth) | +| [beit3_large_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224.pth) | 224x224 | 87.5 | 98.3 | 305M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224_in1k.pth) | + + +## Fine-tuning on VQAv2 (Visual Question Answering) + +The detailed instructions can be found at [`get_started_for_vqav2.md`](get_started/get_started_for_vqav2.md). + +| initialized checkpoint | resolution | augmented data | test-dev | test-std | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-----:|:-----:|:-------:|-------------------| +| [beit3_base_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224.pth) | 480x480 | - | 77.65 | - | 228M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_480_vqa.pth) | +| [beit3_base_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224.pth) | 480x480 | - | 78.46 | - | 228M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_480_vqa.pth) | +| [beit3_large_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224.pth) | 480x480 | - | 81.85 | - | 683M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_480_vqa.pth) | +| [beit3_large_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224.pth) | 480x480 | - | 82.53 | - | 683M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_480_vqa.pth) | +| [beit3_large_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224.pth) | 768x768 | VGQA | 82.97 | 83.03 | 684M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_768_vgqaaug_vqa.pth) | + + +## Fine-tuning on NLVR2 (Visual Reasoning) + +The detailed instructions can be found at [`get_started_for_nlvr2.md`](get_started/get_started_for_nlvr2.md). + +| initialized checkpoint | resolution | dev | test-P | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-----:|:-------:|-------------------| +| [beit3_base_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224.pth) | 224x224 | 83.6 | 84.4 | 226M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224_nlvr2.pth) | +| [beit3_base_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224.pth) | 224x224 | 84.6 | 85.3 | 226M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224_nlvr2.pth) | +| [beit3_large_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224.pth) | 224x224 | 88.5 | 89.4 | 681M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224_nlvr2.pth) | +| [beit3_large_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224.pth) | 224x224 | 89.2 | 90.0 | 681M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_indomain_patch16_224_nlvr2.pth) | + + +## Fine-tuning on COCO Captioning and NoCaps (Image Captioning) + +The detailed instructions can be found at [`get_started_for_image_captioning.md`](get_started/get_started_for_captioning.md). + +### COCO Captioning + +| initialized checkpoint | resolution | test CIDEr | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-------:|-------------------| +| [beit3_base_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224.pth) | 480x480 | 133.6 | 271M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_480_coco_captioning.pth) | +| [beit3_base_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224.pth) | 480x480 | 135.0 | 271M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_480_coco_captioning.pth) | +| [beit3_large_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224.pth) | 480x480 | 143.2 | 739M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_480_coco_captioning.pth) | + +### NoCaps + +| initialized checkpoint | resolution | val CIDEr | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-------:|-------------------| +| [beit3_base_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_224.pth) | 480x480 | 104.4 | 271M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_480_nocaps.pth) | +| [beit3_base_indomain_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_224.pth) | 480x480 | 105.6 | 271M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_indomain_patch16_480_nocaps.pth) | +| [beit3_large_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_224.pth) | 480x480 | 120.2 | 739M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_480_nocaps.pth) | + + +## Fine-tuning on COCO and Flickr30k Retrieval (Image-Text Retrieval) + +The detailed instructions can be found at [`get_started_for_retrieval.md`](get_started/get_started_for_retrieval.md). + +### COCO Retrieval + +| initialized checkpoint | resolution | IR@1 | TR@1 | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-----:|:-------:|-------------------| +| [beit3_base_itc_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_itc_patch16_224.pth) | 384x384 | 61.4 | 79.1 | 222M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_384_coco_retrieval.pth) | +| [beit3_large_itc_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_itc_patch16_224.pth) | 384x384 | 63.4 | 82.1 | 675M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_384_coco_retrieval.pth) | + +### Flickr30k Retrieval + +| initialized checkpoint | resolution | IR@1 | TR@1 | #params | weight | +|:----------------------------------------|:----------:|:-----:|:-----:|:-------:|-------------------| +| [beit3_base_itc_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_base_itc_patch16_224.pth) | 384x384 | 86.2 | 96.3 | 222M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_base_patch16_384_f30k_retrieval.pth) | +| [beit3_large_itc_patch16_224](https://github.com/addf400/files/releases/download/beit3/beit3_large_itc_patch16_224.pth) | 384x384 | 88.1 | 97.2 | 675M | [link](https://github.com/addf400/files/releases/download/beit3/beit3_large_patch16_384_f30k_retrieval.pth) | + + +## Citation + +If you find this repository useful, please consider citing our work: +``` +@inproceedings{beit3, +title={Image as a foreign language: {BEiT} pretraining for vision and vision-language tasks}, +author={Wenhui Wang and Hangbo Bao and Li Dong and Johan Bjorck and Zhiliang Peng and Qiang Liu and Kriti Aggarwal and Owais Khan Mohammed and Saksham Singhal and Subhojit Som and Furu Wei}, +booktitle={Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition}, +year={2023} +} + +@article{beitv2, +title={{BEiT v2}: Masked Image Modeling with Vector-Quantized Visual Tokenizers}, +author={Zhiliang Peng and Li Dong and Hangbo Bao and Qixiang Ye and Furu Wei}, +year={2022}, +eprint={2208.06366}, +archivePrefix={arXiv}, +primaryClass={cs.CV} +} + +@inproceedings{beit, +title={{BEiT}: {BERT} Pre-Training of Image Transformers}, +author={Hangbo Bao and Li Dong and Songhao Piao and Furu Wei}, +booktitle={International Conference on Learning Representations}, +year={2022}, +url={https://openreview.net/forum?id=p-BhZSz59o4} +} +``` + + +## Acknowledgement + +This repository is built using the [BEiT](https://github.com/microsoft/unilm/tree/master/beit), the [BEiTv2](https://github.com/microsoft/unilm/tree/master/beit2), the [CLIP](https://github.com/openai/CLIP), the [open_clip](https://github.com/mlfoundations/open_clip), the [Oscar](https://github.com/microsoft/Oscar), the [DeiT](https://github.com/facebookresearch/deit), the [Dino](https://github.com/facebookresearch/dino) repository and the [timm](https://github.com/rwightman/pytorch-image-models) library. + + +## License +This project is licensed under the license found in the LICENSE file in the root directory of this source tree. + +[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct) + +### Contact Information + +For help or issues using BEiT-3 models, please submit a GitHub issue. diff --git a/py/evf-sam/model/unilm/beit3/datasets.py b/py/evf-sam/model/unilm/beit3/datasets.py new file mode 100644 index 0000000..9f6dab8 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/datasets.py @@ -0,0 +1,847 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +import os +import json +import random +import torch +import glob +from collections import defaultdict, Counter +from torchvision import transforms +from torchvision.datasets.folder import default_loader +from timm.data.constants import IMAGENET_DEFAULT_MEAN, IMAGENET_DEFAULT_STD, IMAGENET_INCEPTION_MEAN, IMAGENET_INCEPTION_STD +from timm.data.transforms import RandomResizedCropAndInterpolation +from timm.data import create_transform + +import utils +from glossary import normalize_word +from randaug import RandomAugment + + +class BaseDataset(torch.utils.data.Dataset): + def __init__( + self, data_path, split, transform, + tokenizer, num_max_bpe_tokens, task=None, + ): + index_files = self.get_index_files(split, task=task) + self.tokenizer = tokenizer + self.num_max_bpe_tokens = num_max_bpe_tokens + self.data_path = data_path + items = [] + self.index_files = index_files + + offset = 0 + for _index_file in index_files: + index_file = os.path.join(data_path, _index_file) + with open(index_file, mode="r", encoding="utf-8") as reader: + for line in reader: + data = json.loads(line) + items.append(data) + print("Load %d image-text pairs from %s. " % (len(items) - offset, index_file)) + offset = len(items) + self.items = items + self.bos_token_id = tokenizer.bos_token_id + self.eos_token_id = tokenizer.eos_token_id + self.pad_token_id = tokenizer.pad_token_id + self.loader = default_loader + self.transform = transform + self.split = split + + @staticmethod + def get_index_files(split): + raise NotImplementedError() + + def _get_image(self, image_path: str): + image_path = os.path.join(self.data_path, image_path) + image = self.loader(image_path) + return self.transform(image) + + def _get_text_segment(self, text_segment, max_len=None): + if isinstance(text_segment, str): + tokens = self.tokenizer.tokenize(text_segment) + else: + tokens = text_segment[:] + if len(tokens) == 0: + raise RuntimeError("The text segment should contains at least one tokens!") + if max_len is None: + max_len = self.num_max_bpe_tokens + + if len(tokens) > max_len - 2: + tokens = tokens[:max_len - 2] + + tokens = [self.bos_token_id] + tokens[:] + [self.eos_token_id] + num_tokens = len(tokens) + padding_mask = [0] * num_tokens + [1] * (max_len - num_tokens) + return tokens + [self.pad_token_id] * (max_len - num_tokens), padding_mask, num_tokens + + def _get_image_text_example(self, index: int, data: dict): + item = self.items[index] + img_path = item["image_path"] + img = self._get_image(img_path) + data["image"] = img + + text_segment = item["text_segment"] + language_tokens, padding_mask, _ = self._get_text_segment(text_segment) + data["language_tokens"] = language_tokens + data["padding_mask"] = padding_mask + + def __getitem__(self, index: int): + data = dict() + self._get_image_text_example(index, data) + return data + + def __len__(self) -> int: + return len(self.items) + + def __repr__(self) -> str: + head = "Dataset " + self.__class__.__name__ + body = '{' + "\n Number of items: %s," % self.__len__() + body += "\n data root = %s," % self.data_path + body += "\n split = %s," % self.split + body += "\n dataset index files = %s" % str(self.index_files) + body += "\n num max bpe tokens = %s" % self.num_max_bpe_tokens + body += "\n transforms = [" + for t in self.transform.transforms: + body += "\n %s" % str(t) + body += "\n ]" + body += "\n}" + + return head + body + + +def _write_data_into_jsonl(items, jsonl_file): + with open(jsonl_file, mode="w", encoding="utf-8") as writer: + for data in items: + writer.write(json.dumps(data, indent=None)) + writer.write('\n') + print("Write %s with %d items !" % (jsonl_file, len(items))) + + +def _make_retrieval_coco_karpathy_dataset_index( + data_path, + tokenizer, + split=("train", "restval"), + split_name="train", +): + coco_karpathy_split_json_file = os.path.join(data_path, "dataset_coco.json") + items = [] + image_counter = set() + print("read %s" % coco_karpathy_split_json_file) + with open(coco_karpathy_split_json_file, mode="r", encoding="utf-8") as reader: + data = json.loads(reader.read()) + for item in data["images"]: + if item["split"] in split: + image_path = os.path.join(item["filepath"], item["filename"]) + for sent in item["sentences"]: + tokens = tokenizer.tokenize(sent["raw"]) + token_ids = tokenizer.convert_tokens_to_ids(tokens) + items.append({ + "image_path": image_path, + "text_segment": token_ids, + "image_id": len(image_counter), + }) + if image_path not in image_counter: + image_counter.add(image_path) + print("Find %d images and %d image-text pairs for karpathy dataset %s split !" % \ + (len(image_counter), len(items), split_name)) + index_file = os.path.join(data_path, "coco_retrieval.%s.jsonl" % split_name) + _write_data_into_jsonl(items, index_file) + pass + + +def _make_captioning_coco_karpathy_dataset_index( + data_path, + tokenizer, + split=("train", "restval"), + split_name="train", +): + coco_karpathy_split_json_file = os.path.join(data_path, "dataset_coco.json") + items = [] + image_counter = set() + print("read %s" % coco_karpathy_split_json_file) + with open(coco_karpathy_split_json_file, mode="r", encoding="utf-8") as reader: + data = json.loads(reader.read()) + for item in data["images"]: + if item["split"] in split: + image_path = os.path.join(item["filepath"], item["filename"]) + if item["split"] in ["train", "restval"]: + for sent in item["sentences"]: + tokens = tokenizer.tokenize(sent["raw"]) + token_ids = tokenizer.convert_tokens_to_ids(tokens) + items.append({ + "image_path": image_path, + "text_segment": token_ids, + "image_id": item["cocoid"], + }) + else: + items.append({ + "image_path": image_path, + "text_segment": None, + "image_id": item["cocoid"], + }) + if image_path not in image_counter: + image_counter.add(image_path) + print("Find %d images and %d image-text pairs for karpathy dataset %s split !" % \ + (len(image_counter), len(items), split_name)) + index_file = os.path.join(data_path, "coco_captioning.%s.jsonl" % split_name) + _write_data_into_jsonl(items, index_file) + pass + + +def _make_nocaps_dataset_index( + data_path, + split="val", +): + if split == "val": + json_file = "nocaps_val_4500_captions.json" + elif split == "test": + json_file = "nocaps_test_image_info.json" + nocaps_split_json_file = os.path.join(data_path, json_file) + items = [] + image_counter = set() + print("read %s" % nocaps_split_json_file) + with open(nocaps_split_json_file, mode="r", encoding="utf-8") as reader: + data = json.loads(reader.read()) + for item in data["images"]: + image_path = os.path.join(split, item["file_name"]) + items.append({ + "image_path": image_path, + "text_segment": None, + "image_id": item["id"], + }) + + if image_path not in image_counter: + image_counter.add(image_path) + + print("Find %d images and %d image-text pairs for nocaps dataset %s split !" % \ + (len(image_counter), len(items), split)) + index_file = os.path.join(data_path, "nocaps.%s.jsonl" % split) + _write_data_into_jsonl(items, index_file) + + +class NLVR2Dataset(BaseDataset): + @staticmethod + def get_index_files(split, task=None): + if split == "train": + return ("nlvr2.train.index.jsonl", ) + elif split == "val": + return ("nlvr2.dev.index.jsonl", ) + elif split == "test": + return ("nlvr2.test-P.index.jsonl", ) + else: + raise RuntimeError("split %s is not found!" % split) + + def __getitem__(self, index: int): + data = super().__getitem__(index) + item = self.items[index] + img_path = item["image2_path"] + img = self._get_image(img_path) + data["image2"] = img + data["label"] = self.items[index]["label"] + return data + + @staticmethod + def __preprocess_json(preifx, json_file, tokenizer, index_file): + items = [] + with open(json_file, mode="r", encoding="utf-8") as reader: + for line in reader: + data = json.loads(line) + path = os.path.join(preifx, str(data["directory"])) if "directory" in data else preifx + path = os.path.join(path, "-".join(data["identifier"].split("-")[:-1])) + tokens = tokenizer.tokenize(data["sentence"]) + token_ids = tokenizer.convert_tokens_to_ids(tokens) + items.append({ + "image_path": path + "-img0.png", + "image2_path": path + "-img1.png", + "text_segment": token_ids, + "label": 1 if data["label"] == "True" else 0, + "identifier": data["identifier"], + }) + _write_data_into_jsonl(items, index_file) + + @classmethod + def make_dataset_index(cls, data_path, tokenizer, nlvr_repo_path): + cls.__preprocess_json( + preifx="images/train", json_file=os.path.join(nlvr_repo_path, "nlvr2/data/train.json"), + tokenizer=tokenizer, index_file=os.path.join(data_path, cls.get_index_files("train")[0]), + ) + cls.__preprocess_json( + preifx="dev", json_file=os.path.join(nlvr_repo_path, "nlvr2/data/dev.json"), + tokenizer=tokenizer, index_file=os.path.join(data_path, cls.get_index_files("val")[0]), + ) + cls.__preprocess_json( + preifx="test1", json_file=os.path.join(nlvr_repo_path, "nlvr2/data/test1.json"), + tokenizer=tokenizer, index_file=os.path.join(data_path, cls.get_index_files("test")[0]), + ) + + +class ImageNetDataset(BaseDataset): + @staticmethod + def get_index_files(split, task=None): + if split == "train": + return ("imagenet.train.index.jsonl", ) + elif split == "val": + return ("imagenet.val.index.jsonl", ) + elif split == "test": + return ("imagenet.val.index.jsonl", ) + else: + raise RuntimeError("split %s is not found!" % split) + + def __getitem__(self, index: int): + data = dict() + item = self.items[index] + img_path = item["image_path"] + img = self._get_image(img_path) + data["image"] = img + data["label"] = item["label"] + return data + + @staticmethod + def _find_classes(dir): + """ + Finds the class folders in a dataset. + Args: + dir (string): Root directory path. + Returns: + tuple: (classes, class_to_idx) where classes are relative to (dir), and class_to_idx is a dictionary. + Ensures: + No class is a subdirectory of another. + """ + classes = [d.name for d in os.scandir(dir) if d.is_dir()] + classes.sort() + class_to_idx = {cls_name: i for i, cls_name in enumerate(classes)} + return classes, class_to_idx + + @staticmethod + def _make_imagenet_index(data_path, index_path, data_path_prefix, class_to_idx, split): + items = [] + index_file = os.path.join(index_path, f"imagenet.{split}.index.jsonl") + for target_class in sorted(class_to_idx.keys()): + class_index = class_to_idx[target_class] + target_dir = os.path.join(data_path, target_class) + if not os.path.isdir(target_dir): + continue + for root, _, fnames in sorted(os.walk(target_dir, followlinks=True)): + for fname in sorted(fnames): + path = os.path.join(root, fname) + path = path.replace(data_path_prefix, "") + items.append({ + "image_path": path, + "label": class_index, + }) + + _write_data_into_jsonl(items, index_file) + + @classmethod + def make_dataset_index(cls, train_data_path, val_data_path, index_path): + data_path_prefix = train_data_path[:[x[0]==x[1] for x in zip(train_data_path, val_data_path)].index(0)] + classes, class_to_idx = cls._find_classes(train_data_path) + cls._make_imagenet_index( + data_path=train_data_path, index_path=index_path, data_path_prefix=data_path_prefix, + class_to_idx=class_to_idx, split="train", + ) + cls._make_imagenet_index( + data_path=val_data_path, index_path=index_path, data_path_prefix=data_path_prefix, + class_to_idx=class_to_idx, split="val", + ) + + +class VQAv2Dataset(BaseDataset): + def __init__(self, data_path, **kwargs): + super().__init__(data_path=data_path, **kwargs) + ans2label_file = os.path.join(data_path, "answer2label.txt") + ans2label = {} + label2ans = [] + with open(ans2label_file, mode="r", encoding="utf-8") as reader: + for i, line in enumerate(reader): + data = json.loads(line) + ans = data["answer"] + label = data["label"] + label = int(label) + assert label == i + ans2label[ans] = i + label2ans.append(ans) + + self.ans2label = ans2label + self.label2ans = label2ans + + @staticmethod + def get_index_files(split, task=None): + if split == "train": + return ("vqa.train.jsonl", "vqa.trainable_val.jsonl") + elif split == "val": + return ("vqa.rest_val.jsonl", ) + elif split == "test": + return ("vqa.test.jsonl", ) + elif split == "test-dev": + return ("vqa.test-dev.jsonl", ) + else: + raise RuntimeError("split %s is not found!" % split) + + def __getitem__(self, index: int): + data = super().__getitem__(index) + if "labels" in self.items[index] and len(self.items[index]["labels"]) > 0: + labels = [0.] * len(self.label2ans) + for l, s in zip(self.items[index]["labels"], self.items[index]["scores"]): + labels[l] = s + data["labels"] = torch.FloatTensor(labels) + else: + data["qid"] = self.items[index]["qid"] + return data + + @staticmethod + def get_score(occurences): + if occurences == 0: + return 0.0 + elif occurences == 1: + return 0.3 + elif occurences == 2: + return 0.6 + elif occurences == 3: + return 0.9 + else: + return 1.0 + + @classmethod + def make_dataset_index(cls, data_path, tokenizer, annotation_data_path): + with open(os.path.join(annotation_data_path, "v2_OpenEnded_mscoco_train2014_questions.json"), "r") as fp: + questions_train2014 = json.load(fp)["questions"] + with open(os.path.join(annotation_data_path, "v2_OpenEnded_mscoco_val2014_questions.json"), "r") as fp: + questions_val2014 = json.load(fp)["questions"] + with open(os.path.join(annotation_data_path, "v2_OpenEnded_mscoco_test2015_questions.json"), "r") as fp: + questions_test2015 = json.load(fp)["questions"] + with open(os.path.join(annotation_data_path, "v2_OpenEnded_mscoco_test-dev2015_questions.json"), "r") as fp: + questions_test_dev2015 = json.load(fp)["questions"] + + with open(os.path.join(annotation_data_path, "v2_mscoco_train2014_annotations.json"), "r") as fp: + annotations_train2014 = json.load(fp)["annotations"] + with open(os.path.join(annotation_data_path, "v2_mscoco_val2014_annotations.json"), "r") as fp: + annotations_val2014 = json.load(fp)["annotations"] + + annotations = dict() + + for split, questions in zip( + ["train", "val", "test", "test-dev"], + [questions_train2014, questions_val2014, questions_test2015, questions_test_dev2015], + ): + _annot = defaultdict(dict) + for q in questions: + question_text = q["question"] + tokens = tokenizer.tokenize(question_text) + token_ids = tokenizer.convert_tokens_to_ids(tokens) + + assert q["question_id"] not in _annot[q["image_id"]] + _annot[q["image_id"]][q["question_id"]] = { + "question": question_text, + "token_ids": token_ids, + } + + annotations[split] = _annot + + all_major_answers = list() + + for split, annots in zip( + ["train", "val"], [annotations_train2014, annotations_val2014], + ): + # _annot = annotations[split] + for q in annots: + all_major_answers.append(q["multiple_choice_answer"]) + + all_major_answers = [normalize_word(word) for word in all_major_answers] + counter = {k: v for k, v in Counter(all_major_answers).items() if v >= 9} + ans2label = {k: i for i, k in enumerate(counter.keys())} + label2ans = list(counter.keys()) + + for split, annots in zip( + ["train", "val"], [annotations_train2014, annotations_val2014], + ): + _annot = annotations[split] + for q in annots: + answers = q["answers"] + answer_count = {} + for answer in answers: + answer_ = answer["answer"] + answer_count[answer_] = answer_count.get(answer_, 0) + 1 + + labels = [] + scores = [] + for answer in answer_count: + if answer not in ans2label: + continue + labels.append(ans2label[answer]) + score = cls.get_score(answer_count[answer]) + scores.append(score) + + assert "labels" not in _annot[q["image_id"]][q["question_id"]] + assert "question" in _annot[q["image_id"]][q["question_id"]] + _annot[q["image_id"]][q["question_id"]]["labels"] = labels + _annot[q["image_id"]][q["question_id"]]["scores"] = scores + + for split in ["train", "val"]: + filtered_annot = dict() + for ik, iv in annotations[split].items(): + new_q = dict() + for qk, qv in iv.items(): + if len(qv["labels"]) != 0: + new_q[qk] = qv + if len(new_q) != 0: + filtered_annot[ik] = new_q + annotations[split] = filtered_annot + + split2items = {} + for split in ["train", "val", "test", "test-dev"]: + annot = annotations[split] + split_name = { + "train": "train2014", + "val": "val2014", + "test": "test2015", + "test-dev": "test2015", + }[split] + paths = list(glob.glob(f"{data_path}/{split_name}/*.jpg")) + random.shuffle(paths) + annot_paths = [path for path in paths \ + if int(path.split("/")[-1].split("_")[-1][:-4]) in annot] + + if len(paths) == len(annot_paths): + print("all images have caption annotations") + else: + print("not all images have caption annotations") + print(len(paths), len(annot_paths), len(annot)) + + items = [] + for path in annot_paths: + iid = int(path.split("/")[-1].split("_")[-1][:-4]) + _annot = annotations[split][iid] + for qid in _annot: + q = _annot[qid] + if split in ["train", "val"]: + labels = q["labels"] + scores = q["scores"] + else: + labels, scores = [], [] + + items.append({ + "image_path": os.path.join(split_name, path.split('/')[-1]), + "text_segment": q["token_ids"], + "labels": labels, + "scores": scores, + "qid": qid, + }) + split2items[split] = items + + _write_data_into_jsonl(items=items, jsonl_file=os.path.join(data_path, "vqa.%s.jsonl" % split)) + + # Following ViLT, we use 1000 images of the original val set as the final val set + val_image2items = defaultdict(list) + for item in split2items["val"]: + val_image2items[item["image_path"]].append(item) + + print("Contains %d image and %d pairs for val set!" % (len(val_image2items), len(split2items["val"]))) + + val_images = list(val_image2items.keys()) + random.shuffle(val_images) + trainable_val = [] + rest_val = [] + for i, image_id in enumerate(val_images): + if i < 1000: + rest_val += val_image2items[image_id] + else: + trainable_val += val_image2items[image_id] + + _write_data_into_jsonl(items=trainable_val, jsonl_file=os.path.join(data_path, "vqa.trainable_val.jsonl")) + _write_data_into_jsonl(items=rest_val, jsonl_file=os.path.join(data_path, "vqa.rest_val.jsonl")) + + with open(os.path.join(data_path, "answer2label.txt"), mode="w", encoding="utf-8") as writer: + for ans in ans2label: + to_json = { + "answer": ans, + "label": ans2label[ans] + } + writer.write("%s\n" % json.dumps(to_json)) + + +class RetrievalDataset(BaseDataset): + @staticmethod + def get_index_files(split, task=None): + if split == "train": + return (f"{task}.train.jsonl", ) + elif split == "val": + return (f"{task}.val.jsonl", ) + elif split == "test": + return (f"{task}.test.jsonl", ) + else: + raise RuntimeError("split %s is not found!" % split) + + def __getitem__(self, index: int): + data = super().__getitem__(index) + data["image_id"] = self.items[index]["image_id"] + return data + + @staticmethod + def make_flickr30k_dataset_index(data_path, tokenizer, karpathy_path): + + with open(os.path.join(karpathy_path, "dataset_flickr30k.json"), "r") as reader: + captions = json.loads(reader.read()) + + captions = captions["images"] + split2items = defaultdict(list) + split2images = defaultdict(set) + + for each_item in captions: + image_path = os.path.join("flickr30k-images", each_item["filename"]) + split = each_item["split"] + + for text_segment in each_item["sentences"]: + tokens = tokenizer.tokenize(text_segment["raw"]) + token_ids = tokenizer.convert_tokens_to_ids(tokens) + + split2items[split].append({ + "image_path": image_path, + "text_segment": token_ids, + "image_id": len(split2images[split]), + }) + + assert each_item["filename"] not in split2images[split] + split2images[split].add(each_item["filename"]) + + for split in split2items: + print("%d images and %d image-text pairs!" % (len(split2images[split]), len(split2items[split]))) + _write_data_into_jsonl(split2items[split], os.path.join(data_path, "flickr30k.%s.jsonl" % split)) + + @staticmethod + def make_coco_dataset_index(data_path, tokenizer): + _make_retrieval_coco_karpathy_dataset_index(data_path, tokenizer, split=("train", "restval"), split_name="train") + _make_retrieval_coco_karpathy_dataset_index(data_path, tokenizer, split=("val", ), split_name="val") + _make_retrieval_coco_karpathy_dataset_index(data_path, tokenizer, split=("test", ), split_name="test") + + +class CaptioningDataset(BaseDataset): + + def __init__(self, data_path, split, transform, + tokenizer, num_max_bpe_tokens, task, mask_prob): + super().__init__( + data_path=data_path, split=split, + transform=transform, tokenizer=tokenizer, + num_max_bpe_tokens=num_max_bpe_tokens, task=task, + ) + self.mask_token_id = tokenizer.mask_token_id + self.language_vocab_size = tokenizer.vocab_size + self.mask_prob = mask_prob + + @staticmethod + def get_index_files(split, task=None): + if split == "train": + return ("coco_captioning.train.jsonl", ) + elif split == "val": + return (f"{task}.val.jsonl", ) + elif split == "test": + return (f"{task}.test.jsonl", ) + else: + raise RuntimeError("split %s is not found!" % split) + + def _get_mask_token(self, token): + p = random.random() + if p < 0.8: + return self.mask_token_id + elif p < 0.9: + return token + else: + return random.randint(3, self.language_vocab_size - 1) + + def _masking_on_text_tokens(self, tokens, num_tokens, mask_prob): + bool_masked_pos = [0] * len(tokens) + to_mask = min(int(num_tokens * mask_prob + 0.5), num_tokens - 1) + to_mask = max(to_mask, 1) + num_masked_tokens = 0 + while num_masked_tokens < to_mask: + i = random.randint(1, num_tokens - 1) + if bool_masked_pos[i] == 0: + bool_masked_pos[i] = 1 + tokens[i] = self._get_mask_token(tokens[i]) + num_masked_tokens += 1 + + return tokens, bool_masked_pos + + def __getitem__(self, index: int): + data = dict() + item = self.items[index] + img_path = item["image_path"] + img = self._get_image(img_path) + data["image"] = img + data["image_id"] = item["image_id"] + + text_segment = item["text_segment"] + if text_segment is not None: + language_tokens, padding_mask, num_tokens = self._get_text_segment(text_segment) + masked_tokens = language_tokens[:] + masked_tokens, language_masked_pos = \ + self._masking_on_text_tokens(masked_tokens, num_tokens, self.mask_prob) + data["language_tokens"] = language_tokens + data["masked_tokens"] = masked_tokens + data["language_masked_pos"] = language_masked_pos + data["padding_mask"] = padding_mask + return data + + @staticmethod + def make_coco_captioning_dataset_index(data_path, tokenizer): + _make_captioning_coco_karpathy_dataset_index(data_path, tokenizer, split=("train", "restval"), split_name="train") + _make_captioning_coco_karpathy_dataset_index(data_path, tokenizer, split=("val", ), split_name="val") + _make_captioning_coco_karpathy_dataset_index(data_path, tokenizer, split=("test", ), split_name="test") + + @staticmethod + def make_nocaps_captioning_dataset_index(data_path): + _make_nocaps_dataset_index(data_path, split="val") + _make_nocaps_dataset_index(data_path, split="test") + + +task2dataset = { + "nlvr2": NLVR2Dataset, + "vqav2": VQAv2Dataset, + "flickr30k": RetrievalDataset, + "coco_retrieval": RetrievalDataset, + "coco_captioning": CaptioningDataset, + "nocaps": CaptioningDataset, + "imagenet": ImageNetDataset, +} + + +def create_dataloader(dataset, is_train, batch_size, num_workers, pin_mem, dist_eval=False): + if is_train or dist_eval: + num_tasks = utils.get_world_size() + global_rank = utils.get_rank() + + if not is_train and dist_eval and len(dataset) % num_tasks != 0: + print('Warning: Enabling distributed evaluation with an eval dataset not divisible by process number. ' + 'This will slightly alter validation results as extra duplicate entries are added to achieve ' + 'equal num of samples per-process.') + + sampler = torch.utils.data.DistributedSampler( + dataset, num_replicas=num_tasks, rank=global_rank, shuffle=is_train + ) + else: + sampler = torch.utils.data.SequentialSampler(dataset) + + return torch.utils.data.DataLoader( + dataset, sampler=sampler, + batch_size=batch_size, + num_workers=num_workers, + pin_memory=pin_mem, + drop_last=is_train, + collate_fn=utils.merge_batch_tensors_by_dict_key, + ) + + +def build_transform(is_train, args): + if args.task in ["imagenet"]: + return build_imagenet_transform(is_train, args) + + if is_train: + t = [ + RandomResizedCropAndInterpolation(args.input_size, scale=(0.5, 1.0), interpolation=args.train_interpolation), + transforms.RandomHorizontalFlip(), + ] + if args.randaug: + t.append( + RandomAugment( + 2, 7, isPIL=True, + augs=[ + 'Identity','AutoContrast','Equalize','Brightness','Sharpness', + 'ShearX', 'ShearY', 'TranslateX', 'TranslateY', 'Rotate', + ])) + t += [ + transforms.ToTensor(), + transforms.Normalize(mean=IMAGENET_INCEPTION_MEAN, std=IMAGENET_INCEPTION_STD), + ] + t = transforms.Compose(t) + else: + t = transforms.Compose([ + transforms.Resize((args.input_size, args.input_size), interpolation=3), + transforms.ToTensor(), + transforms.Normalize(mean=IMAGENET_INCEPTION_MEAN, std=IMAGENET_INCEPTION_STD) + ]) + + return t + + +def build_imagenet_transform(is_train, args): + resize_im = args.input_size > 32 + if is_train: + # this should always dispatch to transforms_imagenet_train + transform = create_transform( + input_size=args.input_size, + is_training=True, + color_jitter=args.color_jitter, + auto_augment=args.aa, + interpolation=args.train_interpolation, + re_prob=args.reprob, + re_mode=args.remode, + re_count=args.recount, + mean=IMAGENET_DEFAULT_MEAN, + std=IMAGENET_DEFAULT_STD, + ) + if not resize_im: + # replace RandomResizedCropAndInterpolation with + # RandomCrop + transform.transforms[0] = transforms.RandomCrop( + args.input_size, padding=4) + return transform + + t = [] + if resize_im: + if args.crop_pct is None: + args.crop_pct = 1.0 + size = int(args.input_size / args.crop_pct) + t.append( + transforms.Resize(size, interpolation=3), # to maintain same ratio w.r.t. 224 images + ) + t.append(transforms.CenterCrop(args.input_size)) + + t.append(transforms.ToTensor()) + t.append(transforms.Normalize(mean=IMAGENET_DEFAULT_MEAN, std=IMAGENET_DEFAULT_STD)) + return transforms.Compose(t) + + +def get_sentencepiece_model_for_beit3(args): + from transformers import XLMRobertaTokenizer + return XLMRobertaTokenizer(args.sentencepiece_model) + + +def create_dataset_by_split(args, split, is_train=True): + transform = build_transform(is_train=is_train, args=args) + dataset_class = task2dataset[args.task] + tokenizer = get_sentencepiece_model_for_beit3(args) + + opt_kwargs = {} + if args.task in ["coco_captioning", "nocaps"]: + opt_kwargs["mask_prob"] = args.captioning_mask_prob + + dataset = dataset_class( + data_path=args.data_path, split=split, + transform=transform, tokenizer=tokenizer, + num_max_bpe_tokens=args.num_max_bpe_tokens, + task=args.task, **opt_kwargs, + ) + if is_train: + batch_size = args.batch_size + elif hasattr(args, "eval_batch_size") and args.eval_batch_size is not None: + batch_size = args.eval_batch_size + else: + batch_size = int(args.batch_size * 1.5) + + return create_dataloader( + dataset, is_train=is_train, batch_size=batch_size, + num_workers=args.num_workers, pin_mem=args.pin_mem, dist_eval=args.dist_eval, + ) + + +def create_downstream_dataset(args, is_eval=False): + if is_eval: + return create_dataset_by_split(args, split="test", is_train=False) + else: + return \ + create_dataset_by_split(args, split="train", is_train=True), \ + create_dataset_by_split(args, split="val", is_train=True) diff --git a/py/evf-sam/model/unilm/beit3/engine_for_finetuning.py b/py/evf-sam/model/unilm/beit3/engine_for_finetuning.py new file mode 100644 index 0000000..9be308c --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/engine_for_finetuning.py @@ -0,0 +1,598 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +import math +import sys +import json +from typing import Iterable, Optional + +import torch +import torch.nn as nn +import torch.nn.functional as F + +from timm.utils import ModelEma +from timm.utils import accuracy, ModelEma +from timm.loss import LabelSmoothingCrossEntropy, SoftTargetCrossEntropy +from datasets import get_sentencepiece_model_for_beit3 + +import utils + + +class TaskHandler(object): + def __init__(self) -> None: + self.metric_logger = None + self.split = None + + def train_batch(self, model, **kwargs): + raise NotImplementedError() + + def eval_batch(self, model, **kwargs): + raise NotImplementedError() + + def before_eval(self, metric_logger, data_loader, **kwargs): + self.metric_logger = metric_logger + self.split = data_loader.dataset.split + + def after_eval(self, **kwargs): + raise NotImplementedError() + + +class NLVR2Handler(TaskHandler): + def __init__(self) -> None: + super().__init__() + self.criterion = torch.nn.CrossEntropyLoss() + + def train_batch(self, model, image, image2, language_tokens, padding_mask, label): + logits = model( + image_a=image, image_b=image2, + text_description=language_tokens, + padding_mask=padding_mask) + acc = (logits.max(-1)[-1] == label).float().mean() + return { + "loss": self.criterion(input=logits, target=label), + "acc": acc, + } + + def eval_batch(self, model, image, image2, language_tokens, padding_mask, label): + logits = model( + image_a=image, image_b=image2, + text_description=language_tokens, + padding_mask=padding_mask) + batch_size = language_tokens.shape[0] + acc = (logits.max(-1)[-1] == label).float().sum(0) * 100.0 / batch_size + self.metric_logger.meters['acc'].update(acc.item(), n=batch_size) + + def after_eval(self, **kwargs): + print('* Acc {acc.global_avg:.3f}'.format(acc=self.metric_logger.acc)) + return {k: meter.global_avg for k, meter in self.metric_logger.meters.items()}, "acc" + + +class ImageNetHandler(TaskHandler): + def __init__(self, args) -> None: + super().__init__() + mixup_active = args.mixup > 0 or args.cutmix > 0. or args.cutmix_minmax is not None + if mixup_active: + # smoothing is handled with mixup label transform + self.criterion = SoftTargetCrossEntropy() + elif args.label_smoothing > 0.: + self.criterion = LabelSmoothingCrossEntropy(smoothing=args.label_smoothing) + else: + self.criterion = torch.nn.CrossEntropyLoss() + + def train_batch(self, model, image, label): + logits = model(image=image) + return { + "loss": self.criterion(logits, label), + } + + def eval_batch(self, model, image, label): + logits = model(image=image) + batch_size = image.shape[0] + acc1, acc5 = accuracy(logits, label, topk=(1, 5)) + self.metric_logger.meters['acc1'].update(acc1.item(), n=batch_size) + self.metric_logger.meters['acc5'].update(acc5.item(), n=batch_size) + + def after_eval(self, **kwargs): + print('* Acc@1 {top1.global_avg:.3f} Acc@5 {top5.global_avg:.3f}' + .format(top1=self.metric_logger.acc1, top5=self.metric_logger.acc5)) + return {k: meter.global_avg for k, meter in self.metric_logger.meters.items()}, "acc1" + + +class RetrievalHandler(TaskHandler): + def __init__(self) -> None: + super().__init__() + self.image_feats = [] + self.text_feats = [] + self.image_ids = [] + self.metric_logger = None + + def train_batch(self, model, image, language_tokens, padding_mask, image_id): + loss, vision_cls, language_cls = model( + image=image, text_description=language_tokens, padding_mask=padding_mask) + return { + "loss": loss, + } + + def before_eval(self, metric_logger, **kwargs): + self.image_feats.clear() + self.text_feats.clear() + self.image_ids.clear() + self.metric_logger = metric_logger + + def eval_batch(self, model, image, language_tokens, padding_mask, image_id): + vision_cls, _ = model(image=image, only_infer=True) + _, language_cls = model( + text_description=language_tokens, padding_mask=padding_mask, only_infer=True) + + self.image_feats.append(vision_cls.clone()) + self.text_feats.append(language_cls.clone()) + self.image_ids.append(image_id.clone()) + + def after_eval(self, **kwargs): + image_feats = {} + for feats, ids in zip(self.image_feats, self.image_ids): + for i, _idx in enumerate(ids): + idx = _idx.item() + if idx not in image_feats: + image_feats[idx] = feats[i] + + tiids = torch.cat(self.image_ids, dim=0) + iids = [] + sorted_tensors = [] + for key in sorted(image_feats.keys()): + sorted_tensors.append(image_feats[key].view(1, -1)) + iids.append(key) + + image_cls_feats = torch.cat(sorted_tensors, dim=0) + text_cls_feats = torch.cat(self.text_feats, dim=0) + + scores = image_cls_feats @ text_cls_feats.t() + iids = torch.LongTensor(iids).to(scores.device) + + print("scores: {}".format(scores.size())) + print("iids: {}".format(iids.size())) + print("tiids: {}".format(tiids.size())) + + topk10 = scores.topk(10, dim=1) + topk5 = scores.topk(5, dim=1) + topk1 = scores.topk(1, dim=1) + + topk10_iids = tiids[topk10.indices] + topk5_iids = tiids[topk5.indices] + topk1_iids = tiids[topk1.indices] + + tr_r10 = (iids.unsqueeze(1) == topk10_iids).float().max(dim=1)[0].mean() + tr_r5 = (iids.unsqueeze(1) == topk5_iids).float().max(dim=1)[0].mean() + tr_r1 = (iids.unsqueeze(1) == topk1_iids).float().max(dim=1)[0].mean() + + topk10 = scores.topk(10, dim=0) + topk5 = scores.topk(5, dim=0) + topk1 = scores.topk(1, dim=0) + topk10_iids = iids[topk10.indices] + topk5_iids = iids[topk5.indices] + topk1_iids = iids[topk1.indices] + + ir_r10 = (tiids.unsqueeze(0) == topk10_iids).float().max(dim=0)[0].mean() + ir_r5 = (tiids.unsqueeze(0) == topk5_iids).float().max(dim=0)[0].mean() + ir_r1 = (tiids.unsqueeze(0) == topk1_iids).float().max(dim=0)[0].mean() + + eval_result = { + "tr_r10": tr_r10.item() * 100.0, + "tr_r5": tr_r5.item() * 100.0, + "tr_r1": tr_r1.item() * 100.0, + "ir_r10": ir_r10.item() * 100.0, + "ir_r5": ir_r5.item() * 100.0, + "ir_r1": ir_r1.item() * 100.0, + "average_score": 100.0 * (tr_r1 + tr_r5 + tr_r10 + ir_r1 + ir_r5 + ir_r10).item() / 6.0, + } + + print('* Eval result = %s' % json.dumps(eval_result)) + return eval_result, "average_score" + + +class VQAHandler(TaskHandler): + def __init__(self) -> None: + super().__init__() + self.predictions = [] + self.criterion = nn.BCEWithLogitsLoss(reduction='mean') + self.label2ans = None + + def train_batch(self, model, image, language_tokens, padding_mask, labels): + logits = model( + image=image, question=language_tokens, + padding_mask=padding_mask) + return { + "loss": self.criterion(input=logits.float(), target=labels.float()) * labels.shape[1], + } + + def before_eval(self, metric_logger, data_loader, **kwargs): + self.predictions.clear() + self.metric_logger = metric_logger + self.label2ans = data_loader.dataset.label2ans + + def eval_batch(self, model, image, language_tokens, padding_mask, labels=None, qid=None): + logits = model( + image=image, question=language_tokens, + padding_mask=padding_mask) + batch_size = language_tokens.shape[0] + if labels is not None: + scores = utils.VQAScore()(logits, labels) * 100.0 + self.metric_logger.meters['score'].update(scores.item(), n=batch_size) + else: + _, preds = logits.max(-1) + for image_id, pred in zip(qid, preds): + self.predictions.append({ + "question_id": image_id.item(), + "answer": self.label2ans[pred.item()], + }) + + def after_eval(self, **kwargs): + if len(self.predictions) == 0: + print('* Score {score.global_avg:.3f}'.format(score=self.metric_logger.score)) + return {k: meter.global_avg for k, meter in self.metric_logger.meters.items()}, "score" + else: + return self.predictions, "prediction" + + +class CaptioningHandler(TaskHandler): + def __init__(self, args) -> None: + super().__init__() + self.predictions = [] + self.criterion = utils.BertCaptioningLoss(args.label_smoothing, args.drop_worst_ratio, args.drop_worst_after) + self.tokenizer = get_sentencepiece_model_for_beit3(args) + self.num_beams = args.num_beams + self.max_len = args.num_max_bpe_tokens + self.length_penalty = args.length_penalty + self.vocab_size = args.vocab_size + + def train_batch(self, model, image, language_tokens, masked_tokens, language_masked_pos, padding_mask, image_id, global_step): + logits, _ = model( + image=image, text_ids=masked_tokens, padding_mask=padding_mask, language_masked_pos=language_masked_pos, image_id=image_id) + masked_labels = language_tokens[language_masked_pos.bool()] + score = torch.max(logits, -1)[1].data == masked_labels + acc = torch.sum(score.float()) / torch.sum(language_masked_pos) + return { + "loss": self.criterion(logits, masked_labels, global_step), + "acc": acc + } + + def before_eval(self, metric_logger, data_loader, **kwargs): + self.predictions.clear() + self.metric_logger = metric_logger + + def eval_batch(self, model, image, image_id=None): + cur_len = 2 + num_keep_best = 1 + TOPN_PER_BEAM = 3 + + batch_size = image.size(0) + mask_id = self.tokenizer.mask_token_id + cls_id = self.tokenizer.cls_token_id + pad_id = self.tokenizer.pad_token_id + sep_id = self.tokenizer.sep_token_id + eos_token_ids = [sep_id] + + cls_ids = torch.full( + (batch_size, 1), cls_id, dtype=torch.long, device=image.device + ) + mask_ids = torch.full( + (batch_size, 1), mask_id, dtype=torch.long, device=image.device + ) + cur_input_ids = torch.cat([cls_ids, mask_ids], dim=1) + tmp_ids = torch.full( + (batch_size, self.max_len-1), mask_id, dtype=torch.long, device=image.device + ) + decoding_results = torch.cat([cls_ids, tmp_ids], dim=1) + + # Expand input to num beams + cur_input_ids = cur_input_ids.unsqueeze(1).expand(batch_size, self.num_beams, cur_len) + cur_input_ids = cur_input_ids.contiguous().view(batch_size * self.num_beams, cur_len) # (batch_size * num_beams, cur_len) + decoding_results = decoding_results.unsqueeze(1).expand(batch_size, self.num_beams, self.max_len) + decoding_results = decoding_results.contiguous().view(batch_size * self.num_beams, self.max_len) # (batch_size * num_beams, cur_len) + image = image.unsqueeze(1).expand(batch_size, self.num_beams, image.size(-3), image.size(-2), image.size(-1)) + image = image.contiguous().view(batch_size * self.num_beams, image.size(-3), image.size(-2), image.size(-1)) + + generated_hyps = [ + utils.BeamHypotheses( + num_keep_best, self.max_len, length_penalty=self.length_penalty, early_stopping=False + ) for _ in range(batch_size) + ] + # scores for each sentence in the beam + beam_scores = torch.zeros((batch_size, self.num_beams), dtype=torch.float, device=cur_input_ids.device) + beam_scores[:, 1:] = -1e9 + beam_scores = beam_scores.view(-1) # shape (batch_size * num_beams,) + + # done sentences + done = [False for _ in range(batch_size)] + incremental_state = {} + + while cur_len <= self.max_len: + next_token_idx = 1 + padding_masks = torch.full( + cur_input_ids.shape, 0, dtype=torch.long, device=image.device + ) + input_image = image + if cur_len != 2: + input_image = None + + outputs, incremental_state_next = model( + image=input_image, text_ids=cur_input_ids, language_masked_pos=None, + padding_mask=padding_masks, text_len=cur_len, incremental_state=incremental_state) + incremental_state = incremental_state_next + + # assert outputs.shape[1] == token_len + scores = outputs[:, next_token_idx, :] # (batch_size * num_beams, vocab_size) + scores = F.log_softmax(scores, dim=-1) # (batch_size * num_beams, vocab_size) + assert scores.size() == (batch_size * self.num_beams, self.vocab_size) + # Add the log prob of the new beams to the log prob of the beginning of the sequence (sum of logs == log of the product) + _scores = scores + beam_scores[:, None].expand_as(scores) # (batch_size * num_beams, vocab_size) + # re-organize to group the beam together (we are keeping top hypothesis accross beams) + _scores = _scores.view(batch_size, self.num_beams * self.vocab_size) # (batch_size, num_beams * vocab_size) + next_scores, next_words = torch.topk(_scores, TOPN_PER_BEAM * self.num_beams, dim=1, largest=True, sorted=True) + assert next_scores.size() == next_words.size() == (batch_size, TOPN_PER_BEAM * self.num_beams) + + # next batch beam content + # list of (batch_size * num_beams) tuple(next hypothesis score, next word, current position in the batch) + next_batch_beam = [] + # for each sentence + for batch_ex in range(batch_size): + # if we are done with this sentence + done[batch_ex] = done[batch_ex] or generated_hyps[batch_ex].is_done(next_scores[batch_ex].max().item()) + if done[batch_ex]: + next_batch_beam.extend([(0, pad_id, 0)] * self.num_beams) # pad the batch + continue + + # next sentence beam content + next_sent_beam = [] + for idx, score in zip(next_words[batch_ex], next_scores[batch_ex]): + # get beam and word IDs + beam_id = idx // self.vocab_size + word_id = idx % self.vocab_size + # end of sentence, or next word + # if word_id.item() in eos_token_ids or cur_len + 1 == max_len: + if (word_id.item() in eos_token_ids and cur_len + 1 <= self.max_len) or (cur_len + 1 == self.max_len): + generated_hyps[batch_ex].add( + decoding_results[batch_ex * self.num_beams + beam_id, :cur_len].clone(), score.item() + ) + else: + next_sent_beam.append((score, word_id, batch_ex * self.num_beams + beam_id)) + # the beam for next step is full + if len(next_sent_beam) == self.num_beams: + break + + # update next beam content + if cur_len + 1 == self.max_len: + assert len(next_sent_beam) == 0 + else: + assert len(next_sent_beam) == self.num_beams + + if len(next_sent_beam) == 0: + next_sent_beam = [(0, pad_id, 0)] * self.num_beams # pad the batch + next_batch_beam.extend(next_sent_beam) + assert len(next_batch_beam) == self.num_beams * (batch_ex + 1) + + # sanity check / prepare next batch + assert len(next_batch_beam) == batch_size * self.num_beams + beam_scores = beam_scores.new([x[0] for x in next_batch_beam]) + beam_words = cur_input_ids.new([x[1] for x in next_batch_beam]) + beam_idx = cur_input_ids.new([x[2] for x in next_batch_beam]) + + # re-order batch + cur_input_ids = cur_input_ids[beam_idx, :] + decoding_results = decoding_results[beam_idx, :] + for module in incremental_state: + for key in incremental_state[module]: + result = incremental_state[module][key].index_select(0, beam_idx) + incremental_state[module][key] = result[:,:,:-1,:] + + next_ids = torch.full( + (batch_size * self.num_beams, 1), mask_id, dtype=torch.long, device=image.device + ) + cur_input_ids = torch.cat([beam_words.unsqueeze(1), next_ids], dim=1) + decoding_results[:, cur_len-1] = beam_words + # update current length + cur_len = cur_len + 1 + # stop when we are done with each sentence + if all(done): + break + + # select the best hypotheses + tgt_len = torch.ones(batch_size, num_keep_best, dtype=torch.long) + logprobs = torch.zeros(batch_size, num_keep_best, + dtype=torch.float).fill_(-1e5).to(cur_input_ids.device) + all_best = [] + + for i, hypotheses in enumerate(generated_hyps): + best = [] + hyp_scores = torch.tensor([x[0] for x in hypotheses.hyp]) + _, best_indices = torch.topk(hyp_scores, + min(num_keep_best, len(hyp_scores)), largest=True) + for best_idx, hyp_idx in enumerate(best_indices): + conf, best_hyp = hypotheses.hyp[hyp_idx] + best.append(best_hyp) + logprobs[i, best_idx] = conf + tgt_len[i, best_idx] = len(best_hyp) + 1 # +1 for the symbol + all_best.append(best) + + # generate target batch, pad to the same length + decoded = cur_input_ids.new(batch_size, num_keep_best, self.max_len).fill_(pad_id) + for batch_idx, best in enumerate(all_best): + for best_idx, hypo in enumerate(best): + decoded[batch_idx, best_idx, : tgt_len[batch_idx, best_idx] - 1] = hypo + decoded[batch_idx, best_idx, tgt_len[batch_idx, best_idx] - 1] = eos_token_ids[0] + + captions = self.tokenizer.batch_decode(decoded.squeeze(1), skip_special_tokens=True) + for qid, pred in zip(image_id, captions): + self.predictions.append({ + "image_id": qid.item(), + "caption": pred, + }) + + def after_eval(self, **kwargs): + return self.predictions, "prediction" + + +def get_handler(args): + if args.task == "nlvr2": + return NLVR2Handler() + elif args.task == "vqav2": + return VQAHandler() + elif args.task in ("flickr30k", "coco_retrieval"): + return RetrievalHandler() + elif args.task in ("coco_captioning", "nocaps"): + return CaptioningHandler(args) + elif args.task in ("imagenet"): + return ImageNetHandler(args) + else: + raise NotImplementedError("Sorry, %s is not support." % args.task) + + +def train_one_epoch( + model: torch.nn.Module, data_loader: Iterable, + optimizer: torch.optim.Optimizer, device: torch.device, + handler: TaskHandler, epoch: int, start_steps: int, + lr_schedule_values: list, loss_scaler, max_norm: float = 0, + update_freq: int = 1, model_ema: Optional[ModelEma] = None, + log_writer: Optional[utils.TensorboardLogger] = None, + task = None, mixup_fn=None, +): + model.train(True) + metric_logger = utils.MetricLogger(delimiter=" ") + metric_logger.add_meter('lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}')) + metric_logger.add_meter('min_lr', utils.SmoothedValue(window_size=1, fmt='{value:.6f}')) + header = 'Epoch: [{}]'.format(epoch) + print_freq = 10 + + if loss_scaler is None: + model.zero_grad() + model.micro_steps = 0 + else: + optimizer.zero_grad() + + for data_iter_step, data in enumerate(metric_logger.log_every(data_loader, print_freq, header)): + step = data_iter_step // update_freq + global_step = start_steps + step # global training iteration + # Update LR & WD for the first acc + if lr_schedule_values is not None and data_iter_step % update_freq == 0: + for i, param_group in enumerate(optimizer.param_groups): + if lr_schedule_values is not None: + param_group["lr"] = lr_schedule_values[global_step] * param_group["lr_scale"] + # put input data into cuda + for tensor_key in data.keys(): + data[tensor_key] = data[tensor_key].to(device, non_blocking=True) + # print("input %s = %s" % (tensor_key, data[tensor_key])) + if loss_scaler is None and tensor_key.startswith("image"): + data[tensor_key] = data[tensor_key].half() + + # mixup for imagenet finetuning + if mixup_fn is not None: + data["image"], data["label"] = mixup_fn(data["image"], data["label"]) + + if task in ["coco_captioning", "nocaps"]: + data["global_step"] = global_step + + if loss_scaler is None: + results = handler.train_batch(model, **data) + else: + with torch.cuda.amp.autocast(): + results = handler.train_batch(model, **data) + + loss = results.pop("loss") + loss_value = loss.item() + + if not math.isfinite(loss_value): + print("Loss is {}, stopping training".format(loss_value)) + sys.exit(1) + + if loss_scaler is None: + loss /= update_freq + model.backward(loss) + model.step() + + if (data_iter_step + 1) % update_freq == 0: + # model.zero_grad() + # Deepspeed will call step() & model.zero_grad() automatic + if model_ema is not None: + model_ema.update(model) + grad_norm = None + loss_scale_value = utils.get_loss_scale_for_deepspeed(model) + else: + # this attribute is added by timm on one optimizer (adahessian) + is_second_order = hasattr(optimizer, 'is_second_order') and optimizer.is_second_order + loss /= update_freq + grad_norm = loss_scaler(loss, optimizer, clip_grad=max_norm, + parameters=model.parameters(), create_graph=is_second_order, + update_grad=(data_iter_step + 1) % update_freq == 0) + if (data_iter_step + 1) % update_freq == 0: + optimizer.zero_grad() + if model_ema is not None: + model_ema.update(model) + loss_scale_value = loss_scaler.state_dict()["scale"] + + torch.cuda.synchronize() + + metric_logger.update(loss=loss_value) + metric_logger.update(loss_scale=loss_scale_value) + min_lr = 10. + max_lr = 0. + for group in optimizer.param_groups: + min_lr = min(min_lr, group["lr"]) + max_lr = max(max_lr, group["lr"]) + + metric_logger.update(lr=max_lr) + metric_logger.update(min_lr=min_lr) + weight_decay_value = None + for group in optimizer.param_groups: + if group["weight_decay"] > 0: + weight_decay_value = group["weight_decay"] + metric_logger.update(weight_decay=weight_decay_value) + metric_logger.update(grad_norm=grad_norm) + + if log_writer is not None: + kwargs = { + "loss": loss_value, + } + for key in results: + kwargs[key] = results[key] + log_writer.update(head="train", **kwargs) + + kwargs = { + "loss_scale": loss_scale_value, + "lr": max_lr, + "min_lr": min_lr, + "weight_decay": weight_decay_value, + "grad_norm": grad_norm, + } + log_writer.update(head="opt", **kwargs) + log_writer.set_step() + + # gather the stats from all processes + metric_logger.synchronize_between_processes() + print("Averaged stats:", metric_logger) + return {k: meter.global_avg for k, meter in metric_logger.meters.items()} + + +@torch.no_grad() +def evaluate(data_loader, model, device, handler): + metric_logger = utils.MetricLogger(delimiter=" ") + header = 'Test:' + + # switch to evaluation mode + model.eval() + handler.before_eval(metric_logger=metric_logger, data_loader=data_loader) + + for data in metric_logger.log_every(data_loader, 10, header): + for tensor_key in data.keys(): + data[tensor_key] = data[tensor_key].to(device, non_blocking=True) + + with torch.cuda.amp.autocast(): + handler.eval_batch(model=model, **data) + + # gather the stats from all processes + metric_logger.synchronize_between_processes() + + return handler.after_eval() diff --git a/py/evf-sam/model/unilm/beit3/get_started/get_started_for_captioning.md b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_captioning.md new file mode 100644 index 0000000..3b0f6bd --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_captioning.md @@ -0,0 +1,176 @@ +# Fine-tuning BEiT-3 on Image Captioning + +## COCO Captioning Setup + +1. [Setup environment](../README.md#setup). +2. Download [2014 train images](http://images.cocodataset.org/zips/train2014.zip), [2014 val images](http://images.cocodataset.org/zips/val2014.zip) and [karpathy split](https://cs.stanford.edu/people/karpathy/deepimagesent/caption_datasets.zip), then organize the dataset as following structure: + +``` +/path/to/your_data/ + train2014/ + COCO_train2014_000000000009.jpg + ... + val2014/ + COCO_val2014_000000000042.jpg + ... + dataset_coco.json +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import CaptioningDataset +from transformers import XLMRobertaTokenizer + +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") + +CaptioningDataset.make_coco_captioning_dataset_index( + data_path="/path/to/your_data", + tokenizer=tokenizer, +) +``` + + +## NoCaps Setup + +1. [Setup environment](README.md#setup). +2. Download [NoCaps val set](https://nocaps.s3.amazonaws.com/nocaps_val_4500_captions.json), [NoCaps test set](https://s3.amazonaws.com/nocaps/nocaps_test_image_info.json) and download imags using the urls in val and test json files, then organize the dataset as following structure: + +``` +/path/to/your_data/ + val/ + 09c863d76bcf6b00.jpg + ... + test/ + 19dc6913830a0a21.jpg + ... + nocaps_val_4500_captions.json + nocaps_test_image_info.json +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import CaptioningDataset +from transformers import XLMRobertaTokenizer + +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") + +CaptioningDataset.make_nocaps_captioning_dataset_index( + data_path="/path/to/your_data", +) +``` +We use COCO captioning training set as the training data of NoCaps. + + +## Example: Fine-tuning BEiT-3 on Captioning + +The BEiT-3 **base** model can be fine-tuned on captioning tasks using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_480 \ + --input_size 480 \ + --task coco_captioning \ + --batch_size 32 \ + --layer_decay 1.0 \ + --lr 4e-5 \ + --randaug \ + --epochs 10 \ + --warmup_epochs 1 \ + --drop_path 0.1 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.05 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --num_max_bpe_tokens 32 \ + --captioning_mask_prob 0.7 \ + --drop_worst_after 12000 \ + --dist_eval \ + --checkpoint_activations \ + --enable_deepspeed +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*32 = 256`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models). +- `--task`: **coco_captioning** for COCO captioning and **nocaps** for NoCaps dataset. +- `lr`: 4e-5 for COCO captioning and 1e-5 for NoCaps. +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory. + + +The BEiT-3 **large** model can be fine-tuned on captioning tasks using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_480 \ + --input_size 480 \ + --task coco_captioning \ + --batch_size 32 \ + --layer_decay 1.0 \ + --lr 8e-6 \ + --randaug \ + --epochs 10 \ + --warmup_epochs 1 \ + --drop_path 0.1 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.05 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --num_max_bpe_tokens 32 \ + --captioning_mask_prob 0.7 \ + --drop_worst_after 12000 \ + --dist_eval \ + --checkpoint_activations \ + --enable_deepspeed +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*32 = 256`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models). +- `--task`: **coco_captioning** for COCO captioning and **nocaps** for NoCaps dataset. +- `lr`: 8e-6 for COCO captioning and NoCaps. +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory. + + +## Example: Evaluate BEiT-3 Fine-tuned model on Captioning + +- Get the prediction file of the fine-tuned BEiT3-base model on captioning with 8 V100-32GB: +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_480 \ + --input_size 480 \ + --task coco_captioning \ + --batch_size 16 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_480_coco_captioning.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_prediction \ + --eval \ + --dist_eval +``` +- `--task`: **coco_captioning** for COCO captioning and **nocaps** for NoCaps dataset. +- `--finetune`: **beit3_base_patch16_480_coco_captioning.pth** for COCO captioning and **beit3_base_patch16_480_nocaps.pth** for NoCaps dataset. + +- Get the prediction file of the fine-tuned BEiT3-large model on captioning with 8 V100-32GB: +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_480 \ + --input_size 480 \ + --task coco_captioning \ + --batch_size 16 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_480_coco_captioning.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_prediction \ + --eval \ + --dist_eval +``` +- `--task`: **coco_captioning** for COCO captioning and **nocaps** for NoCaps dataset. +- `--finetune`: **beit3_large_patch16_480_coco_captioning.pth** for COCO captioning and **beit3_large_patch16_480_nocaps.pth** for NoCaps dataset. + +Please then submit the prediction file in the `output_dir` to the [evaluation server](https://eval.ai/web/challenges/challenge-page/355/overview) to obtain the NoCaps val and test results. diff --git a/py/evf-sam/model/unilm/beit3/get_started/get_started_for_image_classification.md b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_image_classification.md new file mode 100644 index 0000000..18b0231 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_image_classification.md @@ -0,0 +1,138 @@ +# Fine-tuning BEiT-3 on ImageNet-1k (Image Classification) + + +## Setup + +1. [Setup environment](../README.md#setup). +2. Download and extract ImageNet-1k from http://image-net.org/. + +The directory structure is the standard layout of torchvision's [`datasets.ImageFolder`](https://pytorch.org/docs/stable/torchvision/datasets.html#imagefolder). The training and validation data are expected to be in the `train/` folder and `val/` folder, respectively: + +``` +/path/to/imagenet/ + train/ + class1/ + img1.jpeg + class2/ + img2.jpeg + val/ + class1/ + img3.jpeg + class/2 + img4.jpeg +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import ImageNetDataset + +ImageNetDataset.make_dataset_index( + train_data_path = "/path/to/your_data/train", + val_data_path = "/path/to/your_data/val", + index_path = "/path/to/your_data" +) +``` + + +## Example: Fine-tuning BEiT-3 on ImageNet-1k (Image Classification) + +The BEiT-3 **base** model can be finetuned on ImageNet-1k using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_224 \ + --task imagenet \ + --batch_size 128 \ + --layer_decay 0.65 \ + --lr 7e-4 \ + --update_freq 1 \ + --epochs 50 \ + --warmup_epochs 5 \ + --drop_path 0.15 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.05 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --dist_eval \ + --mixup 0.8 \ + --cutmix 1.0 \ + --enable_deepspeed +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*128*1 = 1024`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models) +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. + + +The BEiT-3 **large** model can be finetuned on ImageNet-1k using a DGX box (8 V100-32GB): + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_224 \ + --task imagenet \ + --batch_size 128 \ + --layer_decay 0.8 \ + --lr 2e-4 \ + --update_freq 1 \ + --epochs 50 \ + --warmup_epochs 5 \ + --drop_path 0.25 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.05 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --dist_eval \ + --mixup 0.8 \ + --cutmix 1.0 \ + --enable_deepspeed \ + --checkpoint_activations +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*128 = 1024`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models) +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory + +## Example: Evaluate BEiT-3 Finetuned model on ImageNet-1k (Image Classification) + +- Evaluate our fine-tuned BEiT3-base model on ImageNet val with a single GPU: +```bash +python -m torch.distributed.launch --nproc_per_node=1 run_beit3_finetuning.py \ + --model beit3_base_patch16_224 \ + --task imagenet \ + --batch_size 128 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_224_in1k.pth \ + --data_path /path/to/your_data \ + --eval \ + --dist_eval +``` + +Expected results: +``` +* Acc@1 85.400 Acc@5 97.630 +``` + +- Evaluate our fine-tuned BEiT3-large model on ImageNet val with a single GPU: +```bash +python -m torch.distributed.launch --nproc_per_node=1 run_beit3_finetuning.py \ + --model beit3_large_patch16_224 \ + --task imagenet \ + --batch_size 128 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_224_in1k.pth \ + --data_path /path/to/your_data \ + --eval \ + --dist_eval +``` + +Expected results: +``` +* Acc@1 87.580 Acc@5 98.326 +``` diff --git a/py/evf-sam/model/unilm/beit3/get_started/get_started_for_nlvr2.md b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_nlvr2.md new file mode 100644 index 0000000..68e482e --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_nlvr2.md @@ -0,0 +1,136 @@ +# Fine-tuning BEiT-3 on NLVR2 (Visual Reasoning) + + +## Setup + +1. [Setup environment](../README.md#setup). +2. Clone the [repository](https://github.com/lil-lab/nlvr) and sign the [request form](https://goo.gl/forms/yS29stWnFWzrDBFH3) to download the images, then organize the dataset as following structure: + +``` +/path/to/your_data/ + images/train/ + 0/train-11670-0-img0.png + ... + dev/ + dev-269-0-img0.png + ... + test1/ + test1-261-0-img0.png + ... + nlvr/ (nlvr repo) + nlvr/ + nlvr2/ +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import NLVR2Dataset +from transformers import XLMRobertaTokenizer + +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") + +NLVR2Dataset.make_dataset_index( + data_path="/path/to/your_data", + tokenizer=tokenizer, + nlvr_repo_path="/path/to/your_data/nlvr" +) +``` + + +## Example: Fine-tuning BEiT-3 on NLVR2 (Visual Reasoning) + +The BEiT-3 **base** model can be finetuned on NLVR2 using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_224 \ + --task nlvr2 \ + --batch_size 32 \ + --layer_decay 0.65 \ + --lr 7e-4 \ + --epochs 20 \ + --warmup_epochs 5 \ + --drop_path 0.2 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.2 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --enable_deepspeed +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*32 = 256`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models). +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. +- `--lr`: 7e-4 for `BEiT3-base`, 5e-4 for `BEiT3-base-indomain`. + + +The BEiT-3 **large** model can be finetuned on NLVR2 using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_224 \ + --task nlvr2 \ + --batch_size 32 \ + --layer_decay 0.85 \ + --lr 3e-4 \ + --epochs 20 \ + --warmup_epochs 5 \ + --drop_path 0.2 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.2 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --enable_deepspeed \ + --checkpoint_activations +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*32 = 256`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models). +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. +- `--lr`: 3e-4 for `BEiT3-large`, 1e-4 for `BEiT3-large-indomain`. +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory. + + +## Example: Evaluate BEiT-3 Finetuned model on NLVR2 (Visual Reasoning) + +- Get the result of our fine-tuned BEiT3-base model on NLVR2 test with 8 V100-32GB: +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_224 \ + --task nlvr2 \ + --batch_size 32 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_224_nlvr2.pth \ + --data_path /path/to/your_data \ + --eval \ + --dist_eval +``` + +Expected results: +``` +* Acc 84.386 +``` + +- Get the result of our fine-tuned BEiT3-large model on NLVR2 test with 8 V100-32GB: +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_224 \ + --task nlvr2 \ + --batch_size 32 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_224_nlvr2.pth \ + --data_path /path/to/your_data \ + --eval \ + --dist_eval +``` + +Expected results: +``` +* Acc 89.437 +``` diff --git a/py/evf-sam/model/unilm/beit3/get_started/get_started_for_retrieval.md b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_retrieval.md new file mode 100644 index 0000000..a2ef2c1 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_retrieval.md @@ -0,0 +1,161 @@ +# Fine-tuning BEiT-3 on Image-text Retrieval + +## COCO Retrieval Setup + +1. [Setup environment](../README.md#setup). +2. Download [2014 train images](http://images.cocodataset.org/zips/train2014.zip), [2014 val images](http://images.cocodataset.org/zips/val2014.zip) and [karpathy split](https://cs.stanford.edu/people/karpathy/deepimagesent/caption_datasets.zip), then organize the dataset as following structure: + +``` +/path/to/your_data/ + train2014/ + COCO_train2014_000000000009.jpg + ... + val2014/ + COCO_val2014_000000000042.jpg + ... + dataset_coco.json +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import RetrievalDataset +from transformers import XLMRobertaTokenizer + +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") + +RetrievalDataset.make_coco_dataset_index( + data_path="/path/to/your_data", + tokenizer=tokenizer, +) +``` + + +## Flickr30k Retrieval Setup + +1. [Setup environment](README.md#setup). +2. Sign [flickr images request form](https://forms.illinois.edu/sec/229675) and download [karpathy split](https://cs.stanford.edu/people/karpathy/deepimagesent/caption_datasets.zip), then organize the dataset as following structure: + +``` +/path/to/your_data/ + flickr30k-images/ + 2923475135.jpg + ... + dataset_flickr30k.json +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import RetrievalDataset +from transformers import XLMRobertaTokenizer + +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") + +RetrievalDataset.make_flickr30k_dataset_index( + data_path="/path/to/your_data", + tokenizer=tokenizer, + karpathy_path="/path/to/your_data", +) +``` + + +## Example: Fine-tuning BEiT-3 on Retrieval + +The BEiT-3 **base** model can be finetuned on retrieval tasks using 16 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=16 run_beit3_finetuning.py \ + --model beit3_base_patch16_384 \ + --input_size 384 \ + --task coco_retrieval \ + --batch_size 192 \ + --layer_decay 0.65 \ + --lr 2e-4 \ + --epochs 15 \ + --warmup_epochs 3 \ + --drop_path 0.2 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_itc_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.05 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --enable_deepspeed \ + --checkpoint_activations +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `192*16 = 3072`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models) +- `--task`: **coco_retrieval** for COCO retrieval, **flickr30k** for Flickr30k retrieval +- `--lr`: 2e-4 for COCO retrieval, 1e-4 for Flickr30k retrieval +- `--epochs`: 15 for COCO retrieval, 20 for Flickr30k retrieval +- `--warmup_epochs`: 3 for COCO retrieval, 5 for Flickr30k retrieval +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory + + +The BEiT-3 **large** model can be finetuned on retrieval tasks using 2x16 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=16 --nnodes=2 --node_rank=$NODE_RANK \ + --master_addr=$MASTER_ADDR --master_port=$MASTER_PORT run_beit3_finetuning.py \ + --model beit3_large_patch16_384 \ + --input_size 384 \ + --task coco_retrieval \ + --batch_size 96 \ + --layer_decay 0.85 \ + --lr 5e-5 \ + --epochs 15 \ + --warmup_epochs 3 \ + --drop_path 0.2 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_itc_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.05 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --enable_deepspeed \ + --checkpoint_activations +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `96*32 = 3072`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models) +- `--task`: **coco_retrieval** for COCO retrieval, **flickr30k** for Flickr30k retrieval +- `--epochs`: 15 for COCO retrieval, 20 for Flickr30k retrieval +- `--warmup_epochs`: 3 for COCO retrieval, 5 for Flickr30k retrieval +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory + + +## Example: Evaluate BEiT-3 Fine-tuned model on COCO Retrieval and Flickr30k Retrieval + +- Get the results of our fine-tuned BEiT3-base model on retrieval tasks using a single GPU: +```bash +python -m torch.distributed.launch --nproc_per_node=1 run_beit3_finetuning.py \ + --model beit3_base_patch16_384 \ + --input_size 384 \ + --task coco_retrieval \ + --batch_size 16 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_384_coco_retrieval.pth \ + --data_path /path/to/your_data \ + --eval \ + --dist_eval +``` +- `--task`: **coco_retrieval** for COCO retrieval, **flickr30k** for Flickr30k retrieval +- `--finetune`: **beit3_base_patch16_384_coco_retrieval.pth** for COCO retrieval, **beit3_base_patch16_384_f30k_retrieval.pth** for Flickr30k retrieval + +- Get the results of our fine-tuned BEiT3-large model on retrieval tasks using a single GPU: +```bash +python -m torch.distributed.launch --nproc_per_node=1 run_beit3_finetuning.py \ + --model beit3_large_patch16_384 \ + --input_size 384 \ + --task coco_retrieval \ + --batch_size 16 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_384_coco_retrieval.pth \ + --data_path /path/to/your_data \ + --eval \ + --dist_eval +``` +- `--task`: **coco_retrieval** for COCO retrieval, **flickr30k** for Flickr30k retrieval +- `--finetune`: **beit3_large_patch16_384_coco_retrieval.pth** for COCO retrieval, **beit3_large_patch16_384_f30k_retrieval.pth** for Flickr30k retrieval diff --git a/py/evf-sam/model/unilm/beit3/get_started/get_started_for_vqav2.md b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_vqav2.md new file mode 100644 index 0000000..b834b07 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/get_started/get_started_for_vqav2.md @@ -0,0 +1,144 @@ +# Fine-tuning BEiT-3 on VQAv2 (Visual Question Answering) + + +## Setup + +1. [Setup environment](../README.md#setup). +2. Download COCO [2014 train images](http://images.cocodataset.org/zips/train2014.zip), [2014 val images](http://images.cocodataset.org/zips/val2014.zip), [2015 test images](http://images.cocodataset.org/zips/test2015.zip), annotations ([train](https://s3.amazonaws.com/cvmlp/vqa/mscoco/vqa/v2_Annotations_Train_mscoco.zip), [val](https://s3.amazonaws.com/cvmlp/vqa/mscoco/vqa/v2_Annotations_Val_mscoco.zip)), and questions ([train](https://s3.amazonaws.com/cvmlp/vqa/mscoco/vqa/v2_Questions_Train_mscoco.zip), [val](https://s3.amazonaws.com/cvmlp/vqa/mscoco/vqa/v2_Questions_Val_mscoco.zip), [test](https://s3.amazonaws.com/cvmlp/vqa/mscoco/vqa/v2_Questions_Test_mscoco.zip)), then organize the dataset as following structure: + +``` +/path/to/your_data/ + train2014/ + COCO_train2014_000000000009.jpg + ... + val2014/ + COCO_val2014_000000000042.jpg + ... + test2015/ + COCO_test2015_000000000001.jpg + ... + vqa/ + v2_OpenEnded_mscoco_train2014_questions.json + v2_OpenEnded_mscoco_val2014_questions.json + v2_OpenEnded_mscoco_test2015_questions.json + v2_OpenEnded_mscoco_test-dev2015_questions.json + v2_mscoco_train2014_annotations.json + v2_mscoco_val2014_annotations.json +``` + +We then generate the index json files using the following command. [beit3.spm](https://github.com/addf400/files/releases/download/beit3/beit3.spm) is the sentencepiece model used for tokenizing texts. +``` +from datasets import VQAv2Dataset +from transformers import XLMRobertaTokenizer + +tokenizer = XLMRobertaTokenizer("/your_beit3_model_path/beit3.spm") + +VQAv2Dataset.make_dataset_index( + data_path="/path/to/your_data", + tokenizer=tokenizer, + annotation_data_path="/path/to/your_data/vqa", +) +``` + + +## Example: Fine-tuning BEiT-3 on VQAv2 (Visual Question Answering) + +The BEiT-3 **base** model can be finetuned on VQAv2 using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_480 \ + --input_size 480 \ + --task vqav2 \ + --batch_size 16 \ + --layer_decay 1.0 \ + --lr 3e-5 \ + --update_freq 1 \ + --randaug \ + --epochs 10 \ + --warmup_epochs 1 \ + --drop_path 0.1 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.01 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --task_head_lr_weight 20 \ + --opt_betas 0.9 0.98 \ + --enable_deepspeed +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*16 = 128`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models) +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. + + +The BEiT-3 **large** model can be finetuned on VQAv2 using 8 V100-32GB: + +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_480 \ + --input_size 480 \ + --task vqav2 \ + --batch_size 16 \ + --layer_decay 1.0 \ + --lr 2e-5 \ + --update_freq 1 \ + --randaug \ + --epochs 10 \ + --warmup_epochs 1 \ + --drop_path 0.15 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_224.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_model \ + --log_dir /path/to/save/your_model/log \ + --weight_decay 0.01 \ + --seed 42 \ + --save_ckpt_freq 5 \ + --task_head_lr_weight 20 \ + --opt_betas 0.9 0.98 \ + --enable_deepspeed \ + --checkpoint_activations +``` +- `--batch_size`: batch size per GPU. Effective batch size = `number of GPUs` * `--batch_size` * `--update_freq`. So in the above example, the effective batch size is `8*16 = 128`. +- `--finetune`: weight path of your pretrained models; please download the pretrained model weights in [README.md](../README.md#pretrained-models) +- `--enable_deepspeed`: optional. If you use apex, please enable deepspeed. +- `--checkpoint_activations`: using gradient checkpointing for saving GPU memory + + +## Example: Evaluate BEiT-3 Finetuned model on VQAv2 (Visual Question Answering) + +- Get the prediction file of the fine-tuned BEiT3-base model on VQAv2 test with 8 V100-32GB: +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_base_patch16_480 \ + --input_size 480 \ + --task vqav2 \ + --batch_size 16 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_base_patch16_480_vqa.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_prediction \ + --eval \ + --dist_eval +``` + +- Get the prediction file of the fine-tuned BEiT3-large model on VQAv2 test with 8 V100-32GB: +```bash +python -m torch.distributed.launch --nproc_per_node=8 run_beit3_finetuning.py \ + --model beit3_large_patch16_480 \ + --input_size 480 \ + --task vqav2 \ + --batch_size 16 \ + --sentencepiece_model /your_beit3_model_path/beit3.spm \ + --finetune /your_beit3_model_path/beit3_large_patch16_480_vqa.pth \ + --data_path /path/to/your_data \ + --output_dir /path/to/save/your_prediction \ + --eval \ + --dist_eval +``` + +Please then submit the prediction file in the `output_dir` to the [evaluation server](https://eval.ai/web/challenges/challenge-page/830/overview) to obtain the VQAv2 test-dev and test-std results. diff --git a/py/evf-sam/model/unilm/beit3/glossary.py b/py/evf-sam/model/unilm/beit3/glossary.py new file mode 100644 index 0000000..81fddf3 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/glossary.py @@ -0,0 +1,190 @@ +import re + +contractions = { + "aint": "ain't", + "arent": "aren't", + "cant": "can't", + "couldve": "could've", + "couldnt": "couldn't", + "couldn'tve": "couldn't've", + "couldnt've": "couldn't've", + "didnt": "didn't", + "doesnt": "doesn't", + "dont": "don't", + "hadnt": "hadn't", + "hadnt've": "hadn't've", + "hadn'tve": "hadn't've", + "hasnt": "hasn't", + "havent": "haven't", + "hed": "he'd", + "hed've": "he'd've", + "he'dve": "he'd've", + "hes": "he's", + "howd": "how'd", + "howll": "how'll", + "hows": "how's", + "Id've": "I'd've", + "I'dve": "I'd've", + "Im": "I'm", + "Ive": "I've", + "isnt": "isn't", + "itd": "it'd", + "itd've": "it'd've", + "it'dve": "it'd've", + "itll": "it'll", + "let's": "let's", + "maam": "ma'am", + "mightnt": "mightn't", + "mightnt've": "mightn't've", + "mightn'tve": "mightn't've", + "mightve": "might've", + "mustnt": "mustn't", + "mustve": "must've", + "neednt": "needn't", + "notve": "not've", + "oclock": "o'clock", + "oughtnt": "oughtn't", + "ow's'at": "'ow's'at", + "'ows'at": "'ow's'at", + "'ow'sat": "'ow's'at", + "shant": "shan't", + "shed've": "she'd've", + "she'dve": "she'd've", + "she's": "she's", + "shouldve": "should've", + "shouldnt": "shouldn't", + "shouldnt've": "shouldn't've", + "shouldn'tve": "shouldn't've", + "somebody'd": "somebodyd", + "somebodyd've": "somebody'd've", + "somebody'dve": "somebody'd've", + "somebodyll": "somebody'll", + "somebodys": "somebody's", + "someoned": "someone'd", + "someoned've": "someone'd've", + "someone'dve": "someone'd've", + "someonell": "someone'll", + "someones": "someone's", + "somethingd": "something'd", + "somethingd've": "something'd've", + "something'dve": "something'd've", + "somethingll": "something'll", + "thats": "that's", + "thered": "there'd", + "thered've": "there'd've", + "there'dve": "there'd've", + "therere": "there're", + "theres": "there's", + "theyd": "they'd", + "theyd've": "they'd've", + "they'dve": "they'd've", + "theyll": "they'll", + "theyre": "they're", + "theyve": "they've", + "twas": "'twas", + "wasnt": "wasn't", + "wed've": "we'd've", + "we'dve": "we'd've", + "weve": "we've", + "werent": "weren't", + "whatll": "what'll", + "whatre": "what're", + "whats": "what's", + "whatve": "what've", + "whens": "when's", + "whered": "where'd", + "wheres": "where's", + "whereve": "where've", + "whod": "who'd", + "whod've": "who'd've", + "who'dve": "who'd've", + "wholl": "who'll", + "whos": "who's", + "whove": "who've", + "whyll": "why'll", + "whyre": "why're", + "whys": "why's", + "wont": "won't", + "wouldve": "would've", + "wouldnt": "wouldn't", + "wouldnt've": "wouldn't've", + "wouldn'tve": "wouldn't've", + "yall": "y'all", + "yall'll": "y'all'll", + "y'allll": "y'all'll", + "yall'd've": "y'all'd've", + "y'alld've": "y'all'd've", + "y'all'dve": "y'all'd've", + "youd": "you'd", + "youd've": "you'd've", + "you'dve": "you'd've", + "youll": "you'll", + "youre": "you're", + "youve": "you've", +} + +manual_map = { + "none": "0", + "zero": "0", + "one": "1", + "two": "2", + "three": "3", + "four": "4", + "five": "5", + "six": "6", + "seven": "7", + "eight": "8", + "nine": "9", + "ten": "10", +} +articles = ["a", "an", "the"] +period_strip = re.compile("(?!<=\d)(\.)(?!\d)") +comma_strip = re.compile("(\d)(\,)(\d)") +punct = [ + ";", + r"/", + "[", + "]", + '"', + "{", + "}", + "(", + ")", + "=", + "+", + "\\", + "_", + "-", + ">", + "<", + "@", + "`", + ",", + "?", + "!", +] + + +def normalize_word(token): + _token = token + for p in punct: + if (p + " " in token or " " + p in token) or ( + re.search(comma_strip, token) != None + ): + _token = _token.replace(p, "") + else: + _token = _token.replace(p, " ") + token = period_strip.sub("", _token, re.UNICODE) + + _token = [] + temp = token.lower().split() + for word in temp: + word = manual_map.setdefault(word, word) + if word not in articles: + _token.append(word) + for i, word in enumerate(_token): + if word in contractions: + _token[i] = contractions[word] + token = " ".join(_token) + token = token.replace(",", "") + return token diff --git a/py/evf-sam/model/unilm/beit3/modeling_finetune.py b/py/evf-sam/model/unilm/beit3/modeling_finetune.py new file mode 100644 index 0000000..ba63fad --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/modeling_finetune.py @@ -0,0 +1,386 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +import torch +import torch.nn as nn +import torch.nn.functional as F +from timm.models.registry import register_model +import numpy as np + +import utils +from modeling_utils import BEiT3Wrapper, _get_base_config, _get_large_config + + +class TwoLayerMLP(nn.Module): + def __init__( + self, + in_features, + hidden_features, + out_features, + norm_layer, + norm_input=True, + ): + super().__init__() + self.norm1 = norm_layer(in_features) if norm_input else nn.Identity() + self.dense1 = nn.Linear(in_features, hidden_features) + self.norm2 = norm_layer(hidden_features) + self.act = nn.GELU() + self.dense2 = nn.Linear(hidden_features, out_features) + + def forward(self, x): + x = self.norm1(x) + x = self.dense1(x) + x = self.norm2(x) + x = self.act(x) + return self.dense2(x) + + +class Pooler(nn.Module): + def __init__(self, input_features, output_features, norm_layer): + super().__init__() + self.norm = norm_layer(input_features) + self.dense = nn.Linear(input_features, output_features) + self.activation = nn.Tanh() + + def forward(self, x): + cls_rep = x[:, 0, :] + cls_rep = self.norm(cls_rep) + pooled_output = self.dense(cls_rep) + pooled_output = self.activation(pooled_output) + return pooled_output + + +class BEiT3ForVisualReasoning(BEiT3Wrapper): + def __init__( + self, + args, + num_classes, + norm_layer=nn.LayerNorm, + **kwargs + ): + super(BEiT3ForVisualReasoning, self).__init__(args=args) + embed_dim = args.encoder_embed_dim + self.head = TwoLayerMLP( + in_features=embed_dim * 4, + hidden_features=embed_dim * 2, + out_features=num_classes, + norm_layer=norm_layer, + ) + init_scale = 0.001 + self.head.apply(self._init_weights) + if isinstance(self.head.dense1, nn.Linear): + self.head.dense1.weight.data.mul_(init_scale) + self.head.dense1.bias.data.mul_(init_scale) + + if isinstance(self.head.dense2, nn.Linear): + self.head.dense2.weight.data.mul_(init_scale) + self.head.dense2.bias.data.mul_(init_scale) + + def forward(self, image_a, image_b, text_description, padding_mask, **kwargs): + bsz, _ = text_description.size() + + vision_input = torch.cat((image_a, image_b), dim=0) + language_input = torch.cat((text_description, text_description), dim=0) + padding_mask = torch.cat((padding_mask, padding_mask), dim=0) + + outputs = self.beit3( + textual_tokens=language_input, + visual_tokens=vision_input, + text_padding_position=padding_mask, + ) + x = outputs["encoder_out"] + multiway_split_position = outputs["multiway_split_position"] + + vision_cls = x[:, 0, :] + language_cls = x[:, multiway_split_position, :] + cls_rep = torch.cat((vision_cls, language_cls), dim=-1) + a, b = torch.split(cls_rep, split_size_or_sections=[bsz, bsz], dim=0) + cls_rep = torch.cat((a, b), dim=-1) + return self.head(cls_rep) + + +class BEiT3ForImageClassification(BEiT3Wrapper): + def __init__( + self, + args, + num_classes, + norm_layer=nn.LayerNorm, + **kwargs + ): + super(BEiT3ForImageClassification, self).__init__(args=args) + embed_dim = args.encoder_embed_dim + self.fc_norm = norm_layer(embed_dim) + self.head = nn.Linear(embed_dim, num_classes) if num_classes > 0 else nn.Identity() + + self.fc_norm.apply(self._init_weights) + self.head.apply(self._init_weights) + init_scale = 0.001 + if isinstance(self.head, nn.Linear): + self.head.weight.data.mul_(init_scale) + self.head.bias.data.mul_(init_scale) + + def forward(self, image, **kwargs): + x = self.beit3(textual_tokens=None, visual_tokens=image)["encoder_out"] + t = x[:, 1:, :] + cls_x = self.fc_norm(t.mean(1)) + return self.head(cls_x) + + +class BEiT3ForCaptioning(BEiT3Wrapper): + def __init__( + self, + args, + **kwargs + ): + super(BEiT3ForCaptioning, self).__init__(args=args) + embed_dim = args.encoder_embed_dim + self.mlm_head = nn.Linear(embed_dim, args.vocab_size) + self.mlm_head.apply(self._init_weights) + + def forward(self, image, text_ids, padding_mask, language_masked_pos, text_len=None, incremental_state=None, **kwargs): + text_len = text_len if text_len is not None else text_ids.size(1) + image_len = self.beit3.vision_embed.num_position_embeddings() + max_len = text_len + image_len + uni_mask = torch.zeros((max_len, max_len), dtype=torch.long, device=text_ids.device) + i_start, i_end = 0, image_len + t_start, t_end = image_len, max_len + # triangle mask for caption to caption + uni_mask[t_start:t_end, t_start:t_end] = torch.tril(torch.ones(text_len, text_len, dtype=torch.long, device=text_ids.device)) + # full attention for caption to image + uni_mask[t_start:t_end, i_start:i_end] = 1 + # full attention for image to image + uni_mask[i_start:i_end, i_start:i_end] = 1 + uni_mask = 1-uni_mask + + if incremental_state is not None: + for idx in range(self.get_num_layers()): + if idx not in incremental_state: + incremental_state[idx] = {} + + # for incremental decoding + positions = None + if image is None: + uni_mask = uni_mask[-2:] + padding_mask = None + # start position (2 (fairseq starts at 2) + cur_position) is equal to text_len + positions = torch.arange(text_len, text_ids.size(1) + text_len, device=text_ids.device).long().unsqueeze(0) + + outputs = self.beit3( + textual_tokens=text_ids, + visual_tokens=image, + text_padding_position=padding_mask, + attn_mask=uni_mask, + incremental_state=incremental_state, + positions=positions, + ) + if image is not None: + text_feats = outputs["encoder_out"][:, image_len:] + else: + text_feats = outputs["encoder_out"] + + if language_masked_pos is not None: + text_feats = text_feats[language_masked_pos.bool()] + + return self.mlm_head(text_feats), incremental_state + + +class BEiT3ForVisualQuestionAnswering(BEiT3Wrapper): + def __init__( + self, + args, + num_classes, + norm_layer=nn.LayerNorm, + **kwargs + ): + super(BEiT3ForVisualQuestionAnswering, self).__init__(args=args) + embed_dim = args.encoder_embed_dim + self.pooler = Pooler( + input_features=embed_dim, + output_features=embed_dim, + norm_layer=norm_layer, + ) + self.pooler.apply(self._init_weights) + self.head = nn.Sequential( + nn.Linear(embed_dim, embed_dim * 2), + norm_layer(embed_dim * 2), + nn.GELU(), + nn.Linear(embed_dim * 2, num_classes), + ) + self.head.apply(self._init_weights) + + def forward(self, image, question, padding_mask, **kwargs): + outputs = self.beit3( + textual_tokens=question, + visual_tokens=image, + text_padding_position=padding_mask, + ) + x = outputs["encoder_out"] + cls_rep = self.pooler(x) + return self.head(cls_rep) + + +class BEiT3ForRetrieval(BEiT3Wrapper): + def __init__( + self, + args, + **kwargs + ): + super(BEiT3ForRetrieval, self).__init__(args=args) + embed_dim = args.encoder_embed_dim + self.language_head = nn.Linear(embed_dim, embed_dim, bias=False) + self.vision_head = nn.Linear(embed_dim, embed_dim, bias=False) + self.language_head.apply(self._init_weights) + self.vision_head.apply(self._init_weights) + self.criterion = utils.ClipLoss( + rank=utils.get_rank(), + world_size=utils.get_world_size(), + ) + self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07)) + + def forward(self, image=None, text_description=None, padding_mask=None, only_infer=False, **kwargs): + if image is not None: + outputs = self.beit3( + textual_tokens=None, + visual_tokens=image, + text_padding_position=None, + ) + x = outputs["encoder_out"] + vision_cls = self.vision_head(x[:, 0, :]) + vision_cls = F.normalize(vision_cls, dim=-1) + else: + vision_cls = None + + if text_description is not None: + outputs = self.beit3( + textual_tokens=text_description, + visual_tokens=None, + text_padding_position=padding_mask, + ) + x = outputs["encoder_out"] + language_cls = self.language_head(x[:, 0, :]) + language_cls = F.normalize(language_cls, dim=-1) + else: + language_cls = None + + if only_infer: + return vision_cls, language_cls + else: + loss, logits_per_image, logits_per_text = self.criterion( + vision_cls, language_cls, self.logit_scale.exp()) + return loss, vision_cls, language_cls + + +@register_model +def beit3_base_patch16_224_imageclassification(pretrained=False, **kwargs): + args = _get_base_config(**kwargs) + args.normalize_output = False + model = BEiT3ForImageClassification(args, num_classes=1000, **kwargs) + return model + + +@register_model +def beit3_large_patch16_224_imageclassification(pretrained=False, **kwargs): + args = _get_large_config(**kwargs) + args.normalize_output = False + model = BEiT3ForImageClassification(args, num_classes=1000, **kwargs) + return model + + +@register_model +def beit3_base_patch16_224_nlvr2(pretrained=False, **kwargs): + args = _get_base_config(**kwargs) + model = BEiT3ForVisualReasoning(args, num_classes=2, **kwargs) + return model + + +@register_model +def beit3_large_patch16_224_nlvr2(pretrained=False, **kwargs): + args = _get_large_config(**kwargs) + model = BEiT3ForVisualReasoning(args, num_classes=2, **kwargs) + return model + + +@register_model +def beit3_base_patch16_384_vqav2(pretrained=False, **kwargs): + args = _get_base_config(img_size=384, **kwargs) + args.normalize_output = False + model = BEiT3ForVisualQuestionAnswering(args, num_classes=3129, **kwargs) + return model + + +@register_model +def beit3_base_patch16_480_vqav2(pretrained=False, **kwargs): + args = _get_base_config(img_size=480, **kwargs) + args.normalize_output = False + model = BEiT3ForVisualQuestionAnswering(args, num_classes=3129, **kwargs) + return model + + +@register_model +def beit3_large_patch16_384_vqav2(pretrained=False, **kwargs): + args = _get_large_config(img_size=384, **kwargs) + args.normalize_output = False + model = BEiT3ForVisualQuestionAnswering(args, num_classes=3129, **kwargs) + return model + + +@register_model +def beit3_large_patch16_480_vqav2(pretrained=False, **kwargs): + args = _get_large_config(img_size=480, **kwargs) + args.normalize_output = False + model = BEiT3ForVisualQuestionAnswering(args, num_classes=3129, **kwargs) + return model + + +@register_model +def beit3_large_patch16_768_vqav2(pretrained=False, **kwargs): + args = _get_large_config(img_size=768, **kwargs) + args.normalize_output = False + model = BEiT3ForVisualQuestionAnswering(args, num_classes=3129, **kwargs) + return model + + +@register_model +def beit3_base_patch16_224_captioning(pretrained=False, **kwargs): + args = _get_base_config(**kwargs) + model = BEiT3ForCaptioning(args, **kwargs) + return model + + +@register_model +def beit3_base_patch16_480_captioning(pretrained=False, **kwargs): + args = _get_base_config(img_size=480, **kwargs) + model = BEiT3ForCaptioning(args, **kwargs) + return model + + +@register_model +def beit3_large_patch16_480_captioning(pretrained=False, **kwargs): + args = _get_large_config(img_size=480, **kwargs) + model = BEiT3ForCaptioning(args, **kwargs) + return model + + +@register_model +def beit3_base_patch16_224_retrieval(pretrained=False, **kwargs): + args = _get_base_config(**kwargs) + model = BEiT3ForRetrieval(args, **kwargs) + return model + + +@register_model +def beit3_base_patch16_384_retrieval(pretrained=False, **kwargs): + args = _get_base_config(img_size=384, **kwargs) + model = BEiT3ForRetrieval(args, **kwargs) + return model + + +@register_model +def beit3_large_patch16_384_retrieval(pretrained=False, **kwargs): + args = _get_large_config(img_size=384, **kwargs) + model = BEiT3ForRetrieval(args, **kwargs) + return model diff --git a/py/evf-sam/model/unilm/beit3/modeling_utils.py b/py/evf-sam/model/unilm/beit3/modeling_utils.py new file mode 100644 index 0000000..6581159 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/modeling_utils.py @@ -0,0 +1,76 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +import math +import torch +import torch.nn as nn +from timm.models.layers import trunc_normal_ as __call_trunc_normal_ + +from torchscale.model.BEiT3 import BEiT3 +from torchscale.architecture.config import EncoderConfig + + +def trunc_normal_(tensor, mean=0., std=1.): + __call_trunc_normal_(tensor, mean=mean, std=std, a=-std, b=std) + + +def _get_base_config( + img_size=224, patch_size=16, drop_path_rate=0, + checkpoint_activations=None, mlp_ratio=4, vocab_size=64010, **kwargs +): + return EncoderConfig( + img_size=img_size, patch_size=patch_size, vocab_size=vocab_size, multiway=True, + layernorm_embedding=False, normalize_output=True, no_output_layer=True, + drop_path_rate=drop_path_rate, encoder_embed_dim=768, encoder_attention_heads=12, + encoder_ffn_embed_dim=int(768 * mlp_ratio), encoder_layers=12, + checkpoint_activations=checkpoint_activations, + ) + + +def _get_large_config( + img_size=224, patch_size=16, drop_path_rate=0, + checkpoint_activations=None, mlp_ratio=4, vocab_size=64010, **kwargs +): + return EncoderConfig( + img_size=img_size, patch_size=patch_size, vocab_size=vocab_size, multiway=True, + layernorm_embedding=False, normalize_output=True, no_output_layer=True, + drop_path_rate=drop_path_rate, encoder_embed_dim=1024, encoder_attention_heads=16, + encoder_ffn_embed_dim=int(1024 * mlp_ratio), encoder_layers=24, + checkpoint_activations=checkpoint_activations, + ) + + +class BEiT3Wrapper(nn.Module): + def __init__(self, args, **kwargs): + super().__init__() + self.args = args + self.beit3 = BEiT3(args) + self.apply(self._init_weights) + + def fix_init_weight(self): + def rescale(param, layer_id): + param.div_(math.sqrt(2.0 * layer_id)) + + for layer_id, layer in enumerate(self.blocks): + rescale(layer.attn.proj.weight.data, layer_id + 1) + rescale(layer.mlp.fc2.weight.data, layer_id + 1) + + def get_num_layers(self): + return self.beit3.encoder.num_layers + + @torch.jit.ignore + def no_weight_decay(self): + return {'pos_embed', 'cls_token', 'beit3.encoder.embed_positions.A.weight', 'beit3.vision_embed.cls_token', 'logit_scale'} + + def _init_weights(self, m): + if isinstance(m, nn.Linear): + trunc_normal_(m.weight, std=.02) + if isinstance(m, nn.Linear) and m.bias is not None: + nn.init.constant_(m.bias, 0) + elif isinstance(m, nn.LayerNorm): + nn.init.constant_(m.bias, 0) + nn.init.constant_(m.weight, 1.0) diff --git a/py/evf-sam/model/unilm/beit3/optim_factory.py b/py/evf-sam/model/unilm/beit3/optim_factory.py new file mode 100644 index 0000000..a3d32bb --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/optim_factory.py @@ -0,0 +1,128 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +from torch import optim as optim +from timm.optim.lookahead import Lookahead + +import json + + +def get_num_layer_for_vit(var_name, num_max_layer): + if "embed" in var_name: + return 0 + elif var_name in ( + "cls_token", "mask_token", "pos_embed", "language_pos_embed", + "word_embeddings.weight", "vision_cls_token", "vision_pos_embed" + ): + return 0 + elif var_name.startswith("patch_embed"): + return 0 + elif var_name.startswith("rel_pos_bias"): + return num_max_layer - 1 + elif "layers." in var_name: + layer_id = int(var_name.split('layers.')[1].split('.')[0]) + return layer_id + 1 + else: + return num_max_layer - 1 + + +def get_is_head_flag_for_vit(var_name, num_max_layer): + if var_name.startswith("head"): + return 1 + # elif var_name.startswith("pooler"): + # return 1 + else: + return 0 + + +class LayerDecayValueAssigner(object): + def __init__(self, values, scale_handler=None): + self.scale_handler = scale_handler or get_num_layer_for_vit + self.values = values + + def get_scale(self, layer_id): + return self.values[layer_id] + + def get_layer_id(self, var_name): + return self.scale_handler(var_name, len(self.values)) + + +# The implementation code is modified from Timm (https://github.com/huggingface/pytorch-image-models/tree/main/timm +def get_parameter_groups(model, weight_decay=1e-5, skip_list=(), get_num_layer=None, get_layer_scale=None): + parameter_group_names = {} + parameter_group_vars = {} + + for name, param in model.named_parameters(): + if not param.requires_grad: + continue # frozen weights + if len(param.shape) == 1 or name.endswith(".bias") or name in skip_list: + group_name = "no_decay" + this_weight_decay = 0. + else: + group_name = "decay" + this_weight_decay = weight_decay + if get_num_layer is not None: + layer_id = get_num_layer(name) + group_name = "layer_%d_%s" % (layer_id, group_name) + else: + layer_id = None + + if group_name not in parameter_group_names: + if get_layer_scale is not None: + scale = get_layer_scale(layer_id) + else: + scale = 1. + + parameter_group_names[group_name] = { + "weight_decay": this_weight_decay, + "params": [], + "lr_scale": scale + } + parameter_group_vars[group_name] = { + "weight_decay": this_weight_decay, + "params": [], + "lr_scale": scale + } + + parameter_group_vars[group_name]["params"].append(param) + parameter_group_names[group_name]["params"].append(name) + print("Param groups = %s" % json.dumps(parameter_group_names, indent=2)) + return list(parameter_group_vars.values()) + + +def create_optimizer(args, model, get_num_layer=None, get_layer_scale=None, filter_bias_and_bn=True, skip_list=None): + opt_lower = args.opt.lower() + weight_decay = args.weight_decay + if weight_decay and filter_bias_and_bn: + skip = {} + if skip_list is not None: + skip = skip_list + elif hasattr(model, 'no_weight_decay'): + skip = model.no_weight_decay() + parameters = get_parameter_groups(model, weight_decay, skip, get_num_layer, get_layer_scale) + weight_decay = 0. + else: + parameters = model.parameters() + + opt_args = dict(lr=args.lr, weight_decay=weight_decay) + if hasattr(args, 'opt_eps') and args.opt_eps is not None: + opt_args['eps'] = args.opt_eps + if hasattr(args, 'opt_betas') and args.opt_betas is not None: + opt_args['betas'] = args.opt_betas + + opt_split = opt_lower.split('_') + opt_lower = opt_split[-1] + if opt_lower == 'adamw': + optimizer = optim.AdamW(parameters, **opt_args) + else: + raise ValueError("Invalid optimizer") + + if len(opt_split) > 1: + if opt_split[0] == 'lookahead': + optimizer = Lookahead(optimizer) + + return optimizer diff --git a/py/evf-sam/model/unilm/beit3/randaug.py b/py/evf-sam/model/unilm/beit3/randaug.py new file mode 100644 index 0000000..359e97d --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/randaug.py @@ -0,0 +1,340 @@ +import cv2 +import numpy as np + + +## aug functions +def identity_func(img): + return img + + +def autocontrast_func(img, cutoff=0): + ''' + same output as PIL.ImageOps.autocontrast + ''' + n_bins = 256 + + def tune_channel(ch): + n = ch.size + cut = cutoff * n // 100 + if cut == 0: + high, low = ch.max(), ch.min() + else: + hist = cv2.calcHist([ch], [0], None, [n_bins], [0, n_bins]) + low = np.argwhere(np.cumsum(hist) > cut) + low = 0 if low.shape[0] == 0 else low[0] + high = np.argwhere(np.cumsum(hist[::-1]) > cut) + high = n_bins - 1 if high.shape[0] == 0 else n_bins - 1 - high[0] + if high <= low: + table = np.arange(n_bins) + else: + scale = (n_bins - 1) / (high - low) + offset = -low * scale + table = np.arange(n_bins) * scale + offset + table[table < 0] = 0 + table[table > n_bins - 1] = n_bins - 1 + table = table.clip(0, 255).astype(np.uint8) + return table[ch] + + channels = [tune_channel(ch) for ch in cv2.split(img)] + out = cv2.merge(channels) + return out + + +def equalize_func(img): + ''' + same output as PIL.ImageOps.equalize + PIL's implementation is different from cv2.equalize + ''' + n_bins = 256 + + def tune_channel(ch): + hist = cv2.calcHist([ch], [0], None, [n_bins], [0, n_bins]) + non_zero_hist = hist[hist != 0].reshape(-1) + step = np.sum(non_zero_hist[:-1]) // (n_bins - 1) + if step == 0: return ch + n = np.empty_like(hist) + n[0] = step // 2 + n[1:] = hist[:-1] + table = (np.cumsum(n) // step).clip(0, 255).astype(np.uint8) + return table[ch] + + channels = [tune_channel(ch) for ch in cv2.split(img)] + out = cv2.merge(channels) + return out + + +def rotate_func(img, degree, fill=(0, 0, 0)): + ''' + like PIL, rotate by degree, not radians + ''' + H, W = img.shape[0], img.shape[1] + center = W / 2, H / 2 + M = cv2.getRotationMatrix2D(center, degree, 1) + out = cv2.warpAffine(img, M, (W, H), borderValue=fill) + return out + + +def solarize_func(img, thresh=128): + ''' + same output as PIL.ImageOps.posterize + ''' + table = np.array([el if el < thresh else 255 - el for el in range(256)]) + table = table.clip(0, 255).astype(np.uint8) + out = table[img] + return out + + +def color_func(img, factor): + ''' + same output as PIL.ImageEnhance.Color + ''' + ## implementation according to PIL definition, quite slow + # degenerate = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)[:, :, np.newaxis] + # out = blend(degenerate, img, factor) + # M = ( + # np.eye(3) * factor + # + np.float32([0.114, 0.587, 0.299]).reshape(3, 1) * (1. - factor) + # )[np.newaxis, np.newaxis, :] + M = ( + np.float32([ + [0.886, -0.114, -0.114], + [-0.587, 0.413, -0.587], + [-0.299, -0.299, 0.701]]) * factor + + np.float32([[0.114], [0.587], [0.299]]) + ) + out = np.matmul(img, M).clip(0, 255).astype(np.uint8) + return out + + +def contrast_func(img, factor): + """ + same output as PIL.ImageEnhance.Contrast + """ + mean = np.sum(np.mean(img, axis=(0, 1)) * np.array([0.114, 0.587, 0.299])) + table = np.array([( + el - mean) * factor + mean + for el in range(256) + ]).clip(0, 255).astype(np.uint8) + out = table[img] + return out + + +def brightness_func(img, factor): + ''' + same output as PIL.ImageEnhance.Contrast + ''' + table = (np.arange(256, dtype=np.float32) * factor).clip(0, 255).astype(np.uint8) + out = table[img] + return out + + +def sharpness_func(img, factor): + ''' + The differences the this result and PIL are all on the 4 boundaries, the center + areas are same + ''' + kernel = np.ones((3, 3), dtype=np.float32) + kernel[1][1] = 5 + kernel /= 13 + degenerate = cv2.filter2D(img, -1, kernel) + if factor == 0.0: + out = degenerate + elif factor == 1.0: + out = img + else: + out = img.astype(np.float32) + degenerate = degenerate.astype(np.float32)[1:-1, 1:-1, :] + out[1:-1, 1:-1, :] = degenerate + factor * (out[1:-1, 1:-1, :] - degenerate) + out = out.astype(np.uint8) + return out + + +def shear_x_func(img, factor, fill=(0, 0, 0)): + H, W = img.shape[0], img.shape[1] + M = np.float32([[1, factor, 0], [0, 1, 0]]) + out = cv2.warpAffine(img, M, (W, H), borderValue=fill, flags=cv2.INTER_LINEAR).astype(np.uint8) + return out + + +def translate_x_func(img, offset, fill=(0, 0, 0)): + ''' + same output as PIL.Image.transform + ''' + H, W = img.shape[0], img.shape[1] + M = np.float32([[1, 0, -offset], [0, 1, 0]]) + out = cv2.warpAffine(img, M, (W, H), borderValue=fill, flags=cv2.INTER_LINEAR).astype(np.uint8) + return out + + +def translate_y_func(img, offset, fill=(0, 0, 0)): + ''' + same output as PIL.Image.transform + ''' + H, W = img.shape[0], img.shape[1] + M = np.float32([[1, 0, 0], [0, 1, -offset]]) + out = cv2.warpAffine(img, M, (W, H), borderValue=fill, flags=cv2.INTER_LINEAR).astype(np.uint8) + return out + + +def posterize_func(img, bits): + ''' + same output as PIL.ImageOps.posterize + ''' + out = np.bitwise_and(img, np.uint8(255 << (8 - bits))) + return out + + +def shear_y_func(img, factor, fill=(0, 0, 0)): + H, W = img.shape[0], img.shape[1] + M = np.float32([[1, 0, 0], [factor, 1, 0]]) + out = cv2.warpAffine(img, M, (W, H), borderValue=fill, flags=cv2.INTER_LINEAR).astype(np.uint8) + return out + + +def cutout_func(img, pad_size, replace=(0, 0, 0)): + replace = np.array(replace, dtype=np.uint8) + H, W = img.shape[0], img.shape[1] + rh, rw = np.random.random(2) + pad_size = pad_size // 2 + ch, cw = int(rh * H), int(rw * W) + x1, x2 = max(ch - pad_size, 0), min(ch + pad_size, H) + y1, y2 = max(cw - pad_size, 0), min(cw + pad_size, W) + out = img.copy() + out[x1:x2, y1:y2, :] = replace + return out + + +### level to args +def enhance_level_to_args(MAX_LEVEL): + def level_to_args(level): + return ((level / MAX_LEVEL) * 1.8 + 0.1,) + return level_to_args + + +def shear_level_to_args(MAX_LEVEL, replace_value): + def level_to_args(level): + level = (level / MAX_LEVEL) * 0.3 + if np.random.random() > 0.5: level = -level + return (level, replace_value) + + return level_to_args + + +def translate_level_to_args(translate_const, MAX_LEVEL, replace_value): + def level_to_args(level): + level = (level / MAX_LEVEL) * float(translate_const) + if np.random.random() > 0.5: level = -level + return (level, replace_value) + + return level_to_args + + +def cutout_level_to_args(cutout_const, MAX_LEVEL, replace_value): + def level_to_args(level): + level = int((level / MAX_LEVEL) * cutout_const) + return (level, replace_value) + + return level_to_args + + +def solarize_level_to_args(MAX_LEVEL): + def level_to_args(level): + level = int((level / MAX_LEVEL) * 256) + return (level, ) + return level_to_args + + +def none_level_to_args(level): + return () + + +def posterize_level_to_args(MAX_LEVEL): + def level_to_args(level): + level = int((level / MAX_LEVEL) * 4) + return (level, ) + return level_to_args + + +def rotate_level_to_args(MAX_LEVEL, replace_value): + def level_to_args(level): + level = (level / MAX_LEVEL) * 30 + if np.random.random() < 0.5: + level = -level + return (level, replace_value) + + return level_to_args + + +func_dict = { + 'Identity': identity_func, + 'AutoContrast': autocontrast_func, + 'Equalize': equalize_func, + 'Rotate': rotate_func, + 'Solarize': solarize_func, + 'Color': color_func, + 'Contrast': contrast_func, + 'Brightness': brightness_func, + 'Sharpness': sharpness_func, + 'ShearX': shear_x_func, + 'TranslateX': translate_x_func, + 'TranslateY': translate_y_func, + 'Posterize': posterize_func, + 'ShearY': shear_y_func, +} + +translate_const = 10 +MAX_LEVEL = 10 +replace_value = (128, 128, 128) +arg_dict = { + 'Identity': none_level_to_args, + 'AutoContrast': none_level_to_args, + 'Equalize': none_level_to_args, + 'Rotate': rotate_level_to_args(MAX_LEVEL, replace_value), + 'Solarize': solarize_level_to_args(MAX_LEVEL), + 'Color': enhance_level_to_args(MAX_LEVEL), + 'Contrast': enhance_level_to_args(MAX_LEVEL), + 'Brightness': enhance_level_to_args(MAX_LEVEL), + 'Sharpness': enhance_level_to_args(MAX_LEVEL), + 'ShearX': shear_level_to_args(MAX_LEVEL, replace_value), + 'TranslateX': translate_level_to_args( + translate_const, MAX_LEVEL, replace_value + ), + 'TranslateY': translate_level_to_args( + translate_const, MAX_LEVEL, replace_value + ), + 'Posterize': posterize_level_to_args(MAX_LEVEL), + 'ShearY': shear_level_to_args(MAX_LEVEL, replace_value), +} + + +class RandomAugment(object): + + def __init__(self, N=2, M=10, isPIL=False, augs=[]): + self.N = N + self.M = M + self.isPIL = isPIL + if augs: + self.augs = augs + else: + self.augs = list(arg_dict.keys()) + + def get_random_ops(self): + sampled_ops = np.random.choice(self.augs, self.N) + return [(op, 0.5, self.M) for op in sampled_ops] + + def __call__(self, img): + if self.isPIL: + img = np.array(img) + ops = self.get_random_ops() + for name, prob, level in ops: + if np.random.random() > prob: + continue + args = arg_dict[name](level) + img = func_dict[name](img, *args) + return img + + +if __name__ == '__main__': + a = RandomAugment() + img = np.random.randn(32, 32, 3) + a(img) diff --git a/py/evf-sam/model/unilm/beit3/requirements.txt b/py/evf-sam/model/unilm/beit3/requirements.txt new file mode 100644 index 0000000..4d8ffa5 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/requirements.txt @@ -0,0 +1,22 @@ +torch +torchvision +timm==0.4.12 +Pillow +blobfile +mypy +numpy +pytest +requests +einops +tensorboardX +scipy +ftfy +opencv-python +sentencepiece +pyarrow +torchmetrics==0.7.3 +transformers +deepspeed==0.4.0 +pycocotools +pycocoevalcap +torchscale==0.2.0 diff --git a/py/evf-sam/model/unilm/beit3/run_beit3_finetuning.py b/py/evf-sam/model/unilm/beit3/run_beit3_finetuning.py new file mode 100644 index 0000000..758cd69 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/run_beit3_finetuning.py @@ -0,0 +1,448 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +import argparse +import datetime +import numpy as np +import time +import torch +import torch.backends.cudnn as cudnn +import json +import os + +from pathlib import Path + +from timm.data.mixup import Mixup +from timm.models import create_model +from timm.utils import ModelEma +from optim_factory import create_optimizer, get_parameter_groups, \ + LayerDecayValueAssigner, get_is_head_flag_for_vit + +from engine_for_finetuning import train_one_epoch, get_handler, evaluate +from datasets import create_downstream_dataset +from utils import NativeScalerWithGradNormCount as NativeScaler +import utils +import modeling_finetune + + +def get_args(): + parser = argparse.ArgumentParser('BEiT fine-tuning and evaluation script for image classification', add_help=False) + + # Model parameters + parser.add_argument('--model', default='beit_base_patch16_224', type=str, metavar='MODEL', + help='Name of model to train') + parser.add_argument('--task', type=str, required=True, + choices=['nlvr2', 'vqav2', 'flickr30k', 'coco_retrieval', 'coco_captioning', 'nocaps', 'imagenet'], + help='Name of task to fine-tuning') + + parser.add_argument('--input_size', default=224, type=int, + help='images input size') + parser.add_argument('--drop_path', type=float, default=0.1, metavar='PCT', + help='Drop path rate (default: 0.1)') + + parser.add_argument('--checkpoint_activations', action='store_true', default=None, + help='Enable checkpointing to save your memory.') + parser.add_argument('--sentencepiece_model', type=str, required=True, + help='Sentencepiece model path for the pretrained model.') + parser.add_argument('--vocab_size', type=int, default=64010) + parser.add_argument('--num_max_bpe_tokens', type=int, default=64) + + parser.add_argument('--model_ema', action='store_true', default=False) + parser.add_argument('--model_ema_decay', type=float, default=0.9999, help='') + parser.add_argument('--model_ema_force_cpu', action='store_true', default=False, help='') + + # Optimizer parameters + parser.add_argument('--opt', default='adamw', type=str, metavar='OPTIMIZER', + help='Optimizer (default: "adamw"') + parser.add_argument('--opt_eps', default=1e-8, type=float, metavar='EPSILON', + help='Optimizer Epsilon (default: 1e-8)') + parser.add_argument('--opt_betas', default=[0.9, 0.999], type=float, nargs='+', metavar='BETA', + help='Optimizer Betas (default: 0.9, 0.999, use opt default)') + parser.add_argument('--clip_grad', type=float, default=None, metavar='NORM', + help='Clip gradient norm (default: None, no clipping)') + parser.add_argument('--momentum', type=float, default=0.9, metavar='M', + help='SGD momentum (default: 0.9)') + parser.add_argument('--weight_decay', type=float, default=0.05, + help='weight decay (default: 0.05)') + + parser.add_argument('--lr', type=float, default=5e-4, metavar='LR', + help='learning rate (default: 5e-4)') + parser.add_argument('--layer_decay', type=float, default=0.9) + parser.add_argument('--task_head_lr_weight', type=float, default=0) + + parser.add_argument('--warmup_lr', type=float, default=1e-6, metavar='LR', + help='warmup learning rate (default: 1e-6)') + parser.add_argument('--min_lr', type=float, default=1e-6, metavar='LR', + help='lower lr bound for cyclic schedulers that hit 0 (1e-6)') + parser.add_argument('--warmup_epochs', type=int, default=5, metavar='N', + help='epochs to warmup LR, if scheduler supports') + parser.add_argument('--warmup_steps', type=int, default=-1, metavar='N', + help='num of steps to warmup LR, will overload warmup_epochs if set > 0') + + parser.add_argument('--batch_size', default=64, type=int) + parser.add_argument('--eval_batch_size', default=None, type=int) + parser.add_argument('--epochs', default=20, type=int) + parser.add_argument('--update_freq', default=1, type=int) + parser.add_argument('--save_ckpt_freq', default=5, type=int) + + # Augmentation parameters + parser.add_argument('--randaug', action='store_true', default=False) + parser.add_argument('--train_interpolation', type=str, default='bicubic', + help='Training interpolation (random, bilinear, bicubic default: "bicubic")') + + # Finetuning params + parser.add_argument('--finetune', default='', + help='finetune from checkpoint') + parser.add_argument('--model_key', default='model|module', type=str) + parser.add_argument('--model_prefix', default='', type=str) + + # Dataset parameters + parser.add_argument('--data_path', default='/datasets01/imagenet_full_size/061417/', type=str, + help='dataset path') + + parser.add_argument('--output_dir', default='', + help='path where to save, empty for no saving') + parser.add_argument('--log_dir', default=None, + help='path where to tensorboard log') + parser.add_argument('--device', default='cuda', + help='device to use for training / testing') + parser.add_argument('--seed', default=0, type=int) + parser.add_argument('--resume', default='', + help='resume from checkpoint') + parser.add_argument('--auto_resume', action='store_true') + parser.add_argument('--no_auto_resume', action='store_false', dest='auto_resume') + parser.set_defaults(auto_resume=True) + + parser.add_argument('--save_ckpt', action='store_true') + parser.add_argument('--no_save_ckpt', action='store_false', dest='save_ckpt') + parser.set_defaults(save_ckpt=True) + + parser.add_argument('--start_epoch', default=0, type=int, metavar='N', + help='start epoch') + parser.add_argument('--eval', action='store_true', + help='Perform evaluation only') + parser.add_argument('--dist_eval', action='store_true', default=False, + help='Enabling distributed evaluation') + parser.add_argument('--num_workers', default=10, type=int) + parser.add_argument('--pin_mem', action='store_true', + help='Pin CPU memory in DataLoader for more efficient (sometimes) transfer to GPU.') + parser.add_argument('--no_pin_mem', action='store_false', dest='pin_mem') + parser.set_defaults(pin_mem=True) + + # distributed training parameters + parser.add_argument('--world_size', default=1, type=int, + help='number of distributed processes') + parser.add_argument('--local_rank', default=-1, type=int) + parser.add_argument('--dist_on_itp', action='store_true') + parser.add_argument('--dist_url', default='env://', + help='url used to set up distributed training') + + # parameter for dump predictions (VQA, COCO captioning, NoCaps) + parser.add_argument('--task_cache_path', default=None, type=str) + + # parameter for imagenet finetuning + parser.add_argument('--nb_classes', default=1000, type=int, + help='number of the classification types') + parser.add_argument('--mixup', type=float, default=0, + help='mixup alpha, mixup enabled if > 0.') + parser.add_argument('--cutmix', type=float, default=0, + help='cutmix alpha, cutmix enabled if > 0.') + parser.add_argument('--cutmix_minmax', type=float, nargs='+', default=None, + help='cutmix min/max ratio, overrides alpha and enables cutmix if set (default: None)') + parser.add_argument('--mixup_prob', type=float, default=1.0, + help='Probability of performing mixup or cutmix when either/both is enabled') + parser.add_argument('--mixup_switch_prob', type=float, default=0.5, + help='Probability of switching to cutmix when both mixup and cutmix enabled') + parser.add_argument('--mixup_mode', type=str, default='batch', + help='How to apply mixup/cutmix params. Per "batch", "pair", or "elem"') + + # augmentation parameters for imagenet finetuning + parser.add_argument('--color_jitter', type=float, default=0.4, metavar='PCT', + help='Color jitter factor (default: 0.4)') + parser.add_argument('--aa', type=str, default='rand-m9-mstd0.5-inc1', metavar='NAME', + help='Use AutoAugment policy. "v0" or "original". " + "(default: rand-m9-mstd0.5-inc1)') + parser.add_argument('--smoothing', type=float, default=0.1, + help='Label smoothing (default: 0.1)') + + # evaluation parameters for imagenet + parser.add_argument('--crop_pct', type=float, default=None) + + # random Erase params for imagenet finetuning + parser.add_argument('--reprob', type=float, default=0.25, metavar='PCT', + help='Random erase prob (default: 0.25)') + parser.add_argument('--remode', type=str, default='pixel', + help='Random erase mode (default: "pixel")') + parser.add_argument('--recount', type=int, default=1, + help='Random erase count (default: 1)') + parser.add_argument('--resplit', action='store_true', default=False, + help='Do not random erase first (clean) augmentation split') + + # parameter for captioning finetuning + parser.add_argument('--captioning_mask_prob', type=float, default=0.6) + parser.add_argument('--drop_worst_ratio', type=float, default=0.2) + parser.add_argument('--drop_worst_after', type=int, default=12000) + parser.add_argument('--num_beams', type=int, default=3) + parser.add_argument('--length_penalty', type=float, default=0.6) + + # label smoothing for imagenet and captioning + parser.add_argument('--label_smoothing', type=float, default=0.1) + + # deepspeed parameters + parser.add_argument('--enable_deepspeed', action='store_true', default=False) + parser.add_argument('--initial_scale_power', type=int, default=16) + parser.add_argument('--zero_stage', default=0, type=int, + help='ZeRO optimizer stage (default: 0)') + + known_args, _ = parser.parse_known_args() + + if known_args.enable_deepspeed: + try: + import deepspeed + from deepspeed import DeepSpeedConfig + parser = deepspeed.add_config_arguments(parser) + ds_init = deepspeed.initialize + except: + print("Please 'pip install deepspeed==0.4.0'") + exit(0) + else: + ds_init = None + + return parser.parse_args(), ds_init + + +def main(args, ds_init): + utils.init_distributed_mode(args) + + if ds_init is not None: + utils.create_ds_config(args) + + if args.task_cache_path is None: + args.task_cache_path = args.output_dir + + print(args) + + device = torch.device(args.device) + + # fix the seed for reproducibility + seed = args.seed + utils.get_rank() + torch.manual_seed(seed) + np.random.seed(seed) + # random.seed(seed) + + cudnn.benchmark = True + + if utils.get_rank() == 0 and args.log_dir is not None: + os.makedirs(args.log_dir, exist_ok=True) + log_writer = utils.TensorboardLogger(log_dir=args.log_dir) + else: + log_writer = None + + data_loader_train, data_loader_val = create_downstream_dataset(args) + + if not args.model.endswith(args.task): + if args.task in ("flickr30k", "coco_retrieval"): + model_config = "%s_retrieval" % args.model + elif args.task in ("coco_captioning", "nocaps"): + model_config = "%s_captioning" % args.model + elif args.task in ("imagenet"): + model_config = "%s_imageclassification" % args.model + else: + model_config = "%s_%s" % (args.model, args.task) + else: + model_config = args.model + print("model_config = %s" % model_config) + model = create_model( + model_config, + pretrained=False, + drop_path_rate=args.drop_path, + vocab_size=args.vocab_size, + checkpoint_activations=args.checkpoint_activations, + ) + + if args.finetune: + utils.load_model_and_may_interpolate(args.finetune, model, args.model_key, args.model_prefix) + + model.to(device) + + model_ema = None + if args.model_ema: + # Important to create EMA model after cuda(), DP wrapper, and AMP but before SyncBN and DDP wrapper + model_ema = ModelEma( + model, + decay=args.model_ema_decay, + device='cpu' if args.model_ema_force_cpu else '', + resume='') + print("Using EMA with decay = %.8f" % args.model_ema_decay) + + model_without_ddp = model + n_parameters = sum(p.numel() for p in model.parameters() if p.requires_grad) + + print("Model = %s" % str(model_without_ddp)) + print('number of params:', n_parameters) + + total_batch_size = args.batch_size * args.update_freq * utils.get_world_size() + num_training_steps_per_epoch = len(data_loader_train.dataset) // total_batch_size + print("LR = %.8f" % args.lr) + print("Batch size = %d" % total_batch_size) + print("Update frequent = %d" % args.update_freq) + print("Number of training examples = %d" % len(data_loader_train.dataset)) + print("Number of training training per epoch = %d" % num_training_steps_per_epoch) + + num_layers = model_without_ddp.get_num_layers() + if args.layer_decay < 1.0: + lrs = list(args.layer_decay ** (num_layers + 1 - i) for i in range(num_layers + 2)) + assigner = LayerDecayValueAssigner(lrs) + elif args.task_head_lr_weight > 1: + assigner = LayerDecayValueAssigner([1.0, args.task_head_lr_weight], scale_handler=get_is_head_flag_for_vit) + else: + assigner = None + + if assigner is not None: + print("Assigned values = %s" % str(assigner.values)) + + skip_weight_decay_list = model.no_weight_decay() + + if args.distributed: + torch.distributed.barrier() + if args.enable_deepspeed: + loss_scaler = None + optimizer_params = get_parameter_groups( + model, args.weight_decay, skip_weight_decay_list, + assigner.get_layer_id if assigner is not None else None, + assigner.get_scale if assigner is not None else None) + model, optimizer, _, _ = ds_init( + args=args, model=model, model_parameters=optimizer_params, + dist_init_required=not args.distributed, + ) + + print("model.gradient_accumulation_steps() = %d" % model.gradient_accumulation_steps()) + assert model.gradient_accumulation_steps() == args.update_freq + else: + if args.distributed: + model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.gpu], find_unused_parameters=True) + model_without_ddp = model.module + + optimizer = create_optimizer( + args, model_without_ddp, skip_list=skip_weight_decay_list, + get_num_layer=assigner.get_layer_id if assigner is not None else None, + get_layer_scale=assigner.get_scale if assigner is not None else None) + loss_scaler = NativeScaler() + + lr_schedule_values = utils.cosine_scheduler( + args.lr, args.min_lr, args.epochs, num_training_steps_per_epoch, + warmup_epochs=args.warmup_epochs, warmup_steps=args.warmup_steps, + ) + + utils.auto_load_model( + args=args, model=model, model_without_ddp=model_without_ddp, + optimizer=optimizer, loss_scaler=loss_scaler, model_ema=model_ema) + + task_handler = get_handler(args) + + # mixup for imagenet + mixup_fn = None + if args.task in ["imagenet", "in1k"]: + mixup_active = args.mixup > 0 or args.cutmix > 0. or args.cutmix_minmax is not None + if mixup_active: + print("Mixup is activated!") + mixup_fn = Mixup( + mixup_alpha=args.mixup, cutmix_alpha=args.cutmix, cutmix_minmax=args.cutmix_minmax, + prob=args.mixup_prob, switch_prob=args.mixup_switch_prob, mode=args.mixup_mode, + label_smoothing=args.label_smoothing, num_classes=args.nb_classes) + + if args.eval: + data_loader_test = create_downstream_dataset(args, is_eval=True) + if args.task in ["nlvr2", "flickr30k", "coco_retrieval", "imagenet"]: + ext_test_stats, task_key = evaluate(data_loader_test, model, device, task_handler) + print(f"Accuracy of the network on the {len(data_loader_test.dataset)} test images: {ext_test_stats[task_key]:.3f}%") + exit(0) + elif args.task == "vqav2": + result, _ = evaluate(data_loader_test, model, device, task_handler) + utils.dump_predictions(args, result, "vqav2_test") + exit(0) + elif args.task in ["coco_captioning", "nocaps"]: + predictions, _ = evaluate(data_loader_test, model, device, task_handler) + prediction_file = utils.dump_predictions(args, predictions, "{}_test".format(args.task)) + if utils.is_main_process() and args.task == "coco_captioning": + captioning_result = utils.coco_caption_eval(args.output_dir, prediction_file, "{}_test".format(args.task)) + result_file = os.path.join(args.output_dir, f"{args.task}_result.json") + print(json.dumps(captioning_result)) + utils.write_result_to_jsonl(captioning_result, result_file) + exit(0) + + print(f"Start training for {args.epochs} epochs") + start_time = time.time() + + max_accuracy = 0.0 + for epoch in range(args.start_epoch, args.epochs): + if args.distributed: + data_loader_train.sampler.set_epoch(epoch) + if log_writer is not None: + log_writer.set_step(epoch * num_training_steps_per_epoch * args.update_freq) + train_stats = train_one_epoch( + model, data_loader_train, optimizer, device, task_handler, epoch, + epoch * num_training_steps_per_epoch, lr_schedule_values, loss_scaler, + args.clip_grad, args.update_freq, model_ema, log_writer, args.task, mixup_fn, + ) + if args.output_dir and args.save_ckpt: + if (epoch + 1) % args.save_ckpt_freq == 0 or epoch + 1 == args.epochs: + utils.save_model( + args=args, model=model, model_without_ddp=model_without_ddp, optimizer=optimizer, + loss_scaler=loss_scaler, epoch=epoch, model_ema=model_ema) + if data_loader_val is not None: + if args.task not in ["coco_captioning", "nocaps"]: + test_stats, task_key = evaluate(data_loader_val, model, device, task_handler) + else: + predictions, _ = evaluate(data_loader_val, model, device, task_handler) + prediction_file = utils.dump_predictions(args, predictions, f"{args.task}_val_e{epoch}") + result_file = os.path.join(args.output_dir, f"{args.task}_result_val_e{epoch}.json") + task_key = "CIDEr" + if utils.is_main_process(): + test_stats = utils.coco_caption_eval(args.output_dir, prediction_file, "{}_val".format(args.task)) + utils.write_result_to_jsonl(test_stats, result_file) + torch.distributed.barrier() + if not utils.is_main_process(): + test_stats = utils.read_result_from_jsonl(result_file) + + print(f"Performance of the network on the {len(data_loader_val.dataset)} val images: {test_stats[task_key]:.1f}%") + if max_accuracy < test_stats[task_key]: + max_accuracy = test_stats[task_key] + if args.output_dir and args.save_ckpt: + utils.save_model( + args=args, model=model, model_without_ddp=model_without_ddp, optimizer=optimizer, + loss_scaler=loss_scaler, epoch="best", model_ema=model_ema) + + print(f'Max performance: {max_accuracy:.2f}%') + if log_writer is not None: + log_writer.update(acc=test_stats[task_key], head="perf", step=epoch) + + log_stats = {**{f'train_{k}': v for k, v in train_stats.items()}, + **{f'val_{k}': v for k, v in test_stats.items()}, + 'epoch': epoch, + 'n_parameters': n_parameters} + else: + log_stats = {**{f'train_{k}': v for k, v in train_stats.items()}, + # **{f'test_{k}': v for k, v in test_stats.items()}, + 'epoch': epoch, + 'n_parameters': n_parameters} + + if args.output_dir and utils.is_main_process(): + if log_writer is not None: + log_writer.flush() + with open(os.path.join(args.output_dir, "log.txt"), mode="a", encoding="utf-8") as f: + f.write(json.dumps(log_stats) + "\n") + + total_time = time.time() - start_time + total_time_str = str(datetime.timedelta(seconds=int(total_time))) + print('Training time {}'.format(total_time_str)) + + +if __name__ == '__main__': + opts, ds_init = get_args() + if opts.output_dir: + Path(opts.output_dir).mkdir(parents=True, exist_ok=True) + main(opts, ds_init) diff --git a/py/evf-sam/model/unilm/beit3/utils.py b/py/evf-sam/model/unilm/beit3/utils.py new file mode 100644 index 0000000..ca052f0 --- /dev/null +++ b/py/evf-sam/model/unilm/beit3/utils.py @@ -0,0 +1,913 @@ +# -------------------------------------------------------- +# Image as a Foreign Language: BEiT Pretraining for Vision and Vision-Language Tasks (https://arxiv.org/abs/2208.10442) +# Github source: https://github.com/microsoft/unilm/tree/master/beit3 +# Copyright (c) 2023 Microsoft +# Licensed under The MIT License [see LICENSE for details] +# --------------------------------------------------------' + +import datetime +import io +import os +import math +import time +import json +import argparse +import numpy as np +from pathlib import Path +from collections import defaultdict, deque +from timm.utils import get_state_dict + +import torch +import torch.distributed as dist +import torch.nn as nn +import torch.nn.functional as F +from torch._six import inf +from torchmetrics import Metric +from tensorboardX import SummaryWriter + + +def bool_flag(s): + """ + Parse boolean arguments from the command line. + """ + FALSY_STRINGS = {"off", "false", "0"} + TRUTHY_STRINGS = {"on", "true", "1"} + if s.lower() in FALSY_STRINGS: + return False + elif s.lower() in TRUTHY_STRINGS: + return True + else: + raise argparse.ArgumentTypeError("invalid value for a boolean flag") + + +class SmoothedValue(object): + """Track a series of values and provide access to smoothed values over a + window or the global series average. + """ + + def __init__(self, window_size=20, fmt=None): + if fmt is None: + fmt = "{median:.4f} ({global_avg:.4f})" + self.deque = deque(maxlen=window_size) + self.total = 0.0 + self.count = 0 + self.fmt = fmt + + def update(self, value, n=1): + self.deque.append(value) + self.count += n + self.total += value * n + + def synchronize_between_processes(self): + """ + Warning: does not synchronize the deque! + """ + if not is_dist_avail_and_initialized(): + return + t = torch.tensor([self.count, self.total], dtype=torch.float64, device='cuda') + dist.barrier() + dist.all_reduce(t) + t = t.tolist() + self.count = int(t[0]) + self.total = t[1] + + @property + def median(self): + d = torch.tensor(list(self.deque)) + return d.median().item() + + @property + def avg(self): + d = torch.tensor(list(self.deque), dtype=torch.float32) + return d.mean().item() + + @property + def global_avg(self): + return self.total / self.count + + @property + def max(self): + return max(self.deque) + + @property + def value(self): + return self.deque[-1] + + def __str__(self): + return self.fmt.format( + median=self.median, + avg=self.avg, + global_avg=self.global_avg, + max=self.max, + value=self.value) + + +class MetricLogger(object): + def __init__(self, delimiter="\t"): + self.meters = defaultdict(SmoothedValue) + self.delimiter = delimiter + + def update(self, **kwargs): + for k, v in kwargs.items(): + if v is None: + continue + if isinstance(v, torch.Tensor): + v = v.item() + assert isinstance(v, (float, int)) + self.meters[k].update(v) + + def __getattr__(self, attr): + if attr in self.meters: + return self.meters[attr] + if attr in self.__dict__: + return self.__dict__[attr] + raise AttributeError("'{}' object has no attribute '{}'".format( + type(self).__name__, attr)) + + def __str__(self): + loss_str = [] + for name, meter in self.meters.items(): + loss_str.append( + "{}: {}".format(name, str(meter)) + ) + return self.delimiter.join(loss_str) + + def synchronize_between_processes(self): + for meter in self.meters.values(): + meter.synchronize_between_processes() + + def add_meter(self, name, meter): + self.meters[name] = meter + + def log_every(self, iterable, print_freq, header=None): + i = 0 + if not header: + header = '' + start_time = time.time() + end = time.time() + iter_time = SmoothedValue(fmt='{avg:.4f}') + data_time = SmoothedValue(fmt='{avg:.4f}') + space_fmt = ':' + str(len(str(len(iterable)))) + 'd' + log_msg = [ + header, + '[{0' + space_fmt + '}/{1}]', + 'eta: {eta}', + '{meters}', + 'time: {time}', + 'data: {data}' + ] + if torch.cuda.is_available(): + log_msg.append('max mem: {memory:.0f}') + log_msg = self.delimiter.join(log_msg) + MB = 1024.0 * 1024.0 + for obj in iterable: + data_time.update(time.time() - end) + yield obj + iter_time.update(time.time() - end) + if i % print_freq == 0 or i == len(iterable) - 1: + eta_seconds = iter_time.global_avg * (len(iterable) - i) + eta_string = str(datetime.timedelta(seconds=int(eta_seconds))) + if torch.cuda.is_available(): + print(log_msg.format( + i, len(iterable), eta=eta_string, + meters=str(self), + time=str(iter_time), data=str(data_time), + memory=torch.cuda.max_memory_allocated() / MB)) + else: + print(log_msg.format( + i, len(iterable), eta=eta_string, + meters=str(self), + time=str(iter_time), data=str(data_time))) + i += 1 + end = time.time() + total_time = time.time() - start_time + total_time_str = str(datetime.timedelta(seconds=int(total_time))) + print('{} Total time: {} ({:.4f} s / it)'.format( + header, total_time_str, total_time / len(iterable))) + + +class TensorboardLogger(object): + def __init__(self, log_dir): + self.writer = SummaryWriter(logdir=log_dir) + self.step = 0 + + def set_step(self, step=None): + if step is not None: + self.step = step + else: + self.step += 1 + + def update(self, head='scalar', step=None, **kwargs): + for k, v in kwargs.items(): + if v is None: + continue + if isinstance(v, torch.Tensor): + v = v.item() + assert isinstance(v, (float, int)) + self.writer.add_scalar(head + "/" + k, v, self.step if step is None else step) + + def flush(self): + self.writer.flush() + + +def _load_checkpoint_for_ema(model_ema, checkpoint): + """ + Workaround for ModelEma._load_checkpoint to accept an already-loaded object + """ + mem_file = io.BytesIO() + torch.save(checkpoint, mem_file) + mem_file.seek(0) + model_ema._load_checkpoint(mem_file) + + +def setup_for_distributed(is_master): + """ + This function disables printing when not in master process + """ + import builtins as __builtin__ + builtin_print = __builtin__.print + + def print(*args, **kwargs): + force = kwargs.pop('force', False) + if is_master or force: + builtin_print(*args, **kwargs) + + __builtin__.print = print + + +def is_dist_avail_and_initialized(): + if not dist.is_available(): + return False + if not dist.is_initialized(): + return False + return True + + +def get_world_size(): + if not is_dist_avail_and_initialized(): + return 1 + return dist.get_world_size() + + +def get_rank(): + if not is_dist_avail_and_initialized(): + return 0 + return dist.get_rank() + + +def is_main_process(): + return get_rank() == 0 + + +def save_on_master(*args, **kwargs): + if is_main_process(): + torch.save(*args, **kwargs) + + +def _get_rank_env(): + if "RANK" in os.environ: + return int(os.environ["RANK"]) + else: + return int(os.environ['OMPI_COMM_WORLD_RANK']) + + +def _get_local_rank_env(): + if "LOCAL_RANK" in os.environ: + return int(os.environ["LOCAL_RANK"]) + else: + return int(os.environ['OMPI_COMM_WORLD_LOCAL_RANK']) + + +def _get_world_size_env(): + if "WORLD_SIZE" in os.environ: + return int(os.environ["WORLD_SIZE"]) + else: + return int(os.environ['OMPI_COMM_WORLD_SIZE']) + + +# The implementation code is modified from DeiT (https://github.com/facebookresearch/deit.git) +def init_distributed_mode(args): + if args.dist_on_itp: + args.rank = _get_rank_env() + args.world_size = _get_world_size_env() # int(os.environ['OMPI_COMM_WORLD_SIZE']) + args.gpu = _get_local_rank_env() + args.dist_url = "tcp://%s:%s" % (os.environ['MASTER_ADDR'], os.environ['MASTER_PORT']) + os.environ['LOCAL_RANK'] = str(args.gpu) + os.environ['RANK'] = str(args.rank) + os.environ['WORLD_SIZE'] = str(args.world_size) + # ["RANK", "WORLD_SIZE", "MASTER_ADDR", "MASTER_PORT", "LOCAL_RANK"] + elif 'RANK' in os.environ and 'WORLD_SIZE' in os.environ: + args.rank = int(os.environ["RANK"]) + args.world_size = int(os.environ['WORLD_SIZE']) + args.gpu = int(os.environ['LOCAL_RANK']) + elif 'SLURM_PROCID' in os.environ: + args.rank = int(os.environ['SLURM_PROCID']) + args.gpu = args.rank % torch.cuda.device_count() + else: + print('Not using distributed mode') + args.distributed = False + return + + args.distributed = True + + torch.cuda.set_device(args.gpu) + args.dist_backend = 'nccl' + print('| distributed init (rank {}): {}, gpu {}'.format( + args.rank, args.dist_url, args.gpu), flush=True) + torch.distributed.init_process_group( + backend=args.dist_backend, init_method=args.dist_url, + world_size=args.world_size, rank=args.rank, + timeout=datetime.timedelta(0, 7200) + ) + torch.distributed.barrier() + setup_for_distributed(args.rank == 0) + + +def load_state_dict(model, state_dict, prefix='', ignore_missing="relative_position_index"): + missing_keys = [] + unexpected_keys = [] + error_msgs = [] + # copy state_dict so _load_from_state_dict can modify it + metadata = getattr(state_dict, '_metadata', None) + state_dict = state_dict.copy() + if metadata is not None: + state_dict._metadata = metadata + + def load(module, prefix=''): + local_metadata = {} if metadata is None else metadata.get( + prefix[:-1], {}) + module._load_from_state_dict( + state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs) + for name, child in module._modules.items(): + if child is not None: + load(child, prefix + name + '.') + + load(model, prefix=prefix) + + warn_missing_keys = [] + ignore_missing_keys = [] + for key in missing_keys: + keep_flag = True + for ignore_key in ignore_missing.split('|'): + if ignore_key in key: + keep_flag = False + break + if keep_flag: + warn_missing_keys.append(key) + else: + ignore_missing_keys.append(key) + + missing_keys = warn_missing_keys + + if len(missing_keys) > 0: + print("Weights of {} not initialized from pretrained model: {}".format( + model.__class__.__name__, missing_keys)) + if len(unexpected_keys) > 0: + print("Weights from pretrained model not used in {}: {}".format( + model.__class__.__name__, unexpected_keys)) + if len(ignore_missing_keys) > 0: + print("Ignored weights of {} not initialized from pretrained model: {}".format( + model.__class__.__name__, ignore_missing_keys)) + if len(error_msgs) > 0: + print('\n'.join(error_msgs)) + + +class NativeScalerWithGradNormCount: + state_dict_key = "amp_scaler" + + def __init__(self): + self._scaler = torch.cuda.amp.GradScaler() + + def __call__(self, loss, optimizer, clip_grad=None, parameters=None, create_graph=False, update_grad=True): + self._scaler.scale(loss).backward(create_graph=create_graph) + if update_grad: + if clip_grad is not None: + assert parameters is not None + self._scaler.unscale_(optimizer) # unscale the gradients of optimizer's assigned params in-place + norm = torch.nn.utils.clip_grad_norm_(parameters, clip_grad) + else: + self._scaler.unscale_(optimizer) + norm = get_grad_norm_(parameters) + self._scaler.step(optimizer) + self._scaler.update() + else: + norm = None + return norm + + def state_dict(self): + return self._scaler.state_dict() + + def load_state_dict(self, state_dict): + self._scaler.load_state_dict(state_dict) + + +def get_grad_norm_(parameters, norm_type: float = 2.0) -> torch.Tensor: + if isinstance(parameters, torch.Tensor): + parameters = [parameters] + parameters = [p for p in parameters if p.grad is not None] + norm_type = float(norm_type) + if len(parameters) == 0: + return torch.tensor(0.) + device = parameters[0].grad.device + if norm_type == inf: + total_norm = max(p.grad.detach().abs().max().to(device) for p in parameters) + else: + total_norm = torch.norm(torch.stack([torch.norm(p.grad.detach(), norm_type).to(device) for p in parameters]), norm_type) + return total_norm + + +def cosine_scheduler(base_value, final_value, epochs, niter_per_ep, warmup_epochs=0, + start_warmup_value=0, warmup_steps=-1, sched_type="cos"): + warmup_schedule = np.array([]) + warmup_iters = warmup_epochs * niter_per_ep + if warmup_steps > 0: + warmup_iters = warmup_steps + print("Set warmup steps = %d" % warmup_iters) + if warmup_epochs > 0: + warmup_schedule = np.linspace(start_warmup_value, base_value, warmup_iters) + + if sched_type == "cos": + iters = np.arange(epochs * niter_per_ep - warmup_iters) + schedule = np.array([ + final_value + 0.5 * (base_value - final_value) * (1 + math.cos(math.pi * i / (len(iters)))) for i in iters]) + elif sched_type == "linear": + schedule = np.linspace(base_value, final_value, epochs * niter_per_ep - warmup_iters) + else: + raise NotImplementedError() + + schedule = np.concatenate((warmup_schedule, schedule)) + + assert len(schedule) == epochs * niter_per_ep + return schedule + + +def save_model(args, epoch, model, model_without_ddp, optimizer, loss_scaler, model_ema=None): + output_dir = Path(args.output_dir) + if loss_scaler is not None: + checkpoint_paths = [output_dir / ('checkpoint-%s.pth' % epoch)] + for checkpoint_path in checkpoint_paths: + to_save = { + 'model': model_without_ddp.state_dict(), + 'optimizer': optimizer.state_dict(), + 'epoch': epoch, + 'scaler': loss_scaler.state_dict(), + 'args': args, + } + + if model_ema is not None: + to_save['model_ema'] = get_state_dict(model_ema) + + save_on_master(to_save, checkpoint_path) + else: + client_state = {'epoch': epoch, "args": args} + if model_ema is not None: + client_state['model_ema'] = get_state_dict(model_ema) + model.save_checkpoint(save_dir=args.output_dir, tag="checkpoint-%s" % epoch, client_state=client_state) + + +def auto_load_model(args, model, model_without_ddp, optimizer, loss_scaler, model_ema=None): + output_dir = Path(args.output_dir) + if loss_scaler is not None: + # torch.amp + if args.auto_resume and len(args.resume) == 0: + import glob + all_checkpoints = glob.glob(os.path.join(output_dir, 'checkpoint-*.pth')) + latest_ckpt = -1 + for ckpt in all_checkpoints: + t = ckpt.split('-')[-1].split('.')[0] + if t.isdigit(): + latest_ckpt = max(int(t), latest_ckpt) + if latest_ckpt >= 0: + args.resume = os.path.join(output_dir, 'checkpoint-%d.pth' % latest_ckpt) + print("Auto resume checkpoint: %s" % args.resume) + + if args.resume: + if args.resume.startswith('https'): + checkpoint = torch.hub.load_state_dict_from_url( + args.resume, map_location='cpu', check_hash=True) + else: + checkpoint = torch.load(args.resume, map_location='cpu') + model_without_ddp.load_state_dict(checkpoint['model']) + print("Resume checkpoint %s" % args.resume) + if 'optimizer' in checkpoint and 'epoch' in checkpoint: + optimizer.load_state_dict(checkpoint['optimizer']) + args.start_epoch = checkpoint['epoch'] + 1 + if hasattr(args, 'model_ema') and args.model_ema: + _load_checkpoint_for_ema(model_ema, checkpoint['model_ema']) + if 'scaler' in checkpoint: + loss_scaler.load_state_dict(checkpoint['scaler']) + print("With optim & sched!") + else: + # deepspeed, only support '--auto_resume'. + if args.auto_resume: + import glob + all_checkpoints = glob.glob(os.path.join(output_dir, 'checkpoint-*')) + latest_ckpt = -1 + for ckpt in all_checkpoints: + t = ckpt.split('-')[-1].split('.')[0] + if t.isdigit(): + latest_ckpt = max(int(t), latest_ckpt) + if latest_ckpt >= 0: + args.resume = os.path.join(output_dir, 'checkpoint-%d' % latest_ckpt) + print("Auto resume checkpoint: %d" % latest_ckpt) + _, client_states = model.load_checkpoint(args.output_dir, tag='checkpoint-%d' % latest_ckpt) + args.start_epoch = client_states['epoch'] + 1 + if model_ema is not None: + if args.model_ema: + _load_checkpoint_for_ema(model_ema, client_states['model_ema']) + + +# The implementation code is modified from DeiT (https://github.com/facebookresearch/deit.git) +def load_model_and_may_interpolate(ckpt_path, model, model_key, model_prefix): + if ckpt_path.startswith('https'): + checkpoint = torch.hub.load_state_dict_from_url( + ckpt_path, map_location='cpu', check_hash=True) + else: + checkpoint = torch.load(ckpt_path, map_location='cpu') + + print("Load ckpt from %s" % ckpt_path) + checkpoint_model = None + for model_key in model_key.split('|'): + if model_key in checkpoint: + checkpoint_model = checkpoint[model_key] + print("Load state_dict by model_key = %s" % model_key) + break + + if checkpoint_model is None: + checkpoint_model = checkpoint + + state_dict = model.state_dict() + for k in ['head.weight', 'head.bias']: + if k in checkpoint_model and checkpoint_model[k].shape != state_dict[k].shape: + print(f"Removing key {k} from pretrained checkpoint") + del checkpoint_model[k] + + # interpolate position embedding + for pos_embed_key in ("vision_pos_embed", "pos_embed", "beit3.encoder.embed_positions.A.weight"): + if pos_embed_key in checkpoint_model: + pos_embed_checkpoint = checkpoint_model[pos_embed_key] + embedding_size = pos_embed_checkpoint.shape[-1] + if pos_embed_key == "beit3.encoder.embed_positions.A.weight": + # being consistent with Fairseq, which starts from 2 for position embedding + torchscale_model = True + num_patches = model.beit3.vision_embed.num_patches + num_extra_tokens = model.beit3.vision_embed.num_position_embeddings() + 2 - num_patches + else: + torchscale_model = False + num_patches = model.patch_embed.num_patches + num_extra_tokens = getattr(model, pos_embed_key).shape[-2] - num_patches + # height (== width) for the checkpoint position embedding + orig_size = int((pos_embed_checkpoint.shape[-2] - num_extra_tokens) ** 0.5) + # height (== width) for the new position embedding + new_size = int(num_patches ** 0.5) + # class_token and dist_token are kept unchanged + if orig_size != new_size: + print("Position interpolate from %dx%d to %dx%d" % (orig_size, orig_size, new_size, new_size)) + if torchscale_model: + extra_tokens = pos_embed_checkpoint[:num_extra_tokens].unsqueeze(0) + # only the position tokens are interpolated + pos_tokens = pos_embed_checkpoint[num_extra_tokens:] + else: + extra_tokens = pos_embed_checkpoint[:, :num_extra_tokens] + # only the position tokens are interpolated + pos_tokens = pos_embed_checkpoint[:, num_extra_tokens:] + pos_tokens = pos_tokens.reshape(-1, orig_size, orig_size, embedding_size).permute(0, 3, 1, 2) + pos_tokens = torch.nn.functional.interpolate( + pos_tokens, size=(new_size, new_size), mode='bicubic', align_corners=False) + pos_tokens = pos_tokens.permute(0, 2, 3, 1).flatten(1, 2) + new_pos_embed = torch.cat((extra_tokens, pos_tokens), dim=1) + if torchscale_model: + new_pos_embed = new_pos_embed.squeeze(0) + checkpoint_model[pos_embed_key] = new_pos_embed + + load_state_dict(model, checkpoint_model, prefix=model_prefix) + + +def create_ds_config(args): + args.deepspeed_config = os.path.join(args.output_dir, "deepspeed_config.json") + with open(args.deepspeed_config, mode="w") as writer: + ds_config = { + "train_batch_size": args.batch_size * args.update_freq * get_world_size(), + "train_micro_batch_size_per_gpu": args.batch_size, + "steps_per_print": 1000, + "optimizer": { + "type": "Adam", + "adam_w_mode": True, + "params": { + "lr": args.lr, + "weight_decay": args.weight_decay, + "bias_correction": True, + "betas": [ + args.opt_betas[0], + args.opt_betas[1] + ], + "eps": args.opt_eps + } + }, + "fp16": { + "enabled": True, + "loss_scale": 0, + "initial_scale_power": getattr(args, "initial_scale_power", 12), + "loss_scale_window": 1000, + "hysteresis": 2, + "min_loss_scale": 1 + }, + "amp": { + "enabled": False, + "opt_level": "O2" + } + } + + if args.clip_grad is not None: + ds_config.update({'gradient_clipping': args.clip_grad}) + + if args.zero_stage == 1: + ds_config.update({"zero_optimization": {"stage": args.zero_stage, "reduce_bucket_size": 5e8}}) + elif args.zero_stage > 1: + raise NotImplementedError() + + writer.write(json.dumps(ds_config, indent=2)) + + +def merge_batch_tensors_by_dict_key(batch): + batch_tensors = {} + for tensor_key in batch[0]: + if isinstance(batch[0][tensor_key], torch.Tensor): + batch_tensors[tensor_key] = torch.stack([d[tensor_key] for d in batch]) + else: + batch_tensors[tensor_key] = torch.tensor([d[tensor_key] for d in batch], dtype=torch.long) + return batch_tensors + + +def get_loss_scale_for_deepspeed(model): + optimizer = model.optimizer + loss_scale = None + if hasattr(optimizer, 'loss_scale'): + loss_scale = optimizer.loss_scale + elif hasattr(optimizer, 'cur_scale'): + loss_scale = optimizer.cur_scale + return loss_scale + + +class GatherLayer(torch.autograd.Function): + """ + Gather tensors from all workers with support for backward propagation: + This implementation does not cut the gradients as torch.distributed.all_gather does. + """ + @staticmethod + def forward(ctx, x): + output = [torch.zeros_like(x) for _ in range(dist.get_world_size())] + dist.all_gather(output, x) + return tuple(output) + @staticmethod + def backward(ctx, *grads): + all_gradients = torch.stack(grads) + dist.all_reduce(all_gradients) + return all_gradients[dist.get_rank()] + + +def gather_features( + image_features, + text_features, +): + gathered_image_features = GatherLayer.apply(image_features) + gathered_text_features = GatherLayer.apply(text_features) + all_image_features = torch.cat(gathered_image_features) + all_text_features = torch.cat(gathered_text_features) + + return all_image_features, all_text_features + + +# The implementation code is modified from open_clip (https://github.com/mlfoundations/open_clip.git) +class ClipLoss(nn.Module): + + def __init__( + self, + cache_labels=False, + rank=0, + world_size=1, + ): + super().__init__() + self.cache_labels = cache_labels + self.rank = rank + self.world_size = world_size + + # cache state + self.prev_num_logits = 0 + self.labels = {} + + def forward(self, image_features, text_features, logit_scale): + device = image_features.device + if self.world_size > 1: + all_image_features, all_text_features = gather_features( + image_features, text_features + ) + + logits_per_image = logit_scale * image_features @ all_text_features.T + logits_per_text = logit_scale * text_features @ all_image_features.T + else: + logits_per_image = logit_scale * image_features @ text_features.T + logits_per_text = logit_scale * text_features @ image_features.T + + # calculated ground-truth and cache if enabled + num_logits = logits_per_image.shape[0] + if self.prev_num_logits != num_logits or device not in self.labels: + labels = torch.arange(num_logits, device=device, dtype=torch.long) + if self.world_size > 1: + labels = labels + num_logits * self.rank + if self.cache_labels: + self.labels[device] = labels + self.prev_num_logits = num_logits + else: + labels = self.labels[device] + + total_loss = ( + F.cross_entropy(logits_per_image, labels) + + F.cross_entropy(logits_per_text, labels) + ) / 2 + return total_loss, logits_per_image, logits_per_text + + +def write_result_to_jsonl(test_stats, result_file): + with open(result_file, mode="w", encoding="utf-8") as writer: + writer.write(json.dumps(test_stats, indent=None)) + + +def read_result_from_jsonl(result_file): + with open(result_file, mode="r", encoding="utf-8") as reader: + return json.load(reader) + + +# The implementation code is from ViLT (https://github.com/dandelin/ViLT.git) +class VQAScore(Metric): + def __init__(self, dist_sync_on_step=False): + super().__init__(dist_sync_on_step=dist_sync_on_step) + self.add_state("score", default=torch.tensor(0.0), dist_reduce_fx="sum") + self.add_state("total", default=torch.tensor(0.0), dist_reduce_fx="sum") + + def update(self, logits, target): + logits, target = ( + logits.detach().float().to(self.score.device), + target.detach().float().to(self.score.device), + ) + logits = torch.max(logits, 1)[1] + one_hots = torch.zeros(*target.size()).to(target) + one_hots.scatter_(1, logits.view(-1, 1), 1) + scores = one_hots * target + + self.score += scores.sum() + self.total += len(logits) + + def compute(self): + return self.score / self.total + + +class BertCaptioningLoss(nn.Module): + def __init__(self, label_smoothing, drop_worst_ratio, drop_worst_after): + super().__init__() + self.label_smoothing = label_smoothing + self.drop_worst_ratio = drop_worst_ratio + self.drop_worst_after = drop_worst_after + self.log_soft = nn.LogSoftmax(dim=1) + self.kl = nn.KLDivLoss(reduction='none') + self.iter = 0 + + def forward(self, logits, target, iter): + eps = self.label_smoothing + n_class = logits.size(1) + one_hot = torch.zeros_like(logits).scatter(1, target.view(-1, 1), 1) + one_hot = one_hot * (1 - eps) + (1 - one_hot) * eps / (n_class - 1) + log_prb = self.log_soft(logits) + loss = self.kl(log_prb, one_hot).sum(1) + + if self.drop_worst_ratio > 0 and iter > self.drop_worst_after: + loss, _ = torch.topk(loss, + k=int(loss.shape[0] * (1-self.drop_worst_ratio)), + largest=False) + loss = loss.mean() + + return loss + + +class BeamHypotheses(object): + def __init__(self, n_hyp, max_length, length_penalty, early_stopping): + """ + Initialize n-best list of hypotheses. + """ + self.max_length = max_length - 1 # ignoring bos_token + self.length_penalty = length_penalty + self.early_stopping = early_stopping + self.n_hyp = n_hyp + self.hyp = [] + self.worst_score = 1e9 + + def __len__(self): + """ + Number of hypotheses in the list. + """ + return len(self.hyp) + + def add(self, hyp, sum_logprobs): + """ + Add a new hypothesis to the list. + """ + score = sum_logprobs / len(hyp) ** self.length_penalty + if len(self) < self.n_hyp or score > self.worst_score: + self.hyp.append((score, hyp)) + if len(self) > self.n_hyp: + sorted_scores = sorted([(s, idx) for idx, (s, _) in enumerate(self.hyp)]) + del self.hyp[sorted_scores[0][1]] + self.worst_score = sorted_scores[1][0] + else: + self.worst_score = min(score, self.worst_score) + + def is_done(self, best_sum_logprobs): + """ + If there are enough hypotheses and that none of the hypotheses being generated + can become better than the worst one in the heap, then we are done with this sentence. + """ + if len(self) < self.n_hyp: + return False + elif self.early_stopping: + return True + else: + return self.worst_score >= best_sum_logprobs / self.max_length ** self.length_penalty + + +def dump_predictions(args, result, file_suffix): + global_rank = get_rank() + jsons = None + if global_rank >= 0: + output_file = os.path.join(args.task_cache_path, f"submit_{global_rank}_{file_suffix}.json") + with open(output_file, "w") as fp: + json.dump(result, fp, indent=2) + torch.distributed.barrier() + + if global_rank == 0: + world_size = get_world_size() + jsons = [] + for i in range(world_size): + each_file = os.path.join(args.task_cache_path, f"submit_{i}_{file_suffix}.json") + with open(each_file, "r") as fp: + jsons += json.load(fp) + + new_jsons = [] + res_dict = dict() + if args.task in ["coco_captioning", "nocaps"]: + qid_key = "image_id" + else: + # for VQAv2 + qid_key = "question_id" + for item in jsons: + if item[qid_key] in res_dict: + continue + new_jsons.append(item) + res_dict[item[qid_key]] = item + jsons = new_jsons + + torch.distributed.barrier() + os.remove(output_file) + else: + jsons = result + + result_file = os.path.join(args.output_dir, f"submit_{file_suffix}.json") + if jsons is not None: + with open(result_file, "w") as fp: + json.dump(jsons, fp, indent=2) + print("Infer %d examples into %s" % (len(jsons), result_file)) + return result_file + + +# The evaluation code is from BLIP (https://github.com/salesforce/BLIP) +# For nocaps, please submit the prediction file to the evaluate server (https://eval.ai/web/challenges/challenge-page/355/overview) to obtain the final results +def coco_caption_eval(gt_dir, results_file, split): + from pycocotools.coco import COCO + from pycocoevalcap.eval import COCOEvalCap + from torchvision.datasets.utils import download_url + + urls = {'coco_captioning_val': 'https://storage.googleapis.com/sfr-vision-language-research/datasets/coco_karpathy_val_gt.json', + 'coco_captioning_test': 'https://storage.googleapis.com/sfr-vision-language-research/datasets/coco_karpathy_test_gt.json', + 'nocaps_val': 'https://github.com/addf400/files/releases/download/beit3/nocaps_val_gt.json'} + filenames = {'coco_captioning_val':'coco_karpathy_val_gt.json', + 'coco_captioning_test':'coco_karpathy_test_gt.json', + 'nocaps_val':'nocaps_val_gt.json'} + + download_url(urls[split], gt_dir) + annotation_file = os.path.join(gt_dir, filenames[split]) + + # create coco object and coco_result object + coco = COCO(annotation_file) + coco_result = coco.loadRes(results_file) + + # create coco_eval object by taking coco and coco_result + coco_eval = COCOEvalCap(coco, coco_result) + + # evaluate results + # SPICE will take a few minutes the first time, but speeds up due to caching + coco_eval.evaluate() + + res_dict = dict() + for metric, score in coco_eval.eval.items(): + res_dict[metric] = score + + return res_dict diff --git a/py/evf-sam/utils/ade20k_classes.json b/py/evf-sam/utils/ade20k_classes.json new file mode 100644 index 0000000..1f96e61 --- /dev/null +++ b/py/evf-sam/utils/ade20k_classes.json @@ -0,0 +1,30 @@ +[ + "wall", "building", "sky", "floor", "tree", "ceiling", "road", + "bed", "windowpane", "grass", "cabinet", "sidewalk", + "person", "earth", "door", "table", "mountain", "plant", + "curtain", "chair", "car", "water", "painting", "sofa", + "shelf", "house", "sea", "mirror", "rug", "field", "armchair", + "seat", "fence", "desk", "rock", "wardrobe", "lamp", + "bathtub", "railing", "cushion", "base", "box", "column", + "signboard", "chest of drawers", "counter", "sand", "sink", + "skyscraper", "fireplace", "refrigerator", "grandstand", + "path", "stairs", "runway", "case", "pool table", "pillow", + "screen door", "stairway", "river", "bridge", "bookcase", + "blind", "coffee table", "toilet", "flower", "book", "hill", + "bench", "countertop", "stove", "palm", "kitchen island", + "computer", "swivel chair", "boat", "bar", "arcade machine", + "hovel", "bus", "towel", "light", "truck", "tower", + "chandelier", "awning", "streetlight", "booth", + "television receiver", "airplane", "dirt track", "apparel", + "pole", "land", "bannister", "escalator", "ottoman", "bottle", + "buffet", "poster", "stage", "van", "ship", "fountain", + "conveyer belt", "canopy", "washer", "plaything", + "swimming pool", "stool", "barrel", "basket", "waterfall", + "tent", "bag", "minibike", "cradle", "oven", "ball", "food", + "step", "tank", "trade name", "microwave", "pot", "animal", + "bicycle", "lake", "dishwasher", "screen", "blanket", + "sculpture", "hood", "sconce", "vase", "traffic light", + "tray", "ashcan", "fan", "pier", "crt screen", "plate", + "monitor", "bulletin board", "shower", "radiator", "glass", + "clock", "flag" +] \ No newline at end of file diff --git a/py/evf-sam/utils/aug.py b/py/evf-sam/utils/aug.py new file mode 100644 index 0000000..a01a5eb --- /dev/null +++ b/py/evf-sam/utils/aug.py @@ -0,0 +1,117 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. + +# This source code is licensed under the license found in the +# LICENSE file in the root directory of this source tree. + +from copy import deepcopy +from typing import Tuple + +import numpy as np +import torch +from torch.nn import functional as F +from torchvision.transforms.functional import resize # type: ignore +from torchvision.transforms.functional import to_pil_image +import random + + +class RandomScale: + """ + Resizes images to the longest side 'target_length', as well as provides + methods for resizing coordinates and boxes. Provides methods for + transforming both numpy array and batched torch tensors. + """ + + def __init__(self, max_length: int, min_length: int) -> None: + self.max_length = max_length + self.min_length = min_length + + def apply_image(self, image: np.ndarray) -> np.ndarray: + """ + Expects a numpy array with shape HxWxC in uint8 format. + """ + target_size = self.get_preprocess_shape( + image.shape[0], image.shape[1], self.max_length, self.min_length + ) + return np.array(resize(to_pil_image(image), target_size)) + + def apply_coords( + self, coords: np.ndarray, original_size: Tuple[int, ...] + ) -> np.ndarray: + """ + Expects a numpy array of length 2 in the final dimension. Requires the + original image size in (H, W) format. + """ + old_h, old_w = original_size + new_h, new_w = self.get_preprocess_shape( + original_size[0], original_size[1], self.max_length, self.min_length + ) + coords = deepcopy(coords).astype(float) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + def apply_boxes( + self, boxes: np.ndarray, original_size: Tuple[int, ...] + ) -> np.ndarray: + """ + Expects a numpy array shape Bx4. Requires the original image size + in (H, W) format. + """ + boxes = self.apply_coords(boxes.reshape(-1, 2, 2), original_size) + return boxes.reshape(-1, 4) + + def apply_image_torch(self, image: torch.Tensor) -> torch.Tensor: + """ + Expects batched images with shape BxCxHxW and float format. This + transformation may not exactly match apply_image. apply_image is + the transformation expected by the model. + """ + # Expects an image in BCHW format. May not exactly match apply_image. + target_size = self.get_preprocess_shape( + image.shape[0], image.shape[1], self.max_length, self.min_length + ) + return F.interpolate( + image, target_size, mode="bilinear", align_corners=False, antialias=True + ) + + def apply_coords_torch( + self, coords: torch.Tensor, original_size: Tuple[int, ...] + ) -> torch.Tensor: + """ + Expects a torch tensor with length 2 in the last dimension. Requires the + original image size in (H, W) format. + """ + old_h, old_w = original_size + new_h, new_w = self.get_preprocess_shape( + original_size[0], original_size[1], self.max_length, self.min_length + ) + coords = deepcopy(coords).to(torch.float) + coords[..., 0] = coords[..., 0] * (new_w / old_w) + coords[..., 1] = coords[..., 1] * (new_h / old_h) + return coords + + def apply_boxes_torch( + self, boxes: torch.Tensor, original_size: Tuple[int, ...] + ) -> torch.Tensor: + """ + Expects a torch tensor with shape Bx4. Requires the original image + size in (H, W) format. + """ + boxes = self.apply_coords_torch(boxes.reshape(-1, 2, 2), original_size) + return boxes.reshape(-1, 4) + + @staticmethod + def get_preprocess_shape( + oldh: int, oldw: int, max_length: int, min_length: int + ) -> Tuple[int, int]: + """ + Compute the output size given input size and target long side length. + """ + max_scale = max_length * 1.0 / max(oldh, oldw) + min_scale = min_length * 1.0 / max(oldh, oldw) + scale = min_scale + random.random() * (max_scale-min_scale) + newh, neww = oldh * scale, oldw * scale + neww = int(neww + 0.5) + newh = int(newh + 0.5) + return (newh, neww) diff --git a/py/evf-sam/utils/data_processing.py b/py/evf-sam/utils/data_processing.py new file mode 100644 index 0000000..d47a80f --- /dev/null +++ b/py/evf-sam/utils/data_processing.py @@ -0,0 +1,90 @@ +import glob +import json +import os + +import cv2 +import numpy as np + + +def get_mask_from_json(json_path, img): + try: + with open(json_path, "r") as r: + anno = json.loads(r.read()) + except: + with open(json_path, "r", encoding="cp1252") as r: + anno = json.loads(r.read()) + + inform = anno["shapes"] + comments = anno["text"] + is_sentence = anno["is_sentence"] + + height, width = img.shape[:2] + + ### sort polies by area + area_list = [] + valid_poly_list = [] + for i in inform: + label_id = i["label"] + points = i["points"] + if "flag" == label_id.lower(): ## meaningless deprecated annotations + continue + + tmp_mask = np.zeros((height, width), dtype=np.uint8) + cv2.polylines(tmp_mask, np.array([points], dtype=np.int32), True, 1, 1) + cv2.fillPoly(tmp_mask, np.array([points], dtype=np.int32), 1) + tmp_area = tmp_mask.sum() + + area_list.append(tmp_area) + valid_poly_list.append(i) + + ### ground-truth mask + sort_index = np.argsort(area_list)[::-1].astype(np.int32) + sort_index = list(sort_index) + sort_inform = [] + for s_idx in sort_index: + sort_inform.append(valid_poly_list[s_idx]) + + mask = np.zeros((height, width), dtype=np.uint8) + for i in sort_inform: + label_id = i["label"] + points = i["points"] + + if "ignore" in label_id.lower(): + label_value = 255 # ignored during evaluation + else: + label_value = 1 # target + + cv2.polylines(mask, np.array([points], dtype=np.int32), True, label_value, 1) + cv2.fillPoly(mask, np.array([points], dtype=np.int32), label_value) + + return mask, comments, is_sentence + + +if __name__ == "__main__": + data_dir = "./train" + vis_dir = "./vis" + + if not os.path.exists(vis_dir): + os.makedirs(vis_dir) + + json_path_list = sorted(glob.glob(data_dir + "/*.json")) + for json_path in json_path_list: + img_path = json_path.replace(".json", ".jpg") + img = cv2.imread(img_path)[:, :, ::-1] + + # In generated mask, value 1 denotes valid target region, and value 255 stands for region ignored during evaluaiton. + mask, comments, is_sentence = get_mask_from_json(json_path, img) + + ## visualization. Green for target, and red for ignore. + valid_mask = (mask == 1).astype(np.float32)[:, :, None] + ignore_mask = (mask == 255).astype(np.float32)[:, :, None] + vis_img = img * (1 - valid_mask) * (1 - ignore_mask) + ( + (np.array([0, 255, 0]) * 0.6 + img * 0.4) * valid_mask + + (np.array([255, 0, 0]) * 0.6 + img * 0.4) * ignore_mask + ) + vis_img = np.concatenate([img, vis_img], 1) + vis_path = os.path.join( + vis_dir, json_path.split("/")[-1].replace(".json", ".jpg") + ) + cv2.imwrite(vis_path, vis_img[:, :, ::-1]) + print("Visualization has been saved to: ", vis_path) diff --git a/py/evf-sam/utils/dataset.py b/py/evf-sam/utils/dataset.py new file mode 100644 index 0000000..9969078 --- /dev/null +++ b/py/evf-sam/utils/dataset.py @@ -0,0 +1,386 @@ +import glob +import os +import random + +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from pycocotools import mask + +from model.segment_anything.utils.transforms import ResizeLongestSide + +from .data_processing import get_mask_from_json +from .refer import REFER +from .refer_seg_dataset import ReferSegDataset +from .sem_seg_dataset import SemSegDataset +from torchvision import transforms +import json +from PIL import Image + +def collate_fn( + batch, tokenizer=None, local_rank=-1 +): + image_path_list = [] + images_list = [] + images_evf_list = [] + masks_list = [] + label_list = [] + resize_list = [] + sampled_classes_list = [] + offset_list = [0] + cnt = 0 + inferences = [] + for ( + image_path, + images, + images_evf, + masks, + label, + resize, + sampled_classes, + inference, + ) in batch: + image_path_list.append(image_path) + images_list.append(images) + images_evf_list.append(images_evf) + label_list.append(label) + masks_list.append(masks.float()) + resize_list.append(resize) + sampled_classes_list.extend(sampled_classes) + cnt += len(sampled_classes) + offset_list.append(cnt) + inferences.append(inference) + + input_ids = [ + tokenizer(prompt, return_tensors="pt").input_ids[0] + for prompt in sampled_classes_list + ] + + input_ids = torch.nn.utils.rnn.pad_sequence( + input_ids, batch_first=True, padding_value=tokenizer.pad_token_id + ) + attention_masks = input_ids.ne(tokenizer.pad_token_id) + + if inferences[0] == False: + truncate_len = tokenizer.model_max_length + + if input_ids.shape[1] > truncate_len: + input_ids = input_ids[:, :truncate_len] + targets = targets[:, :truncate_len] + attention_masks = attention_masks[:, :truncate_len] + + return { + "image_paths": image_path_list, + "images": torch.stack(images_list, dim=0), + "images_evf": torch.stack(images_evf_list, dim=0), + "input_ids": input_ids, + "attention_masks": attention_masks, + "masks_list": masks_list, + "label_list": label_list, + "resize_list": resize_list, + "offset": torch.LongTensor(offset_list), + "sampled_classes_list": sampled_classes_list, + "inference": inferences[0], + } + + +class HybridDataset(torch.utils.data.Dataset): + pixel_mean = torch.Tensor([123.675, 116.28, 103.53]).view(-1, 1, 1) + pixel_std = torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1) + img_size = 1024 + ignore_label = 255 + + def __init__( + self, + base_image_dir, + tokenizer, + samples_per_epoch=500 * 8 * 2 * 10, + precision: str = "fp32", + image_size: int = 224, + num_classes_per_sample: int = 3, + exclude_val=False, + dataset="sem_seg||refer_seg", + sample_rate=[9, 3, 3, 1], + sem_seg_data="ade20k||cocostuff||pascal_part||mapillary", + refer_seg_data="refclef||refcoco||refcoco+||refcocog", + explanatory=-1, + model_type="ori", + transform=ResizeLongestSide(1024), + ): + self.transform=transform + self.model_type = model_type + self.exclude_val = exclude_val + self.dataset = dataset + self.samples_per_epoch = samples_per_epoch + self.explanatory = explanatory + self.num_classes_per_sample = num_classes_per_sample + sample_rate = np.array(sample_rate) + self.sample_rate = sample_rate / sample_rate.sum() + + self.base_image_dir = base_image_dir + self.image_size = image_size + self.tokenizer = tokenizer + self.precision = precision + + self.datasets = dataset.split("||") + + self.all_datasets = [] + for dataset in self.datasets: + if dataset == "sem_seg": + self.all_datasets.append( + SemSegDataset( + base_image_dir, + tokenizer, + samples_per_epoch, + precision, + image_size, + num_classes_per_sample, + exclude_val, + sem_seg_data, + self.model_type, + self.transform + ) + ) + elif dataset == "refer_seg": + self.all_datasets.append( + ReferSegDataset( + base_image_dir, + tokenizer, + samples_per_epoch, + precision, + image_size, + num_classes_per_sample, + exclude_val, + refer_seg_data, + self.model_type, + self.transform + ) + ) + + def __len__(self): + return self.samples_per_epoch + + def __getitem__(self, idx): + ind = np.random.choice(list(range(len(self.datasets))), p=self.sample_rate) + data = self.all_datasets[ind] + inference = False + return *data[0], inference + + +def init_ade20k(base_image_dir): + with open("utils/ade20k_classes.json", "r") as f: + ade20k_classes = json.load(f) + ade20k_classes = np.array(ade20k_classes) + image_ids = sorted( + os.listdir(os.path.join(base_image_dir, "ade20k/images", "validation")) + ) + ade20k_image_ids = [] + for x in image_ids: + if x.endswith(".jpg"): + ade20k_image_ids.append(x[:-4]) + ade20k_images = [] + for image_id in ade20k_image_ids: # self.descriptions: + ade20k_images.append( + os.path.join( + base_image_dir, + "ade20k", + "images", + "validation", + "{}.jpg".format(image_id), + ) + ) + ade20k_labels = [ + x.replace(".jpg", ".png").replace("images", "annotations") + for x in ade20k_images + ] + print("ade20k: ", len(ade20k_images)) + return ade20k_classes, ade20k_images, ade20k_labels + + +class ValDataset(torch.utils.data.Dataset): + pixel_mean = torch.Tensor([123.675, 116.28, 103.53]).view(-1, 1, 1) + pixel_std = torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1) + img_size = 1024 + ignore_label = 255 + + def __init__( + self, + base_image_dir, + tokenizer, + val_dataset, + image_size=224, + model_type="ori" + ): + self.model_type = model_type + self.base_image_dir = base_image_dir + splits = val_dataset.split("|") + if len(splits) == 3: + ds, splitBy, split = splits + base_image_dir = os.path.join(base_image_dir, "refer_seg") + refer_api = REFER(base_image_dir, ds, splitBy) + ref_ids_val = refer_api.getRefIds(split=split) + images_ids_val = refer_api.getImgIds(ref_ids=ref_ids_val) + refs_val = refer_api.loadRefs(ref_ids=ref_ids_val) + refer_seg_ds = {} + refer_seg_ds["images"] = [] + loaded_images = refer_api.loadImgs(image_ids=images_ids_val) + for item in loaded_images: + item = item.copy() + if ds == "refclef": + item["file_name"] = os.path.join( + base_image_dir, "images/saiapr_tc-12", item["file_name"] + ) + elif ds in ["refcoco", "refcoco+", "refcocog", "grefcoco"]: + item["file_name"] = os.path.join( + base_image_dir, + "images/mscoco/images/train2014", + item["file_name"], + ) + refer_seg_ds["images"].append(item) + refer_seg_ds["annotations"] = refer_api.Anns # anns_val + + img2refs = {} + for ref in refs_val: + image_id = ref["image_id"] + img2refs[image_id] = img2refs.get(image_id, []) + [ + ref, + ] + refer_seg_ds["img2refs"] = img2refs + self.refer_seg_ds = refer_seg_ds + self.data_type = "refer_seg" + elif val_dataset=="ade": + ds = "ade" + self.classes, self.images, self.labels = init_ade20k(base_image_dir) + self.data_type = "sem_seg" + + + self.ds = ds + self.tokenizer = tokenizer + self.transform = ResizeLongestSide(1024) + self.image_preprocessor = transforms.Compose([ + transforms.ToTensor(), + transforms.Resize((image_size, image_size), interpolation=3), + transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) + ]) + def __len__(self): + if self.data_type == "refer_seg": + return len(self.refer_seg_ds["images"]) + else: + return len(self.images) + + def preprocess(self, x: torch.Tensor) -> torch.Tensor: + """Normalize pixel values and pad to a square input.""" + # Normalize colors + x = (x - self.pixel_mean) / self.pixel_std + + if self.model_type=="effi" or self.model_type=="sam2": + x = F.interpolate(x.unsqueeze(0), (self.img_size, self.img_size), mode="bilinear").squeeze(0) + else: + # Pad + h, w = x.shape[-2:] + padh = self.img_size - h + padw = self.img_size - w + x = F.pad(x, (0, padw, 0, padh)) + return x + + def __getitem__(self, idx): + if self.data_type == "refer_seg": + refer_seg_ds = self.refer_seg_ds + images = refer_seg_ds["images"] + annotations = refer_seg_ds["annotations"] + img2refs = refer_seg_ds["img2refs"] + + image_info = images[idx] + image_path = image_info["file_name"] + image_id = image_info["id"] + + refs = img2refs[image_id] + if len(refs) == 0: + raise ValueError("image {} has no refs".format(image_id)) + + sents = [] + ann_ids = [] + for ref in refs: + for sent in ref["sentences"]: + sents.append(sent["sent"].strip().lower()) + ann_ids.append(ref["ann_id"]) + + sampled_sents = sents + sampled_ann_ids = ann_ids + image = cv2.imread(image_path) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + is_sentence = False + + elif self.data_type == "sem_seg": + image_path = self.images[idx] + label_path = self.labels[idx] + label = Image.open(label_path) + label = np.array(label) + label[label == 0] = 255 + label -= 1 + label[label == 254] = 255 + unique_label = np.unique(label).tolist() + if 255 in unique_label: + unique_label.remove(255) + + sampled_sents = [self.classes[class_id] for class_id in unique_label] + + img = cv2.imread(image_path) + image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + class_ids = unique_label + label = torch.from_numpy(label).long() + masks = [] + for class_id in class_ids: + masks.append(label == class_id) + masks = torch.stack(masks, dim=0) + + # preprocess image for evf + image_evf = self.image_preprocessor(image) + + # preprocess image for sam + image = self.transform.apply_image(image) + resize = image.shape[:2] + image = self.preprocess(torch.from_numpy(image).permute(2, 0, 1).contiguous()) + + if self.data_type == "refer_seg": + masks = [] + for i, ann_id in enumerate(sampled_ann_ids): + ann = annotations[ann_id] + if len(ann["segmentation"]) == 0 and sampled_sents[i] != "": + m = np.zeros((image_info["height"], image_info["width"], 1)) + else: + if type(ann["segmentation"][0]) == list: # polygon + rle = mask.frPyObjects( + ann["segmentation"], + image_info["height"], + image_info["width"], + ) + else: + rle = ann["segmentation"] + for i in range(len(rle)): + if not isinstance(rle[i]["counts"], bytes): + rle[i]["counts"] = rle[i]["counts"].encode() + m = mask.decode(rle) + m = np.sum( + m, axis=2 + ) # sometimes there are multiple binary map (corresponding to multiple segs) + m = m.astype(np.uint8) # convert to np.uint8 + masks.append(m) + + if not isinstance(masks, torch.Tensor): + masks = np.stack(masks, axis=0) + masks = torch.from_numpy(masks) + labels = torch.ones(masks.shape[1], masks.shape[2]) * self.ignore_label + inference = True + + return ( + image_path, + image, + image_evf, + masks, + labels, + resize, + sampled_sents, + inference, + ) diff --git a/py/evf-sam/utils/grefcoco.py b/py/evf-sam/utils/grefcoco.py new file mode 100644 index 0000000..7e1a49f --- /dev/null +++ b/py/evf-sam/utils/grefcoco.py @@ -0,0 +1,193 @@ +import contextlib +import copy +import io +import logging +import os +import random + +import numpy as np +import pycocotools.mask as mask_util +from detectron2.structures import Boxes, BoxMode, PolygonMasks, RotatedBoxes +from detectron2.utils.file_io import PathManager +from fvcore.common.timer import Timer +from PIL import Image + +""" +This file contains functions to parse RefCOCO-format annotations into dicts in "Detectron2 format". +""" + + +logger = logging.getLogger(__name__) + +__all__ = ["load_refcoco_json"] + + +def load_grefcoco_json( + refer_root, + dataset_name, + splitby, + split, + image_root, + extra_annotation_keys=None, + extra_refer_keys=None, +): + if dataset_name == "refcocop": + dataset_name = "refcoco+" + if dataset_name == "refcoco" or dataset_name == "refcoco+": + splitby == "unc" + if dataset_name == "refcocog": + assert splitby == "umd" or splitby == "google" + + dataset_id = "_".join([dataset_name, splitby, split]) + + from .grefer import G_REFER + + logger.info("Loading dataset {} ({}-{}) ...".format(dataset_name, splitby, split)) + logger.info("Refcoco root: {}".format(refer_root)) + timer = Timer() + refer_root = PathManager.get_local_path(refer_root) + with contextlib.redirect_stdout(io.StringIO()): + refer_api = G_REFER(data_root=refer_root, dataset=dataset_name, splitBy=splitby) + if timer.seconds() > 1: + logger.info( + "Loading {} takes {:.2f} seconds.".format(dataset_id, timer.seconds()) + ) + + ref_ids = refer_api.getRefIds(split=split) + img_ids = refer_api.getImgIds(ref_ids) + refs = refer_api.loadRefs(ref_ids) + imgs = [refer_api.loadImgs(ref["image_id"])[0] for ref in refs] + anns = [refer_api.loadAnns(ref["ann_id"]) for ref in refs] + imgs_refs_anns = list(zip(imgs, refs, anns)) + + logger.info( + "Loaded {} images, {} referring object sets in G_RefCOCO format from {}".format( + len(img_ids), len(ref_ids), dataset_id + ) + ) + + dataset_dicts = [] + + ann_keys = ["iscrowd", "bbox", "category_id"] + (extra_annotation_keys or []) + ref_keys = ["raw", "sent_id"] + (extra_refer_keys or []) + + ann_lib = {} + + NT_count = 0 + MT_count = 0 + + for img_dict, ref_dict, anno_dicts in imgs_refs_anns: + record = {} + record["source"] = "grefcoco" + record["file_name"] = os.path.join(image_root, img_dict["file_name"]) + record["height"] = img_dict["height"] + record["width"] = img_dict["width"] + image_id = record["image_id"] = img_dict["id"] + + # Check that information of image, ann and ref match each other + # This fails only when the data parsing logic or the annotation file is buggy. + assert ref_dict["image_id"] == image_id + assert ref_dict["split"] == split + if not isinstance(ref_dict["ann_id"], list): + ref_dict["ann_id"] = [ref_dict["ann_id"]] + + # No target samples + if None in anno_dicts: + assert anno_dicts == [None] + assert ref_dict["ann_id"] == [-1] + record["empty"] = True + obj = {key: None for key in ann_keys if key in ann_keys} + obj["bbox_mode"] = BoxMode.XYWH_ABS + obj["empty"] = True + obj = [obj] + + # Multi target samples + else: + record["empty"] = False + obj = [] + for anno_dict in anno_dicts: + ann_id = anno_dict["id"] + if anno_dict["iscrowd"]: + continue + assert anno_dict["image_id"] == image_id + assert ann_id in ref_dict["ann_id"] + + if ann_id in ann_lib: + ann = ann_lib[ann_id] + else: + ann = {key: anno_dict[key] for key in ann_keys if key in anno_dict} + ann["bbox_mode"] = BoxMode.XYWH_ABS + ann["empty"] = False + + segm = anno_dict.get("segmentation", None) + assert segm # either list[list[float]] or dict(RLE) + if isinstance(segm, dict): + if isinstance(segm["counts"], list): + # convert to compressed RLE + segm = mask_util.frPyObjects(segm, *segm["size"]) + else: + # filter out invalid polygons (< 3 points) + segm = [ + poly + for poly in segm + if len(poly) % 2 == 0 and len(poly) >= 6 + ] + if len(segm) == 0: + num_instances_without_valid_segmentation += 1 + continue # ignore this instance + ann["segmentation"] = segm + ann_lib[ann_id] = ann + + obj.append(ann) + + record["annotations"] = obj + + # Process referring expressions + sents = ref_dict["sentences"] + for sent in sents: + ref_record = record.copy() + ref = {key: sent[key] for key in ref_keys if key in sent} + ref["ref_id"] = ref_dict["ref_id"] + ref_record["sentence"] = ref + dataset_dicts.append(ref_record) + # if ref_record['empty']: + # NT_count += 1 + # else: + # MT_count += 1 + + # logger.info("NT samples: %d, MT samples: %d", NT_count, MT_count) + + # Debug mode + # return dataset_dicts[:100] + + return dataset_dicts + + +if __name__ == "__main__": + """ + Test the COCO json dataset loader. + + Usage: + python -m detectron2.data.datasets.coco \ + path/to/json path/to/image_root dataset_name + + "dataset_name" can be "coco_2014_minival_100", or other + pre-registered ones + """ + import sys + + REFCOCO_PATH = "/mnt/lustre/hhding/code/ReLA/datasets" + COCO_TRAIN_2014_IMAGE_ROOT = "/mnt/lustre/hhding/code/ReLA/datasets/images" + REFCOCO_DATASET = "grefcoco" + REFCOCO_SPLITBY = "unc" + REFCOCO_SPLIT = "train" + + + dicts = load_grefcoco_json( + REFCOCO_PATH, + REFCOCO_DATASET, + REFCOCO_SPLITBY, + REFCOCO_SPLIT, + COCO_TRAIN_2014_IMAGE_ROOT, + ) + print(1) diff --git a/py/evf-sam/utils/grefer.py b/py/evf-sam/utils/grefer.py new file mode 100644 index 0000000..3c881c5 --- /dev/null +++ b/py/evf-sam/utils/grefer.py @@ -0,0 +1,352 @@ +""" +grefer v0.1 +This interface provides access to gRefCOCO. + +The following API functions are defined: +G_REFER - REFER api class +getRefIds - get ref ids that satisfy given filter conditions. +getAnnIds - get ann ids that satisfy given filter conditions. +getImgIds - get image ids that satisfy given filter conditions. +getCatIds - get category ids that satisfy given filter conditions. +loadRefs - load refs with the specified ref ids. +loadAnns - load anns with the specified ann ids. +loadImgs - load images with the specified image ids. +loadCats - load category names with the specified category ids. +getRefBox - get ref's bounding box [x, y, w, h] given the ref_id +showRef - show image, segmentation or box of the referred object with the ref +getMaskByRef - get mask and area of the referred object given ref or ref ids +getMask - get mask and area of the referred object given ref +showMask - show mask of the referred object given ref +""" + +import itertools +import json +import os.path as osp +import pickle +import time + +import matplotlib.pyplot as plt +import numpy as np +import skimage.io as io +from matplotlib.collections import PatchCollection +from matplotlib.patches import Polygon, Rectangle +from pycocotools import mask + + +class G_REFER: + def __init__(self, data_root, dataset="grefcoco", splitBy="unc"): + # provide data_root folder which contains grefcoco + print("loading dataset %s into memory..." % dataset) + self.ROOT_DIR = osp.abspath(osp.dirname(__file__)) + self.DATA_DIR = osp.join(data_root, dataset) + if dataset in ["grefcoco"]: + self.IMAGE_DIR = osp.join(data_root, "images/train2014") + else: + raise KeyError("No refer dataset is called [%s]" % dataset) + + tic = time.time() + + # load refs from data/dataset/refs(dataset).json + self.data = {} + self.data["dataset"] = dataset + + ref_file = osp.join(self.DATA_DIR, f"grefs({splitBy}).p") + if osp.exists(ref_file): + self.data["refs"] = pickle.load(open(ref_file, "rb"), fix_imports=True) + else: + ref_file = osp.join(self.DATA_DIR, f"grefs({splitBy}).json") + if osp.exists(ref_file): + self.data["refs"] = json.load(open(ref_file, "rb")) + else: + raise FileNotFoundError("JSON file not found") + + # load annotations from data/dataset/instances.json + instances_file = osp.join(self.DATA_DIR, "instances.json") + instances = json.load(open(instances_file, "r")) + self.data["images"] = instances["images"] + self.data["annotations"] = instances["annotations"] + self.data["categories"] = instances["categories"] + + # create index + self.createIndex() + print("DONE (t=%.2fs)" % (time.time() - tic)) + + @staticmethod + def _toList(x): + return x if isinstance(x, list) else [x] + + @staticmethod + def match_any(a, b): + a = a if isinstance(a, list) else [a] + b = b if isinstance(b, list) else [b] + return set(a) & set(b) + + def createIndex(self): + # create sets of mapping + # 1) Refs: {ref_id: ref} + # 2) Anns: {ann_id: ann} + # 3) Imgs: {image_id: image} + # 4) Cats: {category_id: category_name} + # 5) Sents: {sent_id: sent} + # 6) imgToRefs: {image_id: refs} + # 7) imgToAnns: {image_id: anns} + # 8) refToAnn: {ref_id: ann} + # 9) annToRef: {ann_id: ref} + # 10) catToRefs: {category_id: refs} + # 11) sentToRef: {sent_id: ref} + # 12) sentToTokens: {sent_id: tokens} + print("creating index...") + # fetch info from instances + Anns, Imgs, Cats, imgToAnns = {}, {}, {}, {} + Anns[-1] = None + for ann in self.data["annotations"]: + Anns[ann["id"]] = ann + imgToAnns[ann["image_id"]] = imgToAnns.get(ann["image_id"], []) + [ann] + for img in self.data["images"]: + Imgs[img["id"]] = img + for cat in self.data["categories"]: + Cats[cat["id"]] = cat["name"] + + # fetch info from refs + Refs, imgToRefs, refToAnn, annToRef, catToRefs = {}, {}, {}, {}, {} + Sents, sentToRef, sentToTokens = {}, {}, {} + availableSplits = [] + for ref in self.data["refs"]: + # ids + ref_id = ref["ref_id"] + ann_id = ref["ann_id"] + category_id = ref["category_id"] + image_id = ref["image_id"] + + if ref["split"] not in availableSplits: + availableSplits.append(ref["split"]) + + # add mapping related to ref + if ref_id in Refs: + print("Duplicate ref id") + Refs[ref_id] = ref + imgToRefs[image_id] = imgToRefs.get(image_id, []) + [ref] + + category_id = self._toList(category_id) + added_cats = [] + for cat in category_id: + if cat not in added_cats: + added_cats.append(cat) + catToRefs[cat] = catToRefs.get(cat, []) + [ref] + + ann_id = self._toList(ann_id) + refToAnn[ref_id] = [Anns[ann] for ann in ann_id] + for ann_id_n in ann_id: + annToRef[ann_id_n] = annToRef.get(ann_id_n, []) + [ref] + + # add mapping of sent + for sent in ref["sentences"]: + Sents[sent["sent_id"]] = sent + sentToRef[sent["sent_id"]] = ref + sentToTokens[sent["sent_id"]] = sent["tokens"] + + # create class members + self.Refs = Refs + self.Anns = Anns + self.Imgs = Imgs + self.Cats = Cats + self.Sents = Sents + self.imgToRefs = imgToRefs + self.imgToAnns = imgToAnns + self.refToAnn = refToAnn + self.annToRef = annToRef + self.catToRefs = catToRefs + self.sentToRef = sentToRef + self.sentToTokens = sentToTokens + self.availableSplits = availableSplits + print("index created.") + + def getRefIds(self, image_ids=[], cat_ids=[], split=[]): + image_ids = self._toList(image_ids) + cat_ids = self._toList(cat_ids) + split = self._toList(split) + + for s in split: + if s not in self.availableSplits: + raise ValueError(f"Invalid split name: {s}") + + refs = self.data["refs"] + + if len(image_ids) > 0: + lists = [self.imgToRefs[image_id] for image_id in image_ids] + refs = list(itertools.chain.from_iterable(lists)) + if len(cat_ids) > 0: + refs = [ref for ref in refs if self.match_any(ref["category_id"], cat_ids)] + if len(split) > 0: + refs = [ref for ref in refs if ref["split"] in split] + + ref_ids = [ref["ref_id"] for ref in refs] + return ref_ids + + def getAnnIds(self, image_ids=[], ref_ids=[]): + image_ids = self._toList(image_ids) + ref_ids = self._toList(ref_ids) + + if any([len(image_ids), len(ref_ids)]): + if len(image_ids) > 0: + lists = [ + self.imgToAnns[image_id] + for image_id in image_ids + if image_id in self.imgToAnns + ] + anns = list(itertools.chain.from_iterable(lists)) + else: + anns = self.data["annotations"] + ann_ids = [ann["id"] for ann in anns] + if len(ref_ids) > 0: + lists = [self.Refs[ref_id]["ann_id"] for ref_id in ref_ids] + anns_by_ref_id = list(itertools.chain.from_iterable(lists)) + ann_ids = list(set(ann_ids).intersection(set(anns_by_ref_id))) + else: + ann_ids = [ann["id"] for ann in self.data["annotations"]] + + return ann_ids + + def getImgIds(self, ref_ids=[]): + ref_ids = self._toList(ref_ids) + + if len(ref_ids) > 0: + image_ids = list(set([self.Refs[ref_id]["image_id"] for ref_id in ref_ids])) + else: + image_ids = self.Imgs.keys() + return image_ids + + def getCatIds(self): + return self.Cats.keys() + + def loadRefs(self, ref_ids=[]): + return [self.Refs[ref_id] for ref_id in self._toList(ref_ids)] + + def loadAnns(self, ann_ids=[]): + if isinstance(ann_ids, str): + ann_ids = int(ann_ids) + return [self.Anns[ann_id] for ann_id in self._toList(ann_ids)] + + def loadImgs(self, image_ids=[]): + return [self.Imgs[image_id] for image_id in self._toList(image_ids)] + + def loadCats(self, cat_ids=[]): + return [self.Cats[cat_id] for cat_id in self._toList(cat_ids)] + + def getRefBox(self, ref_id): + anns = self.refToAnn[ref_id] + return [ann["bbox"] for ann in anns] # [x, y, w, h] + + def showRef(self, ref, seg_box="seg"): + ax = plt.gca() + # show image + image = self.Imgs[ref["image_id"]] + I = io.imread(osp.join(self.IMAGE_DIR, image["file_name"])) + ax.imshow(I) + # show refer expression + for sid, sent in enumerate(ref["sentences"]): + print("%s. %s" % (sid + 1, sent["sent"])) + # show segmentations + if seg_box == "seg": + ann_id = ref["ann_id"] + ann = self.Anns[ann_id] + polygons = [] + color = [] + c = "none" + if type(ann["segmentation"][0]) == list: + # polygon used for refcoco* + for seg in ann["segmentation"]: + poly = np.array(seg).reshape((len(seg) / 2, 2)) + polygons.append(Polygon(poly, True, alpha=0.4)) + color.append(c) + p = PatchCollection( + polygons, + facecolors=color, + edgecolors=(1, 1, 0, 0), + linewidths=3, + alpha=1, + ) + ax.add_collection(p) # thick yellow polygon + p = PatchCollection( + polygons, + facecolors=color, + edgecolors=(1, 0, 0, 0), + linewidths=1, + alpha=1, + ) + ax.add_collection(p) # thin red polygon + else: + # mask used for refclef + rle = ann["segmentation"] + m = mask.decode(rle) + img = np.ones((m.shape[0], m.shape[1], 3)) + color_mask = np.array([2.0, 166.0, 101.0]) / 255 + for i in range(3): + img[:, :, i] = color_mask[i] + ax.imshow(np.dstack((img, m * 0.5))) + # show bounding-box + elif seg_box == "box": + ann_id = ref["ann_id"] + ann = self.Anns[ann_id] + bbox = self.getRefBox(ref["ref_id"]) + box_plot = Rectangle( + (bbox[0], bbox[1]), + bbox[2], + bbox[3], + fill=False, + edgecolor="green", + linewidth=3, + ) + ax.add_patch(box_plot) + + def getMask(self, ann): + if not ann: + return None + if ann["iscrowd"]: + raise ValueError("Crowd object") + image = self.Imgs[ann["image_id"]] + if type(ann["segmentation"][0]) == list: # polygon + rle = mask.frPyObjects(ann["segmentation"], image["height"], image["width"]) + else: + rle = ann["segmentation"] + + m = mask.decode(rle) + m = np.sum( + m, axis=2 + ) # sometimes there are multiple binary map (corresponding to multiple segs) + m = m.astype(np.uint8) # convert to np.uint8 + # compute area + area = sum(mask.area(rle)) # should be close to ann['area'] + return {"mask": m, "area": area} + + def getMaskByRef(self, ref=None, ref_id=None, merge=False): + if not ref and not ref_id: + raise ValueError + if ref: + ann_ids = ref["ann_id"] + ref_id = ref["ref_id"] + else: + ann_ids = self.getAnnIds(ref_ids=ref_id) + + if ann_ids == [-1]: + img = self.Imgs[self.Refs[ref_id]["image_id"]] + return { + "mask": np.zeros([img["height"], img["width"]], dtype=np.uint8), + "empty": True, + } + + anns = self.loadAnns(ann_ids) + mask_list = [self.getMask(ann) for ann in anns if not ann["iscrowd"]] + + if merge: + merged_masks = sum([mask["mask"] for mask in mask_list]) + merged_masks[np.where(merged_masks > 1)] = 1 + return {"mask": merged_masks, "empty": False} + else: + return mask_list + + def showMask(self, ref): + M = self.getMask(ref) + msk = M["mask"] + ax = plt.gca() + ax.imshow(msk) diff --git a/py/evf-sam/utils/refer.py b/py/evf-sam/utils/refer.py new file mode 100644 index 0000000..3b4cea7 --- /dev/null +++ b/py/evf-sam/utils/refer.py @@ -0,0 +1,391 @@ +__author__ = "licheng" + +""" +This interface provides access to four datasets: +1) refclef +2) refcoco +3) refcoco+ +4) refcocog +split by unc and google + +The following API functions are defined: +REFER - REFER api class +getRefIds - get ref ids that satisfy given filter conditions. +getAnnIds - get ann ids that satisfy given filter conditions. +getImgIds - get image ids that satisfy given filter conditions. +getCatIds - get category ids that satisfy given filter conditions. +loadRefs - load refs with the specified ref ids. +loadAnns - load anns with the specified ann ids. +loadImgs - load images with the specified image ids. +loadCats - load category names with the specified category ids. +getRefBox - get ref's bounding box [x, y, w, h] given the ref_id +showRef - show image, segmentation or box of the referred object with the ref +getMask - get mask and area of the referred object given ref +showMask - show mask of the referred object given ref +""" + +import itertools +import json +import os.path as osp +import pickle +import sys +import time +from pprint import pprint + +import matplotlib.pyplot as plt +import numpy as np +import skimage.io as io +from matplotlib.collections import PatchCollection +from matplotlib.patches import Polygon, Rectangle +from pycocotools import mask + + +class REFER: + def __init__(self, data_root, dataset="refcoco", splitBy="unc"): + # provide data_root folder which contains refclef, refcoco, refcoco+ and refcocog + # also provide dataset name and splitBy information + # e.g., dataset = 'refcoco', splitBy = 'unc' + print("loading dataset %s into memory..." % dataset) + self.ROOT_DIR = osp.abspath(osp.dirname(__file__)) + self.DATA_DIR = osp.join(data_root, dataset) + if dataset in ["refcoco", "refcoco+", "refcocog"]: + self.IMAGE_DIR = osp.join(data_root, "images/mscoco/images/train2014") + elif dataset == "refclef": + self.IMAGE_DIR = osp.join(data_root, "images/saiapr_tc-12") + else: + print("No refer dataset is called [%s]" % dataset) + sys.exit() + + self.dataset = dataset + + # load refs from data/dataset/refs(dataset).json + tic = time.time() + + ref_file = osp.join(self.DATA_DIR, "refs(" + splitBy + ").p") + print("ref_file: ", ref_file) + self.data = {} + self.data["dataset"] = dataset + self.data["refs"] = pickle.load(open(ref_file, "rb")) + + # load annotations from data/dataset/instances.json + instances_file = osp.join(self.DATA_DIR, "instances.json") + instances = json.load(open(instances_file, "rb")) + self.data["images"] = instances["images"] + self.data["annotations"] = instances["annotations"] + self.data["categories"] = instances["categories"] + + # create index + self.createIndex() + print("DONE (t=%.2fs)" % (time.time() - tic)) + + def createIndex(self): + # create sets of mapping + # 1) Refs: {ref_id: ref} + # 2) Anns: {ann_id: ann} + # 3) Imgs: {image_id: image} + # 4) Cats: {category_id: category_name} + # 5) Sents: {sent_id: sent} + # 6) imgToRefs: {image_id: refs} + # 7) imgToAnns: {image_id: anns} + # 8) refToAnn: {ref_id: ann} + # 9) annToRef: {ann_id: ref} + # 10) catToRefs: {category_id: refs} + # 11) sentToRef: {sent_id: ref} + # 12) sentToTokens: {sent_id: tokens} + print("creating index...") + # fetch info from instances + Anns, Imgs, Cats, imgToAnns = {}, {}, {}, {} + for ann in self.data["annotations"]: + Anns[ann["id"]] = ann + imgToAnns[ann["image_id"]] = imgToAnns.get(ann["image_id"], []) + [ann] + for img in self.data["images"]: + Imgs[img["id"]] = img + for cat in self.data["categories"]: + Cats[cat["id"]] = cat["name"] + + # fetch info from refs + Refs, imgToRefs, refToAnn, annToRef, catToRefs = {}, {}, {}, {}, {} + Sents, sentToRef, sentToTokens = {}, {}, {} + for ref in self.data["refs"]: + # ids + ref_id = ref["ref_id"] + ann_id = ref["ann_id"] + category_id = ref["category_id"] + image_id = ref["image_id"] + + # add mapping related to ref + Refs[ref_id] = ref + imgToRefs[image_id] = imgToRefs.get(image_id, []) + [ref] + catToRefs[category_id] = catToRefs.get(category_id, []) + [ref] + refToAnn[ref_id] = Anns[ann_id] + annToRef[ann_id] = ref + + # add mapping of sent + for sent in ref["sentences"]: + Sents[sent["sent_id"]] = sent + sentToRef[sent["sent_id"]] = ref + sentToTokens[sent["sent_id"]] = sent["tokens"] + + # create class members + self.Refs = Refs + self.Anns = Anns + self.Imgs = Imgs + self.Cats = Cats + self.Sents = Sents + self.imgToRefs = imgToRefs + self.imgToAnns = imgToAnns + self.refToAnn = refToAnn + self.annToRef = annToRef + self.catToRefs = catToRefs + self.sentToRef = sentToRef + self.sentToTokens = sentToTokens + print("index created.") + + def getRefIds(self, image_ids=[], cat_ids=[], ref_ids=[], split=""): + image_ids = image_ids if type(image_ids) == list else [image_ids] + cat_ids = cat_ids if type(cat_ids) == list else [cat_ids] + ref_ids = ref_ids if type(ref_ids) == list else [ref_ids] + + if len(image_ids) == len(cat_ids) == len(ref_ids) == len(split) == 0: + refs = self.data["refs"] + else: + if not len(image_ids) == 0: + refs = [self.imgToRefs[image_id] for image_id in image_ids] + else: + refs = self.data["refs"] + if not len(cat_ids) == 0: + refs = [ref for ref in refs if ref["category_id"] in cat_ids] + if not len(ref_ids) == 0: + refs = [ref for ref in refs if ref["ref_id"] in ref_ids] + if not len(split) == 0: + if split in ["testA", "testB", "testC"]: + refs = [ + ref for ref in refs if split[-1] in ref["split"] + ] # we also consider testAB, testBC, ... + elif split in ["testAB", "testBC", "testAC"]: + refs = [ + ref for ref in refs if ref["split"] == split + ] # rarely used I guess... + elif split == "test": + refs = [ref for ref in refs if "test" in ref["split"]] + elif split == "train" or split == "val": + refs = [ref for ref in refs if ref["split"] == split] + else: + print("No such split [%s]" % split) + sys.exit() + ref_ids = [ref["ref_id"] for ref in refs] + return ref_ids + + def getAnnIds(self, image_ids=[], cat_ids=[], ref_ids=[]): + image_ids = image_ids if type(image_ids) == list else [image_ids] + cat_ids = cat_ids if type(cat_ids) == list else [cat_ids] + ref_ids = ref_ids if type(ref_ids) == list else [ref_ids] + + if len(image_ids) == len(cat_ids) == len(ref_ids) == 0: + ann_ids = [ann["id"] for ann in self.data["annotations"]] + else: + if not len(image_ids) == 0: + lists = [ + self.imgToAnns[image_id] + for image_id in image_ids + if image_id in self.imgToAnns + ] # list of [anns] + anns = list(itertools.chain.from_iterable(lists)) + else: + anns = self.data["annotations"] + if not len(cat_ids) == 0: + anns = [ann for ann in anns if ann["category_id"] in cat_ids] + ann_ids = [ann["id"] for ann in anns] + if not len(ref_ids) == 0: + ids = set(ann_ids).intersection( + set([self.Refs[ref_id]["ann_id"] for ref_id in ref_ids]) + ) + return ann_ids + + def getImgIds(self, ref_ids=[]): + ref_ids = ref_ids if type(ref_ids) == list else [ref_ids] + + if not len(ref_ids) == 0: + image_ids = list(set([self.Refs[ref_id]["image_id"] for ref_id in ref_ids])) + else: + image_ids = self.Imgs.keys() + return image_ids + + def getCatIds(self): + return self.Cats.keys() + + def loadRefs(self, ref_ids=[]): + if type(ref_ids) == list: + return [self.Refs[ref_id] for ref_id in ref_ids] + elif type(ref_ids) == int: + return [self.Refs[ref_ids]] + + def loadAnns(self, ann_ids=[]): + if type(ann_ids) == list: + return [self.Anns[ann_id] for ann_id in ann_ids] + elif type(ann_ids) == int or type(ann_ids) == unicode: + return [self.Anns[ann_ids]] + + def loadImgs(self, image_ids=[]): + if type(image_ids) == list: + return [self.Imgs[image_id] for image_id in image_ids] + elif type(image_ids) == int: + return [self.Imgs[image_ids]] + + def loadCats(self, cat_ids=[]): + if type(cat_ids) == list: + return [self.Cats[cat_id] for cat_id in cat_ids] + elif type(cat_ids) == int: + return [self.Cats[cat_ids]] + + def getRefBox(self, ref_id): + ref = self.Refs[ref_id] + ann = self.refToAnn[ref_id] + return ann["bbox"] # [x, y, w, h] + + def showRef(self, ref, seg_box="seg"): + ax = plt.gca() + # show image + image = self.Imgs[ref["image_id"]] + I = io.imread(osp.join(self.IMAGE_DIR, image["file_name"])) + ax.imshow(I) + # show refer expression + for sid, sent in enumerate(ref["sentences"]): + print("%s. %s" % (sid + 1, sent["sent"])) + # show segmentations + if seg_box == "seg": + ann_id = ref["ann_id"] + ann = self.Anns[ann_id] + polygons = [] + color = [] + c = "none" + if type(ann["segmentation"][0]) == list: + # polygon used for refcoco* + for seg in ann["segmentation"]: + poly = np.array(seg).reshape((len(seg) / 2, 2)) + polygons.append(Polygon(poly, True, alpha=0.4)) + color.append(c) + p = PatchCollection( + polygons, + facecolors=color, + edgecolors=(1, 1, 0, 0), + linewidths=3, + alpha=1, + ) + ax.add_collection(p) # thick yellow polygon + p = PatchCollection( + polygons, + facecolors=color, + edgecolors=(1, 0, 0, 0), + linewidths=1, + alpha=1, + ) + ax.add_collection(p) # thin red polygon + else: + # mask used for refclef + rle = ann["segmentation"] + m = mask.decode(rle) + img = np.ones((m.shape[0], m.shape[1], 3)) + color_mask = np.array([2.0, 166.0, 101.0]) / 255 + for i in range(3): + img[:, :, i] = color_mask[i] + ax.imshow(np.dstack((img, m * 0.5))) + # show bounding-box + elif seg_box == "box": + ann_id = ref["ann_id"] + ann = self.Anns[ann_id] + bbox = self.getRefBox(ref["ref_id"]) + box_plot = Rectangle( + (bbox[0], bbox[1]), + bbox[2], + bbox[3], + fill=False, + edgecolor="green", + linewidth=3, + ) + ax.add_patch(box_plot) + + def getMask(self, ref): + # return mask, area and mask-center + ann = self.refToAnn[ref["ref_id"]] + image = self.Imgs[ref["image_id"]] + if type(ann["segmentation"][0]) == list: # polygon + rle = mask.frPyObjects(ann["segmentation"], image["height"], image["width"]) + else: + rle = ann["segmentation"] + m = mask.decode(rle) + m = np.sum( + m, axis=2 + ) # sometimes there are multiple binary map (corresponding to multiple segs) + m = m.astype(np.uint8) # convert to np.uint8 + # compute area + area = sum(mask.area(rle)) # should be close to ann['area'] + return {"mask": m, "area": area} + # # position + # position_x = np.mean(np.where(m==1)[1]) # [1] means columns (matlab style) -> x (c style) + # position_y = np.mean(np.where(m==1)[0]) # [0] means rows (matlab style) -> y (c style) + # # mass position (if there were multiple regions, we use the largest one.) + # label_m = label(m, connectivity=m.ndim) + # regions = regionprops(label_m) + # if len(regions) > 0: + # largest_id = np.argmax(np.array([props.filled_area for props in regions])) + # largest_props = regions[largest_id] + # mass_y, mass_x = largest_props.centroid + # else: + # mass_x, mass_y = position_x, position_y + # # if centroid is not in mask, we find the closest point to it from mask + # if m[mass_y, mass_x] != 1: + # print('Finding closes mask point ...') + # kernel = np.ones((10, 10),np.uint8) + # me = cv2.erode(m, kernel, iterations = 1) + # points = zip(np.where(me == 1)[0].tolist(), np.where(me == 1)[1].tolist()) # row, col style + # points = np.array(points) + # dist = np.sum((points - (mass_y, mass_x))**2, axis=1) + # id = np.argsort(dist)[0] + # mass_y, mass_x = points[id] + # # return + # return {'mask': m, 'area': area, 'position_x': position_x, 'position_y': position_y, 'mass_x': mass_x, 'mass_y': mass_y} + # # show image and mask + # I = io.imread(osp.join(self.IMAGE_DIR, image['file_name'])) + # plt.figure() + # plt.imshow(I) + # ax = plt.gca() + # img = np.ones( (m.shape[0], m.shape[1], 3) ) + # color_mask = np.array([2.0,166.0,101.0])/255 + # for i in range(3): + # img[:,:,i] = color_mask[i] + # ax.imshow(np.dstack( (img, m*0.5) )) + # plt.show() + + def showMask(self, ref): + M = self.getMask(ref) + msk = M["mask"] + ax = plt.gca() + ax.imshow(msk) + + +if __name__ == "__main__": + refer = REFER(dataset="refcocog", splitBy="google") + ref_ids = refer.getRefIds() + print(len(ref_ids)) + + print(len(refer.Imgs)) + print(len(refer.imgToRefs)) + + ref_ids = refer.getRefIds(split="train") + print("There are %s training referred objects." % len(ref_ids)) + + for ref_id in ref_ids: + ref = refer.loadRefs(ref_id)[0] + if len(ref["sentences"]) < 2: + continue + + pprint(ref) + print("The label is %s." % refer.Cats[ref["category_id"]]) + plt.figure() + refer.showRef(ref, seg_box="box") + plt.show() + + # plt.figure() + # refer.showMask(ref) + # plt.show() diff --git a/py/evf-sam/utils/refer_seg_dataset.py b/py/evf-sam/utils/refer_seg_dataset.py new file mode 100644 index 0000000..fc8bdcb --- /dev/null +++ b/py/evf-sam/utils/refer_seg_dataset.py @@ -0,0 +1,254 @@ +import os +import random + +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from pycocotools import mask + +from model.segment_anything.utils.transforms import ResizeLongestSide + +from .grefer import G_REFER +from .refer import REFER +from torchvision import transforms + + +class ReferSegDataset(torch.utils.data.Dataset): + pixel_mean = torch.Tensor([123.675, 116.28, 103.53]).view(-1, 1, 1) + pixel_std = torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1) + img_size = 1024 + ignore_label = 255 + + def __init__( + self, + base_image_dir, + tokenizer, + samples_per_epoch=500 * 8 * 2 * 10, + precision: str = "fp32", + image_size: int = 224, + num_classes_per_sample: int = 3, + exclude_val=False, + refer_seg_data="refclef||refcoco||refcoco+||refcocog", + model_type="ori", + transform=ResizeLongestSide(1024), + ): + self.model_type = model_type + self.exclude_val = exclude_val + self.samples_per_epoch = samples_per_epoch + self.num_classes_per_sample = num_classes_per_sample + + self.base_image_dir = base_image_dir + self.tokenizer = tokenizer + self.precision = precision + self.transform = transform + self.image_preprocessor = transforms.Compose([ + transforms.ToTensor(), + transforms.Resize((image_size, image_size), interpolation=3), + transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) + ]) + + DATA_DIR = os.path.join(base_image_dir, "refer_seg") + self.refer_seg_ds_list = refer_seg_data.split( + "||" + ) # ['refclef', 'refcoco', 'refcoco+', 'refcocog'] + self.refer_seg_data = {} + for ds in self.refer_seg_ds_list: + if ds == "refcocog": + splitBy = "umd" + else: + splitBy = "unc" + + if ds == "grefcoco": + refer_api = G_REFER(DATA_DIR, ds, splitBy) + else: + refer_api = REFER(DATA_DIR, ds, splitBy) + + ref_ids_train = refer_api.getRefIds(split="train") + images_ids_train = refer_api.getImgIds(ref_ids=ref_ids_train) + refs_train = refer_api.loadRefs(ref_ids=ref_ids_train) + + refer_seg_ds = {} + refer_seg_ds["images"] = [] + loaded_images = refer_api.loadImgs(image_ids=images_ids_train) + + for item in loaded_images: + item = item.copy() + if ds == "refclef": + item["file_name"] = os.path.join( + DATA_DIR, "images/saiapr_tc-12", item["file_name"] + ) + else: + item["file_name"] = os.path.join( + DATA_DIR, "images/mscoco/images/train2014", item["file_name"] + ) + refer_seg_ds["images"].append(item) + refer_seg_ds["annotations"] = refer_api.Anns # anns_train + + print( + "dataset {} (refs {}) (train split) has {} images and {} annotations.".format( + ds, + splitBy, + len(refer_seg_ds["images"]), + len(refer_seg_ds["annotations"]), + ) + ) + + img2refs = {} + for ref in refs_train: + image_id = ref["image_id"] + img2refs[image_id] = img2refs.get(image_id, []) + [ + ref, + ] + refer_seg_ds["img2refs"] = img2refs + self.refer_seg_data[ds] = refer_seg_ds + + def __len__(self): + return self.samples_per_epoch + + def preprocess(self, x: torch.Tensor) -> torch.Tensor: + """Normalize pixel values and pad to a square input.""" + if self.model_type=="hq": + h, w = x.shape[-2:] + padh = self.img_size - h + padw = self.img_size - w + x = F.pad(x, (0, padw, 0, padh), value=128) + + # Normalize colors + x = (x - self.pixel_mean) / self.pixel_std + + if self.model_type=="effi" or self.model_type=="sam2": + x = F.interpolate(x.unsqueeze(0), (self.img_size, self.img_size), mode="bilinear").squeeze(0) + else: + # Pad + h, w = x.shape[-2:] + padh = self.img_size - h + padw = self.img_size - w + x = F.pad(x, (0, padw, 0, padh)) + return x + + def __getitem__(self, idx): + ds = random.randint(0, len(self.refer_seg_ds_list) - 1) + ds = self.refer_seg_ds_list[ds] + refer_seg_ds = self.refer_seg_data[ds] + images = refer_seg_ds["images"] + annotations = refer_seg_ds["annotations"] + img2refs = refer_seg_ds["img2refs"] + idx = random.randint(0, len(images) - 1) + image_info = images[idx] + image_path = image_info["file_name"] + image_id = image_info["id"] + refs = img2refs[image_id] + if len(refs) == 0: + return self.__getitem__(0) + + sents = [] + ann_ids = [] + for ref in refs: + for sent in ref["sentences"]: + text = sent["sent"] + sents.append(text) + ann_ids.append(ref["ann_id"]) + if len(sents) >= self.num_classes_per_sample: + sampled_inds = np.random.choice( + list(range(len(sents))), size=self.num_classes_per_sample, replace=False + ) + else: + sampled_inds = list(range(len(sents))) + sampled_sents = np.vectorize(sents.__getitem__)(sampled_inds).tolist() + # sampled_ann_ids = np.vectorize(ann_ids.__getitem__)(sampled_inds).tolist() + sampled_ann_ids = [ann_ids[ind] for ind in sampled_inds] + sampled_classes = sampled_sents + image = cv2.imread(image_path) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + # preprocess image for evf + image_evf = self.image_preprocessor(image) + + image = self.transform.apply_image(image) # preprocess image for sam + resize = image.shape[:2] + + image = self.preprocess(torch.from_numpy(image).permute(2, 0, 1).contiguous()) + + flag = False + masks = [] + for ann_id in sampled_ann_ids: + if isinstance(ann_id, list): + flag = True + if -1 in ann_id: + assert len(ann_id) == 1 + m = np.zeros((image_info["height"], image_info["width"])).astype( + np.uint8 + ) + else: + m_final = np.zeros( + (image_info["height"], image_info["width"]) + ).astype(np.uint8) + for ann_id_i in ann_id: + ann = annotations[ann_id_i] + + if len(ann["segmentation"]) == 0: + m = np.zeros( + (image_info["height"], image_info["width"]) + ).astype(np.uint8) + else: + if type(ann["segmentation"][0]) == list: # polygon + rle = mask.frPyObjects( + ann["segmentation"], + image_info["height"], + image_info["width"], + ) + else: + rle = ann["segmentation"] + for i in range(len(rle)): + if not isinstance(rle[i]["counts"], bytes): + rle[i]["counts"] = rle[i]["counts"].encode() + m = mask.decode(rle) + m = np.sum( + m, axis=2 + ) # sometimes there are multiple binary map (corresponding to multiple segs) + m = m.astype(np.uint8) # convert to np.uint8 + m_final = m_final | m + m = m_final + masks.append(m) + continue + + ann = annotations[ann_id] + + if len(ann["segmentation"]) == 0: + m = np.zeros((image_info["height"], image_info["width"])).astype( + np.uint8 + ) + masks.append(m) + continue + + if type(ann["segmentation"][0]) == list: # polygon + rle = mask.frPyObjects( + ann["segmentation"], image_info["height"], image_info["width"] + ) + else: + rle = ann["segmentation"] + for i in range(len(rle)): + if not isinstance(rle[i]["counts"], bytes): + rle[i]["counts"] = rle[i]["counts"].encode() + m = mask.decode(rle) + m = np.sum( + m, axis=2 + ) # sometimes there are multiple binary map (corresponding to multiple segs) + m = m.astype(np.uint8) # convert to np.uint8 + masks.append(m) + + masks = np.stack(masks, axis=0) + + masks = torch.from_numpy(masks) + label = torch.ones(masks.shape[1], masks.shape[2]) * self.ignore_label + + return ( + image_path, + image, + image_evf, + masks, + label, + resize, + sampled_classes, + ) diff --git a/py/evf-sam/utils/sem_seg_dataset.py b/py/evf-sam/utils/sem_seg_dataset.py new file mode 100644 index 0000000..b19fd1a --- /dev/null +++ b/py/evf-sam/utils/sem_seg_dataset.py @@ -0,0 +1,290 @@ +import glob +import json +import os +import random + +import cv2 +import numpy as np +import torch +import torch.nn.functional as F +from PIL import Image +from pycocotools.coco import COCO + +from model.segment_anything.utils.transforms import ResizeLongestSide +from torchvision import transforms + +def init_mapillary(base_image_dir): + mapillary_data_root = os.path.join(base_image_dir, "mapillary") + with open(os.path.join(mapillary_data_root, "config_v2.0.json")) as f: + mapillary_classes = json.load(f)["labels"] + mapillary_classes = [x["readable"].lower() for x in mapillary_classes] + mapillary_classes = np.array(mapillary_classes) + mapillary_labels = sorted( + glob.glob( + os.path.join(mapillary_data_root, "training", "v2.0", "labels", "*.png") + ) + ) + mapillary_images = [ + x.replace(".png", ".jpg").replace("v2.0/labels", "images") + for x in mapillary_labels + ] + print("mapillary: ", len(mapillary_images)) + return mapillary_classes, mapillary_images, mapillary_labels + + +def init_ade20k(base_image_dir): + with open("utils/ade20k_classes.json", "r") as f: + ade20k_classes = json.load(f) + ade20k_classes = np.array(ade20k_classes) + image_ids = sorted( + os.listdir(os.path.join(base_image_dir, "ade20k/images", "training")) + ) + ade20k_image_ids = [] + for x in image_ids: + if x.endswith(".jpg"): + ade20k_image_ids.append(x[:-4]) + ade20k_images = [] + for image_id in ade20k_image_ids: # self.descriptions: + ade20k_images.append( + os.path.join( + base_image_dir, + "ade20k", + "images", + "training", + "{}.jpg".format(image_id), + ) + ) + ade20k_labels = [ + x.replace(".jpg", ".png").replace("images", "annotations") + for x in ade20k_images + ] + print("ade20k: ", len(ade20k_images)) + return ade20k_classes, ade20k_images, ade20k_labels + +def init_paco_lvis(base_image_dir): + coco_api_paco_lvis = COCO( + os.path.join( + base_image_dir, "vlpart", "paco", "annotations", "paco_lvis_v1_train.json" + ) + ) + all_classes = coco_api_paco_lvis.loadCats(coco_api_paco_lvis.getCatIds()) + class_map_paco_lvis = {} + for cat in all_classes: + cat_split = cat["name"].strip().split(":") + if len(cat_split) == 1: + name = cat_split[0].split("_(")[0] + else: + assert len(cat_split) == 2 + obj, part = cat_split + obj = obj.split("_(")[0] + part = part.split("_(")[0] + name = (obj, part) + class_map_paco_lvis[cat["id"]] = name + img_ids = coco_api_paco_lvis.getImgIds() + print("paco_lvis: ", len(img_ids)) + return class_map_paco_lvis, img_ids, coco_api_paco_lvis + + +def init_pascal_part(base_image_dir): + coco_api_pascal_part = COCO( + os.path.join(base_image_dir, "vlpart", "pascal_part", "train.json") + ) + all_classes = coco_api_pascal_part.loadCats(coco_api_pascal_part.getCatIds()) + class_map_pascal_part = {} + for cat in all_classes: + cat_main, cat_part = cat["name"].strip().split(":") + name = (cat_main, cat_part) + class_map_pascal_part[cat["id"]] = name + img_ids = coco_api_pascal_part.getImgIds() + print("pascal_part: ", len(img_ids)) + return class_map_pascal_part, img_ids, coco_api_pascal_part + + +class SemSegDataset(torch.utils.data.Dataset): + pixel_mean = torch.Tensor([123.675, 116.28, 103.53]).view(-1, 1, 1) + pixel_std = torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1) + img_size = 1024 + ignore_label = 255 + + def __init__( + self, + base_image_dir, + tokenizer, + samples_per_epoch=500 * 8 * 2 * 10, + precision: str = "fp32", + image_size: int = 224, + num_classes_per_sample: int = 3, + exclude_val=False, + sem_seg_data="ade20k||pascal_part||mapillary", + model_type="ori", + transform=ResizeLongestSide(1024), + ): + self.model_type = model_type + self.exclude_val = exclude_val + self.samples_per_epoch = samples_per_epoch + self.num_classes_per_sample = num_classes_per_sample + + self.base_image_dir = base_image_dir + self.tokenizer = tokenizer + self.precision = precision + self.transform = transform + self.image_preprocessor = transforms.Compose([ + transforms.ToTensor(), + transforms.Resize((image_size, image_size), interpolation=3), + transforms.Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5)) + ]) + + self.data2list = {} + self.data2classes = {} + + self.sem_seg_datas = sem_seg_data.split("||") + for ds in self.sem_seg_datas: + classes, images, labels = eval("init_{}".format(ds))(base_image_dir) + self.data2list[ds] = (images, labels) + self.data2classes[ds] = classes + + + def __len__(self): + return self.samples_per_epoch + + def preprocess(self, x: torch.Tensor) -> torch.Tensor: + """Normalize pixel values and pad to a square input.""" + if self.model_type=="hq": + h, w = x.shape[-2:] + padh = self.img_size - h + padw = self.img_size - w + x = F.pad(x, (0, padw, 0, padh), value=128) + + # Normalize colors + x = (x - self.pixel_mean) / self.pixel_std + + if self.model_type=="effi" or self.model_type=="sam2": + x = F.interpolate(x.unsqueeze(0), (self.img_size, self.img_size), mode="bilinear").squeeze(0) + else: + # Pad + h, w = x.shape[-2:] + padh = self.img_size - h + padw = self.img_size - w + x = F.pad(x, (0, padw, 0, padh)) + return x + + def __getitem__(self, idx): + ds = random.randint(0, len(self.sem_seg_datas) - 1) + ds = self.sem_seg_datas[ds] + + if ds in ["pascal_part"]: + class_map = self.data2classes[ds] + img_ids, coco_api = self.data2list[ds] + idx = random.randint(0, len(img_ids) - 1) + img_id = img_ids[idx] + image_info = coco_api.loadImgs([img_id])[0] + file_name = image_info["file_name"] + file_name = os.path.join( + "VOCdevkit", "VOC2010", "JPEGImages", file_name + ) + image_path = os.path.join(self.base_image_dir, "vlpart", ds, file_name) + + image = cv2.imread(image_path) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + # preprocess image for evf + image_evf = self.image_preprocessor(image) + + image = self.transform.apply_image(image) # preprocess image for sam + resize = image.shape[:2] + annIds = coco_api.getAnnIds(imgIds=image_info["id"]) + anns = coco_api.loadAnns(annIds) + if len(anns) == 0: + return self.__getitem__(0) + if len(anns) >= self.num_classes_per_sample: + sampled_anns = np.random.choice( + anns, size=self.num_classes_per_sample, replace=False + ).tolist() + else: + sampled_anns = anns + sampled_classes = [] + for ann in sampled_anns: + sampled_cls = class_map[ann["category_id"]] + if isinstance(sampled_cls, tuple): + obj, part = sampled_cls + if random.random() < 0.5: + name = obj + " " + part + else: + name = "the {} of the {}".format(part, obj) + else: + name = sampled_cls + sampled_classes.append(name) + + elif ds in ["ade20k", "mapillary"]: + image, labels = self.data2list[ds] + idx = random.randint(0, len(image) - 1) + image_path = image[idx] + label_path = labels[idx] + label = Image.open(label_path) + label = np.array(label) + if ds == "ade20k": + label[label == 0] = 255 + label -= 1 + label[label == 254] = 255 + + img = cv2.imread(image_path) + image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + # preprocess image for evf + image_evf = self.image_preprocessor(image) + image = self.transform.apply_image(image) # preprocess image for sam + resize = image.shape[:2] + unique_label = np.unique(label).tolist() + if 255 in unique_label: + unique_label.remove(255) + if len(unique_label) == 0: + return self.__getitem__(0) + + classes = [self.data2classes[ds][class_id] for class_id in unique_label] + if len(classes) >= self.num_classes_per_sample: + sampled_classes = np.random.choice( + classes, size=self.num_classes_per_sample, replace=False + ).tolist() + else: + sampled_classes = classes + + class_ids = [] + for sampled_cls in sampled_classes: + assert len(sampled_cls.split("||")) == 1 + + if ds in ["paco_lvis", "pascal_part"]: + continue + + class_id = self.data2classes[ds].tolist().index(sampled_cls) + class_ids.append(class_id) + + image = self.preprocess(torch.from_numpy(image).permute(2, 0, 1).contiguous()) + + if ds in ["pascal_part"]: + masks = [] + for ann in sampled_anns: + try: + masks.append(coco_api.annToMask(ann)) + except Exception as e: + print(e) + return self.__getitem__(0) + + masks = np.stack(masks, axis=0) + masks = torch.from_numpy(masks) + label = torch.ones(masks.shape[1], masks.shape[2]) * self.ignore_label + + else: + label = torch.from_numpy(label).long() + masks = [] + for class_id in class_ids: + masks.append(label == class_id) + masks = torch.stack(masks, dim=0) + # sampled_classes = ["all "+_ for _ in sampled_classes] + return ( + image_path, + image, + image_evf, + masks, + label, + resize, + sampled_classes, + ) diff --git a/py/evf-sam/utils/utils.py b/py/evf-sam/utils/utils.py new file mode 100644 index 0000000..4900182 --- /dev/null +++ b/py/evf-sam/utils/utils.py @@ -0,0 +1,127 @@ +from enum import Enum + +import numpy as np +import torch +import torch.distributed as dist + +IGNORE_INDEX = -100 + +class Summary(Enum): + NONE = 0 + AVERAGE = 1 + SUM = 2 + COUNT = 3 + + +class AverageMeter(object): + """Computes and stores the average and current value""" + + def __init__(self, name, fmt=":f", summary_type=Summary.AVERAGE): + self.name = name + self.fmt = fmt + self.summary_type = summary_type + self.reset() + + def reset(self): + self.val = 0 + self.avg = 0 + self.sum = 0 + self.count = 0 + + def update(self, val, n=1): + self.val = val + self.sum += val * n + self.count += n + self.avg = self.sum / self.count + + def all_reduce(self): + device = "cuda" if torch.cuda.is_available() else "cpu" + if isinstance(self.sum, np.ndarray): + total = torch.tensor( + self.sum.tolist() + + [ + self.count, + ], + dtype=torch.float32, + device=device, + ) + else: + total = torch.tensor( + [self.sum, self.count], dtype=torch.float32, device=device + ) + + dist.all_reduce(total, dist.ReduceOp.SUM, async_op=False) + if total.shape[0] > 2: + self.sum, self.count = total[:-1].cpu().numpy(), total[-1].cpu().item() + else: + self.sum, self.count = total.tolist() + self.avg = self.sum / (self.count + 1e-5) + + def __str__(self): + fmtstr = "{name} {val" + self.fmt + "} ({avg" + self.fmt + "})" + return fmtstr.format(**self.__dict__) + + def summary(self): + fmtstr = "" + if self.summary_type is Summary.NONE: + fmtstr = "" + elif self.summary_type is Summary.AVERAGE: + fmtstr = "{name} {avg:.3f}" + elif self.summary_type is Summary.SUM: + fmtstr = "{name} {sum:.3f}" + elif self.summary_type is Summary.COUNT: + fmtstr = "{name} {count:.3f}" + else: + raise ValueError("invalid summary type %r" % self.summary_type) + + return fmtstr.format(**self.__dict__) + + +def intersectionAndUnionGPU(output, target, K, ignore_index=255): + # 'K' classes, output and target sizes are N or N * L or N * H * W, each value in range 0 to K - 1. + assert output.dim() in [1, 2, 3] + assert output.shape == target.shape + output = output.view(-1) + target = target.view(-1) + output[target == ignore_index] = ignore_index + intersection = output[output == target] + area_intersection = torch.histc(intersection, bins=K, min=0, max=K - 1) + area_output = torch.histc(output, bins=K, min=0, max=K - 1) + area_target = torch.histc(target, bins=K, min=0, max=K - 1) + area_union = area_output + area_target - area_intersection + return area_intersection, area_union, area_target + + +class ProgressMeter(object): + def __init__(self, num_batches, meters, prefix=""): + self.batch_fmtstr = self._get_batch_fmtstr(num_batches) + self.meters = meters + self.prefix = prefix + + def display(self, batch): + entries = [self.prefix + self.batch_fmtstr.format(batch)] + entries += [str(meter) for meter in self.meters] + print("\t".join(entries)) + + def display_summary(self): + entries = [" *"] + entries += [meter.summary() for meter in self.meters] + print(" ".join(entries)) + + def _get_batch_fmtstr(self, num_batches): + num_digits = len(str(num_batches // 1)) + fmt = "{:" + str(num_digits) + "d}" + return "[" + fmt + "/" + fmt.format(num_batches) + "]" + + +def dict_to_cuda(input_dict): + for k, v in input_dict.items(): + if isinstance(input_dict[k], torch.Tensor): + input_dict[k] = v.cuda(non_blocking=True) + elif ( + isinstance(input_dict[k], list) + and len(input_dict[k]) > 0 + and isinstance(input_dict[k][0], torch.Tensor) + ): + input_dict[k] = [ele.cuda(non_blocking=True) for ele in v] + return input_dict diff --git a/py/evf_sam_ultra.py b/py/evf_sam_ultra.py new file mode 100644 index 0000000..9c44ffb --- /dev/null +++ b/py/evf_sam_ultra.py @@ -0,0 +1,112 @@ +''' +推理部分代码来自https://github.com/hustvl/EVF-SAM +''' +import sys + +from .imagefunc import * +sys.path.append(os.path.join(os.path.dirname(__file__), 'evf-sam')) +from inference import evf_sam_main +class EVF_SAM_Ultra: + + def __init__(self): + self.NODE_NAME = 'EVF_SAM Ultra' + pass + + @classmethod + def INPUT_TYPES(cls): + model_list = ["evf-sam2","evf-sam"] + precision_list = ["fp16", "bf16", "fp32"] + load_in_bit_list = [16, 8, 4] + method_list = ['VITMatte', 'VITMatte(local)', 'PyMatting', 'GuidedFilter', ] + device_list = ['cuda', 'cpu'] + return {"required": + { + "image": ("IMAGE",), + "model": (model_list,), + "precision": (precision_list,), + "load_in_bit": (load_in_bit_list,), + "prompt": ("STRING", {"default": "subject"}), + "detail_method": (method_list,), + "detail_erode": ("INT", {"default": 6, "min": 1, "max": 255, "step": 1}), + "detail_dilate": ("INT", {"default": 4, "min": 1, "max": 255, "step": 1}), + "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01, "display": "slider"}), + "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01, "display": "slider"}), + "process_detail": ("BOOLEAN", {"default": True}), + "device": (device_list,), + "max_megapixels": ("FLOAT", {"default": 2.0, "min": 1, "max": 999, "step": 0.1}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK",) + RETURN_NAMES = ("image", "mask",) + FUNCTION = "evf_sam_ultra" + CATEGORY = '😺dzNodes/LayerMask' + + def evf_sam_ultra(self, image, model, precision, load_in_bit, prompt, + detail_method, detail_erode, detail_dilate, black_point, white_point, + process_detail, device, max_megapixels, + ): + + ret_images = [] + ret_masks = [] + + if detail_method == 'VITMatte(local)': + local_files_only = True + else: + local_files_only = False + + if model == 'evf-sam2': + model_type = 'sam2' + elif model == 'evf-sam': + model_type = 'ori' + else: + model_type = 'effi' + + model_path = "" + model_folder_name = 'EVF-SAM' + try: + model_path = os.path.join( + os.path.normpath(folder_paths.folder_names_and_paths[model_folder_name][0][0]), model) + except: + pass + if not os.path.exists(model_path): + model_path = os.path.join(folder_paths.models_dir, model_folder_name, model) + + for i in image: + i = torch.unsqueeze(i, 0) + orig_image = tensor2pil(i).convert('RGB') + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + + mask_image = evf_sam_main(model_path, model_type, precision, load_in_bit, orig_image, prompt) + _mask = pil2tensor(mask_image) + + detail_range = detail_erode + detail_dilate + if process_detail: + if detail_method == 'GuidedFilter': + _mask = guided_filter_alpha(i, _mask, detail_range // 6 + 1) + _mask = tensor2pil(histogram_remap(_mask, black_point, white_point)) + elif detail_method == 'PyMatting': + _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8 + 1, black_point, white_point)) + else: + _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate) + _mask = generate_VITMatte(orig_image, _trimap, local_files_only=local_files_only, device=device, + max_megapixels=max_megapixels) + _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point)) + else: + _mask = mask2image(_mask) + + ret_image = RGB2RGBA(orig_image, _mask.convert('L')) + ret_images.append(pil2tensor(ret_image)) + ret_masks.append(image2mask(_mask)) + + log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish') + return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),) + +NODE_CLASS_MAPPINGS = { + "LayerMask: EVFSAMUltra": EVF_SAM_Ultra +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerMask: EVFSAMUltra": "LayerMask: EVF-SAM Ultra" +} + diff --git a/pyproject.toml b/pyproject.toml index e2e5200..6606267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,9 @@ [project] name = "comfyui_layerstyle" description = "A set of nodes for ComfyUI it generate image like Adobe Photoshop's Layer Style. the Drop Shadow is first completed node, and follow-up work is in progress." -version = "1.0.29" +version = "1.0.30" license = "MIT" -dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "opencv-contrib-python", "pymatting", "segment_anything", "timm", "addict", "yapf", "colour-science", "wget", "mediapipe", "loguru", "typer_config", "fastapi", "rich", "google-generativeai", "diffusers", "omegaconf", "tqdm", "transformers", "kornia", "image-reward", "ultralytics", "blend_modes", "blind-watermark", "qrcode", "pyzbar", "transparent-background", "huggingface_hub", "psd-tools"] +dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "opencv-contrib-python", "pymatting", "segment_anything", "timm", "addict", "yapf", "colour-science", "wget", "mediapipe", "loguru", "typer_config", "fastapi", "rich", "google-generativeai", "diffusers", "omegaconf", "tqdm", "transformers", "kornia", "image-reward", "ultralytics", "blend_modes", "blind-watermark", "qrcode", "pyzbar", "transparent-background", "huggingface_hub", "accelerate", "torchscale", "wandb", "hydra-core", "psd-tools"] [project.urls] Repository = "https://github.com/chflame163/ComfyUI_LayerStyle" diff --git a/repair_dependency_list.txt b/repair_dependency_list.txt index e365b7f..90d5ff5 100644 --- a/repair_dependency_list.txt +++ b/repair_dependency_list.txt @@ -1,4 +1,4 @@ -huggingface_hub==0.23.3 -transformers>=4.38.2 +huggingface_hub>=0.23.3 +transformers>=4.44.0 protobuf>=4.25.3 opencv-contrib-python>=4.9.0.80 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9a71b68..40b2f62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ google-generativeai diffusers omegaconf tqdm -transformers>=4.38.2 +transformers>=4.44.0 kornia image-reward ultralytics @@ -30,5 +30,9 @@ blind-watermark qrcode pyzbar transparent-background -huggingface_hub +huggingface_hub>=0.23.3 +accelerate +torchscale +wandb psd-tools +hydra-core \ No newline at end of file diff --git a/whl/hydra_core-1.3.2-py3-none-any.whl b/whl/hydra_core-1.3.2-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..224d3865b6ea2096d58d998045af262c1a924f24 GIT binary patch literal 154547 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 W+^;%*bHIKG{FjL~(U(EP^8W$Rj!gpq literal 0 HcmV?d00001