La profondeur de la scène est estimée à partir de la photo (MiDaS small ou Depth Anything V2 Small) et mélangée aux gris : les planches suivent les volumes du sujet plutôt que ses lumières et ses ombres. - Onglet « Profondeur » : part de la carte de profondeur, modèle, et détails fins de la photo gravés dans la profondeur (yeux, bouche). - Répartition des niveaux : rapproche les planches d'aires égales quand les valeurs sont tassées sur quelques gris. - Images 16 bits lues sur toute leur plage (elles étaient écrêtées à 255). - Le Python d'Inkscape (MinGW, sans pip) ne peut pas charger onnxruntime : le calcul tourne dans un environnement à part, installé par deploy.ps1 -Depth, lancé en sous-processus, résultat gardé en cache. - Depth Anything sur la carte graphique (DirectML) avec repli sur le processeur ; MiDaS small toujours sur le processeur, DirectML lui faisant rendre une carte fausse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AumyrbcuQobB3GbPrFjG9i
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
# coding=utf-8
|
|
"""
|
|
Carte de profondeur d'une image, pour l'extension « Gray Iso-Layers ».
|
|
|
|
Le Python d'Inkscape ne peut pas charger onnxruntime : le calcul est confie
|
|
au script `depth_estimate.py`, lance avec le Python d'un environnement a part
|
|
(cree par `deploy.ps1 -Depth`). Le resultat est garde sur disque, car
|
|
Inkscape relance l'extension a chaque « Appliquer » : seul le premier calcul
|
|
d'une image coute l'inference.
|
|
|
|
Dossier de donnees (hors du dossier des extensions, qu'Inkscape parcourt en
|
|
entier au demarrage) : `venv/`, `models/` et `cache/`.
|
|
|
|
Sans dependance a inkex. Dependances : numpy et Pillow (fournis avec Inkscape).
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import numpy as np
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# Modeles proposes : nom (parametre depth_model du .inx) -> fichier de poids.
|
|
MODELS = {
|
|
"midas_small": "midas_v21_small_256.onnx",
|
|
"depth_anything_v2_small": "depth_anything_v2_small.onnx",
|
|
}
|
|
|
|
CACHE_LIMIT = 30 # cartes gardees sur disque
|
|
TIMEOUT = 120 # secondes laissees au calcul
|
|
|
|
|
|
class DepthUnavailable(Exception):
|
|
"""La carte de profondeur ne peut pas etre calculee (environnement ou
|
|
modele absent, echec du calcul). Le message en donne la cause."""
|
|
|
|
|
|
def data_dir():
|
|
"""Dossier de l'environnement Python, des modeles et du cache."""
|
|
if os.name == "nt":
|
|
base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
|
else:
|
|
base = os.path.join(os.path.expanduser("~"), ".local", "share")
|
|
return os.path.join(base, "GrayIsoLayers")
|
|
|
|
|
|
def find_python():
|
|
"""Python qui execute `depth_estimate.py` : celui de la variable
|
|
GRAY_ISO_LAYERS_PYTHON, sinon celui de l'environnement du dossier de
|
|
donnees."""
|
|
chosen = os.environ.get("GRAY_ISO_LAYERS_PYTHON")
|
|
if chosen:
|
|
return chosen
|
|
if os.name == "nt":
|
|
return os.path.join(data_dir(), "venv", "Scripts", "python.exe")
|
|
return os.path.join(data_dir(), "venv", "bin", "python")
|
|
|
|
|
|
def depth_field(picture, digest, model="midas_small"):
|
|
"""Profondeur inverse de `picture` (image PIL RGB) : tableau 2D, plus la
|
|
valeur est haute, plus le point est proche. Sa taille est celle de la
|
|
sortie du modele, pas celle de l'image.
|
|
|
|
`digest` identifie l'image (empreinte de ses octets) : avec le nom du
|
|
modele, c'est la cle du cache. Leve `DepthUnavailable` si le calcul est
|
|
impossible.
|
|
"""
|
|
if model not in MODELS:
|
|
raise DepthUnavailable("unknown model '{}'".format(model))
|
|
cache = os.path.join(data_dir(), "cache")
|
|
target = os.path.join(cache, "{}-{}.npy".format(digest, model))
|
|
if os.path.isfile(target):
|
|
try:
|
|
depth = np.load(target, allow_pickle=False)
|
|
os.utime(target) # carte resservie : la derniere a etre purgee
|
|
return depth
|
|
except (OSError, ValueError):
|
|
pass # fichier abime : on recalcule
|
|
|
|
python = find_python()
|
|
script = os.path.join(HERE, "depth_estimate.py")
|
|
weights = os.path.join(data_dir(), "models", MODELS[model])
|
|
for path in (python, script, weights):
|
|
if not os.path.isfile(path):
|
|
raise DepthUnavailable("missing file: {}".format(path))
|
|
|
|
# Inkscape pointe PYTHONPATH et PYTHONHOME vers son propre Python : ils
|
|
# feraient charger ses modules par celui de l'environnement.
|
|
env = {key: value for key, value in os.environ.items()
|
|
if key.upper() not in ("PYTHONPATH", "PYTHONHOME")}
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
source = os.path.join(folder, "image.png")
|
|
output = os.path.join(folder, "depth.npy")
|
|
picture.save(source)
|
|
try:
|
|
done = subprocess.run(
|
|
[python, script, source, output, weights, "--model", model],
|
|
env=env, capture_output=True, encoding="utf-8", errors="replace",
|
|
timeout=TIMEOUT,
|
|
# Pas de fenetre de console sous Windows.
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
|
except (OSError, subprocess.TimeoutExpired) as error:
|
|
raise DepthUnavailable(str(error))
|
|
if done.returncode != 0 or not os.path.isfile(output):
|
|
lines = (done.stderr or "").strip().splitlines()
|
|
raise DepthUnavailable(lines[-1] if lines else
|
|
"exit code {}".format(done.returncode))
|
|
try:
|
|
depth = np.load(output, allow_pickle=False)
|
|
except (OSError, ValueError) as error:
|
|
raise DepthUnavailable(str(error))
|
|
|
|
try:
|
|
os.makedirs(cache, exist_ok=True)
|
|
np.save(target, depth, allow_pickle=False)
|
|
_prune(cache)
|
|
except OSError as error:
|
|
# Le cache n'est qu'un confort : la carte est calculee, on la rend.
|
|
print("gray_iso_layers_depth: {}".format(error), file=sys.stderr)
|
|
return depth
|
|
|
|
|
|
def _prune(cache, limit=None):
|
|
"""Ne garde que les `limit` cartes les plus recemment servies."""
|
|
limit = CACHE_LIMIT if limit is None else limit
|
|
files = [os.path.join(cache, name) for name in os.listdir(cache)
|
|
if name.endswith(".npy")]
|
|
files.sort(key=os.path.getmtime, reverse=True)
|
|
for path in files[limit:]:
|
|
os.remove(path)
|