| | 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 |
| |
|
| | |
| | |
| | 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)) |
| | |
| | |
| | neckline = self._create_neckline_path(neck_center, H, NW, ND) |
| | path_points.extend(neckline) |
| | |
| | |
| | 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='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() |