-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmosaic-generator.py
233 lines (203 loc) · 6.8 KB
/
mosaic-generator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
import argparse
import logging
import math
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional
FILENAME = Path(__file__).stem
LOGGER = logging.getLogger(FILENAME)
def generate_image_mosaic(
src_paths: list[Path],
dst_path: Path,
oiiotool_path: Path,
mosaic_columns: int,
mosaic_gap_size: int,
tile_size: tuple[float, float],
) -> Path:
"""
Use oiiotool to generate a mosaic of images with the given charcteristics.
It is recommended all the sources image provided have the same dimensions.
Mosaic is read from top-left to bottom right.
Args:
oiiotool_path: filesystem path to the oiiotool executable.
src_paths: list of filesystem paths to existing image files readable by oiiotool
dst_path: filesystem to a file that may or may not exist.
mosaic_columns: maximum number of tile per row
mosaic_gap_size: space between tiles in pixels
tile_size:
scale factor in percent for tile to avoid a gigantic output mosaic.
100 means unscaled. Values <=100 are recommended.
Returns:
filesystem path to the mosaic created (dst_file)
"""
# used to copy metadata
ref_file = src_paths[0]
if len(src_paths) <= mosaic_columns:
tiles_w, tiles_h = (len(src_paths), 1)
else:
tiles_w = mosaic_columns
tiles_h = math.ceil(len(src_paths) / mosaic_columns)
command: list[str] = [str(oiiotool_path)]
for src_file in src_paths:
command += [
"-i",
str(src_file),
"--resize",
f"{tile_size[0]}%x{tile_size[1]}%",
# bottom-left text with 30px margin
"--text:x=30:y={TOP.height-30}:shadow=3",
f"{src_file.stem}",
]
# https://openimageio.readthedocs.io/en/latest/oiiotool.html#cmdoption-mosaic
# XXX: needed so hack explained under works
command += ["--metamerge"]
command += [f"--mosaic:pad={mosaic_gap_size}", f"{tiles_w}x{tiles_h}"]
# XXX: hack to preserve metadata that is lost with the mosaic operation
command += ["-i", str(ref_file), "--chappend"]
command += ["-o", str(dst_path)]
LOGGER.debug(f"subprocess.check_call({command})")
try:
subprocess.check_call(command)
except subprocess.CalledProcessError as error:
LOGGER.error(f"stderr={error.stderr}\nstdout={error.stdout}")
raise
return dst_path
def get_cli() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=FILENAME,
description="generate a mosaic of photo took during a session. each photo is labeled with its name.",
)
parser.add_argument(
"mosaic_path",
type=str,
help="filesystem path to write the final mosaic to.",
)
parser.add_argument(
"input_path",
type=Path,
nargs="+",
help=(
"filesystem path to multiple file or directory.\n"
"- recommended files are jpg/tif\n"
"- directories are shallow parsed for any image type\n"
"- mosaic is built from top-left to bottom right with the order of path specified respected.\n"
"- if a directory include the mosaic_path it will be excluded.\n"
),
)
parser.add_argument(
"--oiiotool",
type=Path,
default=os.getenv("OIIOTOOL"),
help=(
"filesystem path to the oiiotool executable."
'if not provided the value is retrieved from an "OIIOTOOL" environment variable.'
"Note oiiotool version must be compiled with text rendering support."
),
)
parser.add_argument(
"--image-extensions",
type=str,
default="jpg",
help="comma-separated list of image extensions to use when parsing directories. ex: jpg,png,tif",
)
parser.add_argument(
"--anamorphic-desqueeze",
type=float,
default=1.0,
help="horizontal stretch factor for anamorphic optic desqueeze",
)
parser.add_argument(
"--columns",
type=int,
default=6,
help="number of columns for the mosaic.",
)
parser.add_argument(
"--gap",
type=int,
default=15,
help="space between mosaic tiles in pixels.",
)
parser.add_argument(
"--resize",
type=float,
default=25.0,
help=(
"resize factor for each image tile, in percent."
"lower value will avoid the final mosaic to have a gigantic dimensions."
"value must be lower or equal to 100."
),
)
return parser
def run_cli(argv: list[str] = None) -> Path:
"""
Args:
argv: list of command line argument for the CLI
Returns:
filesystem path to the mosaic file on disk
"""
cli = get_cli()
argv = argv or sys.argv[1:]
parsed = cli.parse_args(argv)
logging.basicConfig(
level=logging.DEBUG,
format="{levelname: <7} | {asctime} [{name}] {message}",
style="{",
stream=sys.stdout,
)
input_paths: list[Path] = parsed.input_path
image_extensions = parsed.image_extensions.split(",")
src_paths = []
for input_path in input_paths:
if input_path.is_dir():
for image_extention in image_extensions:
children = input_path.glob(f"*.{image_extention}")
src_paths.extend(children)
else:
src_paths.append(input_path)
dst_path = Path(parsed.mosaic_path)
if dst_path in src_paths:
src_paths.remove(dst_path)
oiiotool: Optional[Path] = parsed.oiiotool
if oiiotool:
oiiotool = Path(oiiotool)
anamorphic_desqueeze: float = parsed.anamorphic_desqueeze
columns: int = parsed.columns
gap: int = parsed.gap
resize: float = parsed.resize
tile_size = (resize * anamorphic_desqueeze, resize)
if not oiiotool:
print(
f"❌ no OIIOTOOL executable path could be found",
file=sys.stderr,
)
sys.exit(11)
elif not oiiotool.exists():
print(
f"❌ given OIIOTOOL executable path doesn't exist: {oiiotool}",
file=sys.stderr,
)
sys.exit(10)
start_time = time.time()
LOGGER.info(f"processing {len(src_paths)} files to '{dst_path}'")
generate_image_mosaic(
src_paths=src_paths,
dst_path=dst_path,
oiiotool_path=oiiotool,
mosaic_columns=columns,
mosaic_gap_size=gap,
tile_size=tile_size,
)
LOGGER.info(f"generation took {time.time() - start_time:.2f}s")
if not dst_path.exists():
print(
f"❌ unexpected issue: destination image '{dst_path}' doesn't exist on disk.",
file=sys.stderr,
)
sys.exit(100)
return dst_path
if __name__ == "__main__":
run_cli()