

********************opencv  readme  ***********************
太好了！你已經有 **10,000 筆活塞圖**，可以把它當成「資料 → 參數 → 實體模型」的自動化流水線；另外如果有**實體新品**也能用電腦視覺/量測把尺寸抓回來。下面給你兩條完整方案，直接落地到你現在的 FreeCAD 生成器。

---

# A. 用 10k 資料「生出新活塞」：資料 → 參數 → FreeCAD

## 1) 把資料整理成「參數標註」

先把每筆活塞設計抽成 **設計參數**（我們的 `generate_piston_model.py` 就吃這些）：

* 幾何主參數：`bore, crown_height, skirt_length, pin_diameter, pin_z_offset`
* 環槽：`ring.{count,width,depth,first_z,spacing}`
* 冠部凹：`crown_dish.{type,diameter,depth}`
* 結構：`wall_thickness, crown_thickness, ribs.*, pin_boss.*, slipper.*`
* 製圖/導出用：`name`（品號/版次）

> 來源是 2D 圖或 3D 模型都行；若是圖檔，建議先半自動抽取（下方 B 有影像量測的辦法），或以現有 BOM/圖檔欄位對應出來。

## 2) 建「需求 → 參數」的模型（監督學習）

你可以定義輸入為**需求條件**（例如目標缸徑範圍、壓縮比/腔形偏好、重量目標…），輸出為**設計參數**。先從穩定、好訓練的回歸樹開始：

```python
# pip install scikit-learn joblib
import json, pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from joblib import dump

# 1) 準備資料：X=需求(可先用基本需求: 目標bore、重量上限、預期槽數…)，y=參數
df = pd.read_parquet("pistons_10k.parquet")  # 你整理好的標註集
X = df[["req_bore", "req_mass", "req_slots"]]             # 自行定義的需求特徵
y = df[["bore","crown_height","skirt_length","pin_diameter",
        "ring_count","ring_width","ring_depth","ring_first_z","ring_spacing",
        "dish_d","dish_depth"]]

Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.1, random_state=0)

# 2) 多輸出回歸（或每個 y_i 各訓一個 RF）
rf = RandomForestRegressor(n_estimators=400, random_state=0, n_jobs=-1)
rf.fit(Xtr, ytr)

print("R^2 on holdout:", rf.score(Xte, yte))
dump(rf, "piston_param_rf.joblib")
```

## 3) 加入「設計優化」：用貝葉斯最佳化/遺傳演算法做參數微調

當你有性能評分（例如重量、表面面積、估計應力…），可做 **目標函數** → 最優參數，再餵給 FreeCAD。
簡易版（無外部 CAE）就用輕量代理模型 + 懲罰約束：

```python
# pip install scikit-optimize
from skopt import gp_minimize
from skopt.space import Real, Integer

# 定義搜尋空間（以實際製程/公差限制為準）
space = [
    Real(80, 100,     name="bore"),
    Real(15, 30,      name="crown_height"),
    Real(25, 45,      name="skirt_length"),
    Real(15, 22,      name="pin_diameter"),
    Integer(2, 4,     name="ring_count"),
    Real(2.5, 4.5,    name="ring_width"),
    Real(0.8, 1.5,    name="ring_depth"),
]

def score(params):
    # 假想的代理損失（越小越好）: 重量估計 + penalty
    (bore,crown,skirt,pin,rc, rw, rd) = params
    mass_proxy = 3.14*(bore/2)**2*(crown+skirt) - 100*rd   # 只是示意
    penalty = 0.0
    if rw*rc > 15: penalty += 50    # 避免槽佔太多高
    return mass_proxy + penalty

res = gp_minimize(score, space, n_calls=40, random_state=0)
best = res.x
print("best params:", best)
```

把 `best` 或 RF 的預測結果整成我們的 JSON 欄位，丟給 FreeCAD 生成：

```python
import subprocess, json, tempfile, os

design = {
  "bore": best[0], "crown_height": best[1], "skirt_length": best[2],
  "pin_diameter": best[3],
  "ring": {"count": int(best[4]), "width": best[5], "depth": best[6], "first_z": 3.0, "spacing": 5.0},
  "crown_dish": {"type":"flat","diameter": 0.6*best[0], "depth": 2.0},
  "wall_thickness": 3.0, "crown_thickness": 6.0
}
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".json")
json.dump({"design": design}, open(tmp.name,"w"))
subprocess.run([
  r"C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe",
  r"C:\xampp\htdocs\cs_ai\python\run_freecad_wrapper.py"  # 你也可直接呼叫 generate_piston_model.py
])
```

> 如果你有 FEM/CFD 的離線結果，也能用**代理模型**（例如 XGBoost/NN）學出「參數 → 應力/溫度/變形」，再把它塞回最佳化目標。這樣就變成「快速生成 + 估算 + 微調」的閉環。

