aj commited on
Commit
0ed3d85
·
1 Parent(s): 19c0fb7

Added new dual style gann= files

Browse files
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ shape_predictor_68_face_landmarks.dat*
.gitmodules ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [submodule "DualStyleGAN"]
2
+ path = DualStyleGAN
3
+ url = https://github.com/williamyang1991/DualStyleGAN
.pre-commit-config.yaml ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v4.5.0
4
+ hooks:
5
+ - id: check-executables-have-shebangs
6
+ - id: check-json
7
+ - id: check-merge-conflict
8
+ - id: check-shebang-scripts-are-executable
9
+ - id: check-toml
10
+ - id: check-yaml
11
+ - id: end-of-file-fixer
12
+ - id: mixed-line-ending
13
+ args: ["--fix=lf"]
14
+ - id: requirements-txt-fixer
15
+ - id: trailing-whitespace
16
+ - repo: https://github.com/myint/docformatter
17
+ rev: v1.7.5
18
+ hooks:
19
+ - id: docformatter
20
+ args: ["--in-place"]
21
+ - repo: https://github.com/pycqa/isort
22
+ rev: 5.13.2
23
+ hooks:
24
+ - id: isort
25
+ args: ["--profile", "black"]
26
+ - repo: https://github.com/pre-commit/mirrors-mypy
27
+ rev: v1.8.0
28
+ hooks:
29
+ - id: mypy
30
+ args: ["--ignore-missing-imports"]
31
+ additional_dependencies:
32
+ [
33
+ "types-python-slugify",
34
+ "types-requests",
35
+ "types-PyYAML",
36
+ "types-pytz",
37
+ ]
38
+ - repo: https://github.com/psf/black
39
+ rev: 24.2.0
40
+ hooks:
41
+ - id: black
42
+ language_version: python3.10
43
+ args: ["--line-length", "119"]
44
+ - repo: https://github.com/kynan/nbstripout
45
+ rev: 0.7.1
46
+ hooks:
47
+ - id: nbstripout
48
+ args:
49
+ [
50
+ "--extra-keys",
51
+ "metadata.interpreter metadata.kernelspec cell.metadata.pycharm",
52
+ ]
53
+ - repo: https://github.com/nbQA-dev/nbQA
54
+ rev: 1.7.1
55
+ hooks:
56
+ - id: nbqa-black
57
+ - id: nbqa-pyupgrade
58
+ args: ["--py37-plus"]
59
+ - id: nbqa-isort
60
+ args: ["--float-to-top"]
.vscode/settings.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "files.insertFinalNewline": false,
4
+ "[python]": {
5
+ "editor.defaultFormatter": "ms-python.black-formatter",
6
+ "editor.formatOnType": true,
7
+ "editor.codeActionsOnSave": {
8
+ "source.organizeImports": "explicit"
9
+ }
10
+ },
11
+ "[jupyter]": {
12
+ "files.insertFinalNewline": false
13
+ },
14
+ "black-formatter.args": [
15
+ "--line-length=119"
16
+ ],
17
+ "isort.args": ["--profile", "black"],
18
+ "flake8.args": [
19
+ "--max-line-length=119"
20
+ ],
21
+ "ruff.lint.args": [
22
+ "--line-length=119"
23
+ ],
24
+ "notebook.output.scrolling": true,
25
+ "notebook.formatOnCellExecution": true,
26
+ "notebook.formatOnSave.enabled": true,
27
+ "notebook.codeActionsOnSave": {
28
+ "source.organizeImports": "explicit"
29
+ }
30
+ }
app.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+
7
+ import gradio as gr
8
+
9
+ from dualstylegan import Model
10
+
11
+ DESCRIPTION = """# Portrait Style Transfer with [DualStyleGAN](https://github.com/williamyang1991/DualStyleGAN)
12
+
13
+ <img id="overview" alt="overview" src="https://raw.githubusercontent.com/williamyang1991/DualStyleGAN/main/doc_images/overview.jpg" />
14
+ """
15
+
16
+
17
+ def get_style_image_url(style_name: str) -> str:
18
+ base_url = "https://raw.githubusercontent.com/williamyang1991/DualStyleGAN/main/doc_images"
19
+ filenames = {
20
+ "cartoon": "cartoon_overview.jpg",
21
+ "caricature": "caricature_overview.jpg",
22
+ "anime": "anime_overview.jpg",
23
+ "arcane": "Reconstruction_arcane_overview.jpg",
24
+ "comic": "Reconstruction_comic_overview.jpg",
25
+ "pixar": "Reconstruction_pixar_overview.jpg",
26
+ "slamdunk": "Reconstruction_slamdunk_overview.jpg",
27
+ }
28
+ return f"{base_url}/{filenames[style_name]}"
29
+
30
+
31
+ def get_style_image_markdown_text(style_name: str) -> str:
32
+ url = get_style_image_url(style_name)
33
+ return f'<img id="style-image" src="{url}" alt="style image">'
34
+
35
+
36
+ def update_slider(choice: str) -> dict:
37
+ max_vals = {
38
+ "cartoon": 316,
39
+ "caricature": 198,
40
+ "anime": 173,
41
+ "arcane": 99,
42
+ "comic": 100,
43
+ "pixar": 121,
44
+ "slamdunk": 119,
45
+ }
46
+ return gr.Slider(maximum=max_vals[choice])
47
+
48
+
49
+ def update_style_image(style_name: str) -> dict:
50
+ text = get_style_image_markdown_text(style_name)
51
+ return gr.Markdown(value=text)
52
+
53
+
54
+ model = Model()
55
+
56
+ with gr.Blocks(css="style.css") as demo:
57
+ gr.Markdown(DESCRIPTION)
58
+
59
+ with gr.Group():
60
+ gr.Markdown(
61
+ """## Step 1 (Preprocess Input Image)
62
+
63
+ - Drop an image containing a near-frontal face to the **Input Image**.
64
+ - If there are multiple faces in the image, hit the Edit button in the upper right corner and crop the input image beforehand.
65
+ - Hit the **Preprocess** button.
66
+ - Choose the encoder version. Default is Z+ encoder which has better stylization performance. W+ encoder better reconstructs the input image to preserve more details.
67
+ - The final result will be based on this **Reconstructed Face**. So, if the reconstructed image is not satisfactory, you may want to change the input image.
68
+ """
69
+ )
70
+ with gr.Row():
71
+ encoder_type = gr.Radio(
72
+ label="Encoder Type",
73
+ choices=["Z+ encoder (better stylization)", "W+ encoder (better reconstruction)"],
74
+ value="Z+ encoder (better stylization)",
75
+ )
76
+ with gr.Row():
77
+ with gr.Column():
78
+ with gr.Row():
79
+ input_image = gr.Image(label="Input Image", type="filepath")
80
+ with gr.Row():
81
+ preprocess_button = gr.Button("Preprocess")
82
+ with gr.Column():
83
+ with gr.Row():
84
+ aligned_face = gr.Image(label="Aligned Face", type="numpy", interactive=False)
85
+ with gr.Column():
86
+ reconstructed_face = gr.Image(label="Reconstructed Face", type="numpy")
87
+ instyle = gr.State()
88
+
89
+ with gr.Row():
90
+ paths = sorted(pathlib.Path("images").glob("*.jpg"))
91
+ gr.Examples(examples=[[path.as_posix()] for path in paths], inputs=input_image)
92
+
93
+ with gr.Group():
94
+ gr.Markdown(
95
+ """## Step 2 (Select Style Image)
96
+
97
+ - Select **Style Type**.
98
+ - Select **Style Image Index** from the image table below.
99
+ """
100
+ )
101
+ with gr.Row():
102
+ with gr.Column():
103
+ style_type = gr.Radio(label="Style Type", choices=model.style_types, value=model.style_types[0])
104
+ text = get_style_image_markdown_text("cartoon")
105
+ style_image = gr.Markdown(value=text, latex_delimiters=[])
106
+ style_index = gr.Slider(label="Style Image Index", minimum=0, maximum=316, step=1, value=26)
107
+
108
+ with gr.Row():
109
+ gr.Examples(
110
+ examples=[
111
+ ["cartoon", 26],
112
+ ["caricature", 65],
113
+ ["arcane", 63],
114
+ ["pixar", 80],
115
+ ],
116
+ inputs=[style_type, style_index],
117
+ )
118
+
119
+ with gr.Group():
120
+ gr.Markdown(
121
+ """## Step 3 (Generate Style Transferred Image)
122
+
123
+ - Adjust **Structure Weight** and **Color Weight**.
124
+ - These are weights for the style image, so the larger the value, the closer the resulting image will be to the style image.
125
+ - Tips: For W+ encoder, better way of (Structure Only) is to uncheck (Structure Only) and set Color weight to 0.
126
+ - Hit the **Generate** button.
127
+ """
128
+ )
129
+ with gr.Row():
130
+ with gr.Column():
131
+ with gr.Row():
132
+ structure_weight = gr.Slider(label="Structure Weight", minimum=0, maximum=1, step=0.1, value=0.6)
133
+ with gr.Row():
134
+ color_weight = gr.Slider(label="Color Weight", minimum=0, maximum=1, step=0.1, value=1)
135
+ with gr.Row():
136
+ structure_only = gr.Checkbox(label="Structure Only", value=False)
137
+ with gr.Row():
138
+ generate_button = gr.Button("Generate")
139
+
140
+ with gr.Column():
141
+ result = gr.Image(label="Result")
142
+
143
+ with gr.Row():
144
+ gr.Examples(
145
+ examples=[
146
+ [0.6, 1.0],
147
+ [0.3, 1.0],
148
+ [0.0, 1.0],
149
+ [1.0, 0.0],
150
+ ],
151
+ inputs=[structure_weight, color_weight],
152
+ )
153
+
154
+ preprocess_button.click(
155
+ fn=model.detect_and_align_face,
156
+ inputs=[input_image],
157
+ outputs=aligned_face,
158
+ )
159
+ aligned_face.change(
160
+ fn=model.reconstruct_face,
161
+ inputs=[aligned_face, encoder_type],
162
+ outputs=[
163
+ reconstructed_face,
164
+ instyle,
165
+ ],
166
+ )
167
+ style_type.change(
168
+ fn=update_slider,
169
+ inputs=style_type,
170
+ outputs=style_index,
171
+ )
172
+ style_type.change(
173
+ fn=update_style_image,
174
+ inputs=style_type,
175
+ outputs=style_image,
176
+ )
177
+ generate_button.click(
178
+ fn=model.generate,
179
+ inputs=[
180
+ style_type,
181
+ style_index,
182
+ structure_weight,
183
+ color_weight,
184
+ structure_only,
185
+ instyle,
186
+ ],
187
+ outputs=result,
188
+ )
189
+
190
+ if __name__ == "__main__":
191
+ demo.queue(max_size=20).launch()
dualstylegan.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import pathlib
6
+ import subprocess
7
+ import sys
8
+ from typing import Callable
9
+
10
+ import dlib
11
+ import huggingface_hub
12
+ import numpy as np
13
+ import PIL.Image
14
+ import torch
15
+ import torch.nn as nn
16
+ import torchvision.transforms as T
17
+
18
+ if os.getenv('SYSTEM') == 'spaces':
19
+ os.system("sed -i '10,17d' DualStyleGAN/model/stylegan/op/fused_act.py")
20
+ os.system("sed -i '10,17d' DualStyleGAN/model/stylegan/op/upfirdn2d.py")
21
+
22
+ app_dir = pathlib.Path(__file__).parent
23
+ submodule_dir = app_dir / 'DualStyleGAN'
24
+ sys.path.insert(0, submodule_dir.as_posix())
25
+
26
+ from model.dualstylegan import DualStyleGAN
27
+ from model.encoder.align_all_parallel import align_face
28
+ from model.encoder.psp import pSp
29
+
30
+ MODEL_REPO = 'CVPR/DualStyleGAN'
31
+
32
+
33
+ class Model:
34
+ def __init__(self):
35
+ self.device = torch.device(
36
+ 'cuda:0' if torch.cuda.is_available() else 'cpu')
37
+ self.landmark_model = self._create_dlib_landmark_model()
38
+ self.encoder_dict = self._load_encoder()
39
+ self.transform = self._create_transform()
40
+ self.encoder_type = 'z+'
41
+
42
+ self.style_types = [
43
+ 'cartoon',
44
+ 'caricature',
45
+ 'anime',
46
+ 'arcane',
47
+ 'comic',
48
+ 'pixar',
49
+ 'slamdunk',
50
+ ]
51
+ self.generator_dict = {
52
+ style_type: self._load_generator(style_type)
53
+ for style_type in self.style_types
54
+ }
55
+ self.exstyle_dict = {
56
+ style_type: self._load_exstylecode(style_type)
57
+ for style_type in self.style_types
58
+ }
59
+
60
+ @staticmethod
61
+ def _create_dlib_landmark_model():
62
+ url = 'http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2'
63
+ path = pathlib.Path('shape_predictor_68_face_landmarks.dat')
64
+ if not path.exists():
65
+ bz2_path = 'shape_predictor_68_face_landmarks.dat.bz2'
66
+ torch.hub.download_url_to_file(url, bz2_path)
67
+ subprocess.run(f'bunzip2 -d {bz2_path}'.split())
68
+ return dlib.shape_predictor(path.as_posix())
69
+
70
+ def _load_encoder(self) -> nn.Module:
71
+ ckpt_path = huggingface_hub.hf_hub_download(MODEL_REPO,
72
+ 'models/encoder.pt')
73
+ ckpt = torch.load(ckpt_path, map_location='cpu')
74
+ opts = ckpt['opts']
75
+ opts['device'] = self.device.type
76
+ opts['checkpoint_path'] = ckpt_path
77
+ opts = argparse.Namespace(**opts)
78
+ model = pSp(opts)
79
+ model.to(self.device)
80
+ model.eval()
81
+
82
+ ckpt_path = huggingface_hub.hf_hub_download(MODEL_REPO,
83
+ 'models/encoder_wplus.pt')
84
+ ckpt = torch.load(ckpt_path, map_location='cpu')
85
+ opts = ckpt['opts']
86
+ opts['device'] = self.device.type
87
+ opts['checkpoint_path'] = ckpt_path
88
+ opts['output_size'] = 1024
89
+ opts = argparse.Namespace(**opts)
90
+ model2 = pSp(opts)
91
+ model2.to(self.device)
92
+ model2.eval()
93
+
94
+ return {'z+': model, 'w+': model2}
95
+
96
+ @staticmethod
97
+ def _create_transform() -> Callable:
98
+ transform = T.Compose([
99
+ T.Resize(256),
100
+ T.CenterCrop(256),
101
+ T.ToTensor(),
102
+ T.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),
103
+ ])
104
+ return transform
105
+
106
+ def _load_generator(self, style_type: str) -> nn.Module:
107
+ model = DualStyleGAN(1024, 512, 8, 2, res_index=6)
108
+ ckpt_path = huggingface_hub.hf_hub_download(
109
+ MODEL_REPO, f'models/{style_type}/generator.pt')
110
+ ckpt = torch.load(ckpt_path, map_location='cpu')
111
+ model.load_state_dict(ckpt['g_ema'])
112
+ model.to(self.device)
113
+ model.eval()
114
+ return model
115
+
116
+ @staticmethod
117
+ def _load_exstylecode(style_type: str) -> dict[str, np.ndarray]:
118
+ if style_type in ['cartoon', 'caricature', 'anime']:
119
+ filename = 'refined_exstyle_code.npy'
120
+ else:
121
+ filename = 'exstyle_code.npy'
122
+ path = huggingface_hub.hf_hub_download(
123
+ MODEL_REPO, f'models/{style_type}/{filename}')
124
+ exstyles = np.load(path, allow_pickle=True).item()
125
+ return exstyles
126
+
127
+ def detect_and_align_face(self, image_path) -> np.ndarray:
128
+ image = align_face(filepath=image_path, predictor=self.landmark_model)
129
+ x, y = np.random.randint(255), np.random.randint(255)
130
+ r, g, b = image.getpixel((x, y))
131
+ image.putpixel(
132
+ (x, y), (r, g + 1, b)
133
+ ) # trick to make sure run reconstruct_face() once any input setting changes
134
+ return image
135
+
136
+ @staticmethod
137
+ def denormalize(tensor: torch.Tensor) -> torch.Tensor:
138
+ return torch.clamp((tensor + 1) / 2 * 255, 0, 255).to(torch.uint8)
139
+
140
+ def postprocess(self, tensor: torch.Tensor) -> np.ndarray:
141
+ tensor = self.denormalize(tensor)
142
+ return tensor.cpu().numpy().transpose(1, 2, 0)
143
+
144
+ @torch.inference_mode()
145
+ def reconstruct_face(self, image: np.ndarray,
146
+ encoder_type: str) -> tuple[np.ndarray, torch.Tensor]:
147
+ if encoder_type == 'Z+ encoder (better stylization)':
148
+ self.encoder_type = 'z+'
149
+ z_plus_latent = True
150
+ return_z_plus_latent = True
151
+ else:
152
+ self.encoder_type = 'w+'
153
+ z_plus_latent = False
154
+ return_z_plus_latent = False
155
+ image = PIL.Image.fromarray(image)
156
+ input_data = self.transform(image).unsqueeze(0).to(self.device)
157
+ img_rec, instyle = self.encoder_dict[self.encoder_type](
158
+ input_data,
159
+ randomize_noise=False,
160
+ return_latents=True,
161
+ z_plus_latent=z_plus_latent,
162
+ return_z_plus_latent=return_z_plus_latent,
163
+ resize=False)
164
+ img_rec = torch.clamp(img_rec.detach(), -1, 1)
165
+ img_rec = self.postprocess(img_rec[0])
166
+ return img_rec, instyle
167
+
168
+ @torch.inference_mode()
169
+ def generate(self, style_type: str, style_id: int, structure_weight: float,
170
+ color_weight: float, structure_only: bool,
171
+ instyle: torch.Tensor) -> np.ndarray:
172
+
173
+ if self.encoder_type == 'z+':
174
+ z_plus_latent = True
175
+ input_is_latent = False
176
+ else:
177
+ z_plus_latent = False
178
+ input_is_latent = True
179
+
180
+ generator = self.generator_dict[style_type]
181
+ exstyles = self.exstyle_dict[style_type]
182
+
183
+ style_id = int(style_id)
184
+ stylename = list(exstyles.keys())[style_id]
185
+
186
+ latent = torch.tensor(exstyles[stylename]).to(self.device)
187
+ if structure_only and self.encoder_type == 'z+':
188
+ latent[0, 7:18] = instyle[0, 7:18]
189
+ exstyle = generator.generator.style(
190
+ latent.reshape(latent.shape[0] * latent.shape[1],
191
+ latent.shape[2])).reshape(latent.shape)
192
+ if structure_only and self.encoder_type == 'w+':
193
+ exstyle[:, 7:18] = instyle[:, 7:18]
194
+
195
+ img_gen, _ = generator([instyle],
196
+ exstyle,
197
+ input_is_latent=input_is_latent,
198
+ z_plus_latent=z_plus_latent,
199
+ truncation=0.7,
200
+ truncation_latent=0,
201
+ use_res=True,
202
+ interp_weights=[structure_weight] * 7 +
203
+ [color_weight] * 11)
204
+ img_gen = torch.clamp(img_gen.detach(), -1, 1)
205
+ img_gen = self.postprocess(img_gen[0])
206
+ return img_gen
images/95UF6LXe-Lo.jpg ADDED

