update
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
check_points/
|
||||
pretrain_models*
|
||||
test_dir_enhance_results/
|
||||
test_dir_align_results/
|
||||
test_unalign_results/
|
||||
tmp*
|
||||
local*
|
||||
*.pth
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
.venv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
@@ -0,0 +1,445 @@
|
||||
PSFR-GAN (c) by Chaofeng Chen
|
||||
|
||||
PSFR-GAN is licensed under a
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
|
||||
|
||||
You should have received a copy of the license along with this
|
||||
work. If not, see <http://creativecommons.org/licenses/by-nc-sa/4.0/>.
|
||||
|
||||
Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More_considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
||||
Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-NonCommercial-ShareAlike 4.0 International Public License
|
||||
("Public License"). To the extent this Public License may be
|
||||
interpreted as a contract, You are granted the Licensed Rights in
|
||||
consideration of Your acceptance of these terms and conditions, and the
|
||||
Licensor grants You such rights in consideration of benefits the
|
||||
Licensor receives from making the Licensed Material available under
|
||||
these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. BY-NC-SA Compatible License means a license listed at
|
||||
creativecommons.org/compatiblelicenses, approved by Creative
|
||||
Commons as essentially the equivalent of this Public License.
|
||||
|
||||
d. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
e. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
f. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
g. License Elements means the license attributes listed in the name
|
||||
of a Creative Commons Public License. The License Elements of this
|
||||
Public License are Attribution, NonCommercial, and ShareAlike.
|
||||
|
||||
h. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
i. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
j. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
k. NonCommercial means not primarily intended for or directed towards
|
||||
commercial advantage or monetary compensation. For purposes of
|
||||
this Public License, the exchange of the Licensed Material for
|
||||
other material subject to Copyright and Similar Rights by digital
|
||||
file-sharing or similar means is NonCommercial provided there is
|
||||
no payment of monetary compensation in connection with the
|
||||
exchange.
|
||||
|
||||
l. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
m. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
n. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part, for NonCommercial purposes only; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material for
|
||||
NonCommercial purposes only.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor -- Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You apply.
|
||||
|
||||
c. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties, including when
|
||||
the Licensed Material is used other than for NonCommercial
|
||||
purposes.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-NC-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to, the
|
||||
Adapter's License You apply. You may satisfy this condition
|
||||
in any reasonable manner based on the medium, means, and
|
||||
context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database for NonCommercial purposes
|
||||
only;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material,
|
||||
including for purposes of Section 3(b); and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
@@ -0,0 +1,123 @@
|
||||
# PSFR-GAN in PyTorch
|
||||
|
||||
[Progressive Semantic-Aware Style Transformation for Blind Face Restoration](https://arxiv.org/abs/2009.08709)
|
||||
[Chaofeng Chen](https://chaofengc.github.io), [Xiaoming Li](https://csxmli2016.github.io/), [Lingbo Yang](https://lotayou.github.io), [Xianhui Lin](https://dblp.org/pid/147/7708.html), [Lei Zhang](https://www4.comp.polyu.edu.hk/~cslzhang/), [Kwan-Yee K. Wong](https://i.cs.hku.hk/~kykwong/)
|
||||
|
||||

|
||||

|
||||
|
||||
### Changelog
|
||||
- **2021.04.26**: Add pytorch vgg19 model to GoogleDrive and remove `--distributed` option which causes training error.
|
||||
- **2021.03.22**: Update new model at 15 epoch (52.5k iterations).
|
||||
- **2021.03.19**: Add train codes for PSFRGAN and FPN.
|
||||
|
||||
## Prerequisites and Installation
|
||||
- Ubuntu 18.04
|
||||
- CUDA 10.1
|
||||
- Clone this repository
|
||||
```
|
||||
git clone https://github.com/chaofengc/PSFR-GAN.git
|
||||
cd PSFR-GAN
|
||||
```
|
||||
- Python 3.7, install required packages by `pip3 install -r requirements.txt`
|
||||
|
||||
## Quick Test
|
||||
|
||||
### Download Pretrain Models and Dataset
|
||||
Download the pretrained models from the following link and put them to `./pretrain_models`
|
||||
- [GoogleDrive](https://drive.google.com/drive/folders/1Ubejhxd2xd4fxGc_M_LWl3Ux6CgQd9rP?usp=sharing)
|
||||
- [BaiduNetDisk](https://pan.baidu.com/s/1cru3uUASEfGX6p6L0_7gWQ), extract code: `gj2r`
|
||||
|
||||
### Test single image
|
||||
Run the following script to enhance face(s) in single input
|
||||
```
|
||||
python test_enhance_single_unalign.py --test_img_path ./test_dir/test_hzgg.jpg --results_dir test_hzgg_results --gpus 1
|
||||
```
|
||||
|
||||
This script do the following things:
|
||||
- Crop and align all the faces from input image, stored at `results_dir/LQ_faces`
|
||||
- Parse these faces and then enhance them, results stored at `results_dir/ParseMaps` and `results_dir/HQ`
|
||||
- Paste then enhanced faces back to the original image `results_dir/hq_final.jpg`
|
||||
- You can use `--gpus` to specify how many GPUs to use, `<=0` means running on CPU. The program will use GPU with the most available memory. Set `CUDA_VISIBLE_DEVICE` to specify the GPU if you do not want automatic GPU selection.
|
||||
|
||||
### Test image folder
|
||||
To test multiple images, we first crop out all the faces and align them use the following script.
|
||||
```
|
||||
python align_and_crop_dir.py --src_dir test_dir --results_dir test_dir_align_results
|
||||
```
|
||||
|
||||
For images (*e.g.* `multiface_test.jpg`) contain multiple faces, the aligned faces will be stored as `multiface_test_{face_index}.jpg`
|
||||
And then parse the aligned faces and enhance them with
|
||||
```
|
||||
python test_enhance_dir_align.py --src_dir test_dir_align_results --results_dir test_dir_enhance_results
|
||||
```
|
||||
Results will be saved to three folders respectively: `results_dir/lq`, `results_dir/parse`, `results_dir/hq`.
|
||||
|
||||
### Additional test script
|
||||
|
||||
For your convenience, we also provide script to test multiple unaligned images and paste the enhance results back. **Note the paste back operation could be quite slow for large size images containing many faces (dlib takes time to detect faces in large image).**
|
||||
```
|
||||
python test_enhance_dir_unalign.py --src_dir test_dir --results_dir test_unalign_results
|
||||
```
|
||||
This script basically do the same thing as `test_enhance_single_unalign.py` for each image in `src_dir`
|
||||
|
||||
## Train the Model
|
||||
|
||||
### Data Preparation
|
||||
|
||||
- Download [FFHQ](https://github.com/NVlabs/ffhq-dataset) and put the images to `../datasets/FFHQ/imgs1024`
|
||||
- Download parsing masks (`512x512`) [HERE](https://drive.google.com/file/d/1eQwO8hKcaluyCnxuZAp0eJVOdgMi30uA/view?usp=sharing) generated by the pretrained FPN and put them to `../datasets/FFHQ/masks512`.
|
||||
|
||||
*Note: you may change `../datasets/FFHQ` to your own path. But images and masks must be stored under `your_own_path/imgs1024` and `your_own_path/masks512` respectively.*
|
||||
|
||||
### Train Script for PSFRGAN
|
||||
|
||||
Here is an example train script for PSFRGAN:
|
||||
|
||||
```
|
||||
python train.py --gpus 2 --model enhance --name PSFRGAN_v001 \
|
||||
--g_lr 0.0001 --d_lr 0.0004 --beta1 0.5 \
|
||||
--gan_mode 'hinge' --lambda_pix 10 --lambda_fm 10 --lambda_ss 1000 \
|
||||
--Dinput_nc 22 --D_num 3 --n_layers_D 4 \
|
||||
--batch_size 2 --dataset ffhq --dataroot ../datasets/FFHQ \
|
||||
--visual_freq 100 --print_freq 10 #--continue_train
|
||||
```
|
||||
- Please change the `--name` option for different experiments. Tensorboard records with the same name will be moved to `check_points/log_archive`, and the weight directory will only store weight history of latest experiment with the same name.
|
||||
- `--gpus` specify number of GPUs used to train. The script will use GPUs with more available memory first. To specify the GPU index, use `export CUDA_VISIBLE_DEVICES=your_gpu_ids` before the script.
|
||||
- Uncomment `--continue_train` to resume training. *Current codes do not resume the optimizer state.*
|
||||
- It needs at least **8GB** memory to train with **batch_size=1**.
|
||||
|
||||
### Scripts for FPN
|
||||
|
||||
You may also train your own FPN and generate masks for the HQ images by yourself with the following steps:
|
||||
|
||||
- Download [CelebAHQ-Mask](https://github.com/switchablenorms/CelebAMask-HQ) dataset. Generate `CelebAMask-HQ-mask` and `CelebAMask-HQ-mask-color` with the provided scripts in `CelebAMask-HQ/face_parsing/Data_preprocessing/`.
|
||||
- Train FPN with the following commmand
|
||||
```
|
||||
python train.py --gpus 1 --model parse --name FPN_v001 \
|
||||
--lr 0.0002 --batch_size 8 \
|
||||
--dataset celebahqmask --dataroot ../datasets/CelebAMask-HQ \
|
||||
--visual_freq 100 --print_freq 10 #--continue_train
|
||||
```
|
||||
- Generate parsing masks with your own FPN using the following command:
|
||||
```
|
||||
python generate_masks.py --save_masks_dir ../datasets/FFHQ/masks512 --batch_size 8 --parse_net_weight path/to/your/own/FPN
|
||||
```
|
||||
|
||||
## Citation
|
||||
```
|
||||
@inproceedings{ChenPSFRGAN,
|
||||
author = {Chen, Chaofeng and Li, Xiaoming and Lingbo, Yang and Lin, Xianhui and Zhang, Lei and Wong, KKY},
|
||||
title = {Progressive Semantic-Aware Style Transformation for Blind Face Restoration},
|
||||
Journal = {IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
|
||||
year = {2021}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</a>.
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
This work is inspired by [SPADE](https://github.com/NVlabs/SPADE), and closed related to [DFDNet](https://github.com/csxmli2016/DFDNet) and [HiFaceGAN](https://github.com/Lotayou/Face-Renovation). Our codes largely benefit from [CycleGAN](https://github.com/junyanz/pytorch-CycleGAN-and-pix2pix).
|
||||
@@ -0,0 +1,86 @@
|
||||
import dlib
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
from skimage import transform as trans
|
||||
from skimage import io
|
||||
import argparse
|
||||
|
||||
|
||||
def get_points(img, detector, shape_predictor, size_threshold=999):
|
||||
dets = detector(img, 1)
|
||||
if len(dets) == 0:
|
||||
return None
|
||||
|
||||
all_points = []
|
||||
for det in dets:
|
||||
if isinstance(detector, dlib.cnn_face_detection_model_v1):
|
||||
rec = det.rect # for cnn detector
|
||||
else:
|
||||
rec = det
|
||||
if rec.width() > size_threshold or rec.height() > size_threshold:
|
||||
break
|
||||
shape = shape_predictor(img, rec)
|
||||
single_points = []
|
||||
for i in range(5):
|
||||
single_points.append([shape.part(i).x, shape.part(i).y])
|
||||
all_points.append(np.array(single_points))
|
||||
if len(all_points) <= 0:
|
||||
return None
|
||||
else:
|
||||
return all_points
|
||||
|
||||
def align_and_save(img, save_path, src_points, template_path, template_scale=1):
|
||||
out_size = (512, 512)
|
||||
reference = np.load(template_path) / template_scale
|
||||
|
||||
ext = os.path.splitext(save_path)
|
||||
for idx, spoint in enumerate(src_points):
|
||||
tform = trans.SimilarityTransform()
|
||||
tform.estimate(spoint, reference)
|
||||
M = tform.params[0:2,:]
|
||||
|
||||
crop_img = cv2.warpAffine(img, M, out_size)
|
||||
if len(src_points) > 1:
|
||||
save_path = ext[0] + '_{}'.format(idx) + ext[1]
|
||||
dlib.save_image(crop_img.astype(np.uint8), save_path)
|
||||
print('Saving image', save_path)
|
||||
|
||||
def align_and_save_dir(src_dir, save_dir, template_path='./pretrain_models/FFHQ_template.npy', template_scale=2, use_cnn_detector=True):
|
||||
out_size = (512, 512)
|
||||
if use_cnn_detector:
|
||||
detector = dlib.cnn_face_detection_model_v1('./pretrain_models/mmod_human_face_detector.dat')
|
||||
else:
|
||||
detector = dlib.get_frontal_face_detector()
|
||||
sp = dlib.shape_predictor('./pretrain_models/shape_predictor_5_face_landmarks.dat')
|
||||
|
||||
for name in os.listdir(src_dir):
|
||||
img_path = os.path.join(src_dir, name)
|
||||
img = dlib.load_rgb_image(img_path)
|
||||
|
||||
points = get_points(img, detector, sp)
|
||||
if points is not None:
|
||||
save_path = os.path.join(save_dir, name)
|
||||
align_and_save(img, save_path, points, template_path, template_scale)
|
||||
else:
|
||||
print('No face detected in', img_path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--src_dir', type=str, help='source directory containing images to crop and align.')
|
||||
parser.add_argument('--results_dir', type=str, help='results directory to save the aligned faces.')
|
||||
parser.add_argument('--not_use_cnn_detector', action='store_true', help='do not use cnn face detector in dlib.')
|
||||
args = parser.parse_args()
|
||||
|
||||
src_dir = args.src_dir
|
||||
assert os.path.isdir(src_dir), 'Source path should be a directory containing images'
|
||||
save_dir = args.results_dir
|
||||
if not os.path.exists(save_dir): os.makedirs(save_dir, exist_ok=True)
|
||||
align_and_save_dir(src_dir, save_dir, use_cnn_detector=not args.not_use_cnn_detector)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""This package includes all the modules related to data loading and preprocessing
|
||||
|
||||
To add a custom dataset class called 'dummy', you need to add a file called 'dummy_dataset.py' and define a subclass 'DummyDataset' inherited from BaseDataset.
|
||||
You need to implement four functions:
|
||||
-- <__init__>: initialize the class, first call BaseDataset.__init__(self, opt).
|
||||
-- <__len__>: return the size of dataset.
|
||||
-- <__getitem__>: get a data point from data loader.
|
||||
-- <modify_commandline_options>: (optionally) add dataset-specific options and set default options.
|
||||
|
||||
Now you can use the dataset class by specifying flag '--dataset_mode dummy'.
|
||||
See our template dataset class 'template_dataset.py' for more details.
|
||||
"""
|
||||
import importlib
|
||||
import torch.utils.data
|
||||
from data.base_dataset import BaseDataset
|
||||
|
||||
|
||||
def find_dataset_using_name(dataset_name):
|
||||
"""Import the module "data/[dataset_name]_dataset.py".
|
||||
|
||||
In the file, the class called DatasetNameDataset() will
|
||||
be instantiated. It has to be a subclass of BaseDataset,
|
||||
and it is case-insensitive.
|
||||
"""
|
||||
dataset_filename = "data." + dataset_name + "_dataset"
|
||||
datasetlib = importlib.import_module(dataset_filename)
|
||||
|
||||
dataset = None
|
||||
target_dataset_name = dataset_name.replace('_', '') + 'dataset'
|
||||
for name, cls in datasetlib.__dict__.items():
|
||||
if name.lower() == target_dataset_name.lower() \
|
||||
and issubclass(cls, BaseDataset):
|
||||
dataset = cls
|
||||
|
||||
if dataset is None:
|
||||
raise NotImplementedError("In %s.py, there should be a subclass of BaseDataset with class name that matches %s in lowercase." % (dataset_filename, target_dataset_name))
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
def get_option_setter(dataset_name):
|
||||
"""Return the static method <modify_commandline_options> of the dataset class."""
|
||||
dataset_class = find_dataset_using_name(dataset_name)
|
||||
return dataset_class.modify_commandline_options
|
||||
|
||||
|
||||
def create_dataset(opt):
|
||||
"""Create a dataset given the option.
|
||||
|
||||
This function wraps the class CustomDatasetDataLoader.
|
||||
This is the main interface between this package and 'train.py'/'test.py'
|
||||
|
||||
Example:
|
||||
>>> from data import create_dataset
|
||||
>>> dataset = create_dataset(opt)
|
||||
"""
|
||||
data_loader = CustomDatasetDataLoader(opt)
|
||||
dataset = data_loader.load_data()
|
||||
return dataset
|
||||
|
||||
|
||||
class CustomDatasetDataLoader():
|
||||
"""Wrapper class of Dataset class that performs multi-threaded data loading"""
|
||||
|
||||
def __init__(self, opt):
|
||||
"""Initialize this class
|
||||
|
||||
Step 1: create a dataset instance given the name [dataset_mode]
|
||||
Step 2: create a multi-threaded data loader.
|
||||
"""
|
||||
self.opt = opt
|
||||
dataset_class = find_dataset_using_name(opt.dataset_name)
|
||||
self.dataset = dataset_class(opt)
|
||||
print("dataset [%s] was created" % type(self.dataset).__name__)
|
||||
drop_last = True if opt.isTrain else False
|
||||
self.dataloader = torch.utils.data.DataLoader(
|
||||
self.dataset,
|
||||
batch_size=opt.batch_size,
|
||||
shuffle=not opt.serial_batches,
|
||||
num_workers=int(opt.num_threads), drop_last=drop_last)
|
||||
|
||||
def load_data(self):
|
||||
return self
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of data in the dataset"""
|
||||
return min(len(self.dataset), self.opt.max_dataset_size)
|
||||
|
||||
def __iter__(self):
|
||||
"""Return a batch of data"""
|
||||
for i, data in enumerate(self.dataloader):
|
||||
if i * self.opt.batch_size >= self.opt.max_dataset_size:
|
||||
break
|
||||
yield data
|
||||
@@ -0,0 +1,162 @@
|
||||
"""This module implements an abstract base class (ABC) 'BaseDataset' for datasets.
|
||||
|
||||
It also includes common transformation functions (e.g., get_transform, __scale_width), which can be later used in subclasses.
|
||||
"""
|
||||
import random
|
||||
import numpy as np
|
||||
import torch.utils.data as data
|
||||
from PIL import Image
|
||||
import torchvision.transforms as transforms
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import imgaug as ia
|
||||
import imgaug.augmenters as iaa
|
||||
|
||||
class BaseDataset(data.Dataset, ABC):
|
||||
"""This class is an abstract base class (ABC) for datasets.
|
||||
|
||||
To create a subclass, you need to implement the following four functions:
|
||||
-- <__init__>: initialize the class, first call BaseDataset.__init__(self, opt).
|
||||
-- <__len__>: return the size of dataset.
|
||||
-- <__getitem__>: get a data point.
|
||||
-- <modify_commandline_options>: (optionally) add dataset-specific options and set default options.
|
||||
"""
|
||||
|
||||
def __init__(self, opt):
|
||||
"""Initialize the class; save the options in the class
|
||||
|
||||
Parameters:
|
||||
opt (Option class)-- stores all the experiment flags; needs to be a subclass of BaseOptions
|
||||
"""
|
||||
self.opt = opt
|
||||
self.root = opt.dataroot
|
||||
|
||||
@staticmethod
|
||||
def modify_commandline_options(parser, is_train):
|
||||
"""Add new dataset-specific options, and rewrite default values for existing options.
|
||||
|
||||
Parameters:
|
||||
parser -- original option parser
|
||||
is_train (bool) -- whether training phase or test phase. You can use this flag to add training-specific or test-specific options.
|
||||
|
||||
Returns:
|
||||
the modified parser.
|
||||
"""
|
||||
return parser
|
||||
|
||||
@abstractmethod
|
||||
def __len__(self):
|
||||
"""Return the total number of images in the dataset."""
|
||||
return 0
|
||||
|
||||
@abstractmethod
|
||||
def __getitem__(self, index):
|
||||
"""Return a data point and its metadata information.
|
||||
|
||||
Parameters:
|
||||
index - - a random integer for data indexing
|
||||
|
||||
Returns:
|
||||
a dictionary of data with their names. It ususally contains the data itself and its metadata information.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def get_params(opt, size):
|
||||
w, h = size
|
||||
new_h = h
|
||||
new_w = w
|
||||
if opt.preprocess == 'resize_and_crop':
|
||||
new_h = new_w = opt.load_size
|
||||
elif opt.preprocess == 'scale_width_and_crop':
|
||||
new_w = opt.load_size
|
||||
new_h = opt.load_size * h // w
|
||||
|
||||
x = random.randint(0, np.maximum(0, new_w - opt.crop_size))
|
||||
y = random.randint(0, np.maximum(0, new_h - opt.crop_size))
|
||||
|
||||
flip = random.random() > 0.5
|
||||
|
||||
return {'crop_pos': (x, y), 'flip': flip}
|
||||
|
||||
def get_transform(opt, params=None, grayscale=False, method=Image.BICUBIC, convert=True):
|
||||
transform_list = []
|
||||
if grayscale:
|
||||
# transform_list.append(transforms.Grayscale(1))
|
||||
from util import util
|
||||
transform_list.append(util.RGBtoY)
|
||||
if 'resize' in opt.preprocess:
|
||||
osize = [opt.load_size, opt.load_size]
|
||||
transform_list.append(transforms.Resize(osize, method))
|
||||
elif 'scale_width' in opt.preprocess:
|
||||
transform_list.append(transforms.Lambda(lambda img: __scale_width(img, opt.load_size, method)))
|
||||
|
||||
if 'crop' in opt.preprocess:
|
||||
if params is None:
|
||||
transform_list.append(transforms.RandomCrop(opt.crop_size))
|
||||
else:
|
||||
if 'crop_size' in params:
|
||||
transform_list.append(transforms.Lambda(lambda img: __crop(img, params['crop_pos'], params['crop_size'])))
|
||||
else:
|
||||
transform_list.append(transforms.Lambda(lambda img: __crop(img, params['crop_pos'], opt.crop_size)))
|
||||
|
||||
if opt.preprocess == 'none':
|
||||
transform_list.append(transforms.Lambda(lambda img: __make_power_2(img, base=4, method=method)))
|
||||
|
||||
if not opt.no_flip:
|
||||
if params is None:
|
||||
transform_list.append(transforms.RandomHorizontalFlip())
|
||||
elif params['flip']:
|
||||
transform_list.append(transforms.Lambda(lambda img: __flip(img, params['flip'])))
|
||||
|
||||
if convert:
|
||||
transform_list += [transforms.ToTensor()]
|
||||
if grayscale:
|
||||
transform_list += [transforms.Normalize((0.5,), (0.5,))]
|
||||
else:
|
||||
transform_list += [transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]
|
||||
return transforms.Compose(transform_list)
|
||||
|
||||
def __make_power_2(img, base, method=Image.BICUBIC):
|
||||
ow, oh = img.size
|
||||
h = int(round(oh / base) * base)
|
||||
w = int(round(ow / base) * base)
|
||||
if (h == oh) and (w == ow):
|
||||
return img
|
||||
|
||||
__print_size_warning(ow, oh, w, h)
|
||||
return img.resize((w, h), method)
|
||||
|
||||
|
||||
def __scale_width(img, target_width, method=Image.BICUBIC):
|
||||
ow, oh = img.size
|
||||
if (ow == target_width):
|
||||
return img
|
||||
w = target_width
|
||||
h = int(target_width * oh / ow)
|
||||
return img.resize((w, h), method)
|
||||
|
||||
|
||||
def __crop(img, pos, size):
|
||||
ow, oh = img.size
|
||||
x1, y1 = pos
|
||||
tw = th = size
|
||||
if (ow > tw or oh > th):
|
||||
return img.crop((x1, y1, x1 + tw, y1 + th))
|
||||
return img
|
||||
|
||||
|
||||
def __flip(img, flip):
|
||||
if flip:
|
||||
return img.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
return img
|
||||
|
||||
|
||||
def __print_size_warning(ow, oh, w, h):
|
||||
"""Print warning information about image size(only print once)"""
|
||||
if not hasattr(__print_size_warning, 'has_printed'):
|
||||
print("The image size needs to be a multiple of 4. "
|
||||
"The loaded image size was (%d, %d), so it was adjusted to "
|
||||
"(%d, %d). This adjustment will be done to all images "
|
||||
"whose sizes are not multiples of 4" % (ow, oh, w, h))
|
||||
__print_size_warning.has_printed = True
|
||||
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
import random
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import imgaug as ia
|
||||
import imgaug.augmenters as iaa
|
||||
|
||||
from data.image_folder import make_dataset
|
||||
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from torchvision.transforms import transforms
|
||||
|
||||
from data.base_dataset import BaseDataset
|
||||
from utils.utils import onehot_parse_map
|
||||
|
||||
from data.ffhq_dataset import complex_imgaug, random_gray
|
||||
|
||||
class CelebAHQMaskDataset(BaseDataset):
|
||||
|
||||
def __init__(self, opt):
|
||||
BaseDataset.__init__(self, opt)
|
||||
self.img_size = opt.Pimg_size
|
||||
self.lr_size = opt.Gin_size
|
||||
self.hr_size = opt.Gout_size
|
||||
self.shuffle = True if opt.isTrain else False
|
||||
|
||||
self.img_dataset = sorted(make_dataset(os.path.join(opt.dataroot, 'CelebA-HQ-img')))
|
||||
self.mask_dataset = sorted(make_dataset(os.path.join(opt.dataroot, 'CelebAMask-HQ-mask')))
|
||||
|
||||
self.to_tensor = transforms.Compose([
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
|
||||
])
|
||||
|
||||
def __len__(self,):
|
||||
return len(self.img_dataset)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
sample = {}
|
||||
img_path = self.img_dataset[idx]
|
||||
mask_path = self.mask_dataset[idx]
|
||||
hr_img = Image.open(img_path).convert('RGB')
|
||||
mask_img = Image.open(mask_path)
|
||||
|
||||
hr_img = hr_img.resize((self.hr_size, self.hr_size))
|
||||
hr_img = random_gray(hr_img, p=0.3)
|
||||
scale_size = np.random.randint(32, 256)
|
||||
lr_img = complex_imgaug(hr_img, self.img_size, scale_size)
|
||||
|
||||
mask_img = mask_img.resize((self.hr_size, self.hr_size))
|
||||
mask_label = torch.tensor(np.array(mask_img)).long()
|
||||
|
||||
hr_tensor = self.to_tensor(hr_img)
|
||||
lr_tensor = self.to_tensor(lr_img)
|
||||
|
||||
return {'HR': hr_tensor, 'LR': lr_tensor, 'HR_paths': img_path, 'Mask': mask_label}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
import random
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import imgaug as ia
|
||||
import imgaug.augmenters as iaa
|
||||
|
||||
from data.image_folder import make_dataset
|
||||
|
||||
import torch
|
||||
from torch.utils.data import Dataset
|
||||
from torchvision.transforms import transforms
|
||||
|
||||
from data.base_dataset import BaseDataset
|
||||
from utils.utils import onehot_parse_map
|
||||
|
||||
class FFHQDataset(BaseDataset):
|
||||
|
||||
def __init__(self, opt):
|
||||
BaseDataset.__init__(self, opt)
|
||||
self.img_size = opt.Pimg_size
|
||||
self.lr_size = opt.Gin_size
|
||||
self.hr_size = opt.Gout_size
|
||||
self.shuffle = True if opt.isTrain else False
|
||||
|
||||
self.img_dataset = sorted(make_dataset(os.path.join(opt.dataroot, 'imgs1024')))
|
||||
self.mask_dataset = sorted(make_dataset(os.path.join(opt.dataroot, 'masks512')))
|
||||
|
||||
self.to_tensor = transforms.Compose([
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
|
||||
])
|
||||
self.random_crop = transforms.RandomCrop(self.hr_size)
|
||||
|
||||
def __len__(self,):
|
||||
return len(self.img_dataset)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
sample = {}
|
||||
img_path = self.img_dataset[idx]
|
||||
mask_path = self.mask_dataset[idx]
|
||||
hr_img = Image.open(img_path).convert('RGB')
|
||||
mask_img = Image.open(mask_path).convert('RGB')
|
||||
|
||||
hr_img = hr_img.resize((self.hr_size, self.hr_size))
|
||||
hr_img = random_gray(hr_img, p=0.3)
|
||||
scale_size = np.random.randint(32, 256)
|
||||
lr_img = complex_imgaug(hr_img, self.img_size, scale_size)
|
||||
|
||||
mask_img = mask_img.resize((self.hr_size, self.hr_size))
|
||||
mask_label = onehot_parse_map(mask_img)
|
||||
mask_label = torch.tensor(mask_label).float()
|
||||
|
||||
hr_tensor = self.to_tensor(hr_img)
|
||||
lr_tensor = self.to_tensor(lr_img)
|
||||
|
||||
return {'HR': hr_tensor, 'LR': lr_tensor, 'HR_paths': img_path, 'Mask': mask_label}
|
||||
|
||||
|
||||
def complex_imgaug(x, org_size, scale_size):
|
||||
"""input single RGB PIL Image instance"""
|
||||
x = np.array(x)
|
||||
x = x[np.newaxis, :, :, :]
|
||||
aug_seq = iaa.Sequential([
|
||||
iaa.Sometimes(0.5, iaa.OneOf([
|
||||
iaa.GaussianBlur((3, 15)),
|
||||
iaa.AverageBlur(k=(3, 15)),
|
||||
iaa.MedianBlur(k=(3, 15)),
|
||||
iaa.MotionBlur((5, 25))
|
||||
])),
|
||||
iaa.Resize(scale_size, interpolation=ia.ALL),
|
||||
iaa.Sometimes(0.2, iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.1*255), per_channel=0.5)),
|
||||
iaa.Sometimes(0.7, iaa.JpegCompression(compression=(10, 65))),
|
||||
iaa.Resize(org_size),
|
||||
])
|
||||
|
||||
aug_img = aug_seq(images=x)
|
||||
return aug_img[0]
|
||||
|
||||
|
||||
def random_gray(x, p=0.5):
|
||||
"""input single RGB PIL Image instance"""
|
||||
x = np.array(x)
|
||||
x = x[np.newaxis, :, :, :]
|
||||
aug = iaa.Sometimes(p, iaa.Grayscale(alpha=1.0))
|
||||
aug_img = aug(images=x)
|
||||
return aug_img[0]
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""A modified image folder class
|
||||
|
||||
We modify the official PyTorch image folder (https://github.com/pytorch/vision/blob/master/torchvision/datasets/folder.py)
|
||||
so that this class can load images from both current directory and its subdirectories.
|
||||
"""
|
||||
|
||||
import torch.utils.data as data
|
||||
|
||||
from PIL import Image
|
||||
import os
|
||||
import os.path
|
||||
|
||||
IMG_EXTENSIONS = [
|
||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||
'.png', '.PNG', '.ppm', '.PPM', '.bmp', '.BMP',
|
||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||
]
|
||||
|
||||
|
||||
def is_image_file(filename):
|
||||
return any(filename.endswith(extension) for extension in IMG_EXTENSIONS)
|
||||
|
||||
|
||||
def make_dataset(dir, max_dataset_size=float("inf")):
|
||||
images = []
|
||||
assert os.path.isdir(dir), '%s is not a valid directory' % dir
|
||||
|
||||
for root, _, fnames in sorted(os.walk(dir)):
|
||||
for fname in fnames:
|
||||
if is_image_file(fname):
|
||||
path = os.path.join(root, fname)
|
||||
images.append(path)
|
||||
return images[:min(max_dataset_size, len(images))]
|
||||
|
||||
|
||||
def default_loader(path):
|
||||
return Image.open(path).convert('RGB')
|
||||
|
||||
|
||||
class ImageFolder(data.Dataset):
|
||||
|
||||
def __init__(self, root, transform=None, return_paths=False,
|
||||
loader=default_loader):
|
||||
imgs = make_dataset(root)
|
||||
if len(imgs) == 0:
|
||||
raise(RuntimeError("Found 0 images in: " + root + "\n"
|
||||
"Supported image extensions are: " +
|
||||
",".join(IMG_EXTENSIONS)))
|
||||
|
||||
self.root = root
|
||||
self.imgs = imgs
|
||||
self.transform = transform
|
||||
self.return_paths = return_paths
|
||||
self.loader = loader
|
||||
|
||||
def __getitem__(self, index):
|
||||
path = self.imgs[index]
|
||||
img = self.loader(path)
|
||||
if self.transform is not None:
|
||||
img = self.transform(img)
|
||||
if self.return_paths:
|
||||
return img, path
|
||||
else:
|
||||
return img
|
||||
|
||||
def __len__(self):
|
||||
return len(self.imgs)
|
||||
@@ -0,0 +1,43 @@
|
||||
from data.base_dataset import BaseDataset, get_transform
|
||||
from data.image_folder import make_dataset
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
class SingleDataset(BaseDataset):
|
||||
"""This dataset class can load a set of images specified by the path --dataroot /path/to/data.
|
||||
|
||||
It can be used for generating CycleGAN results only for one side with the model option '-model test'.
|
||||
"""
|
||||
|
||||
def __init__(self, opt):
|
||||
"""Initialize this dataset class.
|
||||
|
||||
Parameters:
|
||||
opt (Option class) -- stores all the experiment flags; needs to be a subclass of BaseOptions
|
||||
"""
|
||||
BaseDataset.__init__(self, opt)
|
||||
self.A_paths = sorted(make_dataset(opt.src_dir, opt.max_dataset_size))
|
||||
input_nc = self.opt.output_nc
|
||||
self.transform = get_transform(opt, grayscale=(input_nc == 1))
|
||||
self.opt = opt
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Return a data point and its metadata information.
|
||||
|
||||
Parameters:
|
||||
index - - a random integer for data indexing
|
||||
|
||||
Returns a dictionary that contains A and A_paths
|
||||
A(tensor) - - an image in one domain
|
||||
A_paths(str) - - the path of the image
|
||||
"""
|
||||
A_path = self.A_paths[index]
|
||||
A_img = Image.open(A_path).convert('RGB')
|
||||
A_img = A_img.resize((512, 512), Image.BICUBIC)
|
||||
|
||||
A = self.transform(A_img)
|
||||
return {'LR': A, 'LR_paths': A_path}
|
||||
|
||||
def __len__(self):
|
||||
"""Return the total number of images in the dataset."""
|
||||
return len(self.A_paths)
|
||||
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
from options.test_options import TestOptions
|
||||
from data import create_dataset
|
||||
from models import create_model
|
||||
from utils import utils
|
||||
from PIL import Image
|
||||
from tqdm import tqdm
|
||||
import torch
|
||||
import time
|
||||
import numpy as np
|
||||
import cv2
|
||||
import glob
|
||||
from torchvision.transforms import transforms
|
||||
|
||||
if __name__ == '__main__':
|
||||
opt = TestOptions()
|
||||
opt = opt.parse() # get test options
|
||||
opt.num_threads = 0 # test code only supports num_threads = 1
|
||||
opt.serial_batches = True # disable data shuffling; comment this line if results on randomly chosen images are needed.
|
||||
opt.no_flip = True
|
||||
|
||||
dataset = create_dataset(opt) # create a dataset given opt.dataset_mode and other options
|
||||
model = create_model(opt) # create a model given opt.model and other options
|
||||
model.load_pretrain_models()
|
||||
|
||||
netP = model.netP
|
||||
model.eval()
|
||||
|
||||
to_tensor = transforms.Compose([
|
||||
transforms.ToTensor(),
|
||||
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
|
||||
])
|
||||
|
||||
image_dir = "G:/VGGFace2-HQ/VGGface2_None_norm_512_true_bygfpgan/"
|
||||
output_dir = "G:/VGGFace2-HQ/VGGface2_HQ_original_aligned_mask"
|
||||
|
||||
temp_path = os.path.join(image_dir,'*/')
|
||||
pathes = glob.glob(temp_path)
|
||||
dataset = []
|
||||
for dir_item in pathes:
|
||||
join_path = glob.glob(os.path.join(dir_item,'*.jpg'))
|
||||
print("processing %s"%dir_item,end='\r')
|
||||
temp_list = []
|
||||
for item in join_path:
|
||||
temp_list.append(item)
|
||||
dataset.append(temp_list)
|
||||
|
||||
# ------------------------ restore ------------------------
|
||||
for i_dir in dataset:
|
||||
path = os.path.dirname(i_dir[0])
|
||||
dir_name = os.path.join(output_dir, os.path.basename(path))
|
||||
if not os.path.exists(dir_name):
|
||||
os.makedirs(dir_name)
|
||||
|
||||
for img_path in i_dir:
|
||||
hr_img = Image.open(img_path).convert('RGB')
|
||||
inp = to_tensor(hr_img).unsqueeze(0)
|
||||
with torch.no_grad():
|
||||
parse_map, _ = netP(inp)
|
||||
parse_map_sm = (parse_map == parse_map.max(dim=1, keepdim=True)[0]).float()
|
||||
ref_parse_img = utils.color_parse_map(parse_map_sm)
|
||||
img_name = os.path.basename(img_path)
|
||||
basename, ext = os.path.splitext(img_name)
|
||||
save_face_name = f'{basename}.png'
|
||||
# print(save_face_name)
|
||||
save_path = os.path.join(dir_name, save_face_name)
|
||||
# os.makedirs(opt.save_masks_dir, exist_ok=True)
|
||||
img = cv2.cvtColor(ref_parse_img[0],cv2.COLOR_RGB2GRAY)
|
||||
cv2.imwrite(save_path,img)
|
||||
|
||||
# for i, data in tqdm(enumerate(dataset), total=len(dataset)//opt.batch_size):
|
||||
# inp = data['LR']
|
||||
# with torch.no_grad():
|
||||
# parse_map, _ = netP(inp)
|
||||
# parse_map_sm = (parse_map == parse_map.max(dim=1, keepdim=True)[0]).float()
|
||||
# img_path = data['LR_paths'] # get image paths
|
||||
# ref_parse_img = utils.color_parse_map(parse_map_sm)
|
||||
# for i in range(len(img_path)):
|
||||
# img_name = os.path.basename(img_path[i])
|
||||
# basename, ext = os.path.splitext(img_name)
|
||||
# save_face_name = f'{basename}.png'
|
||||
# # print(save_face_name)
|
||||
# save_path = os.path.join(opt.save_masks_dir, save_face_name)
|
||||
# os.makedirs(opt.save_masks_dir, exist_ok=True)
|
||||
# img = cv2.cvtColor(ref_parse_img[i],cv2.COLOR_RGB2GRAY)
|
||||
# cv2.imwrite(save_path,img)
|
||||
# save_img = Image.fromarray(ref_parse_img[i])
|
||||
# save_img.save(save_path)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""This package contains modules related to objective functions, optimizations, and network architectures.
|
||||
|
||||
To add a custom model class called 'dummy', you need to add a file called 'dummy_model.py' and define a subclass DummyModel inherited from BaseModel.
|
||||
You need to implement the following five functions:
|
||||
-- <__init__>: initialize the class; first call BaseModel.__init__(self, opt).
|
||||
-- <set_input>: unpack data from dataset and apply preprocessing.
|
||||
-- <forward>: produce intermediate results.
|
||||
-- <optimize_parameters>: calculate loss, gradients, and update network weights.
|
||||
-- <modify_commandline_options>: (optionally) add model-specific options and set default options.
|
||||
|
||||
In the function <__init__>, you need to define four lists:
|
||||
-- self.loss_names (str list): specify the training losses that you want to plot and save.
|
||||
-- self.model_names (str list): define networks used in our training.
|
||||
-- self.visual_names (str list): specify the images that you want to display and save.
|
||||
-- self.optimizers (optimizer list): define and initialize optimizers. You can define one optimizer for each network. If two networks are updated at the same time, you can use itertools.chain to group them. See cycle_gan_model.py for an usage.
|
||||
|
||||
Now you can use the model class by specifying flag '--model dummy'.
|
||||
See our template model class 'template_model.py' for more details.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
from models.base_model import BaseModel
|
||||
|
||||
|
||||
def find_model_using_name(model_name):
|
||||
"""Import the module "models/[model_name]_model.py".
|
||||
|
||||
In the file, the class called DatasetNameModel() will
|
||||
be instantiated. It has to be a subclass of BaseModel,
|
||||
and it is case-insensitive.
|
||||
"""
|
||||
model_filename = "models." + model_name + "_model"
|
||||
modellib = importlib.import_module(model_filename)
|
||||
model = None
|
||||
target_model_name = model_name.replace('_', '') + 'model'
|
||||
for name, cls in modellib.__dict__.items():
|
||||
if name.lower() == target_model_name.lower() \
|
||||
and issubclass(cls, BaseModel):
|
||||
model = cls
|
||||
|
||||
if model is None:
|
||||
print("In %s.py, there should be a subclass of BaseModel with class name that matches %s in lowercase." % (model_filename, target_model_name))
|
||||
exit(0)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def get_option_setter(model_name):
|
||||
"""Return the static method <modify_commandline_options> of the model class."""
|
||||
model_class = find_model_using_name(model_name)
|
||||
return model_class.modify_commandline_options
|
||||
|
||||
|
||||
def create_model(opt):
|
||||
"""Create a model given the option.
|
||||
|
||||
This function warps the class CustomDatasetDataLoader.
|
||||
This is the main interface between this package and 'train.py'/'test.py'
|
||||
|
||||
Example:
|
||||
>>> from models import create_model
|
||||
>>> model = create_model(opt)
|
||||
"""
|
||||
model = find_model_using_name(opt.model)
|
||||
instance = model(opt)
|
||||
print("model [%s] was created" % type(instance).__name__)
|
||||
return instance
|
||||
@@ -0,0 +1,248 @@
|
||||
import os
|
||||
import torch
|
||||
from collections import OrderedDict
|
||||
from abc import ABC, abstractmethod
|
||||
from . import networks
|
||||
|
||||
class BaseModel(ABC):
|
||||
"""This class is an abstract base class (ABC) for models.
|
||||
To create a subclass, you need to implement the following five functions:
|
||||
-- <__init__>: initialize the class; first call BaseModel.__init__(self, opt).
|
||||
-- <set_input>: unpack data from dataset and apply preprocessing.
|
||||
-- <forward>: produce intermediate results.
|
||||
-- <optimize_parameters>: calculate losses, gradients, and update network weights.
|
||||
-- <modify_commandline_options>: (optionally) add model-specific options and set default options.
|
||||
"""
|
||||
|
||||
def __init__(self, opt):
|
||||
"""Initialize the BaseModel class.
|
||||
|
||||
Parameters:
|
||||
opt (Option class)-- stores all the experiment flags; needs to be a subclass of BaseOptions
|
||||
|
||||
When creating your custom class, you need to implement your own initialization.
|
||||
In this fucntion, you should first call <BaseModel.__init__(self, opt)>
|
||||
Then, you need to define four lists:
|
||||
-- self.loss_names (str list): specify the training losses that you want to plot and save.
|
||||
-- self.model_names (str list): specify the images that you want to display and save.
|
||||
-- self.visual_names (str list): define networks used in our training.
|
||||
-- self.optimizers (optimizer list): define and initialize optimizers. You can define one optimizer for each network. If two networks are updated at the same time, you can use itertools.chain to group them. See cycle_gan_model.py for an example.
|
||||
"""
|
||||
self.opt = opt
|
||||
self.gpu_ids = opt.gpu_ids
|
||||
self.isTrain = opt.isTrain
|
||||
self.save_dir = os.path.join(opt.checkpoints_dir, opt.name) # save all the checkpoints to save_dir
|
||||
self.device = torch.device('cuda:{}'.format(self.gpu_ids[0])) if self.gpu_ids else torch.device('cpu') # get device name: CPU or GPU
|
||||
|
||||
self.loss_names = []
|
||||
self.model_names = []
|
||||
self.visual_names = []
|
||||
self.optimizers = []
|
||||
self.image_paths = []
|
||||
self.metric = 0 # used for learning rate policy 'plateau'
|
||||
|
||||
@staticmethod
|
||||
def modify_commandline_options(parser, is_train):
|
||||
"""Add new model-specific options, and rewrite default values for existing options.
|
||||
|
||||
Parameters:
|
||||
parser -- original option parser
|
||||
is_train (bool) -- whether training phase or test phase. You can use this flag to add training-specific or test-specific options.
|
||||
|
||||
Returns:
|
||||
the modified parser.
|
||||
"""
|
||||
return parser
|
||||
|
||||
@abstractmethod
|
||||
def set_input(self, input):
|
||||
"""Unpack input data from the dataloader and perform necessary pre-processing steps.
|
||||
|
||||
Parameters:
|
||||
input (dict): includes the data itself and its metadata information.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def forward(self):
|
||||
"""Run forward pass; called by both functions <optimize_parameters> and <test>."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def optimize_parameters(self):
|
||||
"""Calculate losses, gradients, and update network weights; called in every training iteration"""
|
||||
pass
|
||||
|
||||
def setup(self, opt):
|
||||
"""Load and print networks; create schedulers
|
||||
|
||||
Parameters:
|
||||
opt (Option class) -- stores all the experiment flags; needs to be a subclass of BaseOptions
|
||||
"""
|
||||
if self.isTrain:
|
||||
self.schedulers = [networks.get_scheduler(optimizer, opt) for optimizer in self.optimizers]
|
||||
if not self.isTrain or opt.continue_train:
|
||||
load_suffix = 'iter_%d' % opt.load_iter if opt.load_iter > 0 else opt.epoch
|
||||
self.load_networks(load_suffix)
|
||||
self.print_networks(opt.verbose)
|
||||
|
||||
def eval(self):
|
||||
"""Make models eval mode during test time"""
|
||||
for name in self.model_names:
|
||||
if isinstance(name, str):
|
||||
net = getattr(self, 'net' + name)
|
||||
net.eval()
|
||||
|
||||
def test(self):
|
||||
"""Forward function used in test time.
|
||||
|
||||
This function wraps <forward> function in no_grad() so we don't save intermediate steps for backprop
|
||||
It also calls <compute_visuals> to produce additional visualization results
|
||||
"""
|
||||
with torch.no_grad():
|
||||
self.forward()
|
||||
self.compute_visuals()
|
||||
|
||||
def compute_visuals(self):
|
||||
"""Calculate additional output images for visdom and HTML visualization"""
|
||||
pass
|
||||
|
||||
def get_image_paths(self):
|
||||
""" Return image paths that are used to load current data"""
|
||||
return self.image_paths
|
||||
|
||||
def update_learning_rate(self):
|
||||
"""Update learning rates for all the networks; called at the end of every epoch"""
|
||||
for scheduler in self.schedulers:
|
||||
if self.opt.lr_policy == 'plateau':
|
||||
scheduler.step(self.metric)
|
||||
else:
|
||||
scheduler.step()
|
||||
|
||||
lr = self.optimizers[0].param_groups[0]['lr']
|
||||
print('learning rate = %.7f' % lr)
|
||||
|
||||
def get_lr(self,):
|
||||
lrs = {}
|
||||
for idx, p in enumerate(self.optimizers):
|
||||
lrs['LR{}'.format(idx)] = p.param_groups[0]['lr']
|
||||
return lrs
|
||||
|
||||
def get_current_visuals(self):
|
||||
"""Return visualization images. train.py will display these images with visdom, and save the images to a HTML"""
|
||||
visual_ret = OrderedDict()
|
||||
for name in self.visual_names:
|
||||
if isinstance(name, str):
|
||||
visual_ret[name] = getattr(self, name)
|
||||
return visual_ret
|
||||
|
||||
def get_current_losses(self):
|
||||
"""Return traning losses / errors. train.py will print out these errors on console, and save them to a file"""
|
||||
errors_ret = OrderedDict()
|
||||
for name in self.loss_names:
|
||||
if isinstance(name, str):
|
||||
errors_ret['Loss_' + name] = float(getattr(self, 'loss_' + name)) # float(...) works for both scalar tensor and float number
|
||||
return errors_ret
|
||||
|
||||
def save_networks(self, epoch, info=None):
|
||||
"""Save all the networks to the disk.
|
||||
|
||||
Parameters:
|
||||
epoch (int) -- current epoch; used in the file name '%s_net_%s.pth' % (epoch, name)
|
||||
"""
|
||||
for name in self.model_names:
|
||||
if isinstance(name, str):
|
||||
save_filename = '%s_net_%s.pth' % (epoch, name)
|
||||
save_path = os.path.join(self.save_dir, save_filename)
|
||||
net = getattr(self, 'net' + name)
|
||||
|
||||
if len(self.gpu_ids) > 0 and torch.cuda.is_available():
|
||||
torch.save(net.module.cpu().state_dict(), save_path)
|
||||
print('Model saved in:', save_filename)
|
||||
net.cuda(self.gpu_ids[0])
|
||||
else:
|
||||
torch.save(net.cpu().state_dict(), save_path)
|
||||
if info is not None:
|
||||
torch.save(info, os.path.join(self.save_dir, '%s.info' % epoch))
|
||||
|
||||
def __patch_instance_norm_state_dict(self, state_dict, module, keys, i=0):
|
||||
"""Fix InstanceNorm checkpoints incompatibility (prior to 0.4)"""
|
||||
key = keys[i]
|
||||
if i + 1 == len(keys): # at the end, pointing to a parameter/buffer
|
||||
if module.__class__.__name__.startswith('InstanceNorm') and \
|
||||
(key == 'running_mean' or key == 'running_var'):
|
||||
if getattr(module, key) is None:
|
||||
state_dict.pop('.'.join(keys))
|
||||
if module.__class__.__name__.startswith('InstanceNorm') and \
|
||||
(key == 'num_batches_tracked'):
|
||||
state_dict.pop('.'.join(keys))
|
||||
else:
|
||||
self.__patch_instance_norm_state_dict(state_dict, getattr(module, key), keys, i + 1)
|
||||
|
||||
def load_networks(self, epoch):
|
||||
"""Load all the networks from the disk.
|
||||
|
||||
Parameters:
|
||||
epoch (int) -- current epoch; used in the file name '%s_net_%s.pth' % (epoch, name)
|
||||
"""
|
||||
for name in self.load_model_names:
|
||||
if isinstance(name, str):
|
||||
load_filename = '%s_net_%s.pth' % (epoch, name)
|
||||
load_path = os.path.join(self.save_dir, load_filename)
|
||||
net = getattr(self, 'net' + name)
|
||||
if isinstance(net, torch.nn.DataParallel) or isinstance(net, torch.nn.parallel.DistributedDataParallel):
|
||||
net = net.module
|
||||
print('loading the model from %s' % load_path)
|
||||
# if you are using PyTorch newer than 0.4 (e.g., built from
|
||||
# GitHub source), you can remove str() on self.device
|
||||
map_location = str(self.device)
|
||||
|
||||
state_dict = torch.load(load_path, map_location=map_location)
|
||||
|
||||
# patch InstanceNorm checkpoints prior to 0.4
|
||||
# for key in list(state_dict.keys()): # need to copy keys here because we mutate in loop
|
||||
# self.__patch_instance_norm_state_dict(state_dict, net, key.split('.'))
|
||||
# net.load_state_dict(state_dict)
|
||||
if not self.opt.no_strict_load:
|
||||
net.load_state_dict(state_dict)
|
||||
# Load partial weights
|
||||
else:
|
||||
model_dict = net.state_dict()
|
||||
pretrained_dict = {k: v for k, v in state_dict.items() if k in model_dict}
|
||||
model_dict.update(pretrained_dict)
|
||||
net.load_state_dict(model_dict, strict=False)
|
||||
|
||||
info_path = os.path.join(self.save_dir, '%s.info' % epoch)
|
||||
if os.path.exists(info_path):
|
||||
info_dict = torch.load(info_path)
|
||||
for k, v in info_dict.items():
|
||||
setattr(self.opt, k, v)
|
||||
|
||||
def print_networks(self, verbose):
|
||||
"""Print the total number of parameters in the network and (if verbose) network architecture
|
||||
|
||||
Parameters:
|
||||
verbose (bool) -- if verbose: print the network architecture
|
||||
"""
|
||||
print('---------- Networks initialized -------------')
|
||||
for name in self.model_names:
|
||||
if isinstance(name, str):
|
||||
net = getattr(self, 'net' + name)
|
||||
num_params = 0
|
||||
for param in net.parameters():
|
||||
num_params += param.numel()
|
||||
print('[Network %s] Total number of parameters : %.3f M' % (name, num_params / 1e6))
|
||||
print('-----------------------------------------------')
|
||||
|
||||
def set_requires_grad(self, nets, requires_grad=False):
|
||||
"""Set requies_grad=Fasle for all the networks to avoid unnecessary computations
|
||||
Parameters:
|
||||
nets (network list) -- a list of networks
|
||||
requires_grad (bool) -- whether the networks require gradients or not
|
||||
"""
|
||||
if not isinstance(nets, list):
|
||||
nets = [nets]
|
||||
for net in nets:
|
||||
if net is not None:
|
||||
for param in net.parameters():
|
||||
param.requires_grad = requires_grad
|
||||
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.nn.parameter import Parameter
|
||||
from torch.nn import functional as F
|
||||
import numpy as np
|
||||
|
||||
|
||||
class NormLayer(nn.Module):
|
||||
"""Normalization Layers.
|
||||
------------
|
||||
# Arguments
|
||||
- channels: input channels, for batch norm and instance norm.
|
||||
- input_size: input shape without batch size, for layer norm.
|
||||
"""
|
||||
def __init__(self, channels, normalize_shape=None, norm_type='bn'):
|
||||
super(NormLayer, self).__init__()
|
||||
norm_type = norm_type.lower()
|
||||
self.norm_type = norm_type
|
||||
self.channels = channels
|
||||
if norm_type == 'bn':
|
||||
self.norm = nn.BatchNorm2d(channels, affine=True)
|
||||
elif norm_type == 'in':
|
||||
self.norm = nn.InstanceNorm2d(channels, affine=False)
|
||||
elif norm_type == 'gn':
|
||||
self.norm = nn.GroupNorm(32, channels, affine=True)
|
||||
elif norm_type == 'pixel':
|
||||
self.norm = lambda x: F.normalize(x, p=2, dim=1)
|
||||
elif norm_type == 'layer':
|
||||
self.norm = nn.LayerNorm(normalize_shape)
|
||||
elif norm_type == 'none':
|
||||
self.norm = lambda x: x*1.0
|
||||
else:
|
||||
assert 1==0, 'Norm type {} not support.'.format(norm_type)
|
||||
|
||||
def forward(self, x, ref=None):
|
||||
return self.norm(x)
|
||||
|
||||
|
||||
class ReluLayer(nn.Module):
|
||||
"""Relu Layer.
|
||||
------------
|
||||
# Arguments
|
||||
- relu type: type of relu layer, candidates are
|
||||
- ReLU
|
||||
- LeakyReLU: default relu slope 0.2
|
||||
- PRelu
|
||||
- SELU
|
||||
- none: direct pass
|
||||
"""
|
||||
def __init__(self, channels, relu_type='relu'):
|
||||
super(ReluLayer, self).__init__()
|
||||
relu_type = relu_type.lower()
|
||||
if relu_type == 'relu':
|
||||
self.func = nn.ReLU(True)
|
||||
elif relu_type == 'leakyrelu':
|
||||
self.func = nn.LeakyReLU(0.2, inplace=True)
|
||||
elif relu_type == 'prelu':
|
||||
self.func = nn.PReLU(channels)
|
||||
elif relu_type == 'selu':
|
||||
self.func = nn.SELU(True)
|
||||
elif relu_type == 'none':
|
||||
self.func = lambda x: x*1.0
|
||||
else:
|
||||
assert 1==0, 'Relu type {} not support.'.format(relu_type)
|
||||
|
||||
def forward(self, x):
|
||||
return self.func(x)
|
||||
|
||||
|
||||
class ConvLayer(nn.Module):
|
||||
def __init__(self, in_channels, out_channels, kernel_size=3, scale='none', norm_type='none', relu_type='none', use_pad=True, bias=True):
|
||||
super(ConvLayer, self).__init__()
|
||||
self.use_pad = use_pad
|
||||
self.norm_type = norm_type
|
||||
self.in_channels = in_channels
|
||||
if norm_type in ['bn']:
|
||||
bias = False
|
||||
|
||||
stride = 2 if scale == 'down' else 1
|
||||
self.scale = scale
|
||||
|
||||
self.scale_func = lambda x: x
|
||||
if scale == 'up':
|
||||
self.scale_func = lambda x: nn.functional.interpolate(x, scale_factor=2, mode='nearest')
|
||||
|
||||
self.reflection_pad = nn.ReflectionPad2d(int(np.ceil((kernel_size - 1.)/2)))
|
||||
self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size, stride, bias=bias)
|
||||
|
||||
self.avgpool = nn.AvgPool2d(2, 2)
|
||||
self.relu = ReluLayer(out_channels, relu_type)
|
||||
self.norm = NormLayer(out_channels, norm_type=norm_type)
|
||||
|
||||
def forward(self, x):
|
||||
out = self.scale_func(x)
|
||||
if self.use_pad:
|
||||
out = self.reflection_pad(out)
|
||||
out = self.conv2d(out)
|
||||
if self.scale == 'down_avg':
|
||||
out = self.avgpool(out)
|
||||
out = self.norm(out)
|
||||
out = self.relu(out)
|
||||
return out
|
||||
|
||||
|
||||
class ResidualBlock(nn.Module):
|
||||
"""
|
||||
Residual block recommended in: http://torch.ch/blog/2016/02/04/resnets.html
|
||||
"""
|
||||
def __init__(self, c_in, c_out, relu_type='prelu', norm_type='bn', scale='none'):
|
||||
super(ResidualBlock, self).__init__()
|
||||
|
||||
if scale == 'none' and c_in == c_out:
|
||||
self.shortcut_func = lambda x: x
|
||||
else:
|
||||
self.shortcut_func = ConvLayer(c_in, c_out, 3, scale)
|
||||
|
||||
scale_config_dict = {'down': ['none', 'down'], 'up': ['up', 'none'], 'none': ['none', 'none']}
|
||||
scale_conf = scale_config_dict[scale]
|
||||
|
||||
self.conv1 = ConvLayer(c_in, c_out, 3, scale_conf[0], norm_type=norm_type, relu_type=relu_type)
|
||||
self.conv2 = ConvLayer(c_out, c_out, 3, scale_conf[1], norm_type=norm_type, relu_type='none')
|
||||
|
||||
def forward(self, x):
|
||||
identity = self.shortcut_func(x)
|
||||
|
||||
res = self.conv1(x)
|
||||
res = self.conv2(res)
|
||||
return identity + res
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import collections
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
|
||||
from models import loss
|
||||
from models import networks
|
||||
from .base_model import BaseModel
|
||||
from utils import utils
|
||||
|
||||
class EnhanceModel(BaseModel):
|
||||
|
||||
def modify_commandline_options(parser, is_train):
|
||||
if is_train:
|
||||
parser.add_argument('--parse_net_weight', type=str, default='./pretrain_models/parse_multi_iter_90000.pth', help='parse model path')
|
||||
parser.add_argument('--lambda_pix', type=float, default=10.0, help='weight for parsing map')
|
||||
parser.add_argument('--lambda_pcp', type=float, default=0.0, help='weight for vgg perceptual loss')
|
||||
parser.add_argument('--lambda_fm', type=float, default=10.0, help='weight for sr')
|
||||
parser.add_argument('--lambda_g', type=float, default=1.0, help='weight for sr')
|
||||
parser.add_argument('--lambda_ss', type=float, default=1000., help='weight for global style')
|
||||
return parser
|
||||
|
||||
def __init__(self, opt):
|
||||
BaseModel.__init__(self, opt)
|
||||
|
||||
self.netP = networks.define_P(opt, weight_path=opt.parse_net_weight)
|
||||
self.netG = networks.define_G(opt, use_norm='spectral_norm')
|
||||
|
||||
if self.isTrain:
|
||||
self.netD = networks.define_D(opt, opt.Dinput_nc, use_norm='spectral_norm')
|
||||
self.vgg_model = loss.PCPFeat(weight_path='./pretrain_models/vgg19-dcbb9e9d.pth').to(opt.device)
|
||||
if len(opt.gpu_ids) > 0:
|
||||
self.vgg_model = torch.nn.DataParallel(self.vgg_model, opt.gpu_ids, output_device=opt.device)
|
||||
|
||||
self.model_names = ['G']
|
||||
self.loss_names = ['Pix', 'PCP', 'G', 'FM', 'D', 'SS'] # Generator loss, fm loss, parsing loss, discriminator loss
|
||||
self.visual_names = ['img_LR', 'img_HR', 'img_SR', 'ref_Parse', 'hr_mask']
|
||||
self.fm_weights = [1**x for x in range(opt.D_num)]
|
||||
|
||||
if self.isTrain:
|
||||
self.model_names = ['G', 'D']
|
||||
self.load_model_names = ['G', 'D']
|
||||
|
||||
self.criterionParse = torch.nn.CrossEntropyLoss().to(opt.device)
|
||||
self.criterionFM = loss.FMLoss().to(opt.device)
|
||||
self.criterionGAN = loss.GANLoss(opt.gan_mode).to(opt.device)
|
||||
self.criterionPCP = loss.PCPLoss(opt)
|
||||
self.criterionPix= nn.L1Loss()
|
||||
self.criterionRS = loss.RegionStyleLoss()
|
||||
|
||||
self.optimizer_G = optim.Adam([p for p in self.netG.parameters() if p.requires_grad], lr=opt.g_lr, betas=(opt.beta1, 0.999))
|
||||
self.optimizer_D = optim.Adam([p for p in self.netD.parameters() if p.requires_grad], lr=opt.d_lr, betas=(opt.beta1, 0.999))
|
||||
self.optimizers = [self.optimizer_G, self.optimizer_D]
|
||||
|
||||
def eval(self):
|
||||
self.netG.eval()
|
||||
self.netP.eval()
|
||||
|
||||
def load_pretrain_models(self,):
|
||||
self.netP.eval()
|
||||
print('Loading pretrained LQ face parsing network from', self.opt.parse_net_weight)
|
||||
if len(self.opt.gpu_ids) > 0:
|
||||
self.netP.module.load_state_dict(torch.load(self.opt.parse_net_weight))
|
||||
else:
|
||||
self.netP.load_state_dict(torch.load(self.opt.parse_net_weight))
|
||||
self.netG.eval()
|
||||
print('Loading pretrained PSFRGAN from', self.opt.psfr_net_weight)
|
||||
if len(self.opt.gpu_ids) > 0:
|
||||
self.netG.module.load_state_dict(torch.load(self.opt.psfr_net_weight), strict=False)
|
||||
else:
|
||||
self.netG.load_state_dict(torch.load(self.opt.psfr_net_weight), strict=False)
|
||||
|
||||
def set_input(self, input, cur_iters=None):
|
||||
self.cur_iters = cur_iters
|
||||
self.img_LR = input['LR'].to(self.opt.device)
|
||||
self.img_HR = input['HR'].to(self.opt.device)
|
||||
self.hr_mask = input['Mask'].to(self.opt.device)
|
||||
if self.opt.debug:
|
||||
print('SRNet input shape:', self.img_LR.shape, self.img_HR.shape)
|
||||
|
||||
def forward(self):
|
||||
with torch.no_grad():
|
||||
ref_mask, _ = self.netP(self.img_LR)
|
||||
self.ref_mask_onehot = (ref_mask == ref_mask.max(dim=1, keepdim=True)[0]).float().detach()
|
||||
|
||||
if self.opt.debug:
|
||||
print('SRNet reference mask shape:', self.ref_mask_onehot.shape)
|
||||
self.img_SR = self.netG(self.img_LR, self.ref_mask_onehot)
|
||||
|
||||
self.real_D_results = self.netD(torch.cat((self.img_HR, self.hr_mask), dim=1), return_feat=True)
|
||||
self.fake_D_results = self.netD(torch.cat((self.img_SR.detach(), self.hr_mask), dim=1), return_feat=False)
|
||||
self.fake_G_results = self.netD(torch.cat((self.img_SR, self.hr_mask), dim=1), return_feat=True)
|
||||
|
||||
self.img_SR_feats = self.vgg_model(self.img_SR)
|
||||
self.img_HR_feats = self.vgg_model(self.img_HR)
|
||||
|
||||
def backward_G(self):
|
||||
# Pix Loss
|
||||
self.loss_Pix = self.criterionPix(self.img_SR, self.img_HR) * self.opt.lambda_pix
|
||||
|
||||
# semantic style loss
|
||||
self.loss_SS = self.criterionRS(self.img_SR_feats, self.img_HR_feats, self.hr_mask) * self.opt.lambda_ss
|
||||
|
||||
# perceptual loss
|
||||
self.loss_PCP = self.criterionPCP(self.img_SR_feats, self.img_HR_feats) * self.opt.lambda_pcp
|
||||
|
||||
# Feature matching loss
|
||||
tmp_loss = 0
|
||||
for i, w in zip(range(self.opt.D_num), self.fm_weights):
|
||||
tmp_loss = tmp_loss + self.criterionFM(self.fake_G_results[i][1], self.real_D_results[i][1]) * w
|
||||
self.loss_FM = tmp_loss * self.opt.lambda_fm / self.opt.D_num
|
||||
|
||||
# Generator loss
|
||||
tmp_loss = 0
|
||||
for i in range(self.opt.D_num):
|
||||
tmp_loss = tmp_loss + self.criterionGAN(self.fake_G_results[i][0], True, for_discriminator=False)
|
||||
self.loss_G = tmp_loss * self.opt.lambda_g / self.opt.D_num
|
||||
|
||||
total_loss = self.loss_Pix + self.loss_PCP + self.loss_FM + self.loss_G + self.loss_SS
|
||||
total_loss.backward()
|
||||
|
||||
def backward_D(self, ):
|
||||
self.loss_D = 0
|
||||
for i in range(self.opt.D_num):
|
||||
self.loss_D += 0.5 * (self.criterionGAN(self.fake_D_results[i], False) + self.criterionGAN(self.real_D_results[i][0], True))
|
||||
self.loss_D /= self.opt.D_num
|
||||
self.loss_D.backward()
|
||||
|
||||
def optimize_parameters(self, ):
|
||||
# ---- Update G ------------
|
||||
self.optimizer_G.zero_grad()
|
||||
self.backward_G()
|
||||
self.optimizer_G.step()
|
||||
|
||||
# ---- Update D ------------
|
||||
self.optimizer_D.zero_grad()
|
||||
self.backward_D()
|
||||
self.optimizer_D.step()
|
||||
|
||||
def get_current_visuals(self, size=512):
|
||||
out = []
|
||||
visual_imgs = []
|
||||
out.append(utils.tensor_to_numpy(self.img_LR))
|
||||
out.append(utils.tensor_to_numpy(self.img_SR))
|
||||
out.append(utils.tensor_to_numpy(self.img_HR))
|
||||
|
||||
out_imgs = [utils.batch_numpy_to_image(x, size) for x in out]
|
||||
|
||||
visual_imgs += out_imgs
|
||||
visual_imgs.append(utils.color_parse_map(self.ref_mask_onehot, size))
|
||||
visual_imgs.append(utils.color_parse_map(self.hr_mask, size))
|
||||
|
||||
return visual_imgs
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import torch
|
||||
from torchvision import models
|
||||
from utils import utils
|
||||
from torch import nn
|
||||
|
||||
|
||||
def tv_loss(x):
|
||||
"""
|
||||
Total Variation Loss.
|
||||
"""
|
||||
return torch.sum(torch.abs(x[:, :, :, :-1] - x[:, :, :, 1:])
|
||||
) + torch.sum(torch.abs(x[:, :, :-1, :] - x[:, :, 1:, :]))
|
||||
|
||||
|
||||
class PCPFeat(torch.nn.Module):
|
||||
"""
|
||||
Features used to calculate Perceptual Loss based on ResNet50 features.
|
||||
Input: (B, C, H, W), RGB, [0, 1]
|
||||
"""
|
||||
def __init__(self, weight_path, model='vgg'):
|
||||
super(PCPFeat, self).__init__()
|
||||
if model == 'vgg':
|
||||
self.model = models.vgg19(pretrained=False)
|
||||
self.build_vgg_layers()
|
||||
elif model == 'resnet':
|
||||
self.model = models.resnet50(pretrained=False)
|
||||
self.build_resnet_layers()
|
||||
|
||||
self.model.load_state_dict(torch.load(weight_path))
|
||||
self.model.eval()
|
||||
for param in self.model.parameters():
|
||||
param.requires_grad = False
|
||||
|
||||
def build_resnet_layers(self):
|
||||
self.layer1 = torch.nn.Sequential(
|
||||
self.model.conv1,
|
||||
self.model.bn1,
|
||||
self.model.relu,
|
||||
self.model.maxpool,
|
||||
self.model.layer1
|
||||
)
|
||||
self.layer2 = self.model.layer2
|
||||
self.layer3 = self.model.layer3
|
||||
self.layer4 = self.model.layer4
|
||||
self.features = torch.nn.ModuleList(
|
||||
[self.layer1, self.layer2, self.layer3, self.layer4]
|
||||
)
|
||||
|
||||
def build_vgg_layers(self):
|
||||
vgg_pretrained_features = self.model.features
|
||||
self.features = []
|
||||
feature_layers = [0, 3, 8, 17, 26, 35]
|
||||
for i in range(len(feature_layers)-1):
|
||||
module_layers = torch.nn.Sequential()
|
||||
for j in range(feature_layers[i], feature_layers[i+1]):
|
||||
module_layers.add_module(str(j), vgg_pretrained_features[j])
|
||||
self.features.append(module_layers)
|
||||
self.features = torch.nn.ModuleList(self.features)
|
||||
|
||||
def preprocess(self, x):
|
||||
x = (x + 1) / 2
|
||||
mean = torch.Tensor([0.485, 0.456, 0.406]).to(x)
|
||||
std = torch.Tensor([0.229, 0.224, 0.225]).to(x)
|
||||
mean = mean.view(1, 3, 1, 1)
|
||||
std = std.view(1, 3, 1, 1)
|
||||
x = (x - mean) / std
|
||||
if x.shape[3] < 224:
|
||||
x = torch.nn.functional.interpolate(x, size=(224, 224), mode='bilinear', align_corners=False)
|
||||
return x
|
||||
|
||||
def forward(self, x):
|
||||
x = self.preprocess(x)
|
||||
|
||||
features = []
|
||||
for m in self.features:
|
||||
x = m(x)
|
||||
features.append(x)
|
||||
return features
|
||||
|
||||
|
||||
class PCPLoss(torch.nn.Module):
|
||||
"""Perceptual Loss.
|
||||
"""
|
||||
def __init__(self,
|
||||
opt,
|
||||
layer=5,
|
||||
model='vgg',
|
||||
):
|
||||
super(PCPLoss, self).__init__()
|
||||
|
||||
self.mse = torch.nn.MSELoss()
|
||||
self.weights = [1.0/32, 1.0/16, 1.0/8, 1.0/4, 1.0]
|
||||
|
||||
def forward(self, x_feats, y_feats):
|
||||
loss = 0
|
||||
for xf, yf, w in zip(x_feats, y_feats, self.weights):
|
||||
loss = loss + self.mse(xf, yf.detach()) * w
|
||||
return loss
|
||||
|
||||
|
||||
class FMLoss(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mse = torch.nn.MSELoss()
|
||||
|
||||
def forward(self, x_feats, y_feats):
|
||||
loss = 0
|
||||
for xf, yf in zip(x_feats, y_feats):
|
||||
loss = loss + self.mse(xf, yf.detach())
|
||||
return loss
|
||||
|
||||
|
||||
class GANLoss(nn.Module):
|
||||
def __init__(self, gan_mode, target_real_label=1.0, target_fake_label=0.0):
|
||||
""" Initialize the GANLoss class.
|
||||
Parameters:
|
||||
gan_mode (str) - - the type of GAN objective. It currently supports vanilla, lsgan, and wgangp.
|
||||
target_real_label (bool) - - label for a real image
|
||||
target_fake_label (bool) - - label of a fake image
|
||||
Note: Do not use sigmoid as the last layer of Discriminator.
|
||||
LSGAN needs no sigmoid. vanilla GANs will handle it with BCEWithLogitsLoss.
|
||||
"""
|
||||
super(GANLoss, self).__init__()
|
||||
self.register_buffer('real_label', torch.tensor(target_real_label))
|
||||
self.register_buffer('fake_label', torch.tensor(target_fake_label))
|
||||
self.gan_mode = gan_mode
|
||||
if gan_mode == 'lsgan':
|
||||
self.loss = nn.MSELoss()
|
||||
elif gan_mode == 'vanilla':
|
||||
self.loss = nn.BCEWithLogitsLoss()
|
||||
elif gan_mode == 'hinge':
|
||||
pass
|
||||
elif gan_mode in ['wgangp']:
|
||||
self.loss = None
|
||||
else:
|
||||
raise NotImplementedError('gan mode %s not implemented' % gan_mode)
|
||||
|
||||
def get_target_tensor(self, prediction, target_is_real):
|
||||
if target_is_real:
|
||||
target_tensor = self.real_label
|
||||
else:
|
||||
target_tensor = self.fake_label
|
||||
return target_tensor.expand_as(prediction)
|
||||
|
||||
def __call__(self, prediction, target_is_real, for_discriminator=True):
|
||||
"""Calculate loss given Discriminator's output and grount truth labels.
|
||||
Parameters:
|
||||
prediction (tensor) - - tpyically the prediction output from a discriminator
|
||||
target_is_real (bool) - - if the ground truth label is for real images or fake images
|
||||
Returns:
|
||||
the calculated loss.
|
||||
"""
|
||||
if self.gan_mode in ['lsgan', 'vanilla']:
|
||||
target_tensor = self.get_target_tensor(prediction, target_is_real)
|
||||
loss = self.loss(prediction, target_tensor)
|
||||
elif self.gan_mode == 'hinge':
|
||||
if for_discriminator:
|
||||
if target_is_real:
|
||||
loss = nn.ReLU()(1 - prediction).mean()
|
||||
else:
|
||||
loss = nn.ReLU()(1 + prediction).mean()
|
||||
else:
|
||||
assert target_is_real, "The generator's hinge loss must be aiming for real"
|
||||
loss = - prediction.mean()
|
||||
return loss
|
||||
|
||||
elif self.gan_mode == 'wgangp':
|
||||
if target_is_real:
|
||||
loss = -prediction.mean()
|
||||
else:
|
||||
loss = prediction.mean()
|
||||
return loss
|
||||
|
||||
|
||||
class RegionStyleLoss(nn.Module):
|
||||
def __init__(self, reg_num=19, eps=1e-8):
|
||||
super().__init__()
|
||||
self.reg_num = reg_num
|
||||
self.eps = eps
|
||||
self.mse = nn.MSELoss()
|
||||
|
||||
def __masked_gram_matrix(self, x, m):
|
||||
b, c, h, w = x.shape
|
||||
m = m.view(b, -1, h*w)
|
||||
x = x.view(b, -1, h*w)
|
||||
total_elements = m.sum(2) + self.eps
|
||||
|
||||
x = x * m
|
||||
G = torch.bmm(x, x.transpose(1, 2))
|
||||
return G / (c * total_elements.view(b, 1, 1))
|
||||
|
||||
def __layer_gram_matrix(self, x, mask):
|
||||
b, c, h, w = x.shape
|
||||
all_gm = []
|
||||
for i in range(self.reg_num):
|
||||
sub_mask = mask[:, i].unsqueeze(1)
|
||||
gram_matrix = self.__masked_gram_matrix(x, sub_mask)
|
||||
all_gm.append(gram_matrix)
|
||||
return torch.stack(all_gm, dim=1)
|
||||
|
||||
def forward(self, x_feats, y_feats, mask):
|
||||
loss = 0
|
||||
for xf, yf in zip(x_feats[2:], y_feats[2:]):
|
||||
tmp_mask = torch.nn.functional.interpolate(mask, xf.shape[2:])
|
||||
xf_gm = self.__layer_gram_matrix(xf, tmp_mask)
|
||||
yf_gm = self.__layer_gram_matrix(yf, tmp_mask)
|
||||
tmp_loss = self.mse(xf_gm, yf_gm.detach())
|
||||
loss = loss + tmp_loss
|
||||
return loss
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
x = [
|
||||
torch.randn(2, 64, 512, 512),
|
||||
torch.randn(2, 128, 256, 256),
|
||||
torch.randn(2, 256, 128, 128),
|
||||
torch.randn(2, 512, 64, 64),
|
||||
torch.randn(2, 512, 32, 32),
|
||||
]
|
||||
|
||||
y = torch.randint(10, (2, 19, 512, 512)).float()
|
||||
loss = RegionStyleLoss()
|
||||
print(loss(x, x, y))
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
from models.blocks import *
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import init
|
||||
from torch.optim import lr_scheduler
|
||||
from utils import utils
|
||||
import numpy as np
|
||||
|
||||
from models import psfrnet
|
||||
import torch.nn.utils as tutils
|
||||
from models.loss import PCPFeat
|
||||
|
||||
|
||||
def apply_norm(net, weight_norm_type):
|
||||
for m in net.modules():
|
||||
if isinstance(m, nn.Conv2d):
|
||||
if weight_norm_type.lower() == 'spectral_norm':
|
||||
tutils.spectral_norm(m)
|
||||
elif weight_norm_type.lower() == 'weight_norm':
|
||||
tutils.weight_norm(m)
|
||||
else:
|
||||
pass
|
||||
|
||||
|
||||
def init_weights(net, init_type='normal', init_gain=0.02):
|
||||
"""Initialize network weights.
|
||||
Parameters:
|
||||
net (network) -- network to be initialized
|
||||
init_type (str) -- the name of an initialization method: normal | xavier | kaiming | orthogonal
|
||||
init_gain (float) -- scaling factor for normal, xavier and orthogonal.
|
||||
We use 'normal' in the original pix2pix and CycleGAN paper. But xavier and kaiming might
|
||||
work better for some applications. Feel free to try yourself.
|
||||
"""
|
||||
def init_func(m): # define the initialization function
|
||||
classname = m.__class__.__name__
|
||||
if hasattr(m, 'weight') and (classname.find('Conv') != -1 or classname.find('Linear') != -1):
|
||||
if init_type == 'normal':
|
||||
init.normal_(m.weight.data, 0.0, init_gain)
|
||||
elif init_type == 'xavier':
|
||||
init.xavier_normal_(m.weight.data, gain=init_gain)
|
||||
elif init_type == 'kaiming':
|
||||
init.kaiming_normal_(m.weight.data, a=0, mode='fan_in')
|
||||
elif init_type == 'orthogonal':
|
||||
init.orthogonal_(m.weight.data, gain=init_gain)
|
||||
else:
|
||||
raise NotImplementedError('initialization method [%s] is not implemented' % init_type)
|
||||
if hasattr(m, 'bias') and m.bias is not None:
|
||||
init.constant_(m.bias.data, 0.0)
|
||||
elif classname.find('BatchNorm2d') != -1: # BatchNorm Layer's weight is not a matrix; only normal distribution applies.
|
||||
init.normal_(m.weight.data, 1.0, init_gain)
|
||||
init.constant_(m.bias.data, 0.0)
|
||||
|
||||
print('initialize network with %s' % init_type)
|
||||
net.apply(init_func) # apply the initialization function <init_func>
|
||||
|
||||
|
||||
def init_net(net, init_type='normal', init_gain=0.02, gpu_ids=[]):
|
||||
"""Initialize a network: 1. register CPU/GPU device (with multi-GPU support); 2. initialize the network weights
|
||||
Parameters:
|
||||
net (network) -- the network to be initialized
|
||||
init_type (str) -- the name of an initialization method: normal | xavier | kaiming | orthogonal
|
||||
gain (float) -- scaling factor for normal, xavier and orthogonal.
|
||||
gpu_ids (int list) -- which GPUs the network runs on: e.g., 0,1,2
|
||||
Return an initialized network.
|
||||
"""
|
||||
if len(gpu_ids) > 0:
|
||||
assert(torch.cuda.is_available())
|
||||
net.to(gpu_ids[0])
|
||||
net = torch.nn.DataParallel(net, gpu_ids) # multi-GPUs
|
||||
init_weights(net, init_type, init_gain=init_gain)
|
||||
return net
|
||||
|
||||
|
||||
def get_scheduler(optimizer, opt):
|
||||
"""Return a learning rate scheduler
|
||||
Parameters:
|
||||
optimizer -- the optimizer of the network
|
||||
opt (option class) -- stores all the experiment flags; needs to be a subclass of BaseOptions.
|
||||
opt.lr_policy is the name of learning rate policy: linear | step | plateau | cosine
|
||||
For 'linear', we keep the same learning rate for the first <opt.n_epochs> epochs
|
||||
and linearly decay the rate to zero over the next <opt.n_epochs_decay> epochs.
|
||||
For other schedulers (step, plateau, and cosine), we use the default PyTorch schedulers.
|
||||
See https://pytorch.org/docs/stable/optim.html for more details.
|
||||
"""
|
||||
if opt.lr_policy == 'linear':
|
||||
def lambda_rule(epoch):
|
||||
lr_l = 1.0 - max(0, epoch + opt.epoch_count - opt.n_epochs) / float(opt.n_epochs_decay + 1)
|
||||
return lr_l
|
||||
scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lambda_rule)
|
||||
elif opt.lr_policy == 'step':
|
||||
scheduler = lr_scheduler.StepLR(optimizer, step_size=opt.lr_decay_iters, gamma=0.1)
|
||||
elif opt.lr_policy == 'plateau':
|
||||
scheduler = lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.2, threshold=0.01, patience=5)
|
||||
elif opt.lr_policy == 'cosine':
|
||||
scheduler = lr_scheduler.CosineAnnealingLR(optimizer, T_max=opt.n_epochs, eta_min=0)
|
||||
else:
|
||||
return NotImplementedError('learning rate policy [%s] is not implemented', opt.lr_policy)
|
||||
return scheduler
|
||||
|
||||
|
||||
def define_P(opt, in_size=512, out_size=512, min_feat_size=32, relu_type='LeakyReLU', isTrain=True, weight_path=None):
|
||||
net = ParseNet(in_size, out_size, min_feat_size, 64, 19, norm_type=opt.Pnorm, relu_type=relu_type, ch_range=[32, 256])
|
||||
if not isTrain:
|
||||
net.eval()
|
||||
if weight_path is not None:
|
||||
net.load_state_dict(torch.load(weight_path))
|
||||
if len(opt.gpu_ids) > 0:
|
||||
assert(torch.cuda.is_available())
|
||||
net.to(opt.device)
|
||||
net = torch.nn.DataParallel(net, opt.gpu_ids, output_device=opt.device)
|
||||
return net
|
||||
|
||||
|
||||
def define_G(opt, isTrain=True, use_norm='none', relu_type='LeakyReLU'):
|
||||
net = psfrnet.PSFRGenerator(3, 3, in_size=opt.Gin_size, out_size=opt.Gout_size, relu_type=relu_type, parse_ch=19, norm_type=opt.Gnorm)
|
||||
apply_norm(net, use_norm)
|
||||
if not isTrain:
|
||||
net.eval()
|
||||
if len(opt.gpu_ids) > 0:
|
||||
assert(torch.cuda.is_available())
|
||||
net.to(opt.device)
|
||||
net = torch.nn.DataParallel(net, opt.gpu_ids, output_device=opt.device)
|
||||
# init_weights(net, init_type='normal', init_gain=0.02)
|
||||
return net
|
||||
|
||||
|
||||
def define_D(opt, in_channel=3, isTrain=True, use_norm='none'):
|
||||
net = MultiScaleDiscriminator(in_channel, opt.ndf, opt.n_layers_D, opt.Dnorm, num_D=opt.D_num)
|
||||
apply_norm(net, use_norm)
|
||||
if not isTrain:
|
||||
net.eval()
|
||||
if len(opt.gpu_ids) > 0:
|
||||
assert(torch.cuda.is_available())
|
||||
net.to(opt.device)
|
||||
net = torch.nn.DataParallel(net, opt.gpu_ids, output_device=opt.device)
|
||||
init_weights(net, init_type='normal', init_gain=0.02)
|
||||
return net
|
||||
|
||||
|
||||
class ParseNet(nn.Module):
|
||||
def __init__(self,
|
||||
in_size=128,
|
||||
out_size=128,
|
||||
min_feat_size=32,
|
||||
base_ch=64,
|
||||
parsing_ch=19,
|
||||
res_depth=10,
|
||||
relu_type='prelu',
|
||||
norm_type='bn',
|
||||
ch_range=[32, 512],
|
||||
):
|
||||
super().__init__()
|
||||
self.res_depth = res_depth
|
||||
act_args = {'norm_type': norm_type, 'relu_type': relu_type}
|
||||
min_ch, max_ch = ch_range
|
||||
|
||||
ch_clip = lambda x: max(min_ch, min(x, max_ch))
|
||||
min_feat_size = min(in_size, min_feat_size)
|
||||
|
||||
down_steps = int(np.log2(in_size//min_feat_size))
|
||||
up_steps = int(np.log2(out_size//min_feat_size))
|
||||
|
||||
# =============== define encoder-body-decoder ====================
|
||||
self.encoder = []
|
||||
self.encoder.append(ConvLayer(3, base_ch, 3, 1))
|
||||
head_ch = base_ch
|
||||
for i in range(down_steps):
|
||||
cin, cout = ch_clip(head_ch), ch_clip(head_ch * 2)
|
||||
self.encoder.append(ResidualBlock(cin, cout, scale='down', **act_args))
|
||||
head_ch = head_ch * 2
|
||||
|
||||
self.body = []
|
||||
for i in range(res_depth):
|
||||
self.body.append(ResidualBlock(ch_clip(head_ch), ch_clip(head_ch), **act_args))
|
||||
|
||||
self.decoder = []
|
||||
for i in range(up_steps):
|
||||
cin, cout = ch_clip(head_ch), ch_clip(head_ch // 2)
|
||||
self.decoder.append(ResidualBlock(cin, cout, scale='up', **act_args))
|
||||
head_ch = head_ch // 2
|
||||
|
||||
self.encoder = nn.Sequential(*self.encoder)
|
||||
self.body = nn.Sequential(*self.body)
|
||||
self.decoder = nn.Sequential(*self.decoder)
|
||||
self.out_img_conv = ConvLayer(ch_clip(head_ch), 3)
|
||||
self.out_mask_conv = ConvLayer(ch_clip(head_ch), parsing_ch)
|
||||
|
||||
def forward(self, x):
|
||||
feat = self.encoder(x)
|
||||
x = feat + self.body(feat)
|
||||
x = self.decoder(x)
|
||||
out_img = self.out_img_conv(x)
|
||||
out_mask = self.out_mask_conv(x)
|
||||
return out_mask, out_img
|
||||
|
||||
|
||||
class MultiScaleDiscriminator(nn.Module):
|
||||
def __init__(self, input_ch, base_ch=64, n_layers=3, norm_type='none', relu_type='LeakyReLU', num_D=4):
|
||||
super().__init__()
|
||||
|
||||
self.D_pool = nn.ModuleList()
|
||||
for i in range(num_D):
|
||||
netD = NLayerDiscriminator(input_ch, base_ch, depth=n_layers, norm_type=norm_type, relu_type=relu_type)
|
||||
self.D_pool.append(netD)
|
||||
|
||||
self.downsample = nn.AvgPool2d(3, stride=2, padding=[1, 1], count_include_pad=False)
|
||||
|
||||
def forward(self, input, return_feat=False):
|
||||
results = []
|
||||
for netd in self.D_pool:
|
||||
output = netd(input, return_feat)
|
||||
results.append(output)
|
||||
# Downsample input
|
||||
input = self.downsample(input)
|
||||
return results
|
||||
|
||||
|
||||
class NLayerDiscriminator(nn.Module):
|
||||
def __init__(self,
|
||||
input_ch = 3,
|
||||
base_ch = 64,
|
||||
max_ch = 1024,
|
||||
depth = 4,
|
||||
norm_type = 'none',
|
||||
relu_type = 'LeakyReLU',
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
nargs = {'norm_type': norm_type, 'relu_type': relu_type}
|
||||
self.norm_type = norm_type
|
||||
self.input_ch = input_ch
|
||||
|
||||
self.model = []
|
||||
self.model.append(ConvLayer(input_ch, base_ch, norm_type='none', relu_type=relu_type))
|
||||
for i in range(depth):
|
||||
cin = min(base_ch * 2**(i), max_ch)
|
||||
cout = min(base_ch * 2**(i+1), max_ch)
|
||||
self.model.append(ConvLayer(cin, cout, scale='down_avg', **nargs))
|
||||
self.model = nn.Sequential(*self.model)
|
||||
self.score_out = ConvLayer(cout, 1, use_pad=False)
|
||||
|
||||
def forward(self, x, return_feat=False):
|
||||
ret_feats = []
|
||||
for idx, m in enumerate(self.model):
|
||||
x = m(x)
|
||||
ret_feats.append(x)
|
||||
x = self.score_out(x)
|
||||
if return_feat:
|
||||
return x, ret_feats
|
||||
else:
|
||||
return x
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import torch
|
||||
from .base_model import BaseModel
|
||||
from . import networks
|
||||
from utils import utils
|
||||
|
||||
class ParseModel(BaseModel):
|
||||
def modify_commandline_options(parser, is_train):
|
||||
if is_train:
|
||||
parser.add_argument('--parse_map', type=float, default=1.0, help='weight for parsing map')
|
||||
parser.add_argument('--parse_sr', type=float, default=1.0, help='weight for sr')
|
||||
return parser
|
||||
|
||||
def __init__(self, opt):
|
||||
"""Initialize this model class.
|
||||
|
||||
Parameters:
|
||||
opt -- training/test options
|
||||
|
||||
A few things can be done here.
|
||||
- (required) call the initialization function of BaseModel
|
||||
- define loss function, visualization images, model names, and optimizers
|
||||
"""
|
||||
BaseModel.__init__(self, opt) # call the initialization method of BaseModel
|
||||
self.loss_names = ['P', 'SR']
|
||||
self.visual_names = ['img_LR', 'img_HR', 'gt_Parse', 'img_SR', 'pred_Parse']
|
||||
|
||||
self.model_names = ['P']
|
||||
self.netP = networks.define_P(opt)
|
||||
|
||||
if self.isTrain: # only defined during training time
|
||||
self.criterionParse = torch.nn.CrossEntropyLoss()
|
||||
self.criterionSR = torch.nn.L1Loss()
|
||||
self.optimizer = torch.optim.Adam(self.netP.parameters(), lr=opt.lr, betas=(0.9, 0.999))
|
||||
self.optimizers = [self.optimizer]
|
||||
|
||||
def set_input(self, input, cur_iters=None):
|
||||
self.img_LR = input['LR'].to(self.opt.device)
|
||||
self.img_HR = input['HR'].to(self.opt.device)
|
||||
self.gt_Parse = input['Mask'].to(self.opt.device)
|
||||
if self.opt.debug:
|
||||
print('ParseNet input shape:', self.img_LR.shape, self.img_HR.shape, self.gt_Parse.shape)
|
||||
|
||||
def load_pretrain_models(self,):
|
||||
self.netP.eval()
|
||||
print('Loading pretrained LQ face parsing network from', self.opt.parse_net_weight)
|
||||
self.netP.load_state_dict(torch.load(self.opt.parse_net_weight))
|
||||
|
||||
def forward(self):
|
||||
self.pred_Parse, self.img_SR = self.netP(self.img_LR)
|
||||
if self.opt.debug:
|
||||
print('ParseNet output shape', self.pred_Parse.shape, self.img_SR.shape)
|
||||
|
||||
def backward(self):
|
||||
self.loss_P = self.criterionParse(self.pred_Parse, self.gt_Parse) * self.opt.parse_map
|
||||
self.loss_SR = self.criterionSR(self.img_SR, self.img_HR) * self.opt.parse_sr
|
||||
|
||||
loss = self.loss_P + self.loss_SR
|
||||
loss.backward()
|
||||
|
||||
def optimize_parameters(self):
|
||||
self.optimizer.zero_grad() # clear network G's existing gradients
|
||||
self.backward() # calculate gradients for network G
|
||||
self.optimizer.step()
|
||||
|
||||
def get_current_visuals(self, size=512):
|
||||
out = []
|
||||
visual_imgs = []
|
||||
out.append(utils.tensor_to_numpy(self.img_LR))
|
||||
out.append(utils.tensor_to_numpy(self.img_SR))
|
||||
out.append(utils.tensor_to_numpy(self.img_HR))
|
||||
out_imgs = [utils.batch_numpy_to_image(x, size) for x in out]
|
||||
|
||||
visual_imgs.append(out_imgs[0])
|
||||
visual_imgs.append(out_imgs[1])
|
||||
visual_imgs.append(utils.color_parse_map(self.pred_Parse))
|
||||
visual_imgs.append(utils.color_parse_map(self.gt_Parse.unsqueeze(1)))
|
||||
visual_imgs.append(out_imgs[2])
|
||||
|
||||
return visual_imgs
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.nn import init
|
||||
import numpy as np
|
||||
from models.blocks import *
|
||||
|
||||
|
||||
class SPADENorm(nn.Module):
|
||||
def __init__(self, norm_nc, ref_nc, norm_type='spade', ksz=3):
|
||||
super().__init__()
|
||||
|
||||
self.param_free_norm = nn.InstanceNorm2d(norm_nc, affine=False)
|
||||
mid_c = 64
|
||||
|
||||
self.norm_type = norm_type
|
||||
if norm_type == 'spade':
|
||||
self.conv1 = nn.Sequential(
|
||||
nn.Conv2d(ref_nc, mid_c, ksz, 1, ksz//2),
|
||||
nn.LeakyReLU(0.2, True),
|
||||
)
|
||||
self.gamma_conv = nn.Conv2d(mid_c, norm_nc, ksz, 1, ksz//2)
|
||||
self.beta_conv = nn.Conv2d(mid_c, norm_nc, ksz, 1, ksz//2)
|
||||
|
||||
def get_gamma_beta(self, x, conv, gamma_conv, beta_conv):
|
||||
act = conv(x)
|
||||
gamma = gamma_conv(act)
|
||||
beta = beta_conv(act)
|
||||
return gamma, beta
|
||||
|
||||
def forward(self, x, ref):
|
||||
normalized_input = self.param_free_norm(x)
|
||||
if x.shape[-1] != ref.shape[-1]:
|
||||
ref = nn.functional.interpolate(ref, x.shape[2:], mode='bicubic', align_corners=False)
|
||||
if self.norm_type == 'spade':
|
||||
gamma, beta = self.get_gamma_beta(ref, self.conv1, self.gamma_conv, self.beta_conv)
|
||||
return normalized_input * gamma + beta
|
||||
elif self.norm_type == 'in':
|
||||
return normalized_input
|
||||
|
||||
|
||||
class SPADEResBlock(nn.Module):
|
||||
def __init__(self, fin, fout, ref_nc, relu_type, norm_type='spade'):
|
||||
super().__init__()
|
||||
|
||||
fmiddle = min(fin, fout)
|
||||
self.conv_0 = nn.Conv2d(fin, fmiddle, kernel_size=3, padding=1)
|
||||
self.conv_1 = nn.Conv2d(fmiddle, fout, kernel_size=3, padding=1)
|
||||
|
||||
# define normalization layers
|
||||
self.norm_0 = SPADENorm(fmiddle, ref_nc, norm_type)
|
||||
self.norm_1 = SPADENorm(fmiddle, ref_nc, norm_type)
|
||||
self.relu = ReluLayer(fmiddle, relu_type)
|
||||
|
||||
def forward(self, x, ref):
|
||||
res = self.conv_0(self.relu(self.norm_0(x, ref)))
|
||||
res = self.conv_1(self.relu(self.norm_1(res, ref)))
|
||||
out = x + res
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class PSFRGenerator(nn.Module):
|
||||
def __init__(self, input_nc, output_nc, in_size=512, out_size=512, min_feat_size=16, ngf=64, n_blocks=9, parse_ch=19, relu_type='relu',
|
||||
ch_range=[32, 1024], norm_type='spade'):
|
||||
super().__init__()
|
||||
|
||||
min_ch, max_ch = ch_range
|
||||
ch_clip = lambda x: max(min_ch, min(x, max_ch))
|
||||
get_ch = lambda size: ch_clip(1024*16//size)
|
||||
|
||||
self.const_input = nn.Parameter(torch.randn(1, get_ch(min_feat_size), min_feat_size, min_feat_size))
|
||||
up_steps = int(np.log2(out_size//min_feat_size))
|
||||
self.up_steps = up_steps
|
||||
|
||||
ref_ch = 19+3
|
||||
|
||||
head_ch = get_ch(min_feat_size)
|
||||
head = [
|
||||
nn.Conv2d(head_ch, head_ch, kernel_size=3, padding=1),
|
||||
SPADEResBlock(head_ch, head_ch, ref_ch, relu_type, norm_type),
|
||||
]
|
||||
|
||||
body = []
|
||||
for i in range(up_steps):
|
||||
cin, cout = ch_clip(head_ch), ch_clip(head_ch // 2)
|
||||
body += [
|
||||
nn.Sequential(
|
||||
nn.Upsample(scale_factor=2),
|
||||
nn.Conv2d(cin, cout, kernel_size=3, padding=1),
|
||||
SPADEResBlock(cout, cout, ref_ch, relu_type, norm_type)
|
||||
)
|
||||
]
|
||||
head_ch = head_ch // 2
|
||||
|
||||
self.img_out = nn.Conv2d(ch_clip(head_ch), output_nc, kernel_size=3, padding=1)
|
||||
|
||||
self.head = nn.Sequential(*head)
|
||||
self.body = nn.Sequential(*body)
|
||||
self.upsample = nn.Upsample(scale_factor=2)
|
||||
|
||||
def forward_spade(self, net, x, ref):
|
||||
for m in net:
|
||||
x = self.forward_spade_m(m, x, ref)
|
||||
return x
|
||||
|
||||
def forward_spade_m(self, m, x, ref):
|
||||
if isinstance(m, SPADENorm) or isinstance(m, SPADEResBlock):
|
||||
x = m(x, ref)
|
||||
else:
|
||||
x = m(x)
|
||||
return x
|
||||
|
||||
def forward(self, x, ref):
|
||||
b, c, h, w = x.shape
|
||||
const_input = self.const_input.repeat(b, 1, 1, 1)
|
||||
ref_input = torch.cat((x, ref), dim=1)
|
||||
|
||||
feat = self.forward_spade(self.head, const_input, ref_input)
|
||||
|
||||
for idx, m in enumerate(self.body):
|
||||
feat = self.forward_spade(m, feat, ref_input)
|
||||
|
||||
out_img = self.img_out(feat)
|
||||
|
||||
return out_img
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
x = torch.randn(2, 16, 567, 234)
|
||||
nearest_interpolate(x)
|
||||
@@ -0,0 +1 @@
|
||||
"""This package options includes option modules: training options, test options, and basic options (used in both training and test)."""
|
||||
@@ -0,0 +1,165 @@
|
||||
import argparse
|
||||
import os
|
||||
import numpy as np
|
||||
import random
|
||||
from utils import utils
|
||||
import torch
|
||||
import models
|
||||
import data
|
||||
from utils import utils
|
||||
|
||||
|
||||
class BaseOptions():
|
||||
"""This class defines options used during both training and test time.
|
||||
|
||||
It also implements several helper functions such as parsing, printing, and saving the options.
|
||||
It also gathers additional options defined in <modify_commandline_options> functions in both dataset class and model class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Reset the class; indicates the class hasn't been initailized"""
|
||||
self.initialized = False
|
||||
|
||||
def initialize(self, parser):
|
||||
"""Define the common options that are used in both training and test."""
|
||||
# basic parameters
|
||||
parser.add_argument('--dataroot', required=False, help='path to images')
|
||||
parser.add_argument('--name', type=str, default='experiment_name', help='name of the experiment. It decides where to store samples and models')
|
||||
parser.add_argument('--gpus', type=int, default=1, help='how many gpus to use')
|
||||
parser.add_argument('--seed', type=int, default=123, help='Random seed for training')
|
||||
parser.add_argument('--checkpoints_dir', type=str, default='./check_points', help='models are saved here')
|
||||
# model parameters
|
||||
parser.add_argument('--model', type=str, default='enhance', help='chooses which model to train [parse|enhance]')
|
||||
parser.add_argument('--input_nc', type=int, default=3, help='# of input image channels: 3 for RGB and 1 for grayscale')
|
||||
parser.add_argument('--Dinput_nc', type=int, default=3, help='# of input image channels: 3 for RGB and 1 for grayscale')
|
||||
parser.add_argument('--output_nc', type=int, default=3, help='# of output image channels: 3 for RGB and 1 for grayscale')
|
||||
parser.add_argument('--ngf', type=int, default=64, help='# of gen filters in the last conv layer')
|
||||
parser.add_argument('--ndf', type=int, default=64, help='# of discrim filters in the first conv layer')
|
||||
parser.add_argument('--n_layers_D', type=int, default=4, help='downsampling layers in discriminator')
|
||||
parser.add_argument('--D_num', type=int, default=3, help='numbers of discriminators')
|
||||
|
||||
parser.add_argument('--Pnorm', type=str, default='bn', help='parsing net norm [in | bn| none]')
|
||||
parser.add_argument('--Gnorm', type=str, default='spade', help='generator norm [in | bn | none]')
|
||||
parser.add_argument('--Dnorm', type=str, default='in', help='discriminator norm [in | bn | none]')
|
||||
parser.add_argument('--init_type', type=str, default='normal', help='network initialization [normal | xavier | kaiming | orthogonal]')
|
||||
parser.add_argument('--init_gain', type=float, default=0.02, help='scaling factor for normal, xavier and orthogonal.')
|
||||
# dataset parameters
|
||||
parser.add_argument('--dataset_name', type=str, default='single', help='dataset name')
|
||||
parser.add_argument('--Pimg_size', type=int, default='512', help='image size for face parse net')
|
||||
parser.add_argument('--Gin_size', type=int, default='512', help='image size for face parse net')
|
||||
parser.add_argument('--Gout_size', type=int, default='512', help='image size for face parse net')
|
||||
parser.add_argument('--serial_batches', action='store_true', help='if true, takes images in order to make batches, otherwise takes them randomly')
|
||||
parser.add_argument('--num_threads', default=8, type=int, help='# threads for loading data')
|
||||
parser.add_argument('--batch_size', type=int, default=16, help='input batch size')
|
||||
parser.add_argument('--load_size', type=int, default=512, help='scale images to this size')
|
||||
parser.add_argument('--crop_size', type=int, default=256, help='then crop to this size')
|
||||
parser.add_argument('--max_dataset_size', type=int, default=float("inf"), help='Maximum number of samples allowed per dataset. If the dataset directory contains more than max_dataset_size, only a subset is loaded.')
|
||||
parser.add_argument('--preprocess', type=str, default='none', help='scaling and cropping of images at load time [resize_and_crop | crop | scale_width | scale_width_and_crop | none]')
|
||||
parser.add_argument('--no_flip', action='store_true', help='if specified, do not flip the images for data augmentation')
|
||||
# additional parameters
|
||||
parser.add_argument('--epoch', type=str, default='latest', help='which epoch to load? set to latest to use latest cached model')
|
||||
parser.add_argument('--load_iter', type=int, default='0', help='which iteration to load? if load_iter > 0, the code will load models by iter_[load_iter]; otherwise, the code will load models by [epoch]')
|
||||
parser.add_argument('--verbose', action='store_true', help='if specified, print more debugging information')
|
||||
parser.add_argument('--suffix', default='', type=str, help='customized suffix: opt.name = opt.name + suffix: e.g., {model}_{netG}_size{load_size}')
|
||||
|
||||
parser.add_argument('--debug', action='store_true', help='if specified, set to debug mode')
|
||||
self.initialized = True
|
||||
return parser
|
||||
|
||||
def gather_options(self):
|
||||
"""Initialize our parser with basic options(only once).
|
||||
Add additional model-specific and dataset-specific options.
|
||||
These options are defined in the <modify_commandline_options> function
|
||||
in model and dataset classes.
|
||||
"""
|
||||
if not self.initialized: # check if it has been initialized
|
||||
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
parser = self.initialize(parser)
|
||||
|
||||
# get the basic options
|
||||
opt, _ = parser.parse_known_args()
|
||||
|
||||
# modify model-related parser options
|
||||
model_name = opt.model
|
||||
model_option_setter = models.get_option_setter(model_name)
|
||||
parser = model_option_setter(parser, self.isTrain)
|
||||
opt, _ = parser.parse_known_args() # parse again with new defaults
|
||||
|
||||
# modify dataset-related parser options
|
||||
dataset_name = opt.dataset_name
|
||||
dataset_option_setter = data.get_option_setter(dataset_name)
|
||||
parser = dataset_option_setter(parser, self.isTrain)
|
||||
|
||||
# save and return the parser
|
||||
self.parser = parser
|
||||
return parser.parse_args()
|
||||
|
||||
def print_options(self, opt):
|
||||
"""Print and save options
|
||||
|
||||
It will print both current options and default values(if different).
|
||||
It will save options into a text file / [checkpoints_dir] / opt.txt
|
||||
"""
|
||||
message = ''
|
||||
message += '----------------- Options ---------------\n'
|
||||
for k, v in sorted(vars(opt).items()):
|
||||
comment = ''
|
||||
default = self.parser.get_default(k)
|
||||
if v != default:
|
||||
comment = '\t[default: %s]' % str(default)
|
||||
message += '{:>25}: {:<30}{}\n'.format(str(k), str(v), comment)
|
||||
message += '----------------- End -------------------'
|
||||
print(message)
|
||||
|
||||
# save to the disk
|
||||
opt.expr_dir = os.path.join(opt.checkpoints_dir, opt.name)
|
||||
utils.mkdirs(opt.expr_dir)
|
||||
file_name = os.path.join(opt.expr_dir, '{}_opt.txt'.format(opt.phase))
|
||||
with open(file_name, 'wt') as opt_file:
|
||||
opt_file.write(message)
|
||||
opt_file.write('\n')
|
||||
|
||||
opt.log_dir = os.path.join(opt.checkpoints_dir, 'log_dir')
|
||||
utils.mkdirs(opt.log_dir)
|
||||
opt.log_archive = os.path.join(opt.checkpoints_dir, 'log_archive')
|
||||
utils.mkdirs(opt.log_archive)
|
||||
|
||||
def parse(self):
|
||||
"""Parse our options, create checkpoints directory suffix, and set up gpu device."""
|
||||
opt = self.gather_options()
|
||||
opt.isTrain = self.isTrain # train or test
|
||||
|
||||
if opt.debug:
|
||||
opt.name = 'debug'
|
||||
opt.save_iter_freq = 1
|
||||
opt.save_latest_freq = 1
|
||||
opt.visual_freq = 1
|
||||
opt.print_freq = 1
|
||||
|
||||
# Find avaliable GPUs automatically
|
||||
if opt.gpus > 0:
|
||||
opt.gpu_ids = utils.get_gpu_memory_map()[1][:opt.gpus]
|
||||
if not isinstance(opt.gpu_ids, list):
|
||||
opt.gpu_ids = [opt.gpu_ids]
|
||||
torch.cuda.set_device(opt.gpu_ids[0])
|
||||
opt.device = torch.device('cuda:{}'.format(opt.gpu_ids[0 % opt.gpus]))
|
||||
opt.data_device = torch.device('cuda:{}'.format(opt.gpu_ids[1 % opt.gpus]))
|
||||
else:
|
||||
opt.gpu_ids = []
|
||||
opt.device = torch.device('cpu')
|
||||
|
||||
# set random seeds to ensure reproducibility
|
||||
np.random.seed(opt.seed)
|
||||
random.seed(opt.seed)
|
||||
torch.manual_seed(opt.seed)
|
||||
torch.cuda.manual_seed_all(opt.seed)
|
||||
|
||||
# process opt.suffix
|
||||
if opt.suffix:
|
||||
suffix = ('_' + opt.suffix.format(**vars(opt))) if opt.suffix != '' else ''
|
||||
opt.name = opt.name + suffix
|
||||
|
||||
self.print_options(opt)
|
||||
|
||||
self.opt = opt
|
||||
return self.opt
|
||||
@@ -0,0 +1,30 @@
|
||||
from .base_options import BaseOptions
|
||||
|
||||
|
||||
class TestOptions(BaseOptions):
|
||||
"""This class includes test options.
|
||||
|
||||
It also includes shared options defined in BaseOptions.
|
||||
"""
|
||||
|
||||
def initialize(self, parser):
|
||||
parser = BaseOptions.initialize(self, parser) # define shared options
|
||||
parser.add_argument('--src_dir', type=str, default='G:/VGGFace2-HQ/VGGface2_None_norm_512_true_bygfpgan/n000002', help='source directory containing test images')
|
||||
parser.add_argument('--save_masks_dir', type=str, default='../datasets/FFHQ/masks512', help='path to save parsing masks for FFHQ')
|
||||
parser.add_argument('--test_img_path', type=str, default='', help='path for single image test')
|
||||
parser.add_argument('--test_upscale', type=float, default=1, help='upsample scale for single image test')
|
||||
parser.add_argument('--ntest', type=int, default=float("inf"), help='# of test examples.')
|
||||
parser.add_argument('--results_dir', type=str, default='./results/', help='saves results here.')
|
||||
parser.add_argument('--aspect_ratio', type=float, default=1.0, help='aspect ratio of result images')
|
||||
parser.add_argument('--phase', type=str, default='test', help='train, val, test, etc')
|
||||
# Dropout and Batchnorm has different behavioir during training and test.
|
||||
parser.add_argument('--eval', action='store_true', help='use eval mode during test time.')
|
||||
parser.add_argument('--num_test', type=int, default=50, help='how many test images to run')
|
||||
parser.add_argument('--parse_net_weight', type=str, default='./pretrain_models/parse_multi_iter_90000.pth', help='parse model path')
|
||||
parser.add_argument('--psfr_net_weight', type=str, default='./pretrain_models/psfrgan_epoch15_net_G.pth', help='parse model path')
|
||||
# rewrite devalue values
|
||||
# To avoid cropping, the load_size should be the same as crop_size
|
||||
parser.set_defaults(load_size=parser.get_default('crop_size'))
|
||||
self.isTrain = False
|
||||
|
||||
return parser
|
||||
@@ -0,0 +1,41 @@
|
||||
from .base_options import BaseOptions
|
||||
|
||||
class TrainOptions(BaseOptions):
|
||||
"""This class includes training options.
|
||||
|
||||
It also includes shared options defined in BaseOptions.
|
||||
"""
|
||||
|
||||
def initialize(self, parser):
|
||||
parser = BaseOptions.initialize(self, parser)
|
||||
# visdom and HTML visualization parameters
|
||||
parser.add_argument('--visual_freq', type=int, default=400, help='frequency of show training images in tensorboard')
|
||||
parser.add_argument('--print_freq', type=int, default=100, help='frequency of showing training results on console')
|
||||
# network saving and loading parameters
|
||||
parser.add_argument('--save_iter_freq', type=int, default=5000, help='frequency of saving the models')
|
||||
parser.add_argument('--save_latest_freq', type=int, default=500, help='save latest freq')
|
||||
parser.add_argument('--save_epoch_freq', type=int, default=5, help='frequency of saving checkpoints at the end of epochs')
|
||||
parser.add_argument('--save_by_iter', action='store_true', help='whether saves model by iteration')
|
||||
parser.add_argument('--continue_train', action='store_true', help='continue training: load the latest model')
|
||||
parser.add_argument('--no_strict_load', action='store_true', help='set strict load to false')
|
||||
parser.add_argument('--epoch_count', type=int, default=1, help='the starting epoch count, we save the model by <epoch_count>, <epoch_count>+<save_latest_freq>, ...')
|
||||
parser.add_argument('--phase', type=str, default='train', help='train, val, test, etc')
|
||||
# training parameters
|
||||
parser.add_argument('--resume_epoch', type=int, default=0, help='training resume epoch')
|
||||
parser.add_argument('--resume_iter', type=int, default=0, help='training resume iter')
|
||||
parser.add_argument('--total_epochs', type=int, default=50, help='# of epochs to train')
|
||||
parser.add_argument('--n_epochs', type=int, default=100, help='number of epochs with the initial learning rate')
|
||||
parser.add_argument('--n_epochs_decay', type=int, default=100, help='number of epochs to linearly decay learning rate to zero')
|
||||
parser.add_argument('--niter_decay', type=int, default=100, help='# of iter to linearly decay learning rate to zero')
|
||||
parser.add_argument('--beta1', type=float, default=0.5, help='momentum term of adam')
|
||||
parser.add_argument('--lr', type=float, default=0.0002, help='initial learning rate for adam')
|
||||
parser.add_argument('--g_lr', type=float, default=0.0001, help='generator learning rate')
|
||||
parser.add_argument('--d_lr', type=float, default=0.0004, help='discriminator learning rate')
|
||||
parser.add_argument('--gan_mode', type=str, default='hinge', help='the type of GAN objective. [vanilla| lsgan | wgangp]. vanilla GAN loss is the cross-entropy objective used in the original GAN paper.')
|
||||
parser.add_argument('--lr_policy', type=str, default='step', help='learning rate policy. [linear | step | plateau | cosine]')
|
||||
parser.add_argument('--lr_decay_iters', type=int, default=50, help='multiply by a gamma every lr_decay_iters iterations')
|
||||
parser.add_argument('--lr_decay_gamma', type=float, default=1, help='multiply by a gamma every lr_decay_iters iterations')
|
||||
|
||||
self.isTrain = True
|
||||
|
||||
return parser
|
||||
@@ -0,0 +1,11 @@
|
||||
torch==1.5.1
|
||||
torchvision==0.6.1
|
||||
tensorflow>=1.15.4
|
||||
tensorboard==1.15.0
|
||||
tensorboardX==2.1
|
||||
opencv-python
|
||||
dlib
|
||||
scikit-image==0.17.2
|
||||
scipy==1.4.1
|
||||
tqdm
|
||||
imgaug
|
||||
@@ -0,0 +1,62 @@
|
||||
import os
|
||||
from options.test_options import TestOptions
|
||||
from data import create_dataset
|
||||
from models import create_model
|
||||
from utils import utils
|
||||
from PIL import Image
|
||||
from tqdm import tqdm
|
||||
import torch
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
if __name__ == '__main__':
|
||||
opt = TestOptions().parse() # get test options
|
||||
opt.num_threads = 0 # test code only supports num_threads = 1
|
||||
opt.batch_size = 4 # test code only supports batch_size = 1
|
||||
opt.serial_batches = True # disable data shuffling; comment this line if results on randomly chosen images are needed.
|
||||
opt.no_flip = True
|
||||
|
||||
dataset = create_dataset(opt) # create a dataset given opt.dataset_mode and other options
|
||||
model = create_model(opt) # create a model given opt.model and other options
|
||||
model.load_pretrain_models()
|
||||
|
||||
save_dir = opt.results_dir
|
||||
os.makedirs(save_dir, exist_ok=True)
|
||||
|
||||
print('creating result directory', save_dir)
|
||||
netP = model.netP
|
||||
netG = model.netG
|
||||
model.eval()
|
||||
max_size = 9999
|
||||
os.makedirs(os.path.join(save_dir, 'sr'), exist_ok=True)
|
||||
for i, data in tqdm(enumerate(dataset), total=len(dataset)//opt.batch_size):
|
||||
inp = data['LR']
|
||||
with torch.no_grad():
|
||||
parse_map, _ = netP(inp)
|
||||
parse_map_sm = (parse_map == parse_map.max(dim=1, keepdim=True)[0]).float()
|
||||
output_SR = netG(inp, parse_map_sm)
|
||||
img_path = data['LR_paths'] # get image paths
|
||||
for i in tqdm(range(len(img_path))):
|
||||
inp_img = utils.batch_tensor_to_img(inp)
|
||||
output_sr_img = utils.batch_tensor_to_img(output_SR)
|
||||
ref_parse_img = utils.color_parse_map(parse_map_sm)
|
||||
|
||||
save_path = os.path.join(save_dir, 'lq', os.path.basename(img_path[i]))
|
||||
os.makedirs(os.path.join(save_dir, 'lq'), exist_ok=True)
|
||||
save_img = Image.fromarray(inp_img[i])
|
||||
save_img.save(save_path)
|
||||
|
||||
save_path = os.path.join(save_dir, 'hq', os.path.basename(img_path[i]))
|
||||
os.makedirs(os.path.join(save_dir, 'hq'), exist_ok=True)
|
||||
save_img = Image.fromarray(output_sr_img[i])
|
||||
save_img.save(save_path)
|
||||
|
||||
save_path = os.path.join(save_dir, 'parse', os.path.basename(img_path[i]))
|
||||
os.makedirs(os.path.join(save_dir, 'parse'), exist_ok=True)
|
||||
save_img = Image.fromarray(ref_parse_img[i])
|
||||
save_img.save(save_path)
|
||||
|
||||
if i > max_size: break
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'''
|
||||
This script enhance images with unaligned faces in a folder and paste it back to the original place.
|
||||
'''
|
||||
import dlib
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
from skimage import transform as trans
|
||||
from skimage import io
|
||||
|
||||
import torch
|
||||
from utils import utils
|
||||
from options.test_options import TestOptions
|
||||
from models import create_model
|
||||
|
||||
from test_enhance_single_unalign import *
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opt = TestOptions().parse()
|
||||
# face_detector = dlib.get_frontal_face_detector()
|
||||
face_detector = dlib.cnn_face_detection_model_v1('./pretrain_models/mmod_human_face_detector.dat')
|
||||
lmk_predictor = dlib.shape_predictor('./pretrain_models/shape_predictor_5_face_landmarks.dat')
|
||||
template_path = './pretrain_models/FFHQ_template.npy'
|
||||
enhance_model = def_models(opt)
|
||||
|
||||
for img_name in os.listdir(opt.src_dir):
|
||||
img_path = os.path.join(opt.src_dir, img_name)
|
||||
save_current_dir = os.path.join(opt.results_dir, os.path.splitext(img_name)[0])
|
||||
os.makedirs(save_current_dir, exist_ok=True)
|
||||
print('======> Loading image', img_path)
|
||||
img = dlib.load_rgb_image(img_path)
|
||||
aligned_faces, tform_params = detect_and_align_faces(img, face_detector, lmk_predictor, template_path)
|
||||
# Save aligned LQ faces
|
||||
save_lq_dir = os.path.join(save_current_dir, 'LQ_faces')
|
||||
os.makedirs(save_lq_dir, exist_ok=True)
|
||||
print('======> Saving aligned LQ faces to', save_lq_dir)
|
||||
save_imgs(aligned_faces, save_lq_dir)
|
||||
|
||||
hq_faces, lq_parse_maps = enhance_faces(aligned_faces, enhance_model)
|
||||
# Save LQ parsing maps and enhanced faces
|
||||
save_parse_dir = os.path.join(save_current_dir, 'ParseMaps')
|
||||
save_hq_dir = os.path.join(save_current_dir, 'HQ')
|
||||
os.makedirs(save_parse_dir, exist_ok=True)
|
||||
os.makedirs(save_hq_dir, exist_ok=True)
|
||||
print('======> Save parsing map and the enhanced faces.')
|
||||
save_imgs(lq_parse_maps, save_parse_dir)
|
||||
save_imgs(hq_faces, save_hq_dir)
|
||||
|
||||
print('======> Paste the enhanced faces back to the original image.')
|
||||
hq_img = past_faces_back(img, hq_faces, tform_params, upscale=opt.test_upscale)
|
||||
final_save_path = os.path.join(save_current_dir, 'hq_final.jpg')
|
||||
print('======> Save final result to', final_save_path)
|
||||
io.imsave(final_save_path, hq_img)
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
'''
|
||||
This script enhance all faces in one image with PSFR-GAN and paste it back to the original place.
|
||||
'''
|
||||
import dlib
|
||||
import os
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
from skimage import transform as trans
|
||||
from skimage import io
|
||||
|
||||
import torch
|
||||
from utils import utils
|
||||
from options.test_options import TestOptions
|
||||
from models import create_model
|
||||
|
||||
|
||||
def detect_and_align_faces(img, face_detector, lmk_predictor, template_path, template_scale=2, size_threshold=999):
|
||||
align_out_size = (512, 512)
|
||||
ref_points = np.load(template_path) / template_scale
|
||||
|
||||
# Detect landmark points
|
||||
face_dets = face_detector(img, 1)
|
||||
assert len(face_dets) > 0, 'No faces detected'
|
||||
|
||||
aligned_faces = []
|
||||
tform_params = []
|
||||
for det in face_dets:
|
||||
if isinstance(face_detector, dlib.cnn_face_detection_model_v1):
|
||||
rec = det.rect # for cnn detector
|
||||
else:
|
||||
rec = det
|
||||
if rec.width() > size_threshold or rec.height() > size_threshold:
|
||||
print('Face is too large')
|
||||
break
|
||||
landmark_points = lmk_predictor(img, rec)
|
||||
single_points = []
|
||||
for i in range(5):
|
||||
single_points.append([landmark_points.part(i).x, landmark_points.part(i).y])
|
||||
single_points = np.array(single_points)
|
||||
tform = trans.SimilarityTransform()
|
||||
tform.estimate(single_points, ref_points)
|
||||
tmp_face = trans.warp(img, tform.inverse, output_shape=align_out_size, order=3)
|
||||
aligned_faces.append(tmp_face*255)
|
||||
tform_params.append(tform)
|
||||
return [aligned_faces, tform_params]
|
||||
|
||||
|
||||
def def_models(opt):
|
||||
model = create_model(opt)
|
||||
model.load_pretrain_models()
|
||||
model.netP.to(opt.device)
|
||||
model.netG.to(opt.device)
|
||||
return model
|
||||
|
||||
|
||||
def enhance_faces(LQ_faces, model):
|
||||
hq_faces = []
|
||||
lq_parse_maps = []
|
||||
for lq_face in tqdm(LQ_faces):
|
||||
with torch.no_grad():
|
||||
lq_tensor = torch.tensor(lq_face.transpose(2, 0, 1)) / 255. * 2 - 1
|
||||
lq_tensor = lq_tensor.unsqueeze(0).float().to(model.device)
|
||||
parse_map, _ = model.netP(lq_tensor)
|
||||
parse_map_onehot = (parse_map == parse_map.max(dim=1, keepdim=True)[0]).float()
|
||||
output_SR = model.netG(lq_tensor, parse_map_onehot)
|
||||
hq_faces.append(utils.tensor_to_img(output_SR))
|
||||
lq_parse_maps.append(utils.color_parse_map(parse_map_onehot)[0])
|
||||
return hq_faces, lq_parse_maps
|
||||
|
||||
|
||||
def past_faces_back(img, hq_faces, tform_params, upscale=1):
|
||||
h, w = img.shape[:2]
|
||||
img = cv2.resize(img, (int(w*upscale), int(h*upscale)), interpolation=cv2.INTER_CUBIC)
|
||||
for hq_img, tform in tqdm(zip(hq_faces, tform_params), total=len(hq_faces)):
|
||||
tform.params[0:2,0:2] /= upscale
|
||||
back_img = trans.warp(hq_img/255., tform, output_shape=[int(h*upscale), int(w*upscale)], order=3) * 255
|
||||
|
||||
# blur mask to avoid border artifacts
|
||||
mask = (back_img == 0)
|
||||
mask = cv2.blur(mask.astype(np.float32), (5,5))
|
||||
mask = (mask > 0)
|
||||
img = img * mask + (1 - mask) * back_img
|
||||
return img.astype(np.uint8)
|
||||
|
||||
|
||||
def save_imgs(img_list, save_dir):
|
||||
for idx, img in enumerate(img_list):
|
||||
save_path = os.path.join(save_dir, '{:03d}.jpg'.format(idx))
|
||||
io.imsave(save_path, img.astype(np.uint8))
|
||||
|
||||
if __name__ == '__main__':
|
||||
opt = TestOptions().parse()
|
||||
# face_detector = dlib.get_frontal_face_detector()
|
||||
face_detector = dlib.cnn_face_detection_model_v1('./pretrain_models/mmod_human_face_detector.dat')
|
||||
lmk_predictor = dlib.shape_predictor('./pretrain_models/shape_predictor_5_face_landmarks.dat')
|
||||
template_path = './pretrain_models/FFHQ_template.npy'
|
||||
|
||||
print('======> Loading images, crop and align faces.')
|
||||
img_path = opt.test_img_path
|
||||
img = dlib.load_rgb_image(img_path)
|
||||
aligned_faces, tform_params = detect_and_align_faces(img, face_detector, lmk_predictor, template_path)
|
||||
# Save aligned LQ faces
|
||||
save_lq_dir = os.path.join(opt.results_dir, 'LQ_faces')
|
||||
os.makedirs(save_lq_dir, exist_ok=True)
|
||||
print('======> Saving aligned LQ faces to', save_lq_dir)
|
||||
save_imgs(aligned_faces, save_lq_dir)
|
||||
|
||||
enhance_model = def_models(opt)
|
||||
hq_faces, lq_parse_maps = enhance_faces(aligned_faces, enhance_model)
|
||||
# Save LQ parsing maps and enhanced faces
|
||||
save_parse_dir = os.path.join(opt.results_dir, 'ParseMaps')
|
||||
save_hq_dir = os.path.join(opt.results_dir, 'HQ')
|
||||
os.makedirs(save_parse_dir, exist_ok=True)
|
||||
os.makedirs(save_hq_dir, exist_ok=True)
|
||||
print('======> Save parsing map and the enhanced faces.')
|
||||
save_imgs(lq_parse_maps, save_parse_dir)
|
||||
save_imgs(hq_faces, save_hq_dir)
|
||||
|
||||
print('======> Paste the enhanced faces back to the original image.')
|
||||
hq_img = past_faces_back(img, hq_faces, tform_params, upscale=opt.test_upscale)
|
||||
final_save_path = os.path.join(opt.results_dir, 'hq_final.jpg')
|
||||
print('======> Save final result to', final_save_path)
|
||||
io.imsave(final_save_path, hq_img)
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from utils.timer import Timer
|
||||
from utils.logger import Logger
|
||||
from utils import utils
|
||||
|
||||
from options.train_options import TrainOptions
|
||||
from data import create_dataset
|
||||
from models import create_model
|
||||
|
||||
import torch
|
||||
import os
|
||||
import torch.multiprocessing as mp
|
||||
|
||||
def train(opt):
|
||||
|
||||
dataset = create_dataset(opt) # create a dataset given opt.dataset_mode and other options
|
||||
dataset_size = len(dataset) # get the number of images in the dataset.
|
||||
print('The number of training images = %d' % dataset_size)
|
||||
|
||||
model = create_model(opt)
|
||||
model.setup(opt)
|
||||
|
||||
logger = Logger(opt)
|
||||
timer = Timer()
|
||||
|
||||
single_epoch_iters = (dataset_size // opt.batch_size)
|
||||
total_iters = opt.total_epochs * single_epoch_iters
|
||||
cur_iters = opt.resume_iter + opt.resume_epoch * single_epoch_iters
|
||||
start_iter = opt.resume_iter
|
||||
print('Start training from epoch: {:05d}; iter: {:07d}'.format(opt.resume_epoch, opt.resume_iter))
|
||||
for epoch in range(opt.resume_epoch, opt.total_epochs + 1):
|
||||
for i, data in enumerate(dataset, start=start_iter):
|
||||
cur_iters += 1
|
||||
logger.set_current_iter(cur_iters)
|
||||
# =================== load data ===============
|
||||
model.set_input(data, cur_iters)
|
||||
timer.update_time('DataTime')
|
||||
|
||||
# =================== model train ===============
|
||||
model.forward(), timer.update_time('Forward')
|
||||
model.optimize_parameters()
|
||||
loss = model.get_current_losses()
|
||||
loss.update(model.get_lr())
|
||||
logger.record_losses(loss)
|
||||
timer.update_time('Backward')
|
||||
|
||||
# =================== save model and visualize ===============
|
||||
if cur_iters % opt.print_freq == 0:
|
||||
print('Model log directory: {}'.format(opt.expr_dir))
|
||||
epoch_progress = '{:03d}|{:05d}/{:05d}'.format(epoch, i, single_epoch_iters)
|
||||
logger.printIterSummary(epoch_progress, cur_iters, total_iters, timer)
|
||||
|
||||
if cur_iters % opt.visual_freq == 0:
|
||||
visual_imgs = model.get_current_visuals()
|
||||
logger.record_images(visual_imgs)
|
||||
|
||||
if cur_iters % opt.save_iter_freq == 0:
|
||||
print('saving current model (epoch %d, iters %d)' % (epoch, cur_iters))
|
||||
save_suffix = 'iter_%d' % cur_iters
|
||||
info = {'resume_epoch': epoch, 'resume_iter': i+1}
|
||||
model.save_networks(save_suffix, info)
|
||||
|
||||
if cur_iters % opt.save_latest_freq == 0:
|
||||
print('saving the latest model (epoch %d, iters %d)' % (epoch, cur_iters))
|
||||
info = {'resume_epoch': epoch, 'resume_iter': i+1}
|
||||
model.save_networks('latest', info)
|
||||
|
||||
if i >= single_epoch_iters - 1:
|
||||
start_iter = 0
|
||||
break
|
||||
|
||||
# model.update_learning_rate()
|
||||
if opt.debug: break
|
||||
if opt.debug and epoch >= 0: break
|
||||
logger.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
opt = TrainOptions().parse()
|
||||
train(opt)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
import numpy as np
|
||||
from .utils import mkdirs
|
||||
from tensorboardX import SummaryWriter
|
||||
from datetime import datetime
|
||||
import socket
|
||||
import shutil
|
||||
|
||||
class Logger():
|
||||
def __init__(self, opts):
|
||||
time_stamp = '_{}'.format(datetime.now().strftime('%Y-%m-%d_%H:%M'))
|
||||
self.opts = opts
|
||||
self.log_dir = os.path.join(opts.log_dir, opts.name+time_stamp)
|
||||
self.phase_keys = ['train', 'val', 'test']
|
||||
self.iter_log = []
|
||||
self.epoch_log = OrderedDict()
|
||||
self.set_mode(opts.phase)
|
||||
|
||||
# check if exist previous log belong to the same experiment name
|
||||
exist_log = None
|
||||
for log_name in os.listdir(opts.log_dir):
|
||||
if opts.name in log_name:
|
||||
exist_log = log_name
|
||||
if exist_log is not None:
|
||||
old_dir = os.path.join(opts.log_dir, exist_log)
|
||||
archive_dir = os.path.join(opts.log_archive, exist_log)
|
||||
shutil.move(old_dir, archive_dir)
|
||||
|
||||
self.mk_log_file()
|
||||
|
||||
self.writer = SummaryWriter(self.log_dir)
|
||||
|
||||
def mk_log_file(self):
|
||||
mkdirs(self.log_dir)
|
||||
self.txt_files = OrderedDict()
|
||||
for i in self.phase_keys:
|
||||
self.txt_files[i] = os.path.join(self.log_dir, 'log_{}'.format(i))
|
||||
|
||||
def set_mode(self, mode):
|
||||
self.mode = mode
|
||||
self.epoch_log[mode] = []
|
||||
|
||||
def set_current_iter(self, cur_iter):
|
||||
self.cur_iter = cur_iter
|
||||
|
||||
def record_losses(self, items):
|
||||
"""
|
||||
iteration log: [iter][{key: value}]
|
||||
"""
|
||||
self.iter_log.append(items)
|
||||
for k, v in items.items():
|
||||
if 'loss' in k.lower():
|
||||
self.writer.add_scalar('loss/{}'.format(k), v, self.cur_iter)
|
||||
|
||||
def record_scalar(self, items):
|
||||
"""
|
||||
Add scalar records. item, {key: value}
|
||||
"""
|
||||
for i in items.keys():
|
||||
self.writer.add_scalar('{}'.format(i), items[i], self.cur_iter)
|
||||
|
||||
def record_images(self, visuals, nrow=6, tag='ckpt_image'):
|
||||
imgs = []
|
||||
max_len = visuals[0].shape[0]
|
||||
for i in range(nrow):
|
||||
if i >= max_len: continue
|
||||
tmp_imgs = [x[i] for x in visuals]
|
||||
imgs.append(np.hstack(tmp_imgs))
|
||||
imgs = np.vstack(imgs).astype(np.uint8)
|
||||
self.writer.add_image(tag, imgs, self.cur_iter, dataformats='HWC')
|
||||
|
||||
def record_text(self, tag, text):
|
||||
self.writer.add_text(tag, text)
|
||||
|
||||
def printIterSummary(self, epoch, cur_iters, total_it, timer):
|
||||
msg = '{}\nIter: [{}]{:03d}/{:03d}\t\t'.format(
|
||||
timer.to_string(total_it - cur_iters), epoch, cur_iters, total_it)
|
||||
for k, v in self.iter_log[-1].items():
|
||||
msg += '{}: {:.6f}\t'.format(k, v)
|
||||
print(msg + '\n')
|
||||
with open(self.txt_files[self.mode], 'a+') as f:
|
||||
f.write(msg + '\n')
|
||||
|
||||
def close(self):
|
||||
self.writer.export_scalars_to_json(os.path.join(self.log_dir, 'all_scalars.json'))
|
||||
self.writer.close()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import time
|
||||
import datetime
|
||||
from collections import OrderedDict
|
||||
|
||||
class Timer():
|
||||
def __init__(self):
|
||||
self.reset_timer()
|
||||
self.start = time.time()
|
||||
|
||||
def reset_timer(self):
|
||||
self.before = time.time()
|
||||
self.timer = OrderedDict()
|
||||
|
||||
def restart(self):
|
||||
self.before = time.time()
|
||||
|
||||
def update_time(self, key):
|
||||
self.timer[key] = time.time() - self.before
|
||||
self.before = time.time()
|
||||
|
||||
def to_string(self, iters_left, short=False):
|
||||
iter_total = sum(self.timer.values())
|
||||
msg = "{:%Y-%m-%d %H:%M:%S}\tElapse: {}\tTimeLeft: {}\t".format(
|
||||
datetime.datetime.now(),
|
||||
datetime.timedelta(seconds=round(time.time() - self.start)),
|
||||
datetime.timedelta(seconds=round(iter_total*iters_left))
|
||||
)
|
||||
if short:
|
||||
msg += '{}: {:.2f}s'.format('|'.join(self.timer.keys()), iter_total)
|
||||
else:
|
||||
msg += '\tIterTotal: {:.2f}s\t{}: {} '.format(iter_total,
|
||||
'|'.join(self.timer.keys()), ' '.join('{:.2f}s'.format(x) for x in self.timer.values()))
|
||||
return msg
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2 as cv
|
||||
from skimage import io
|
||||
from PIL import Image
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
# MASK_COLORMAP = [[0, 0, 0], [204, 0, 0], [76, 153, 0], [204, 204, 0], [51, 51, 255], [204, 0, 204], [0, 255, 255], [255, 204, 204], [102, 51, 0], [255, 0, 0], [102, 204, 0], [255, 255, 0], [0, 0, 153], [0, 0, 204], [255, 51, 153], [0, 204, 204], [0, 51, 0], [255, 153, 51], [0, 204, 0]]
|
||||
MASK_COLORMAP = [[0, 0, 0], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [255, 255, 255], [0,0, 0], [0, 0, 0], [255, 255, 255], [255, 255, 255], [255, 255, 255], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
|
||||
label_list = ['skin', 'nose', 'eye_g', 'l_eye', 'r_eye', 'l_brow', 'r_brow', 'l_ear', 'r_ear', 'mouth', 'u_lip', 'l_lip', 'hair', 'hat', 'ear_r', 'neck_l', 'neck', 'cloth']
|
||||
|
||||
def array_to_heatmap(x):
|
||||
x = (x - x.min()) / (x.max() - x.min()) * 255
|
||||
x = x.astype(np.uint8)
|
||||
return cv.applyColorMap(x.astype(np.uint8), cv.COLORMAP_RAINBOW)
|
||||
|
||||
def img_to_tensor(img_path, device, size=None, mode='rgb'):
|
||||
"""
|
||||
Read image from img_path, and convert to (C, H, W) tensor in range [-1, 1]
|
||||
"""
|
||||
img = Image.open(img_path).convert('RGB')
|
||||
img = np.array(img)
|
||||
if mode=='bgr':
|
||||
img = img[..., ::-1]
|
||||
if size:
|
||||
img = cv.resize(img, size)
|
||||
img = img / 255 * 2 - 1
|
||||
img_tensor = torch.from_numpy(img.transpose(2, 0, 1)).unsqueeze(0).to(device)
|
||||
return img_tensor.float()
|
||||
|
||||
def tensor_to_img(tensor, save_path=None, size=None, mode='RGB', normal=[-1, 1]):
|
||||
"""
|
||||
mode: RGB or L (gray image)
|
||||
Input: tensor with shape (C, H, W)
|
||||
Output: PIL Image
|
||||
"""
|
||||
if isinstance(size, int):
|
||||
size = (size, size)
|
||||
img_array = tensor.squeeze().data.cpu().numpy()
|
||||
if mode == 'RGB':
|
||||
img_array = img_array.transpose(1, 2, 0)
|
||||
|
||||
if size is not None:
|
||||
img_array = cv.resize(img_array, size, interpolation=cv.INTER_LINEAR)
|
||||
|
||||
if len(normal):
|
||||
img_array = (img_array - normal[0]) / (normal[1] - normal[0]) * 255
|
||||
img_array = img_array.clip(0, 255)
|
||||
|
||||
img_array = img_array.astype(np.uint8)
|
||||
if save_path:
|
||||
img = Image.fromarray(img_array, mode)
|
||||
img.save(save_path)
|
||||
|
||||
return img_array
|
||||
|
||||
def tensor_to_numpy(tensor):
|
||||
return tensor.data.cpu().numpy()
|
||||
|
||||
def batch_numpy_to_image(array, size=None):
|
||||
"""
|
||||
Input: numpy array (B, C, H, W) in [-1, 1]
|
||||
"""
|
||||
if isinstance(size, int):
|
||||
size = (size, size)
|
||||
|
||||
out_imgs = []
|
||||
array = np.clip((array + 1)/2 * 255, 0, 255)
|
||||
array = np.transpose(array, (0, 2, 3, 1))
|
||||
for i in range(array.shape[0]):
|
||||
if size is not None:
|
||||
tmp_array = cv.resize(array[i], size)
|
||||
else:
|
||||
tmp_array = array[i]
|
||||
out_imgs.append(tmp_array)
|
||||
return np.array(out_imgs).astype(np.uint8)
|
||||
|
||||
def batch_tensor_to_img(tensor, size=None):
|
||||
"""
|
||||
Input: (B, C, H, W)
|
||||
Return: RGB image, [0, 255]
|
||||
"""
|
||||
arrays = tensor_to_numpy(tensor)
|
||||
out_imgs = batch_numpy_to_image(arrays, size)
|
||||
return out_imgs
|
||||
|
||||
def color_parse_map(tensor, size=None):
|
||||
"""
|
||||
input: tensor or batch tensor
|
||||
return: colorized parsing maps
|
||||
"""
|
||||
if len(tensor.shape) < 4:
|
||||
tensor = tensor.unsqueeze(0)
|
||||
if tensor.shape[1] > 1:
|
||||
tensor = tensor.argmax(dim=1)
|
||||
|
||||
tensor = tensor.squeeze(1).data.cpu().numpy()
|
||||
color_maps = []
|
||||
for t in tensor:
|
||||
tmp_img = np.zeros(tensor.shape[1:] + (3,))
|
||||
for idx, color in enumerate(MASK_COLORMAP):
|
||||
tmp_img[t == idx] = color
|
||||
if size is not None:
|
||||
tmp_img = cv.resize(tmp_img, (size, size))
|
||||
color_maps.append(tmp_img.astype(np.uint8))
|
||||
return color_maps
|
||||
|
||||
def onehot_parse_map(img):
|
||||
"""
|
||||
input: RGB color parse map
|
||||
output: one hot encoding of parse map
|
||||
"""
|
||||
n_label = len(MASK_COLORMAP)
|
||||
img = np.array(img, dtype=np.uint8)
|
||||
h, w = img.shape[:2]
|
||||
onehot_label = np.zeros((n_label, h, w))
|
||||
colormap = np.array(MASK_COLORMAP).reshape(n_label, 1, 1, 3)
|
||||
colormap = np.tile(colormap, (1, h, w, 1))
|
||||
for idx, color in enumerate(MASK_COLORMAP):
|
||||
tmp_label = colormap[idx] == img
|
||||
onehot_label[idx] = tmp_label[..., 0] * tmp_label[..., 1] * tmp_label[..., 2]
|
||||
return onehot_label
|
||||
|
||||
|
||||
def mkdirs(paths):
|
||||
if isinstance(paths, list) and not isinstance(paths, str):
|
||||
for path in paths:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
else:
|
||||
if not os.path.exists(paths):
|
||||
os.makedirs(paths)
|
||||
|
||||
|
||||
def get_gpu_memory_map():
|
||||
"""Get the current gpu usage within visible cuda devices.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Memory Map: dict
|
||||
Keys are device ids as integers.
|
||||
Values are memory usage as integers in MB.
|
||||
Device Ids: gpu ids sorted in descending order according to the available memory.
|
||||
"""
|
||||
result = subprocess.check_output(
|
||||
[
|
||||
'nvidia-smi', '--query-gpu=memory.used',
|
||||
'--format=csv,nounits,noheader'
|
||||
]).decode('utf-8')
|
||||
# Convert lines into a dictionary
|
||||
gpu_memory = np.array([int(x) for x in result.strip().split('\n')])
|
||||
if 'CUDA_VISIBLE_DEVICES' in os.environ:
|
||||
visible_devices = sorted([int(x) for x in os.environ['CUDA_VISIBLE_DEVICES'].split(',')])
|
||||
else:
|
||||
visible_devices = range(len(gpu_memory))
|
||||
gpu_memory_map = dict(zip(range(len(visible_devices)), gpu_memory[visible_devices]))
|
||||
return gpu_memory_map, sorted(gpu_memory_map, key=gpu_memory_map.get)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
hm = torch.randn(32, 68, 128, 128).cuda()
|
||||
flip(hm, 2)
|
||||
x = torch.ones(32, 68)
|
||||
y = torch.ones(32, 68)
|
||||
print(get_gpu_memory_map())
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user