Files changed (1) hide show
  1. app.py +51 -127
app.py CHANGED
@@ -4,9 +4,9 @@ import gradio as gr
4
  from datetime import datetime
5
  import os
6
  import tempfile
7
- from PIL import Image, ImageDraw, ImageFont #zh-tw
8
 
9
- # 定義固定的暫存目錄
10
  TEMP_DIR = os.path.join(tempfile.gettempdir(), "gradio_side2front")
11
  os.makedirs(TEMP_DIR, exist_ok=True)
12
 
@@ -20,217 +20,151 @@ def manual_perspective_transform(image, points):
20
  return result
21
 
22
  def format_selected_points(points):
23
- labels = ['左上', '右上', '左下', '右下']
24
  formatted_points = []
25
  for i, point in enumerate(points):
26
  if i >= len(labels):
27
- break # 防止超出標籤範圍
28
  formatted_points.append(f"{labels[i]}[{point[0]}, {point[1]}]")
29
  return "、".join(formatted_points)
30
 
31
  def update_coordinates(original_image, evt: gr.SelectData, points):
32
  if original_image is None:
33
- # 如果圖片被清除,重置點和狀態訊息
34
- return [], "已重置初始化,請上傳新圖片", "已重置初始化,請上傳新圖片", None
35
  if len(points) < 4:
36
- # 使用者選擇新的點
37
  points.append([evt.index[0], evt.index[1]])
38
- # 根據已選擇的點生成訊息
39
- formatted_message = f"已選擇的點({len(points)}/4):{format_selected_points(points)}"
40
 
41
- # 獲取圖片尺寸
 
42
  height, width = original_image.shape[:2]
43
  min_dim = min(width, height)
44
 
45
- # 定義圓點半徑和字體大小(根據圖片尺寸動態調整)
46
- circle_radius = max(10, int(min_dim * 0.005)) # 0.5% 的最小邊長,至少 10 像素
47
- font_size = max(16, int(min_dim * 0.02)) # 2% 的最小邊長,至少 16 像素
48
 
49
- # 繪製選取的點在圖片上
50
  annotated = original_image.copy()
51
 
52
- labels = ['左上', '右上', '左下', '右下']
53
 
54
  for i, point in enumerate(points):
55
- if i >= len(labels):
56
- label = f"Point{i+1}"
57
- else:
58
- label = labels[i]
59
- # 繪製較大的圓點
60
- cv2.circle(annotated, tuple(point), circle_radius, (255, 0, 0), -1) # 紅色圓點RGB
61
 
62
- # 使用Pillow來繪製文字
63
  pil_image = Image.fromarray(cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB))
64
- # pil_image = Image.fromarray(annotated)
65
  draw = ImageDraw.Draw(pil_image)
66
 
67
  try:
68
- # 載入支援中文的字型
69
- font = ImageFont.truetype("GenSekiGothic-B.ttc", font_size) # 確保字型文件路徑正確
70
  except IOError:
71
- # 如果字型載入失敗,使用默認字型
72
  font = ImageFont.load_default()
73
 
74
  for i, point in enumerate(points):
75
  if i >= len(labels):
76
- label = f"Point{i+1}"
77
  else:
78
  label = labels[i]
79
 
80
- # 添加文字標籤,稍微偏移以避免重疊
81
  text_position = (point[0] + 10 + circle_radius, point[1] - 5 - circle_radius)
82
- draw.text(text_position, label, font=font, fill=(0, 0, 255)) # 紅色文字BGR
83
 
84
-
85
- # 將Pillow圖像轉回OpenCV圖像 (BGR)
86
  annotated = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
87
 
88
  return points, formatted_message, "", annotated
89
 
90
  def reset_points(original_image):
91
  if original_image is None:
92
- return [], "已重置所有點", "已重置所有點", None
93
  else:
94
- return [], "", "已重置所有點", original_image
95
 
96
  def process_image(image, points, custom_filename, file_format):
97
- # 驗證輸入
98
  if image is None:
99
- return None, None, None, "請先上傳圖片。"
100
  if len(points) != 4:
101
- return None, None, None, "請選擇準確的4個點(左上、右上、左下、右下)。"
102
  height, width = image.shape[:2]
103
  if height < 300 or width < 300:
104
- return None, None, None, "圖片尺寸太小。請上傳至少 300x300 像素的圖片。"
105
 
106
  try:
107
- # 處理圖片
108
  result = manual_perspective_transform(image, points)
109
  if result is None:
110
- return None, None, None, "轉換失敗,請重試。"
111
 