---

# B. 有「新活塞實物」時，如何量測尺寸 → 反饋到參數

你有兩種路徑：**手工量測**（最可靠）+ **影像量測**（快速半自動）。

## B1. 手工量測 → 直接填 JSON

準備單頁 checklist（照著 caliper/高度規量）：

* `bore`（最大外徑）、`crown_height`（冠部高度）、`skirt_length`
* `pin_diameter`、`pin_z_offset`（中心到中線距）
* 環槽：數量、寬度、深度、第一道位置 `first_z`、間距 `spacing`
* 冠部凹：直徑、深度（球面 or 平底）
* 結構：壁厚、冠厚；是否有 `slipper`、`ribs`、`pin_boss`

填進 JSON → 丟給 `generate_piston_model.py` 即可產生復刻 3D。

## B2. 影像/視覺量測（OpenCV 半自動）

用**側視照** + **頂視照** 各一張，放一張 **ArUco 尺度標記**（或鋼尺）校正毫米/像素。流程：

1. 拍照 Tips

* 鏡頭軸向**垂直**於零件面，避免透視誤差；環境散射光。
* 在畫面放一張 **ArUco marker**（已知邊長，如 20.00 mm）。
* 側視照可量：高度、環槽 z 位置與寬度。頂視照可量：外徑、pin 孔徑與位置、冠部凹直徑。

2. 量測範例（OpenCV，mm/pixel 標定 + 圓與槽偵測）

```python
# pip install opencv-python numpy
import cv2, numpy as np

# --- 取比例：藉由 ArUco marker (DICT_4X4_50) 邊長 known_mm ---
aruco = cv2.aruco
dict4 = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
params = aruco.DetectorParameters()
detector = aruco.ArucoDetector(dict4, params)

img = cv2.imread("top_view.jpg")
corners, ids, _ = detector.detectMarkers(img)
assert ids is not None, "找不到 ArUco"

# 假設只放一張，已知邊長 20.0 mm
c = corners[0][0]
edge_px = np.linalg.norm(c[0]-c[1])  # 單邊像素長
mm_per_px = 20.0 / edge_px

# --- 頂視：抓外徑與 pin 孔 ---
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (9,9), 2)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, dp=1.2,
                           minDist=50, param1=100, param2=30,
                           minRadius=50, maxRadius=0)  # 視影像調整
circles = np.uint16(np.around(circles[0]))

# 取最大圓當外徑
radii = [c[2] for c in circles]
Rmax = max(radii)
bore_mm = 2*Rmax*mm_per_px

# 取第二大圓近似 pin 孔（若頂視可見）
cand = sorted(radii, reverse=True)
pin_mm = None
if len(cand) >= 2:
    R2 = cand[1]
    pin_mm = 2*R2*mm_per_px

print("bore(mm)≈", round(bore_mm,2), " pin_d(mm)≈", pin_mm)

# --- 側視：沿 Z（垂直）方向掃描邊緣，找環槽位置與寬度 ---
side = cv2.imread("side_view.jpg", cv2.IMREAD_GRAYSCALE)
edge = cv2.Canny(side, 50, 150)
# 取一條靠外徑的垂直掃描線（x=外輪廓略內側）
x = int(edge.shape[1]*0.6)
col = edge[:, x]
# 找到一段段凹槽(邊緣強度低/空白)的區間；這裡示意用投影與閾值
proj = 255 - col  # 邊緣少=值大
th = np.percentile(proj, 95)
groove_spans = []
in_gap = False; start = 0
for z in range(len(proj)):
    if proj[z] > th and not in_gap:
        in_gap = True; start=z
    if proj[z] <= th and in_gap:
        in_gap = False; groove_spans.append((start, z))
if in_gap: groove_spans.append((start, len(proj)-1))

# 轉成 mm 與 z 位置
ring_widths = [(z2-z1)*mm_per_px for (z1,z2) in groove_spans]
print("groove widths(mm)≈", [round(w,2) for w in ring_widths])
```

> 這段是**骨架**：實務上要針對你相機/布光調整 `HoughCircles/Canny` 參數，並做一點型態學過濾來穩定結果。
> 有了 `mm_per_px` 之後，其他量（`crown_height, skirt_length, ring.first_z`）都能從垂直距離轉 mm。

3. 產出 JSON → 丟給 FreeCAD
   把影像量出的尺寸整理成我們的 `design` 結構，寫成 `params_new.json`，再呼叫 `run_freecad_wrapper.py` 生成 FCStd/STEP/STL。

---

# C. 串起來的一鍵流程（建議架構）

