242 lines
10 KiB
Python
242 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
"""
|
|
Genere le schema explicatif de l'onglet « Profondeur ».
|
|
|
|
python docs/schema_profondeur.py # les deux versions
|
|
python docs/schema_profondeur.py fr # docs/profondeur.svg + .png (README)
|
|
python docs/schema_profondeur.py en # docs/depth_en.svg
|
|
# + images/depth_en.png (onglet
|
|
# « Depth » de la boite de dialogue)
|
|
|
|
Une ligne par parametre de l'onglet, reperee par le numero que le parametre
|
|
porte dans la boite de dialogue, avec le resultat pour plusieurs valeurs.
|
|
|
|
Le portrait d'essai (doc/personne.jpg) passe par les memes fonctions que
|
|
l'extension : profondeur estimee par `gray_iso_layers_depth` (il faut donc
|
|
l'environnement installe par `deploy.ps1 -Depth`), puis filtre guide, details
|
|
fins et decoupe en planches du noyau. Le schema reste fidele au resultat reel.
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import io
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.path.dirname(HERE)
|
|
sys.path.insert(0, ROOT)
|
|
sys.path.insert(0, HERE)
|
|
|
|
import numpy as np # noqa: E402
|
|
from PIL import Image # noqa: E402
|
|
|
|
import gray_iso_layers_core as core # noqa: E402
|
|
import schema_parametres as base # noqa: E402
|
|
from gray_iso_layers_depth import DepthUnavailable, depth_field # noqa: E402
|
|
|
|
PHOTO = os.path.join(ROOT, "doc", "personne.jpg")
|
|
RESOLUTION = 500 # points de calcul sur le grand cote
|
|
LEVELS = 8 # planches des vignettes
|
|
|
|
W, ROW = 1400, 430 # 3 lignes de 4 vignettes
|
|
H = 3 * ROW
|
|
COLUMN = (W - 30) / 4.0
|
|
TILE_W, TILE_H = 225.0, 286.0 # vignette, aux proportions du portrait
|
|
|
|
VERSIONS = {
|
|
"fr": {
|
|
"svg": os.path.join(HERE, "profondeur.svg"),
|
|
"png": os.path.join(HERE, "profondeur.png"),
|
|
"png_width": 1400,
|
|
"font_scale": 1.0,
|
|
"text": {
|
|
"title1": "Part de la carte de profondeur",
|
|
"note1": "Dose le mélange entre les gris de la photo (0 %) et sa "
|
|
"profondeur estimée (100 %). Planche claire = devant, "
|
|
"planche sombre = au fond.",
|
|
"tiles1": ["La photo", "0 % : gris seuls", "50 %", "100 % : volumes"],
|
|
"title2": "Modèle de profondeur",
|
|
"note2": "Rapide : calcul court, carte lisse. Fin : cheveux et visage "
|
|
"mieux détourés. (Exemples avec les réglages 1 et 3 à 100 %.)",
|
|
"tiles2": ["Rapide : profondeur", "Rapide : planches",
|
|
"Fin : profondeur", "Fin : planches"],
|
|
"title3": "Détails fins de la photo",
|
|
"note3": "Grave dans la profondeur les yeux, la bouche, les sourcils. "
|
|
"Trop fort : petites formes parasites. (Exemples avec le "
|
|
"réglage 1 à 100 %, modèle Fin.)",
|
|
"tiles3": ["0 % : visage lisse", "50 %", "100 %", "200 %"],
|
|
},
|
|
},
|
|
"en": {
|
|
"svg": os.path.join(HERE, "depth_en.svg"),
|
|
"png": os.path.join(ROOT, "images", "depth_en.png"),
|
|
"png_width": 900,
|
|
"font_scale": 1.25,
|
|
"text": {
|
|
"title1": "Depth map share",
|
|
"note1": "Mixes the grays of the photo (0 %) with its estimated depth "
|
|
"(100 %). Light board = front, dark board = back.",
|
|
"tiles1": ["The photo", "0 %: grays alone", "50 %", "100 %: volumes"],
|
|
"title2": "Depth model",
|
|
"note2": "Fast: short run, smooth map. Fine: sharper hair and face. "
|
|
"(Examples with settings 1 and 3 at 100 %.)",
|
|
"tiles2": ["Fast: depth", "Fast: boards", "Fine: depth", "Fine: boards"],
|
|
"title3": "Fine details of the photo",
|
|
"note3": "Carves eyes, mouth and eyebrows into the depth. Too high: "
|
|
"stray small shapes. (Examples with setting 1 at 100 %, "
|
|
"Fine model.)",
|
|
"tiles3": ["0 %: smooth face", "50 %", "100 %", "200 %"],
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Champs : memes etapes que GrayIsoLayers.add_depth
|
|
# --------------------------------------------------------------------------
|
|
|
|
class Portrait:
|
|
"""Le portrait d'essai : ses gris et sa profondeur selon chaque modele."""
|
|
|
|
def __init__(self):
|
|
with open(PHOTO, "rb") as handle:
|
|
data = handle.read()
|
|
picture = Image.open(io.BytesIO(data)).convert("RGB")
|
|
zoom = RESOLUTION / float(max(picture.size))
|
|
size = (int(round(picture.width * zoom)), int(round(picture.height * zoom)))
|
|
self.gray = core.normalize(np.asarray(
|
|
picture.convert("L").resize(size, Image.LANCZOS), dtype=float))
|
|
rows, cols = self.gray.shape
|
|
self.sigma = max(rows, cols) / 40.0
|
|
|
|
small = picture.copy()
|
|
small.thumbnail((768, 768), Image.LANCZOS)
|
|
digest = hashlib.sha1(data).hexdigest()
|
|
self.depth = {}
|
|
for model in ("midas_small", "depth_anything_v2_small"):
|
|
depth = depth_field(small, digest, model)
|
|
depth = Image.fromarray(np.asarray(depth, dtype=np.float32))
|
|
depth = core.normalize(np.asarray(depth.resize(size, Image.BICUBIC),
|
|
dtype=float))
|
|
self.depth[model] = core.guided_filter(
|
|
self.gray, depth, max(2, int(round(max(rows, cols) / 100.0))))
|
|
|
|
def field(self, mix, detail, model="depth_anything_v2_small"):
|
|
"""Champ decoupe par l'extension pour ces reglages (en %)."""
|
|
depth = core.add_detail(self.depth[model], self.gray, detail / 100.0,
|
|
self.sigma)
|
|
return core.blend_fields(self.gray, depth, mix / 100.0)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Dessin
|
|
# --------------------------------------------------------------------------
|
|
|
|
def tile_box(row, column):
|
|
return 30 + column * COLUMN, row * ROW + 98
|
|
|
|
|
|
def frame(x, y):
|
|
base.out.append('<rect x="{}" y="{}" width="{}" height="{}" fill="none" '
|
|
'stroke="{}" stroke-width="1.2"/>'.format(
|
|
base.fmt(x), base.fmt(y), TILE_W, TILE_H, base.EDGE))
|
|
|
|
|
|
def draw_field(field, x, y):
|
|
"""Vignette d'un champ, en image de gris."""
|
|
buffer = io.BytesIO()
|
|
Image.fromarray((np.clip(field, 0, 1) * 255).round().astype(np.uint8)).save(
|
|
buffer, "PNG")
|
|
base.out.append(
|
|
'<image x="{}" y="{}" width="{}" height="{}" preserveAspectRatio="none" '
|
|
'xlink:href="data:image/png;base64,{}"/>'.format(
|
|
base.fmt(x), base.fmt(y), TILE_W, TILE_H,
|
|
base64.b64encode(buffer.getvalue()).decode()))
|
|
frame(x, y)
|
|
|
|
|
|
def draw_boards(field, x, y):
|
|
"""Vignette des planches decoupees dans un champ, chacune de son gris."""
|
|
rows, cols = field.shape
|
|
layers = core.iso_layers(field, levels=LEVELS, blur=0.01 * max(rows, cols),
|
|
min_area=0.0004, tolerance=0.4, shapes="light",
|
|
preserve_edges=True)
|
|
scale_x, scale_y = TILE_W / (cols - 1.0), TILE_H / (rows - 1.0)
|
|
box = (x, y, x + TILE_W, y + TILE_H)
|
|
for _level, gray, rings in layers:
|
|
if not rings:
|
|
continue
|
|
d = core.rings_to_d(core.scale_rings(rings, scale_x, scale_y, x, y),
|
|
box=box, precision=1)
|
|
base.out.append('<path d="{}" fill="{}" fill-rule="evenodd" stroke="{}" '
|
|
'stroke-width="0.45" stroke-linejoin="round"/>'.format(
|
|
d, core.gray_to_hex(gray), base.EDGE))
|
|
frame(x, y)
|
|
|
|
|
|
def row(number, tiles):
|
|
"""Ligne du parametre `number` : pastille, titre, note, puis les
|
|
vignettes `tiles`, liste de `(dessin, champ)`."""
|
|
top = (number - 1) * ROW
|
|
# Pastille au numero du parametre, comme dans la boite de dialogue.
|
|
base.out.append('<circle cx="47" cy="{}" r="17" fill="{}"/>'.format(
|
|
top + 43, base.DIM))
|
|
base.text(47, top + 43 + 7.5 * base.FS, str(number), size=21, color="#ffffff",
|
|
anchor="middle", weight="bold", halo=False)
|
|
base.text(76, top + 51, base.T["title{}".format(number)], size=22,
|
|
weight="bold", halo=False)
|
|
base.text(30, top + 84, base.T["note{}".format(number)], size=15, halo=False)
|
|
captions = base.T["tiles{}".format(number)]
|
|
for column, ((draw, field), caption) in enumerate(zip(tiles, captions)):
|
|
x, y = tile_box(number - 1, column)
|
|
draw(field, x, y)
|
|
base.text(x + TILE_W / 2.0, y + TILE_H + 22 * base.FS, caption, size=16,
|
|
color=base.DIM, anchor="middle", weight="bold", halo=False)
|
|
|
|
|
|
def build(language, portrait):
|
|
version = VERSIONS[language]
|
|
base.out.clear()
|
|
base.T.clear()
|
|
base.T.update(version["text"])
|
|
base.FS = version["font_scale"]
|
|
|
|
base.out.append('<rect width="{}" height="{}" fill="#ffffff"/>'.format(W, H))
|
|
for y in (ROW, 2 * ROW):
|
|
base.out.append('<line x1="20" y1="{0}" x2="{1}" y2="{0}" stroke="#dee2e6" '
|
|
'stroke-width="1.5"/>'.format(y, W - 20))
|
|
fine, fast = "depth_anything_v2_small", "midas_small"
|
|
row(1, [(draw_field, portrait.gray)]
|
|
+ [(draw_boards, portrait.field(mix, 100)) for mix in (0, 50, 100)])
|
|
row(2, [(draw_field, portrait.depth[fast]),
|
|
(draw_boards, portrait.field(100, 100, fast)),
|
|
(draw_field, portrait.depth[fine]),
|
|
(draw_boards, portrait.field(100, 100, fine))])
|
|
row(3, [(draw_boards, portrait.field(100, detail))
|
|
for detail in (0, 50, 100, 200)])
|
|
|
|
svg = ('<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
'<svg xmlns="http://www.w3.org/2000/svg" '
|
|
'xmlns:xlink="http://www.w3.org/1999/xlink" width="{0}" height="{1}" '
|
|
'viewBox="0 0 {0} {1}">\n{2}\n</svg>\n').format(W, H, "\n".join(base.out))
|
|
with open(version["svg"], "w", encoding="utf-8", newline="\n") as handle:
|
|
handle.write(svg)
|
|
print(version["svg"])
|
|
base.export_png(version["svg"], version["png"], version["png_width"])
|
|
|
|
|
|
def main(argv):
|
|
try:
|
|
portrait = Portrait()
|
|
except DepthUnavailable as error:
|
|
sys.exit("Carte de profondeur indisponible ({}) : lancer "
|
|
"deploy.ps1 -Depth.".format(error))
|
|
for language in argv or sorted(VERSIONS):
|
|
build(language, portrait)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|