112
- # 準備檔案名稱
113
  if not custom_filename:
114
- custom_filename = f"processedimg_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
115
 
116
- # 移除任何不安全的字符
117
  custom_filename = "".join(c for c in custom_filename if c.isalnum() or c in ('-', '_'))
118
  if not custom_filename:
119
- custom_filename = f"processedimg_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
120
 
121
- # 把結果存到暫存檔 (以便下載)
122
- # temp_dir = tempfile.mkdtemp()
123
- # out_path = os.path.join(temp_dir, custom_filename + file_format)
124
- # cv2.imwrite(out_path, cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
125
-
126
- # 把結果存到固定的暫存檔,覆蓋舊檔案
127
  out_path = os.path.join(TEMP_DIR, custom_filename + file_format)
128
  cv2.imwrite(out_path, cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
129
 
130
- return result, out_path, out_path, "處理成功!"
131
 
132
  except Exception as e:
133
- return None, None, None, f"處理過程發生錯誤:{str(e)}"
134
 
135
- # 修改圖片變更事件處理函數
136
  def handle_image_change(img):
137
  if img is not None:
138
- return img, [], "", "已上傳新圖片,請標示四個角落點", img
139
- return None, [], "已清除圖片,請上傳新圖", "已清除圖片,請上傳新圖", None
140
 
141
- # Gradio Interface Setup
142
  with gr.Blocks(theme=gr.themes.Default(primary_hue=gr.themes.colors.yellow, secondary_hue=gr.themes.colors.red)) as interface:
143
- # 標題和描述
144
  gr.HTML("""
145
- <div style='width: 800px; color: white;'>
146
- <h1>OpenCV工具:書本側面轉正面視圖</h1>
147
- <h3>使用者選擇圖檔輸入方式:上傳檔案或是透過已複製到剪貼簿的圖片,平台進行圖片拉平渲染。</h3>
148
  </div>
149
  """)
150
 
151
- # with gr.Blocks() as demo:
152
- # gr.Markdown("## 書本側面轉正面工具")
153
- # gr.Markdown("### 注意:請上傳至少 300x300 像素的清晰圖片")
154
-
155
  with gr.Row():
156
  with gr.Column():
157
- # 輸入區域
158
- gr.Markdown("## 上傳側面圖片檔案(至少300x300像素的清晰圖片)")
159
- gr.Markdown("#### 請於下方圖區以+字點擊四個角落點(請依序定位:左上、右上、左下、右下)")
160
-
161
- # gr.HTML("""
162
- # <div style='width: 400px; color: white;'>
163
- # <h2>上傳側面圖片檔案 PS.至少 300x300 像素的清晰圖片</h2>
164
- # <h5>請上傳後於此圖上標示四個角落點</h5>
165
- # </div>
166
- # """)
167
- image_input = gr.Image(label="圖檔標示區", type="numpy", interactive=True)
168
-
169
  points_tracker = gr.State([])
170
 
171
  with gr.Column():
172
- gr.Markdown("## 角落點視覺化參考")
173
- gr.Markdown("#### 已選取的點示意")
174
-
175
- # gr.HTML("""
176
- # <div style='width: 400px; color: white;'>
177
- # <h2>角落點視覺化參考</h2>
178
- # <h5>已選取的點示意</h5>
179
- # </div>
180
- # """)
181
-
182
- annotated_image = gr.Image(label="視覺化圖區", type="numpy", interactive=False)
183
-
184
-
185
 
186
  with gr.Row():
187
  with gr.Column():
188
- coords_output = gr.Textbox(label="已選擇的點位置", interactive=False)
189
- reset_button = gr.Button("重置選擇的點")
190
- original_image = gr.State() # 用於儲存原始圖片
191
 
192
-
193
-
194
- gr.HTML("""<hr></hr>""")
195
 
196
  with gr.Row():
197
-
198
-
199
  with gr.Column():
200
-
201
- # 輸出區域
202
- gr.Markdown("## 輸出檔案設定")
203
  custom_filename = gr.Textbox(
204
- label="自定義檔案名稱(可選)",
205
- placeholder="輸入檔名或留空使用時間戳",
206
  value=""
207
  )
208
  file_format = gr.Dropdown(
209
- label="檔案格式",
210
  choices=[".png", ".jpg"],
211
  value=".png"
212
  )
213
- process_button = gr.Button("生成正面視圖", variant="primary")
214
-
215
- gr.Markdown("## 狀態提示")
216
- status_output = gr.Textbox(label="狀態訊息", interactive=False)
217
 
218
  with gr.Column():
219
- gr.Markdown("## 正面視圖結果")
220
- output_preview = gr.Image(label="預覽區")
221
- # 新增 File 元件來做檔案下載
222
- download_file = gr.File(label="檔案完成下載區")
223
-
224
-
225
 
226
- # 事件綁定
227
  image_input.select(
228
  update_coordinates,
229
  inputs=[original_image, points_tracker],
230
  outputs=[points_tracker, coords_output, status_output, annotated_image]
231
  )
232
 
233
- # 新增圖片變更事件,當圖片被上傳或清除時更新 original_image 和 annotated_image
234
  image_input.change(
235
  handle_image_change,
236
  inputs=image_input,
@@ -245,19 +179,9 @@ with gr.Blocks(theme=gr.themes.Default(primary_hue=gr.themes.colors.yellow, seco
245
 
246
  process_button.click(
247
  process_image,
248
- inputs=[
249
- image_input,
250
- points_tracker,
251
- custom_filename,
252
- file_format
253
- ],
254
- outputs=[
255
- output_preview,
256
- download_file,
257
- download_file, # 同一個檔案路徑給 File(第一個是檔案路徑,第二個是 MIME path)
258
- status_output
259
- ]
260
  )
261
 
262
  if __name__ == "__main__":
263
- interface.launch()
 
4
  from datetime import datetime
5
  import os
6
  import tempfile
7
+ from PIL import Image, ImageDraw, ImageFont
8
 
9
+ # Definierter temporärer Ordner
10
  TEMP_DIR = os.path.join(tempfile.gettempdir(), "gradio_side2front")
11
  os.makedirs(TEMP_DIR, exist_ok=True)
12
 
 
20
  return result
21
 
22
  def format_selected_points(points):
23
+ labels = ['Oben links', 'Oben rechts', 'Unten links', 'Unten rechts']
24
  formatted_points = []
25
  for i, point in enumerate(points):
26
  if i >= len(labels):
27
+ break
28
  formatted_points.append(f"{labels[i]}[{point[0]}, {point[1]}]")
29
  return "、".join(formatted_points)
30
 
31
  def update_coordinates(original_image, evt: gr.SelectData, points):
32
  if original_image is None:
33
+ return [], "Zurückgesetzt, bitte neues Bild hochladen", "Zurückgesetzt, bitte neues Bild hochladen", None
 
34
  if len(points) < 4:
 
35
  points.append([evt.index[0], evt.index[1]])
 
 
36
 
37
+ formatted_message = f"Ausgewählte Punkte({len(points)}/4):{format_selected_points(points)}"
38
+
39
  height, width = original_image.shape[:2]
40
  min_dim = min(width, height)
41
 
42
+ circle_radius = max(10, int(min_dim * 0.005))
43
+ font_size = max(16, int(min_dim * 0.02))
 
44
 
 
45
  annotated = original_image.copy()
46
 
47
+ labels = ['Oben links', 'Oben rechts', 'Unten links', 'Unten rechts']
48
 
49
  for i, point in enumerate(points):
50
+ cv2.circle(annotated, tuple(point), circle_radius, (255, 0, 0), -1)
 
 
 
 
 
51
 
 
52
  pil_image = Image.fromarray(cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB))
 
53
  draw = ImageDraw.Draw(pil_image)
54
 
55
  try:
56
+ font = ImageFont.truetype("GenSekiGothic-B.ttc", font_size)
 
57
  except IOError:
 
58
  font = ImageFont.load_default()
59
 
60
  for i, point in enumerate(points):
61
  if i >= len(labels):
62
+ label = f"Punkt{i+1}"
63
  else:
64
  label = labels[i]
65
 
 
66
  text_position = (point[0] + 10 + circle_radius, point[1] - 5 - circle_radius)
67
+ draw.text(text_position, label, font=font, fill=(0, 0, 255))
68
 
 
 
69
  annotated = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
70
 
71
  return points, formatted_message, "", annotated
72
 
73
  def reset_points(original_image):
74
  if original_image is None:
75
+ return [], "Alle Punkte zurückgesetzt", "Alle Punkte zurückgesetzt", None
76
  else:
77
+ return [], "", "Alle Punkte zurückgesetzt", original_image
78
 
79
  def process_image(image, points, custom_filename, file_format):
 
80
  if image is None:
81
+ return None, None, None, "Bitte zuerst Bild hochladen"
82
  if len(points) != 4:
83
+ return None, None, None, "Bitte genau 4 Punkte auswählen (Oben links, Oben rechts, Unten links, Unten rechts)"
84
  height, width = image.shape[:2]
85
  if height < 300 or width < 300:
86
+ return None, None, None, "Bild zu klein. Mindestgröße: 300x300 Pixel"
87
 
88
  try:
 
89
  result = manual_perspective_transform(image, points)
90
  if result is None:
91
+ return None, None, None, "Transformation fehlgeschlagen"
92
 
 
93
  if not custom_filename:
94
+ custom_filename = f"prozessiert_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
95
 
 
96
  custom_filename = "".join(c for c in custom_filename if c.isalnum() or c in ('-', '_'))
97
  if not custom_filename:
98
+ custom_filename = f"prozessiert_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
99
 
 
 
 
 
 
 
100
  out_path = os.path.join(TEMP_DIR, custom_filename + file_format)
101
  cv2.imwrite(out_path, cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
102
 
103
+ return result, out_path, out_path, "Erfolgreich verarbeitet!"
104
 
105
  except Exception as e:
106
+ return None, None, None, f"Fehler: {str(e)}"
107
 
 
108
  def handle_image_change(img):
109
  if img is not None:
110
+ return img, [], "", "Neues Bild hochgeladen, bitte 4 Eckpunkte markieren", img
111
+ return None, [], "Bild entfernt, bitte neues Bild hochladen", "Bild entfernt, bitte neues Bild hochladen", None
112
 
 
113
  with gr.Blocks(theme=gr.themes.Default(primary_hue=gr.themes.colors.yellow, secondary_hue=gr.themes.colors.red)) as interface:
 
114
  gr.HTML("""
115
+ <div style='width: 800px;'>
116
+ <h1>OpenCV-Werkzeug: Buchseite zur Vorderansicht</h1>
117
+ <h3>Laden Sie ein Bild hoch und markieren Sie 4 Eckpunkte für die Perspektivkorrektur</h3>
118
  </div>
119
  """)
120
 
 
 
 
 
121
  with gr.Row():
122
  with gr.Column():
123
+ gr.Markdown("## Seitenbild hochladen (mind. 300x300 Pixel)")
124
+ gr.Markdown("#### Bitte markieren Sie 4 Eckpunkte in dieser Reihenfolge: Oben links, Oben rechts, Unten links, Unten rechts")
125
+ image_input = gr.Image(label="Bildbearbeitungsbereich", type="numpy", interactive=True)
 
 
 
 
 
 
 
 
 
126
  points_tracker = gr.State([])
127
 
128
  with gr.Column():
129
+ gr.Markdown("## Vorschau der markierten Punkte")
130
+ annotated_image = gr.Image(label="Punktvisualisierung", type="numpy", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  with gr.Row():
133
  with gr.Column():
134
+ coords_output = gr.Textbox(label="Ausgewählte Positionen", interactive=False)
135
+ reset_button = gr.Button("Punkte zurücksetzen")
136
+ original_image = gr.State()
137
 
138
+ gr.HTML("""<hr>""")
 
 
139
 
140
  with gr.Row():
 
 
141
  with gr.Column():
142
+ gr.Markdown("## Ausgabeeinstellungen")
 
 
143
  custom_filename = gr.Textbox(
144
+ label="Benutzerdefinierter Dateiname (optional)",
145
+ placeholder="Dateiname eingeben oder leer lassen",
146
  value=""
147
  )
148
  file_format = gr.Dropdown(
149
+ label="Dateiformat",
150
  choices=[".png", ".jpg"],
151
  value=".png"
152
  )
153
+ process_button = gr.Button("Vorderansicht generieren", variant="primary")
154
+ gr.Markdown("## Systemmeldungen")
155
+ status_output = gr.Textbox(label="Statusinformationen", interactive=False)
 
156
 
157
  with gr.Column():
158
+ gr.Markdown("## Ergebnisvorschau")
159
+ output_preview = gr.Image(label="Vorschau der Vorderansicht")
160
+ download_file = gr.File(label="Verarbeitete Datei herunterladen")
 
 
 
161
 
 
162
  image_input.select(
163
  update_coordinates,
164
  inputs=[original_image, points_tracker],
165
  outputs=[points_tracker, coords_output, status_output, annotated_image]
166
  )
167
 
 
168
  image_input.change(
169
  handle_image_change,
170
  inputs=image_input,
 
179
 
180
  process_button.click(
181
  process_image,
182
+ inputs=[image_input, points_tracker, custom_filename, file_format],
183
+ outputs=[output_preview, download_file, download_file, status_output]
 
 
 
 
 
 
 
 
 
 
184
  )
185
 
186
  if __name__ == "__main__":
187
+ interface.launch()