1. **規格輸入**（前端）：輸入需求/目標 → 送到後端
2. **參數預測**（後端）：用 RF/NN 產生初始參數
3. **約束修正**：用我們腳本內建的 `validate_and_autofix` 保障幾何合理
4. **（可選）最佳化**：以重量/散熱/應力代理做貝葉斯微調
5. **FreeCAD 生成**：呼叫 `generate_piston_model.py` → 出 FCStd/STEP/STL
6. **檢核報告**：把輸入/輸出參數、預測不確定度、最佳化目標寫成 CSV/HTML
7. **資料回饋**：把使用者選的版本回寫資料庫，擴充你的 10k → 主動學習

---

# 想要我幫你什麼？

* 我可以把 **影像量測** 的骨架程式改成「指定兩張圖就輸出 JSON」。
* 或把 **RF/最佳化** 包成一支 `design_new_piston.py`，輸入需求（如 `--target-bore 86 --max-mass 300g`），自動吐出 `params.json` 並直接呼叫 FreeCAD 生模型。
* 如果你願意分享一小段匿名化的資料欄位（不含機密），我可以替你寫出**可直接訓練**與**推論**的完整範例。


*********************opencv end ******************






********************ai_infer_piston_all.py read me************************
搞定！我幫你把「一次輸出多顆」的整套批次工具都生好了 ✅

把以下檔案下載到 `C:\xampp\htdocs\cs_ai\python\`：

* [req\_dims\_batch.json](sandbox:/mnt/data/req_dims_batch.json)（多顆的輸入清單）
* [batch\_ai\_infer\_and\_build.py](sandbox:/mnt/data/batch_ai_infer_and_build.py)（批次：推論→產生 JSON→FreeCAD 建模）
* [run\_ai\_batch\_full.ps1](sandbox:/mnt/data/run_ai_batch_full.ps1)（PowerShell 批次執行）
* [run\_ai\_batch\_full.bat](sandbox:/mnt/data/run_ai_batch_full.bat)（BAT 批次執行；建議用這個，避開 PS 政策）

---

## 怎麼用（建議用 .bat）

1. 把你的圖片放到：

```
C:\xampp\htdocs\cs_ai\new_samples\new_piston_v1.png
C:\xampp\htdocs\cs_ai\new_samples\new_piston_v2.png
C:\xampp\htdocs\cs_ai\new_samples\new_piston_v3.png
```

2. 開啟並編輯 `req_dims_batch.json`（已放三個示例）：

```json
[
  { "name": "piston_batch_v1", "image": "C:\\xampp\\htdocs\\cs_ai\\new_samples\\new_piston_v1.png", "dims": [85, 60, 18, 3.0, 1.0] },
  { "name": "piston_batch_v2", "image": "C:\\xampp\\htdocs\\cs_ai\\new_samples\\new_piston_v2.png", "dims": [88, 62, 19, 3.2, 1.1] },
  { "name": "piston_batch_v3", "image": "C:\\xampp\\htdocs\\cs_ai\\new_samples\\new_piston_v3.png", "dims": [86, 58, 18, 3.0, 1.2] }
]
```

> `dims` 的 5 個值順序要跟你訓練時一致：
> `diameter, length, pin_hole_diameter, ring_groove_1_width, ring_groove_1_depth`

3. 直接雙擊 **`run_ai_batch_full.bat`**
   它會逐筆：

* 用 `ai_infer_piston.py` 產生 `out\params_<name>.json`
* 呼叫 FreeCAD 生成 `out\<name>.FCStd / .STEP / .STL`

---

## 想客製路徑？

* 編輯 `run_ai_batch_full.bat` 或 `run_ai_batch_full.ps1` 最上方路徑即可。
* 或直接改 `batch_ai_infer_and_build.py` 裡的常數（`FREECAD/AI_INFER/GEN_PY/BATCH_SPEC/OUTDIR`），也支援用環境變數覆蓋。

---

## 小提醒

* 權重檔預設讀：`C:\xampp\htdocs\cs_ai\ai_models\piston_model_weights_final.weights.h5`
* 如果你的 FreeCAD 安裝路徑不同，記得改 `.bat/.ps1` 裡的 `FREECAD_EXE`。
* 如果 `ai_infer_piston.py` 對輸出參數的「切分比例」想換（例如冠部:裙長=40:60），在該檔把比例改一下即可。

需要我幫你把 `req_dims_batch.json` 直接改成你「實際想跑的多組規格」嗎？你把每顆的 `name / image / dims` 傳給我，我幫你填好。
******************************end**********************************************************************************



************************************************cvs
好了～我把你要的三件事一次補齊了：**參數安全範圍（clamp）＋幾何檢核＋批次 CSV 總表**，而且整合進批次流程。

### 下載這三個檔案到 `C:\xampp\htdocs\cs_ai\python\`

* **更安全的推論腳本**： [ai\_infer\_piston\_safe.py](sandbox:/mnt/data/ai_infer_piston_safe.py)
* **批次執行（含 CSV 總表）**： [batch\_ai\_infer\_and\_build\_safe.py](sandbox:/mnt/data/batch_ai_infer_and_build_safe.py)
* **一鍵執行 .BAT**： [run\_ai\_batch\_full\_safe.bat](sandbox:/mnt/data/run_ai_batch_full_safe.bat)

> 這些不會改到你原本的檔案（`ai_infer_piston.py` / `batch_ai_infer_and_build.py`），只是提供一套 **Safe** 版本，你可以並存使用。

---

## 有哪些改進？

### 1) 推論端加上「幾何安全範圍」與檢核

在 `ai_infer_piston_safe.py` 中，我針對關鍵輸出加上上下限（可用 CLI 覆蓋）：

* `bore`：60–110 mm
* `total_height`：20–80 mm → 自動切成 `crown_height` 與 `skirt_length`（預設比 40:60，可用 `--crown-ratio` 改）
* `pin_diameter`：12–24 mm
* `ring.width`：0.8–5.0 mm
* `ring.depth`：0.5–3.0 mm
* 並保證 `ring.width >= ring.depth + 0.2`（避免幾何不合理）

參數可透過指令調整，例如：

```powershell
python ai_infer_piston_safe.py --weights ... --image ... --dims 85 60 18 3 1 ^
  --out out\params_x.json --name x --csv out\batch_summary.csv ^
  --crown-ratio 0.42 --pin-min 14 --pin-max 22
