├── .gitignore ├── README.md ├── assets └── figure │ ├── 001.png │ ├── 002.png │ ├── 003.png │ ├── 004.png │ ├── 005.png │ ├── 006.png │ ├── 007.png │ ├── 008.png │ ├── 009.png │ ├── 010.png │ ├── 011.png │ ├── 2S1.png │ ├── confuser-rejection.png │ ├── cr-confusion-matrix.png │ ├── cr-roc.png │ ├── cr-training-plot.png │ ├── eoc-1-confusion-matrix.png │ ├── eoc-1-training-plot.png │ ├── eoc-2-cv-confusion-matrix.png │ ├── eoc-2-cv-training-plot.png │ ├── eoc-2-vv-confusion-matrix.png │ ├── eoc-2-vv-training-plot.png │ ├── soc-confusion-matrix.png │ └── soc-training-plot.png ├── dataset ├── confuser-rejection │ ├── .gitkeep │ └── raw │ │ └── rename.py ├── eoc-1-t72-132 │ ├── .gitkeep │ └── raw │ │ └── rename.py ├── eoc-1-t72-a64 │ ├── .gitkeep │ └── raw │ │ └── rename.py ├── eoc-2-cv │ ├── .gitkeep │ └── raw │ │ └── rename.py ├── eoc-2-vv │ ├── .gitkeep │ └── raw │ │ └── rename.py └── soc │ ├── .gitkeep │ └── raw │ └── rename.py ├── docker └── Dockerfile ├── experiments └── config │ ├── AConvNet-CR.json │ ├── AConvNet-EOC-1-T72-132.json │ ├── AConvNet-EOC-1-T72-A64.json │ ├── AConvNet-EOC-2-CV.json │ ├── AConvNet-EOC-2-VV.json │ └── AConvNet-SOC.json ├── notebook ├── experiments-CR.ipynb ├── experiments-EOC-1-T72-132.ipynb ├── experiments-EOC-1-T72-A64.ipynb ├── experiments-EOC-2-CV.ipynb ├── experiments-EOC-2-VV.ipynb ├── experiments-SOC.ipynb └── target-chip.ipynb ├── requirements.txt ├── run-docker.sh └── src ├── data ├── __init__.py ├── generate_dataset.py ├── loader.py ├── mstar.py └── preprocess.py ├── model ├── __init__.py ├── _base.py ├── _blocks.py └── network.py ├── train.py └── utils ├── __init__.py └── common.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,linux,pycharm+all,python 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,linux,pycharm+all,python 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### PyCharm+all ### 21 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 22 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 23 | 24 | # User-specific stuff 25 | .idea/**/workspace.xml 26 | .idea/**/tasks.xml 27 | .idea/**/usage.statistics.xml 28 | .idea/**/dictionaries 29 | .idea/**/shelf 30 | 31 | # Generated files 32 | .idea/**/contentModel.xml 33 | 34 | # Sensitive or high-churn files 35 | .idea/**/dataSources/ 36 | .idea/**/dataSources.ids 37 | .idea/**/dataSources.local.xml 38 | .idea/**/sqlDataSources.xml 39 | .idea/**/dynamic.xml 40 | .idea/**/uiDesigner.xml 41 | .idea/**/dbnavigator.xml 42 | 43 | # Gradle 44 | .idea/**/gradle.xml 45 | .idea/**/libraries 46 | 47 | # Gradle and Maven with auto-import 48 | # When using Gradle or Maven with auto-import, you should exclude module files, 49 | # since they will be recreated, and may cause churn. Uncomment if using 50 | # auto-import. 51 | # .idea/artifacts 52 | # .idea/compiler.xml 53 | # .idea/jarRepositories.xml 54 | # .idea/modules.xml 55 | # .idea/*.iml 56 | # .idea/modules 57 | # *.iml 58 | # *.ipr 59 | 60 | # CMake 61 | cmake-build-*/ 62 | 63 | # Mongo Explorer plugin 64 | .idea/**/mongoSettings.xml 65 | 66 | # File-based project format 67 | *.iws 68 | 69 | # IntelliJ 70 | out/ 71 | 72 | # mpeltonen/sbt-idea plugin 73 | .idea_modules/ 74 | 75 | # JIRA plugin 76 | atlassian-ide-plugin.xml 77 | 78 | # Cursive Clojure plugin 79 | .idea/replstate.xml 80 | 81 | # Crashlytics plugin (for Android Studio and IntelliJ) 82 | com_crashlytics_export_strings.xml 83 | crashlytics.properties 84 | crashlytics-build.properties 85 | fabric.properties 86 | 87 | # Editor-based Rest Client 88 | .idea/httpRequests 89 | 90 | # Android studio 3.1+ serialized cache file 91 | .idea/caches/build_file_checksums.ser 92 | 93 | ### PyCharm+all Patch ### 94 | # Ignores the whole .idea folder and all .iml files 95 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 96 | 97 | .idea/ 98 | 99 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 100 | 101 | *.iml 102 | modules.xml 103 | .idea/misc.xml 104 | *.ipr 105 | 106 | # Sonarlint plugin 107 | .idea/sonarlint 108 | 109 | ### Python ### 110 | # Byte-compiled / optimized / DLL files 111 | __pycache__/ 112 | *.py[cod] 113 | *$py.class 114 | 115 | # C extensions 116 | *.so 117 | 118 | # Distribution / packaging 119 | .Python 120 | build/ 121 | develop-eggs/ 122 | dist/ 123 | downloads/ 124 | eggs/ 125 | .eggs/ 126 | parts/ 127 | sdist/ 128 | var/ 129 | wheels/ 130 | pip-wheel-metadata/ 131 | share/python-wheels/ 132 | *.egg-info/ 133 | .installed.cfg 134 | *.egg 135 | MANIFEST 136 | 137 | # PyInstaller 138 | # Usually these files are written by a python script from a template 139 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 140 | *.manifest 141 | *.spec 142 | 143 | # Installer logs 144 | pip-log.txt 145 | pip-delete-this-directory.txt 146 | 147 | # Unit test / coverage reports 148 | htmlcov/ 149 | .tox/ 150 | .nox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | nosetests.xml 155 | coverage.xml 156 | *.cover 157 | *.py,cover 158 | .hypothesis/ 159 | .pytest_cache/ 160 | pytestdebug.log 161 | 162 | # Translations 163 | *.mo 164 | *.pot 165 | 166 | # Django stuff: 167 | *.log 168 | local_settings.py 169 | db.sqlite3 170 | db.sqlite3-journal 171 | 172 | # Flask stuff: 173 | instance/ 174 | .webassets-cache 175 | 176 | # Scrapy stuff: 177 | .scrapy 178 | 179 | # Sphinx documentation 180 | docs/_build/ 181 | doc/_build/ 182 | 183 | # PyBuilder 184 | target/ 185 | 186 | # Jupyter Notebook 187 | .ipynb_checkpoints 188 | 189 | # IPython 190 | profile_default/ 191 | ipython_config.py 192 | 193 | # pyenv 194 | .python-version 195 | 196 | # pipenv 197 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 198 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 199 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 200 | # install all needed dependencies. 201 | #Pipfile.lock 202 | 203 | # poetry 204 | #poetry.lock 205 | 206 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 207 | __pypackages__/ 208 | 209 | # Celery stuff 210 | celerybeat-schedule 211 | celerybeat.pid 212 | 213 | # SageMath parsed files 214 | *.sage.py 215 | 216 | # Environments 217 | # .env 218 | .env/ 219 | .venv/ 220 | env/ 221 | venv/ 222 | ENV/ 223 | env.bak/ 224 | venv.bak/ 225 | pythonenv* 226 | 227 | # Spyder project settings 228 | .spyderproject 229 | .spyproject 230 | 231 | # Rope project settings 232 | .ropeproject 233 | 234 | # mkdocs documentation 235 | /site 236 | 237 | # mypy 238 | .mypy_cache/ 239 | .dmypy.json 240 | dmypy.json 241 | 242 | # Pyre type checker 243 | .pyre/ 244 | 245 | # pytype static type analyzer 246 | .pytype/ 247 | 248 | # operating system-related files 249 | *.DS_Store #file properties cache/storage on macOS 250 | Thumbs.db #thumbnail cache on Windows 251 | 252 | # profiling data 253 | .prof 254 | 255 | 256 | ### Windows ### 257 | # Windows thumbnail cache files 258 | Thumbs.db 259 | Thumbs.db:encryptable 260 | ehthumbs.db 261 | ehthumbs_vista.db 262 | 263 | # Dump file 264 | *.stackdump 265 | 266 | # Folder config file 267 | [Dd]esktop.ini 268 | 269 | # Recycle Bin used on file shares 270 | $RECYCLE.BIN/ 271 | 272 | # Windows Installer files 273 | *.cab 274 | *.msi 275 | *.msix 276 | *.msm 277 | *.msp 278 | 279 | # Windows shortcuts 280 | *.lnk 281 | 282 | # asdf 283 | dataset/soc/raw/train/ 284 | dataset/soc/raw/test/ 285 | dataset/soc/train/ 286 | dataset/soc/test/ 287 | 288 | dataset/eoc-1-*/raw/train/ 289 | dataset/eoc-1-*/raw/test/ 290 | dataset/eoc-1-*/train/ 291 | dataset/eoc-1-*/test/ 292 | 293 | dataset/eoc-2*/raw/train/ 294 | dataset/eoc-2*/raw/test/ 295 | dataset/eoc-2*/train/ 296 | dataset/eoc-2*/test/ 297 | 298 | dataset/confuser-rejection/raw/train/ 299 | dataset/confuser-rejection/raw/test/ 300 | dataset/confuser-rejection/train/ 301 | dataset/confuser-rejection/test/ 302 | 303 | experiments/history 304 | 305 | *.pth 306 | *.zip 307 | # End of https://www.toptal.com/developers/gitignore/api/windows,linux,pycharm+all,python 308 | 309 | 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AConvNet 2 | 3 | ### Target Classification Using the Deep Convolutional Networks for SAR Images 4 | 5 | This repository is reproduced-implementation of AConvNet which recognize target from MSTAR dataset. 6 | You can see the official implementation of the author at [MSTAR-AConvNet](https://github.com/fudanxu/MSTAR-AConvNet). 7 | 8 | ## Dataset 9 | 10 | ### MSTAR (Moving and Stationary Target Acquisition and Recognition) Database 11 | 12 | #### Format 13 | 14 | - Header 15 | - Type: ASCII 16 | - Including data shape(width, height), serial number, azimuth angle, etc. 17 | - Data 18 | - Type: Two-bytes 19 | - Shape: W x H x 2 20 | - Magnitude block 21 | - Phase Block 22 | 23 | Below figure is the example of magnitude block(Left) and phase block(Right) 24 | 25 | ![Example of data block: 2S1](./assets/figure/001.png) 26 | 27 | ## Model 28 | 29 | The proposed model only consists of **sparsely connected layers** without any fully connected layers. 30 | 31 | - It eases the over-fitting problem by reducing the number of free parameters(model capacity) 32 | 33 | | layer | Input | Conv 1 | Conv 2 | Conv 3 | Conv 4 | Conv 5 | 34 | | :---------: | ------ | :--------: | :--------: | :--------: | :----: | :-----: | 35 | | channels | 2 | 16 | 32 | 64 | 128 | 10 | 36 | | weight size | - | 5 x 5 | 5 x 5 | 6 x 6 | 5 x 5 | 3 x 3 | 37 | | pooling | - | 2 x 2 - s2 | 2 x 2 - s2 | 2 x 2 - s2 | - | - | 38 | | dropout | - | - | - | - | 0.5 | - | 39 | | activation | linear | ReLU | ReLU | ReLU | ReLU | Softmax | 40 | 41 | ## Training 42 | For training, this implementation fixes the random seed to `12321` for `reproducibility`. 43 | 44 | The experimental conditions are same as in the paper, except for `data augmentation`. 45 | You can see the details in `src/model/_base.py` and `experiments/config/AConvNet-SOC.json` 46 | 47 | ### Data Augmentation 48 | 49 | - The author uses random shifting to extract 88 x 88 patches from 128 x 128 SAR image chips. 50 | - The number of training images per one SAR image chip could be increased at maximum by (128 - 88 + 1) x (128 - 88 + 1) = 1681. 51 | 52 | - However, for SOC, this repository does not use random shifting tue to accuracy issue. 53 | - You can see the details in `src/data/generate_dataset.py` and `src/data/mstar.py` 54 | - The implementation details for data augmentation is as: 55 | - Crop the center of 94 x 94 size image on 100 x 100 SAR image chip (49 patches per image chip). 56 | - Extract 88 x 88 patches with stride 1 from 94 x 94 image with random cropping. 57 | 58 | 59 | ## Experiments 60 | 61 | You can download the MSTAR Dataset from [MSTAR Overview](https://www.sdms.afrl.af.mil/index.php?collection=mstar) 62 | - MSTAR Clutter - CD1 / CD2 63 | - MSTAR Target Chips (T72 BMP2 BTR70 SLICY) - CD1 64 | - MSTAR / IU Mixed Targets - CD1 / CD2 65 | - MSTAR / IU T-72 Variants - CD1 / CD2 66 | - MSTAR Predictlite Software - CD1 67 | 68 | You can download the experimental results of this repository from [experiments](https://github.com/jangsoopark/AConvNet-pytorch/releases/download/v2.2.0/experiments.zip) 69 | 70 | ### Standard Operating Condition (SOC) 71 | 72 | | | | Train | | Test | | 73 | | ------- | ---------- | ---------- | ---------- | ---------- | ---------- | 74 | | Class | Serial No. | Depression | No. Images | Depression | No. Images | 75 | | BMP-2 | 9563 | 17 | 233 | 15 | 196 | 76 | | BTR-70 | c71 | 17 | 233 | 15 | 196 | 77 | | T-72 | 132 | 17 | 232 | 15 | 196 | 78 | | BTR-60 | k10yt7532 | 17 | 256 | 15 | 195 | 79 | | 2S1 | b01 | 17 | 299 | 15 | 274 | 80 | | BRDM-2 | E-71 | 17 | 298 | 15 | 274 | 81 | | D7 | 92v13015 | 17 | 299 | 15 | 274 | 82 | | T-62 | A51 | 17 | 299 | 15 | 273 | 83 | | ZIL-131 | E12 | 17 | 299 | 15 | 274 | 84 | | ZSU-234 | d08 | 17 | 299 | 15 | 274 | 85 | 86 | ##### Training Set (Depression: 17$\degree$) 87 | 88 | ```shell 89 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 90 | ├ TARGETS/TRAIN/17_DEG 91 | │ ├ BMP2/SN_9563/*.000 (233 images) 92 | │ ├ BTR70/SN_C71/*.004 (233 images) 93 | │ └ T72/SN_132/*.015 (232 images) 94 | └ ... 95 | 96 | MSTAR-PublicMixedTargets-CD2/MSTAR_PUBLIC_MIXED_TARGETS_CD2 97 | ├ 17_DEG 98 | │ ├ COL1/SCENE1/BTR_60/*.003 (256 images) 99 | │ └ COL2/SCENE1 100 | │ ├ 2S1/*.000 (299 images) 101 | │ ├ BRDM_2/*.001 (298 images) 102 | │ ├ D7/*.005 (299 images) 103 | │ ├ SLICY 104 | │ ├ T62/*.016 (299 images) 105 | │ ├ ZIL131/*.025 (299 images) 106 | │ └ ZSU_23_4/*.026 (299 images) 107 | └ ... 108 | 109 | ``` 110 | 111 | ##### Test Set (Depression: 15$\degree$) 112 | 113 | ```shell 114 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 115 | ├ TARGETS/TEST/15_DEG 116 | │ ├ BMP2/SN_9563/*.000 (195 images) 117 | │ ├ BTR70/SN_C71/*.004 (196 images) 118 | │ └ T72/SN_132/*.015 (196 images) 119 | └ ... 120 | 121 | MSTAR-PublicMixedTargets-CD1/MSTAR_PUBLIC_MIXED_TARGETS_CD1 122 | ├ 15_DEG 123 | │ ├ COL1/SCENE1/BTR_60/*.003 (195 images) 124 | │ └ COL2/SCENE1 125 | │ ├ 2S1/*.000 (274 images) 126 | │ ├ BRDM_2/*.001 (274 images) 127 | │ ├ D7/*.005 (274 images) 128 | │ ├ SLICY 129 | │ ├ T62/*.016 (273 images) 130 | │ ├ ZIL131/*.025 (274 images) 131 | │ └ ZSU_23_4/*.026 (274 images) 132 | └ ... 133 | 134 | ``` 135 | ##### Quick Start Guide for Training 136 | 137 | - Dataset Preparation 138 | - Download the [dataset.zip](https://github.com/jangsoopark/AConvNet-pytorch/releases/download/v2.2.0/dataset.zip) 139 | - After extracting it, you can find `train` and `test` directories inside `raw` directory. 140 | - Place the two directories (`train` and `test`) to the `dataset/soc/raw`. 141 | ```shell 142 | $ cd src/data 143 | $ python3 generate_dataset.py --is_train=True --use_phase=True --chip_size=100 --patch_size=94 --use_phase=True --dataset=soc 144 | $ python3 generate_dataset.py --is_train=False --use_phase=True --chip_size=128 --patch_size=128 --use_phase=True --dataset=soc 145 | $ cd .. 146 | $ python3 train.py --config_name=config/AConvNet-SOC.json 147 | ``` 148 | 149 | ##### Results of SOC 150 | - Final Accuracy is **99.34%** at epoch 29 (The official accuracy is 99.13%) 151 | - You can see the details in `notebook/experiments-SOC.ipynb` 152 | 153 | - Visualization of training loss and test accuracy 154 | 155 | ![soc-training-plot](./assets/figure/soc-training-plot.png) 156 | 157 | - Confusion Matrix with best model at **epoch 28** 158 | 159 | ![soc-confusion-matrix](./assets/figure/soc-confusion-matrix.png) 160 | 161 | - Noise Simulation [1] 162 | - i.i.d samples from a uniform distribution 163 | - This simulation does not fix the random seed 164 | 165 | | Noise | 1% | 5% | 10% | 15%| 166 | | :---: | :---: | :---: | :---: | :---: | 167 | | AConvNet-PyTorch | 98.64 | 94.10 | 84.54 | 71.55 | 168 | | AConvNet-Official | 91.76 | 88.52 | 75.84 | 54.68 | 169 | 170 | 171 | ### Extended Operating Conditions (EOC) 172 | 173 | #### EOC-1 (Large depression angle change) 174 | 175 | | | | Train | | Test | | 176 | | ------- | ---------- | ---------- | ---------- | ---------- | ---------- | 177 | | Class | Serial No. | Depression | No. Images | Depression | No. Images | 178 | | T-72 | A64 | 17 | 299 | 30 | 196 | 179 | | 2S1 | b01 | 17 | 299 | 30 | 274 | 180 | | BRDM-2 | E-71 | 17 | 298 | 30 | 274 | 181 | | ZSU-234 | d08 | 17 | 299 | 30 | 274 | 182 | 183 | ##### Training Set (Depression: 17$\degree$) 184 | ```shell 185 | MSTAR-PublicT72Variants-CD2/MSTAR_PUBLIC_T_72_VARIANTS_CD2 186 | ├ 17_DEG/COL2/SCENE1 187 | │ └ A64/*.024 (299 images) 188 | └ ... 189 | 190 | MSTAR-PublicMixedTargets-CD2/MSTAR_PUBLIC_MIXED_TARGETS_CD2 191 | ├ 17_DEG 192 | │ └ COL2/SCENE1 193 | │ ├ 2S1/*.000 (299 images) 194 | │ ├ BRDM_2/*.001 (298 images) 195 | │ └ ZSU_23_4/*.026 (299 images) 196 | └ ... 197 | 198 | ``` 199 | 200 | ##### Test Set (Depression: 30$\degree$) 201 | 202 | ```shell 203 | MSTAR-PublicT72Variants-CD2/MSTAR_PUBLIC_T_72_VARIANTS_CD2 204 | ├ 30_DEG/COL2/SCENE1 205 | │ └ A64/*.024 (288 images) 206 | └ ... 207 | 208 | MSTAR-PublicMixedTargets-CD2/MSTAR_PUBLIC_MIXED_TARGETS_CD2 209 | ├ 30_DEG 210 | │ └ COL2/SCENE1 211 | │ ├ 2S1/*.000 (288 images) 212 | │ ├ BRDM_2/*.001 (287 images) 213 | │ ├ ZSU_23_4/*.026 (288 images) 214 | │ └ ... 215 | └ ... 216 | 217 | ``` 218 | 219 | ##### Quick Start Guide for Training 220 | 221 | - Dataset Preparation 222 | - Download the [dataset.zip](https://github.com/jangsoopark/AConvNet-pytorch/releases/download/v2.2.0/dataset.zip) 223 | - After extracting it, you can find `train` and `test` directories inside `raw` directory. 224 | - Place the two directories (`train` and `test`) to the `dataset/eoc-1-t72-a64/raw`. 225 | ```shell 226 | $ cd src/data 227 | $ python3 generate_dataset.py --is_train=True --use_phase=True --chip_size=100 --patch_size=94 --use_phase=True --dataset=eoc-1-t72-a64 228 | $ python3 generate_dataset.py --is_train=False --use_phase=True --chip_size=128 --patch_size=128 --use_phase=True --dataset=eoc-1-t72-a64 229 | $ cd .. 230 | $ python3 train.py --config_name=config/AConvNet-EOC-1-T72-A64.json 231 | ``` 232 | 233 | ##### Results of EOC-1 234 | - Final Accuracy is **91.49%** at epoch 17 (The official accuracy is 96.12%) 235 | - You can see the details in `notebook/experiments-EOC-1-T72-A64.ipynb` 236 | 237 | - Visualization of training loss and test accuracy 238 | 239 | ![eoc-1-training-plot](./assets/figure/eoc-1-training-plot.png) 240 | 241 | - Confusion Matrix with best model at **epoch 28** 242 | 243 | ![eoc-1-confusion-matrix](./assets/figure/eoc-1-confusion-matrix.png) 244 | 245 | 246 | #### EOC-2 (Target configuration variants) 247 | 248 | | | | Train | | Test | | 249 | | ------- | ---------- | ---------- | ---------- | ---------- | ---------- | 250 | | Class | Serial No. | Depression | No. Images | Depression | No. Images | 251 | | BMP-2 | 9563 | 17 | 233 | - | - | 252 | | BRDM-2 | E-71 | 17 | 298 | - | - | 253 | | BTR-70 | c71 | 17 | 233 | - | - | 254 | | T-72 | 132 | 17 | 232 | - | - | 255 | | T-72 | S7 | - | - | 15, 17 | 419 | 256 | | T-72 | A32 | - | - | 15, 17 | 572 | 257 | | T-72 | A62 | - | - | 15, 17 | 573 | 258 | | T-72 | A63 | - | - | 15, 17 | 573 | 259 | | T-72 | A64 | - | - | 15, 17 | 573 | 260 | 261 | ##### Training Set (Depression: 17$\degree$) 262 | ```shell 263 | # BMP2, BRDM2, BTR70, T72 are selected from SOC training data 264 | ``` 265 | 266 | ##### Test Set (Depression: 15$\degree$) 267 | 268 | ```shell 269 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 270 | ├ TARGETS/TRAIN/17_DEG 271 | │ └ T72/SN_S7/*.017 (228 images) 272 | └ ... 273 | 274 | MSTAR-PublicT72Variants-CD2/MSTAR_PUBLIC_T_72_VARIANTS_CD2 275 | ├ 17_DEG/COL2/SCENE1 276 | │ ├ A32/*.017 (299 images) 277 | │ ├ A62/*.018 (299 images) 278 | │ ├ A63/*.019 (299 images) 279 | │ ├ A64/*.020 (299 images) 280 | ├ └ ... 281 | └ ... 282 | 283 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 284 | ├ TARGETS/TEST/15_DEG 285 | │ └ T72/SN_S7/*.017 (191 images) 286 | └ ... 287 | 288 | MSTAR-PublicT72Variants-CD1/MSTAR_PUBLIC_T_72_VARIANTS_CD1 289 | ├ 15_DEG/COL2/SCENE1 290 | │ ├ A32/*.017 (274 images) 291 | │ ├ A62/*.018 (274 images) 292 | │ ├ A63/*.019 (274 images) 293 | │ ├ A64/*.020 (274 images) 294 | ├ └ ... 295 | └ ... 296 | 297 | ``` 298 | 299 | ##### Quick Start Guide for Training 300 | 301 | - Dataset Preparation 302 | - Download the [dataset.zip](https://github.com/jangsoopark/AConvNet-pytorch/releases/download/v2.2.0/dataset.zip) 303 | - After extracting it, you can find `train` and `test` directories inside `raw` directory. 304 | - Place the two directories (`train` and `test`) to the `dataset/eoc-2-cv/raw`. 305 | ```shell 306 | $ cd src/data 307 | $ python3 generate_dataset.py --is_train=True --use_phase=True --chip_size=100 --patch_size=94 --use_phase=True --dataset=eoc-2-cv 308 | $ python3 generate_dataset.py --is_train=False --use_phase=True --chip_size=128 --patch_size=128 --use_phase=True --dataset=eoc-2-cv 309 | $ cd .. 310 | $ python3 train.py --config_name=config/AConvNet-EOC-2-CV.json 311 | ``` 312 | 313 | ##### Results of EOC-2 Configuration Variants 314 | - Final Accuracy is **99.41%** at epoch 95 (The official accuracy is 98.93%) 315 | - You can see the details in `notebook/experiments-EOC-2-CV.ipynb` 316 | 317 | - Visualization of training loss and test accuracy 318 | 319 | ![eoc-2-cv-training-plot](./assets/figure/eoc-2-cv-training-plot.png) 320 | 321 | - Confusion Matrix with best model at **epoch 95** 322 | 323 | ![eoc-2-cv-confusion-matrix](./assets/figure/eoc-2-cv-confusion-matrix.png) 324 | 325 | 326 | #### EOC-2 (Target version variants) 327 | 328 | | | | Train | | Test | | 329 | | ------- | ---------- | ---------- | ---------- | ---------- | ---------- | 330 | | Class | Serial No. | Depression | No. Images | Depression | No. Images | 331 | | BMP-2 | 9563 | 17 | 233 | - | - | 332 | | BRDM-2 | E-71 | 17 | 298 | - | - | 333 | | BTR-70 | c71 | 17 | 233 | - | - | 334 | | T-72 | 132 | 17 | 232 | - | - | 335 | | BMP-2 | 9566 | - | - | 15, 17 | 428 | 336 | | BMP-2 | c21 | - | - | 15, 17 | 429 | 337 | | T-72 | 812 | - | - | 15, 17 | 426 | 338 | | T-72 | A04 | - | - | 15, 17 | 573 | 339 | | T-72 | A05 | - | - | 15, 17 | 573 | 340 | | T-72 | A07 | - | - | 15, 17 | 573 | 341 | | T-72 | A10 | - | - | 15, 17 | 567 | 342 | 343 | ##### Training Set (Depression: 17$\degree$) 344 | ```shell 345 | # BMP2, BRDM2, BTR70, T72 are selected from SOC training data 346 | ``` 347 | 348 | ##### Test Set (Depression: 15$\degree$) 349 | 350 | ```shell 351 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 352 | ├ TARGETS/TRAIN/17_DEG 353 | │ ├ BMP2/SN_9566/*.001 (232 images) 354 | │ ├ BMP2/SN_C21/*.002 (233 images) 355 | │ ├ T72/SN_812/*.016 (231 images) 356 | │ └ ... 357 | └ ... 358 | 359 | MSTAR-PublicT72Variants-CD2/MSTAR_PUBLIC_T_72_VARIANTS_CD2 360 | ├ 17_DEG/COL2/SCENE1 361 | │ ├ A04/*.017 (299 images) 362 | │ ├ A05/*.018 (299 images) 363 | │ ├ A07/*.019 (299 images) 364 | │ ├ A10/*.020 (296 images) 365 | ├ └ ... 366 | └ ... 367 | 368 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 369 | ├ TARGETS/TEST/15_DEG 370 | │ ├ BMP2/SN_9566/*.001 (196 images) 371 | │ ├ BMP2/SN_C21/*.002 (196 images) 372 | │ ├ T72/SN_812/*.0176 (195 images) 373 | │ └ ... 374 | └ ... 375 | 376 | MSTAR-PublicT72Variants-CD1/MSTAR_PUBLIC_T_72_VARIANTS_CD1 377 | ├ 15_DEG/COL2/SCENE1 378 | │ ├ A04/*.017 (274 images) 379 | │ ├ A05/*.018 (274 images) 380 | │ ├ A07/*.019 (274 images) 381 | │ ├ A10/*.020 (271 images) 382 | ├ └ ... 383 | └ ... 384 | 385 | ``` 386 | 387 | ##### Quick Start Guide for Training 388 | 389 | - Dataset Preparation 390 | - Download the [dataset.zip](https://github.com/jangsoopark/AConvNet-pytorch/releases/download/v2.2.0/dataset.zip) 391 | - After extracting it, you can find `train` and `test` directories inside `raw` directory. 392 | - Place the two directories (`train` and `test`) to the `dataset/eoc-2-vv/raw`. 393 | ```shell 394 | $ cd src/data 395 | $ python3 generate_dataset.py --is_train=True --use_phase=True --chip_size=100 --patch_size=94 --use_phase=True --dataset=eoc-2-vv 396 | $ python3 generate_dataset.py --is_train=False --use_phase=True --chip_size=128 --patch_size=128 --use_phase=True --dataset=eoc-2-vv 397 | $ cd .. 398 | $ python3 train.py --config_name=config/AConvNet-EOC-2-CV.json 399 | ``` 400 | 401 | ##### Results of EOC-2 Version Variants 402 | - Final Accuracy is **97.17%** at epoch 88 (The official accuracy is 98.60%) 403 | - You can see the details in `notebook/experiments-EOC-2-VV.ipynb` 404 | 405 | - Visualization of training loss and test accuracy 406 | 407 | ![eoc-2-vv-training-plot](./assets/figure/eoc-2-vv-training-plot.png) 408 | 409 | - Confusion Matrix with best model at **epoch 88** 410 | 411 | ![eoc-2-vv-confusion-matrix](./assets/figure/eoc-2-vv-confusion-matrix.png) 412 | 413 | 414 | 415 | ### Outlier Rejection 416 | 417 | | | | Train | | Test | | Remarks. | 418 | | ------- | ---------- | ---------- | ---------- | ---------- | ---------- | ---------- | 419 | | Class | Serial No. | Depression | No. Images | Depression | No. Images | Type. | 420 | | BMP-2 | 9563 | 17 | 233 | 15 | 196 | Known | 421 | | BTR-70 | c71 | 17 | 233 | 15 | 196 | Known | 422 | | T-72 | 132 | 17 | 232 | 15 | 196 | Known | 423 | | 2S1 | b01 | 17 | - | 15 | 274 | Confuser | 424 | | ZSU-234 | d08 | 17 | - | 15 | 274 | Confuser | 425 | 426 | ##### Training Set (Known targets in Depression: 17$\degree$) 427 | 428 | ```shell 429 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 430 | ├ TARGETS/TRAIN/17_DEG # KNOWN 431 | │ ├ BMP2/SN_9563/*.000 (233 images) 432 | │ ├ BTR70/SN_C71/*.004 (233 images) 433 | │ └ T72/SN_132/*.015 (232 images) 434 | └ ... 435 | 436 | MSTAR-PublicMixedTargets-CD2/MSTAR_PUBLIC_MIXED_TARGETS_CD2 437 | ├ 17_DEG # Confuser 438 | │ └ COL2/SCENE1 439 | │ ├ 2S1/*.000 (299 images) 440 | │ └ ZIL131/*.025 (299 images) 441 | └ ... 442 | 443 | ``` 444 | 445 | 446 | ##### Test Set (Known targets and confuser targets in Depression: 15$\degree$) 447 | ```shell 448 | MSTAR-PublicTargetChips-T72-BMP2-BTR70-SLICY/MSTAR_PUBLIC_TARGETS_CHIPS_T72_BMP2_BTR70_SLICY 449 | ├ TARGETS/TEST/15_DEG # KNOWN 450 | │ ├ BMP2/SN_9563/*.000 (195 images) 451 | │ ├ BTR70/SN_C71/*.004 (196 images) 452 | │ └ T72/SN_132/*.015 (196 images) 453 | └ ... 454 | 455 | MSTAR-PublicMixedTargets-CD1/MSTAR_PUBLIC_MIXED_TARGETS_CD1 456 | ├ 15_DEG # Confuser 457 | │ └ COL2/SCENE1 458 | │ ├ 2S1/*.000 (274 images) 459 | │ └ ZIL131/*.025 (274 images) 460 | └ ... 461 | 462 | ``` 463 | 464 | ##### Quick Start Guide for Training 465 | 466 | - Dataset Preparation 467 | - Download the [dataset.zip](https://github.com/jangsoopark/AConvNet-pytorch/releases/download/v2.2.0/dataset.zip) 468 | - After extracting it, you can find `train` and `test` directories inside `raw` directory. 469 | - Place the two directories (`train` and `test`) to the `dataset/confuser-rejection/raw`. 470 | 471 | ```shell 472 | $ cd src/data 473 | $ python3 generate_dataset.py --is_train=True --use_phase=True --chip_size=100 --patch_size=94 --use_phase=True --dataset=confuser-rejection 474 | $ python3 generate_dataset.py --is_train=False --use_phase=True --chip_size=128 --patch_size=128 --use_phase=True --dataset=confuser-rejection 475 | $ cd .. 476 | $ 477 | $ # Remove Confuser objects when training to check validation accuracy with known targets 478 | $ cd ../dataset/confuser-rejection/test 479 | $ rm -rf 2S1 480 | $ rm -rf ZIL131 481 | $ cd - 482 | $ 483 | $ python3 train.py --config_name=config/AConvNet-CR.json 484 | $ 485 | $ # Restore the unknown targets 486 | $ cd ../dataset/confuser-rejection/ 487 | $ rm -rf test 488 | $ python3 generate_dataset.py --is_train=False --use_phase=True --chip_size=128 --patch_size=128 --use_phase=True --dataset=confuser-rejection 489 | ``` 490 | 491 | 492 | ##### Results of Outlier Rejection 493 | - Final Accuracy for known targets is **98.81%** at epoch 39 494 | - You can see the details in `notebook/experiments-CR.ipynb` 495 | 496 | - Visualization of training loss and test accuracy 497 | 498 | ![cr-training-plot](./assets/figure/cr-training-plot.png) 499 | 500 | - Confusion Matrix with best model at **epoch 88** 501 | 502 | ![cr-confusion-matrix](./assets/figure/cr-confusion-matrix.png) 503 | 504 | 505 | - Outlier Rejection: TODO: LOoking for more details.. 506 | ```python 507 | # Rules 508 | output_probability = model(image) 509 | is_confuser = output_probability < thresh 510 | 511 | if sum(is_confuser) == 3: 512 | target is confuser 513 | else: 514 | target is known 515 | 516 | ``` 517 | 518 | ![cr-roc](./assets/figure/cr-roc.png) 519 | 520 | 523 | ## Details about the specific environment of this repository 524 | 525 | | | | 526 | | :---------: | :------: | 527 | | OS | Ubuntu 20.04 LTS | 528 | | CPU | Intel i7-10700k | 529 | | GPU | RTX 2080Ti 11GB | 530 | | Memory | 32GB | 531 | | SSD | 500GB | 532 | | HDD | 2TB | 533 | 534 | ## Citation 535 | 536 | ```bibtex 537 | @ARTICLE{7460942, 538 | author={S. {Chen} and H. {Wang} and F. {Xu} and Y. {Jin}}, 539 | journal={IEEE Transactions on Geoscience and Remote Sensing}, 540 | title={Target Classification Using the Deep Convolutional Networks for SAR Images}, 541 | year={2016}, 542 | volume={54}, 543 | number={8}, 544 | pages={4806-4817}, 545 | doi={10.1109/TGRS.2016.2551720} 546 | } 547 | ``` 548 | 549 | ## References 550 | [1] G. Dong, N. Wang, and G. Kuang, 551 | "Sparse representation of monogenic signal: With application to target recognition in SAR images," 552 | *IEEE Signal Process. Lett.*, vol. 21, no. 8, pp. 952-956, Aug. 2014. 553 | 554 | 555 | --- 556 | 557 | ## TODO 558 | 559 | - [ ] Implementation 560 | - [ ] Data generation 561 | - [X] SOC 562 | - [X] EOC 563 | - [X] Outlier Rejection 564 | - [ ] End-to-End SAR-ATR 565 | - [ ] Data Loader 566 | - [X] SOC 567 | - [X] EOC 568 | - [X] Outlier Rejection 569 | - [ ] End-to-End SAR-ATR 570 | - [X] Model 571 | - [X] Network 572 | - [X] Training 573 | - [X] Early Stopping 574 | - [X] Hyper-parameter Optimization 575 | - [ ] Experiments 576 | - [X] Reproduce the SOC Results 577 | - [X] Reproduce the EOC Results 578 | - [X] Reproduce the outlier rejection 579 | - [ ] Reproduce the end-to-end SAR-ATR 580 | 581 | -------------------------------------------------------------------------------- /assets/figure/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/001.png -------------------------------------------------------------------------------- /assets/figure/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/002.png -------------------------------------------------------------------------------- /assets/figure/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/003.png -------------------------------------------------------------------------------- /assets/figure/004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/004.png -------------------------------------------------------------------------------- /assets/figure/005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/005.png -------------------------------------------------------------------------------- /assets/figure/006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/006.png -------------------------------------------------------------------------------- /assets/figure/007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/007.png -------------------------------------------------------------------------------- /assets/figure/008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/008.png -------------------------------------------------------------------------------- /assets/figure/009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/009.png -------------------------------------------------------------------------------- /assets/figure/010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/010.png -------------------------------------------------------------------------------- /assets/figure/011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/011.png -------------------------------------------------------------------------------- /assets/figure/2S1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/2S1.png -------------------------------------------------------------------------------- /assets/figure/confuser-rejection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/confuser-rejection.png -------------------------------------------------------------------------------- /assets/figure/cr-confusion-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/cr-confusion-matrix.png -------------------------------------------------------------------------------- /assets/figure/cr-roc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/cr-roc.png -------------------------------------------------------------------------------- /assets/figure/cr-training-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/cr-training-plot.png -------------------------------------------------------------------------------- /assets/figure/eoc-1-confusion-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/eoc-1-confusion-matrix.png -------------------------------------------------------------------------------- /assets/figure/eoc-1-training-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/eoc-1-training-plot.png -------------------------------------------------------------------------------- /assets/figure/eoc-2-cv-confusion-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/eoc-2-cv-confusion-matrix.png -------------------------------------------------------------------------------- /assets/figure/eoc-2-cv-training-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/eoc-2-cv-training-plot.png -------------------------------------------------------------------------------- /assets/figure/eoc-2-vv-confusion-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/eoc-2-vv-confusion-matrix.png -------------------------------------------------------------------------------- /assets/figure/eoc-2-vv-training-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/eoc-2-vv-training-plot.png -------------------------------------------------------------------------------- /assets/figure/soc-confusion-matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/soc-confusion-matrix.png -------------------------------------------------------------------------------- /assets/figure/soc-training-plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/assets/figure/soc-training-plot.png -------------------------------------------------------------------------------- /dataset/confuser-rejection/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/dataset/confuser-rejection/.gitkeep -------------------------------------------------------------------------------- /dataset/confuser-rejection/raw/rename.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | target_list = glob.glob('./*/*') 6 | 7 | for name in target_list: 8 | print(name, name.replace('_', '')) 9 | os.rename(name, name.replace('_', '')) 10 | -------------------------------------------------------------------------------- /dataset/eoc-1-t72-132/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/dataset/eoc-1-t72-132/.gitkeep -------------------------------------------------------------------------------- /dataset/eoc-1-t72-132/raw/rename.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | target_list = glob.glob('./*/*') 6 | 7 | for name in target_list: 8 | print(name, name.replace('_', '')) 9 | os.rename(name, name.replace('_', '')) 10 | -------------------------------------------------------------------------------- /dataset/eoc-1-t72-a64/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/dataset/eoc-1-t72-a64/.gitkeep -------------------------------------------------------------------------------- /dataset/eoc-1-t72-a64/raw/rename.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | target_list = glob.glob('./*/*') 6 | 7 | for name in target_list: 8 | print(name, name.replace('_', '')) 9 | os.rename(name, name.replace('_', '')) 10 | -------------------------------------------------------------------------------- /dataset/eoc-2-cv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/dataset/eoc-2-cv/.gitkeep -------------------------------------------------------------------------------- /dataset/eoc-2-cv/raw/rename.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | target_list = glob.glob('./*/*') 6 | 7 | for name in target_list: 8 | print(name, name.replace('_', '')) 9 | os.rename(name, name.replace('_', '')) 10 | -------------------------------------------------------------------------------- /dataset/eoc-2-vv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/dataset/eoc-2-vv/.gitkeep -------------------------------------------------------------------------------- /dataset/eoc-2-vv/raw/rename.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | target_list = glob.glob('./*/*') 6 | 7 | for name in target_list: 8 | print(name, name.replace('_', '')) 9 | os.rename(name, name.replace('_', '')) 10 | -------------------------------------------------------------------------------- /dataset/soc/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/dataset/soc/.gitkeep -------------------------------------------------------------------------------- /dataset/soc/raw/rename.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | 4 | 5 | target_list = glob.glob('./*/*') 6 | 7 | for name in target_list: 8 | print(name, name.replace('_', '')) 9 | os.rename(name, name.replace('_', '')) 10 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | # docker build . -t aconvnet-pytorch 3 | # Base container: docker pull pytorch/pytorch:1.9.0-cuda11.1-cudnn8-devel 4 | 5 | FROM pytorch/pytorch:1.9.0-cuda11.1-cudnn8-devel 6 | 7 | ARG DEBIAN_FRONTEND=noninteractive 8 | 9 | RUN apt update 10 | 11 | RUN pip install seaborn && \ 12 | pip install numpy && \ 13 | pip install scipy&& \ 14 | pip install tqdm && \ 15 | pip install jupyter && \ 16 | pip install matplotlib && \ 17 | pip install scikit-image && \ 18 | pip install scikit-learn && \ 19 | pip install opencv-python && \ 20 | pip install absl-py && \ 21 | pip install optuna 22 | 23 | 24 | RUN apt update && \ 25 | apt install -y wget vim emacs nano libgl1-mesa-glx 26 | 27 | 28 | RUN mkdir -p /workspace 29 | 30 | ARG work_dir=/workspace 31 | 32 | WORKDIR ${work_dir} 33 | -------------------------------------------------------------------------------- /experiments/config/AConvNet-CR.json: -------------------------------------------------------------------------------- 1 | { 2 | "model_name": "AConvNet-CR", 3 | "dataset": "confuser-rejection", 4 | "num_classes": 3, 5 | "channels": 2, 6 | "batch_size": 100, 7 | "epochs": 100, 8 | "momentum": 0.9, 9 | "lr": 1e-3, 10 | "lr_step": [50], 11 | "lr_decay": 0.1, 12 | "weight_decay": 4e-3, 13 | "dropout_rate": 0.5 14 | } 15 | -------------------------------------------------------------------------------- /experiments/config/AConvNet-EOC-1-T72-132.json: -------------------------------------------------------------------------------- 1 | { 2 | "model_name": "AConvNet-EOC-1-T72-132", 3 | "dataset": "eoc-1-t72-132", 4 | "num_classes": 4, 5 | "channels": 2, 6 | "batch_size": 100, 7 | "epochs": 100, 8 | "momentum": 0.9, 9 | "lr": 1e-3, 10 | "lr_step": [50], 11 | "lr_decay": 0.1, 12 | "weight_decay": 4e-3, 13 | "dropout_rate": 0.5 14 | } 15 | -------------------------------------------------------------------------------- /experiments/config/AConvNet-EOC-1-T72-A64.json: -------------------------------------------------------------------------------- 1 | { 2 | "model_name": "AConvNet-EOC-1-T72-A64", 3 | "dataset": "eoc-1-t72-a64", 4 | "num_classes": 4, 5 | "channels": 2, 6 | "batch_size": 100, 7 | "epochs": 100, 8 | "momentum": 0.9, 9 | "lr": 1e-3, 10 | "lr_step": [50], 11 | "lr_decay": 0.1, 12 | "weight_decay": 4e-3, 13 | "dropout_rate": 0.5 14 | } 15 | -------------------------------------------------------------------------------- /experiments/config/AConvNet-EOC-2-CV.json: -------------------------------------------------------------------------------- 1 | { 2 | "model_name": "AConvNet-EOC-2-CV", 3 | "dataset": "eoc-2-cv", 4 | "num_classes": 4, 5 | "channels": 2, 6 | "batch_size": 100, 7 | "epochs": 100, 8 | "momentum": 0.9, 9 | "lr": 1e-3, 10 | "lr_step": [5], 11 | "lr_decay": 0.1, 12 | "weight_decay": 4e-3, 13 | "dropout_rate": 0.5 14 | } 15 | -------------------------------------------------------------------------------- /experiments/config/AConvNet-EOC-2-VV.json: -------------------------------------------------------------------------------- 1 | { 2 | "model_name": "AConvNet-EOC-2-VV", 3 | "dataset": "eoc-2-vv", 4 | "num_classes": 4, 5 | "channels": 2, 6 | "batch_size": 100, 7 | "epochs": 100, 8 | "momentum": 0.9, 9 | "lr": 1e-3, 10 | "lr_step": [5], 11 | "lr_decay": 0.1, 12 | "weight_decay": 4e-3, 13 | "dropout_rate": 0.5 14 | } 15 | -------------------------------------------------------------------------------- /experiments/config/AConvNet-SOC.json: -------------------------------------------------------------------------------- 1 | { 2 | "model_name": "AConvNet-SOC", 3 | "dataset": "soc", 4 | "num_classes": 10, 5 | "channels": 2, 6 | "batch_size": 100, 7 | "epochs": 100, 8 | "momentum": 0.9, 9 | "lr": 1e-3, 10 | "lr_step": [50], 11 | "lr_decay": 0.1, 12 | "weight_decay": 4e-3, 13 | "dropout_rate": 0.5 14 | } 15 | -------------------------------------------------------------------------------- /notebook/experiments-EOC-1-T72-A64.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import matplotlib.pyplot as plt\n", 10 | "\n", 11 | "import numpy as np\n", 12 | "\n", 13 | "import json\n", 14 | "import glob\n", 15 | "import sys\n", 16 | "import os\n", 17 | "\n", 18 | "sys.path.append('../src')" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### Visualization of training loss and test accuracy" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "with open('../experiments/history/history-AConvNet-EOC-1-T72-A64.json') as f:\n", 35 | " history = json.load(f)" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 3, 41 | "metadata": {}, 42 | "outputs": [ 43 | { 44 | "data": { 45 | "image/png": "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\n", 46 | "text/plain": [ 47 | "
" 48 | ] 49 | }, 50 | "metadata": { 51 | "needs_background": "light" 52 | }, 53 | "output_type": "display_data" 54 | } 55 | ], 56 | "source": [ 57 | "training_loss = history['loss']\n", 58 | "test_accuracy = history['accuracy']\n", 59 | "\n", 60 | "epochs = np.arange(len(training_loss))\n", 61 | "\n", 62 | "fig, ax1 = plt.subplots()\n", 63 | "ax2 = ax1.twinx()\n", 64 | "\n", 65 | "plot1, = ax1.plot(epochs, training_loss, marker='.', c='blue', label='loss')\n", 66 | "plot2, = ax2.plot(epochs, test_accuracy, marker='.', c='red', label='accuracy')\n", 67 | "plt.legend([plot1, plot2], ['loss', 'accuracy'], loc='upper right')\n", 68 | "\n", 69 | "plt.grid()\n", 70 | "\n", 71 | "ax1.set_xlabel('Epoch')\n", 72 | "ax1.set_ylabel('loss', color='blue')\n", 73 | "ax2.set_ylabel('accuracy', color='red')\n", 74 | "plt.show()\n" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "### Early Stopping" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 4, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "from tqdm import tqdm\n", 91 | "import torchvision\n", 92 | "import torch\n", 93 | "\n", 94 | "from utils import common\n", 95 | "from data import preprocess\n", 96 | "from data import loader\n", 97 | "import model" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 5, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "def load_dataset(path, is_train, name, batch_size):\n", 107 | "\n", 108 | " _dataset = loader.Dataset(\n", 109 | " path, name=name, is_train=is_train,\n", 110 | " transform=torchvision.transforms.Compose([\n", 111 | " preprocess.CenterCrop(88), torchvision.transforms.ToTensor()\n", 112 | " ])\n", 113 | " )\n", 114 | " data_loader = torch.utils.data.DataLoader(\n", 115 | " _dataset, batch_size=batch_size, shuffle=is_train, num_workers=1\n", 116 | " )\n", 117 | " return data_loader\n", 118 | "\n", 119 | "\n", 120 | "def evaluate(_m, ds):\n", 121 | " \n", 122 | " num_data = 0\n", 123 | " corrects = 0\n", 124 | " \n", 125 | " _m.net.eval()\n", 126 | " _softmax = torch.nn.Softmax(dim=1)\n", 127 | " for i, data in enumerate(ds):\n", 128 | " images, labels, _ = data\n", 129 | "\n", 130 | " predictions = _m.inference(images)\n", 131 | " predictions = _softmax(predictions)\n", 132 | "\n", 133 | " _, predictions = torch.max(predictions.data, 1)\n", 134 | " labels = labels.type(torch.LongTensor)\n", 135 | " num_data += labels.size(0)\n", 136 | " corrects += (predictions == labels.to(m.device)).sum().item()\n", 137 | "\n", 138 | " accuracy = 100 * corrects / num_data\n", 139 | " return accuracy" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 6, 145 | "metadata": {}, 146 | "outputs": [ 147 | { 148 | "name": "stderr", 149 | "output_type": "stream", 150 | "text": [ 151 | "load test data set: 1151it [00:00, 2070.14it/s]\n", 152 | "d:\\ivs\\project\\004-research\\signal-processing\\image-processing\\remote-sensing\\aconvnet\\aconvnet-pytorch\\venv\\lib\\site-packages\\torch\\nn\\functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at ..\\c10/core/TensorImpl.h:1156.)\n", 153 | " return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)\n" 154 | ] 155 | }, 156 | { 157 | "name": "stdout", 158 | "output_type": "stream", 159 | "text": [ 160 | "Best accuracy at epoch=0 with 53.69%\n", 161 | "Best accuracy at epoch=1 with 70.20%\n", 162 | "Best accuracy at epoch=2 with 84.88%\n", 163 | "Best accuracy at epoch=3 with 89.31%\n", 164 | "Best accuracy at epoch=5 with 90.44%\n", 165 | "Best accuracy at epoch=15 with 90.96%\n", 166 | "Best accuracy at epoch=17 with 91.49%\n", 167 | "Final model is epoch=17 with accurayc=91.49%\n", 168 | "Path=D:\\ivs\\Project\\004-research\\signal-processing\\image-processing\\remote-sensing\\aconvnet\\AConvNet-pytorch\\experiments/model/AConvNet-EOC-1-T72-A64\\model-018.pth\n" 169 | ] 170 | } 171 | ], 172 | "source": [ 173 | "\n", 174 | "config = common.load_config(os.path.join(common.project_root, 'experiments/config/AConvNet-EOC-1-T72-A64.json'))\n", 175 | "model_name = config['model_name']\n", 176 | "test_set = load_dataset('dataset', False, 'eoc-1-t72-a64', 100)\n", 177 | "\n", 178 | "m = model.Model(\n", 179 | " classes=config['num_classes'], channels=config['channels'],\n", 180 | ")\n", 181 | "\n", 182 | "model_history = glob.glob(os.path.join(common.project_root, f'experiments/model/{model_name}/*.pth'))\n", 183 | "model_history = sorted(model_history, key=os.path.basename)\n", 184 | "\n", 185 | "best = {\n", 186 | " 'epoch': 0,\n", 187 | " 'accuracy': 0,\n", 188 | " 'path': ''\n", 189 | "}\n", 190 | "\n", 191 | "for i, model_path in enumerate(model_history):\n", 192 | " m.load(model_path)\n", 193 | " accuracy = evaluate(m, test_set)\n", 194 | " if accuracy > best['accuracy']:\n", 195 | " best['epoch'] = i\n", 196 | " best['accuracy'] = accuracy\n", 197 | " best['path'] = model_path\n", 198 | " print(f'Best accuracy at epoch={i} with {accuracy:.2f}%')\n", 199 | " \n", 200 | "best_epoch = best['epoch']\n", 201 | "best_accuracy = best['accuracy']\n", 202 | "best_path = best['path']\n", 203 | "\n", 204 | "print(f'Final model is epoch={best_epoch} with accurayc={best_accuracy:.2f}%')\n", 205 | "print(f'Path={best_path}')" 206 | ] 207 | }, 208 | { 209 | "cell_type": "markdown", 210 | "metadata": {}, 211 | "source": [ 212 | "### Confusion Matrix with Best Model" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 7, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "from sklearn import metrics\n", 222 | "from data import mstar\n", 223 | "\n", 224 | "def confusion_matrix(_m, ds):\n", 225 | " _pred = []\n", 226 | " _gt = []\n", 227 | " \n", 228 | " _m.net.eval()\n", 229 | " _softmax = torch.nn.Softmax(dim=1)\n", 230 | " for i, data in enumerate(ds):\n", 231 | " images, labels, _ = data\n", 232 | " \n", 233 | " predictions = _m.inference(images)\n", 234 | " predictions = _softmax(predictions)\n", 235 | "\n", 236 | " _, predictions = torch.max(predictions.data, 1)\n", 237 | " labels = labels.type(torch.LongTensor)\n", 238 | " \n", 239 | " _pred += predictions.cpu().tolist()\n", 240 | " _gt += labels.cpu().tolist()\n", 241 | " \n", 242 | " conf_mat = metrics.confusion_matrix(_gt, _pred)\n", 243 | " \n", 244 | " return conf_mat" 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": 8, 250 | "metadata": {}, 251 | "outputs": [ 252 | { 253 | "data": { 254 | "image/png": "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tQICAnTp0iXdvHlTW7duValSpex9aKmexTAe8oF1ANKlMWPGaOPGjfrwww9VvXp1SbLemqFv374qVaqUqlWrpkGDBmnx4sX66aefVLRoUUmyftjwunXrVLFixXR3Hx0gtTp16pSaN2+ukydP6v3339egQYN0+fJl5c2bV7t27VKpUqV0584dbd26Vffu3VPFihXl6OioHDlySJIqVqyoIkWKaN68eXY+kmcXJ1YBWPXu3Vuurq7WIBUQEKC5c+eqRo0aWrx4serVq6epU6dqwoQJunfvnoKDgxUdHS3pr5sd1q1bl6AFpBIJCQmaM2eOChcurDNnzljvdbV9+3Z5e3vLx8dHCQkJcnd3V506dfTaa6/Jx8dHOXLkUEJCgm7duiVXV1dlz57dzkfybCNsAbDKnDmzOnXqpI0bN6p+/fpq0aKFDh48qBkzZmj79u2qVKmSgoODJUmjRo3S+PHjdfjwYUlcFAGkRo6Ojvroo4+0aNEiZcmSRXFxcZKk8+fPy83NTTlz5nzoz25CQoKioqI0duxY3bx503o3ePw3zNkCYOP111/Xd999p3z58qlHjx7KkiWLoqOj5ebmppdeeklTpkzR1atX1bNnTyUkJKhcuXL2LhnAv/j7fa4eXG3422+/KSAgQNL9UekHVxBeunRJixcv1qVLl7Rs2TJlypRJc+bMUfny5VO87rSEkS0ANpydnfXuu+/q3r17mjZtmqT7H6tz69Yt7d27V2+99ZYyZcokSerTp489SwXwHxiGofDwcBUrVizJsv3790uSLl68qDFjxujAgQMErWRA2AKQRPny5VWhQgWFhITo2LFj2rhxo0qXLq2wsDD16tVLGTJksHeJAP6j+Ph4HT16VLlz55Z0/75Y4eHhatu2rdq2bat69epp0aJFat26tZ0rTTsIWwCSsFgsGjhwoO7du6eAgAA1atRI3bp10+7du/XCCy/YuzwAT+HPP//UrVu3VK1aNUnS2LFj5e/vr/Pnz+u3337T888/b98C0yDmbAF4qLx586p58+aqWrWqPvroI0azgDTi1q1bypUrl3744QfNmDFD9+7d07fffqv69evbu7Q0i/tsAfhHD+6dBSDt+Oabb9SuXTt5enpqyJAhGjJkiL1LSvMIWwAApCPbt2/Xli1brHeGh/kIWwAApCOMWKc8JsgDAJCOELRSHmELAADARIQt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\n", 255 | "text/plain": [ 256 | "
" 257 | ] 258 | }, 259 | "metadata": {}, 260 | "output_type": "display_data" 261 | } 262 | ], 263 | "source": [ 264 | "import matplotlib.pyplot as plt\n", 265 | "import seaborn as sns\n", 266 | "\n", 267 | "m.load(best_path)\n", 268 | "_conf_mat = confusion_matrix(m, test_set)\n", 269 | "\n", 270 | "sns.reset_defaults()\n", 271 | "ax = sns.heatmap(_conf_mat, annot=True, fmt='d', cbar=False)\n", 272 | "ax.set_yticklabels(mstar.target_name_eoc_1, rotation=0)\n", 273 | "ax.set_xticklabels(mstar.target_name_eoc_1, rotation=30)\n", 274 | "\n", 275 | "plt.xlabel('prediction', fontsize=12)\n", 276 | "plt.ylabel('label', fontsize=12)\n", 277 | "\n", 278 | "\n", 279 | "plt.show()" 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "### Noise Simulation" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 9, 292 | "metadata": {}, 293 | "outputs": [], 294 | "source": [ 295 | "from skimage import util\n", 296 | "\n", 297 | "\n", 298 | "def generate_noise(_images, amount):\n", 299 | " \n", 300 | " n, _, h, w = _images.shape\n", 301 | " \n", 302 | " noise = np.array([np.random.uniform(size=(1, h, w)) for _ in range(n)])\n", 303 | " portions = np.array([\n", 304 | " util.random_noise(np.zeros((1, h, w)), mode='s&p', amount=amount)\n", 305 | " for _ in range(n)\n", 306 | " ])\n", 307 | " noise = noise * portions\n", 308 | " \n", 309 | " return _images + noise.astype(np.float32)\n", 310 | "\n", 311 | "\n", 312 | "def noise_simulation(_m, ds, noise_ratio):\n", 313 | " \n", 314 | " num_data = 0\n", 315 | " corrects = 0\n", 316 | " \n", 317 | " _m.net.eval()\n", 318 | " _softmax = torch.nn.Softmax(dim=1)\n", 319 | " for i, data in enumerate(ds):\n", 320 | " images, labels, _ = data\n", 321 | " images = generate_noise(images, noise_ratio)\n", 322 | "\n", 323 | " predictions = _m.inference(images)\n", 324 | " predictions = _softmax(predictions)\n", 325 | "\n", 326 | " _, predictions = torch.max(predictions.data, 1)\n", 327 | " labels = labels.type(torch.LongTensor)\n", 328 | " num_data += labels.size(0)\n", 329 | " corrects += (predictions == labels.to(m.device)).sum().item()\n", 330 | "\n", 331 | " accuracy = 100 * corrects / num_data\n", 332 | " \n", 333 | " return accuracy" 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": 10, 339 | "metadata": {}, 340 | "outputs": [ 341 | { 342 | "name": "stdout", 343 | "output_type": "stream", 344 | "text": [ 345 | "ratio = 0.01, accuracy = 90.96\n", 346 | "ratio = 0.05, accuracy = 85.23\n", 347 | "ratio = 0.10, accuracy = 83.23\n", 348 | "ratio = 0.15, accuracy = 80.54\n" 349 | ] 350 | } 351 | ], 352 | "source": [ 353 | "noise_result = {}\n", 354 | "\n", 355 | "for ratio in [0.01, 0.05, 0.10, 0.15]:\n", 356 | " noise_result[ratio] = noise_simulation(m, test_set, ratio)\n", 357 | " print(f'ratio = {ratio:.2f}, accuracy = {noise_result[ratio]:.2f}')\n" 358 | ] 359 | } 360 | ], 361 | "metadata": { 362 | "kernelspec": { 363 | "display_name": "Python 3 (ipykernel)", 364 | "language": "python", 365 | "name": "python3" 366 | }, 367 | "language_info": { 368 | "codemirror_mode": { 369 | "name": "ipython", 370 | "version": 3 371 | }, 372 | "file_extension": ".py", 373 | "mimetype": "text/x-python", 374 | "name": "python", 375 | "nbconvert_exporter": "python", 376 | "pygments_lexer": "ipython3", 377 | "version": "3.7.9" 378 | } 379 | }, 380 | "nbformat": 4, 381 | "nbformat_minor": 4 382 | } 383 | -------------------------------------------------------------------------------- /notebook/experiments-EOC-2-CV.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import matplotlib.pyplot as plt\n", 10 | "\n", 11 | "import numpy as np\n", 12 | "\n", 13 | "import json\n", 14 | "import glob\n", 15 | "import sys\n", 16 | "import os\n", 17 | "\n", 18 | "sys.path.append('../src')" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": {}, 24 | "source": [ 25 | "### Visualization of training loss and test accuracy" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "with open('../experiments/history/history-AConvNet-EOC-2-CV.json') as f:\n", 35 | " history = json.load(f)" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": 3, 41 | "metadata": {}, 42 | "outputs": [ 43 | { 44 | "data": { 45 | "image/png": "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\n", 46 | "text/plain": [ 47 | "
" 48 | ] 49 | }, 50 | "metadata": { 51 | "needs_background": "light" 52 | }, 53 | "output_type": "display_data" 54 | } 55 | ], 56 | "source": [ 57 | "training_loss = history['loss']\n", 58 | "test_accuracy = history['accuracy']\n", 59 | "\n", 60 | "epochs = np.arange(len(training_loss))\n", 61 | "\n", 62 | "fig, ax1 = plt.subplots()\n", 63 | "ax2 = ax1.twinx()\n", 64 | "\n", 65 | "plot1, = ax1.plot(epochs, training_loss, marker='.', c='blue', label='loss')\n", 66 | "plot2, = ax2.plot(epochs, test_accuracy, marker='.', c='red', label='accuracy')\n", 67 | "plt.legend([plot1, plot2], ['loss', 'accuracy'], loc='upper right')\n", 68 | "\n", 69 | "plt.grid()\n", 70 | "\n", 71 | "ax1.set_xlabel('Epoch')\n", 72 | "ax1.set_ylabel('loss', color='blue')\n", 73 | "ax2.set_ylabel('accuracy', color='red')\n", 74 | "plt.show()\n" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "### Early Stopping" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 4, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "from tqdm import tqdm\n", 91 | "import torchvision\n", 92 | "import torch\n", 93 | "\n", 94 | "from utils import common\n", 95 | "from data import preprocess\n", 96 | "from data import loader\n", 97 | "import model" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 5, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "def load_dataset(path, is_train, name, batch_size):\n", 107 | "\n", 108 | " _dataset = loader.Dataset(\n", 109 | " path, name=name, is_train=is_train,\n", 110 | " transform=torchvision.transforms.Compose([\n", 111 | " preprocess.CenterCrop(88), torchvision.transforms.ToTensor()\n", 112 | " ])\n", 113 | " )\n", 114 | " data_loader = torch.utils.data.DataLoader(\n", 115 | " _dataset, batch_size=batch_size, shuffle=is_train, num_workers=1\n", 116 | " )\n", 117 | " return data_loader\n", 118 | "\n", 119 | "\n", 120 | "def evaluate(_m, ds):\n", 121 | " \n", 122 | " num_data = 0\n", 123 | " corrects = 0\n", 124 | " \n", 125 | " _m.net.eval()\n", 126 | " _softmax = torch.nn.Softmax(dim=1)\n", 127 | " for i, data in enumerate(ds):\n", 128 | " images, labels, _ = data\n", 129 | "\n", 130 | " predictions = _m.inference(images)\n", 131 | " predictions = _softmax(predictions)\n", 132 | "\n", 133 | " _, predictions = torch.max(predictions.data, 1)\n", 134 | " labels = labels.type(torch.LongTensor)\n", 135 | " num_data += labels.size(0)\n", 136 | " corrects += (predictions == labels.to(m.device)).sum().item()\n", 137 | "\n", 138 | " accuracy = 100 * corrects / num_data\n", 139 | " return accuracy" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 6, 145 | "metadata": {}, 146 | "outputs": [ 147 | { 148 | "name": "stderr", 149 | "output_type": "stream", 150 | "text": [ 151 | "load test data set: 2710it [00:01, 2205.05it/s]\n", 152 | "d:\\ivs\\project\\004-research\\signal-processing\\image-processing\\remote-sensing\\aconvnet\\aconvnet-pytorch\\venv\\lib\\site-packages\\torch\\nn\\functional.py:718: UserWarning: Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at ..\\c10/core/TensorImpl.h:1156.)\n", 153 | " return torch.max_pool2d(input, kernel_size, stride, padding, dilation, ceil_mode)\n" 154 | ] 155 | }, 156 | { 157 | "name": "stdout", 158 | "output_type": "stream", 159 | "text": [ 160 | "Best accuracy at epoch=0 with 8.45%\n", 161 | "Best accuracy at epoch=1 with 61.73%\n", 162 | "Best accuracy at epoch=3 with 81.92%\n", 163 | "Best accuracy at epoch=4 with 85.90%\n", 164 | "Best accuracy at epoch=5 with 96.79%\n", 165 | "Best accuracy at epoch=9 with 97.82%\n", 166 | "Best accuracy at epoch=17 with 98.78%\n", 167 | "Best accuracy at epoch=23 with 99.15%\n", 168 | "Best accuracy at epoch=35 with 99.19%\n", 169 | "Best accuracy at epoch=42 with 99.30%\n", 170 | "Best accuracy at epoch=95 with 99.41%\n", 171 | "Final model is epoch=95 with accurayc=99.41%\n", 172 | "Path=D:\\ivs\\Project\\004-research\\signal-processing\\image-processing\\remote-sensing\\aconvnet\\AConvNet-pytorch\\experiments/model/AConvNet-EOC-2-CV\\model-096.pth\n" 173 | ] 174 | } 175 | ], 176 | "source": [ 177 | "\n", 178 | "config = common.load_config(os.path.join(common.project_root, 'experiments/config/AConvNet-EOC-2-CV.json'))\n", 179 | "model_name = config['model_name']\n", 180 | "test_set = load_dataset('dataset', False, 'eoc-2-cv', 100)\n", 181 | "\n", 182 | "m = model.Model(\n", 183 | " classes=config['num_classes'], channels=config['channels'],\n", 184 | ")\n", 185 | "\n", 186 | "model_history = glob.glob(os.path.join(common.project_root, f'experiments/model/{model_name}/*.pth'))\n", 187 | "model_history = sorted(model_history, key=os.path.basename)\n", 188 | "\n", 189 | "best = {\n", 190 | " 'epoch': 0,\n", 191 | " 'accuracy': 0,\n", 192 | " 'path': ''\n", 193 | "}\n", 194 | "\n", 195 | "for i, model_path in enumerate(model_history):\n", 196 | " m.load(model_path)\n", 197 | " accuracy = evaluate(m, test_set)\n", 198 | " if accuracy > best['accuracy']:\n", 199 | " best['epoch'] = i\n", 200 | " best['accuracy'] = accuracy\n", 201 | " best['path'] = model_path\n", 202 | " print(f'Best accuracy at epoch={i} with {accuracy:.2f}%')\n", 203 | " \n", 204 | "best_epoch = best['epoch']\n", 205 | "best_accuracy = best['accuracy']\n", 206 | "best_path = best['path']\n", 207 | "\n", 208 | "print(f'Final model is epoch={best_epoch} with accurayc={best_accuracy:.2f}%')\n", 209 | "print(f'Path={best_path}')" 210 | ] 211 | }, 212 | { 213 | "cell_type": "markdown", 214 | "metadata": {}, 215 | "source": [ 216 | "### Confusion Matrix with Best Model" 217 | ] 218 | }, 219 | { 220 | "cell_type": "code", 221 | "execution_count": 7, 222 | "metadata": {}, 223 | "outputs": [], 224 | "source": [ 225 | "from sklearn import metrics\n", 226 | "from data import mstar\n", 227 | "\n", 228 | "def confusion_matrix(_m, ds):\n", 229 | " conf_mat = {\n", 230 | " 'A32': np.zeros((1, 4), dtype=np.int32),\n", 231 | " 'A62': np.zeros((1, 4), dtype=np.int32),\n", 232 | " 'A63': np.zeros((1, 4), dtype=np.int32),\n", 233 | " 'A64': np.zeros((1, 4), dtype=np.int32),\n", 234 | " 's7': np.zeros((1, 4), dtype=np.int32),\n", 235 | " }\n", 236 | " _pred = []\n", 237 | " _gt = []\n", 238 | " \n", 239 | " _m.net.eval()\n", 240 | " _softmax = torch.nn.Softmax(dim=1)\n", 241 | " for i, data in enumerate(ds):\n", 242 | " images, labels, serial_numbers = data\n", 243 | " \n", 244 | " predictions = _m.inference(images)\n", 245 | " predictions = _softmax(predictions)\n", 246 | "\n", 247 | " _, predictions = torch.max(predictions.data, 1)\n", 248 | " \n", 249 | "# _pred += predictions.cpu().tolist()\n", 250 | " for s, c in zip(serial_numbers, predictions):\n", 251 | " conf_mat[s][0, c] += 1\n", 252 | " \n", 253 | " conf_mat = np.r_[\n", 254 | " conf_mat['A32'], \n", 255 | " conf_mat['A62'], \n", 256 | " conf_mat['A63'], \n", 257 | " conf_mat['A64'], \n", 258 | " conf_mat['s7']\n", 259 | " ]\n", 260 | " return conf_mat" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 8, 266 | "metadata": {}, 267 | "outputs": [ 268 | { 269 | "data": { 270 | "image/png": "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KgoxMTEA8seaeXl54dy5c7Czs4O7uztmzZolnzF73lmrwMBAjB49+jl7rK4ihS8iovKsvIQvKpoyHb4qOoYvIqLygeGrYilq+CqzA+6JiIiIyiKGLyIiIiIFMXwRERERKYjhi4iIiEhBDF9ERERECmL4IiIiIlIQwxcRERGRghi+iIiIiBTE8EVERESkIIYvIiIiIgUxfBEREREpiOGLiIiISEEMX0REREQKYvgiIiIiUhDDFxEREZGCGL6IiIiIFMTwRURERKQghi8iIiIiBTF8ERERESmI4YuIiIhIQQxfRERERApi+CIiIiJSEMMXERERkYIYvoiIiIgUxPBFREREpCCGLyIiIiIFMXwRERERKYjhi4iIiEhBDF9ERERECmL4IiIiIlIQwxcRERGRghi+iIiIiBTE8EVERESkIIYvIiIiIgUxfBEREREpiOGLiIiISEEMX0REREQKYvgiIiIiUhDDFxEREZGCGL6IiIiIFMTwRURERKQghi8iIiIiBTF8ERERESmI4YuIiIhIQQxfRERERApi+CIiIiJSEMMXERERkYIYvoiIiIgUxPBFREREpCCGLyIiIiIFMXwRERERKYjhi4iIiEhBOq/SeezYsa+8AUmS4O/v/8rLEREREZVHrxS+IiIiIEnSK23gVfsTERERlWevFL5u3bpVQmUQERERVQwc80VERESkoFc68/U8UVFROHLkCBITEzFlyhTUr18f6enpuHLlCho0aABjY+Pi2AwRERFRmfdGZ76ys7MxcOBAdOnSBXPnzsXq1asRFxeXv2ItLbz99ttYtWpVsRRKREREVB68UfiaN28efv75Z/j5+SE2NhZCCHlepUqVMGTIEOzateuNiyQiIiIqL94ofP3444+YPHkyJkyYAAsLiwLzGzdujJs3b77JJoiIiIjKlTcKX4mJiWjevPlz52trayM9Pf1NNkFERERUrrxR+KpRowauXLny3PknTpxAvXr13mQTREREROXKG4WvESNG4Ntvv8WpU6fktmcPVd24cSPCwsIwatSoN6uQyrVZn07FqZN78fhRLO7dOY/t2/zRoEFdTZdFJWzyJDdcvxqFtCc3cPL4HrRr21LTJVEJ6Na1A3b+FITbt84gN/suXFz6arokek3rAoLRrNs7atOADycBAO7eTygw79l04MhxeR33ExIx+ZOFaNt7ELoP+BC+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NwpfKpUKc+bMwfr165FeyOc7PaPE5zwSERERlQVvdNnR29sby5cvh6urKzZv3gwhBL788kts2LABDg4OaNGiBZ9vRURERPQvbxS+goKCMHToUPj5+cHJyQlA/sfpjB8/HqdPn4YkSWqfjUhERERU0b1R+Lpz5w569uwJAPLnEz77SBg9PT24urpiy5Ytb1giERERUfnxRuGrSpUqSEtLAwAYGxvD1NQUN2/eVOvz+PHjN9kEERERUbnyxp/tGB0dLb92dHTEypUr0apVK6hUKqxevbrA5yUSERERVWRvdOZrwoQJyMrKQlZWFgDgiy++QHJyMrp374633noLT548wYoVK4qlUCIiIqLy4I0+WLswKSkpiIyMhLa2Njp37gwLC4viXH2FwM92JCIqH/jZjhVLiXyw9u3bt1+rmJo1a77WchUVwxcRUfnA8FWxlMgHa9euXbvAB2sXBR+ySkRERJTvlcJXQEDAa4UvIiIiIspX7GO+6M3xsiMRUfnAy44VS1EvO77R3Y5ERERE9GoYvoiIiIgUxPBFREREpCCGLyIiIiIFMXwRERERKYjhi4iIiEhBDF9ERERECmL4IiIiIlIQwxcRERGRghi+iIiIiBTE8EVERESkIIYvIiIiIgUxfBEREREpiOGLiIiISEE6mi6AiIio3NLS1nQFVArxzBcRERGRghi+iIiIiBTE8EVERESkIIYvIiIiIgUxfBEREREpiOGLiIiISEEMX0REREQKYvgiIiIiUhDDFxEREZGCGL6IiIiIFMTwRURERKQghi8iIiIiBTF8ERERESmI4YuIiIhIQQxfRERERApi+CIiIiJSEMMXERERkYIYvoiIiIgUxPBFREREpCCGLyIiIiIFMXwRERERKYjhi4iIiEhBDF9ERERECmL4IiIiIlIQwxcRERGRghi+iIiIiBTE8EVERESkIIYvIiIiIgUxfBEREREpiOGLiIiISEEMX0REREQKYviiUmHyJDdcvxqFtCc3cPL4HrRr21LTJVEJ4vGuWHi8y751/j+gWZd+atOAD8YDAO7eTygw79l0IOIYACA55QkmfvQ5HF0+RKseA9Dr/ZH4YsV6pD19qsnd0phSE74kSXrhtHDhQgQFBT13fmJiIgBgx44d6NOnD6ysrGBqaopOnTrhwIEDRa5j4sSJ0NbWRnh4eKHzk5OT4eHhARsbG+jr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\n", 271 | "text/plain": [ 272 | "
" 273 | ] 274 | }, 275 | "metadata": {}, 276 | "output_type": "display_data" 277 | } 278 | ], 279 | "source": [ 280 | "import matplotlib.pyplot as plt\n", 281 | "import seaborn as sns\n", 282 | "\n", 283 | "m.load(best_path)\n", 284 | "_conf_mat = confusion_matrix(m, test_set)\n", 285 | "\n", 286 | "sns.reset_defaults()\n", 287 | "ax = sns.heatmap(_conf_mat, annot=True, fmt='d', cbar=False)\n", 288 | "ax.set_yticklabels(mstar.target_name_eoc_2_cv, rotation=0)\n", 289 | "ax.set_xticklabels(mstar.target_name_eoc_2, rotation=30)\n", 290 | "\n", 291 | "plt.xlabel('prediction', fontsize=12)\n", 292 | "plt.ylabel('label', fontsize=12)\n", 293 | "\n", 294 | "\n", 295 | "plt.show()" 296 | ] 297 | }, 298 | { 299 | "cell_type": "markdown", 300 | "metadata": {}, 301 | "source": [ 302 | "### Noise Simulation" 303 | ] 304 | }, 305 | { 306 | "cell_type": "code", 307 | "execution_count": 9, 308 | "metadata": {}, 309 | "outputs": [], 310 | "source": [ 311 | "from skimage import util\n", 312 | "\n", 313 | "\n", 314 | "def generate_noise(_images, amount):\n", 315 | " \n", 316 | " n, _, h, w = _images.shape\n", 317 | " \n", 318 | " noise = np.array([np.random.uniform(size=(1, h, w)) for _ in range(n)])\n", 319 | " portions = np.array([\n", 320 | " util.random_noise(np.zeros((1, h, w)), mode='s&p', amount=amount)\n", 321 | " for _ in range(n)\n", 322 | " ])\n", 323 | " noise = noise * portions\n", 324 | " \n", 325 | " return _images + noise.astype(np.float32)\n", 326 | "\n", 327 | "\n", 328 | "def noise_simulation(_m, ds, noise_ratio):\n", 329 | " \n", 330 | " num_data = 0\n", 331 | " corrects = 0\n", 332 | " \n", 333 | " _m.net.eval()\n", 334 | " _softmax = torch.nn.Softmax(dim=1)\n", 335 | " for i, data in enumerate(ds):\n", 336 | " images, labels, _ = data\n", 337 | " images = generate_noise(images, noise_ratio)\n", 338 | "\n", 339 | " predictions = _m.inference(images)\n", 340 | " predictions = _softmax(predictions)\n", 341 | "\n", 342 | " _, predictions = torch.max(predictions.data, 1)\n", 343 | " labels = labels.type(torch.LongTensor)\n", 344 | " num_data += labels.size(0)\n", 345 | " corrects += (predictions == labels.to(m.device)).sum().item()\n", 346 | "\n", 347 | " accuracy = 100 * corrects / num_data\n", 348 | " \n", 349 | " return accuracy" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 10, 355 | "metadata": {}, 356 | "outputs": [ 357 | { 358 | "name": "stdout", 359 | "output_type": "stream", 360 | "text": [ 361 | "ratio = 0.01, accuracy = 99.15\n", 362 | "ratio = 0.05, accuracy = 93.10\n", 363 | "ratio = 0.10, accuracy = 61.85\n", 364 | "ratio = 0.15, accuracy = 26.94\n" 365 | ] 366 | } 367 | ], 368 | "source": [ 369 | "noise_result = {}\n", 370 | "\n", 371 | "for ratio in [0.01, 0.05, 0.10, 0.15]:\n", 372 | " noise_result[ratio] = noise_simulation(m, test_set, ratio)\n", 373 | " print(f'ratio = {ratio:.2f}, accuracy = {noise_result[ratio]:.2f}')\n" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [] 382 | } 383 | ], 384 | "metadata": { 385 | "kernelspec": { 386 | "display_name": "Python 3 (ipykernel)", 387 | "language": "python", 388 | "name": "python3" 389 | }, 390 | "language_info": { 391 | "codemirror_mode": { 392 | "name": "ipython", 393 | "version": 3 394 | }, 395 | "file_extension": ".py", 396 | "mimetype": "text/x-python", 397 | "name": "python", 398 | "nbconvert_exporter": "python", 399 | "pygments_lexer": "ipython3", 400 | "version": "3.7.9" 401 | } 402 | }, 403 | "nbformat": 4, 404 | "nbformat_minor": 4 405 | } 406 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scikit-image==0.18.2 2 | numpy==1.22.0 3 | absl-py 4 | torch==1.9.0+cu111 5 | tqdm==4.61.2 6 | torchvision==0.10.0+cu111 7 | matplotlib 8 | scikit-learn 9 | seaborn 10 | Pillow -------------------------------------------------------------------------------- /run-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE= 4 | 5 | docker run --gpus all --rm -it -p 8888:8888 --mount type=bind,src=${WORKSPACE},dst=/workspace aconvnet-pytorch /bin/bash 6 | -------------------------------------------------------------------------------- /src/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/src/data/__init__.py -------------------------------------------------------------------------------- /src/data/generate_dataset.py: -------------------------------------------------------------------------------- 1 | from absl import logging 2 | from absl import flags 3 | from absl import app 4 | 5 | from multiprocessing import Pool 6 | from PIL import Image 7 | import numpy as np 8 | 9 | import json 10 | import glob 11 | import os 12 | 13 | import mstar 14 | 15 | flags.DEFINE_string('image_root', default='dataset', help='') 16 | flags.DEFINE_string('dataset', default='soc', help='') 17 | flags.DEFINE_boolean('is_train', default=False, help='') 18 | flags.DEFINE_integer('chip_size', default=100, help='') 19 | flags.DEFINE_integer('patch_size', default=94, help='') 20 | flags.DEFINE_boolean('use_phase', default=True, help='') 21 | 22 | FLAGS = flags.FLAGS 23 | 24 | project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 25 | 26 | 27 | def data_scaling(chip): 28 | r = chip.max() - chip.min() 29 | return (chip - chip.min()) / r 30 | 31 | 32 | def log_scale(chip): 33 | return np.log10(np.abs(chip) + 1) 34 | 35 | 36 | def generate(src_path, dst_path, is_train, chip_size, patch_size, use_phase, dataset): 37 | if not os.path.exists(src_path): 38 | return 39 | if not os.path.exists(dst_path): 40 | os.makedirs(dst_path, exist_ok=True) 41 | print(f'Target Name: {os.path.basename(dst_path)}') 42 | 43 | _mstar = mstar.MSTAR( 44 | name=dataset, is_train=is_train, chip_size=chip_size, patch_size=patch_size, use_phase=use_phase, stride=1 45 | ) 46 | 47 | image_list = glob.glob(os.path.join(src_path, '*')) 48 | 49 | for path in image_list: 50 | label, _images = _mstar.read(path) 51 | for i, _image in enumerate(_images): 52 | name = os.path.splitext(os.path.basename(path))[0] 53 | with open(os.path.join(dst_path, f'{name}-{i}.json'), mode='w', encoding='utf-8') as f: 54 | json.dump(label, f, ensure_ascii=False, indent=2) 55 | 56 | # _image = log_scale(_image) 57 | np.save(os.path.join(dst_path, f'{name}-{i}.npy'), _image) 58 | # Image.fromarray(data_scaling(_image)).convert('L').save(os.path.join(dst_path, f'{name}-{i}.bmp')) 59 | 60 | 61 | def main(_): 62 | dataset_root = os.path.join(project_root, FLAGS.image_root, FLAGS.dataset) 63 | raw_root = os.path.join(dataset_root, 'raw') 64 | 65 | mode = 'train' if FLAGS.is_train else 'test' 66 | 67 | output_root = os.path.join(dataset_root, mode) 68 | if not os.path.exists(output_root): 69 | os.makedirs(output_root, exist_ok=True) 70 | 71 | arguments = [ 72 | ( 73 | os.path.join(raw_root, mode, target), 74 | os.path.join(output_root, target), 75 | FLAGS.is_train, FLAGS.chip_size, FLAGS.patch_size, FLAGS.use_phase, FLAGS.dataset 76 | ) for target in mstar.target_name[FLAGS.dataset] 77 | ] 78 | 79 | with Pool(10) as p: 80 | p.starmap(generate, arguments) 81 | 82 | 83 | if __name__ == '__main__': 84 | app.run(main) 85 | -------------------------------------------------------------------------------- /src/data/loader.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from skimage import io 4 | import torch 5 | import tqdm 6 | 7 | import json 8 | import glob 9 | import os 10 | 11 | # import utils.common as common 12 | project_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 13 | 14 | 15 | class Dataset(torch.utils.data.Dataset): 16 | 17 | def __init__(self, path, name='soc', is_train=False, transform=None): 18 | self.is_train = is_train 19 | self.name = name 20 | 21 | self.images = [] 22 | self.labels = [] 23 | self.serial_number = [] 24 | 25 | self.transform = transform 26 | self._load_data(path) 27 | 28 | def __len__(self): 29 | return len(self.labels) 30 | 31 | def __getitem__(self, idx): 32 | if torch.is_tensor(idx): 33 | idx = idx.tolist() 34 | 35 | _image = self.images[idx] 36 | _label = self.labels[idx] 37 | _serial_number = self.serial_number[idx] 38 | 39 | if self.transform: 40 | _image = self.transform(_image) 41 | 42 | return _image, _label, _serial_number 43 | 44 | def _load_data(self, path): 45 | mode = 'train' if self.is_train else 'test' 46 | 47 | image_list = glob.glob(os.path.join(project_root, path, f'{self.name}/{mode}/*/*.npy')) 48 | label_list = glob.glob(os.path.join(project_root, path, f'{self.name}/{mode}/*/*.json')) 49 | image_list = sorted(image_list, key=os.path.basename) 50 | label_list = sorted(label_list, key=os.path.basename) 51 | 52 | for image_path, label_path in tqdm.tqdm(zip(image_list, label_list), desc=f'load {mode} data set'): 53 | self.images.append(np.load(image_path)) 54 | 55 | with open(label_path, mode='r', encoding='utf-8') as f: 56 | _label = json.load(f) 57 | 58 | self.labels.append(_label['class_id']) 59 | self.serial_number.append(_label['serial_number']) 60 | -------------------------------------------------------------------------------- /src/data/mstar.py: -------------------------------------------------------------------------------- 1 | from skimage.util import shape 2 | 3 | import numpy as np 4 | import tqdm 5 | 6 | import glob 7 | import os 8 | 9 | target_name_soc = ('2S1', 'BMP2', 'BRDM2', 'BTR60', 'BTR70', 'D7', 'T62', 'T72', 'ZIL131', 'ZSU234') 10 | target_name_eoc_1 = ('2S1', 'BRDM2', 'T72', 'ZSU234') 11 | 12 | target_name_eoc_2 = ('BMP2', 'BRDM2', 'BTR70', 'T72') 13 | target_name_eoc_2_cv = ('T72-A32', 'T72-A62', 'T72-A63', 'T72-A64', 'T72-S7') 14 | target_name_eoc_2_vv = ('BMP2-9566', 'BMP2-C21', 'T72-812', 'T72-A04', 'T72-A05', 'T72-A07', 'T72-A10') 15 | 16 | target_name_confuser_rejection = ('BMP2', 'BTR70', 'T72', '2S1', 'ZIL131') 17 | 18 | target_name = { 19 | 'soc': target_name_soc, 20 | 'eoc-1': target_name_eoc_1, 21 | 'eoc-1-t72-132': target_name_eoc_1, 22 | 'eoc-1-t72-a64': target_name_eoc_1, 23 | 'eoc-2-cv': target_name_eoc_2 + target_name_eoc_2_cv, 24 | 'eoc-2-vv': target_name_eoc_2 + target_name_eoc_2_vv, 25 | 'confuser-rejection': target_name_confuser_rejection 26 | } 27 | 28 | serial_number = { 29 | 'b01': 0, 30 | 31 | '9563': 1, 32 | '9566': 1, 33 | 'c21': 1, 34 | 35 | 'E-71': 2, 36 | 'k10yt7532': 3, 37 | 'c71': 4, 38 | '92v13015': 5, 39 | 'A51': 6, 40 | 41 | '132': 7, 42 | '812': 7, 43 | 's7': 7, 44 | 'A04': 7, 45 | 'A05': 7, 46 | 'A07': 7, 47 | 'A10': 7, 48 | 'A32': 7, 49 | 'A62': 7, 50 | 'A63': 7, 51 | 'A64': 7, 52 | 53 | 'E12': 8, 54 | 'd08': 9 55 | } 56 | 57 | 58 | class MSTAR(object): 59 | 60 | def __init__(self, name='soc', is_train=False, use_phase=False, chip_size=94, patch_size=88, stride=40): 61 | self.name = name 62 | self.is_train = is_train 63 | self.use_phase = use_phase 64 | self.chip_size = chip_size 65 | self.patch_size = patch_size 66 | self.stride = stride 67 | 68 | def read(self, path): 69 | f = open(path, 'rb') 70 | _header = self._parse_header(f) 71 | _data = np.fromfile(f, dtype='>f4') 72 | f.close() 73 | 74 | h = eval(_header['NumberOfRows']) 75 | w = eval(_header['NumberOfColumns']) 76 | 77 | _data = _data.reshape(-1, h, w) 78 | _data = _data.transpose(1, 2, 0) 79 | _data = _data.astype(np.float32) 80 | if not self.use_phase: 81 | _data = np.expand_dims(_data[:, :, 0], axis=2) 82 | 83 | # _data = self._normalize(_data) 84 | _data = self._center_crop(_data) 85 | 86 | if self.is_train: 87 | _data = self._data_augmentation(_data, patch_size=self.patch_size, stride=self.stride) 88 | else: 89 | _data = [self._center_crop(_data, size=self.patch_size)] 90 | 91 | meta_label = self._extract_meta_label(_header) 92 | return meta_label, _data 93 | 94 | @staticmethod 95 | def _parse_header(file): 96 | header = {} 97 | for line in file: 98 | line = line.decode('utf-8') 99 | line = line.strip() 100 | 101 | if not line: 102 | continue 103 | 104 | if 'PhoenixHeaderVer' in line: 105 | continue 106 | 107 | if 'EndofPhoenixHeader' in line: 108 | break 109 | 110 | key, value = line.split('=') 111 | header[key.strip()] = value.strip() 112 | 113 | return header 114 | 115 | @staticmethod 116 | def _center_crop(data, size=128): 117 | h, w, _ = data.shape 118 | 119 | y = (h - size) // 2 120 | x = (w - size) // 2 121 | 122 | return data[y: y + size, x: x + size] 123 | 124 | def _data_augmentation(self, data, patch_size=88, stride=40): 125 | # patch extraction 126 | _data = MSTAR._center_crop(data, size=self.chip_size) 127 | _, _, channels = _data.shape 128 | patches = shape.view_as_windows(_data, window_shape=(patch_size, patch_size, channels), step=stride) 129 | patches = patches.reshape(-1, patch_size, patch_size, channels) 130 | return patches 131 | 132 | def _extract_meta_label(self, header): 133 | 134 | target_type = header['TargetType'] 135 | sn = header['TargetSerNum'] 136 | 137 | class_id = serial_number[sn] 138 | if not self.name == 'soc': 139 | class_id = target_name[self.name].index(target_name_soc[class_id]) 140 | 141 | azimuth_angle = MSTAR._get_azimuth_angle(header['TargetAz']) 142 | 143 | return { 144 | 'class_id': class_id, 145 | 'target_type': target_type, 146 | 'serial_number': sn, 147 | 'azimuth_angle': azimuth_angle 148 | } 149 | 150 | @staticmethod 151 | def _get_azimuth_angle(angle): 152 | azimuth_angle = eval(angle) 153 | if azimuth_angle > 180: 154 | azimuth_angle -= 180 155 | return int(azimuth_angle) 156 | 157 | @staticmethod 158 | def _normalize(x): 159 | d = (x - x.min()) / (x.max() - x.min()) 160 | return d.astype(np.float32) 161 | -------------------------------------------------------------------------------- /src/data/preprocess.py: -------------------------------------------------------------------------------- 1 | from skimage import transform 2 | import numpy as np 3 | 4 | 5 | class ToTensor(object): 6 | 7 | def __init__(self): 8 | pass 9 | 10 | def __call__(self, sample): 11 | _input = sample 12 | 13 | if len(_input.shape) < 3: 14 | _input = np.expand_dims(_input, axis=2) 15 | 16 | _input = _input.transpose((2, 0, 1)) 17 | 18 | return _input 19 | 20 | 21 | class RandomCrop(object): 22 | 23 | def __init__(self, size): 24 | if isinstance(size, int): 25 | self.size = (size, size) 26 | else: 27 | assert len(size) == 2 28 | self.size = size 29 | 30 | def __call__(self, sample): 31 | _input = sample 32 | 33 | if len(_input.shape) < 3: 34 | _input = np.expand_dims(_input, axis=2) 35 | 36 | h, w, _ = _input.shape 37 | oh, ow = self.size 38 | 39 | dh = h - oh 40 | dw = w - ow 41 | y = np.random.randint(0, dh) if dh > 0 else 0 42 | x = np.random.randint(0, dw) if dw > 0 else 0 43 | oh = oh if dh > 0 else h 44 | ow = ow if dw > 0 else w 45 | 46 | return _input[y: y + oh, x: x + ow, :] 47 | 48 | 49 | class CenterCrop(object): 50 | 51 | def __init__(self, size): 52 | if isinstance(size, int): 53 | self.size = (size, size) 54 | else: 55 | assert len(size) == 2 56 | self.size = size 57 | 58 | def __call__(self, sample): 59 | _input = sample 60 | 61 | if len(_input.shape) < 3: 62 | _input = np.expand_dims(_input, axis=2) 63 | 64 | h, w, _ = _input.shape 65 | oh, ow = self.size 66 | y = (h - oh) // 2 67 | x = (w - ow) // 2 68 | 69 | return _input[y: y + oh, x: x + ow, :] 70 | -------------------------------------------------------------------------------- /src/model/__init__.py: -------------------------------------------------------------------------------- 1 | from ._base import * 2 | -------------------------------------------------------------------------------- /src/model/_base.py: -------------------------------------------------------------------------------- 1 | import torch 2 | 3 | import model.network 4 | 5 | 6 | class Model(object): 7 | def __init__(self, **params): 8 | self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') 9 | self.net = model.network.Network( 10 | classes=params.get('classes', 10), 11 | channels=params.get('channels', 1), 12 | dropout_rate=params.get('dropout_rate', 0.5) 13 | ) 14 | self.net.to(self.device) 15 | 16 | self.lr = params.get('lr', 1e-3) 17 | self.lr_step = params.get('lr_step', [50]) 18 | self.lr_decay = params.get('lr_decay', 0.1) 19 | 20 | self.lr_scheduler = None 21 | 22 | self.momentum = params.get('momentum', 0.9) 23 | self.weight_decay = params.get('weight_decay', 4e-3) 24 | 25 | self.criterion = torch.nn.CrossEntropyLoss() 26 | self.optimizer = torch.optim.SGD( 27 | self.net.parameters(), 28 | lr=self.lr, 29 | momentum=self.momentum, 30 | weight_decay=self.weight_decay 31 | ) 32 | 33 | if self.lr_decay: 34 | self.lr_scheduler = torch.optim.lr_scheduler.MultiStepLR( 35 | optimizer=self.optimizer, 36 | milestones=self.lr_step, 37 | gamma=self.lr_decay 38 | ) 39 | 40 | def optimize(self, x, y): 41 | p = self.net(x.to(self.device)) 42 | loss = self.criterion(p, y.to(self.device)) 43 | 44 | self.optimizer.zero_grad() 45 | loss.backward() 46 | self.optimizer.step() 47 | 48 | return loss.item() 49 | 50 | @torch.no_grad() 51 | def inference(self, x): 52 | return self.net(x.to(self.device)) 53 | 54 | def save(self, path): 55 | torch.save(self.net.state_dict(), path) 56 | 57 | def load(self, path): 58 | self.net.load_state_dict(torch.load(path)) 59 | self.net.eval() 60 | -------------------------------------------------------------------------------- /src/model/_blocks.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | 3 | import collections 4 | 5 | _activations = { 6 | 'relu': nn.ReLU, 7 | 'relu6': nn.ReLU6, 8 | 'leaky_relu': nn.LeakyReLU 9 | } 10 | 11 | 12 | class BaseBlock(nn.Module): 13 | 14 | def __init__(self): 15 | super(BaseBlock, self).__init__() 16 | self._layer: nn.Sequential 17 | 18 | def forward(self, x): 19 | return self._layer(x) 20 | 21 | 22 | class DenseBlock(BaseBlock): 23 | 24 | def __init__(self, shape, **params): 25 | super(DenseBlock, self).__init__() 26 | in_dims, out_dims = shape 27 | _seq = collections.OrderedDict([ 28 | ('dense', nn.Linear(in_dims, out_dims)), 29 | ]) 30 | _act_name = params.get('activation') 31 | if _act_name: 32 | _seq.update({_act_name: _activations[_act_name](inplace=True)}) 33 | 34 | self._layer = nn.Sequential(_seq) 35 | 36 | w_init = params.get('w_init', None) 37 | idx = list(dict(self._layer.named_children()).keys()).index('dense') 38 | if w_init: 39 | w_init(self._layer[idx].weight) 40 | b_init = params.get('b_init', None) 41 | if b_init: 42 | b_init(self._layer[idx].bias) 43 | 44 | 45 | class Conv2DBlock(BaseBlock): 46 | 47 | def __init__(self, shape, stride, padding='same', **params): 48 | super(Conv2DBlock, self).__init__() 49 | 50 | h, w, in_channels, out_channels = shape 51 | _seq = collections.OrderedDict([ 52 | ('conv', nn.Conv2d(in_channels, out_channels, kernel_size=(h, w), stride=stride, padding=padding)) 53 | ]) 54 | 55 | _bn = params.get('batch_norm') 56 | if _bn: 57 | _seq.update({'bn': nn.BatchNorm2d(out_channels)}) 58 | 59 | _act_name = params.get('activation') 60 | if _act_name: 61 | _seq.update({_act_name: _activations[_act_name](inplace=True)}) 62 | 63 | _max_pool = params.get('max_pool') 64 | if _max_pool: 65 | _kernel_size = params.get('max_pool_size', 2) 66 | _stride = params.get('max_pool_stride', _kernel_size) 67 | _seq.update({'max_pool': nn.MaxPool2d(kernel_size=_kernel_size, stride=_stride)}) 68 | 69 | self._layer = nn.Sequential(_seq) 70 | 71 | w_init = params.get('w_init', None) 72 | idx = list(dict(self._layer.named_children()).keys()).index('conv') 73 | if w_init: 74 | w_init(self._layer[idx].weight) 75 | b_init = params.get('b_init', None) 76 | if b_init: 77 | b_init(self._layer[idx].bias) 78 | -------------------------------------------------------------------------------- /src/model/network.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | import torch 3 | 4 | from . import _blocks 5 | 6 | 7 | class Network(nn.Module): 8 | 9 | def __init__(self, **params): 10 | super(Network, self).__init__() 11 | self.dropout_rate = params.get('dropout_rate', 0.5) 12 | self.classes = params.get('classes', 10) 13 | self.channels = params.get('channels', 1) 14 | 15 | _w_init = params.get('w_init', lambda x: nn.init.kaiming_normal_(x, nonlinearity='relu')) 16 | _b_init = params.get('b_init', lambda x: nn.init.constant_(x, 0.1)) 17 | 18 | self._layer = nn.Sequential( 19 | _blocks.Conv2DBlock( 20 | shape=[5, 5, self.channels, 16], stride=1, padding='valid', activation='relu', max_pool=True, 21 | w_init=_w_init, b_init=_b_init 22 | ), 23 | _blocks.Conv2DBlock( 24 | shape=[5, 5, 16, 32], stride=1, padding='valid', activation='relu', max_pool=True, 25 | w_init=_w_init, b_init=_b_init 26 | ), 27 | _blocks.Conv2DBlock( 28 | shape=[6, 6, 32, 64], stride=1, padding='valid', activation='relu', max_pool=True, 29 | w_init=_w_init, b_init=_b_init 30 | ), 31 | _blocks.Conv2DBlock( 32 | shape=[5, 5, 64, 128], stride=1, padding='valid', activation='relu', 33 | w_init=_w_init, b_init=_b_init 34 | ), 35 | nn.Dropout(p=self.dropout_rate), 36 | _blocks.Conv2DBlock( 37 | shape=[3, 3, 128, self.classes], stride=1, padding='valid', 38 | w_init=_w_init, b_init=nn.init.zeros_ 39 | ), 40 | nn.Flatten() 41 | ) 42 | 43 | def forward(self, x): 44 | return self._layer(x) 45 | -------------------------------------------------------------------------------- /src/train.py: -------------------------------------------------------------------------------- 1 | from absl import logging 2 | from absl import flags 3 | from absl import app 4 | 5 | from tqdm import tqdm 6 | 7 | from torch.utils import tensorboard 8 | 9 | import torchvision 10 | import torch 11 | 12 | import numpy as np 13 | 14 | import json 15 | import os 16 | 17 | from data import preprocess 18 | from data import loader 19 | from utils import common 20 | import model 21 | 22 | flags.DEFINE_string('experiments_path', os.path.join(common.project_root, 'experiments'), help='') 23 | flags.DEFINE_string('config_name', 'config/AConvNet-SOC.json', help='') 24 | FLAGS = flags.FLAGS 25 | 26 | 27 | common.set_random_seed(12321) 28 | 29 | 30 | def load_dataset(path, is_train, name, batch_size): 31 | transform = [preprocess.CenterCrop(88), torchvision.transforms.ToTensor()] 32 | if is_train: 33 | transform = [preprocess.RandomCrop(88), torchvision.transforms.ToTensor()] 34 | _dataset = loader.Dataset( 35 | path, name=name, is_train=is_train, 36 | transform=torchvision.transforms.Compose(transform) 37 | ) 38 | data_loader = torch.utils.data.DataLoader( 39 | _dataset, batch_size=batch_size, shuffle=is_train, num_workers=1 40 | ) 41 | return data_loader 42 | 43 | 44 | @torch.no_grad() 45 | def validation(m, ds): 46 | num_data = 0 47 | corrects = 0 48 | 49 | # Test loop 50 | m.net.eval() 51 | _softmax = torch.nn.Softmax(dim=1) 52 | for i, data in enumerate(tqdm(ds)): 53 | images, labels, _ = data 54 | 55 | predictions = m.inference(images) 56 | predictions = _softmax(predictions) 57 | 58 | _, predictions = torch.max(predictions.data, 1) 59 | labels = labels.type(torch.LongTensor) 60 | num_data += labels.size(0) 61 | corrects += (predictions == labels.to(m.device)).sum().item() 62 | 63 | accuracy = 100 * corrects / num_data 64 | return accuracy 65 | 66 | 67 | def run(epochs, dataset, classes, channels, batch_size, 68 | lr, lr_step, lr_decay, weight_decay, dropout_rate, 69 | model_name, experiments_path=None): 70 | train_set = load_dataset('dataset', True, dataset, batch_size) 71 | valid_set = load_dataset('dataset', False, dataset, batch_size) 72 | 73 | m = model.Model( 74 | classes=classes, dropout_rate=dropout_rate, channels=channels, 75 | lr=lr, lr_step=lr_step, lr_decay=lr_decay, 76 | weight_decay=weight_decay 77 | ) 78 | 79 | model_path = os.path.join(experiments_path, f'model/{model_name}') 80 | if not os.path.exists(model_path): 81 | os.makedirs(model_path, exist_ok=True) 82 | 83 | history_path = os.path.join(experiments_path, 'history') 84 | if not os.path.exists(history_path): 85 | os.makedirs(history_path, exist_ok=True) 86 | 87 | history = { 88 | 'loss': [], 89 | 'accuracy': [] 90 | } 91 | 92 | for epoch in range(epochs): 93 | _loss = [] 94 | 95 | m.net.train() 96 | for i, data in enumerate(tqdm(train_set)): 97 | images, labels, _ = data 98 | _loss.append(m.optimize(images, labels)) 99 | 100 | if m.lr_scheduler: 101 | lr = m.lr_scheduler.get_last_lr()[0] 102 | m.lr_scheduler.step() 103 | 104 | accuracy = validation(m, valid_set) 105 | 106 | logging.info( 107 | f'Epoch: {epoch + 1:03d}/{epochs:03d} | loss={np.mean(_loss):.4f} | lr={lr} | accuracy={accuracy:.2f}' 108 | ) 109 | 110 | history['loss'].append(np.mean(_loss)) 111 | history['accuracy'].append(accuracy) 112 | 113 | if experiments_path: 114 | m.save(os.path.join(model_path, f'model-{epoch + 1:03d}.pth')) 115 | 116 | with open(os.path.join(history_path, f'history-{model_name}.json'), mode='w', encoding='utf-8') as f: 117 | json.dump(history, f, ensure_ascii=True, indent=2) 118 | 119 | 120 | def main(_): 121 | logging.info('Start') 122 | experiments_path = FLAGS.experiments_path 123 | config_name = FLAGS.config_name 124 | 125 | config = common.load_config(os.path.join(experiments_path, config_name)) 126 | 127 | dataset = config['dataset'] 128 | classes = config['num_classes'] 129 | channels = config['channels'] 130 | epochs = config['epochs'] 131 | batch_size = config['batch_size'] 132 | 133 | lr = config['lr'] 134 | lr_step = config['lr_step'] 135 | lr_decay = config['lr_decay'] 136 | 137 | weight_decay = config['weight_decay'] 138 | dropout_rate = config['dropout_rate'] 139 | 140 | model_name = config['model_name'] 141 | 142 | run(epochs, dataset, classes, channels, batch_size, 143 | lr, lr_step, lr_decay, weight_decay, dropout_rate, 144 | model_name, experiments_path) 145 | 146 | logging.info('Finish') 147 | 148 | 149 | if __name__ == '__main__': 150 | app.run(main) 151 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jangsoopark/AConvNet-pytorch/c60740d40407f68c9df71a8c0b871601d5ba849d/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/common.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | 4 | import random 5 | import json 6 | import os 7 | 8 | project_root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) 9 | 10 | 11 | def set_random_seed(random_seed): 12 | torch.manual_seed(random_seed) 13 | torch.cuda.manual_seed(random_seed) 14 | torch.cuda.manual_seed_all(random_seed) # if use multi-GPU 15 | 16 | torch.backends.cudnn.deterministic = True 17 | torch.backends.cudnn.benchmark = False 18 | 19 | np.random.seed(random_seed) 20 | random.seed(random_seed) 21 | 22 | 23 | def load_config(path): 24 | with open(path, mode='r', encoding='utf-8') as f: 25 | return json.load(f) 26 | --------------------------------------------------------------------------------