import gradio as gr import numpy as np import matplotlib.pyplot as plt import matplotlib.patches as patches from matplotlib.font_manager import FontProperties from PIL import Image, ImageDraw, ImageFont import io import base64 import json import re import os from typing import Dict, Tuple, Optional from openai import OpenAI # 日本語フォント設定 try: plt.rcParams['font.family'] = ['DejaVu Sans', 'IPAexGothic', 'VL PGothic', 'Hiragino Sans', 'Yu Gothic', 'Meiryo'] jp_font = FontProperties(family='IPAexGothic', size=12) except: jp_font = FontProperties(size=12) class OCRProcessor: def __init__(self): api_key = os.getenv("OPENAI_API_KEY") if api_key: self.client = OpenAI(api_key=api_key) self.ocr_available = True else: self.client = None self.ocr_available = False def encode_image_to_base64(self, image: Image.Image) -> str: buffer = io.BytesIO() image.save(buffer, format="JPEG") image_bytes = buffer.getvalue() return base64.b64encode(image_bytes).decode('utf-8') def extract_measurements_from_image(self, image: Image.Image) -> Dict[str, float]: if not self.ocr_available: return {"error": "OpenAI API key not found"} try: base64_image = self.encode_image_to_base64(image) response = self.client.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "system", "content": """あなたはニット編み仕様書のOCR専門家です。 画像から以下の寸法情報を抽出してJSONで出力してください: - 身丈, 身幅, 袖丈, 袖幅, 肩幅, 首ぐり幅, 首ぐり深さ, 袖ぐり深さ 出力例: { "身丈": 60.0, "身幅": 50.0, "袖丈": 55.0, "袖幅": 35.0, "肩幅": 40.0 } 数値のみを抽出し、単位(cm等)は除いてください。 見つからない項目は省略してください。""" }, { "role": "user", "content": [ { "type": "text", "text": "この編み仕様書から寸法情報を抽出してください。手書き文字も可能な限り読み取ってください。" }, { "type": "image_url", "image_url": { "url": f"data:image/jpeg;base64,{base64_image}" } } ] } ], max_tokens=500 ) content = response.choices[0].message.content try: if "```json" in content: json_str = content.split("```json")[1].split("```")[0].strip() elif "```" in content: json_str = content.split("```")[1].strip() else: json_str = content.strip() measurements = json.loads(json_str) return measurements except json.JSONDecodeError: return self._extract_from_text_fallback(content) except Exception as e: return {"error": f"OCR処理エラー: {str(e)}"} def _extract_from_text_fallback(self, text: str) -> Dict[str, float]: measurements = {} patterns = { "身丈": [r"身丈[::\s]*(\d+\.?\d*)", r"着丈[::\s]*(\d+\.?\d*)"], "身幅": [r"身幅[::\s]*(\d+\.?\d*)", r"胸囲[::\s]*(\d+\.?\d*)"], "袖丈": [r"袖丈[::\s]*(\d+\.?\d*)"], "袖幅": [r"袖幅[::\s]*(\d+\.?\d*)"], "肩幅": [r"肩幅[::\s]*(\d+\.?\d*)"], "首ぐり幅": [r"首ぐり幅[::\s]*(\d+\.?\d*)"], "首ぐり深さ": [r"首ぐり深さ[::\s]*(\d+\.?\d*)"], "袖ぐり深さ": [r"袖ぐり深さ[::\s]*(\d+\.?\d*)"] } for key, pattern_list in patterns.items(): for pattern in pattern_list: match = re.search(pattern, text) if match: measurements[key] = float(match.group(1)) break return measurements class KnitPatternGenerator: def __init__(self): self.measurement_keys = [ "身丈", "身幅", "袖丈", "袖幅", "肩幅", "首ぐり幅", "首ぐり深さ", "袖ぐり深さ" ] def extract_measurements_from_text(self, text: str) -> Dict[str, float]: measurements = {} patterns = { "身丈": [r"身丈[::\s]*(\d+\.?\d*)", r"着丈[::\s]*(\d+\.?\d*)"], "身幅": [r"身幅[::\s]*(\d+\.?\d*)", r"胸囲[::\s]*(\d+\.?\d*)"], "袖丈": [r"袖丈[::\s]*(\d+\.?\d*)"], "袖幅": [r"袖幅[::\s]*(\d+\.?\d*)"], "肩幅": [r"肩幅[::\s]*(\d+\.?\d*)"], "首ぐり幅": [r"首ぐり幅[::\s]*(\d+\.?\d*)"], "首ぐり深さ": [r"首ぐり深さ[::\s]*(\d+\.?\d*)"], "袖ぐり深さ": [r"袖ぐり深さ[::\s]*(\d+\.?\d*)"] } for key, pattern_list in patterns.items(): for pattern in pattern_list: match = re.search(pattern, text) if match: measurements[key] = float(match.group(1)) break return measurements def create_pullover_pattern(self, measurements: Dict[str, float]) -> Image.Image: """プルオーバーの型紙を生成(超シンプル版)""" fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 14)) try: fig.suptitle('ニット型紙 - プルオーバー(滑らかなカーブ版)', fontsize=16, fontweight='bold', fontproperties=jp_font) except: fig.suptitle('Knit Pattern - Pullover (Smooth Curve Version)', fontsize=16, fontweight='bold') # デフォルト値の設定 defaults = { "身丈": 60, "身幅": 50, "袖丈": 55, "袖幅": 35, "肩幅": 40, "首ぐり幅": 20, "首ぐり深さ": 8, "袖ぐり深さ": 22 } m = {**defaults, **measurements} # 前身頃 self._draw_front_body_ultra_simple(ax1, m) # 後身頃 self._draw_back_body_ultra_simple(ax2, m) # 袖 self._draw_sleeve_ultra_simple(ax3, m) # 寸法表 self._draw_measurement_table(ax4, m) plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png', dpi=300, bbox_inches='tight') buf.seek(0) image = Image.open(buf) plt.close() return image def _draw_front_body_ultra_simple(self, ax, m): """前身頃をきれいに描画(滑らかなカーブ版)""" try: ax.set_title('前身頃', fontweight='bold', fontsize=14, fontproperties=jp_font) except: ax.set_title('Front Body', fontweight='bold', fontsize=14) ax.set_aspect('equal') W = m["身幅"] H = m["身丈"] SW = m["肩幅"] NW = m["首ぐり幅"] ND = m["首ぐり深さ"] AD = m["袖ぐり深さ"] # 各種座標の計算 neck_x0 = (W - NW) / 2 neck_x1 = neck_x0 + NW neck_center = (neck_x0 + neck_x1) / 2 shoulder_x0 = (W - SW) / 2 shoulder_x1 = shoulder_x0 + SW # パスの作成 # 1. 下辺から開始 path_points = [(0, 0)] # 2. 右下角 path_points.append((W, 0)) # 3. 右側面(袖ぐり下まで) path_points.append((W, H - AD)) # 4. 右袖ぐりカーブ right_armhole = self._create_armhole_curve(shoulder_x1, H, AD, 'right') path_points.extend(right_armhole) # 5. 右肩から首ぐり開始点 path_points.append((neck_x1, H)) # 6. 首ぐりU字カーブ neckline = self._create_neckline_path(neck_center, H, NW, ND) path_points.extend(neckline) # 7. 左肩 path_points.append((shoulder_x0, H)) # 8. 左袖ぐりカーブ left_armhole = self._create_armhole_curve(shoulder_x0, H, AD, 'left') path_points.extend(left_armhole) # 9. 左側面 path_points.append((0, H - AD)) # 10. 閉じる path_points.append((0, 0)) # パスを描画 path = patches.Polygon(path_points, closed=True, linewidth=2, edgecolor='black', facecolor='lightblue', alpha=0.3) ax.add_patch(path) # 寸法線 self._add_dimension_line(ax, 0, -8, W, -8, f"身幅: {W}cm", 'below') self._add_dimension_line(ax, -8, 0, -8, H, f"身丈: {H}cm", 'left') self._add_dimension_line(ax, shoulder_x0, H + 5, shoulder_x1, H + 5, f"肩幅: {SW}cm", 'above') self._add_dimension_line(ax, neck_x0, H - ND - 5, neck_x1, H - ND - 5, f"首幅: {NW}cm", 'below') ax.set_xlim(-15, W + 15) ax.set_ylim(-15, H + 15) ax.grid(True, alpha=0.3) ax.set_xlabel('幅 (cm)') ax.set_ylabel('丈 (cm)') def _draw_back_body_ultra_simple(self, ax, m): """後身頃をきれいに描画(滑らかなカーブ版)""" try: ax.set_title('後身頃', fontweight='bold', fontsize=14, fontproperties=jp_font) except: ax.set_title('Back Body', fontweight='bold', fontsize=14) ax.set_aspect('equal') W = m["身幅"] H = m["身丈"] SW = m["肩幅"] NW = m["首ぐり幅"] ND = m["首ぐり深さ"] AD = m["袖ぐり深さ"] # 後身頃の首ぐりは浅い back_neck_width = NW * 0.6 back_neck_depth = ND * 0.3 # 各種座標の計算 neck_x0 = (W - back_neck_width) / 2 neck_x1 = neck_x0 + back_neck_width neck_center = (neck_x0 + neck_x1) / 2 shoulder_x0 = (W - SW) / 2 shoulder_x1 = shoulder_x0 + SW # パスの作成 path_points = [(0, 0)] path_points.append((W, 0)) path_points.append((W, H - AD)) # 右袖ぐりカーブ right_armhole = self._create_armhole_curve(shoulder_x1, H, AD, 'right') path_points.extend(right_armhole) # 右肩から首ぐり path_points.append((neck_x1, H)) # 浅い首ぐりカーブ if back_neck_depth > 1: neckline = self._create_neckline_path(neck_center, H, back_neck_width, back_neck_depth, num_points=10) path_points.extend(neckline) else: path_points.append((neck_x0, H)) # 左肩 path_points.append((shoulder_x0, H)) # 左袖ぐりカーブ left_armhole = self._create_armhole_curve(shoulder_x0, H, AD, 'left') path_points.extend(left_armhole) path_points.append((0, H - AD)) path_points.append((0, 0)) # パスを描画 path = patches.Polygon(path_points, closed=True, linewidth=2, edgecolor='black', facecolor='lightgreen', alpha=0.3) ax.add_patch(path) ax.set_xlim(-15, W + 15) ax.set_ylim(-15, H + 15) ax.grid(True, alpha=0.3) ax.set_xlabel('幅 (cm)') ax.set_ylabel('丈 (cm)') def _draw_sleeve_ultra_simple(self, ax, m): """袖をきれいに描画(滑らかなカーブ版)""" try: ax.set_title('袖', fontweight='bold', fontsize=14, fontproperties=jp_font) except: ax.set_title('Sleeve', fontweight='bold', fontsize=14) ax.set_aspect('equal') sleeve_length = m["袖丈"] sleeve_width = m["袖幅"] # 袖山の高さ cap_height = m["袖ぐり深さ"] * 0.6 # 袖口と袖山の幅 cuff_width = sleeve_width * 0.5 cap_width = sleeve_width center_x = cap_width / 2 # 袖山カーブの生成 theta = np.linspace(0, np.pi, 30) cap_x = center_x + (cap_width/2) * np.cos(theta) cap_y = sleeve_length + cap_height * np.sin(theta) # パスポイントの作成 path_points = [] # 左袖口から開始 path_points.append((center_x - cuff_width/2, 0)) # 右袖口 path_points.append((center_x + cuff_width/2, 0)) # 右側面 path_points.append((cap_width, sleeve_length)) # 袖山カーブ(右から左へ) cap_points = list(zip(cap_x, cap_y)) path_points.extend(cap_points) # 左側面 path_points.append((0, sleeve_length)) # 閉じる path_points.append((center_x - cuff_width/2, 0)) # パスを描画 sleeve = patches.Polygon(path_points, closed=True, linewidth=2, edgecolor='black', facecolor='lightyellow', alpha=0.3) ax.add_patch(sleeve) # 寸法線 self._add_dimension_line(ax, center_x - cuff_width/2, -5, center_x + cuff_width/2, -5, f"袖口: {cuff_width:.1f}cm", 'below') self._add_dimension_line(ax, -5, 0, -5, sleeve_length, f"袖丈: {sleeve_length}cm", 'left') self._add_dimension_line(ax, 0, sleeve_length + cap_height + 3, cap_width, sleeve_length + cap_height + 3, f"袖幅: {cap_width}cm", 'above') ax.set_xlim(-10, cap_width + 15) ax.set_ylim(-10, sleeve_length + cap_height + 15) ax.grid(True, alpha=0.3) ax.set_xlabel('幅 (cm)') ax.set_ylabel('丈 (cm)') def _draw_measurement_table(self, ax, measurements): """寸法表を描画""" try: ax.set_title('寸法表', fontweight='bold', fontsize=14, fontproperties=jp_font) except: ax.set_title('Measurements', fontweight='bold', fontsize=14) ax.axis('off') table_data = [] for key in self.measurement_keys: value = measurements.get(key, '-') if isinstance(value, float): value = f"{value:.1f}cm" table_data.append([key, value]) table = ax.table(cellText=table_data, colLabels=['項目', '寸法'], cellLoc='center', loc='center', colWidths=[0.5, 0.3]) table.auto_set_font_size(False) table.set_fontsize(12) table.scale(1, 2.5) for i in range(2): table[(0, i)].set_facecolor('#40466e') table[(0, i)].set_text_props(weight='bold', color='white') for i in range(1, len(table_data) + 1): table[(i, 0)].set_facecolor('#f1f1f2') table[(i, 1)].set_facecolor('#ffffff') def _create_neckline_path(self, x_center, y_top, width, depth, num_points=20): """U字型の襟ぐりパスを生成""" theta = np.linspace(0, np.pi, num_points) x = x_center + (width/2) * np.cos(theta) y = y_top - depth + depth * np.sin(theta) # 左から右へのポイント points = list(zip(x[::-1], y[::-1])) return points def _create_armhole_curve(self, x_shoulder, y_shoulder, armhole_depth, direction='left', num_points=10): """袖ぐりカーブのポイントを生成""" if direction == 'left': # 左袖ぐり:右下から左上へのカーブ theta = np.linspace(3*np.pi/2, 2*np.pi, num_points) else: # 右袖ぐり:左下から右上へのカーブ theta = np.linspace(np.pi, 3*np.pi/2, num_points) radius = armhole_depth * 0.8 x = x_shoulder + radius * np.cos(theta) y = (y_shoulder - armhole_depth) + radius * np.sin(theta) return list(zip(x, y)) def _add_dimension_line(self, ax, x1, y1, x2, y2, text, position='above'): """寸法線を追加""" ax.annotate('', xy=(x2, y2), xytext=(x1, y1), arrowprops=dict(arrowstyle='<->', color='red', lw=1.5)) mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 if position == 'above': mid_y += 2 elif position == 'below': mid_y -= 2 elif position == 'left': mid_x -= 2 elif position == 'right': mid_x += 2 ax.text(mid_x, mid_y, text, ha='center', va='center', fontsize=9, color='red', fontweight='bold', bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.9, edgecolor='red')) """寸法線を追加""" ax.annotate('', xy=(x2, y2), xytext=(x1, y1), arrowprops=dict(arrowstyle='<->', color='red', lw=1.5)) mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 if position == 'above': mid_y += 2 elif position == 'below': mid_y -= 2 elif position == 'left': mid_x -= 2 elif position == 'right': mid_x += 2 ax.text(mid_x, mid_y, text, ha='center', va='center', fontsize=9, color='red', fontweight='bold', bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.9, edgecolor='red')) def ocr_and_generate_pattern(image, manual_measurements_text, pattern_type): """メイン処理関数""" generator = KnitPatternGenerator() ocr_processor = OCRProcessor() measurements = {} status_messages = [] if image is not None: if ocr_processor.ocr_available: status_messages.append("🔍 画像からOCR処理を実行中...") ocr_results = ocr_processor.extract_measurements_from_image(image) if "error" in ocr_results: status_messages.append(f"❌ OCR エラー: {ocr_results['error']}") else: measurements.update(ocr_results) status_messages.append(f"✅ OCRで {len(ocr_results)} 項目の寸法を抽出しました") ocr_details = [] for key, value in ocr_results.items(): ocr_details.append(f" {key}: {value}cm") if ocr_details: status_messages.append("📋 OCR抽出結果:\n" + "\n".join(ocr_details)) else: status_messages.append("⚠️ OpenAI API キーが設定されていません。手動入力を使用します。") if manual_measurements_text: manual_measurements = generator.extract_measurements_from_text(manual_measurements_text) measurements.update(manual_measurements) if manual_measurements: status_messages.append(f"✏️ 手動入力で {len(manual_measurements)} 項目を設定/上書きしました") defaults_used = [] defaults = { "身丈": 60, "身幅": 50, "袖丈": 55, "袖幅": 35, "肩幅": 40, "首ぐり幅": 20, "首ぐり深さ": 8, "袖ぐり深さ": 22 } for key, default_value in defaults.items(): if key not in measurements: measurements[key] = default_value defaults_used.append(key) if defaults_used: status_messages.append(f"📐 デフォルト値を使用: {', '.join(defaults_used)}") try: if pattern_type == "プルオーバー": pattern_image = generator.create_pullover_pattern(measurements) status_messages.append("🎯 数学的に正確な滑らかなカーブの型紙を生成しました") else: pattern_image = generator.create_pullover_pattern(measurements) status_messages.append("🎯 滑らかなカーブの型紙を生成しました(現在はプルオーバーのみ対応)") except Exception as e: status_messages.append(f"❌ 型紙生成エラー: {str(e)}") pattern_image = None measurements_display = "📏 使用された寸法:\n" for key in generator.measurement_keys: value = measurements.get(key, "未設定") if isinstance(value, (int, float)): measurements_display += f" {key}: {value:.1f}cm\n" else: measurements_display += f" {key}: {value}\n" status_text = "\n".join(status_messages) return pattern_image, measurements_display, status_text def create_interface(): with gr.Blocks(title="ニット型紙メーカー(滑らかなカーブ版)", theme=gr.themes.Soft()) as app: gr.Markdown("# 🧶 ニット型紙メーカー(滑らかなカーブ版)") gr.Markdown("NumPyを使った数学的に正確な滑らかなカーブで美しい型紙を生成") with gr.Row(): with gr.Column(scale=1): gr.Markdown("## 📸 入力方法") image_input = gr.Image( label="編み仕様書の画像(オプション)", type="pil" ) manual_input = gr.Textbox( label="手動入力(例:身丈:60、身幅:50、袖丈:55)", placeholder="身丈:60、身幅:50、袖丈:55、袖幅:35、肩幅:40", lines=3 ) pattern_type = gr.Dropdown( choices=["プルオーバー", "カーディガン", "ベスト"], value="プルオーバー", label="型紙の種類" ) generate_btn = gr.Button("🎯 型紙生成", variant="primary") with gr.Column(scale=2): gr.Markdown("## 📋 生成結果") pattern_output = gr.Image( label="生成された型紙", type="pil" ) with gr.Row(): measurements_output = gr.Textbox( label="抽出された寸法", lines=8 ) status_output = gr.Textbox( label="処理状況", lines=4 ) generate_btn.click( fn=ocr_and_generate_pattern, inputs=[image_input, manual_input, pattern_type], outputs=[pattern_output, measurements_output, status_output] ) gr.Markdown(""" ## 🎯 滑らかなカーブ版の特長 ### ✅ NumPyを使った数学的に正確なカーブ - **連続したPolygonパス**:一体型の美しい輪郭線 - **滑らかなU字襟ぐり**:数学関数で生成した自然なカーブ - **自然な袖ぐり**:半円弧カーブによる美しい形状 - **完璧な袖山**:三角関数で計算された滑らかな山型 ### ✅ 技術的な改良 1. **_create_neckline_path**:NumPy三角関数でU字カーブ生成 2. **_create_armhole_curve**:左右の袖ぐりを個別計算 3. **連続パス描画**:切れ目のない一体型の輪郭 4. **数学的精度**:理論的に正確な曲線計算 ## 📝 使用方法 - **手動入力例**:「身丈:60、身幅:50、袖丈:55、袖幅:35、肩幅:40」 - **結果**:プロレベルの滑らかで美しい型紙 ## 💡 技術的優位性 **数学的精度** - 実際の型紙作成で使用される曲線を忠実に再現 """) return app if __name__ == "__main__": app = create_interface() app.launch()