```

### 2) 自動產出 **批次總表 CSV**

* Safe 版推論支援 `--csv` 參數，會把每顆的關鍵尺寸與檔名 **追加**到 `out\batch_summary.csv`。
* 欄位：`name,image,bore,crown_height,skirt_length,pin_diameter,ring_width,ring_depth,crown_ratio,out_json`。

### 3) 安全版批次腳本

`batch_ai_infer_and_build_safe.py`：

* 讀 `req_dims_batch.json`
* 逐筆呼叫 **safe 推論腳本**（帶 `--name` 與 `--csv`）
* 再呼叫 FreeCAD 導出 `FCStd / STEP / STL`
* 結束時回報 **Summary CSV 路徑** 與 **輸出資料夾**

搭配的 `.bat`（`run_ai_batch_full_safe.bat`）已把環境變數都設定好了（FreeCAD 路徑、權重路徑、輸出 CSV），你直接雙擊就能跑。

---

## 怎麼跑（建議用 safe 版）

```powershell
cd C:\xampp\htdocs\cs_ai\python
.\run_ai_batch_full_safe.bat
```

跑完後檢查：

```
C:\xampp\htdocs\cs_ai\python\out\
  ├─ batch_summary.csv   ← 總表
  ├─ params_*.json
  ├─ *.FCStd
  ├─ *.step
  └─ *.stl
