knit / app.py
Yasu777's picture
Update app.py
0c7e14f verified
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()