You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

280 lines
9.2 KiB

"""
ocp_screenshot.py
=================
Screenshot d'un modèle build123d via OCP (OCCT), fond transparent.
Dépendances : build123d, Pillow, numpy
Nécessite : Xvfb (sudo apt install xvfb)
Lancement
---------
# Directement (si DISPLAY est déjà dispo) :
python ocp_screenshot.py
# Headless (sans serveur X) :
xvfb-run -a python ocp_screenshot.py
API
---
from ocp_screenshot import screenshot
screenshot(
shape, # build123d Shape / Compound / Part
"output.png",
width=800, height=600,
orientation="ZupRight", # voir ORIENTATIONS ci-dessous
fit_padding=0.05, # marge autour du modèle (0–1)
background=None, # None = transparent, ou "#RRGGBB"
color="#A8C4DC", # couleur du solide
line_color="#1a1a1a", # couleur des arêtes
show_edges=True,
)
Orientations disponibles
------------------------
ZupRight → isométrique Z-haut, côté droit (défaut, standard build123d)
ZupLeft → isométrique Z-haut, côté gauche
YupRight → isométrique Y-haut, côté droit
YupLeft → isométrique Y-haut, côté gauche
Top / Bottom / Front / Back / Left / Right
"""
import subprocess, sys, os, tempfile
import numpy as np
from PIL import Image
# ── Orientations ────────────────────────────────────────────────────────────
from OCP.V3d import V3d_TypeOfOrientation as _Ori
ORIENTATIONS = {
"ZupRight" : _Ori.V3d_TypeOfOrientation_Zup_AxoRight,
"ZupLeft" : _Ori.V3d_TypeOfOrientation_Zup_AxoLeft,
"YupRight" : _Ori.V3d_TypeOfOrientation_Yup_AxoRight,
"YupLeft" : _Ori.V3d_TypeOfOrientation_Yup_AxoLeft,
"Top" : _Ori.V3d_Zpos,
"Bottom" : _Ori.V3d_Zneg,
"Front" : _Ori.V3d_Xpos,
"Back" : _Ori.V3d_Xneg,
"Left" : _Ori.V3d_Ypos,
"Right" : _Ori.V3d_Yneg,
}
def _hex_to_occ_color(hex_str: str):
"""Convertit '#RRGGBB' en Quantity_Color OCC."""
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
h = hex_str.lstrip("#")
r, g, b = (int(h[i:i+2], 16) / 255.0 for i in (0, 2, 4))
return Quantity_Color(r, g, b, Quantity_TOC_RGB)
_OCC_SESSION: dict = {} # conn + driver + viewer réutilisés dans le même process
def _make_viewer(width: int, height: int):
"""
Crée (ou réutilise) un viewer OCC offscreen.
Aspect_DisplayConnection ne peut être instancié qu'une fois par process
sous Xvfb — on conserve donc la connexion et le driver en cache de module.
Chaque appel crée en revanche une nouvelle View (fenêtre offscreen).
"""
from OCP.Aspect import Aspect_DisplayConnection
from OCP.OpenGl import OpenGl_GraphicDriver
from OCP.V3d import V3d_Viewer
from OCP.Xw import Xw_Window
if "viewer" not in _OCC_SESSION:
conn = Aspect_DisplayConnection()
driver = OpenGl_GraphicDriver(conn, False)
viewer = V3d_Viewer(driver)
viewer.SetDefaultLights()
viewer.SetLightOn()
_OCC_SESSION["conn"] = conn
_OCC_SESSION["driver"] = driver
_OCC_SESSION["viewer"] = viewer
viewer = _OCC_SESSION["viewer"]
conn = _OCC_SESSION["conn"]
view = viewer.CreateView()
win = Xw_Window(conn, "ocp_screenshot", 0, 0, width, height)
view.SetWindow(win)
win.Map()
return viewer, view
def _render_to_array(view, shape_occ, ctx, width: int, height: int,
bg_color, orientation, fit_padding: float) -> np.ndarray:
"""Configure la vue, lance le rendu, retourne un array HxWx3 uint8."""
from OCP.Image import Image_AlienPixMap
from OCP.TCollection import TCollection_AsciiString
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
view.SetBackgroundColor(bg_color)
view.SetProj(orientation)
view.FitAll(fit_padding)
view.Redraw()
pix = Image_AlienPixMap()
view.ToPixMap(pix, width, height)
# Sauvegarde temporaire puis lecture Pillow
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
tmp = f.name
pix.Save(TCollection_AsciiString(tmp))
arr = np.array(Image.open(tmp).convert("RGB"), dtype=np.float32)
os.unlink(tmp)
return arr
def _double_render_transparent(view, width, height, orientation,
fit_padding) -> np.ndarray:
"""
Technique double-render blanc/noir → canal alpha propre.
Principe : sur fond blanc Cw = C·α + 1·(1−α) = C·α + 1 − α
sur fond noir Cb = C·α
donc α = 1 − (Cw − Cb) et C = Cb / α
"""
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
white = Quantity_Color(1.0, 1.0, 1.0, Quantity_TOC_RGB)
black = Quantity_Color(0.0, 0.0, 0.0, Quantity_TOC_RGB)
w = _render_to_array(view, None, None, width, height,
white, orientation, fit_padding)
b = _render_to_array(view, None, None, width, height,
black, orientation, fit_padding)
# Alpha (par canal, on prend le max pour être robuste aux artefacts)
alpha = 1.0 - (w - b) / 255.0
alpha = np.clip(alpha.max(axis=2), 0.0, 1.0)
# Couleur pré-multipliée → non-pré-multipliée
a3 = alpha[..., np.newaxis]
rgb = np.where(a3 > 0.01, b / np.maximum(a3, 0.01), 0.0)
rgb = np.clip(rgb, 0, 255).astype(np.uint8)
return np.dstack([rgb, (alpha * 255).astype(np.uint8)]) # RGBA
def screenshot(
shape,
output_path: str,
width: int = 800,
height: int = 600,
orientation: str = "ZupRight",
fit_padding: float = 0.05,
background: "str | None" = None,
color: str = "#A8C4DC",
line_color: str = "#1a1a1a",
show_edges: bool = True,
):
"""
Génère un screenshot PNG du modèle build123d.
Parameters
----------
shape : build123d Shape (Solid, Compound, Part…)
output_path : chemin de sortie (.png)
width/height : dimensions en pixels
orientation : voir ORIENTATIONS (défaut "ZupRight")
fit_padding : marge relative autour du modèle (0.05 = 5 %)
background : None → fond transparent | "#RRGGBB" → fond coloré
color : couleur du solide (hex)
line_color : couleur des arêtes (hex)
show_edges : afficher les arêtes
"""
from OCP.AIS import AIS_InteractiveContext, AIS_Shape
from OCP.Prs3d import Prs3d_Drawer
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
from OCP.Aspect import Aspect_TypeOfLine
if orientation not in ORIENTATIONS:
raise ValueError(
f"Orientation '{orientation}' inconnue. "
f"Choisir parmi : {list(ORIENTATIONS.keys())}"
)
ori = ORIENTATIONS[orientation]
# Extraire le TopoDS_Shape depuis build123d
occ_shape = shape.wrapped
viewer, view = _make_viewer(width, height)
ctx = AIS_InteractiveContext(viewer)
# Couleur du solide
ais = AIS_Shape(occ_shape)
ais.SetColor(_hex_to_occ_color(color))
ais.SetTransparency(0.0)
if show_edges:
# Configurer les arêtes dans le drawer
drawer = ais.Attributes()
drawer.SetFaceBoundaryDraw(True)
drawer.FaceBoundaryAspect().SetColor(_hex_to_occ_color(line_color))
drawer.FaceBoundaryAspect().SetWidth(1.0)
ctx.Display(ais, True)
view.SetProj(ori)
view.FitAll(fit_padding)
if background is None:
# Fond transparent via double-render
rgba = _double_render_transparent(view, width, height, ori, fit_padding)
Image.fromarray(rgba, "RGBA").save(output_path)
else:
# Fond coloré direct
from OCP.Image import Image_AlienPixMap
from OCP.TCollection import TCollection_AsciiString
view.SetBackgroundColor(_hex_to_occ_color(background))
view.Redraw()
pix = Image_AlienPixMap()
view.ToPixMap(pix, width, height)
pix.Save(TCollection_AsciiString(output_path))
print(f"{output_path} ({width}×{height}px, orientation={orientation})")
# ─────────────────────────────────────────────
# Exemple d'utilisation
# ─────────────────────────────────────────────
if __name__ == "__main__":
from build123d import *
# Modèle simple : boîte + cylindre (Compound)
with BuildPart() as p:
Box(40, 30, 15)
with Locations((0, 0, 15)):
Cylinder(8, 20)
# Fond transparent
screenshot(
p.part,
"model_transparent.png",
width=800, height=600,
orientation="ZupRight",
color="#5B8DB8",
)
# Fond coloré
screenshot(
p.part,
"model_bg.png",
width=800, height=600,
orientation="ZupRight",
background="#F0EDE6",
color="#5B8DB8",
)
# Quelques orientations
for ori in ["Top", "Front", "ZupLeft"]:
screenshot(
p.part,
f"model_{ori.lower()}.png",
width=600, height=450,
orientation=ori,
color="#C0392B",
)