```

---

如果你希望把「安全上下限」換成你公司內規（例如不同缸徑族群用不同 pin 孔範圍），把那幾個值寫給我，我直接幫你固化到程式預設值裡。或者你要我改成 **使用資料庫的料號族群 → 自動載入對應 clamp 範圍**，也可以加一個小查表。
2

了解 👍 你這邊看不到直接下載按鈕。沒關係，我把三個檔案的程式碼完整貼給你，你只要在 `C:\xampp\htdocs\cs_ai\python\` 新建檔案，複製貼上就能用了。

---

## 1. **ai\_infer\_piston\_safe.py**

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ai_infer_piston_safe.py
- 加入幾何 clamp（安全範圍）、簡單檢核
- 可輸出 CSV 總表（batch summary）
"""

import argparse, os, json, numpy as np, csv
from pathlib import Path
from PIL import Image

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Conv2D, Flatten, Dense, Concatenate

def build_model():
    image_input = Input(shape=(256, 256, 1))
    x = Conv2D(32, (3,3), activation='relu')(image_input)
    x = Conv2D(64, (3,3), activation='relu')(x)
    x = Flatten()(x)
    image_features = Dense(128, activation='relu')(x)

    dimensions_input = Input(shape=(5,))
    y = Dense(64, activation='relu')(dimensions_input)
    dimensions_features = Dense(128, activation='relu')(y)

    combined = Concatenate()([image_features, dimensions_features])
    output = Dense(5, name="predicted_dims")(combined)
    model = Model(inputs=[image_input, dimensions_input], outputs=output)
    model.compile(optimizer='adam', loss='mse')
    return model

def load_and_preprocess_image(path):
    img = Image.open(path).convert('L').resize((256,256))
    arr = np.array(img, dtype=np.float32) / 255.0
    return np.expand_dims(arr, axis=-1)

def clamp(v, lo, hi): return float(max(lo, min(hi, v)))

def parse_args():
    ap = argparse.ArgumentParser()
    ap.add_argument("--weights", required=True)
    ap.add_argument("--image", required=True)
    g = ap.add_mutually_exclusive_group(required=True)
    g.add_argument("--dims", nargs=5, type=float)
    g.add_argument("--dims-json")
    ap.add_argument("--out", required=True)
    ap.add_argument("--name", default=None, help="名稱（CSV 用）")
    ap.add_argument("--csv", default=None, help="CSV 路徑")
    ap.add_argument("--crown-ratio", type=float, default=0.40)
    # clamp 範圍
    ap.add_argument("--bore-min", type=float, default=60.0)
    ap.add_argument("--bore-max", type=float, default=110.0)
    ap.add_argument("--height-min", type=float, default=20.0)
    ap.add_argument("--height-max", type=float, default=80.0)
    ap.add_argument("--pin-min", type=float, default=12.0)
    ap.add_argument("--pin-max", type=float, default=24.0)
    ap.add_argument("--ringw-min", type=float, default=0.8)
    ap.add_argument("--ringw-max", type=float, default=5.0)
    ap.add_argument("--ringd-min", type=float, default=0.5)
    ap.add_argument("--ringd-max", type=float, default=3.0)
    return ap.parse_args()

def maybe_append_csv(csv_path, row, header_order):
    new_file = not os.path.exists(csv_path)
    os.makedirs(os.path.dirname(csv_path) or ".", exist_ok=True)
    with open(csv_path, "a", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=header_order)
        if new_file: w.writeheader()
        w.writerow(row)

def main():
    args = parse_args()
    model = build_model()
    model.load_weights(args.weights)

    img = load_and_preprocess_image(args.image)
    if args.dims is not None:
        dims = np.array(args.dims, dtype=np.float32)
    else:
        data = json.load(open(args.dims_json, "r", encoding="utf-8"))
        dims = np.array([
            data.get("diameter",0.0),
            data.get("length",0.0),
            data.get("pin_hole_diameter",0.0),
            data.get("ring_groove_1_width",0.0),
            data.get("ring_groove_1_depth",0.0),
        ], dtype=np.float32)

    pred = model.predict([np.array([img]), np.array([dims])], verbose=0)[0]
    raw_bore, raw_total_h, raw_pin, raw_ring_w, raw_ring_d = map(float, pred)

    bore         = clamp(raw_bore, args.bore_min, args.bore_max)
    total_height = clamp(raw_total_h, args.height_min, args.height_max)
    pin_diameter = clamp(raw_pin, args.pin_min, args.pin_max)
    ring_w       = clamp(raw_ring_w, args.ringw_min, args.ringw_max)
    ring_d       = clamp(raw_ring_d, args.ringd_min, args.ringd_max)
    if ring_w < ring_d: ring_w = ring_d + 0.2

    crown_ratio  = min(max(args.crown_ratio, 0.05), 0.95)
    crown_height = round(crown_ratio * total_height, 2)
    skirt_length = round(total_height - crown_height, 2)

    design = {
        "bore": round(bore,2),
        "crown_height": crown_height,
        "skirt_length": skirt_length,
        "pin_diameter": round(pin_diameter,2),
        "ring": {
            "count": 3,
            "width": round(ring_w,2),
            "depth": round(ring_d,2),
            "first_z": 3.0,
            "spacing": 5.0
        },
        "crown_dish": { "type":"flat", "diameter": round(0.6*bore,2), "depth": 2.0 },
        "wall_thickness": 3.0,
        "crown_thickness": 6.0
    }

    os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
    with open(args.out,"w",encoding="utf-8") as f: json.dump({"design":design},f,indent=2)

    print("Saved params JSON ->", args.out)
    print("Raw predictions:", {"bore":raw_bore,"height":raw_total_h,"pin":raw_pin,"ring_w":raw_ring_w,"ring_d":raw_ring_d})
    print("Clamped design:", json.dumps(design,indent=2))

    if args.csv:
        name = args.name or Path(args.out).stem
        header = ["name","image","bore","crown_height","skirt_length","pin_diameter","ring_width","ring_depth","crown_ratio","out_json"]
        row = {
            "name": name, "image": args.image,
            "bore": design["bore"], "crown_height": design["crown_height"], "skirt_length": design["skirt_length"],
            "pin_diameter": design["pin_diameter"], "ring_width": design["ring"]["width"], "ring_depth": design["ring"]["depth"],
            "crown_ratio": crown_ratio, "out_json": args.out
        }
        maybe_append_csv(args.csv,row,header)

if __name__=="__main__": main()
```

---

