DawnC commited on
Commit
cc27278
1 Parent(s): 67f2673

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -1142
app.py DELETED
@@ -1,1142 +0,0 @@
1
- import os
2
- import numpy as np
3
- import torch
4
- import torch.nn as nn
5
- import gradio as gr
6
- from torchvision.models import efficientnet_v2_m, EfficientNet_V2_M_Weights
7
- from torchvision.ops import nms, box_iou
8
- import torch.nn.functional as F
9
- from torchvision import transforms
10
- from PIL import Image, ImageDraw, ImageFont, ImageFilter
11
- from data_manager import get_dog_description
12
- from urllib.parse import quote
13
- from ultralytics import YOLO
14
- import asyncio
15
- import traceback
16
-
17
-
18
- model_yolo = YOLO('yolov8l.pt')
19
-
20
-
21
- dog_breeds = ["Afghan_Hound", "African_Hunting_Dog", "Airedale", "American_Staffordshire_Terrier",
22
- "Appenzeller", "Australian_Terrier", "Bedlington_Terrier", "Bernese_Mountain_Dog",
23
- "Blenheim_Spaniel", "Border_Collie", "Border_Terrier", "Boston_Bull", "Bouvier_Des_Flandres",
24
- "Brabancon_Griffon", "Brittany_Spaniel", "Cardigan", "Chesapeake_Bay_Retriever",
25
- "Chihuahua", "Dandie_Dinmont", "Doberman", "English_Foxhound", "English_Setter",
26
- "English_Springer", "EntleBucher", "Eskimo_Dog", "French_Bulldog", "German_Shepherd",
27
- "German_Short-Haired_Pointer", "Gordon_Setter", "Great_Dane", "Great_Pyrenees",
28
- "Greater_Swiss_Mountain_Dog", "Ibizan_Hound", "Irish_Setter", "Irish_Terrier",
29
- "Irish_Water_Spaniel", "Irish_Wolfhound", "Italian_Greyhound", "Japanese_Spaniel",
30
- "Kerry_Blue_Terrier", "Labrador_Retriever", "Lakeland_Terrier", "Leonberg", "Lhasa",
31
- "Maltese_Dog", "Mexican_Hairless", "Newfoundland", "Norfolk_Terrier", "Norwegian_Elkhound",
32
- "Norwich_Terrier", "Old_English_Sheepdog", "Pekinese", "Pembroke", "Pomeranian",
33
- "Rhodesian_Ridgeback", "Rottweiler", "Saint_Bernard", "Saluki", "Samoyed",
34
- "Scotch_Terrier", "Scottish_Deerhound", "Sealyham_Terrier", "Shetland_Sheepdog",
35
- "Shih-Tzu", "Siberian_Husky", "Staffordshire_Bullterrier", "Sussex_Spaniel",
36
- "Tibetan_Mastiff", "Tibetan_Terrier", "Walker_Hound", "Weimaraner",
37
- "Welsh_Springer_Spaniel", "West_Highland_White_Terrier", "Yorkshire_Terrier",
38
- "Affenpinscher", "Basenji", "Basset", "Beagle", "Black-and-Tan_Coonhound", "Bloodhound",
39
- "Bluetick", "Borzoi", "Boxer", "Briard", "Bull_Mastiff", "Cairn", "Chow", "Clumber",
40
- "Cocker_Spaniel", "Collie", "Curly-Coated_Retriever", "Dhole", "Dingo",
41
- "Flat-Coated_Retriever", "Giant_Schnauzer", "Golden_Retriever", "Groenendael", "Keeshond",
42
- "Kelpie", "Komondor", "Kuvasz", "Malamute", "Malinois", "Miniature_Pinscher",
43
- "Miniature_Poodle", "Miniature_Schnauzer", "Otterhound", "Papillon", "Pug", "Redbone",
44
- "Schipperke", "Silky_Terrier", "Soft-Coated_Wheaten_Terrier", "Standard_Poodle",
45
- "Standard_Schnauzer", "Toy_Poodle", "Toy_Terrier", "Vizsla", "Whippet",
46
- "Wire-Haired_Fox_Terrier"]
47
-
48
- class MultiHeadAttention(nn.Module):
49
-
50
- def __init__(self, in_dim, num_heads=8):
51
- super().__init__()
52
- self.num_heads = num_heads
53
- self.head_dim = max(1, in_dim // num_heads)
54
- self.scaled_dim = self.head_dim * num_heads
55
- self.fc_in = nn.Linear(in_dim, self.scaled_dim)
56
- self.query = nn.Linear(self.scaled_dim, self.scaled_dim)
57
- self.key = nn.Linear(self.scaled_dim, self.scaled_dim)
58
- self.value = nn.Linear(self.scaled_dim, self.scaled_dim)
59
- self.fc_out = nn.Linear(self.scaled_dim, in_dim)
60
-
61
- def forward(self, x):
62
- N = x.shape[0]
63
- x = self.fc_in(x)
64
- q = self.query(x).view(N, self.num_heads, self.head_dim)
65
- k = self.key(x).view(N, self.num_heads, self.head_dim)
66
- v = self.value(x).view(N, self.num_heads, self.head_dim)
67
-
68
- energy = torch.einsum("nqd,nkd->nqk", [q, k])
69
- attention = F.softmax(energy / (self.head_dim ** 0.5), dim=2)
70
-
71
- out = torch.einsum("nqk,nvd->nqd", [attention, v])
72
- out = out.reshape(N, self.scaled_dim)
73
- out = self.fc_out(out)
74
- return out
75
-
76
- class BaseModel(nn.Module):
77
- def __init__(self, num_classes, device='cuda' if torch.cuda.is_available() else 'cpu'):
78
- super().__init__()
79
- self.device = device
80
- self.backbone = efficientnet_v2_m(weights=EfficientNet_V2_M_Weights.IMAGENET1K_V1)
81
- self.feature_dim = self.backbone.classifier[1].in_features
82
- self.backbone.classifier = nn.Identity()
83
-
84
- self.num_heads = max(1, min(8, self.feature_dim // 64))
85
- self.attention = MultiHeadAttention(self.feature_dim, num_heads=self.num_heads)
86
-
87
- self.classifier = nn.Sequential(
88
- nn.LayerNorm(self.feature_dim),
89
- nn.Dropout(0.3),
90
- nn.Linear(self.feature_dim, num_classes)
91
- )
92
-
93
- self.to(device)
94
-
95
- def forward(self, x):
96
- x = x.to(self.device)
97
- features = self.backbone(x)
98
- attended_features = self.attention(features)
99
- logits = self.classifier(attended_features)
100
- return logits, attended_features
101
-
102
-
103
- num_classes = 120
104
- device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
105
- model = BaseModel(num_classes=num_classes, device=device)
106
-
107
- checkpoint = torch.load('best_model_81_dog.pth', map_location=torch.device('cpu'))
108
- model.load_state_dict(checkpoint['model_state_dict'])
109
-
110
- # evaluation mode
111
- model.eval()
112
-
113
- # Image preprocessing function
114
- def preprocess_image(image):
115
- # If the image is numpy.ndarray turn into PIL.Image
116
- if isinstance(image, np.ndarray):
117
- image = Image.fromarray(image)
118
-
119
- # Use torchvision.transforms to process images
120
- transform = transforms.Compose([
121
- transforms.Resize((224, 224)),
122
- transforms.ToTensor(),
123
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
124
- ])
125
-
126
- return transform(image).unsqueeze(0)
127
-
128
-
129
- def get_akc_breeds_link(breed):
130
- base_url = "https://www.akc.org/dog-breeds/"
131
- breed_url = breed.lower().replace('_', '-')
132
- return f"{base_url}{breed_url}/"
133
-
134
-
135
- async def predict_single_dog(image):
136
- image_tensor = preprocess_image(image)
137
- with torch.no_grad():
138
- output = model(image_tensor)
139
- logits = output[0] if isinstance(output, tuple) else output
140
- probabilities = F.softmax(logits, dim=1)
141
- topk_probs, topk_indices = torch.topk(probabilities, k=3)
142
- top1_prob = topk_probs[0][0].item()
143
- topk_breeds = [dog_breeds[idx.item()] for idx in topk_indices[0]]
144
-
145
- # Calculate relative probabilities for display
146
- raw_probs = [prob.item() for prob in topk_probs[0]]
147
- sum_probs = sum(raw_probs)
148
- relative_probs = [f"{(prob/sum_probs * 100):.2f}%" for prob in raw_probs]
149
-
150
- return top1_prob, topk_breeds, relative_probs
151
-
152
-
153
- async def detect_multiple_dogs(image, conf_threshold=0.3, iou_threshold=0.45):
154
- results = model_yolo(image, conf=conf_threshold, iou=iou_threshold)[0]
155
- dogs = []
156
- boxes = []
157
- for box in results.boxes:
158
- if box.cls == 16: # COCO dataset class for dog is 16
159
- xyxy = box.xyxy[0].tolist()
160
- confidence = box.conf.item()
161
- boxes.append((xyxy, confidence))
162
-
163
- if not boxes:
164
- dogs.append((image, 1.0, [0, 0, image.width, image.height]))
165
- else:
166
- nms_boxes = non_max_suppression(boxes, iou_threshold)
167
-
168
- for box, confidence in nms_boxes:
169
- x1, y1, x2, y2 = box
170
- w, h = x2 - x1, y2 - y1
171
- x1 = max(0, x1 - w * 0.05)
172
- y1 = max(0, y1 - h * 0.05)
173
- x2 = min(image.width, x2 + w * 0.05)
174
- y2 = min(image.height, y2 + h * 0.05)
175
- cropped_image = image.crop((x1, y1, x2, y2))
176
- dogs.append((cropped_image, confidence, [x1, y1, x2, y2]))
177
-
178
- return dogs
179
-
180
-
181
- def non_max_suppression(boxes, iou_threshold):
182
- keep = []
183
- boxes = sorted(boxes, key=lambda x: x[1], reverse=True)
184
- while boxes:
185
- current = boxes.pop(0)
186
- keep.append(current)
187
- boxes = [box for box in boxes if calculate_iou(current[0], box[0]) < iou_threshold]
188
- return keep
189
-
190
-
191
- def calculate_iou(box1, box2):
192
- x1 = max(box1[0], box2[0])
193
- y1 = max(box1[1], box2[1])
194
- x2 = min(box1[2], box2[2])
195
- y2 = min(box1[3], box2[3])
196
-
197
- intersection = max(0, x2 - x1) * max(0, y2 - y1)
198
- area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
199
- area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
200
-
201
- iou = intersection / float(area1 + area2 - intersection)
202
- return iou
203
-
204
-
205
- async def process_single_dog(image):
206
- top1_prob, topk_breeds, relative_probs = await predict_single_dog(image)
207
-
208
- # Case 1: Low confidence - unclear image or breed not in dataset
209
- if top1_prob < 0.15:
210
- error_message = '''
211
- <div class="dog-info-card">
212
- <div class="breed-info">
213
- <p class="warning-message">
214
- <span class="icon">⚠️</span>
215
- The image is unclear or the breed is not in the dataset. Please upload a clearer image of a dog.
216
- </p>
217
- </div>
218
- </div>
219
- '''
220
- initial_state = {
221
- "explanation": error_message,
222
- "image": None,
223
- "is_multi_dog": False
224
- }
225
- return error_message, None, initial_state
226
-
227
- breed = topk_breeds[0]
228
-
229
- # Case 2: High confidence - single breed result
230
- if top1_prob >= 0.45:
231
- description = get_dog_description(breed)
232
- formatted_description = format_description_html(description, breed) # 使用 format_description_html
233
- html_content = f'''
234
- <div class="dog-info-card">
235
- <div class="breed-info">
236
- {formatted_description}
237
- </div>
238
- </div>
239
- '''
240
- initial_state = {
241
- "explanation": html_content,
242
- "image": image,
243
- "is_multi_dog": False
244
- }
245
- return html_content, image, initial_state
246
-
247
- # Case 3: Medium confidence - show top 3 breeds with relative probabilities
248
- else:
249
- breeds_html = ""
250
- for i, (breed, prob) in enumerate(zip(topk_breeds, relative_probs)):
251
- description = get_dog_description(breed)
252
- formatted_description = format_description_html(description, breed) # 使用 format_description_html
253
- breeds_html += f'''
254
- <div class="dog-info-card">
255
- <div class="breed-info">
256
- <div class="breed-header">
257
- <span class="breed-name">Breed {i+1}: {breed}</span>
258
- <span class="confidence-badge">Confidence: {prob}</span>
259
- </div>
260
- {formatted_description}
261
- </div>
262
- </div>
263
- '''
264
-
265
- initial_state = {
266
- "explanation": breeds_html,
267
- "image": image,
268
- "is_multi_dog": False
269
- }
270
- return breeds_html, image, initial_state
271
-
272
-
273
- async def predict(image):
274
- if image is None:
275
- return "Please upload an image to start.", None, None
276
-
277
- try:
278
- if isinstance(image, np.ndarray):
279
- image = Image.fromarray(image)
280
-
281
- dogs = await detect_multiple_dogs(image)
282
- # 更新顏色組合
283
- single_dog_color = '#34C759' # 清爽的綠色作為單狗顏色
284
- color_list = [
285
- '#FF5733', # 珊瑚紅
286
- '#28A745', # 深綠色
287
- '#3357FF', # 寶藍色
288
- '#FF33F5', # 粉紫色
289
- '#FFB733', # 橙黃色
290
- '#33FFF5', # 青藍色
291
- '#A233FF', # 紫色
292
- '#FF3333', # 紅色
293
- '#33FFB7', # 青綠色
294
- '#FFE033' # 金黃色
295
- ]
296
- annotated_image = image.copy()
297
- draw = ImageDraw.Draw(annotated_image)
298
-
299
- try:
300
- font = ImageFont.truetype("arial.ttf", 24)
301
- except:
302
- font = ImageFont.load_default()
303
-
304
- dogs_info = ""
305
-
306
- for i, (cropped_image, detection_confidence, box) in enumerate(dogs):
307
- color = single_dog_color if len(dogs) == 1 else color_list[i % len(color_list)]
308
-
309
- # 優化圖片上的標記
310
- draw.rectangle(box, outline=color, width=4)
311
- label = f"Dog {i+1}"
312
- label_bbox = draw.textbbox((0, 0), label, font=font)
313
- label_width = label_bbox[2] - label_bbox[0]
314
- label_height = label_bbox[3] - label_bbox[1]
315
-
316
- label_x = box[0] + 5
317
- label_y = box[1] + 5
318
- draw.rectangle(
319
- [label_x - 2, label_y - 2, label_x + label_width + 4, label_y + label_height + 4],
320
- fill='white',
321
- outline=color,
322
- width=2
323
- )
324
- draw.text((label_x, label_y), label, fill=color, font=font)
325
-
326
- top1_prob, topk_breeds, relative_probs = await predict_single_dog(cropped_image)
327
- combined_confidence = detection_confidence * top1_prob
328
-
329
- # 開始資訊卡片
330
- dogs_info += f'<div class="dog-info-card" style="border-left: 6px solid {color};">'
331
-
332
- if combined_confidence < 0.15:
333
- dogs_info += f'''
334
- <div class="dog-info-header" style="background-color: {color}10;">
335
- <span class="dog-label" style="color: {color};">Dog {i+1}</span>
336
- </div>
337
- <div class="breed-info">
338
- <p class="warning-message">
339
- <span class="icon">⚠️</span>
340
- The image is unclear or the breed is not in the dataset. Please upload a clearer image.
341
- </p>
342
- </div>
343
- '''
344
- elif top1_prob >= 0.45:
345
- breed = topk_breeds[0]
346
- description = get_dog_description(breed)
347
- dogs_info += f'''
348
- <div class="dog-info-header" style="background-color: {color}10;">
349
- <span class="dog-label" style="color: {color};">
350
- <span class="icon">🐾</span> {breed}
351
- </span>
352
- </div>
353
- <div class="breed-info">
354
- <h2 class="section-title">
355
- <span class="icon">📋</span> BASIC INFORMATION
356
- </h2>
357
- <div class="info-section">
358
- <div class="info-item">
359
- <span class="tooltip tooltip-left">
360
- <span class="icon">📏</span>
361
- <span class="label">Size:</span>
362
- <span class="tooltip-icon">ⓘ</span>
363
- <span class="tooltip-text">
364
- <strong>Size Categories:</strong><br>
365
- • Small: Under 20 pounds<br>
366
- • Medium: 20-60 pounds<br>
367
- • Large: Over 60 pounds<br>
368
- • Giant: Over 100 pounds<br>
369
- • Varies: Depends on variety
370
- </span>
371
- </span>
372
- <span class="value">{description['Size']}</span>
373
- </div>
374
- <div class="info-item">
375
- <span class="tooltip">
376
- <span class="icon">⏳</span>
377
- <span class="label">Lifespan:</span>
378
- <span class="tooltip-icon">ⓘ</span>
379
- <span class="tooltip-text">
380
- <strong>Average Lifespan:</strong><br>
381
- • Short: 6-8 years<br>
382
- • Average: 10-15 years<br>
383
- • Long: 12-20 years<br>
384
- • Varies by size: Larger breeds typically have shorter lifespans
385
- </span>
386
- </span>
387
- <span class="value">{description['Lifespan']}</span>
388
- </div>
389
- </div>
390
-
391
- <h2 class="section-title">
392
- <span class="icon">🐕</span> TEMPERAMENT & PERSONALITY
393
- </h2>
394
- <div class="temperament-section">
395
- <span class="tooltip">
396
- <span class="value">{description['Temperament']}</span>
397
- <span class="tooltip-icon">ⓘ</span>
398
- <span class="tooltip-text">
399
- <strong>Temperament Guide:</strong><br>
400
- • Describes the dog's natural behavior and personality<br>
401
- • Important for matching with owner's lifestyle<br>
402
- • Can be influenced by training and socialization
403
- </span>
404
- </span>
405
- </div>
406
-
407
- <h2 class="section-title">
408
- <span class="icon">💪</span> CARE REQUIREMENTS
409
- </h2>
410
- <div class="care-section">
411
- <div class="info-item">
412
- <span class="tooltip tooltip-left">
413
- <span class="icon">🏃</span>
414
- <span class="label">Exercise:</span>
415
- <span class="tooltip-icon">ⓘ</span>
416
- <span class="tooltip-text">
417
- <strong>Exercise Needs:</strong><br>
418
- • Low: Short walks and play sessions<br>
419
- • Moderate: 1-2 hours of daily activity<br>
420
- • High: Extensive exercise (2+ hours/day)<br>
421
- • Very High: Constant activity and mental stimulation needed
422
- </span>
423
- </span>
424
- <span class="value">{description['Exercise Needs']}</span>
425
- </div>
426
- <div class="info-item">
427
- <span class="tooltip">
428
- <span class="icon">✂️</span>
429
- <span class="label">Grooming:</span>
430
- <span class="tooltip-icon">ⓘ</span>
431
- <span class="tooltip-text">
432
- <strong>Grooming Requirements:</strong><br>
433
- • Low: Basic brushing, occasional baths<br>
434
- • Moderate: Weekly brushing, occasional grooming<br>
435
- • High: Daily brushing, frequent professional grooming needed<br>
436
- • Professional care recommended for all levels
437
- </span>
438
- </span>
439
- <span class="value">{description['Grooming Needs']}</span>
440
- </div>
441
- <div class="info-item">
442
- <span class="tooltip">
443
- <span class="icon">⭐</span>
444
- <span class="label">Care Level:</span>
445
- <span class="tooltip-icon">ⓘ</span>
446
- <span class="tooltip-text">
447
- <strong>Care Level Explained:</strong><br>
448
- • Low: Basic care and attention needed<br>
449
- • Moderate: Regular care and routine needed<br>
450
- • High: Significant time and attention needed<br>
451
- • Very High: Extensive care, training and attention required
452
- </span>
453
- </span>
454
- <span class="value">{description['Care Level']}</span>
455
- </div>
456
- </div>
457
-
458
- <h2 class="section-title">
459
- <span class="icon">👨‍👩‍👧‍👦</span> FAMILY COMPATIBILITY
460
- </h2>
461
- <div class="family-section">
462
- <div class="info-item">
463
- <span class="tooltip">
464
- <span class="icon"></span>
465
- <span class="label">Good with Children:</span>
466
- <span class="tooltip-icon">ⓘ</span>
467
- <span class="tooltip-text">
468
- <strong>Child Compatibility:</strong><br>
469
- • Yes: Excellent with kids, patient and gentle<br>
470
- • Moderate: Good with older children<br>
471
- • No: Better suited for adult households
472
- </span>
473
- </span>
474
- <span class="value">{description['Good with Children']}</span>
475
- </div>
476
- </div>
477
-
478
- <h2 class="section-title">
479
- <span class="icon">📝</span> DESCRIPTION
480
- </h2>
481
- <div class="description-section">
482
- <p>{description.get('Description', '')}</p>
483
- </div>
484
-
485
- <div class="action-section">
486
- <a href="{get_akc_breeds_link(breed)}" target="_blank" class="akc-button">
487
- <span class="icon">🌐</span>
488
- Learn more about {breed} on AKC website
489
- </a>
490
- </div>
491
- </div>
492
- '''
493
- else:
494
- dogs_info += f'''
495
- <div class="dog-info-header" style="background-color: {color}10;">
496
- <span class="dog-label" style="color: {color};">Dog {i+1}</span>
497
- </div>
498
- <div class="breed-info">
499
- <div class="model-uncertainty-note">
500
- <span class="icon">ℹ️</span>
501
- Note: The model is showing some uncertainty in its predictions.
502
- Here are the most likely breeds based on the available visual features.
503
- </div>
504
- <div class="breeds-list">
505
- '''
506
-
507
- for j, (breed, prob) in enumerate(zip(topk_breeds, relative_probs)):
508
- description = get_dog_description(breed)
509
- dogs_info += f'''
510
- <div class="breed-option uncertainty-mode">
511
- <div class="breed-header">
512
- <span class="option-number">Option {j+1}</span>
513
- <span class="breed-name">{breed}</span>
514
- <span class="confidence-badge" style="background-color: {color}20; color: {color};">
515
- Confidence: {prob}
516
- </span>
517
- </div>
518
- <div class="breed-content">
519
- {format_description_html(description, breed)}
520
- </div>
521
- </div>
522
- '''
523
- dogs_info += '</div></div>'
524
-
525
- dogs_info += '</div>'
526
-
527
-
528
- html_output = f"""
529
- <style>
530
- .dog-info-card {{
531
- border: 1px solid #e1e4e8;
532
- margin: 40px 0; /* 增加卡片間距 */
533
- padding: 0;
534
- border-radius: 12px;
535
- box-shadow: 0 2px 12px rgba(0,0,0,0.08);
536
- overflow: hidden;
537
- transition: all 0.3s ease;
538
- background: white;
539
- }}
540
-
541
- .dog-info-card:hover {{
542
- box-shadow: 0 4px 16px rgba(0,0,0,0.12);
543
- }}
544
-
545
- .dog-info-header {{
546
- padding: 24px 28px; /* 增加內距 */
547
- margin: 0;
548
- font-size: 22px;
549
- font-weight: bold;
550
- border-bottom: 1px solid #e1e4e8;
551
- }}
552
-
553
- .breed-info {{
554
- padding: 28px; /* 增加整體內距 */
555
- line-height: 1.6;
556
- }}
557
-
558
- .section-title {{
559
- font-size: 1.3em;
560
- font-weight: 700;
561
- color: #2c3e50;
562
- margin: 32px 0 20px 0;
563
- padding: 12px 0;
564
- border-bottom: 2px solid #e1e4e8;
565
- text-transform: uppercase;
566
- letter-spacing: 0.5px;
567
- display: flex;
568
- align-items: center;
569
- gap: 8px;
570
- position: relative;
571
- }}
572
-
573
- .icon {{
574
- font-size: 1.2em;
575
- display: inline-flex;
576
- align-items: center;
577
- justify-content: center;
578
- }}
579
-
580
- .info-section, .care-section, .family-section {{
581
- display: flex;
582
- flex-wrap: wrap;
583
- gap: 16px;
584
- margin-bottom: 28px; /* 增加底部間距 */
585
- padding: 20px; /* 增加內距 */
586
- background: #f8f9fa;
587
- border-radius: 12px;
588
- border: 1px solid #e1e4e8; /* 添加邊框 */
589
- }}
590
-
591
- .info-item {{
592
- background: white; /* 改為白色背景 */
593
- padding: 14px 18px; /* 增加內距 */
594
- border-radius: 8px;
595
- display: flex;
596
- align-items: center;
597
- gap: 10px;
598
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
599
- border: 1px solid #e1e4e8;
600
- flex: 1 1 auto;
601
- min-width: 200px;
602
- }}
603
-
604
- .label {{
605
- color: #666;
606
- font-weight: 600;
607
- font-size: 1.1rem;
608
- }}
609
-
610
- .value {{
611
- color: #2c3e50;
612
- font-weight: 500;
613
- font-size: 1.1rem;
614
- }}
615
-
616
- .temperament-section {{
617
- background: #f8f9fa;
618
- padding: 20px; /* 增加內距 */
619
- border-radius: 12px;
620
- margin-bottom: 28px; /* 增加間距 */
621
- color: #444;
622
- border: 1px solid #e1e4e8; /* 添加邊框 */
623
- }}
624
-
625
- .description-section {{
626
- background: #f8f9fa;
627
- padding: 24px; /* 增加內距 */
628
- border-radius: 12px;
629
- margin: 28px 0; /* 增加上下間距 */
630
- line-height: 1.8;
631
- color: #444;
632
- border: 1px solid #e1e4e8; /* 添加邊框 */
633
- fontsize: 1.1rem;
634
- }}
635
-
636
- .description-section p {{
637
- margin: 0;
638
- padding: 0;
639
- text-align: justify; /* 文字兩端對齊 */
640
- word-wrap: break-word; /* 確保長單字會換行 */
641
- white-space: pre-line; /* 保留換行但合併空白 */
642
- max-width: 100%; /* 確保不會超出容器 */
643
- }}
644
-
645
- .action-section {{
646
- margin-top: 24px;
647
- text-align: center;
648
- }}
649
-
650
- .akc-button,
651
- .breed-section .akc-link,
652
- .breed-option .akc-link {{
653
- display: inline-flex;
654
- align-items: center;
655
- padding: 14px 28px;
656
- background: linear-gradient(145deg, #00509E, #003F7F);
657
- color: white;
658
- border-radius: 12px; /* 增加圓角 */
659
- text-decoration: none;
660
- gap: 12px; /* 增加圖標和文字間距 */
661
- transition: all 0.3s ease;
662
- font-weight: 600;
663
- font-size: 1.1em;
664
- box-shadow:
665
- 0 2px 4px rgba(0,0,0,0.1),
666
- inset 0 1px 1px rgba(255,255,255,0.1);
667
- border: 1px solid rgba(255,255,255,0.1);
668
- }}
669
-
670
- .akc-button:hover,
671
- .breed-section .akc-link:hover,
672
- .breed-option .akc-link:hover {{
673
- background: linear-gradient(145deg, #003F7F, #00509E);
674
- transform: translateY(-2px);
675
- color: white;
676
- box-shadow:
677
- 0 6px 12px rgba(0,0,0,0.2),
678
- inset 0 1px 1px rgba(255,255,255,0.2);
679
- border: 1px solid rgba(255,255,255,0.2);
680
- }}
681
-
682
- .icon {{
683
- font-size: 1.3em;
684
- filter: drop-shadow(0 1px 1px rgba(0,0,0,0.2));
685
- }}
686
-
687
- .warning-message {{
688
- display: flex;
689
- align-items: center;
690
- gap: 8px;
691
- color: #ff3b30;
692
- font-weight: 500;
693
- margin: 0;
694
- padding: 16px;
695
- background: #fff5f5;
696
- border-radius: 8px;
697
- }}
698
-
699
- .model-uncertainty-note {{
700
- display: flex;
701
- align-items: center;
702
- gap: 12px;
703
- padding: 16px;
704
- background-color: #f8f9fa;
705
- border-left: 4px solid #6c757d;
706
- margin-bottom: 20px;
707
- color: #495057;
708
- border-radius: 4px;
709
- }}
710
-
711
- .breeds-list {{
712
- display: flex;
713
- flex-direction: column;
714
- gap: 20px;
715
- }}
716
-
717
- .breed-option {{
718
- background: white;
719
- border: 1px solid #e1e4e8;
720
- border-radius: 8px;
721
- overflow: hidden;
722
- }}
723
-
724
- .breed-header {{
725
- display: flex;
726
- align-items: center;
727
- padding: 16px;
728
- background: #f8f9fa;
729
- gap: 12px;
730
- border-bottom: 1px solid #e1e4e8;
731
- }}
732
-
733
- .option-number {{
734
- font-weight: 600;
735
- color: #666;
736
- padding: 4px 8px;
737
- background: #e1e4e8;
738
- border-radius: 4px;
739
- }}
740
-
741
- .breed-name {{
742
- font-size: 1.5em;
743
- font-weight: bold;
744
- color: #2c3e50;
745
- flex-grow: 1;
746
- }}
747
-
748
- .confidence-badge {{
749
- padding: 4px 12px;
750
- border-radius: 20px;
751
- font-size: 0.9em;
752
- font-weight: 500;
753
- }}
754
-
755
- .breed-content {{
756
- padding: 20px;
757
- }}
758
-
759
- .breed-content li {{
760
- margin-bottom: 8px;
761
- display: flex;
762
- align-items: flex-start; /* 改為頂部對齊 */
763
- gap: 8px;
764
- flex-wrap: wrap; /* 允許內容換行 */
765
- }}
766
-
767
- .breed-content li strong {{
768
- flex: 0 0 auto; /* 不讓標題縮放 */
769
- min-width: 100px; /* 給標題一個固定最小寬度 */
770
- }}
771
-
772
- ul {{
773
- padding-left: 0;
774
- margin: 0;
775
- list-style-type: none;
776
- }}
777
-
778
- li {{
779
- margin-bottom: 8px;
780
- display: flex;
781
- align-items: center;
782
- gap: 8px;
783
- }}
784
-
785
- .akc-link {{
786
- color: white;
787
- text-decoration: none;
788
- font-weight: 600;
789
- font-size: 1.1em;
790
- transition: all 0.3s ease;
791
- }}
792
-
793
- .akc-link:hover {{
794
- text-decoration: underline;
795
- color: #D3E3F0;
796
- }}
797
-
798
- .tooltip {{
799
- position: relative;
800
- display: inline-flex;
801
- align-items: center;
802
- gap: 4px;
803
- cursor: help;
804
- }}
805
-
806
- .tooltip .tooltip-icon {{
807
- font-size: 14px;
808
- color: #666;
809
- }}
810
-
811
- .tooltip .tooltip-text {{
812
- visibility: hidden;
813
- width: 250px;
814
- background-color: rgba(44, 62, 80, 0.95);
815
- color: white;
816
- text-align: left;
817
- border-radius: 8px;
818
- padding: 8px 10px;
819
- position: absolute;
820
- z-index: 100;
821
- bottom: 150%;
822
- left: 50%;
823
- transform: translateX(-50%);
824
- opacity: 0;
825
- transition: all 0.3s ease;
826
- font-size: 14px;
827
- line-height: 1.3;
828
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
829
- border: 1px solid rgba(255, 255, 255, 0.1)
830
- margin-bottom: 10px;
831
- }}
832
-
833
- .tooltip.tooltip-left .tooltip-text {{
834
- left: 0;
835
- transform: translateX(0);
836
- }}
837
-
838
- .tooltip.tooltip-right .tooltip-text {{
839
- left: auto;
840
- right: 0;
841
- transform: translateX(0);
842
- }}
843
-
844
- .tooltip-text strong {{
845
- color: white !important;
846
- background-color: transparent !important;
847
- display: block; /* 讓標題獨立一行 */
848
- margin-bottom: 2px; /* 增加標題下方間距 */
849
- padding-bottom: 2px; /* 加入小間距 */
850
- border-bottom: 1px solid rgba(255,255,255,0.2);
851
- }}
852
-
853
- .tooltip-text {{
854
- font-size: 13px; /* 稍微縮小字體 */
855
- }}
856
-
857
- /* 調整列表符號和文字的間距 */
858
- .tooltip-text ul {{
859
- margin: 0;
860
- padding-left: 15px; /* 減少列表符號的縮進 */
861
- }}
862
-
863
- .tooltip-text li {{
864
- margin-bottom: 1px; /* 減少列表項目間的間距 */
865
- }}
866
-
867
- .tooltip-text br {{
868
- line-height: 1.2; /* 減少行距 */
869
- }}
870
-
871
- .tooltip .tooltip-text::after {{
872
- content: "";
873
- position: absolute;
874
- top: 100%;
875
- left: 20%; /* 調整箭頭位置 */
876
- margin-left: -5px;
877
- border-width: 5px;
878
- border-style: solid;
879
- border-color: rgba(44, 62, 80, 0.95) transparent transparent transparent;
880
- }}
881
-
882
- .tooltip-left .tooltip-text::after {{
883
- left: 20%;
884
- }}
885
-
886
- /* 右側箭頭 */
887
- .tooltip-right .tooltip-text::after {{
888
- left: 80%;
889
- }}
890
-
891
- .tooltip:hover .tooltip-text {{
892
- visibility: visible;
893
- opacity: 1;
894
- }}
895
-
896
- .tooltip .tooltip-text::after {{
897
- content: "";
898
- position: absolute;
899
- top: 100%;
900
- left: 50%;
901
- transform: translateX(-50%);
902
- border-width: 8px;
903
- border-style: solid;
904
- border-color: rgba(44, 62, 80, 0.95) transparent transparent transparent;
905
- }}
906
-
907
- .uncertainty-mode .tooltip .tooltip-text {{
908
- position: absolute;
909
- left: 100%;
910
- bottom: auto;
911
- top: 50%;
912
- transform: translateY(-50%);
913
- margin-left: 10px;
914
- z-index: 1000; /* 確保提示框在最上層 */
915
- }}
916
-
917
- .uncertainty-mode .tooltip .tooltip-text::after {{
918
- content: "";
919
- position: absolute;
920
- top: 50%;
921
- right: 100%;
922
- transform: translateY(-50%);
923
- border-width: 5px;
924
- border-style: solid;
925
- border-color: transparent rgba(44, 62, 80, 0.95) transparent transparent;
926
- }}
927
-
928
- .uncertainty-mode .breed-content {{
929
- font-size: 1.1rem; /* 增加字體大小 */
930
- }}
931
-
932
- .description-section,
933
- .description-section p,
934
- .temperament-section,
935
- .temperament-section .value,
936
- .info-item,
937
- .info-item .value,
938
- .breed-content {{
939
- font-size: 1.1rem !important; /* 使用 !important 確保覆蓋其他樣式 */
940
- }}
941
- </style>
942
- {dogs_info}
943
- """
944
-
945
- initial_state = {
946
- "dogs_info": dogs_info,
947
- "image": annotated_image,
948
- "is_multi_dog": len(dogs) > 1,
949
- "html_output": html_output
950
- }
951
-
952
- return html_output, annotated_image, initial_state
953
-
954
- except Exception as e:
955
- error_msg = f"An error occurred: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
956
- print(error_msg)
957
- return error_msg, None, None
958
-
959
-
960
- def show_details_html(choice, previous_output, initial_state):
961
- if not choice:
962
- return previous_output, gr.update(visible=True), initial_state
963
-
964
- try:
965
- breed = choice.split("More about ")[-1]
966
- description = get_dog_description(breed)
967
- formatted_description = format_description_html(description, breed)
968
-
969
- html_output = f"""
970
- <div class="dog-info">
971
- <h2>{breed}</h2>
972
- {formatted_description}
973
- </div>
974
- """
975
-
976
- initial_state["current_description"] = html_output
977
- initial_state["original_buttons"] = initial_state.get("buttons", [])
978
-
979
- return html_output, gr.update(visible=True), initial_state
980
- except Exception as e:
981
- error_msg = f"An error occurred while showing details: {e}"
982
- print(error_msg)
983
- return f"<p style='color: red;'>{error_msg}</p>", gr.update(visible=True), initial_state
984
-
985
-
986
- def format_description_html(description, breed):
987
- html = "<ul style='list-style-type: none; padding-left: 0;'>"
988
- if isinstance(description, dict):
989
- for key, value in description.items():
990
- if key != "Breed": # 跳過重複的品種顯示
991
- if key == "Size":
992
- html += f'''
993
- <li style='margin-bottom: 10px;'>
994
- <span class="tooltip">
995
- <strong>{key}:</strong>
996
- <span class="tooltip-icon">ⓘ</span>
997
- <span class="tooltip-text">
998
- <strong>Size Categories:</strong><br>
999
- • Small: Under 20 pounds<br>
1000
- • Medium: 20-60 pounds<br>
1001
- • Large: Over 60 pounds
1002
- </span>
1003
- </span> {value}
1004
- </li>
1005
- '''
1006
- elif key == "Exercise Needs":
1007
- html += f'''
1008
- <li style='margin-bottom: 10px;'>
1009
- <span class="tooltip">
1010
- <strong>{key}:</strong>
1011
- <span class="tooltip-icon">ⓘ</span>
1012
- <span class="tooltip-text">
1013
- <strong>Exercise Needs:</strong><br>
1014
- • High: 2+ hours of daily exercise<br>
1015
- • Moderate: 1-2 hours of daily activity<br>
1016
- • Low: Short walks and play sessions
1017
- </span>
1018
- </span> {value}
1019
- </li>
1020
- '''
1021
- elif key == "Grooming Needs":
1022
- html += f'''
1023
- <li style='margin-bottom: 10px;'>
1024
- <span class="tooltip">
1025
- <strong>{key}:</strong>
1026
- <span class="tooltip-icon">ⓘ</span>
1027
- <span class="tooltip-text">
1028
- <strong>Grooming Requirements:</strong><br>
1029
- • High: Daily brushing, regular professional care<br>
1030
- • Moderate: Weekly brushing, occasional grooming<br>
1031
- • Low: Minimal brushing, basic maintenance
1032
- </span>
1033
- </span> {value}
1034
- </li>
1035
- '''
1036
- elif key == "Care Level":
1037
- html += f'''
1038
- <li style='margin-bottom: 10px;'>
1039
- <span class="tooltip">
1040
- <strong>{key}:</strong>
1041
- <span class="tooltip-icon">ⓘ</span>
1042
- <span class="tooltip-text">
1043
- <strong>Care Level Explained:</strong><br>
1044
- • High: Needs significant training and attention<br>
1045
- • Moderate: Regular care and routine needed<br>
1046
- • Low: More independent, basic care sufficient
1047
- </span>
1048
- </span> {value}
1049
- </li>
1050
- '''
1051
- elif key == "Good with Children":
1052
- html += f'''
1053
- <li style='margin-bottom: 10px;'>
1054
- <span class="tooltip">
1055
- <strong>{key}:</strong>
1056
- <span class="tooltip-icon">ⓘ</span>
1057
- <span class="tooltip-text">
1058
- <strong>Child Compatibility:</strong><br>
1059
- • Yes: Excellent with kids, patient and gentle<br>
1060
- • Moderate: Good with older children<br>
1061
- • No: Better suited for adult households
1062
- </span>
1063
- </span> {value}
1064
- </li>
1065
- '''
1066
- elif key == "Lifespan":
1067
- html += f'''
1068
- <li style='margin-bottom: 10px;'>
1069
- <span class="tooltip">
1070
- <strong>{key}:</strong>
1071
- <span class="tooltip-icon">ⓘ</span>
1072
- <span class="tooltip-text">
1073
- <strong>Average Lifespan:</strong><br>
1074
- • Short: 6-8 years<br>
1075
- • Average: 10-15 years<br>
1076
- • Long: 12-20 years
1077
- </span>
1078
- </span> {value}
1079
- </li>
1080
- '''
1081
- elif key == "Temperament":
1082
- html += f'''
1083
- <li style='margin-bottom: 10px;'>
1084
- <span class="tooltip">
1085
- <strong>{key}:</strong>
1086
- <span class="tooltip-icon">ⓘ</span>
1087
- <span class="tooltip-text">
1088
- <strong>Temperament Guide:</strong><br>
1089
- • Describes the dog's natural behavior<br>
1090
- • Important for matching with owner
1091
- </span>
1092
- </span> {value}
1093
- </li>
1094
- '''
1095
- else:
1096
- # 其他欄位保持原樣顯示
1097
- html += f"<li style='margin-bottom: 10px;'><strong>{key}:</strong> {value}</li>"
1098
- else:
1099
- html += f"<li>{description}</li>"
1100
- html += "</ul>"
1101
-
1102
- # 添加AKC連結
1103
- html += f'''
1104
- <div class="action-section">
1105
- <a href="{get_akc_breeds_link(breed)}" target="_blank" class="akc-button">
1106
- <span class="icon">🌐</span>
1107
- Learn more about {breed} on AKC website
1108
- </a>
1109
- </div>
1110
- '''
1111
- return html
1112
-
1113
-
1114
- with gr.Blocks() as iface:
1115
- gr.HTML("<h1 style='text-align: center;'>🐶 Dog Breed Classifier 🔍</h1>")
1116
- gr.HTML("<p style='text-align: center;'>Upload a picture of a dog, and the model will predict its breed and provide detailed information!</p>")
1117
- gr.HTML("<p style='text-align: center; color: #666; font-size: 0.9em;'>Note: The model's predictions may not always be 100% accurate, and it is recommended to use the results as a reference.</p>")
1118
-
1119
-
1120
- with gr.Row():
1121
- input_image = gr.Image(label="Upload a dog image", type="pil")
1122
- output_image = gr.Image(label="Annotated Image")
1123
-
1124
- output = gr.HTML(label="Prediction Results")
1125
- initial_state = gr.State()
1126
-
1127
- input_image.change(
1128
- predict,
1129
- inputs=input_image,
1130
- outputs=[output, output_image, initial_state]
1131
- )
1132
-
1133
- gr.Examples(
1134
- examples=['Border_Collie.jpg', 'Golden_Retriever.jpeg', 'Saint_Bernard.jpeg', 'French_Bulldog.jpeg', 'Samoyed.jpg'],
1135
- inputs=input_image
1136
- )
1137
-
1138
- gr.HTML('For more details on this project and other work, feel free to visit my GitHub <a href="https://github.com/Eric-Chung-0511/Learning-Record/tree/main/Data%20Science%20Projects/Dog_Breed_Classifier">Dog Breed Classifier</a>')
1139
-
1140
-
1141
- if __name__ == "__main__":
1142
- iface.launch()