Test axonométrique du 27/09 : pipeline de fabrication et prototype
pipeline/axo : extraction du Rhino, recalage sur la planche (1:640), maisons, arbres (positions réelles des blocs, couleurs de la planche), scène en mètres, sols depuis le PDF Illustrator vectoriel (calques pilotés), bulles de la page 15, planche de l'eau nettoyée. prototypes/axo-test : la page de test et son assembleur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9e0143536
commit
731531dbe2
@@ -0,0 +1,43 @@
|
||||
# Extrait toute la géométrie utile du plan Rhino TRANSFO en mètres (JSON), une fois pour toutes.
|
||||
import sys, json, rhino3dm, math
|
||||
m = rhino3dm.File3dm.Read(sys.argv[1])
|
||||
L = {i: l.Name for i, l in enumerate(m.Layers)}
|
||||
def pts(c, n=48):
|
||||
if isinstance(c, rhino3dm.PolylineCurve):
|
||||
return [(c.Point(i).X/100, c.Point(i).Y/100) for i in range(c.PointCount)]
|
||||
if isinstance(c, rhino3dm.LineCurve):
|
||||
return [(c.PointAtStart.X/100, c.PointAtStart.Y/100), (c.PointAtEnd.X/100, c.PointAtEnd.Y/100)]
|
||||
d = c.Domain
|
||||
return [(c.PointAt(d.T0+(d.T1-d.T0)*i/n).X/100, c.PointAt(d.T0+(d.T1-d.T0)*i/n).Y/100) for i in range(n+1)]
|
||||
courbes = []
|
||||
instances = []
|
||||
defs = {str(d.Id): d.Name for d in m.InstanceDefinitions}
|
||||
for o in m.Objects:
|
||||
g = o.Geometry; ln = L.get(o.Attributes.LayerIndex, "")
|
||||
if isinstance(g, rhino3dm.InstanceReference):
|
||||
x = g.Xform
|
||||
px, py = x.M03/100, x.M13/100
|
||||
sx = math.hypot(x.M00, x.M10);
|
||||
instances.append({"l": ln, "d": defs.get(str(g.ParentIdefId), "?"), "x": round(px, 3), "y": round(py, 3), "s": round(sx, 4)})
|
||||
continue
|
||||
if not isinstance(g, rhino3dm.Curve): continue
|
||||
try: P = pts(g)
|
||||
except Exception: continue
|
||||
if len(P) < 2: continue
|
||||
courbes.append({"l": ln, "f": bool(g.IsClosed), "p": [[round(a, 3), round(b, 3)] for a, b in P]})
|
||||
# boîte englobante de chaque définition de bloc (pour le rayon des arbres)
|
||||
bb = {}
|
||||
for d in m.InstanceDefinitions:
|
||||
xs, ys = [], []
|
||||
for oid in d.GetObjectIds():
|
||||
ob = m.Objects.FindId(oid)
|
||||
if ob is None: continue
|
||||
try:
|
||||
b = ob.Geometry.GetBoundingBox(); xs += [b.Min.X, b.Max.X]; ys += [b.Min.Y, b.Max.Y]
|
||||
except Exception: pass
|
||||
if xs: bb[d.Name] = [round((max(xs)-min(xs))/100, 3), round((max(ys)-min(ys))/100, 3)]
|
||||
json.dump({"courbes": courbes, "instances": instances, "defs_bbox": bb}, open(sys.argv[2], "w"))
|
||||
import collections
|
||||
print("courbes", len(courbes), "instances", len(instances))
|
||||
print(collections.Counter(i["l"] for i in instances).most_common(12))
|
||||
print("defs bbox (échantillon)", list(bb.items())[:8])
|
||||
@@ -0,0 +1,55 @@
|
||||
import json, numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
R = json.load(open("rhino.json"))
|
||||
img = np.asarray(Image.open("base100-1.png").convert("RGB")).astype(int)
|
||||
H, W, _ = img.shape
|
||||
r, g, b = img[..., 0], img[..., 1], img[..., 2]
|
||||
bleu = ((b - r) > 30) & (b > 150) # eau
|
||||
vert = ((g - r) > 6) & ((g - b) > 25) & (g > 170) # limites de parcelles (vert pâle)
|
||||
cible = (bleu * 2.0 + vert * 1.0).astype(np.float32)
|
||||
print("pixels bleus", bleu.sum(), "verts", vert.sum())
|
||||
eau = [c for c in R["courbes"] if c["l"] == "B_EAU noues" and c["f"]]
|
||||
parc = [c for c in R["courbes"] if c["l"] == "PARCELLES"]
|
||||
allp = np.array([p for c in parc for p in c["p"]])
|
||||
x0, y1 = allp[:, 0].min(), allp[:, 1].max()
|
||||
def raster(s, shape):
|
||||
im = Image.new("F", (shape[1], shape[0]), 0); d = ImageDraw.Draw(im)
|
||||
for c in eau:
|
||||
d.polygon([((x - x0) * s, (y1 - y) * s) for x, y in c["p"]], fill=2.0)
|
||||
for c in parc:
|
||||
d.line([((x - x0) * s, (y1 - y) * s) for x, y in c["p"]], fill=1.0, width=max(1, int(s * 1.2)))
|
||||
return np.asarray(im, dtype=np.float32)
|
||||
def correl(a, bimg):
|
||||
P = (2 * H, 2 * W)
|
||||
Fa = np.fft.rfft2(a, P); Fb = np.fft.rfft2(bimg, P)
|
||||
c = np.fft.irfft2(Fa * np.conj(Fb), P)
|
||||
k = np.unravel_index(np.argmax(c), c.shape)
|
||||
return c[k], k
|
||||
best = None
|
||||
for s in np.arange(2.0, 9.0, 0.05):
|
||||
rs = raster(s, (H, W))
|
||||
v, k = correl(cible, rs)
|
||||
v = v / (np.sqrt((rs ** 2).sum()) + 1e-6)
|
||||
if best is None or v > best[0]: best = (v, s, k)
|
||||
v, s, k = best
|
||||
dy = k[0] if k[0] < H else k[0] - 2 * H
|
||||
dx = k[1] if k[1] < W else k[1] - 2 * W
|
||||
print("grossier : s=%.3f px/m dx=%d dy=%d score=%.1f" % (s, dx, dy, v))
|
||||
# affinage
|
||||
best2 = None
|
||||
for s2 in np.arange(s - 0.06, s + 0.06, 0.005):
|
||||
rs = raster(s2, (H, W)); v2, k2 = correl(cible, rs); v2 /= (np.sqrt((rs ** 2).sum()) + 1e-6)
|
||||
if best2 is None or v2 > best2[0]: best2 = (v2, s2, k2)
|
||||
v, s, k = best2
|
||||
dy = k[0] if k[0] < H else k[0] - 2 * H
|
||||
dx = k[1] if k[1] < W else k[1] - 2 * W
|
||||
print("fin : s=%.4f px/m à 100 dpi dx=%d dy=%d" % (s, dx, dy))
|
||||
# transformation mètres -> pixels (100 dpi) : px = (x - x0)*s + dx ; py = (y1 - y)*s + dy
|
||||
T = {"dpi": 100, "s": float(s), "x0": float(x0), "y1": float(y1), "dx": int(dx), "dy": int(dy)}
|
||||
json.dump(T, open("transfo.json", "w"))
|
||||
# contrôle visuel : géométrie Rhino en rouge par-dessus le dessin
|
||||
ov = Image.open("base100-1.png").convert("RGB"); d = ImageDraw.Draw(ov)
|
||||
f = lambda x, y: ((x - x0) * s + dx, (y1 - y) * s + dy)
|
||||
for c in parc: d.line([f(*p) for p in c["p"]], fill=(220, 30, 30), width=1)
|
||||
for c in eau: d.line([f(*p) for p in c["p"]] + [f(*c["p"][0])], fill=(200, 0, 200), width=2)
|
||||
ov.save("controle-recalage.png")
|
||||
@@ -0,0 +1,33 @@
|
||||
import json, numpy as np, collections
|
||||
from PIL import Image, ImageDraw
|
||||
from shapely.geometry import Polygon
|
||||
R = json.load(open("rhino.json")); T = json.load(open("transfo.json"))
|
||||
s, x0, y1, dx, dy = T["s"], T["x0"], T["y1"], T["dx"], T["dy"]
|
||||
f = lambda x, y: ((x - x0) * s + dx, (y1 - y) * s + dy)
|
||||
W, H = 1654, 1170
|
||||
cand = []
|
||||
for c in R["courbes"]:
|
||||
if not c["f"] or len(c["p"]) < 4: continue
|
||||
try: P = Polygon(c["p"])
|
||||
except Exception: continue
|
||||
if not P.is_valid: continue
|
||||
a = P.area
|
||||
if not (40 <= a <= 1500): continue
|
||||
mrr = P.minimum_rotated_rectangle
|
||||
if mrr.area <= 0 or a / mrr.area < 0.88: continue
|
||||
cx, cy = f(P.centroid.x, P.centroid.y)
|
||||
if not (0 <= cx <= W and 0 <= cy <= H): continue
|
||||
cand.append({"l": c["l"], "a": round(a, 1), "poly": P, "c": (cx, cy)})
|
||||
print(collections.Counter(c["l"] for c in cand).most_common())
|
||||
ov = Image.open("base100-1.png").convert("RGB"); d = ImageDraw.Draw(ov)
|
||||
pal = {}
|
||||
cols = [(230, 40, 40), (40, 90, 230), (20, 160, 60), (200, 120, 0), (160, 0, 200), (0, 170, 170), (120, 120, 120), (230, 0, 130)]
|
||||
for c in cand:
|
||||
col = pal.setdefault(c["l"], cols[len(pal) % len(cols)])
|
||||
xs, ys = c["poly"].exterior.xy
|
||||
d.line([f(x, y) for x, y in zip(xs, ys)], fill=col, width=3)
|
||||
y = 10
|
||||
for l, col in pal.items():
|
||||
d.rectangle([10, y, 30, y + 14], fill=col); d.text((36, y), f"{l}", fill=(0, 0, 0)); y += 18
|
||||
ov.save("controle-maisons.png")
|
||||
json.dump([{"l": c["l"], "a": c["a"], "p": [list(p) for p in c["poly"].exterior.coords]} for c in cand], open("cand_maisons.json", "w"))
|
||||
@@ -0,0 +1,77 @@
|
||||
import json, numpy as np, collections, math
|
||||
from PIL import Image, ImageDraw
|
||||
from shapely.geometry import Polygon, MultiPoint, LineString, Point
|
||||
R = json.load(open("rhino.json")); T = json.load(open("transfo.json"))
|
||||
s, x0, y1, dx, dy = T["s"], T["x0"], T["y1"], T["dx"], T["dy"]
|
||||
f = lambda x, y: ((x - x0) * s + dx, (y1 - y) * s + dy)
|
||||
W, H = 1654, 1170
|
||||
inimg = lambda x, y: 0 <= f(x, y)[0] <= W and 0 <= f(x, y)[1] <= H
|
||||
# 1. maisons existantes : rectangles de 3_ TRAITS VUS
|
||||
cand = json.load(open("cand_maisons.json"))
|
||||
exist = [Polygon(c["p"]) for c in cand if c["l"] == "3_ TRAITS VUS"]
|
||||
# faîtage : ligne intérieure la plus longue de 3_ TRAITS VUS contenue dans la maison
|
||||
lignes = [LineString(c["p"]) for c in R["courbes"] if c["l"] in ("3_ TRAITS VUS", "4_ TRAITS VUS FIINS") and not c["f"] and len(c["p"]) >= 2]
|
||||
maisons = []
|
||||
for P in exist:
|
||||
inner = [l for l in lignes if P.buffer(0.3).contains(l) and l.length > 3]
|
||||
mrr = list(P.minimum_rotated_rectangle.exterior.coords)[:4]
|
||||
e1 = np.subtract(mrr[1], mrr[0]); e2 = np.subtract(mrr[2], mrr[1])
|
||||
if inner:
|
||||
l = max(inner, key=lambda l: l.length); a, b = l.coords[0], l.coords[-1]
|
||||
ang = math.atan2(b[1] - a[1], b[0] - a[0])
|
||||
else:
|
||||
e = e1 if np.linalg.norm(e1) >= np.linalg.norm(e2) else e2; ang = math.atan2(e[1], e[0])
|
||||
maisons.append({"type": "existante", "p": [list(map(lambda v: round(v, 2), q)) for q in mrr], "faitage": round(ang, 4)})
|
||||
# 2. maisons transformées et grand bâtiment : grappes du calque A_ISOL _ COUPE
|
||||
iso = [c for c in R["courbes"] if c["l"] == "A_ISOL _ COUPE"]
|
||||
pts = [(p, i) for i, c in enumerate(iso) for p in c["p"]]
|
||||
centres = [np.mean(np.array(c["p"]), axis=0) for c in iso]
|
||||
# regroupement simple par distance (8 m)
|
||||
groupes = []
|
||||
for i, cc in enumerate(centres):
|
||||
for g in groupes:
|
||||
if min(np.linalg.norm(cc - centres[j]) for j in g) < 8: g.append(i); break
|
||||
else: groupes.append([i])
|
||||
# fusion itérative des groupes proches
|
||||
chg = True
|
||||
while chg:
|
||||
chg = False
|
||||
for a in range(len(groupes)):
|
||||
for b in range(a + 1, len(groupes)):
|
||||
if min(np.linalg.norm(centres[i] - centres[j]) for i in groupes[a] for j in groupes[b]) < 8:
|
||||
groupes[a] += groupes[b]; del groupes[b]; chg = True; break
|
||||
if chg: break
|
||||
transfo = []
|
||||
for g in groupes:
|
||||
P = MultiPoint([tuple(p) for i in g for p in iso[i]["p"]]).minimum_rotated_rectangle
|
||||
if P.area < 60: continue
|
||||
cx, cy = P.centroid.x, P.centroid.y
|
||||
if not inimg(cx, cy): continue
|
||||
mrr = list(P.exterior.coords)[:4]
|
||||
e1 = np.subtract(mrr[1], mrr[0]); e2 = np.subtract(mrr[2], mrr[1])
|
||||
e = e1 if np.linalg.norm(e1) >= np.linalg.norm(e2) else e2
|
||||
transfo.append({"type": "grand" if P.area > 300 else "transformee", "p": [[round(v, 2) for v in q] for q in mrr], "faitage": round(math.atan2(e[1], e[0]), 4), "aire": round(P.area, 1)})
|
||||
print("existantes", len(maisons), "| transformées/grand", [(t["type"], t["aire"]) for t in transfo])
|
||||
# 3. arbres : instances de blocs, couleur échantillonnée sur le dessin TOTAL
|
||||
tot = np.asarray(Image.open("total100-1.png").convert("RGB")).astype(int)
|
||||
arbres = []
|
||||
rayons = {"Ishigami's fruit tree's 1": 1.6, "ishigami foret": 2.6, "B_Existantbuisson": 1.2, "A_Existant arbre": 2.4, "0": 2.0, "Ishigami's tree's 2": 2.4}
|
||||
INST = json.load(open("instances_monde.json"))
|
||||
for it in INST:
|
||||
if it["l"] not in rayons or not inimg(it["x"], it["y"]): continue
|
||||
rayons[it["l"]] = rayons[it["l"]]
|
||||
px, py = f(it["x"], it["y"]); r = max(2, int(max(it["r"], 0.8) * s * 0.7))
|
||||
x_, y_ = int(px), int(py)
|
||||
zone = tot[max(0, y_ - r):y_ + r, max(0, x_ - r):x_ + r].reshape(-1, 3)
|
||||
col = zone[(zone.sum(1) < 690) & (zone.max(1) - zone.min(1) > 18)]
|
||||
c = col.mean(0) if len(col) > 3 else np.array([150, 185, 120])
|
||||
arbres.append({"x": it["x"], "y": it["y"], "k": it["l"], "r": round(max(it["r"], 0.8), 2), "c": "#%02x%02x%02x" % tuple(int(v) for v in c)})
|
||||
print("arbres", len(arbres), collections.Counter(a["k"] for a in arbres))
|
||||
json.dump({"maisons": maisons, "speciaux": transfo, "arbres": arbres}, open("batiments.json", "w"))
|
||||
# contrôle sur TOTAL
|
||||
ov = Image.open("total100-1.png").convert("RGB"); d = ImageDraw.Draw(ov)
|
||||
for m_ in maisons: d.polygon([f(*q) for q in m_["p"]], outline=(0, 60, 230), width=3)
|
||||
for t in transfo: d.polygon([f(*q) for q in t["p"]], outline=(230, 0, 0), width=4)
|
||||
for a in arbres[::3]:
|
||||
px, py = f(a["x"], a["y"]); rr = a["r"] * s * 0.5; d.ellipse([px - rr, py - rr, px + rr, py + rr], outline=(0, 0, 0))
|
||||
ov.save("controle-batiments.png")
|
||||
@@ -0,0 +1,32 @@
|
||||
import sys, json, rhino3dm, math
|
||||
m = rhino3dm.File3dm.Read(sys.argv[1])
|
||||
L = {i: l.Name for i, l in enumerate(m.Layers)}
|
||||
centre = {}
|
||||
for d in m.InstanceDefinitions:
|
||||
xs, ys = [], []
|
||||
for oid in d.GetObjectIds():
|
||||
ob = m.Objects.FindId(oid)
|
||||
if ob is None: continue
|
||||
try:
|
||||
b = ob.Geometry.GetBoundingBox(); xs += [b.Min.X, b.Max.X]; ys += [b.Min.Y, b.Max.Y]
|
||||
except Exception: pass
|
||||
if xs: centre[str(d.Id)] = ((min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2, (max(xs) - min(xs)), (max(ys) - min(ys)))
|
||||
out = []
|
||||
for o in m.Objects:
|
||||
g = o.Geometry
|
||||
if not isinstance(g, rhino3dm.InstanceReference): continue
|
||||
ln = L.get(o.Attributes.LayerIndex, "")
|
||||
c = centre.get(str(g.ParentIdefId))
|
||||
if not c: continue
|
||||
x = g.Xform
|
||||
wx = x.M00 * c[0] + x.M01 * c[1] + x.M03
|
||||
wy = x.M10 * c[0] + x.M11 * c[1] + x.M13
|
||||
sc = math.hypot(x.M00, x.M10)
|
||||
out.append({"l": ln, "x": round(wx / 100, 3), "y": round(wy / 100, 3), "r": round(max(c[2], c[3]) * sc / 200, 3)})
|
||||
json.dump(out, open(sys.argv[2], "w"))
|
||||
import collections
|
||||
print(len(out), collections.Counter(o["l"] for o in out).most_common(8))
|
||||
for l in ["Ishigami's fruit tree's 1", "ishigami foret", "PERSOS"]:
|
||||
a = [o for o in out if o["l"] == l]
|
||||
xs = [o["x"] for o in a]; ys = [o["y"] for o in a]; rs = sorted(o["r"] for o in a)
|
||||
print(l, "x %.0f..%.0f y %.0f..%.0f rayon médian %.2f m" % (min(xs), max(xs), min(ys), max(ys), rs[len(rs)//2]))
|
||||
@@ -0,0 +1,99 @@
|
||||
import json, math, random, numpy as np
|
||||
from PIL import Image
|
||||
from shapely.geometry import Polygon, Point, LineString
|
||||
random.seed(7)
|
||||
R = json.load(open("rhino.json")); T = json.load(open("transfo.json")); B = json.load(open("batiments.json"))
|
||||
INST = json.load(open("instances_monde.json")); ROUTES = json.load(open("routes_px.json"))
|
||||
s, x0, y1, dx, dy = T["s"], T["x0"], T["y1"], T["dx"], T["dy"]
|
||||
W, H = 1654, 1170
|
||||
px = lambda x, y: ((x - x0) * s + dx, (y1 - y) * s + dy) # Rhino m -> pixel (100 dpi)
|
||||
wd = lambda X, Y: (round((X - W / 2) / s, 2), round((Y - H / 2) / s, 2)) # pixel -> monde (x, z)
|
||||
rw = lambda x, y: wd(*px(x, y)) # Rhino m -> monde
|
||||
dedans = lambda X, Y, m=0: -m <= X <= W + m and -m <= Y <= H + m
|
||||
# --- routes : retouche ouest et carrefour (relevé sur zoom), reste = axe calculé
|
||||
princ = ROUTES["principale"]
|
||||
ouest = [(0, 180), (100, 236), (200, 292), (300, 349), (400, 405), (460, 440)]
|
||||
milieu = [p for p in princ if 490 <= p[0] <= 846 and p[1] < 880]
|
||||
bas = [p for p in princ if p[1] >= 960]
|
||||
ROUTES["principale"] = ouest + milieu + [(868, 888), (930, 935)] + bas
|
||||
routes = {k: [wd(*p) for p in v] for k, v in ROUTES.items()}
|
||||
# --- maisons
|
||||
def poly_monde(pts): return [rw(*p) for p in pts]
|
||||
maisons = [{"t": m["type"], "p": poly_monde(m["p"]), "f": m["faitage"]} for m in B["maisons"]]
|
||||
for sp in B["speciaux"]:
|
||||
maisons.append({"t": "grand" if sp["aire"] > 200 else "transformee", "p": poly_monde(sp["p"]), "f": sp["faitage"]})
|
||||
# --- arbres : palette quantifiée
|
||||
arbres = B["arbres"]
|
||||
def hexrgb(h): return tuple(int(h[i:i + 2], 16) for i in (1, 3, 5))
|
||||
# densification de la forêt (zone est, délimitée sur le dessin)
|
||||
foret = Polygon([(1335, 0), (1654, 0), (1654, 780), (1565, 752), (1160, 596), (1330, 40)])
|
||||
fond = np.asarray(Image.open("fond1654.png").convert("RGB")).astype(int)
|
||||
existants = [px(a["x"], a["y"]) for a in arbres]
|
||||
grille = {}
|
||||
for (X, Y) in existants: grille.setdefault((int(X // 20), int(Y // 20)), []).append((X, Y))
|
||||
sentier_foret = [LineString([px(*p) for p in c["p"]]) for c in R["courbes"] if c["l"] == "C_SENTIER" and not c["f"] and len(c["p"]) > 5]
|
||||
ajouts = 0
|
||||
for _ in range(9000):
|
||||
X, Y = random.uniform(1160, 1654), random.uniform(0, 780)
|
||||
if not foret.contains(Point(X, Y)): continue
|
||||
voisins = [q for gx in range(int(X // 20) - 1, int(X // 20) + 2) for gy in range(int(Y // 20) - 1, int(Y // 20) + 2) for q in grille.get((gx, gy), [])]
|
||||
if any((X - a) ** 2 + (Y - b) ** 2 < (2.9 * s) ** 2 for a, b in voisins): continue
|
||||
if any(l.distance(Point(X, Y)) < 1.6 * s for l in sentier_foret): continue
|
||||
z = fond[max(0, int(Y) - 4):int(Y) + 5, max(0, int(X) - 4):int(X) + 5].reshape(-1, 3)
|
||||
z = z[(z.sum(1) < 690) & (z.max(1) - z.min(1) > 18)]
|
||||
c = z.mean(0) if len(z) > 4 else np.array([140, 190, 140])
|
||||
r = random.uniform(1.3, 2.4)
|
||||
Xm, Ym = (X - dx) / s + x0, y1 - (Y - dy) / s
|
||||
arbres.append({"x": Xm, "y": Ym, "k": "foret+", "r": r, "c": "#%02x%02x%02x" % tuple(int(v) for v in c)})
|
||||
grille.setdefault((int(X // 20), int(Y // 20)), []).append((X, Y)); ajouts += 1
|
||||
# palette : couleurs éclaircies façon lavis, quantifiées
|
||||
def lavis(c):
|
||||
c = np.array(c, float); g = c.mean(); c = g + (c - g) * 1.15; c = c * 0.85 + 255 * 0.15
|
||||
return tuple(int(max(0, min(255, v))) for v in c)
|
||||
cols = [lavis(hexrgb(a["c"])) for a in arbres]
|
||||
pal, idx = [], []
|
||||
for c in cols:
|
||||
q = tuple(int(v // 24 * 24 + 12) for v in c)
|
||||
if q not in pal: pal.append(q)
|
||||
idx.append(pal.index(q))
|
||||
kind = {"Ishigami's fruit tree's 1": 0, "ishigami foret": 1, "foret+": 1, "A_Existant arbre": 2, "B_Existantbuisson": 3}
|
||||
arbres_out = []
|
||||
for a, i in zip(arbres, idx):
|
||||
X, Z = rw(a["x"], a["y"]); arbres_out.append([X, Z, round(a["r"], 2), i, kind.get(a["k"], 1)])
|
||||
# --- personnages
|
||||
persos = [rw(i["x"], i["y"]) for i in INST if i["l"] == "PERSOS" and dedans(*px(i["x"], i["y"]))]
|
||||
# --- eau, sentiers, étals, bassins plantés, liaisons
|
||||
def polys(layer, ferme=True, min_pts=3):
|
||||
out = []
|
||||
for c in R["courbes"]:
|
||||
if c["l"] != layer or c["f"] != ferme or len(c["p"]) < min_pts: continue
|
||||
P = [px(*p) for p in c["p"]]
|
||||
if not any(dedans(*q) for q in P): continue
|
||||
out.append([wd(*q) for q in P])
|
||||
return out
|
||||
noues = polys("B_EAU noues", True)
|
||||
liaisons = [l for l in polys("TIRE-T", False, 2) if LineString(l).length < 40]
|
||||
plantes = polys("planté", True) + polys("planté", False, 2)
|
||||
sentiers = polys("C_SENTIER", True)
|
||||
chemins = polys("C_SENTIER", False, 2)
|
||||
etals = []
|
||||
for c in R["courbes"]:
|
||||
if c["l"] == "5_ tire-T pointillés" and c["f"]:
|
||||
P = Polygon(c["p"])
|
||||
if 6 < P.area < 60:
|
||||
q = px(P.centroid.x, P.centroid.y)
|
||||
if dedans(*q): etals.append({"p": [rw(*p) for p in list(P.minimum_rotated_rectangle.exterior.coords)[:4]], "c": wd(*q)})
|
||||
# --- bulles de la page 15 (centres relevés sur la page alignée, en pixels 100 dpi)
|
||||
bulles = [("film", 880, 95, 30), ("marche", 388, 238, 38), ("ballons", 775, 240, 37), ("barbecue", 856, 238, 37), ("enfant", 937, 240, 37),
|
||||
("confiture", 1153, 203, 37), ("bouteille", 1233, 196, 37), ("pomme", 1287, 292, 35), ("miel", 1494, 300, 38), ("abeille", 1403, 360, 38),
|
||||
("fleur", 1310, 418, 38), ("arbre", 1376, 503, 37), ("biere", 157, 383, 38), ("couverts", 157, 463, 38), ("outils", 157, 543, 38),
|
||||
("musique", 573, 513, 37), ("oiseaux", 684, 578, 27), ("promeneurs", 857, 388, 38), ("bols", 1036, 525, 28), ("soin", 963, 782, 28),
|
||||
("entraide", 1259, 780, 28), ("reemploi", 225, 880, 28), ("garage", 1233, 992, 28), ("apaise1", 293, 352, 17), ("apaise2", 857, 860, 17)]
|
||||
json.dump([{"id": b[0], "x": b[1], "y": b[2], "r": b[3], "w": wd(b[1], b[2])} for b in bulles], open("bulles_px.json", "w"))
|
||||
scene = {"s": s, "W": W, "H": H, "Wm": round(W / s, 2), "Hm": round(H / s, 2), "maisons": maisons, "pal": pal, "arbres": arbres_out, "persos": persos,
|
||||
"noues": noues, "liaisons": liaisons, "plantes": plantes, "sentiers": sentiers, "chemins": chemins, "etals": etals, "routes": routes,
|
||||
"bulles": [{"id": b[0], "w": wd(b[1], b[2])} for b in bulles]}
|
||||
json.dump(scene, open("scene.json", "w"), separators=(",", ":"))
|
||||
print("maisons", len(maisons), "arbres", len(arbres_out), "(forêt ajoutée", ajouts, ") palette", len(pal), "persos", len(persos))
|
||||
print("noues", len(noues), "liaisons", len(liaisons), "plantés", len(plantes), "sentiers", len(sentiers), "chemins", len(chemins), "étals", len(etals))
|
||||
import os; print("scene.json %.0f Ko" % (os.path.getsize("scene.json") / 1024))
|
||||
@@ -0,0 +1,62 @@
|
||||
"""06 — Les deux sols du quartier (2035 et aujourd'hui), depuis le plan vectoriel Illustrator.
|
||||
|
||||
Entrées : TRANSFO_TOTAL.pdf (Illustrator, 155 calques), la planche de fond du concours (lavis),
|
||||
la texture de papier aquarelle, transfo.json (recalage), scene.json (axes des routes).
|
||||
Sorties : lignes2035.png, lignesauj.png, sol2035.jpg, solauj.jpg.
|
||||
|
||||
Principe : on éteint dans le PDF les calques qui deviendront des objets 3D (arbres, ombres,
|
||||
personnages) ; pour « aujourd'hui », on éteint aussi tous les calques « 8_ NEW » et « 10_NEW ».
|
||||
Le lavis (jardins vert pâle, routes grises, forêt ou champ) est multiplié sous les traits,
|
||||
avec un grain de papier passe-haut (les grandes taches de la texture sont retirées).
|
||||
|
||||
Usage : python 06_sols.py <TOTAL.pdf> <texture_papier.jpeg> [dpi=155]
|
||||
"""
|
||||
import sys, json
|
||||
import numpy as np
|
||||
import pymupdf
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
PDF, PAPIER = sys.argv[1], sys.argv[2]
|
||||
DPI = int(sys.argv[3]) if len(sys.argv) > 3 else 155
|
||||
T = json.load(open("transfo.json")); s = T["s"] # px/m à 100 dpi
|
||||
SC = json.load(open("scene.json"))
|
||||
|
||||
def rendre(eteindre, sortie):
|
||||
doc = pymupdf.open(PDF)
|
||||
for u in doc.layer_ui_configs():
|
||||
if eteindre(u["text"]):
|
||||
doc.set_layer_ui_config(u["number"], action=2) # 2 = éteint
|
||||
doc[0].get_pixmap(dpi=DPI).save(sortie)
|
||||
|
||||
objets_3d = lambda t: any(k in t for k in ["FORET", "VEGETATION", "ombres", "OMBRES", "PERSOS", "Calque 37"]) or t == "0"
|
||||
rendre(objets_3d, "lignes2035.png")
|
||||
rendre(lambda t: objets_3d(t) or t.startswith("8_ NEW") or t.startswith("10_NEW"), "lignesauj.png")
|
||||
|
||||
L35 = Image.open("lignes2035.png").convert("RGB"); Wt, Ht = L35.size; k = Wt / 1654
|
||||
tex = Image.open(PAPIER).convert("L").resize((Wt // 2, Ht // 2))
|
||||
t = np.asarray(tex).astype(np.float32); lo = np.asarray(tex.filter(ImageFilter.GaussianBlur(14))).astype(np.float32)
|
||||
g = np.clip((t - lo) / ((t - lo).std() + 1e-6), -2.5, 2.5)
|
||||
g = np.asarray(Image.fromarray(((g + 2.5) / 5 * 255).astype(np.uint8)).resize((Wt, Ht), Image.BILINEAR)).astype(np.float32) / 255 - 0.5
|
||||
pap = 1.0 + 0.035 * g
|
||||
|
||||
routes = {n: [(x * s + 827, z * s + 585) for x, z in pts] for n, pts in SC["routes"].items()}
|
||||
largeur = {"principale": 9.5, "verticale": 7.5, "anneau": 6.5, "liaison_anneau": 6.5, "droite": 7.0}
|
||||
foret = [(1335, 0), (1654, 0), (1654, 780), (1565, 752), (1160, 596), (1330, 40)] # relevé sur la planche
|
||||
|
||||
def lavis(couleur_est):
|
||||
w = Image.new("RGB", (Wt, Ht), (239, 240, 219)); d = ImageDraw.Draw(w)
|
||||
d.polygon([(x * k, y * k) for x, y in foret], fill=couleur_est)
|
||||
for n, pts in routes.items():
|
||||
wpx = largeur[n] * s * k
|
||||
d.line([(x * k, y * k) for x, y in pts], fill=(229, 229, 224), width=int(wpx), joint="curve")
|
||||
for x, y in pts:
|
||||
d.ellipse([x * k - wpx / 2, y * k - wpx / 2, x * k + wpx / 2, y * k + wpx / 2], fill=(229, 229, 224))
|
||||
return np.asarray(w.filter(ImageFilter.GaussianBlur(2))).astype(np.float32) * pap[..., None]
|
||||
|
||||
def composer(lignes, lav, sortie):
|
||||
l = np.asarray(lignes.convert("RGB").resize((Wt, Ht))).astype(np.float32) / 255
|
||||
Image.fromarray(np.clip(lav * l, 0, 255).astype(np.uint8)).save(sortie, quality=80, optimize=True, progressive=True)
|
||||
|
||||
composer(L35, lavis((226, 236, 212)), "sol2035.jpg") # forêt nourricière
|
||||
composer(Image.open("lignesauj.png"), lavis((241, 236, 214)), "solauj.jpg") # aujourd'hui : un champ
|
||||
print("sols écrits", Wt, "x", Ht)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""07 — Les bulles d'icônes de la planche « le quartier en 2030 » (TMIP 2024, p. 15).
|
||||
|
||||
La carte de cette page est une image de 1 190 px (72 dpi) ; les bulles, elles, sont vectorielles.
|
||||
On retire l'image de la page (rédaction PDF, images seules), on rend à 300 dpi et on découpe
|
||||
chaque bulle en rond sur fond blanc. Les centres sont relevés sur la page, alignée au pixel
|
||||
près sur la planche du concours (décalage mesuré : 1 px).
|
||||
|
||||
Usage : python 07_bulles.py <presentation_TMIP_2024.pdf> (lit bulles_px.json, écrit bulles/*.png)
|
||||
"""
|
||||
import sys, os, json
|
||||
import pymupdf
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
doc = pymupdf.open(sys.argv[1]); page = doc[14]
|
||||
page.add_redact_annot(page.rect)
|
||||
page.apply_redactions(images=pymupdf.PDF_REDACT_IMAGE_REMOVE, graphics=pymupdf.PDF_REDACT_LINE_ART_NONE, text=pymupdf.PDF_REDACT_TEXT_NONE)
|
||||
page.get_pixmap(dpi=300).save("p15_sans_carte.png")
|
||||
p = Image.open("p15_sans_carte.png").convert("RGBA")
|
||||
os.makedirs("bulles", exist_ok=True)
|
||||
for b in json.load(open("bulles_px.json")):
|
||||
cx, cy, r = b["x"] * 3, b["y"] * 3, b["r"] * 3 + 6
|
||||
c = p.crop((int(cx - r), int(cy - r), int(cx + r), int(cy + r)))
|
||||
m = Image.new("L", c.size, 0); ImageDraw.Draw(m).ellipse([2, 2, c.size[0] - 3, c.size[1] - 3], fill=255)
|
||||
fond = Image.new("RGBA", c.size, (255, 255, 255, 255)); fond.alpha_composite(c); fond.putalpha(m.filter(ImageFilter.GaussianBlur(1)))
|
||||
fond.resize((112, 112), Image.LANCZOS).save("bulles/%s.png" % b["id"], optimize=True)
|
||||
print(len(os.listdir("bulles")), "bulles")
|
||||
@@ -0,0 +1,39 @@
|
||||
"""08 — La planche du réseau hydrique alternatif, nettoyée de ses textes, pour le zoom « cycle de l'eau ».
|
||||
|
||||
Source : le PDF d'impression du concours Habiter Demain 2020 (page 6, dessins à 417 dpi),
|
||||
identique au pixel près à la page 13 de la présentation TMIP 2024 (décalage mesuré : 0).
|
||||
Les blocs de texte sont effacés : en dégradé dans la bande haute (le fond y passe du blanc au crème),
|
||||
en aplat clair ailleurs. Les numéros d'étape deviennent des points actifs de la page web.
|
||||
Si Jules fournit les originaux sans texte, remplacer cette étape par eux.
|
||||
|
||||
Usage : python 08_planche_eau.py <concours_IMPRESSION.pdf>
|
||||
Sorties : eau_crop.jpg (2422 x 1290), eau_crop.json (points actifs et trajets, repère 100 dpi recadré)
|
||||
"""
|
||||
import sys, json, subprocess
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from scipy.ndimage import median_filter
|
||||
|
||||
subprocess.run(["pdftoppm", "-f", "6", "-l", "6", "-r", "150", "-png", sys.argv[1], "c6_150"], check=True)
|
||||
im = np.asarray(Image.open("c6_150-06.png").convert("RGB")).astype(np.float32); k = 1.5
|
||||
haut = [(55,28,1210,118),(85,443,305,543),(205,452,640,462),(632,452,642,532),(950,430,1113,572),(1118,428,1342,567)]
|
||||
bas = [(958,568,968,652),(958,645,1182,655),(1226,560,1236,640),(78,722,248,818),(222,724,325,736),(88,925,282,1098),(230,932,495,944),
|
||||
(578,936,752,952),(578,954,803,1118),(897,938,995,952),(915,954,1098,1118),(751,896,763,936),(915,905,926,938),
|
||||
(1438,493,1620,752),(1410,657,1442,667),(1445,745,1622,962),(1355,752,1450,762),(1445,980,1615,1078),(1275,868,1462,878),(1453,868,1463,992),
|
||||
(1158,978,1372,1090),(1165,948,1175,986),(480,1112,1220,1152),(1520,1100,1615,1145)]
|
||||
for x0, y0, x1, y1 in haut:
|
||||
X0, Y0, X1, Y1 = int(x0 * k), int(y0 * k), int(x1 * k), int(y1 * k)
|
||||
h = median_filter(np.median(im[max(0, Y0 - 5):Y0, X0:X1], axis=0), size=(41, 1))
|
||||
b = median_filter(np.median(im[Y1:Y1 + 5, X0:X1], axis=0), size=(41, 1))
|
||||
t = np.linspace(0, 1, Y1 - Y0)[:, None, None]; im[Y0:Y1, X0:X1] = h[None] * (1 - t) + b[None] * t
|
||||
for x0, y0, x1, y1 in bas:
|
||||
X0, Y0, X1, Y1 = int(x0 * k), int(y0 * k), int(x1 * k), int(y1 * k)
|
||||
a = np.concatenate([im[max(0, Y0 - 8):Y0, X0:X1].reshape(-1, 3), im[Y1:Y1 + 8, X0:X1].reshape(-1, 3), im[Y0:Y1, max(0, X0 - 8):X0].reshape(-1, 3), im[Y0:Y1, X1:X1 + 8].reshape(-1, 3)])
|
||||
clair = a[a.mean(1) > 228]; im[Y0:Y1, X0:X1] = np.median(clair if len(clair) > 20 else a, axis=0)
|
||||
Image.fromarray(np.clip(im, 0, 255).astype(np.uint8)).crop((30, 195, 2452, 1485)).save("eau_crop.jpg", quality=78, optimize=True, progressive=True)
|
||||
pts = {"h1": (430, 790), "h2": (610, 850), "h3": (637, 612), "h4": (840, 660), "h5": (855, 800), "h6": (1130, 700), "h7": (1215, 790), "h8": (1378, 625),
|
||||
"h9": (1320, 780), "h10": (1255, 880), "h11": (1105, 875), "h1b": (630, 300), "h5b": (1025, 372)}
|
||||
flux = {"usees": [(592,822),(640,872),(700,925),(760,948),(830,950),(885,935),(940,895),(1000,850),(1050,805),(1090,765)], "pluie": [(585,748),(630,750),(690,758)]}
|
||||
off = (20, 130)
|
||||
json.dump({"W": 1615, "H": 860, "pts": {n: [x - off[0], y - off[1]] for n, (x, y) in pts.items()}, "flux": {n: [[x - off[0], y - off[1]] for x, y in l] for n, l in flux.items()}}, open("eau_crop.json", "w"))
|
||||
print("planche de l'eau écrite")
|
||||
@@ -0,0 +1,37 @@
|
||||
# Pipeline « axo » — du dossier du concours à la maquette 3D
|
||||
|
||||
Écrit pendant le test axonométrique du 27/09/2026. Chaque étape est rejouable ; aucune géométrie n'est dessinée à la main hors des relevés signalés.
|
||||
|
||||
## Sources (hors Dropbox : copier dans `sources/`, ignoré par git)
|
||||
|
||||
| Fichier | Où dans le vault | Rôle |
|
||||
|---|---|---|
|
||||
| `plan de quartier-concours VP TRANSFO.3dm` | `Archives TMIP/04_ArchiJeunes/02_rhino,autocad,illus/PL-quartier transformé/` | géométrie en mètres (maisons, noues, sentiers, liaisons, étals, 3 962 blocs d'arbres et de personnages) |
|
||||
| `…_TRANSFO_TOTAL.pdf` (Illustrator, **vectoriel, 155 calques**) | même dossier, `01_ai, pdf/` | le sol : ton dessin exact, calques pilotables |
|
||||
| `…_TRANSFO_fond.png` | même dossier, `doc final/` | lavis de la planche finale (sert au recalage des couleurs) |
|
||||
| `grey-watercolor-texture-7.jpeg` | même dossier, `00_psd textures/` | grain du papier |
|
||||
| présentation TMIP 2024 (PDF) | `1 PROJETS/TMIP/TMIP présentation/` | page 15 : bulles vectorielles (la carte y est une image de 1 190 px, inutilisable en grand) |
|
||||
| rendu A3 d'impression du concours 2020 | `Archives TMIP/04_ArchiJeunes/rendu/…IMPRESSION…pdf` | page 6 : planche de l'eau à 417 dpi |
|
||||
|
||||
À ne pas prendre : le rendu A3 « léger » (144 dpi) et ORENO 10p (200 dpi, c'est la maquette du site RE(in)NOVER de 2023, référence pour la page de couverture).
|
||||
|
||||
## Ordre
|
||||
|
||||
```
|
||||
01_dump_rhino.py <TRANSFO.3dm> rhino.json # toutes les courbes et instances, en mètres
|
||||
02_recalage.py # échelle et décalage Rhino -> planche (corrélation eau + parcelles) : 6,15 px/m à 100 dpi, soit 1:640
|
||||
03a_maisons_candidates.py # rectangles du calque 3_ TRAITS VUS = 24 maisons existantes
|
||||
04_instances_monde.py <TRANSFO.3dm> instances_monde.json # vraie position des blocs (centre de la définition x transformation)
|
||||
03b_batiments_arbres.py # maisons transformées et grand bâtiment (grappes du calque d'isolation), arbres et couleurs échantillonnées
|
||||
05_scene.py # scene.json en mètres monde : maisons, arbres (palette lavis), forêt densifiée, personnages, eau, sentiers, étals, routes, bulles
|
||||
06_sols.py <TOTAL.pdf> <papier.jpeg> # sol2035.jpg, solauj.jpg
|
||||
07_bulles.py <presentation_2024.pdf> # bulles/*.png
|
||||
08_planche_eau.py <concours_IMPRESSION.pdf> # eau_crop.jpg, eau_crop.json
|
||||
../../prototypes/axo-test/assembler.py # la maquette autonome
|
||||
```
|
||||
|
||||
Contrôles visuels à regarder à chaque étape : superposition Rhino/planche (recalage), rectangles des maisons, cercles des arbres, axes des routes. Les relevés faits à l'œil (et donc à revérifier si la planche change) : axes des routes hors grande diagonale, contour de la forêt, centres des 25 bulles, points actifs et trajets de la planche de l'eau.
|
||||
|
||||
## Chiffres de ce premier passage
|
||||
|
||||
31 volumes (24 maisons existantes, 6 transformées, 1 grand bâtiment) · 3 704 arbres (2 577 de haies, 776 de forêt dont 150 ajoutés, 54 existants, 297 buissons) · 53 personnages · 11 noues · 39 liaisons maison-noue · 9 étals · 5 routes · 25 bulles · page autonome de 1,66 Mo.
|
||||
@@ -1,3 +1,7 @@
|
||||
rhino3dm
|
||||
shapely
|
||||
matplotlib
|
||||
numpy
|
||||
scipy
|
||||
pillow
|
||||
pymupdf
|
||||
|
||||
Reference in New Issue
Block a user