## 2. **batch\_ai\_infer\_and\_build\_safe.py**

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批次安全版：讀取 req_dims_batch.json
每顆：
- 呼叫 ai_infer_piston_safe.py (會寫 JSON & 追加 CSV)
- 呼叫 FreeCAD 產生 FCStd/STEP/STL
"""
import os, json, subprocess, pathlib, sys

FREECAD = os.environ.get("FREECAD_EXE", r"C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe")
AI_INFER = os.environ.get("AI_INFER_PY", r"C:\xampp\htdocs\cs_ai\python\ai_infer_piston_safe.py")
GEN_PY   = os.environ.get("GEN_PY",      r"C:\xampp\htdocs\cs_ai\python\generate_piston_model.py")
BATCH_SPEC = os.environ.get("BATCH_SPEC",r"C:\xampp\htdocs\cs_ai\python\req_dims_batch.json")
OUTDIR   = os.environ.get("OUTDIR",      r"C:\xampp\htdocs\cs_ai\python\out")
WEIGHTS  = os.environ.get("WEIGHTS",     r"C:\xampp\htdocs\cs_ai\ai_models\piston_model_weights_final.weights.h5")
SUMMARY  = os.environ.get("SUMMARY",     os.path.join(OUTDIR, "batch_summary.csv"))

def run(cmd):
    print(">>", cmd)
    return subprocess.run(cmd, shell=True, check=True)

def main():
    spec_path = pathlib.Path(BATCH_SPEC)
    if not spec_path.exists():
        print("Spec not found:", spec_path); sys.exit(1)
    os.makedirs(OUTDIR, exist_ok=True)
    jobs = json.load(open(spec_path,"r",encoding="utf-8"))

    for job in jobs:
        name = job["name"]; image = job["image"]
        dims, dims_json = job.get("dims"), job.get("dims_json")
        outjson = os.path.join(OUTDIR, f"params_{name}.json")
        if dims is not None:
            dims_str = " ".join(map(str,dims))
            cmd1 = fr'python "{AI_INFER}" --weights "{WEIGHTS}" --image "{image}" --dims {dims_str} --out "{outjson}" --name "{name}" --csv "{SUMMARY}" --crown-ratio 0.40'
        elif dims_json is not None:
            cmd1 = fr'python "{AI_INFER}" --weights "{WEIGHTS}" --image "{image}" --dims-json "{dims_json}" --out "{outjson}" --name "{name}" --csv "{SUMMARY}" --crown-ratio 0.40'
        else:
            print(f"[{name}] 缺少 dims/dims_json"); continue
        run(cmd1)
        cmd2 = fr'"{FREECAD}" "{GEN_PY}" --pass "--json {outjson} --outdir {OUTDIR} --name {name} --export fcstd step stl"'
        run(cmd2)

    print("✅ 批次完成")
    print("Summary CSV:", SUMMARY)
    print("Outputs in :", OUTDIR)

if __name__=="__main__": main()
```

---

## 3. **run\_ai\_batch\_full\_safe.bat**

```bat
@echo off
set FREECAD_EXE=C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe
set AI_INFER_PY=C:\xampp\htdocs\cs_ai\python\ai_infer_piston_safe.py
set GEN_PY=C:\xampp\htdocs\cs_ai\python\generate_piston_model.py
set BATCH_SPEC=C:\xampp\htdocs\cs_ai\python\req_dims_batch.json
set OUTDIR=C:\xampp\htdocs\cs_ai\python\out
set WEIGHTS=C:\xampp\htdocs\cs_ai\ai_models\piston_model_weights_final.weights.h5
set SUMMARY=C:\xampp\htdocs\cs_ai\python\out\batch_summary.csv

python "C:\xampp\htdocs\cs_ai\python\batch_ai_infer_and_build_safe.py"
pause
```

---

## ✅ 使用方式

1. 在 `C:\xampp\htdocs\cs_ai\python\` 建立這三個檔案（把上面程式碼貼進去存檔）。
2. 準備好 `req_dims_batch.json` 與圖片。
3. 直接雙擊 `run_ai_batch_full_safe.bat`。

   * 每顆會輸出 `params_x.json` + `FCStd/STEP/STL`
   * 同時在 `out\batch_summary.csv` 追加總表。

---

要不要我幫你也貼出一個 **範例 batch\_summary.csv** 的樣子，讓你知道會有哪些欄位？


****csv2


了解 👍 你這邊看不到直接下載按鈕。沒關係，我把三個檔案的程式碼完整貼給你，你只要在 `C:\xampp\htdocs\cs_ai\python\` 新建檔案，複製貼上就能用了。

---

## 1. **ai\_infer\_piston\_safe.py**

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ai_infer_piston_safe.py
- 加入幾何 clamp（安全範圍）、簡單檢核
- 可輸出 CSV 總表（batch summary）
"""

import argparse, os, json, numpy as np, csv
from pathlib import Path
from PIL import Image

import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Conv2D, Flatten, Dense, Concatenate