Git LFS Details

  • SHA256: 9ba751a6519822fa683e062ee3a383e748f15b41d4ca87d14c4fa73f9beed845
  • Pointer size: 131 Bytes
  • Size of remote file: 503 kB
images/ILip77SbmOE.jpg ADDED

Git LFS Details

  • SHA256: 3eed82923bc76a90f067415f148d56239fdfa4a1aca9eef1d459bc6050c9dde8
  • Pointer size: 131 Bytes
  • Size of remote file: 939 kB
images/README.md ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ These images are freely-usable ones from [Unsplash](https://unsplash.com/).
2
+
3
+ - https://unsplash.com/photos/rDEOVtE7vOs
4
+ - https://unsplash.com/photos/et_78QkMMQs
5
+ - https://unsplash.com/photos/ILip77SbmOE
6
+ - https://unsplash.com/photos/95UF6LXe-Lo
images/et_78QkMMQs.jpg ADDED

Git LFS Details

  • SHA256: c63a2e9de5eda3cb28012cfc8e4ba9384daeda8cca7a8989ad90b21a1293cc6f
  • Pointer size: 131 Bytes
  • Size of remote file: 371 kB
images/rDEOVtE7vOs.jpg ADDED

Git LFS Details

  • SHA256: b136bf195fef5599f277a563f0eef79af5301d9352d4ebf82bd7a0a061b7bdc0
  • Pointer size: 131 Bytes
  • Size of remote file: 155 kB
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ bzip2
2
+ cmake
3
+ ninja-build
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ dlib==19.24.4
2
+ gradio==4.36.1
3
+ numpy==1.23.5
4
+ opencv-python-headless==4.9.0.80
5
+ Pillow==10.3.0
6
+ scipy==1.13.1
7
+ torch==2.0.1
8
+ torchvision==0.15.2
style.css ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ h1 {
2
+ text-align: center;
3
+ display: block;
4
+ }
5
+
6
+ #duplicate-button {
7
+ margin: auto;
8
+ color: #fff;
9
+ background: #1565c0;
10
+ border-radius: 100vh;
11
+ }
12
+
13
+ img#overview {
14
+ max-width: 1000px;
15
+ max-height: 600px;
16
+ display: block;
17
+ margin: auto;
18
+ }
19
+
20
+ img#style-image {
21
+ max-width: 1000px;
22
+ max-height: 600px;
23
+ display: block;
24
+ margin: auto;
25
+ }