#!/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(''.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( ''.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(''.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(''.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(''.format(W, H)) for y in (ROW, 2 * ROW): base.out.append(''.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 = ('\n' '\n{2}\n\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:])