def build_model():
    image_input = Input(shape=(256, 256, 1))
    x = Conv2D(32, (3,3), activation='relu')(image_input)
    x = Conv2D(64, (3,3), activation='relu')(x)
    x = Flatten()(x)
    image_features = Dense(128, activation='relu')(x)

    dimensions_input = Input(shape=(5,))
    y = Dense(64, activation='relu')(dimensions_input)
    dimensions_features = Dense(128, activation='relu')(y)

    combined = Concatenate()([image_features, dimensions_features])
    output = Dense(5, name="predicted_dims")(combined)
    model = Model(inputs=[image_input, dimensions_input], outputs=output)
    model.compile(optimizer='adam', loss='mse')
    return model

def load_and_preprocess_image(path):
    img = Image.open(path).convert('L').resize((256,256))
    arr = np.array(img, dtype=np.float32) / 255.0
    return np.expand_dims(arr, axis=-1)

def clamp(v, lo, hi): return float(max(lo, min(hi, v)))

def parse_args():
    ap = argparse.ArgumentParser()
    ap.add_argument("--weights", required=True)
    ap.add_argument("--image", required=True)
    g = ap.add_mutually_exclusive_group(required=True)
    g.add_argument("--dims", nargs=5, type=float)
    g.add_argument("--dims-json")
    ap.add_argument("--out", required=True)
    ap.add_argument("--name", default=None, help="名稱（CSV 用）")
    ap.add_argument("--csv", default=None, help="CSV 路徑")
    ap.add_argument("--crown-ratio", type=float, default=0.40)
    # clamp 範圍
    ap.add_argument("--bore-min", type=float, default=60.0)
    ap.add_argument("--bore-max", type=float, default=110.0)
    ap.add_argument("--height-min", type=float, default=20.0)
    ap.add_argument("--height-max", type=float, default=80.0)
    ap.add_argument("--pin-min", type=float, default=12.0)
    ap.add_argument("--pin-max", type=float, default=24.0)
    ap.add_argument("--ringw-min", type=float, default=0.8)
    ap.add_argument("--ringw-max", type=float, default=5.0)
    ap.add_argument("--ringd-min", type=float, default=0.5)
    ap.add_argument("--ringd-max", type=float, default=3.0)
    return ap.parse_args()

def maybe_append_csv(csv_path, row, header_order):
    new_file = not os.path.exists(csv_path)
    os.makedirs(os.path.dirname(csv_path) or ".", exist_ok=True)
    with open(csv_path, "a", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=header_order)
        if new_file: w.writeheader()
        w.writerow(row)

def main():
    args = parse_args()
    model = build_model()
    model.load_weights(args.weights)

    img = load_and_preprocess_image(args.image)
    if args.dims is not None:
        dims = np.array(args.dims, dtype=np.float32)
    else:
        data = json.load(open(args.dims_json, "r", encoding="utf-8"))
        dims = np.array([
            data.get("diameter",0.0),
            data.get("length",0.0),
            data.get("pin_hole_diameter",0.0),
            data.get("ring_groove_1_width",0.0),
            data.get("ring_groove_1_depth",0.0),
        ], dtype=np.float32)

    pred = model.predict([np.array([img]), np.array([dims])], verbose=0)[0]
    raw_bore, raw_total_h, raw_pin, raw_ring_w, raw_ring_d = map(float, pred)

    bore         = clamp(raw_bore, args.bore_min, args.bore_max)
    total_height = clamp(raw_total_h, args.height_min, args.height_max)
    pin_diameter = clamp(raw_pin, args.pin_min, args.pin_max)
    ring_w       = clamp(raw_ring_w, args.ringw_min, args.ringw_max)
    ring_d       = clamp(raw_ring_d, args.ringd_min, args.ringd_max)
    if ring_w < ring_d: ring_w = ring_d + 0.2

    crown_ratio  = min(max(args.crown_ratio, 0.05), 0.95)
    crown_height = round(crown_ratio * total_height, 2)
    skirt_length = round(total_height - crown_height, 2)

    design = {
        "bore": round(bore,2),
        "crown_height": crown_height,
        "skirt_length": skirt_length,
        "pin_diameter": round(pin_diameter,2),
        "ring": {
            "count": 3,
            "width": round(ring_w,2),
            "depth": round(ring_d,2),
            "first_z": 3.0,
            "spacing": 5.0
        },
        "crown_dish": { "type":"flat", "diameter": round(0.6*bore,2), "depth": 2.0 },
        "wall_thickness": 3.0,
        "crown_thickness": 6.0
    }

    os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
    with open(args.out,"w",encoding="utf-8") as f: json.dump({"design":design},f,indent=2)

    print("Saved params JSON ->", args.out)
    print("Raw predictions:", {"bore":raw_bore,"height":raw_total_h,"pin":raw_pin,"ring_w":raw_ring_w,"ring_d":raw_ring_d})
    print("Clamped design:", json.dumps(design,indent=2))

    if args.csv:
        name = args.name or Path(args.out).stem
        header = ["name","image","bore","crown_height","skirt_length","pin_diameter","ring_width","ring_depth","crown_ratio","out_json"]
        row = {
            "name": name, "image": args.image,
            "bore": design["bore"], "crown_height": design["crown_height"], "skirt_length": design["skirt_length"],
            "pin_diameter": design["pin_diameter"], "ring_width": design["ring"]["width"], "ring_depth": design["ring"]["depth"],
            "crown_ratio": crown_ratio, "out_json": args.out
        }
        maybe_append_csv(args.csv,row,header)

if __name__=="__main__": main()
```

---

## 2. **batch\_ai\_infer\_and\_build\_safe.py**

```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
批次安全版：讀取 req_dims_batch.json
每顆：
- 呼叫 ai_infer_piston_safe.py (會寫 JSON & 追加 CSV)
- 呼叫 FreeCAD 產生 FCStd/STEP/STL
"""
import os, json, subprocess, pathlib, sys

FREECAD = os.environ.get("FREECAD_EXE", r"C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe")
AI_INFER = os.environ.get("AI_INFER_PY", r"C:\xampp\htdocs\cs_ai\python\ai_infer_piston_safe.py")
GEN_PY   = os.environ.get("GEN_PY",      r"C:\xampp\htdocs\cs_ai\python\generate_piston_model.py")
BATCH_SPEC = os.environ.get("BATCH_SPEC",r"C:\xampp\htdocs\cs_ai\python\req_dims_batch.json")
OUTDIR   = os.environ.get("OUTDIR",      r"C:\xampp\htdocs\cs_ai\python\out")
WEIGHTS  = os.environ.get("WEIGHTS",     r"C:\xampp\htdocs\cs_ai\ai_models\piston_model_weights_final.weights.h5")
SUMMARY  = os.environ.get("SUMMARY",     os.path.join(OUTDIR, "batch_summary.csv"))

def run(cmd):
    print(">>", cmd)
    return subprocess.run(cmd, shell=True, check=True)

def main():
    spec_path = pathlib.Path(BATCH_SPEC)
    if not spec_path.exists():
        print("Spec not found:", spec_path); sys.exit(1)
    os.makedirs(OUTDIR, exist_ok=True)
    jobs = json.load(open(spec_path,"r",encoding="utf-8"))

    for job in jobs:
        name = job["name"]; image = job["image"]
        dims, dims_json = job.get("dims"), job.get("dims_json")
        outjson = os.path.join(OUTDIR, f"params_{name}.json")
        if dims is not None:
            dims_str = " ".join(map(str,dims))
            cmd1 = fr'python "{AI_INFER}" --weights "{WEIGHTS}" --image "{image}" --dims {dims_str} --out "{outjson}" --name "{name}" --csv "{SUMMARY}" --crown-ratio 0.40'
        elif dims_json is not None:
            cmd1 = fr'python "{AI_INFER}" --weights "{WEIGHTS}" --image "{image}" --dims-json "{dims_json}" --out "{outjson}" --name "{name}" --csv "{SUMMARY}" --crown-ratio 0.40'
        else:
            print(f"[{name}] 缺少 dims/dims_json"); continue
        run(cmd1)
        cmd2 = fr'"{FREECAD}" "{GEN_PY}" --pass "--json {outjson} --outdir {OUTDIR} --name {name} --export fcstd step stl"'
        run(cmd2)

    print("✅ 批次完成")
    print("Summary CSV:", SUMMARY)
    print("Outputs in :", OUTDIR)

if __name__=="__main__": main()
```

---

## 3. **run\_ai\_batch\_full\_safe.bat**

```bat
@echo off
set FREECAD_EXE=C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe
set AI_INFER_PY=C:\xampp\htdocs\cs_ai\python\ai_infer_piston_safe.py
set GEN_PY=C:\xampp\htdocs\cs_ai\python\generate_piston_model.py
set BATCH_SPEC=C:\xampp\htdocs\cs_ai\python\req_dims_batch.json
set OUTDIR=C:\xampp\htdocs\cs_ai\python\out
set WEIGHTS=C:\xampp\htdocs\cs_ai\ai_models\piston_model_weights_final.weights.h5
set SUMMARY=C:\xampp\htdocs\cs_ai\python\out\batch_summary.csv

python "C:\xampp\htdocs\cs_ai\python\batch_ai_infer_and_build_safe.py"
pause
```

---

## ✅ 使用方式

1. 在 `C:\xampp\htdocs\cs_ai\python\` 建立這三個檔案（把上面程式碼貼進去存檔）。
2. 準備好 `req_dims_batch.json` 與圖片。
3. 直接雙擊 `run_ai_batch_full_safe.bat`。

   * 每顆會輸出 `params_x.json` + `FCStd/STEP/STL`
   * 同時在 `out\batch_summary.csv` 追加總表。

---

要不要我幫你也貼出一個 **範例 batch\_summary.csv** 的樣子，讓你知道會有哪些欄位？
