diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Calculate_Process/Calculate.py b/Calculate_Process/Calculate.py index e80953f..f8b048c 100644 --- a/Calculate_Process/Calculate.py +++ b/Calculate_Process/Calculate.py @@ -5,28 +5,26 @@ from decimal import Decimal, ROUND_HALF_UP class Calculate(): def __init__(self) -> None: self.Loss, self.Accuracy, self.Precision, self.Recall, self.F1, self.AUC = 0, 0, 0, 0, 0, 0 - self.Loss_Record, self.Accuracy_Record, self.Precision_Record, self.Recall_Record, self.F1_Record, self.AUC_Record = [], [], [], [], [], [] + self.Loss_Record, self.Accuracy_Record, self.Precision_Record, self.Recall_Record, self.F1_Record = [], [], [], [], [] self.History = [] pass - def Append_numbers(self, Loss, Accuracy, Precision, Recall, AUC, F1): + def Append_numbers(self, Loss, Accuracy, Precision, Recall, F1): self.Loss_Record.append(Loss) self.Accuracy_Record.append(Accuracy) self.Precision_Record.append(Precision) self.Recall_Record.append(Recall) self.F1_Record.append(F1) - self.AUC_Record.append(AUC) pass - def Construction_To_DataFrame(self, Loss, Accuracy, Precision, Recall, F1, AUC): + def Construction_To_DataFrame(self, Loss, Accuracy, Precision, Recall, F1): DataFrame = pd.DataFrame( { "loss" : "{:.2f}".format(Loss), - "precision" : "{:.2f}".format(Precision * 100), - "recall" : "{:.2f}".format(Recall * 100), - "accuracy" : "{:.2f}".format(Accuracy * 100), - "f1" : "{:.2f}".format(F1 * 100), - "AUC" : "{:.2f}".format(AUC * 100) + "precision" : "{:.2f}".format(Precision), + "recall" : "{:.2f}".format(Recall), + "accuracy" : "{:.2f}".format(Accuracy), + "f1" : "{:.2f}".format(F1), }, index = [0] ) self.History.append(DataFrame) @@ -38,9 +36,8 @@ class Calculate(): Precision_Mean = np.mean(self.Precision_Record) Recall_Mean = np.mean(self.Recall_Record) F1_Mean = np.mean(self.F1_Record) - AUC_Mean = np.mean(self.AUC_Record) - Mean_DataFram = self.Construction_To_DataFrame(Loss_Mean, Accuracy_Mean, Precision_Mean, Recall_Mean, F1_Mean, AUC_Mean) + Mean_DataFram = self.Construction_To_DataFrame(Loss_Mean, Accuracy_Mean * 100, Precision_Mean * 100, Recall_Mean * 100, F1_Mean * 100) return Mean_DataFram @@ -50,20 +47,18 @@ class Calculate(): Precision_Std = Decimal(str(np.std(self.Precision_Record))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) Recall_Std = Decimal(str(np.std(self.Recall_Record))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) F1_Std = Decimal(str(np.std(self.F1_Record))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - AUC_Std = Decimal(str(np.std(self.AUC_Record))).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) - Std_DataFram = self.Construction_To_DataFrame(Loss_Std, Accuracy_Std, Precision_Std, Recall_Std, F1_Std, AUC_Std) + Std_DataFram = self.Construction_To_DataFrame(Loss_Std, Accuracy_Std, Precision_Std, Recall_Std, F1_Std) return Std_DataFram def Output_Style(self): Result = pd.DataFrame( { - "loss" : "{}%±{}".format(self.History[0]["loss"][0], self.History[1]["loss"][0]), + "loss" : "{}±{}".format(self.History[0]["loss"][0], self.History[1]["loss"][0]), "precision" : "{}%±{}".format(self.History[0]["precision"][0], self.History[1]["precision"][0]), "recall" : "{}%±{}".format(self.History[0]["recall"][0], self.History[1]["recall"][0]), "accuracy" : "{}%±{}".format(self.History[0]["accuracy"][0], self.History[1]["accuracy"][0]), "f1" : "{}%±{}".format(self.History[0]["f1"][0], self.History[1]["f1"][0]), - "AUC" : "{}%±{}".format(self.History[0]["AUC"][0], self.History[1]["AUC"][0]) }, index = [0] ) return Result \ No newline at end of file diff --git a/Calculate_Process/__pycache__/Calculate.cpython-311.pyc b/Calculate_Process/__pycache__/Calculate.cpython-311.pyc index 099f699..1b525b1 100644 Binary files a/Calculate_Process/__pycache__/Calculate.cpython-311.pyc and b/Calculate_Process/__pycache__/Calculate.cpython-311.pyc differ diff --git a/Calculate_Process/__pycache__/Calculate.cpython-312.pyc b/Calculate_Process/__pycache__/Calculate.cpython-312.pyc new file mode 100644 index 0000000..0e9fc34 Binary files /dev/null and b/Calculate_Process/__pycache__/Calculate.cpython-312.pyc differ diff --git a/Calculate_Process/__pycache__/Calculate.cpython-313.pyc b/Calculate_Process/__pycache__/Calculate.cpython-313.pyc new file mode 100644 index 0000000..d192fac Binary files /dev/null and b/Calculate_Process/__pycache__/Calculate.cpython-313.pyc differ diff --git a/Calculate_Process/__pycache__/__init__.cpython-311.pyc b/Calculate_Process/__pycache__/__init__.cpython-311.pyc index 3406166..57d9b4a 100644 Binary files a/Calculate_Process/__pycache__/__init__.cpython-311.pyc and b/Calculate_Process/__pycache__/__init__.cpython-311.pyc differ diff --git a/Calculate_Process/__pycache__/__init__.cpython-312.pyc b/Calculate_Process/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0069b7a Binary files /dev/null and b/Calculate_Process/__pycache__/__init__.cpython-312.pyc differ diff --git a/Calculate_Process/__pycache__/__init__.cpython-313.pyc b/Calculate_Process/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..6088ad5 Binary files /dev/null and b/Calculate_Process/__pycache__/__init__.cpython-313.pyc differ diff --git a/Density_Peak_Algorithm.py b/Density_Peak_Algorithm.py new file mode 100644 index 0000000..fc0cd1a --- /dev/null +++ b/Density_Peak_Algorithm.py @@ -0,0 +1,760 @@ +import torch +import matplotlib +matplotlib.use('Agg') # 設置非 GUI 後端,避免 Tkinter 錯誤 +import matplotlib.pyplot as plt +from PIL import Image +import numpy as np +import os +from skimage.segmentation import slic +from skimage.util import img_as_float +from skimage.color import label2rgb +from scipy.spatial.distance import cdist + +# 設置全局中文字體支持 +plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'Arial Unicode MS', 'DejaVu Sans'] +plt.rcParams['axes.unicode_minus'] = False + +from utils.Stomach_Config import Loading_Config, Save_Result_File_Config +from Load_process.LoadData import Loding_Data_Root +from Load_process.file_processing import Process_File +from merge_class.merge import merge +from model_data_processing.processing import make_label_list +from Load_process.LoadData import Load_Data_Prepare + + +def save_superpixel_regions(image_array, segments, save_dir, image_name, max_regions=50): + """ + Save individual superpixel regions as separate images. + + Args: + - image_array: Original image array (H, W, 3) + - segments: Superpixel segmentation labels (H, W) + - save_dir: Directory to save superpixel region images + - image_name: Base name for the image files + - max_regions: Maximum number of regions to save (to avoid too many files) + + Returns: + - saved_regions: List of saved region information + """ + os.makedirs(save_dir, exist_ok=True) + saved_regions = [] + unique_segments = np.unique(segments) + + # Limit the number of regions to save + if len(unique_segments) > max_regions: + print(f"⚠️ 超像素數量 ({len(unique_segments)}) 超過限制 ({max_regions}),只保存前 {max_regions} 個") + unique_segments = unique_segments[:max_regions] + + print(f"💾 保存 {len(unique_segments)} 個超像素區域到: {save_dir}") + + for i, segment_id in enumerate(unique_segments): + # Get mask for current superpixel + mask = segments == segment_id + + # Get bounding box of the region + y_coords, x_coords = np.where(mask) + if len(y_coords) == 0: + continue + + min_y, max_y = y_coords.min(), y_coords.max() + min_x, max_x = x_coords.min(), x_coords.max() + + # Extract the region with some padding + padding = 5 + min_y = max(0, min_y - padding) + max_y = min(image_array.shape[0], max_y + padding + 1) + min_x = max(0, min_x - padding) + max_x = min(image_array.shape[1], max_x + padding + 1) + + # Extract region from original image + region_image = image_array[min_y:max_y, min_x:max_x].copy() + region_mask = mask[min_y:max_y, min_x:max_x] + + # Apply mask to make background transparent/black + region_image[~region_mask] = [0, 0, 0] # Set non-region pixels to black + + # Convert to PIL Image and save + region_pil = Image.fromarray(region_image.astype(np.uint8)) + + # Create filename + region_filename = f"{image_name}_superpixel_{segment_id:03d}_region_{i+1:03d}.png" + region_path = os.path.join(save_dir, region_filename) + + # Save the region + region_pil.save(region_path) + + # Calculate region statistics + region_pixels = image_array[mask] + mean_color = np.mean(region_pixels, axis=0) + region_area = np.sum(mask) + centroid_y = np.mean(y_coords) + centroid_x = np.mean(x_coords) + + region_info = { + 'segment_id': segment_id, + 'filename': region_filename, + 'path': region_path, + 'area': region_area, + 'centroid': (centroid_x, centroid_y), + 'mean_color': mean_color, + 'bbox': (min_x, min_y, max_x, max_y) + } + saved_regions.append(region_info) + + print(f"✅ 成功保存 {len(saved_regions)} 個超像素區域影像") + return saved_regions + + +def calculate_optimal_superpixel_params(image_size): + """ + Calculate optimal superpixel parameters based on image size. + + Args: + - image_size: Total number of pixels (width * height) + + Returns: + - n_segments: Number of superpixel segments + - compactness: Compactness parameter for SLIC + """ + # 根據圖片大小動態調整參數 + if image_size < 50000: # 小圖片 (< 224x224) + n_segments = min(100, max(50, image_size // 500)) + compactness = 15 + elif image_size < 200000: # 中等圖片 (< 447x447) + n_segments = min(300, max(100, image_size // 800)) + compactness = 12 + elif image_size < 500000: # 大圖片 (< 707x707) + n_segments = min(500, max(200, image_size // 1000)) + compactness = 10 + else: # 超大圖片 + n_segments = min(800, max(300, image_size // 1500)) + compactness = 8 + + return int(n_segments), compactness + + +def extract_superpixel_features(image_array, segments): + """ + Extract features for each superpixel region. + + Args: + - image_array: Original image array (H, W, 3) + - segments: Superpixel segmentation labels (H, W) + + Returns: + - features: Array of features for each superpixel (N_superpixels, 5) + [mean_R, mean_G, mean_B, norm_centroid_x, norm_centroid_y] + - centroids: Array of centroid positions for each superpixel (N_superpixels, 2) + """ + n_segments = len(np.unique(segments)) + features = [] + centroids = [] + + for segment_id in np.unique(segments): + # Get mask for current superpixel + mask = segments == segment_id + + # Extract color features (mean RGB) + region_pixels = image_array[mask] + mean_color = np.mean(region_pixels, axis=0) + + # Extract position features (centroid) + y_coords, x_coords = np.where(mask) + centroid_y = np.mean(y_coords) + centroid_x = np.mean(x_coords) + + # Combine features: [mean_R, mean_G, mean_B, centroid_x, centroid_y] + # Normalize centroid coordinates to [0, 1] range + norm_centroid_x = centroid_x / image_array.shape[1] + norm_centroid_y = centroid_y / image_array.shape[0] + feature_vector = np.concatenate([mean_color, [norm_centroid_x, norm_centroid_y]]) + + features.append(feature_vector) + centroids.append([centroid_x, centroid_y]) + + return np.array(features), np.array(centroids) + + +def fuzzy_c_means(data, n_clusters, m=2.0, max_iter=100, tol=1e-4, random_state=None): + """ + Fuzzy C-means clustering algorithm implementation. + + Args: + - data: Input data array (N_samples, N_features) + - n_clusters: Number of clusters + - m: Fuzziness parameter (m > 1, typically 2.0) + - max_iter: Maximum number of iterations + - tol: Tolerance for convergence + - random_state: Random seed for reproducibility + + Returns: + - centers: Cluster centers (n_clusters, N_features) + - membership: Membership matrix (N_samples, n_clusters) + - labels: Hard cluster assignments (N_samples,) + - objective: Final objective function value + - n_iter: Number of iterations performed + """ + if random_state is not None: + np.random.seed(random_state) + torch.manual_seed(random_state) + + # Convert to torch tensor + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + X = torch.from_numpy(data).float().to(device) + n_samples, n_features = X.shape + + # Initialize membership matrix randomly + U = torch.rand(n_samples, n_clusters, device=device) + U = U / U.sum(dim=1, keepdim=True) # Normalize so each row sums to 1 + + print(f"🔄 開始 Fuzzy C-means 聚類: {n_clusters} 個聚類中心, 模糊度參數 m={m}") + + for iteration in range(max_iter): + U_old = U.clone() + + # Update cluster centers + Um = U ** m # Membership matrix raised to power m + centers = (Um.T @ X) / Um.sum(dim=0, keepdim=True).T + + # Update membership matrix + # Calculate distances from each point to each center + distances = torch.cdist(X, centers) # (n_samples, n_clusters) + + # Avoid division by zero + distances = torch.clamp(distances, min=1e-10) + + # Calculate new membership values + power = 2.0 / (m - 1.0) + distance_matrix = distances ** power + + # For each sample, calculate membership to each cluster + for i in range(n_samples): + for j in range(n_clusters): + denominator = torch.sum((distance_matrix[i, j] / distance_matrix[i, :])) + U[i, j] = 1.0 / denominator + + # Check for convergence + diff = torch.norm(U - U_old) + if diff < tol: + print(f"✅ Fuzzy C-means 收斂於第 {iteration + 1} 次迭代 (差異: {diff:.6f})") + break + + if (iteration + 1) % 20 == 0: + print(f" 迭代 {iteration + 1}/{max_iter}, 差異: {diff:.6f}") + + # Calculate final objective function value + Um = U ** m + distances_squared = torch.cdist(X, centers) ** 2 + objective = torch.sum(Um * distances_squared).item() + + # Get hard cluster assignments (highest membership) + labels = torch.argmax(U, dim=1) + + # Convert back to numpy + centers_np = centers.cpu().numpy() + membership_np = U.cpu().numpy() + labels_np = labels.cpu().numpy() + + print(f"🎯 Fuzzy C-means 完成: 目標函數值 = {objective:.4f}") + + return centers_np, membership_np, labels_np, objective, iteration + 1 + + +def determine_optimal_clusters(data, gamma_values, max_clusters=10, min_clusters=2): + """ + Determine optimal number of clusters using gamma values from density peak analysis + and fuzzy clustering validation indices. + + Args: + - data: Input data array (N_samples, N_features) + - gamma_values: Gamma values from density peak analysis + - max_clusters: Maximum number of clusters to test + - min_clusters: Minimum number of clusters to test + + Returns: + - optimal_k: Optimal number of clusters + - scores: Dictionary containing validation scores for each k + """ + n_samples = len(data) + max_clusters = min(max_clusters, n_samples - 1) + + # Method 1: Use gamma values to estimate cluster centers + # Sort gamma values and look for significant drops + sorted_gamma = np.sort(gamma_values)[::-1] # Descending order + gamma_diffs = np.diff(sorted_gamma) + + # Find the largest drop in gamma values (elbow method) + if len(gamma_diffs) > 0: + elbow_idx = np.argmax(np.abs(gamma_diffs)) + 1 + gamma_suggested_k = min(max_clusters, max(min_clusters, elbow_idx)) + else: + gamma_suggested_k = min_clusters + + print(f"📊 基於 Gamma 值分析建議的聚類數: {gamma_suggested_k}") + + # Method 2: Test different k values with fuzzy clustering validation + scores = {} + best_k = gamma_suggested_k + best_score = -np.inf + + print(f"🔍 測試聚類數從 {min_clusters} 到 {max_clusters}...") + + for k in range(min_clusters, max_clusters + 1): + try: + # Perform fuzzy c-means clustering + centers, membership, labels, objective, n_iter = fuzzy_c_means( + data, k, m=2.0, max_iter=50, random_state=42 + ) + + # Calculate Partition Coefficient (PC) - higher is better + pc = np.mean(np.sum(membership ** 2, axis=1)) + + # Calculate Partition Entropy (PE) - lower is better + pe = -np.mean(np.sum(membership * np.log(membership + 1e-10), axis=1)) + + # Calculate Modified Partition Coefficient (MPC) - higher is better + mpc = 1 - k / (k - 1) * (1 - pc) if k > 1 else pc + + # Combined score (higher is better) + combined_score = pc - 0.1 * pe + 0.5 * mpc + + scores[k] = { + 'pc': pc, + 'pe': pe, + 'mpc': mpc, + 'combined_score': combined_score, + 'objective': objective, + 'n_iter': n_iter + } + + print(f" K={k}: PC={pc:.3f}, PE={pe:.3f}, MPC={mpc:.3f}, 組合分數={combined_score:.3f}") + + if combined_score > best_score: + best_score = combined_score + best_k = k + + except Exception as e: + print(f" K={k}: 聚類失敗 - {e}") + scores[k] = {'error': str(e)} + + print(f"🎯 最佳聚類數: {best_k} (組合分數: {best_score:.3f})") + + return best_k, scores + + +def visualize_clustering_results(image_array, segments, labels, centers, membership, save_path): + """ + Visualize fuzzy clustering results on the original image. + + Args: + - image_array: Original image array (H, W, 3) + - segments: Superpixel segmentation labels (H, W) + - labels: Hard cluster assignments for each superpixel + - centers: Cluster centers + - membership: Fuzzy membership matrix + - save_path: Path to save the visualization + """ + # 設置中文字體支持 + plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'DejaVu Sans'] + plt.rcParams['axes.unicode_minus'] = False + + fig, axes = plt.subplots(2, 3, figsize=(18, 12)) + + # Original image + axes[0, 0].imshow(image_array) + axes[0, 0].set_title('原始影像', fontsize=14) + axes[0, 0].axis('off') + + # Superpixel segmentation + superpixel_img = label2rgb(segments, image_array, kind='avg', bg_label=0) + axes[0, 1].imshow(superpixel_img) + axes[0, 1].set_title(f'超像素分割 ({len(np.unique(segments))} 個區域)', fontsize=14) + axes[0, 1].axis('off') + + # Hard clustering result + n_clusters = len(centers) + colors = plt.cm.Set3(np.linspace(0, 1, n_clusters)) + + # Create clustering visualization + cluster_img = np.zeros_like(image_array) + for segment_id in np.unique(segments): + if segment_id < len(labels): + cluster_id = labels[segment_id] + mask = segments == segment_id + cluster_img[mask] = colors[cluster_id][:3] + + axes[0, 2].imshow(cluster_img) + axes[0, 2].set_title(f'硬聚類結果 ({n_clusters} 個聚類)', fontsize=14) + axes[0, 2].axis('off') + + # Fuzzy membership visualization for top 3 clusters + for i in range(min(3, n_clusters)): + fuzzy_img = np.zeros(image_array.shape[:2]) + for segment_id in np.unique(segments): + if segment_id < len(membership): + mask = segments == segment_id + fuzzy_img[mask] = membership[segment_id, i] + + im = axes[1, i].imshow(fuzzy_img, cmap='hot', vmin=0, vmax=1) + axes[1, i].set_title(f'聚類 {i+1} 的模糊隸屬度', fontsize=14) + axes[1, i].axis('off') + plt.colorbar(im, ax=axes[1, i], fraction=0.046, pad=0.04) + + plt.tight_layout() + plt.savefig(save_path, dpi=300, bbox_inches='tight') + plt.close() + print(f"📊 聚類結果可視化已保存至: {save_path}") + + +def compute_decision_graph(image_path, Save_Root, dc=None, use_gaussian=False, threshold_factor=2.0, use_superpixels=True, n_segments=None, compactness=None, save_regions=True, max_regions=50): + """ + Process a single image to compute the decision graph using Density Peak Clustering principles. + Identifies potential cluster centers without performing full clustering. + Also computes gamma (rho * delta) and n (index in descending order of gamma) values. + + Args: + - image_path: Path to the image file. + - dc: Cut-off distance. If None, approximates it as 2% of average neighbors. + - use_gaussian: If True, uses Gaussian kernel for density; else cut-off kernel. + - threshold_factor: Factor for std deviation to determine thresholds for centers. + - use_superpixels: If True, uses SLIC superpixel segmentation instead of raw pixels. + - n_segments: Number of superpixel segments (only used if use_superpixels=True). + - compactness: Compactness parameter for SLIC algorithm (only used if use_superpixels=True). + + Returns: + - dict: Contains center_indices, center_points, rho, delta, gamma, and n arrays. + gamma = rho * delta (product of local density and minimum distance) + n = index in descending order of gamma (1-indexed as per Density Peak convention) + If use_superpixels=True, also contains 'segments' and 'superpixel_features'. + """ + # Load image + img = Image.open(image_path).convert('RGB') + img_array = np.array(img) / 255.0 # Normalize to [0, 1] + image_size = img_array.shape[0] * img_array.shape[1] + + # 強制使用 Superpixel 分割,並動態計算參數 + if n_segments is None or compactness is None: + n_segments, compactness = calculate_optimal_superpixel_params(image_size) + + print(f"🖼️ 圖像大小: {img.size} ({image_size:,} 像素)") + print(f"🔧 使用動態 Superpixel 參數: n_segments={n_segments}, compactness={compactness}") + + # Apply SLIC superpixel segmentation (強制使用) + print(f"🎯 應用 SLIC 超像素分割,目標 {n_segments} 個區域...") + segments = slic(img_array, n_segments=n_segments, compactness=compactness, + start_label=1, enforce_connectivity=True) + + # Extract features for each superpixel + superpixel_features, superpixel_centroids = extract_superpixel_features(img_array, segments) + points = torch.from_numpy(superpixel_features).float() + + print(f"✅ 成功從 {img_array.shape[0] * img_array.shape[1]:,} 像素壓縮到 {len(superpixel_features)} 個超像素") + + # Save superpixel regions if requested + if save_regions: + # Get image name without extension + image_name = os.path.splitext(os.path.basename(image_path))[0] + superpixel_regions_dir = os.path.join(Save_Root, f"{image_name}_superpixel_regions") + + # Convert back to 0-255 range for saving + img_array_255 = (img_array * 255).astype(np.uint8) + saved_regions = save_superpixel_regions(img_array_255, segments, superpixel_regions_dir, image_name, max_regions) + print(f"💾 已保存 {len(saved_regions)} 個超像素區域影像到: {superpixel_regions_dir}") + + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + points = points.to(device) + N = points.shape[0] + + print(f"🔄 處理圖像: {os.path.basename(image_path)} 在 {device} 上 (使用超像素分割)") + + # Compute pairwise distances + dist = torch.cdist(points, points) + + # Approximate dc if not provided (using a sample for efficiency) + if dc is None: + # Sample 1000 points to estimate dc + sample_size = min(1000, N) + sample_idx = torch.randperm(N)[:sample_size] + sample_dist = torch.cdist(points[sample_idx], points) + sample_dist_flat = sample_dist.flatten() + sample_dist_flat = sample_dist_flat[sample_dist_flat > 0] # Exclude zeros + sorted_dist = torch.sort(sample_dist_flat)[0] + pos = int(len(sorted_dist) * 0.02) + dc = sorted_dist[pos].item() + print(f"Approximated dc: {dc}") + + # Compute local density rho + if use_gaussian: + rho = torch.exp(-(dist ** 2) / (2 * dc ** 2)).sum(dim=1) - 1 + else: + rho = (dist < dc).float().sum(dim=1) - 1 + + # Compute delta + sorted_rho, sorted_idx = torch.sort(rho, descending=True) + delta = torch.full((N,), 0.0, device=device) + nn = torch.full((N,), -1, dtype=torch.long, device=device) + + # For the highest density point + delta[sorted_idx[0]] = dist[sorted_idx[0]].max() + + # For others + for i in range(1, N): + higher_idx = sorted_idx[:i] + cur_idx = sorted_idx[i] + dists_to_higher = dist[cur_idx, higher_idx] + min_dist_idx = torch.argmin(dists_to_higher) + delta[cur_idx] = dists_to_higher[min_dist_idx] + nn[cur_idx] = higher_idx[min_dist_idx] + + # Calculate gamma (rho * delta) and n (index in descending order of gamma) + gamma = rho * delta + sorted_gamma_indices = torch.argsort(gamma, descending=True) + n = torch.empty_like(sorted_gamma_indices) + n[sorted_gamma_indices] = torch.arange(1, N + 1, device=device) + + # Plot decision graph with two subplots + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) + + # Decision graph (rho vs delta) + rho_np = rho.cpu().numpy() + delta_np = delta.cpu().numpy() + + # 使用顏色映射來顯示密度 + scatter1 = ax1.scatter(rho_np, delta_np, c=gamma.cpu().numpy(), s=8, alpha=0.7, cmap='viridis') + ax1.set_xlabel('Rho (Local Density)', fontsize=12) + ax1.set_ylabel('Delta (Min Distance to Higher Density)', fontsize=12) + ax1.set_title(f'Decision Graph - {os.path.basename(image_path)}\n(Superpixels: {len(rho_np)})', fontsize=14) + ax1.grid(True, alpha=0.3) + + # 添加顏色條 + cbar1 = plt.colorbar(scatter1, ax=ax1) + cbar1.set_label('Gamma (Rho × Delta)', fontsize=10) + + # Gamma vs n plot - with data validation for log scale + gamma_np = gamma.cpu().numpy() + n_np = n.cpu().numpy() + + # Filter out non-positive values for log scale + positive_mask = gamma_np > 0 + if positive_mask.sum() > 0: + scatter2 = ax2.scatter(n_np[positive_mask], gamma_np[positive_mask], + c=rho_np[positive_mask], s=8, alpha=0.7, cmap='plasma') + ax2.set_yscale('log') + + # 添加顏色條 + cbar2 = plt.colorbar(scatter2, ax=ax2) + cbar2.set_label('Rho (Local Density)', fontsize=10) + else: + # Fallback to linear scale if no positive values + ax2.scatter(n_np, gamma_np, s=8, alpha=0.7, color='blue') + print("⚠️ Warning: No positive gamma values found, using linear scale instead of log scale") + + ax2.set_xlabel('N (Index in descending order of Gamma)', fontsize=12) + ax2.set_ylabel('Gamma (Rho × Delta)', fontsize=12) + ax2.set_title('Gamma vs N Plot\n(Cluster Center Selection)', fontsize=14) + ax2.grid(True, alpha=0.3) + + # Apply tight_layout with error handling + try: + plt.tight_layout() + except Exception as e: + print(f"Warning: tight_layout failed ({e}), using default layout") + # Use manual spacing as fallback + plt.subplots_adjust(left=0.1, right=0.95, top=0.9, bottom=0.1, wspace=0.3) + + file = Process_File() + file.JudgeRoot_MakeDir(Save_Root) + # 使用原始檔名而不是索引編號 + original_filename = os.path.basename(image_path) + path = file.Make_Save_Root(original_filename, Save_Root) + + plt.savefig(path, dpi=300, bbox_inches='tight') + plt.close() + print(f"Decision graph and gamma vs n plot saved to: {path}") + + # Identify potential cluster centers (high rho and high delta) + mean_rho = rho.mean() + std_rho = rho.std() + threshold_rho = mean_rho + threshold_factor * std_rho + + mean_delta = delta.mean() + std_delta = delta.std() + threshold_delta = mean_delta + threshold_factor * std_delta + + is_center = (rho > threshold_rho) & (delta > threshold_delta) + + # Properly handle torch.nonzero result to avoid issues with empty tensors + center_nonzero = torch.nonzero(is_center) + if center_nonzero.numel() > 0: + center_indices = center_nonzero.squeeze().cpu().numpy() + # Ensure center_indices is always a 1D array, even for single element + if center_indices.ndim == 0: + center_indices = np.array([center_indices.item()]) + else: + center_indices = np.array([]) + + # 識別潛在的聚類中心(超像素) + center_points = superpixel_features[center_indices] if len(center_indices) > 0 else np.array([]) + print(f"🎯 發現潛在聚類中心: {len(center_indices)} 個超像素") + for idx in center_indices: + print(f" 中心超像素 {idx}: RGB({superpixel_features[idx][0]:.3f}, {superpixel_features[idx][1]:.3f}, {superpixel_features[idx][2]:.3f})") + + # ========== Fuzzy C-means 聚類分析 ========== + print(f"\n🔄 開始 Fuzzy C-means 聚類分析...") + + # 確定最佳聚類數 + gamma_np = gamma.cpu().numpy() + optimal_k, cluster_scores = determine_optimal_clusters( + superpixel_features, gamma_np, max_clusters=8, min_clusters=2 + ) + + # 執行 Fuzzy C-means 聚類 + print(f"\n🎯 使用最佳聚類數 {optimal_k} 進行 Fuzzy C-means 聚類...") + cluster_centers, membership_matrix, cluster_labels, objective_value, n_iterations = fuzzy_c_means( + superpixel_features, optimal_k, m=2.0, max_iter=100, random_state=42 + ) + + # 計算聚類統計信息 + cluster_stats = {} + for cluster_id in range(optimal_k): + cluster_mask = cluster_labels == cluster_id + cluster_size = np.sum(cluster_mask) + avg_membership = np.mean(membership_matrix[cluster_mask, cluster_id]) + cluster_stats[cluster_id] = { + 'size': cluster_size, + 'avg_membership': avg_membership, + 'center': cluster_centers[cluster_id] + } + print(f" 聚類 {cluster_id}: {cluster_size} 個超像素, 平均隸屬度: {avg_membership:.3f}") + + # 生成聚類結果可視化 + print(f"\n📊 生成聚類結果可視化...") + # 將圖像數組轉換回0-255範圍用於可視化 + img_array_255 = (img_array * 255).astype(np.uint8) + + # 創建可視化保存路徑 + original_filename = os.path.basename(image_path) + clustering_viz_path = os.path.join(Save_Root, f"clustering_results_{original_filename}") + + # 生成可視化 + visualize_clustering_results( + img_array_255, segments, cluster_labels, cluster_centers, + membership_matrix, clustering_viz_path + ) + + # Prepare return dictionary (包含所有信息:密度峰值分析 + 模糊聚類結果) + result = { + # 原有的密度峰值分析結果 + 'center_indices': center_indices, + 'center_points': center_points, + 'rho': rho.cpu().numpy(), + 'delta': delta.cpu().numpy(), + 'gamma': gamma.cpu().numpy(), + 'n': n.cpu().numpy(), + 'segments': segments, + 'superpixel_features': superpixel_features, + 'superpixel_centroids': superpixel_centroids, + 'n_superpixels': len(superpixel_features), + 'compression_ratio': len(superpixel_features) / image_size, + + # 新增的 Fuzzy C-means 聚類結果 + 'optimal_clusters': optimal_k, + 'cluster_centers': cluster_centers, + 'membership_matrix': membership_matrix, + 'cluster_labels': cluster_labels, + 'cluster_stats': cluster_stats, + 'clustering_objective': objective_value, + 'clustering_iterations': n_iterations, + 'cluster_scores': cluster_scores, + 'clustering_viz_path': clustering_viz_path + } + + return result + +# Example usage: +if __name__ == "__main__": + Label_Length = len(Loading_Config["Training_Labels"]) + Merge = merge() + Prepare = Load_Data_Prepare() + + load = Loding_Data_Root(Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["ImageGenerator_Data_Root"]) + Data_Dict_Data = load.process_main(False) + + Total_Size_List = [] + for label in Loading_Config["Training_Labels"]: + Total_Size_List.append(len(Data_Dict_Data[label])) + + # 做出跟資料相同數量的Label + Classes = [] + i = 0 + for encording in Loading_Config["Training_Labels"]: + Classes.append(make_label_list(Total_Size_List[i], encording)) + i += 1 + + # 將資料做成Dict的資料型態 + Prepare.Set_Final_Dict_Data(Loading_Config["Training_Labels"], Data_Dict_Data, Classes, Label_Length) + Final_Dict_Data = Prepare.Get_Final_Data_Dict() + keys = list(Final_Dict_Data.keys()) + + Training_Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list + for i in range(2, Label_Length): + Training_Data = Merge.merge_all_image_data(Training_Data, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + Training_Label = Merge.merge_all_image_data(Final_Dict_Data[keys[Label_Length]], Final_Dict_Data[keys[Label_Length + 1]]) #將訓練資料的label合併成一個label的list + for i in range(Label_Length + 2, 2 * Label_Length): + Training_Label = Merge.merge_all_image_data(Training_Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + for i in range(len(Training_Data)): + print(f"\n{'='*60}") + print(f"🖼️ 處理圖像 {i+1}/{len(Training_Data)}: {Training_Data[i]}") + + # 所有圖片都使用 Superpixel 分割進行 Density Peak 分析 + print("🎯 使用 SLIC 超像素分割進行 Density Peak 分析") + try: + result_superpixels = compute_decision_graph( + Training_Data[i], + f'{Save_Result_File_Config["Density_Peak_Save_Root"]}/{Training_Label[i]}_superpixels', + use_superpixels=True, + save_regions=True, + max_regions=50 + ) + + # 獲取圖片信息用於統計 + test_image = Image.open(Training_Data[i]) + image_size = test_image.size[0] * test_image.size[1] + + print(f"\n✅ 超像素處理成功:") + print(f"📊 超像素數量: {len(result_superpixels['rho'])} 個數據點") + print(f"📈 壓縮比例: {len(result_superpixels['rho']) / image_size:.6f}") + print(f"🔄 壓縮倍數: {image_size / len(result_superpixels['rho']):.1f}x") + print(f"🎯 Decision-graph 已生成並保存") + + # 顯示 Fuzzy C-means 聚類結果 + print(f"\n🎯 Fuzzy C-means 聚類結果:") + print(f"📊 最佳聚類數: {result_superpixels['optimal_clusters']}") + print(f"🔄 聚類迭代次數: {result_superpixels['clustering_iterations']}") + print(f"📈 目標函數值: {result_superpixels['clustering_objective']:.4f}") + + # 顯示各聚類的詳細信息 + print(f"📋 各聚類詳細信息:") + for cluster_id, stats in result_superpixels['cluster_stats'].items(): + print(f" 聚類 {cluster_id}: {stats['size']} 個超像素 (平均隸屬度: {stats['avg_membership']:.3f})") + center = stats['center'] + print(f" 中心特徵: RGB({center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f}), 位置({center[3]:.3f}, {center[4]:.3f})") + + print(f"📊 聚類可視化已保存至: {result_superpixels['clustering_viz_path']}") + + # 顯示聚類品質評估 + if 'cluster_scores' in result_superpixels and result_superpixels['optimal_clusters'] in result_superpixels['cluster_scores']: + scores = result_superpixels['cluster_scores'][result_superpixels['optimal_clusters']] + print(f"📈 聚類品質評估:") + print(f" 分割係數 (PC): {scores['pc']:.3f}") + print(f" 分割熵 (PE): {scores['pe']:.3f}") + print(f" 修正分割係數 (MPC): {scores['mpc']:.3f}") + print(f" 綜合分數: {scores['combined_score']:.3f}") + + except Exception as e: + print(f"❌ 超像素處理失敗: {e}") + continue + + print(f"\n💾 結果保存至: {Save_Result_File_Config['Density_Peak_Save_Root']}") diff --git a/Image_Process/Image_Generator.py b/Image_Process/Image_Generator.py index 61b57a1..282468e 100644 --- a/Image_Process/Image_Generator.py +++ b/Image_Process/Image_Generator.py @@ -1,39 +1,51 @@ -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image +from model_data_processing.processing import make_label_list from _validation.ValidationTheEnterData import validation_the_enter_data from Load_process.file_processing import Process_File -from Load_process.LoadData import Load_Data_Prepare -from torchvision import transforms +from Load_process.LoadData import Load_Data_Prepare, Load_Data_Tools from Training_Tools.PreProcess import Training_Precesses -import numpy as np -from PIL import Image -import torch -import cv2 + +from torchvision import transforms class Image_generator(): '''製作資料強化''' - def __init__(self, Generator_Root, Labels, Image_Size) -> None: + def __init__(self, Training_Root, Generator_Root, Labels, Image_Size, Class_Count) -> None: self._validation = validation_the_enter_data() self.stop = 0 self.Labels = Labels + self.Training_Root = Training_Root self.Generator_Root = Generator_Root self.Image_Size = Image_Size - self.Class_Count = 904 + self.Class_Count = Class_Count pass - def Processing_Main(self, Training_Dict_Data_Root): + def Processing_Main(self): data_size = 2712 + File = Process_File() + Prepare = Load_Data_Prepare() + Load_Tool = Load_Data_Tools() - # 製作標準資料增強 - ''' - 這裡我想要做的是依照paper上的資料強化IMAGE DATA COLLECTION AND IMPLEMENTATION OF DEEP LEARNING-BASED MODEL IN DETECTING MONKEYPOX DISEASE USING MODIFIED VGG16 - 產生出資料強化後的影像 - ''' - for i in range(1, 5, 1): - print("\nAugmentation one Generator image") - data_size = self.get_processing_Augmentation(Training_Dict_Data_Root, i, data_size) - self.stop += data_size + if not File.Judge_File_Exist(self.Generator_Root): # 檔案若不存在 + # 確定我要多少個List + Prepare.Set_Data_Content([], len(self.Labels)) - print() + # 製作讀檔字典並回傳檔案路徑 + Prepare.Set_Label_List(self.Labels) + Prepare.Set_Data_Dictionary(Prepare.Get_Label_List(), Prepare.Get_Data_Content(), len(self.Labels)) + Original_Dict_Data_Root = Prepare.Get_Data_Dict() + get_all_original_image_data = Load_Tool.get_data_root(self.Training_Root, Original_Dict_Data_Root, Prepare.Get_Label_List()) + + # 儲存資料強化後資料 + # 製作標準資料增強 + ''' + 這裡我想要做的是依照paper上的資料強化IMAGE DATA COLLECTION AND IMPLEMENTATION OF DEEP LEARNING-BASED MODEL IN DETECTING MONKEYPOX DISEASE USING MODIFIED VGG16 + 產生出資料強化後的影像 + ''' + for i in range(1, 5, 1): + print(f"\nAugmentation {i} Generator image") + data_size = self.get_processing_Augmentation(get_all_original_image_data, i, data_size) + self.stop += data_size + else: # 若檔案存在 + print("standard data and myself data are exist\n") def get_processing_Augmentation(self, original_image_root : dict, Augment_choose, data_size): Prepaer = Load_Data_Prepare() @@ -51,7 +63,6 @@ class Image_generator(): strardand = 要使用哪種Image Augmentation ''' File = Process_File() - image_processing = Read_image_and_Process_image(self.Image_Size) tool = Training_Precesses(self.Image_Size) Classes = [] Transform = self.Generator_Content(stardand) @@ -60,15 +71,15 @@ class Image_generator(): Image_Roots = self.get_data_roots[label] save_root = File.Make_Save_Root(label, save_roots) # 合併路徑 - Classes = image_processing.make_label_list(len(Image_Roots), "1") - Training_Dataset = tool.Setting_DataSet(Image_Roots, Classes) + Classes = make_label_list(len(Image_Roots), "1") + Training_Dataset = tool.Setting_DataSet(Image_Roots, Classes, "Generator") Training_DataLoader = tool.Dataloader_Sampler(Training_Dataset, 1, False) if File.JudgeRoot_MakeDir(save_root): # 判斷要存的資料夾存不存在,不存在則創立 print("The file is exist.This Script is not creating new fold.") for i in range(1, int(self.Class_Count / len(Image_Roots)) + 1, 1): - for batch_idx, (images, labels) in enumerate(Training_DataLoader): + for batch_idx, (images, labels, File_Name, File_Classes) in enumerate(Training_DataLoader): for j, img in enumerate(images): # if i == self.stop: # break @@ -78,7 +89,6 @@ class Image_generator(): # 轉換為 NumPy 陣列並從 BGR 轉為 RGB img_np = img.numpy().transpose(1, 2, 0) # 轉回 HWC 格式 - img_np = cv2.cvtColor(img_np, cv2.COLOR_BGR2RGB) # BGR 轉 RGB img_pil = transforms.ToPILImage()(img_np) File.Save_PIL_File("image_" + label + str(data_size) + ".png", save_root, img_pil) # 存檔 diff --git a/Image_Process/Image_Mask_Ground_Truth_Processing.py b/Image_Process/Image_Mask_Ground_Truth_Processing.py new file mode 100644 index 0000000..1cefe51 --- /dev/null +++ b/Image_Process/Image_Mask_Ground_Truth_Processing.py @@ -0,0 +1,262 @@ +import xml.etree.ElementTree as ET +import cv2 +import os +import numpy as np +from typing import List, Dict, Optional, Tuple +from utils.Stomach_Config import Loading_Config + +class XMLAnnotationProcessor: + """ + XML標註檔案處理器 + 專門處理包含bounding box資訊的XML檔案,並在對應圖片上繪製邊界框 + """ + + def __init__(self, dataset_root: str): + """ + 初始化XML處理器 + + Args: + dataset_root: 圖片資料集根目錄 + output_folder: 輸出資料夾 + """ + self.dataset_root = dataset_root + self.box_color = (0, 255, 0) # 綠色邊界框 + self.text_color = (0, 255, 0) # 綠色文字 + self.box_thickness = 2 + self.font_scale = 0.5 + self.font = cv2.FONT_HERSHEY_SIMPLEX + + def _ensure_output_folder(self, Save_Root: str) -> None: + """確保輸出資料夾存在""" + if not os.path.exists(Save_Root): + os.makedirs(Save_Root) + + def parse_xml(self, xml_file_path: str, Label: str) -> Optional[Dict]: + """ + 解析XML檔案並提取所有相關資訊 + + Args: + xml_file_path: XML檔案路徑 + + Returns: + Dict: 包含檔案資訊和bounding box的字典,解析失敗時返回None + """ + try: + tree = ET.parse(xml_file_path) + root = tree.getroot() + + # 提取基本資訊 + filename_element = root.find('filename') + + if filename_element is None: + print(f"找不到path元素在 {xml_file_path}") + return None + + filename = filename_element.text if filename_element is not None else "Unknown" + Original_Image_Data_Root = os.path.join(self.dataset_root, Label) + Original_Image_Data_Root = os.path.join(Original_Image_Data_Root, filename) + + # 提取圖片尺寸 + size_element = root.find('size') + width = int(size_element.find('width').text) if size_element is not None else 0 + height = int(size_element.find('height').text) if size_element is not None else 0 + depth = int(size_element.find('depth').text) if size_element is not None else 3 + + # 提取所有bounding box + bounding_boxes = [] + objects = root.findall('object') + + for obj in objects: + bndbox = obj.find('bndbox') + if bndbox is not None: + bbox_info = { + 'name': obj.find('name').text if obj.find('name') is not None else "Unknown", + 'pose': obj.find('pose').text if obj.find('pose') is not None else "Unspecified", + 'truncated': int(obj.find('truncated').text) if obj.find('truncated') is not None else 0, + 'difficult': int(obj.find('difficult').text) if obj.find('difficult') is not None else 0, + 'xmin': int(bndbox.find('xmin').text), + 'ymin': int(bndbox.find('ymin').text), + 'xmax': int(bndbox.find('xmax').text), + 'ymax': int(bndbox.find('ymax').text) + } + bounding_boxes.append(bbox_info) + + return { + 'filename': filename, + 'image_path': Original_Image_Data_Root, + 'width': width, + 'height': height, + 'depth': depth, + 'bounding_boxes': bounding_boxes + } + + except Exception as e: + print(f"解析XML檔案 {xml_file_path} 時發生錯誤: {str(e)}") + return None + + def load_image(self, image_path: str) -> Optional[np.ndarray]: + """ + 載入圖片檔案 + + Args: + image_path: 圖片檔案路徑 + + Returns: + np.ndarray: 圖片陣列,載入失敗時返回None + """ + if not os.path.exists(image_path): + print(f"圖片檔案不存在: {image_path}") + return None + + image = cv2.imread(image_path) + if image is None: + print(f"無法讀取圖片: {image_path}") + return None + + return image + + def draw_bounding_boxes(self, image: np.ndarray, bounding_boxes: List[Dict]) -> np.ndarray: + """ + 創建遮罩圖片:bounding box內保持原圖,外部為黑色 + + Args: + image: 圖片陣列 + bounding_boxes: bounding box資訊列表 + + Returns: + np.ndarray: 處理後的遮罩圖片陣列 + """ + # 創建黑色背景圖片 + height, width = image.shape[:2] + result_image = np.zeros((height, width, 3), dtype=np.uint8) + + for i, bbox in enumerate(bounding_boxes): + xmin, ymin = bbox['xmin'], bbox['ymin'] + xmax, ymax = bbox['xmax'], bbox['ymax'] + object_name = bbox['name'] + + # 確保座標在圖片範圍內 + xmin = max(0, min(xmin, width-1)) + ymin = max(0, min(ymin, height-1)) + xmax = max(0, min(xmax, width-1)) + ymax = max(0, min(ymax, height-1)) + + # 將bounding box範圍內的原圖複製到結果圖像中 + result_image[ymin:ymax, xmin:xmax] = image[ymin:ymax, xmin:xmax] + + print(f"Object {i+1}: {object_name} - 座標: ({xmin}, {ymin}, {xmax}, {ymax})") + + return result_image + + def save_annotated_image(self, image: np.ndarray, original_filename: str, Annotation_Root : str, Label : str) -> str: + """ + 儲存標註後的圖片 + + Args: + image: 標註後的圖片陣列 + original_filename: 原始檔案名稱 + + Returns: + str: 儲存的檔案路徑 + """ + output_filename = f"annotated_{original_filename}" + output_path = os.path.join(Annotation_Root, Label) + Save_Image_Roots = os.path.join(output_path, output_filename) + # 確保輸出資料夾存在 + self._ensure_output_folder(output_path) + + cv2.imwrite(Save_Image_Roots, image) + print(f"已儲存標註圖片至: {Save_Image_Roots}") + return Save_Image_Roots + + def process_single_xml(self, xml_file_path: str, Annotation_Root : str, Label : str) -> Optional[Tuple[np.ndarray, str]]: + """ + 處理單一XML檔案 + + Args: + xml_file_path: XML檔案路徑 + + Returns: + Tuple[np.ndarray, str]: (標註後的圖片, 輸出路徑),處理失敗時返回None + """ + # 解析XML + xml_data = self.parse_xml(xml_file_path, Label) + if xml_data is None: + return None + + # 載入圖片 + image = self.load_image(xml_data['image_path']) + if image is None: + return None + + # 繪製bounding box + annotated_image = self.draw_bounding_boxes(image, xml_data['bounding_boxes']) + + # 儲存結果 + output_path = self.save_annotated_image(annotated_image, xml_data['filename'], Annotation_Root, Label) + + return annotated_image, output_path + + def process_multiple_xml(self, xml_folder_path: str, Annotation_Root : str, Label : str) -> List[Tuple[str, bool]]: + """ + 批量處理多個XML檔案 + + Args: + xml_folder_path: 包含XML檔案的資料夾路徑 + + Returns: + List[Tuple[str, bool]]: [(檔案名稱, 處理成功與否), ...] + """ + if not os.path.exists(xml_folder_path): + print(f"XML資料夾不存在: {xml_folder_path}") + return [] + + xml_files = [f for f in os.listdir(xml_folder_path) if f.endswith('.xml')] + + if not xml_files: + print(f"在 {xml_folder_path} 中找不到XML檔案") + return [] + + print(f"找到 {len(xml_files)} 個XML檔案") + for xml_file in xml_files: + try: + Read_XML_File = os.path.join(xml_folder_path, xml_file) + self.process_single_xml(Read_XML_File, Annotation_Root, Label) + print(f"\n處理檔案: {xml_file}") + except Exception as e: + print(f"處理 {xml_file} 時發生錯誤: {str(e)}") + return + + def get_bounding_boxes_info(self, xml_file_path: str) -> Optional[Dict]: + """ + 僅提取XML中的bounding box資訊,不進行圖片處理 + + Args: + xml_file_path: XML檔案路徑 + + Returns: + Dict: 包含檔案資訊和bounding box座標的字典 + """ + return self.parse_xml(xml_file_path) + + def set_drawing_style(self, box_color: Tuple[int, int, int] = None, + text_color: Tuple[int, int, int] = None, + box_thickness: int = None, + font_scale: float = None) -> None: + """ + 設定繪圖樣式 + + Args: + box_color: 邊界框顏色 (B, G, R) + text_color: 文字顏色 (B, G, R) + box_thickness: 邊界框粗細 + font_scale: 字體大小 + """ + if box_color is not None: + self.box_color = box_color + if text_color is not None: + self.text_color = text_color + if box_thickness is not None: + self.box_thickness = box_thickness + if font_scale is not None: + self.font_scale = font_scale diff --git a/Image_Process/__pycache__/Image_Generator.cpython-311.pyc b/Image_Process/__pycache__/Image_Generator.cpython-311.pyc index aed1412..6b4ed4c 100644 Binary files a/Image_Process/__pycache__/Image_Generator.cpython-311.pyc and b/Image_Process/__pycache__/Image_Generator.cpython-311.pyc differ diff --git a/Image_Process/__pycache__/Image_Generator.cpython-312.pyc b/Image_Process/__pycache__/Image_Generator.cpython-312.pyc new file mode 100644 index 0000000..cb08e7b Binary files /dev/null and b/Image_Process/__pycache__/Image_Generator.cpython-312.pyc differ diff --git a/Image_Process/__pycache__/Image_Generator.cpython-313.pyc b/Image_Process/__pycache__/Image_Generator.cpython-313.pyc new file mode 100644 index 0000000..d77d829 Binary files /dev/null and b/Image_Process/__pycache__/Image_Generator.cpython-313.pyc differ diff --git a/Image_Process/__pycache__/Image_Mask_Ground_Truth_Processing.cpython-311.pyc b/Image_Process/__pycache__/Image_Mask_Ground_Truth_Processing.cpython-311.pyc new file mode 100644 index 0000000..83cb435 Binary files /dev/null and b/Image_Process/__pycache__/Image_Mask_Ground_Truth_Processing.cpython-311.pyc differ diff --git a/Image_Process/__pycache__/Image_Mask_Ground_Truth_Processing.cpython-313.pyc b/Image_Process/__pycache__/Image_Mask_Ground_Truth_Processing.cpython-313.pyc new file mode 100644 index 0000000..e8b6b15 Binary files /dev/null and b/Image_Process/__pycache__/Image_Mask_Ground_Truth_Processing.cpython-313.pyc differ diff --git a/Image_Process/__pycache__/image_enhancement.cpython-311.pyc b/Image_Process/__pycache__/image_enhancement.cpython-311.pyc new file mode 100644 index 0000000..ff1a57a Binary files /dev/null and b/Image_Process/__pycache__/image_enhancement.cpython-311.pyc differ diff --git a/Image_Process/__pycache__/image_enhancement.cpython-312.pyc b/Image_Process/__pycache__/image_enhancement.cpython-312.pyc new file mode 100644 index 0000000..e58aa36 Binary files /dev/null and b/Image_Process/__pycache__/image_enhancement.cpython-312.pyc differ diff --git a/Image_Process/__pycache__/image_enhancement.cpython-313.pyc b/Image_Process/__pycache__/image_enhancement.cpython-313.pyc new file mode 100644 index 0000000..9077bb5 Binary files /dev/null and b/Image_Process/__pycache__/image_enhancement.cpython-313.pyc differ diff --git a/Image_Process/image_enhancement.py b/Image_Process/image_enhancement.py index ce3cc4e..35a2ea6 100644 --- a/Image_Process/image_enhancement.py +++ b/Image_Process/image_enhancement.py @@ -1,87 +1,309 @@ import cv2 import numpy as np +import torch +from PIL import Image +import torchvision +import functools +import inspect -def shapen(image): # 銳化處理 - sigma = 100 - blur_img = cv2.GaussianBlur(image, (0, 0), sigma) - usm = cv2.addWeighted(image, 1.5, blur_img, -0.5, 0) - - return usm - -def increase_contrast(image): # 增加資料對比度 - output = image # 建立 output 變數 - alpha = 2 - beta = 10 - cv2.convertScaleAbs(image, output, alpha, beta) # 套用 convertScaleAbs - - return output - -def adaptive_histogram_equalization(image): - ycrcb = cv2.cvtColor(image, cv2.COLOR_BGR2YCR_CB) - channels = cv2.split(ycrcb) - clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) - clahe.apply(channels[0], channels[0]) - - ycrcb = cv2.merge(channels) - Change_image = cv2.cvtColor(ycrcb, cv2.COLOR_YCR_CB2BGR) +# 套用裝飾器到現有函數 +def unsharp_mask(image, kernel_size=(5, 5), sigma=1.0, amount=1.0, threshold=0): + """使用OpenCV實現的Unsharp Mask銳化處理 + 參數: + image: PIL.Image對象(RGB格式) + kernel_size: 高斯模糊的核大小,必須是奇數 + sigma: 高斯模糊的標準差 + amount: 銳化程度,值越大效果越強 + threshold: 邊緣檢測閾值,僅在邊緣處進行銳化 + 返回: + 銳化後的PIL.Image對象 + """ + # 轉換PIL圖像為numpy數組 + numpy_img = np.array(image, dtype=np.uint8) - return Change_image + # 對原圖進行高斯模糊 + blurred = cv2.GaussianBlur(numpy_img, kernel_size, sigma) + + # 計算銳化後的圖像 + sharpened = cv2.addWeighted(numpy_img, 1 + amount, blurred, -amount, 0) + + # 如果設置了threshold,只在邊緣處應用銳化 + if threshold > 0: + low_contrast_mask = np.absolute(numpy_img - blurred) < threshold + np.copyto(sharpened, numpy_img, where=low_contrast_mask) + + # 確保像素值在有效範圍內 + sharpened = np.clip(sharpened, 0, 255).astype(np.uint8) + + # 轉回PIL圖像 + return Image.fromarray(sharpened) -def Remove_Background(image, Matrix_Size): - skinCrCbHist = np.zeros((256,256), dtype= np.uint8) - cv2.ellipse(skinCrCbHist, (113,155),(23,25), 43, 0, 360, (255, 255, 255), -1) #繪製橢圓弧線 +def histogram_equalization(image): + """GPU加速的一般直方圖等化 + 參數: + image: PIL.Image對象(RGB格式) + 返回: + 直方圖等化後的PIL.Image對象 + """ + # 轉換為numpy數組並轉為PyTorch張量 + numpy_img = np.array(image) + tensor_img = torch.from_numpy(numpy_img).float().to('cuda') + + # 分離通道並進行直方圖等化 + result = torch.zeros_like(tensor_img) + for i in range(3): # 對RGB三個通道分別處理 + channel = tensor_img[..., i] + + # 計算直方圖 + hist = torch.histc(channel, bins=256, min=0, max=255) + + # 計算累積分布函數(CDF) + cdf = torch.cumsum(hist, dim=0) + cdf_normalized = ((cdf - cdf.min()) * 255) / (cdf.max() - cdf.min()) + + # 應用直方圖等化 + result[..., i] = cdf_normalized[channel.long()] + + # 轉回CPU和numpy數組 + result = torch.clamp(result, 0, 255).byte() + result_np = result.cpu().numpy() + return Image.fromarray(result_np) - img_ycrcb = cv2.cvtColor(image, cv2.COLOR_BGR2YCR_CB) - y,cr,cb = cv2.split(img_ycrcb) #拆分出Y,Cr,Cb值 +def Contrast_Limited_Adaptive_Histogram_Equalization(image, clip_limit=3.0, tile_size=(8, 8)): + """使用OpenCV實現的對比度限制自適應直方圖均衡化(CLAHE) + + 參數: + image: PIL.Image對象(RGB格式) + clip_limit: 剪切限制,用於限制對比度增強的程度,較大的值會產生更強的對比度 + tile_size: 圖像分塊大小的元組(height, width),較小的值會產生更局部的增強效果 + + 返回: + CLAHE處理後的PIL.Image對象 + """ + # 將PIL圖像轉換為OpenCV格式(BGR) + numpy_img = np.array(image) + bgr_img = cv2.cvtColor(numpy_img, cv2.COLOR_RGB2BGR) + + # 轉換到LAB色彩空間 + lab_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2LAB) + + # 創建CLAHE對象 + clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_size) + + # 分離LAB通道 + l, a, b = cv2.split(lab_img) + + # 對L通道應用CLAHE + l_clahe = clahe.apply(l) + + # 合併處理後的L通道與原始的a和b通道 + lab_output = cv2.merge([l_clahe, a, b]) + + # 將LAB轉回BGR,然後轉換為RGB + bgr_output = cv2.cvtColor(lab_output, cv2.COLOR_LAB2BGR) + rgb_output = cv2.cvtColor(bgr_output, cv2.COLOR_BGR2RGB) + + # 轉換為PIL圖像並返回 + return Image.fromarray(rgb_output) - skin = np.zeros(cr.shape, dtype = np.uint8) #掩膜 - (x,y) = cr.shape +def adaptive_histogram_equalization_without_limit(image, tile_size=(8, 8)): + """使用OpenCV實現的自適應直方圖均衡化(AHE) + + 參數: + image: PIL.Image對象(RGB格式) + tile_size: 圖像分塊大小的元組(height, width),較小的值會產生更局部的增強效果 + + 返回: + AHE處理後的PIL.Image對象 + """ + # 將PIL圖像轉換為OpenCV格式(BGR) + numpy_img = np.array(image) + bgr_img = cv2.cvtColor(numpy_img, cv2.COLOR_RGB2BGR) + + # 轉換到LAB色彩空間 + lab_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2LAB) + + # 分離LAB通道 + l, a, b = cv2.split(lab_img) + + # 創建AHE對象(不設置clip limit) + clahe = cv2.createCLAHE(clipLimit=None, tileGridSize=tile_size) + + # 對L通道應用AHE + l_ahe = clahe.apply(l) + + # 合併處理後的L通道與原始的a和b通道 + lab_output = cv2.merge([l_ahe, a, b]) + + # 將LAB轉回BGR,然後轉換為RGB + bgr_output = cv2.cvtColor(lab_output, cv2.COLOR_LAB2BGR) + rgb_output = cv2.cvtColor(bgr_output, cv2.COLOR_BGR2RGB) + + # 轉換為PIL圖像並返回 + return Image.fromarray(rgb_output) - # 依序取出圖片中每個像素 - for i in range(x): - for j in range(y): - if skinCrCbHist [cr[i][j], cb[i][j]] > 0: #若不在橢圓區間中 - skin[i][j] = 255 - # 如果該像素的灰階度大於 200,調整該像素的透明度 - # 使用 255 - gray[y, x] 可以將一些邊緣的像素變成半透明,避免太過鋸齒的邊緣 - # img_change = cv2.cvtColor(img_change, cv2.COLOR_BGRA2BGR) - img = cv2.bitwise_and(image, image, mask = skin) - img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) +def laplacian_sharpen(image): + """ + GPU加速的拉普拉斯銳化處理函數 + 參數: + image: PIL.Image對象(RGB格式) + 返回: + 銳化後的PIL.Image對象 + """ + # 轉換為numpy數組並轉為PyTorch張量 + numpy_img = np.array(image) + tensor_img = torch.from_numpy(numpy_img).float().to('cuda') + + # 創建拉普拉斯算子 + laplacian_kernel = torch.tensor([ + [0, 1, 0], + [1, -4, 1], + [0, 1, 0] + ], dtype=torch.float32, device='cuda').unsqueeze(0).unsqueeze(0) + + # 對每個通道進行處理 + result = torch.zeros_like(tensor_img) + for i in range(3): # RGB三個通道 + channel = tensor_img[..., i] + # 添加批次和通道維度 + channel = channel.unsqueeze(0).unsqueeze(0) + # 應用拉普拉斯算子 + laplacian = torch.nn.functional.conv2d(channel, laplacian_kernel, padding=1) + # 移除批次和通道維度 + laplacian = laplacian.squeeze() + # 銳化處理:原圖 - 拉普拉斯 + result[..., i] = channel.squeeze() - laplacian + + # 確保像素值在合理範圍內 + result = torch.clamp(result, 0, 255).byte() + + # 轉回CPU和numpy數組 + result_np = result.cpu().numpy() + return Image.fromarray(result_np) - h = image.shape[0] # 取得圖片高度 - w = image.shape[1] # 取得圖片寬度 +def adjust_hsv(image, v_adjustment=0): + """調整圖像的HSV色彩空間中的H和V通道 + + 參數: + image: PIL.Image對象(RGB格式) + v_adjustment: V通道的調整值,範圍建議在[-255, 255]之間 + + 返回: + HSV調整後的PIL.Image對象 + """ + # 將PIL圖像轉換為OpenCV格式(BGR) + numpy_img = np.array(image) + bgr_img = cv2.cvtColor(numpy_img, cv2.COLOR_RGB2BGR) + + # 轉換到HSV色彩空間 + hsv_img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2HSV) + + # 調整V通道 + hsv_img[..., 2] = np.clip(hsv_img[..., 2] + v_adjustment, 0, 255) + + # 將HSV轉回BGR,然後轉換為RGB + bgr_output = cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR) + rgb_output = cv2.cvtColor(bgr_output, cv2.COLOR_BGR2RGB) + + # 轉換為PIL圖像並返回 + return Image.fromarray(rgb_output) - for x in range(w): - for y in range(h): - if img_gray[y, x] == 0: - # if x == 0 and y == 0: # 當X Y都在左上角時 - # image[y, x] = Add(1, Matrix_Size, image[y, x]) / Matrix_Size - # if x == w - 1 and y == 0: # 當X Y都在右上角時 - # image[y, x] = Add(w - Matrix_Size, w, image[y, x]) / Matrix_Size - # if x == 0 and y == h - 1: # 當X Y都在左下角時 - # image[y, x] = (image[y - 1, x] + image[y - 1, x + 1] + image[y, x + 1]) / 3 - # if x == w - 1 and y == h - 1: # 當X Y都在右下角時 - # image[y, x] = (image[y, x - 1] + image[y - 1, x - 1] + image[y - 1, x]) / 3 +def gamma_correction(image, gamma=1.0): + """對圖像進行伽馬校正 + + 參數: + image: PIL.Image對象(RGB格式) + gamma: 伽馬值,gamma > 1 時圖像變暗,gamma < 1 時圖像變亮,gamma = 1 時保持不變 + + 返回: + 伽馬校正後的PIL.Image對象 + """ + # 將PIL圖像轉換為numpy數組 + numpy_img = np.array(image) + + # 將像素值歸一化到[0, 1]範圍 + normalized = numpy_img.astype(float) / 255.0 + + # 應用伽馬校正 + corrected = np.power(normalized, gamma) + + # 將值縮放回[0, 255]範圍 + output = np.clip(corrected * 255.0, 0, 255).astype(np.uint8) + + # 轉換回PIL圖像並返回 + return Image.fromarray(output) - # if (x > 0 and x < w - 1) and y == 0: # 當上面的X Y從左到右 - # image[y, x] = (image[y, x - 1] + image[y + 1, x - 1] + image[y + 1, x] + image[y, x + 1] + image[y + 1, x + 1]) / 5 - # if (x > 0 and x < w - 1) and y == h - 1: # 當下面的X Y從左到右 - # image[y, x] = (image[y, x - 1] + image[y - 1, x - 1] + image[y - 1, x] + image[y, x + 1] + image[y - 1, x + 1]) / 5 - # if x == 0 and (y > 0 and y < h - 1): # 當左邊的X Y從上到下 - # image[y, x] = (image[y - 1, x] + image[y - 1, x + 1] + image[y, x + 1] + image[y + 1, x + 1] + image[y + 1, x]) / 5 - # if x == w - 1 and (y > 0 and y < h - 1): # 當右邊X Y從上到下 - # image[y, x] = (image[y - 1, x] + image[y - 1, x - 1] + image[y, x - 1] + image[y + 1, x - 1] + image[y + 1, x]) / 5 +def Hight_Light(image, Threshold): + image = np.array(image) - if (x >= 1 and x < w - 1) and (y >= 1 and y < h - 1): # 當y >= 2 且 X >= 2 - image[y, x] = Add(x, y, image, Matrix_Size) / Matrix_Size - # BGRA_image[y, x, 3] = 255 - gray[y, x] - return image + gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) + # 使用閾值檢測高光點(白色液滴) + _, thresh = cv2.threshold(gray, Threshold, 255, cv2.THRESH_BINARY) + # 使用形態學操作(膨脹)來擴大遮罩區域 + kernel = np.ones((5, 5), np.uint8) + dilated = cv2.dilate(thresh, kernel, iterations=1) + # 使用 inpaint 修復高光點 + image_inpaint = cv2.inpaint(image, dilated, 3, cv2.INPAINT_TELEA) + return Image.fromarray(image_inpaint) -def Add(width_Center, Height_Center, image, Mask_Size): - total = 0 - for i in range(Mask_Size): - for j in range(Mask_Size): - total += image[width_Center - ((Mask_Size - 1) / 2) + j, Height_Center - ((Mask_Size - 1) / 2) + i] +def median_filter(image: Image.Image, kernel_size: int = 3): + """ + 中值濾波(Median Filter)實現 + + 參數: + image: PIL.Image對象(RGB格式) + kernel_size: 濾波核大小,必須是奇數 + + 返回: + 濾波後的PIL.Image對象 + """ + # 確保kernel_size是奇數 + if kernel_size % 2 == 0: + kernel_size += 1 + + # 轉換PIL圖像為numpy數組 + numpy_img = np.array(image, dtype=np.uint8) + + # 對每個通道應用中值濾波 + result = np.zeros_like(numpy_img) + for i in range(3): # 對RGB三個通道分別處理 + result[:, :, i] = cv2.medianBlur(numpy_img[:, :, i], kernel_size) + + # 確保像素值在有效範圍內 + result = np.clip(result, 0, 255).astype(np.uint8) + + # 轉回PIL圖像 + return Image.fromarray(result) - return total \ No newline at end of file +def mean_filter(image: Image.Image, kernel_size: int = 3): + """ + 均質濾波(Mean Filter)實現 + + 參數: + image: PIL.Image對象(RGB格式) + kernel_size: 濾波核大小,必須是奇數 + + 返回: + 濾波後的PIL.Image對象 + """ + # 確保kernel_size是奇數 + if kernel_size % 2 == 0: + kernel_size += 1 + + # 轉換PIL圖像為numpy數組 + numpy_img = np.array(image, dtype=np.uint8) + + # 創建均質濾波核(所有元素都是1/(kernel_size*kernel_size)) + kernel = np.ones((kernel_size, kernel_size), np.float32) / (kernel_size * kernel_size) + + # 對每個通道應用均質濾波 + result = np.zeros_like(numpy_img) + for i in range(3): # 對RGB三個通道分別處理 + result[:, :, i] = cv2.filter2D(numpy_img[:, :, i], -1, kernel) + + # 確保像素值在有效範圍內 + result = np.clip(result, 0, 255).astype(np.uint8) + + # 轉回PIL圖像 + return Image.fromarray(result) \ No newline at end of file diff --git a/Image_Process/load_and_ImageGenerator.py b/Image_Process/load_and_ImageGenerator.py deleted file mode 100644 index f879a4e..0000000 --- a/Image_Process/load_and_ImageGenerator.py +++ /dev/null @@ -1,58 +0,0 @@ -from Load_process.LoadData import Loding_Data_Root -from Image_Process.Image_Generator import Image_generator -from Load_process.file_processing import Process_File -from model_data_processing.processing_for_cut_image import Cut_Indepentend_Data -from Load_process.Loading_Tools import Load_Data_Prepare, Load_Data_Tools - -class Load_ImageGenerator(): - ''' - 這是一個拿來進行資料強化的物件,最主要結合了學姊給的資料強化與我自行設定的資料強化。 -藉由此物件先將資料讀取出來,並將資料分別進行資料強化,利用資料強化來迷部資料的不平衡 -這只是其中一個實驗 - -Parmeter - standard_root: 做跟學姊給的資料強化同一種的資料強化 - myself_root: 資料強化的內容參數是我自己設定的 - IndependentDataRoot: 要存回去的資料夾路徑 - Herpeslabels: 皰疹的類別 - MonKeyPoxlabels: 猴痘的類別(猴痘、水痘、正常) - herpes_data: 合併herpes Dataset的資料成一個List - MonkeyPox_data: 合併MonkeyPox DataSet 的資料成一個List - ''' - def __init__(self, Training_Root,Test_Root, Generator_Root, Labels, Image_Size) -> None: - self.Training_Root = Training_Root - self.TestRoot = Test_Root - self.GeneratoRoot = Generator_Root - self.Labels = Labels - self.Image_Size = Image_Size - pass - - def process_main(self, Data_Length : int): - File = Process_File() - Prepare = Load_Data_Prepare() - load = Loding_Data_Root(self.Labels, self.Training_Root, self.GeneratoRoot) - Indepentend = Cut_Indepentend_Data(self.Training_Root, self.Labels) - Load_Tool = Load_Data_Tools() - Generator = Image_generator(self.GeneratoRoot, self.Labels, self.Image_Size) - - # 將測試資料獨立出來 - test_size = 0.2 - Indepentend.IndependentData_main(self.TestRoot, test_size) - - if not File.Judge_File_Exist(self.GeneratoRoot): # 檔案若不存在 - # 確定我要多少個List - Prepare.Set_Data_Content([], Data_Length) - - # 製作讀檔字典並回傳檔案路徑 - Prepare.Set_Label_List(self.Labels) - Prepare.Set_Data_Dictionary(Prepare.Get_Label_List(), Prepare.Get_Data_Content(), Data_Length) - Original_Dict_Data_Root = Prepare.Get_Data_Dict() - get_all_original_image_data = Load_Tool.get_data_root(self.Training_Root, Original_Dict_Data_Root, Prepare.Get_Label_List()) - - # 儲存資料強化後資料 - Generator.Processing_Main(get_all_original_image_data) # 執行資料強化 - else: # 若檔案存在 - print("standard data and myself data are exist\n") - - # 執行讀檔 - return load.process_main() \ No newline at end of file diff --git a/Load_process/LoadData.py b/Load_process/LoadData.py index 9d57b25..3acfd46 100644 --- a/Load_process/LoadData.py +++ b/Load_process/LoadData.py @@ -11,15 +11,16 @@ class Loding_Data_Root(Process_File): super().__init__() pass - def process_main(self): + def process_main(self, status): '''處理讀Training、Image Generator檔資料''' Merge = merge() get_Image_Data = self.get_Image_data_roots(self.Train_Root) - Get_ImageGenerator_Image_Data = self.get_Image_data_roots(self.Generator_Root) - # Get_Total_Image_Data_Root = Merge.merge_dict_to_dict(get_Image_Data, Get_ImageGenerator_Image_Data) - # Get_Total_Image_Data_Root = Merge.merge_data_main(get_Image_Data, 0, len(self.Label_List)) + if status: + Get_ImageGenerator_Image_Data = self.get_Image_data_roots(self.Generator_Root) + Get_Total_Image_Data_Root = Merge.merge_dict_to_dict(get_Image_Data, Get_ImageGenerator_Image_Data) + return Get_Total_Image_Data_Root return get_Image_Data diff --git a/Load_process/Load_Indepentend.py b/Load_process/Load_Indepentend.py index 5cdc158..9971c35 100644 --- a/Load_process/Load_Indepentend.py +++ b/Load_process/Load_Indepentend.py @@ -1,11 +1,10 @@ -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image from merge_class.merge import merge -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image from Load_process.LoadData import Load_Data_Prepare, Load_Data_Tools -from model_data_processing.processing import Balance_Process +from model_data_processing.processing import make_label_list +from utils.Stomach_Config import Loading_Config class Load_Indepentend_Data(): - def __init__(self, Labels, OneHot_Encording): + def __init__(self, OneHot_Encording): ''' 影像切割物件 label有2類,會將其轉成one-hot-encoding的形式 @@ -13,33 +12,33 @@ class Load_Indepentend_Data(): [1, 0] = NPC_positive ''' self.merge = merge() - self.Labels = Labels self.OneHot_Encording = OneHot_Encording pass - def process_main(self, Test_data_root): - self.test, self.test_label = self.get_Independent_image(Test_data_root) + def process_main(self, Test_data_root, Test_mask_root): + self.test, self.test_label, self.test_mask = self.get_Independent_image(Test_data_root, Test_mask_root) print("\ntest_labels有" + str(len(self.test_label)) + "筆資料\n") - # self.validation, self.validation_label = self.get_Independent_image(Validation_data_root) - # print("validation_labels有 " + str(len(self.validation_label)) + " 筆資料\n") - - def get_Independent_image(self, independent_DataRoot): - image_processing = Read_image_and_Process_image(123) - - classify_image = [] + def get_Independent_image(self, independent_DataRoot, independent_MaskRoot): Total_Size_List = [] - Total_Dict_Data_Root = self.Get_Independent_data_Root(independent_DataRoot) # 讀取測試資料集的資料 + Total_Dict_Data_Root = self.Get_Independent_data_Root(independent_DataRoot, Loading_Config["Training_Labels"], len(Loading_Config["Training_Labels"])) # 讀取測試資料集的資料 + Total_Dict_Mask_Root = self.Get_Independent_data_Root(independent_MaskRoot, Loading_Config["XML_Loading_Label"], len(Loading_Config["XML_Loading_Label"])) # 讀取測試資料集的mask資料 + # 將測試資料字典轉成列表,並且將其排序 Total_List_Data_Root = [] - for Label in self.Labels: + for Label in Loading_Config["Training_Labels"]: Total_List_Data_Root.append(Total_Dict_Data_Root[Label]) + + # 將測試資料字典轉成列表,並且將其排序 + Total_List_Mask_Data_Root = [] + for Label in Loading_Config["XML_Loading_Label"]: + Total_List_Mask_Data_Root.append(Total_Dict_Mask_Root[Label]) - test_label, Classify_Label = [], [] + classify_image, Classify_Label = [], [] i = 0 # 計算classify_image的counter,且計算總共有幾筆資料 for test_title in Total_List_Data_Root: # 藉由讀取所有路徑來進行讀檔 - test_label = image_processing.make_label_list(len(test_title), self.OneHot_Encording[i]) # 製作對應圖片數量的label出來+ - print(self.Labels[i] + " 有 " + str(len(test_label)) + " 筆資料 ") + test_label = make_label_list(len(test_title), self.OneHot_Encording[i]) # 製作對應圖片數量的label出來+ + print(Loading_Config["Training_Labels"][i] + " 有 " + str(len(test_label)) + " 筆資料 ") Total_Size_List.append(len(test_label)) @@ -47,28 +46,28 @@ class Load_Indepentend_Data(): Classify_Label.append(test_label) i += 1 - test = self.merge.merge_data_main(classify_image, 0, len(self.Labels)) - test_label = self.merge.merge_data_main(Classify_Label, 0, len(self.Labels)) + classify_Mask_image = [] + i = 0 # 計算classify_image的counter,且計算總共有幾筆資料 + for test_title in Total_List_Mask_Data_Root: # 藉由讀取所有路徑來進行讀檔 + print(Loading_Config["XML_Loading_Label"][i] + " 有 " + str(len(test_title)) + " 筆資料 ") - # test = [] - # test = image_processing.Data_Augmentation_Image(original_test_root) - # test, test_label = image_processing.image_data_processing(test, original_test_label) + classify_Mask_image.append(test_title) + i += 1 - # Balance_Data = list(zip(test, test_label)) - # test, test_label = Balance_Process(Balance_Data, Total_Size_List) # 打亂並取出指定資料筆數的資料 - # test = image_processing.normalization(test) - - - return test, test_label + test = self.merge.merge_data_main(classify_image, 0, len(Loading_Config["Training_Labels"])) + test_label = self.merge.merge_data_main(Classify_Label, 0, len(Loading_Config["Training_Labels"])) + test_Mask = self.merge.merge_data_main(classify_Mask_image, 0, len(Loading_Config["XML_Loading_Label"])) + + return test, test_label, test_Mask - def Get_Independent_data_Root(self, load_data_root): + def Get_Independent_data_Root(self, load_data_root, Dictory_Keys, Length): Prepare = Load_Data_Prepare() Load_Tool = Load_Data_Tools() - Prepare.Set_Data_Content([], len(self.Labels)) - Prepare.Set_Data_Dictionary(self.Labels, Prepare.Get_Data_Content(), 2) + Prepare.Set_Data_Content([], Length) + Prepare.Set_Data_Dictionary(Dictory_Keys, Prepare.Get_Data_Content(), Length) Get_Data_Dict_Content = Prepare.Get_Data_Dict() - Total_Data_Roots = Load_Tool.get_data_root(load_data_root, Get_Data_Dict_Content, self.Labels) + Total_Data_Roots = Load_Tool.get_data_root(load_data_root, Get_Data_Dict_Content, Dictory_Keys) - return Total_Data_Roots \ No newline at end of file + return Total_Data_Roots diff --git a/Load_process/Loading_Data_Roots_And_Training_Preprocessing.py b/Load_process/Loading_Data_Roots_And_Training_Preprocessing.py new file mode 100644 index 0000000..bf40244 --- /dev/null +++ b/Load_process/Loading_Data_Roots_And_Training_Preprocessing.py @@ -0,0 +1,87 @@ +from Load_process.LoadData import Loding_Data_Root +from utils.Stomach_Config import Loading_Config +from merge_class.merge import merge +from Load_process.LoadData import Load_Data_Prepare +from Training_Tools.Tools import Tool + +class Training_Preprocessing(): + def __init__(self): + pass + + def Process_Main(self, Loading_Data_Roots, Labels, Have_Label_Lists = True, Generator_Roots = None): + """ + 讀取資料成 Dict 資料型態,並計算各資料筆數與總共資料筆數 + 回傳Loading_Data_Dict_Data + """ + Loading_Data_Dict_Data, Total_Size_Lists = self.Loading_Data_Dicts(Loading_Data_Roots, Labels, Generator_Roots) + + # 做出跟資料相同數量的Label + # 取得One-hot encording 的資料 + tool = Tool() + Label_Length = len(Labels) + + if Have_Label_Lists: + tool.Set_OneHotEncording(Loading_Config["Training_Labels"]) + Encording_Label = tool.Get_OneHot_Encording_Label() + + Classes = self.make_label_list(Encording_Label, Total_Size_Lists) + return self.Merge_Total_Data(Loading_Data_Dict_Data, Classes, Label_Length) + + return self.Merge_Total_Data(Loading_Data_Dict_Data, Classes, Label_Length) + + def Loading_Data_Dicts(self, Loading_Data_Roots, Labels, Generator_Roots): + """ + 讀取資料成 Dict 資料型態,並計算各資料筆數與總共資料筆數 + + 回傳Loading_Data_Dict_Data + """ + Load_Data = Loding_Data_Root(Loading_Data_Roots, Labels, Generator_Roots) + Loading_Data_Dict_Data = Load_Data.process_main(False) + Total_Size_Lists = [] + + print("讀取的資料集總數") + for label in Labels: + Train_Size += len(Loading_Data_Dict_Data[label]) + Total_Size_Lists.append(len(Loading_Data_Dict_Data[label])) + print(f"Labels: {label}, 總數為: {len(Loading_Data_Dict_Data[label])}") + + print("總共有 " + str(Train_Size) + " 筆資料") + + return Loading_Data_Dict_Data, Total_Size_Lists + + def make_label_list(Contents, Lengths): + '''製作label的列表''' + Classes = [] + for Content in Contents: + label_list = [] + for i in range(Lengths): + label_list.append(Content) + Classes.append(label_list) + + return label_list + + def Merge_Total_Data(self, Data_Dict, Classes, Label_Length): + """ + 將所有的資料合併成一個list + """ + Prepare = Load_Data_Prepare() + Merge = merge() + + # 將資料做成Dict的資料型態 + if Classes == None: + Classes = [[], [], []] + Prepare.Set_Final_Dict_Data(Loading_Config["Training_Labels"], Data_Dict, Classes, Label_Length) + Final_Dict_Data = Prepare.Get_Final_Data_Dict() + keys = Loading_Config["Training_Labels"] + + Training_Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list + for i in range(2, Label_Length): + Training_Data = Merge.merge_all_image_data(Training_Data, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + if Classes is not [[], [], []]: + Training_Label = Merge.merge_all_image_data(Final_Dict_Data[keys[Label_Length]], Final_Dict_Data[keys[Label_Length + 1]]) #將訓練資料的label合併成一個label的list + for i in range(Label_Length + 2, 2 * Label_Length): + Training_Label = Merge.merge_all_image_data(Training_Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + return Training_Data, Training_Label + + return Training_Data diff --git a/Load_process/__pycache__/LoadData.cpython-311.pyc b/Load_process/__pycache__/LoadData.cpython-311.pyc index 5f87c98..bf1abe3 100644 Binary files a/Load_process/__pycache__/LoadData.cpython-311.pyc and b/Load_process/__pycache__/LoadData.cpython-311.pyc differ diff --git a/Load_process/__pycache__/LoadData.cpython-312.pyc b/Load_process/__pycache__/LoadData.cpython-312.pyc new file mode 100644 index 0000000..5759b87 Binary files /dev/null and b/Load_process/__pycache__/LoadData.cpython-312.pyc differ diff --git a/Load_process/__pycache__/LoadData.cpython-313.pyc b/Load_process/__pycache__/LoadData.cpython-313.pyc new file mode 100644 index 0000000..080bb51 Binary files /dev/null and b/Load_process/__pycache__/LoadData.cpython-313.pyc differ diff --git a/Load_process/__pycache__/Load_Indepentend.cpython-311.pyc b/Load_process/__pycache__/Load_Indepentend.cpython-311.pyc index 2d7ab71..fd30a97 100644 Binary files a/Load_process/__pycache__/Load_Indepentend.cpython-311.pyc and b/Load_process/__pycache__/Load_Indepentend.cpython-311.pyc differ diff --git a/Load_process/__pycache__/Load_Indepentend.cpython-312.pyc b/Load_process/__pycache__/Load_Indepentend.cpython-312.pyc new file mode 100644 index 0000000..be032eb Binary files /dev/null and b/Load_process/__pycache__/Load_Indepentend.cpython-312.pyc differ diff --git a/Load_process/__pycache__/Load_Indepentend.cpython-313.pyc b/Load_process/__pycache__/Load_Indepentend.cpython-313.pyc new file mode 100644 index 0000000..f22bc86 Binary files /dev/null and b/Load_process/__pycache__/Load_Indepentend.cpython-313.pyc differ diff --git a/Load_process/__pycache__/Loading_Tools.cpython-311.pyc b/Load_process/__pycache__/Loading_Tools.cpython-311.pyc index 2cbc0d0..926a3c3 100644 Binary files a/Load_process/__pycache__/Loading_Tools.cpython-311.pyc and b/Load_process/__pycache__/Loading_Tools.cpython-311.pyc differ diff --git a/Load_process/__pycache__/Loading_Tools.cpython-312.pyc b/Load_process/__pycache__/Loading_Tools.cpython-312.pyc new file mode 100644 index 0000000..720496a Binary files /dev/null and b/Load_process/__pycache__/Loading_Tools.cpython-312.pyc differ diff --git a/Load_process/__pycache__/Loading_Tools.cpython-313.pyc b/Load_process/__pycache__/Loading_Tools.cpython-313.pyc new file mode 100644 index 0000000..69aff96 Binary files /dev/null and b/Load_process/__pycache__/Loading_Tools.cpython-313.pyc differ diff --git a/Load_process/__pycache__/file_processing.cpython-311.pyc b/Load_process/__pycache__/file_processing.cpython-311.pyc index 923bee5..bbf945e 100644 Binary files a/Load_process/__pycache__/file_processing.cpython-311.pyc and b/Load_process/__pycache__/file_processing.cpython-311.pyc differ diff --git a/Load_process/__pycache__/file_processing.cpython-312.pyc b/Load_process/__pycache__/file_processing.cpython-312.pyc new file mode 100644 index 0000000..5339628 Binary files /dev/null and b/Load_process/__pycache__/file_processing.cpython-312.pyc differ diff --git a/Load_process/__pycache__/file_processing.cpython-313.pyc b/Load_process/__pycache__/file_processing.cpython-313.pyc new file mode 100644 index 0000000..9717742 Binary files /dev/null and b/Load_process/__pycache__/file_processing.cpython-313.pyc differ diff --git a/Load_process/file_processing.py b/Load_process/file_processing.py index c5c4de7..d09a826 100644 --- a/Load_process/file_processing.py +++ b/Load_process/file_processing.py @@ -35,16 +35,14 @@ class Process_File(): save_root = self.Make_Save_Root(FileName, save_root) np.save(save_root, image) - def Save_CSV_File(self, file_name, data): # 儲存訓練結果 - Save_Root = '../Result/Training_Result/save_the_train_result(' + str(datetime.date.today()) + ")" + def Save_CSV_File(self, Save_Root, file_name, data): # 儲存訓練結果 self.JudgeRoot_MakeDir(Save_Root) modelfiles = self.Make_Save_Root(file_name + ".csv", Save_Root) # 將檔案名稱及路徑字串合併成完整路徑 data.to_csv(modelfiles, mode = "a") - def Save_TXT_File(self, content, File_Name): - model_dir = '../Result/save_the_train_result(' + str(datetime.date.today()) + ")" # 儲存的檔案路徑,由save_the_train_result + 當天日期 - self.JudgeRoot_MakeDir(model_dir) - modelfiles = self.Make_Save_Root(File_Name + ".txt", model_dir) # 將檔案名稱及路徑字串合併成完整路徑 + def Save_TXT_File(self, content, Save_Root, File_Name): + self.JudgeRoot_MakeDir(Save_Root) + modelfiles = self.Make_Save_Root(f"{File_Name}.txt", Save_Root) # 將檔案名稱及路徑字串合併成完整路徑 with open(modelfiles, mode = 'a') as file: file.write(content) diff --git a/Model_Loss/CIOU_Loss.py b/Model_Loss/CIOU_Loss.py new file mode 100644 index 0000000..76db8a6 --- /dev/null +++ b/Model_Loss/CIOU_Loss.py @@ -0,0 +1,315 @@ +import torch +import torch.nn as nn +import math + +class CIOULoss(nn.Module): + """ + Complete Intersection over Union (CIOU) Loss + 適用於目標檢測中的邊界框回歸任務 + + CIOU Loss 考慮了三個幾何因子: + 1. 重疊面積 (Overlap area) + 2. 中心點距離 (Central point distance) + 3. 長寬比一致性 (Aspect ratio consistency) + """ + + def __init__(self, eps=1e-7): + super(CIOULoss, self).__init__() + self.eps = eps + + def forward(self, pred_boxes, target_boxes): + """ + 計算 CIOU Loss + + Args: + pred_boxes: 預測邊界框 [N, 4] (x1, y1, x2, y2) 或 [N, 4] (cx, cy, w, h) 或分割掩碼 [B, 1, H, W] + target_boxes: 真實邊界框 [N, 4] (x1, y1, x2, y2) 或 [N, 4] (cx, cy, w, h) 或分割掩碼 [B, 1, H, W] + + Returns: + CIOU loss value + """ + # 檢查輸入是否為分割掩碼格式 + if len(pred_boxes.shape) == 4 and pred_boxes.shape[1] == 1: + # 將分割掩碼轉換為邊界框格式 + pred_boxes = self._mask_to_boxes(pred_boxes) + target_boxes = self._mask_to_boxes(target_boxes) + + # 如果無法從掩碼中提取有效的邊界框,則返回一個小的損失值 + if pred_boxes is None or target_boxes is None: + return torch.tensor(0.01, device=pred_boxes.device if pred_boxes is not None else target_boxes.device) + + # 確保輸入為浮點數 + pred_boxes = pred_boxes.float() + target_boxes = target_boxes.float() + + # 檢查邊界框維度是否正確 + if pred_boxes.dim() == 1: + # 如果是單個邊界框,擴展為批次大小為1的張量 + pred_boxes = pred_boxes.unsqueeze(0) + if target_boxes.dim() == 1: + target_boxes = target_boxes.unsqueeze(0) + + # 確保邊界框有4個坐標 + if pred_boxes.shape[1] != 4 or target_boxes.shape[1] != 4: + # 如果坐標數量不正確,返回一個小的損失值 + return torch.tensor(0.01, device=pred_boxes.device) + + # 如果輸入是 (cx, cy, w, h) 格式,轉換為 (x1, y1, x2, y2) + if self._is_center_format(pred_boxes, target_boxes): + pred_boxes = self._center_to_corner(pred_boxes) + target_boxes = self._center_to_corner(target_boxes) + + # 計算交集區域 + intersection = self._calculate_intersection(pred_boxes, target_boxes) + + # 計算各自的面積 + pred_area = (pred_boxes[:, 2] - pred_boxes[:, 0]) * (pred_boxes[:, 3] - pred_boxes[:, 1]) + target_area = (target_boxes[:, 2] - target_boxes[:, 0]) * (target_boxes[:, 3] - target_boxes[:, 1]) + + # 計算聯集面積 + union = pred_area + target_area - intersection + self.eps + + # 計算 IoU + iou = intersection / union + + # 計算最小外接矩形 + enclose_x1 = torch.min(pred_boxes[:, 0], target_boxes[:, 0]) + enclose_y1 = torch.min(pred_boxes[:, 1], target_boxes[:, 1]) + enclose_x2 = torch.max(pred_boxes[:, 2], target_boxes[:, 2]) + enclose_y2 = torch.max(pred_boxes[:, 3], target_boxes[:, 3]) + + # 計算最小外接矩形的對角線距離平方 + enclose_diagonal_sq = (enclose_x2 - enclose_x1) ** 2 + (enclose_y2 - enclose_y1) ** 2 + self.eps + + # 計算兩個邊界框中心點之間的距離平方 + pred_center_x = (pred_boxes[:, 0] + pred_boxes[:, 2]) / 2 + pred_center_y = (pred_boxes[:, 1] + pred_boxes[:, 3]) / 2 + target_center_x = (target_boxes[:, 0] + target_boxes[:, 2]) / 2 + target_center_y = (target_boxes[:, 1] + target_boxes[:, 3]) / 2 + + center_distance_sq = (pred_center_x - target_center_x) ** 2 + (pred_center_y - target_center_y) ** 2 + + # 計算長寬比一致性項 + pred_w = pred_boxes[:, 2] - pred_boxes[:, 0] + pred_h = pred_boxes[:, 3] - pred_boxes[:, 1] + target_w = target_boxes[:, 2] - target_boxes[:, 0] + target_h = target_boxes[:, 3] - target_boxes[:, 1] + + # 避免除零 + pred_w = torch.clamp(pred_w, min=self.eps) + pred_h = torch.clamp(pred_h, min=self.eps) + target_w = torch.clamp(target_w, min=self.eps) + target_h = torch.clamp(target_h, min=self.eps) + + v = (4 / (math.pi ** 2)) * torch.pow(torch.atan(target_w / target_h) - torch.atan(pred_w / pred_h), 2) + + # 計算 alpha 參數 + with torch.no_grad(): + alpha = v / (1 - iou + v + self.eps) + + # 計算 CIOU + ciou = iou - (center_distance_sq / enclose_diagonal_sq) - alpha * v + + # 返回 CIOU Loss (1 - CIOU) + ciou_loss = 1 - ciou + + return ciou_loss.mean() + + def _is_center_format(self, pred_boxes, target_boxes): + """ + 判斷輸入格式是否為中心點格式 (cx, cy, w, h) + 簡單的啟發式判斷:如果第三、四列的值都是正數且相對較小,可能是寬高 + """ + # 這裡使用簡單的判斷邏輯,實際使用時可能需要更精確的判斷 + return False # 預設假設輸入為 (x1, y1, x2, y2) 格式 + + def _center_to_corner(self, boxes): + """ + 將中心點格式 (cx, cy, w, h) 轉換為角點格式 (x1, y1, x2, y2) + """ + cx, cy, w, h = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] + x1 = cx - w / 2 + y1 = cy - h / 2 + x2 = cx + w / 2 + y2 = cy + h / 2 + return torch.stack([x1, y1, x2, y2], dim=1) + + def _mask_to_boxes(self, masks): + """ + 將分割掩碼轉換為邊界框格式 [N, 4] (x1, y1, x2, y2) + + Args: + masks: 分割掩碼 [B, 1, H, W] + + Returns: + boxes: 邊界框 [B, 4] (x1, y1, x2, y2) + """ + batch_size = masks.size(0) + device = masks.device + + # 將掩碼轉換為二值掩碼 + binary_masks = (torch.sigmoid(masks) > 0.5).float() + + # 初始化邊界框張量 + boxes = torch.zeros(batch_size, 4, device=device) + + # 對每個批次處理 + for b in range(batch_size): + mask = binary_masks[b, 0] # [H, W] + + # 找出非零元素的索引 + non_zero_indices = torch.nonzero(mask, as_tuple=True) + + # 如果掩碼中沒有非零元素,則使用默認的小邊界框 + if len(non_zero_indices[0]) == 0: + # 返回一個默認的小邊界框 + boxes[b] = torch.tensor([0, 0, 1, 1], device=device) + continue + + # 計算邊界框坐標 + y_min = torch.min(non_zero_indices[0]) + y_max = torch.max(non_zero_indices[0]) + x_min = torch.min(non_zero_indices[1]) + x_max = torch.max(non_zero_indices[1]) + + # 存儲邊界框 [x1, y1, x2, y2] + boxes[b] = torch.tensor([x_min, y_min, x_max, y_max], device=device) + + return boxes + + def _calculate_intersection(self, pred_boxes, target_boxes): + """ + 計算兩個邊界框的交集面積 + """ + x1 = torch.max(pred_boxes[:, 0], target_boxes[:, 0]) + y1 = torch.max(pred_boxes[:, 1], target_boxes[:, 1]) + x2 = torch.min(pred_boxes[:, 2], target_boxes[:, 2]) + y2 = torch.min(pred_boxes[:, 3], target_boxes[:, 3]) + + # 計算交集的寬度和高度 + intersection_w = torch.clamp(x2 - x1, min=0) + intersection_h = torch.clamp(y2 - y1, min=0) + + return intersection_w * intersection_h + + +class DIoULoss(nn.Module): + """ + Distance Intersection over Union (DIoU) Loss + CIOU 的簡化版本,只考慮重疊面積和中心點距離 + """ + + def __init__(self, eps=1e-7): + super(DIoULoss, self).__init__() + self.eps = eps + + def forward(self, pred_boxes, target_boxes): + # 確保輸入為浮點數 + pred_boxes = pred_boxes.float() + target_boxes = target_boxes.float() + + # 計算交集區域 + intersection = self._calculate_intersection(pred_boxes, target_boxes) + + # 計算各自的面積 + pred_area = (pred_boxes[:, 2] - pred_boxes[:, 0]) * (pred_boxes[:, 3] - pred_boxes[:, 1]) + target_area = (target_boxes[:, 2] - target_boxes[:, 0]) * (target_boxes[:, 3] - target_boxes[:, 1]) + + # 計算聯集面積 + union = pred_area + target_area - intersection + self.eps + + # 計算 IoU + iou = intersection / union + + # 計算最小外接矩形的對角線距離平方 + enclose_x1 = torch.min(pred_boxes[:, 0], target_boxes[:, 0]) + enclose_y1 = torch.min(pred_boxes[:, 1], target_boxes[:, 1]) + enclose_x2 = torch.max(pred_boxes[:, 2], target_boxes[:, 2]) + enclose_y2 = torch.max(pred_boxes[:, 3], target_boxes[:, 3]) + + enclose_diagonal_sq = (enclose_x2 - enclose_x1) ** 2 + (enclose_y2 - enclose_y1) ** 2 + self.eps + + # 計算中心點距離平方 + pred_center_x = (pred_boxes[:, 0] + pred_boxes[:, 2]) / 2 + pred_center_y = (pred_boxes[:, 1] + pred_boxes[:, 3]) / 2 + target_center_x = (target_boxes[:, 0] + target_boxes[:, 2]) / 2 + target_center_y = (target_boxes[:, 1] + target_boxes[:, 3]) / 2 + + center_distance_sq = (pred_center_x - target_center_x) ** 2 + (pred_center_y - target_center_y) ** 2 + + # 計算 DIoU + diou = iou - (center_distance_sq / enclose_diagonal_sq) + + # 返回 DIoU Loss + diou_loss = 1 - diou + + return diou_loss.mean() + + def _calculate_intersection(self, pred_boxes, target_boxes): + """計算交集面積""" + x1 = torch.max(pred_boxes[:, 0], target_boxes[:, 0]) + y1 = torch.max(pred_boxes[:, 1], target_boxes[:, 1]) + x2 = torch.min(pred_boxes[:, 2], target_boxes[:, 2]) + y2 = torch.min(pred_boxes[:, 3], target_boxes[:, 3]) + + intersection_w = torch.clamp(x2 - x1, min=0) + intersection_h = torch.clamp(y2 - y1, min=0) + + return intersection_w * intersection_h + + +class GIoULoss(nn.Module): + """ + Generalized Intersection over Union (GIoU) Loss + IoU 的泛化版本,考慮了最小外接矩形 + """ + + def __init__(self, eps=1e-7): + super(GIoULoss, self).__init__() + self.eps = eps + + def forward(self, pred_boxes, target_boxes): + # 確保輸入為浮點數 + pred_boxes = pred_boxes.float() + target_boxes = target_boxes.float() + + # 計算交集 + intersection = self._calculate_intersection(pred_boxes, target_boxes) + + # 計算各自面積 + pred_area = (pred_boxes[:, 2] - pred_boxes[:, 0]) * (pred_boxes[:, 3] - pred_boxes[:, 1]) + target_area = (target_boxes[:, 2] - target_boxes[:, 0]) * (target_boxes[:, 3] - target_boxes[:, 1]) + + # 計算聯集 + union = pred_area + target_area - intersection + self.eps + + # 計算 IoU + iou = intersection / union + + # 計算最小外接矩形面積 + enclose_x1 = torch.min(pred_boxes[:, 0], target_boxes[:, 0]) + enclose_y1 = torch.min(pred_boxes[:, 1], target_boxes[:, 1]) + enclose_x2 = torch.max(pred_boxes[:, 2], target_boxes[:, 2]) + enclose_y2 = torch.max(pred_boxes[:, 3], target_boxes[:, 3]) + + enclose_area = (enclose_x2 - enclose_x1) * (enclose_y2 - enclose_y1) + self.eps + + # 計算 GIoU + giou = iou - (enclose_area - union) / enclose_area + + # 返回 GIoU Loss + giou_loss = 1 - giou + + return giou_loss.mean() + + def _calculate_intersection(self, pred_boxes, target_boxes): + """計算交集面積""" + x1 = torch.max(pred_boxes[:, 0], target_boxes[:, 0]) + y1 = torch.max(pred_boxes[:, 1], target_boxes[:, 1]) + x2 = torch.min(pred_boxes[:, 2], target_boxes[:, 2]) + y2 = torch.min(pred_boxes[:, 3], target_boxes[:, 3]) + + intersection_w = torch.clamp(x2 - x1, min=0) + intersection_h = torch.clamp(y2 - y1, min=0) + + return intersection_w * intersection_h \ No newline at end of file diff --git a/Model_Loss/Loss.py b/Model_Loss/Loss.py index a9c340b..0102208 100644 --- a/Model_Loss/Loss.py +++ b/Model_Loss/Loss.py @@ -1,6 +1,7 @@ from torch import nn from torch.nn import functional import torch +import numpy as np class Entropy_Loss(nn.Module): @@ -8,10 +9,10 @@ class Entropy_Loss(nn.Module): super(Entropy_Loss, self).__init__() def forward(self, outputs, labels): - # 範例: 使用均方誤差作為損失計算 - # outputs = torch.argmax(outputs, 1) + # 转换为张量并确保在同一設備上 outputs_New = torch.as_tensor(outputs, dtype=torch.float32) - labels_New = torch.as_tensor(labels, dtype=torch.float32) - + labels_New = torch.as_tensor(labels, dtype=torch.float32) # 標籤應該是 long 類型用於索引 + loss = functional.cross_entropy(outputs_New, labels_New) - return torch.as_tensor(loss, dtype = torch.float32) \ No newline at end of file + + return torch.as_tensor(loss, dtype=torch.float32) diff --git a/Model_Loss/Perceptual_Loss.py b/Model_Loss/Perceptual_Loss.py new file mode 100644 index 0000000..d2ac6dd --- /dev/null +++ b/Model_Loss/Perceptual_Loss.py @@ -0,0 +1,116 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +from torchvision import models, transforms + +class VGGPerceptualLoss(nn.Module): + """ + 基於VGG19的感知損失函數 + 使用預訓練的VGG19網絡提取特徵,計算特徵空間中的損失 + """ + def __init__(self, feature_layers=[2, 7, 12, 21, 30], use_normalization=True): + super(VGGPerceptualLoss, self).__init__() + + # 載入預訓練的VGG19模型 + vgg = models.vgg19(pretrained=True).features + + # 凍結VGG參數 + for param in vgg.parameters(): + param.requires_grad = False + + # 將模型移到與輸入相同的設備上(在forward中處理) + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + # 選擇要使用的特徵層 + self.feature_layers = feature_layers + self.vgg_layers = nn.ModuleList() + + # 分割VGG網絡到指定層 + layer_idx = 0 + current_layer = 0 + + for i, layer in enumerate(vgg): + if layer_idx < len(feature_layers) and i <= feature_layers[layer_idx]: + self.vgg_layers.append(layer) + if i == feature_layers[layer_idx]: + layer_idx += 1 + else: + break + + # 是否使用ImageNet標準化 + self.use_normalization = use_normalization + if use_normalization: + self.normalize = transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ) + + # 損失權重 + self.weights = [1.0, 1.0, 1.0, 1.0, 1.0] # 可以調整不同層的權重 + + def extract_features(self, x): + """ + 提取VGG特徵 + """ + # 確保輸入在[0,1]範圍內 + if x.min() < 0 or x.max() > 1: + x = torch.clamp(x, 0, 1) + + # 標準化 + if self.use_normalization: + # 確保normalize在與輸入相同的設備上 + if hasattr(self, 'normalize') and not isinstance(self.normalize, torch.nn.Module): + self.normalize = transforms.Normalize( + mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225] + ).to(x.device) + x = self.normalize(x) + + features = [] + layer_idx = 0 + + # 確保所有VGG層都在與輸入相同的設備上 + device = x.device + for i, layer in enumerate(self.vgg_layers): + layer = layer.to(device) # 確保層在正確的設備上 + x = layer(x) + + # 檢查是否到達目標特徵層 + if layer_idx < len(self.feature_layers) and i == self.feature_layers[layer_idx]: + features.append(x) + layer_idx += 1 + + return features + + def forward(self, pred, target): + """ + 計算感知損失 + pred: 預測圖像 [B, C, H, W] + target: 目標圖像 [B, C, H, W] + """ + # 確保模型在與輸入相同的設備上 + device = pred.device + self.vgg_layers = nn.ModuleList([layer.to(device) for layer in self.vgg_layers]) + + # 確保輸入尺寸匹配 + if pred.shape != target.shape: + pred = F.interpolate(pred, size=target.shape[2:], mode='bilinear', align_corners=False) + + # 如果是單通道,轉換為三通道 + if pred.shape[1] == 1: + pred = pred.repeat(1, 3, 1, 1) + if target.shape[1] == 1: + target = target.repeat(1, 3, 1, 1) + + # 提取特徵 + pred_features = self.extract_features(pred) + target_features = self.extract_features(target) + + # 計算特徵損失 + perceptual_loss = 0 + for i, (pred_feat, target_feat) in enumerate(zip(pred_features, target_features)): + # 使用MSE計算特徵差異 + feat_loss = F.mse_loss(pred_feat, target_feat) + perceptual_loss += self.weights[i] * feat_loss + + return perceptual_loss diff --git a/Model_Loss/Segmentation_Loss.py b/Model_Loss/Segmentation_Loss.py new file mode 100644 index 0000000..230c33a --- /dev/null +++ b/Model_Loss/Segmentation_Loss.py @@ -0,0 +1,22 @@ +from multiprocessing import Value +import pstats +import torch +import torch.nn as nn +import torch.nn.functional as F +import torchvision.models as models +from torchvision import transforms +from Model_Loss.CIOU_Loss import CIOULoss +from Model_Loss.Perceptual_Loss import VGGPerceptualLoss + +class Segmentation_Loss(nn.Module): + def __init__(self) -> None: + super(Segmentation_Loss, self).__init__() + self.Perceptual_Loss = VGGPerceptualLoss() + self.CIOU = CIOULoss() + pass + + def forward(self, Output_Result, GroundTruth_Result): + Perceptual_Loss = self.Perceptual_Loss(Output_Result, GroundTruth_Result) + CIOU_Loss = self.CIOU(Output_Result, GroundTruth_Result) + + return Perceptual_Loss + CIOU_Loss diff --git a/Model_Loss/__pycache__/CIOU_Loss.cpython-311.pyc b/Model_Loss/__pycache__/CIOU_Loss.cpython-311.pyc new file mode 100644 index 0000000..26311f9 Binary files /dev/null and b/Model_Loss/__pycache__/CIOU_Loss.cpython-311.pyc differ diff --git a/Model_Loss/__pycache__/CIOU_Loss.cpython-313.pyc b/Model_Loss/__pycache__/CIOU_Loss.cpython-313.pyc new file mode 100644 index 0000000..0c5f412 Binary files /dev/null and b/Model_Loss/__pycache__/CIOU_Loss.cpython-313.pyc differ diff --git a/Model_Loss/__pycache__/Loss.cpython-311.pyc b/Model_Loss/__pycache__/Loss.cpython-311.pyc index 1e8d4c4..729a916 100644 Binary files a/Model_Loss/__pycache__/Loss.cpython-311.pyc and b/Model_Loss/__pycache__/Loss.cpython-311.pyc differ diff --git a/Model_Loss/__pycache__/Loss.cpython-312.pyc b/Model_Loss/__pycache__/Loss.cpython-312.pyc new file mode 100644 index 0000000..2d6a51a Binary files /dev/null and b/Model_Loss/__pycache__/Loss.cpython-312.pyc differ diff --git a/Model_Loss/__pycache__/Loss.cpython-313.pyc b/Model_Loss/__pycache__/Loss.cpython-313.pyc new file mode 100644 index 0000000..f6302ca Binary files /dev/null and b/Model_Loss/__pycache__/Loss.cpython-313.pyc differ diff --git a/Model_Loss/__pycache__/Perceptual_Loss.cpython-311.pyc b/Model_Loss/__pycache__/Perceptual_Loss.cpython-311.pyc new file mode 100644 index 0000000..ee8bb76 Binary files /dev/null and b/Model_Loss/__pycache__/Perceptual_Loss.cpython-311.pyc differ diff --git a/Model_Loss/__pycache__/Perceptual_Loss.cpython-313.pyc b/Model_Loss/__pycache__/Perceptual_Loss.cpython-313.pyc new file mode 100644 index 0000000..04a46d2 Binary files /dev/null and b/Model_Loss/__pycache__/Perceptual_Loss.cpython-313.pyc differ diff --git a/Model_Loss/__pycache__/Segmentation_Loss.cpython-311.pyc b/Model_Loss/__pycache__/Segmentation_Loss.cpython-311.pyc new file mode 100644 index 0000000..1911c9c Binary files /dev/null and b/Model_Loss/__pycache__/Segmentation_Loss.cpython-311.pyc differ diff --git a/Model_Loss/__pycache__/Segmentation_Loss.cpython-313.pyc b/Model_Loss/__pycache__/Segmentation_Loss.cpython-313.pyc new file mode 100644 index 0000000..d584a68 Binary files /dev/null and b/Model_Loss/__pycache__/Segmentation_Loss.cpython-313.pyc differ diff --git a/Model_Loss/__pycache__/binary_cross_entropy.cpython-311.pyc b/Model_Loss/__pycache__/binary_cross_entropy.cpython-311.pyc new file mode 100644 index 0000000..458dde3 Binary files /dev/null and b/Model_Loss/__pycache__/binary_cross_entropy.cpython-311.pyc differ diff --git a/Model_Loss/__pycache__/binary_cross_entropy.cpython-313.pyc b/Model_Loss/__pycache__/binary_cross_entropy.cpython-313.pyc new file mode 100644 index 0000000..a5d29c8 Binary files /dev/null and b/Model_Loss/__pycache__/binary_cross_entropy.cpython-313.pyc differ diff --git a/Model_Loss/binary_cross_entropy.py b/Model_Loss/binary_cross_entropy.py new file mode 100644 index 0000000..632c3e3 --- /dev/null +++ b/Model_Loss/binary_cross_entropy.py @@ -0,0 +1,146 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + + +class BinaryCrossEntropy(nn.Module): + """ + 基本的二元交叉熵損失函數 + """ + def __init__(self, reduction='mean'): + """ + 初始化 + + Args: + reduction (str): 'mean', 'sum' 或 'none',指定如何減少損失 + """ + super(BinaryCrossEntropy, self).__init__() + + def forward(self, predictions, targets): + """ + 計算二元交叉熵損失 + + Args: + predictions (torch.Tensor): 模型的預測輸出,形狀為 [batch_size, ...] + targets (torch.Tensor): 目標標籤,形狀與 predictions 相同 + + Returns: + torch.Tensor: 計算得到的損失值 + """ + # 確保輸入是張量 + predictions = torch.as_tensor(predictions, dtype=torch.float32) + targets = torch.as_tensor(targets, dtype=torch.float32) + Loss = nn.BCELoss() + + return Loss(predictions, targets) + + # # 檢查輸出和標籤的維度是否匹配 + # if predictions.shape[1] != targets.shape[1]: + # # 如果維度不匹配,使用交叉熵損失函數 + # # 對於交叉熵損失,標籤需要是類別索引而不是one-hot編碼 + # # 將one-hot編碼轉換為類別索引 + # _, targets_indices = torch.max(targets, dim=1) + # return F.cross_entropy(predictions, targets_indices, reduction=self.reduction) + # else: + # # 如果維度匹配,使用二元交叉熵損失函數 + # # 使用 PyTorch 內建的 binary_cross_entropy_with_logits 函數 + # # 它會自動應用 sigmoid 函數,避免輸入值超出 [0,1] 範圍 + # return F.binary_cross_entropy_with_logits(predictions, targets, reduction=self.reduction) + + +class WeightedBinaryCrossEntropy(nn.Module): + """ + 帶權重的二元交叉熵損失函數 + """ + def __init__(self, pos_weight=1.0, neg_weight=1.0, reduction='mean'): + """ + 初始化 + + Args: + pos_weight (float): 正樣本的權重 + neg_weight (float): 負樣本的權重 + reduction (str): 'mean', 'sum' 或 'none',指定如何減少損失 + """ + super(WeightedBinaryCrossEntropy, self).__init__() + self.pos_weight = pos_weight + self.neg_weight = neg_weight + self.reduction = reduction + + def forward(self, predictions, targets): + """ + 計算帶權重的二元交叉熵損失 + + Args: + predictions (torch.Tensor): 模型的預測輸出,形狀為 [batch_size, ...] + targets (torch.Tensor): 目標標籤,形狀與 predictions 相同 + + Returns: + torch.Tensor: 計算得到的損失值 + """ + # 確保輸入是張量 + predictions = torch.as_tensor(predictions, dtype=torch.float32) + targets = torch.as_tensor(targets, dtype=torch.float32) + + # 使用 sigmoid 確保預測值在 [0,1] 範圍內 + predictions = torch.sigmoid(predictions) + + # 計算帶權重的二元交叉熵損失 + loss = -self.pos_weight * targets * torch.log(predictions + 1e-7) - \ + self.neg_weight * (1 - targets) * torch.log(1 - predictions + 1e-7) + + # 根據 reduction 方式返回損失 + if self.reduction == 'mean': + return loss.mean() + elif self.reduction == 'sum': + return loss.sum() + else: # 'none' + return loss + + +class LabelSmoothingBCE(nn.Module): + """ + 帶標籤平滑的二元交叉熵損失函數 + """ + def __init__(self, smoothing=0.1, reduction='mean'): + """ + 初始化 + + Args: + smoothing (float): 標籤平滑係數,範圍 [0, 1] + reduction (str): 'mean', 'sum' 或 'none',指定如何減少損失 + """ + super(LabelSmoothingBCE, self).__init__() + self.smoothing = smoothing + self.reduction = reduction + + def forward(self, predictions, targets): + """ + 計算帶標籤平滑的二元交叉熵損失 + + Args: + predictions (torch.Tensor): 模型的預測輸出,形狀為 [batch_size, ...] + targets (torch.Tensor): 目標標籤,形狀與 predictions 相同 + + Returns: + torch.Tensor: 計算得到的損失值 + """ + # 確保輸入是張量 + predictions = torch.as_tensor(predictions, dtype=torch.float32) + targets = torch.as_tensor(targets, dtype=torch.float32) + + # 應用標籤平滑 + targets = targets * (1 - self.smoothing) + 0.5 * self.smoothing + + # 使用 sigmoid 確保預測值在 [0,1] 範圍內 + predictions = torch.sigmoid(predictions) + + # 計算二元交叉熵損失 + loss = -targets * torch.log(predictions + 1e-7) - (1 - targets) * torch.log(1 - predictions + 1e-7) + + # 根據 reduction 方式返回損失 + if self.reduction == 'mean': + return loss.mean() + elif self.reduction == 'sum': + return loss.sum() + else: # 'none' + return loss \ No newline at end of file diff --git a/README.md b/README.md index 24be781..e8fd911 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,116 @@ -main.py: 主程式檔 +# 胃試鏡疾病判斷系統 + +## 項目概述 + +本項目是一個基於深度學習的胃試鏡疾病自動診斷系統,主要用於檢測和分類胃部疾病,特別是胃癌(CA)。系統採用了分割和分類的兩階段方法,首先對胃試鏡圖像進行分割以識別可疑區域,然後對這些區域進行分類以確定是否存在疾病。 + +* 資料集類別: 3分類(胃癌、非胃癌但有病、正常資料) +* 基礎模型: Xception + +## 主要功能 + +- **圖像預處理**:包括直方圖均衡化、自適應直方圖均衡化、銳化、HSV調整、伽馬校正等多種圖像增強方法 +- **數據增強**:通過圖像生成器擴充訓練數據集 +- **疾病分割**:使用GastroSegNet模型對胃試鏡圖像中的可疑區域進行分割 +- **疾病分類**:使用修改版Xception模型對分割後的區域進行分類,分為正常(Normal)、胃癌(CA)和待確認(Have_Question)三類 +- **結果可視化**:提供混淆矩陣、訓練曲線等可視化工具 + +## 系統架構 + +系統採用了兩階段的處理流程: + +1. **分割階段**:使用GastroSegNet模型對輸入圖像進行分割,識別可疑區域 +2. **分類階段**: + - 第一組分類器:區分正常(Normal)和其他(Others) + - 第二組分類器:區分胃癌(CA)和待確認(Have_Question) + +## 環境要求 + +- Python 3.8+ +- PyTorch 1.8+ +- CUDA(推薦用於加速訓練) +- 其他依賴庫:torchvision, numpy, opencv-python, scikit-image, pandas等 + +## 使用方法 + +### 數據準備 + +1. 將訓練數據放置在`../Dataset/Training`目錄下 +2. 將測試數據放置在`../Dataset/Testing`目錄下 +3. 標註數據(XML格式)放置在`../Label_Image`目錄下 + +### 訓練模型 + +```bash +uv run main.py +``` + +訓練過程將自動執行以下步驟: +1. 數據預處理和增強 +2. 訓練分割模型(GastroSegNet) +3. 使用分割模型處理圖像 +4. 訓練分類模型(修改版Xception) + +### 結果查看 + +訓練結果將保存在`../Result`目錄下,包括: +- 訓練曲線圖:`../Result/Training_Image` +- 混淆矩陣:`../Result/Matrix_Image` +- 訓練結果數據:`../Result/Training_Result` +- 最佳模型:`../Result/save_the_best_model` + +## 項目結構 + +- `main.py`:程序入口點 +- `experiments/`:實驗相關代碼 + - `experiment.py`:實驗主流程 + - `Training/`:訓練相關代碼 + - `Models/`:模型定義 +- `Image_Process/`:圖像處理相關代碼 +- `Model_Loss/`:損失函數定義 +- `Training_Tools/`:訓練工具 +- `utils/`:工具函數和配置 + +## 配置說明 + +系統配置在`utils/Stomach_Config.py`中定義,主要包括: + +- `Image_Enhance`:圖像增強方法 +- `Loading_Config`:數據加載配置 +- `Training_Config`:訓練參數配置 +- `Model_Config`:模型參數配置 +- `Save_Result_File_Config`:結果保存路徑配置 + +## 模型說明 + +### 分割模型(GastroSegNet) + +用於識別胃試鏡圖像中的可疑區域,輸出分割掩碼。 + +### 分類模型(修改版Xception) + +基於Xception架構,針對胃試鏡圖像分類任務進行了修改,主要用於區分正常、胃癌和待確認三類。 +* 主執行檔: main.py ## load_process ### 負責讀取影像檔案、分割獨立資料(測試、驗證)、讀取獨立資料、一般檔案的操作 -File_Process : 檔案操作的主程式,包含開檔、創立檔案、判斷檔案是否存在等都是他負責的範圍。是一般物件也是LoadData的父物件 -LoadData : 讀檔主程式,一切讀檔動作由他開始。繼承File_Process(子物件) -Cutting_Indepentend_Image : 讀取獨立資料(testing、Validation)的物件 +* File_Process : 檔案操作的主程式,包含開檔、創立檔案、判斷檔案是否存在等都是他負責的範圍。是一般物件也是LoadData的父物件 +* LoadData : 讀檔主程式,一切讀檔動作由他開始。繼承File_Process(子物件) +* Cutting_Indepentend_Image : 讀取獨立資料(testing、Validation)的物件 + +## Calculate_Process +### 計算模型的評估指標的內容 +* Calculate: 計算模型的評估指標的平均跟標準差,並將結果儲存到檔案中 ## Image_Process ### 負責進行資料擴增、影像處理等的操作 -* Generator_Content : 負責建立基礎Generator項目,為Image_Generator的父類別 -* Image_Generator : 負責製造資料擴增的資料,並將資料存到檔案中。繼承Generator_Content(子物件) -* image_enhancement : 負責進行影像處理並將資料回傳 +* Image_Generator : 負責製造資料擴增的資料,並將資料存到檔案中。 +* image_enhancement : 負責進行影像處理將資料強化。 +* + +## all_models_tools +### 模型的調控細節,如early stop、降低學習率和儲存最佳模型 +* all_model_tools: call back的方法 ## Model_Tools ### 負責進行模型的基礎架構,包含Convolution、Dense、以及其他模型的配件 @@ -42,11 +142,13 @@ Cutting_Indepentend_Image : 讀取獨立資料(testing、Validation)的物件 ### 負責驗證程式碼內的資料型態或輸入錯誤等問題 * Validation : 驗證程式碼錯誤 -## Draw +## draw_tools ### 負責畫圖的工具 -* Draw_Tools : 畫出混淆矩陣、走勢圖的工具 +* draw : 畫出混淆矩陣、走勢圖的工具 * Grad_CAM : 畫出模型可視化的熱力圖的工具 ## Experiment ### 執行實驗的主程式 -* Experiment : 負責執行讀檔、設定模型Compile的細節、執行訓練、驗證結果等功能 +* Experiment : 負責執行讀檔、設定模型與實驗的細節、執行訓練、驗證結果等功能 +* Model_All_Step : 執行模型的訓練流程設定與細節參數設定 +* pytorch_Model: 設定模型的架構 diff --git a/Read_and_process_image/ReadAndProcess.py b/Read_and_process_image/ReadAndProcess.py deleted file mode 100644 index 02ce968..0000000 --- a/Read_and_process_image/ReadAndProcess.py +++ /dev/null @@ -1,74 +0,0 @@ -import cv2 -import numpy as np -import torch - -class Read_image_and_Process_image: - def __init__(self, Image_Size) -> None: - self.Image_Size = Image_Size - pass - def get_data(self, path): - '''讀檔''' - try: - img_arr = cv2.imread(path, cv2.IMREAD_COLOR) # 讀檔(彩色) - # img_arr = cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 讀檔(灰階) - resized_arr = cv2.resize(img_arr, (self.Image_Size, self.Image_Size)) # 濤整圖片大小 - except Exception as e: - print(e) - - return resized_arr - - def Data_Augmentation_Image(self, path): - resized_arr = [] - - for p in path: - try: - img_arr = cv2.imread(p, cv2.IMREAD_COLOR) # 讀檔(彩色) - # img_arr = cv2.imread(path, cv2.IMREAD_GRAYSCALE) # 讀檔(灰階) - resized_arr.append(cv2.resize(img_arr, (self.Image_Size, self.Image_Size))) # 調整圖片大小 - except Exception as e: - print(e) - - return np.array(resized_arr) - - def image_data_processing(self, data, label): - '''讀檔後處理圖片''' - data = np.asarray(data).astype(np.float32) # 將圖list轉成np.array - data = data.reshape(-1, self.Image_Size, self.Image_Size, 3) # 更改陣列形狀 - label = np.array(label) # 將label從list型態轉成 numpy array - return data, label - - def normalization(self, images): - imgs = [] - for img in images: - img = np.asarray(img).astype(np.float32) # 將圖list轉成np.array - img = img / 255 # 標準化影像資料 - imgs.append(img) - - return torch.as_tensor(imgs) - - # def load_numpy_data(self, file_names): - # '''載入numpy圖檔,並執行影像處理提高特徵擷取''' - # i = 0 - # numpy_image = [] - # original_image = [] - # for file_name in file_names: - # compare = str(file_name).split(".") - # if compare[-1] == "npy": - # image = np.load(file_name) # 讀圖片檔 - # numpy_image.append(image) # 合併成一個陣列 - # else: - # original_image.append(file_name) - - # original_image = self.get_data(original_image) - - # for file in original_image: - # numpy_image.append(file) - - # return numpy_image - - def make_label_list(self, length, content): - '''製作label的列表''' - label_list = [] - for i in range(length): - label_list.append(content) - return label_list \ No newline at end of file diff --git a/Read_and_process_image/__pycache__/ReadAndProcess.cpython-310.pyc b/Read_and_process_image/__pycache__/ReadAndProcess.cpython-310.pyc deleted file mode 100644 index 2969226..0000000 Binary files a/Read_and_process_image/__pycache__/ReadAndProcess.cpython-310.pyc and /dev/null differ diff --git a/Read_and_process_image/__pycache__/ReadAndProcess.cpython-311.pyc b/Read_and_process_image/__pycache__/ReadAndProcess.cpython-311.pyc deleted file mode 100644 index 9c2f924..0000000 Binary files a/Read_and_process_image/__pycache__/ReadAndProcess.cpython-311.pyc and /dev/null differ diff --git a/Read_and_process_image/__pycache__/ReadAndProcess.cpython-39.pyc b/Read_and_process_image/__pycache__/ReadAndProcess.cpython-39.pyc deleted file mode 100644 index 083de5a..0000000 Binary files a/Read_and_process_image/__pycache__/ReadAndProcess.cpython-39.pyc and /dev/null differ diff --git a/Read_and_process_image/__pycache__/__init__.cpython-310.pyc b/Read_and_process_image/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 47c799c..0000000 Binary files a/Read_and_process_image/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/Read_and_process_image/__pycache__/__init__.cpython-311.pyc b/Read_and_process_image/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index ad3439c..0000000 Binary files a/Read_and_process_image/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/Read_and_process_image/__pycache__/__init__.cpython-39.pyc b/Read_and_process_image/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 660ec1c..0000000 Binary files a/Read_and_process_image/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/Training_Tools/PreProcess.py b/Training_Tools/PreProcess.py index a01d880..a60d2f4 100644 --- a/Training_Tools/PreProcess.py +++ b/Training_Tools/PreProcess.py @@ -7,32 +7,69 @@ import numpy as np import cv2 class ListDataset(Dataset): - def __init__(self, data_list, labels_list, transform): + def __init__(self, data_list, labels_list, Mask_List, transform): self.data = data_list self.labels = labels_list + self.Mask_Truth_List = Mask_List self.transform = transform + self.roots = [] def __len__(self): return len(self.data) def __getitem__(self, idx): Image_Root = self.data[idx] + + # Mask_Ground_Truth = None + # if self.Mask_Truth_List is not None: + # mask_path = self.Mask_Truth_List[idx] + # if mask_path is not None: # 確保掩碼路徑不為None + # try: + # Mask_Ground_Truth = Image.open(mask_path).convert("RGB") + # # 先不轉換為 tensor,等待 transform 處理完後再轉換 + # except Exception as e: + # print(e) + + Split_Roots = Image_Root.split("/") + Split_Roots = Split_Roots[-1].split("\\") + File_Name = Split_Roots[-1] + classes = Split_Roots[-2] + try: - with open(Image_Root, 'rb') as file: - Images = Image.open(file).convert("RGB") - # Image = cv2.imread(Image_Root, cv2.IMREAD_COLOR) # 讀檔(彩色) - # Image = cv2.cvtColor(Image, cv2.COLOR_BGR2RGB) + Images = Image.open(Image_Root).convert("RGB") except Exception as e: - print(e) + assert e is not None, f"Error loading image {Image_Root}: {e}" - if self.transform is not "Generator": + if self.transform != "Generator": Images = self.transform(Images) + # if self.Mask_Truth_List is not None and Mask_Ground_Truth is not None and not isinstance(Mask_Ground_Truth, torch.Tensor): + # Mask_Ground_Truth = self.transform(Mask_Ground_Truth) + # # 確保 Images 是 tensor + # if not isinstance(Images, torch.Tensor): + # Images = torch.tensor(np.array(Images)) + + # # 確保 Mask_Ground_Truth 是 tensor + # if self.Mask_Truth_List is not None and Mask_Ground_Truth is not None and not isinstance(Mask_Ground_Truth, torch.Tensor): + # Mask_Ground_Truth = torch.tensor(np.array(Mask_Ground_Truth)) + Images = torch.tensor(np.array(Images)) label = self.labels[idx] + # if self.Mask_Truth_List is not None: + # # 如果掩碼為None,創建一個與圖像相同大小的空掩碼 + # if Mask_Ground_Truth is None: + # if isinstance(Images, torch.Tensor): + # # 創建與圖像相同大小的空掩碼張量 + # Mask_Ground_Truth = torch.zeros_like(Images) + # else: + # # 如果圖像不是張量,創建一個空的PIL圖像 + # Mask_Ground_Truth = Image.new('RGB', Images.size, (0, 0, 0)) + # if self.transform != "Generator": + # Mask_Ground_Truth = self.transform(Mask_Ground_Truth) + # return Images, Mask_Ground_Truth, label, File_Name, classes # print(f"Dataset_Data: \n{sample}\n") - return Images, label + return Images, label, File_Name, classes class Training_Precesses: def __init__(self, ImageSize): @@ -43,27 +80,19 @@ class Training_Precesses: def Dataloader_Sampler(self, SubDataSet, Batch_Size, Sampler=True): if Sampler: - # Data_Loader = DataLoader( - # dataset=SubDataSet, - # batch_size=Batch_Size, - # num_workers=0, - # pin_memory=True, - # sampler=self.Setting_RandomSampler_Content(SubDataSet) - # ) Data_Loader = DataLoader( dataset=SubDataSet, batch_size=Batch_Size, num_workers=0, pin_memory=True, - sampler=self.Setting_RandomSampler_Content(SubDataSet) + sampler=self.Setting_WeightedRandomSampler_Content(SubDataSet) ) else: Data_Loader = DataLoader( dataset=SubDataSet, batch_size=Batch_Size, num_workers=0, - pin_memory=True, - shuffle=True + pin_memory=True ) return Data_Loader @@ -84,6 +113,16 @@ class Training_Precesses: if labels.ndim > 1: # If one-hot encoded labels = np.argmax(labels, axis=1) + # 確保標籤是整數類型 + try: + # 嘗試將標籤轉換為整數 + labels = labels.astype(np.int64) + except ValueError: + # 如果標籤是字符串,先將其映射到整數 + unique_labels = np.unique(labels) + label_to_idx = {label: idx for idx, label in enumerate(unique_labels)} + labels = np.array([label_to_idx[label] for label in labels]) + # Count occurrences of each class class_counts = np.bincount(labels) class_weights = 1.0 / class_counts # Inverse frequency as weight @@ -98,20 +137,28 @@ class Training_Precesses: def Setting_RandomSampler_Content(self, Dataset): return RandomSampler(Dataset, generator = self.generator) - def Setting_DataSet(self, Datas, Labels, transform = None): + def Setting_DataSet(self, Datas, Labels, Mask_List, transform = None): # 資料預處理 if transform == None: transform = transforms.Compose([ - transforms.Resize((256, 256)) + transforms.Resize((self.ImageSize, self.ImageSize)) ]) elif transform == "Transform": - transform = transforms.Compose([ - transforms.Resize((256, 256)), + transform = transforms.Compose([ + transforms.Resize((self.ImageSize, self.ImageSize)), transforms.ToTensor() ]) elif transform == "Generator": transform = "Generator" # Create Dataset - list_dataset = ListDataset(Datas, Labels , transform) - return list_dataset \ No newline at end of file + list_dataset = ListDataset(Datas, Labels, Mask_List, transform) + return list_dataset + + def Setting_SubsetRandomSampler_Content(self, SubDataSet): + # Calculate subset indices (example: using a fraction of the dataset) + dataset_size = len(SubDataSet) + subset_size = int(0.8 * dataset_size) # Use 80% of the dataset as an example + subset_indices = torch.randperm(dataset_size, generator=self.generator)[:subset_size] + + return SubsetRandomSampler(subset_indices, generator=self.generator) \ No newline at end of file diff --git a/Training_Tools/Tools.py b/Training_Tools/Tools.py index b39e469..7f30087 100644 --- a/Training_Tools/Tools.py +++ b/Training_Tools/Tools.py @@ -5,39 +5,9 @@ import torch class Tool: def __init__(self) -> None: - self.__ICG_Training_Root = "" - self.__Normal_Training_Root = "" - self.__Comprehensive_Training_Root = "" - - self.__ICG_Test_Data_Root = "" - self.__Normal_Test_Data_Root = "" - self.__Comprehensive_Testing_Root = "" - - self.__ICG_ImageGenerator_Data_Root = "" - self.__Normal_ImageGenerator_Data_Root = "" - self.__Comprehensive_Generator_Root = "" - - self.__Labels = [] self.__OneHot_Encording = [] pass - def Set_Labels(self): - self.__Labels = ["stomach_cancer_Crop", "Normal_Crop", "Have_Question_Crop"] - # self.__Labels = ["NPC_negative", "NPC_positive"] - - def Set_Save_Roots(self): - self.__ICG_Training_Root = "../Dataset/Training" - self.__Normal_Training_Root = "../Dataset/Training/CA" - self.__Comprehensive_Training_Root = "../Dataset/Training/Mixed" - - self.__ICG_Test_Data_Root = "../Dataset/Testing" - self.__Normal_Test_Data_Root = "../Dataset/Training/Normal_TestData" - self.__Comprehensive_Testing_Root = "../Dataset/Training/Comprehensive_TestData" - - self.__ICG_ImageGenerator_Data_Root = "../Dataset/ImageGenerator" - self.__Normal_ImageGenerator_Data_Root = "../Dataset/Training/Normal_ImageGenerator" - self.__Comprehensive_Generator_Root = "../Dataset/Training/Comprehensive_ImageGenerator" - def Set_OneHotEncording(self, content): Counter = [] for i in range(len(content)): @@ -46,35 +16,6 @@ class Tool: Counter = torch.tensor(Counter) self.__OneHot_Encording = functional.one_hot(Counter, len(content)) pass - - def Get_Data_Label(self): - ''' - 取得所需資料的Labels - ''' - return self.__Labels - - def Get_Save_Roots(self, choose): - '''回傳結果為Train, test, validation - choose = 1 => 取白光 Label - else => 取濾光 Label - - 若choose != 1 || choose != 2 => 會回傳四個結果 - ''' - if choose == 1: - return self.__ICG_Training_Root, self.__ICG_Test_Data_Root - if choose == 2: - return self.__Normal_Training_Root, self.__Normal_Test_Data_Root - else: - return self.__Comprehensive_Training_Root, self.__Comprehensive_Testing_Root - - def Get_Generator_Save_Roots(self, choose): - '''回傳結果為Train, test, validation''' - if choose == 1: - return self.__ICG_ImageGenerator_Data_Root - if choose == 2: - return self.__Normal_ImageGenerator_Data_Root - else: - return self.__Comprehensive_Generator_Root def Get_OneHot_Encording_Label(self): return self.__OneHot_Encording \ No newline at end of file diff --git a/Training_Tools/__pycache__/PreProcess.cpython-311.pyc b/Training_Tools/__pycache__/PreProcess.cpython-311.pyc index c4a7a1a..6eb158c 100644 Binary files a/Training_Tools/__pycache__/PreProcess.cpython-311.pyc and b/Training_Tools/__pycache__/PreProcess.cpython-311.pyc differ diff --git a/Training_Tools/__pycache__/PreProcess.cpython-312.pyc b/Training_Tools/__pycache__/PreProcess.cpython-312.pyc new file mode 100644 index 0000000..3dacc79 Binary files /dev/null and b/Training_Tools/__pycache__/PreProcess.cpython-312.pyc differ diff --git a/Training_Tools/__pycache__/PreProcess.cpython-313.pyc b/Training_Tools/__pycache__/PreProcess.cpython-313.pyc new file mode 100644 index 0000000..7e6f8d1 Binary files /dev/null and b/Training_Tools/__pycache__/PreProcess.cpython-313.pyc differ diff --git a/Training_Tools/__pycache__/Tools.cpython-311.pyc b/Training_Tools/__pycache__/Tools.cpython-311.pyc index 7ba88ba..3614d3d 100644 Binary files a/Training_Tools/__pycache__/Tools.cpython-311.pyc and b/Training_Tools/__pycache__/Tools.cpython-311.pyc differ diff --git a/Training_Tools/__pycache__/Tools.cpython-312.pyc b/Training_Tools/__pycache__/Tools.cpython-312.pyc new file mode 100644 index 0000000..d47147f Binary files /dev/null and b/Training_Tools/__pycache__/Tools.cpython-312.pyc differ diff --git a/Training_Tools/__pycache__/Tools.cpython-313.pyc b/Training_Tools/__pycache__/Tools.cpython-313.pyc new file mode 100644 index 0000000..0d6c7cb Binary files /dev/null and b/Training_Tools/__pycache__/Tools.cpython-313.pyc differ diff --git a/Training_Tools/__pycache__/__init__.cpython-311.pyc b/Training_Tools/__pycache__/__init__.cpython-311.pyc index 62f65d9..d629207 100644 Binary files a/Training_Tools/__pycache__/__init__.cpython-311.pyc and b/Training_Tools/__pycache__/__init__.cpython-311.pyc differ diff --git a/Training_Tools/__pycache__/__init__.cpython-312.pyc b/Training_Tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c489d7f Binary files /dev/null and b/Training_Tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/Training_Tools/__pycache__/__init__.cpython-313.pyc b/Training_Tools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b34ca08 Binary files /dev/null and b/Training_Tools/__pycache__/__init__.cpython-313.pyc differ diff --git a/__pycache__/Density_Peak_Algorithm.cpython-311.pyc b/__pycache__/Density_Peak_Algorithm.cpython-311.pyc new file mode 100644 index 0000000..6e58890 Binary files /dev/null and b/__pycache__/Density_Peak_Algorithm.cpython-311.pyc differ diff --git a/__pycache__/Density_Peak_Algorithm.cpython-313.pyc b/__pycache__/Density_Peak_Algorithm.cpython-313.pyc new file mode 100644 index 0000000..2361b3a Binary files /dev/null and b/__pycache__/Density_Peak_Algorithm.cpython-313.pyc differ diff --git a/__pycache__/Processing_image.cpython-311.pyc b/__pycache__/Processing_image.cpython-311.pyc new file mode 100644 index 0000000..5f3b3a6 Binary files /dev/null and b/__pycache__/Processing_image.cpython-311.pyc differ diff --git a/__pycache__/claculate_output_data.cpython-311.pyc b/__pycache__/claculate_output_data.cpython-311.pyc new file mode 100644 index 0000000..f463f61 Binary files /dev/null and b/__pycache__/claculate_output_data.cpython-311.pyc differ diff --git a/__pycache__/decision_graph.cpython-311.pyc b/__pycache__/decision_graph.cpython-311.pyc new file mode 100644 index 0000000..2943ddf Binary files /dev/null and b/__pycache__/decision_graph.cpython-311.pyc differ diff --git a/__pycache__/density_peak_clustering.cpython-311.pyc b/__pycache__/density_peak_clustering.cpython-311.pyc new file mode 100644 index 0000000..828ca23 Binary files /dev/null and b/__pycache__/density_peak_clustering.cpython-311.pyc differ diff --git a/__pycache__/generate_training_data_with_segmentation.cpython-311.pyc b/__pycache__/generate_training_data_with_segmentation.cpython-311.pyc new file mode 100644 index 0000000..8e8743a Binary files /dev/null and b/__pycache__/generate_training_data_with_segmentation.cpython-311.pyc differ diff --git a/__pycache__/image_density_clustering.cpython-311.pyc b/__pycache__/image_density_clustering.cpython-311.pyc new file mode 100644 index 0000000..33fea70 Binary files /dev/null and b/__pycache__/image_density_clustering.cpython-311.pyc differ diff --git a/__pycache__/image_loader.cpython-311.pyc b/__pycache__/image_loader.cpython-311.pyc new file mode 100644 index 0000000..28a96b9 Binary files /dev/null and b/__pycache__/image_loader.cpython-311.pyc differ diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..1d1dbac Binary files /dev/null and b/__pycache__/main.cpython-311.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..3ee63ef Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/model_evaluation.cpython-313.pyc b/__pycache__/model_evaluation.cpython-313.pyc new file mode 100644 index 0000000..05896af Binary files /dev/null and b/__pycache__/model_evaluation.cpython-313.pyc differ diff --git a/__pycache__/test_binary_cross_entropy.cpython-311.pyc b/__pycache__/test_binary_cross_entropy.cpython-311.pyc new file mode 100644 index 0000000..5eb1754 Binary files /dev/null and b/__pycache__/test_binary_cross_entropy.cpython-311.pyc differ diff --git a/__pycache__/test_bounding_box.cpython-311.pyc b/__pycache__/test_bounding_box.cpython-311.pyc new file mode 100644 index 0000000..c56c146 Binary files /dev/null and b/__pycache__/test_bounding_box.cpython-311.pyc differ diff --git a/__pycache__/test_main.cpython-311.pyc b/__pycache__/test_main.cpython-311.pyc new file mode 100644 index 0000000..06c08d8 Binary files /dev/null and b/__pycache__/test_main.cpython-311.pyc differ diff --git a/__pycache__/test_mask_loading.cpython-311.pyc b/__pycache__/test_mask_loading.cpython-311.pyc new file mode 100644 index 0000000..39f1e2b Binary files /dev/null and b/__pycache__/test_mask_loading.cpython-311.pyc differ diff --git a/__pycache__/test_model_branch.cpython-311.pyc b/__pycache__/test_model_branch.cpython-311.pyc new file mode 100644 index 0000000..c2c1594 Binary files /dev/null and b/__pycache__/test_model_branch.cpython-311.pyc differ diff --git a/__pycache__/test_processing_main.cpython-311.pyc b/__pycache__/test_processing_main.cpython-311.pyc new file mode 100644 index 0000000..e17904a Binary files /dev/null and b/__pycache__/test_processing_main.cpython-311.pyc differ diff --git a/__pycache__/test_segmentation.cpython-311.pyc b/__pycache__/test_segmentation.cpython-311.pyc new file mode 100644 index 0000000..c57440f Binary files /dev/null and b/__pycache__/test_segmentation.cpython-311.pyc differ diff --git a/__pycache__/testing_Labels_Accuracy.cpython-311.pyc b/__pycache__/testing_Labels_Accuracy.cpython-311.pyc new file mode 100644 index 0000000..f7fa948 Binary files /dev/null and b/__pycache__/testing_Labels_Accuracy.cpython-311.pyc differ diff --git a/_validation/__pycache__/ValidationTheEnterData.cpython-311.pyc b/_validation/__pycache__/ValidationTheEnterData.cpython-311.pyc index f3a8bd9..46e2e1f 100644 Binary files a/_validation/__pycache__/ValidationTheEnterData.cpython-311.pyc and b/_validation/__pycache__/ValidationTheEnterData.cpython-311.pyc differ diff --git a/_validation/__pycache__/ValidationTheEnterData.cpython-312.pyc b/_validation/__pycache__/ValidationTheEnterData.cpython-312.pyc new file mode 100644 index 0000000..69ee064 Binary files /dev/null and b/_validation/__pycache__/ValidationTheEnterData.cpython-312.pyc differ diff --git a/_validation/__pycache__/ValidationTheEnterData.cpython-313.pyc b/_validation/__pycache__/ValidationTheEnterData.cpython-313.pyc new file mode 100644 index 0000000..6372701 Binary files /dev/null and b/_validation/__pycache__/ValidationTheEnterData.cpython-313.pyc differ diff --git a/all_models_tools/__pycache__/__init__.cpython-311.pyc b/all_models_tools/__pycache__/__init__.cpython-311.pyc index e397ef7..f848b32 100644 Binary files a/all_models_tools/__pycache__/__init__.cpython-311.pyc and b/all_models_tools/__pycache__/__init__.cpython-311.pyc differ diff --git a/all_models_tools/__pycache__/__init__.cpython-312.pyc b/all_models_tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..2748b92 Binary files /dev/null and b/all_models_tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/all_models_tools/__pycache__/__init__.cpython-313.pyc b/all_models_tools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..88e24f6 Binary files /dev/null and b/all_models_tools/__pycache__/__init__.cpython-313.pyc differ diff --git a/all_models_tools/__pycache__/all_model_tools.cpython-311.pyc b/all_models_tools/__pycache__/all_model_tools.cpython-311.pyc index f1d5835..e534707 100644 Binary files a/all_models_tools/__pycache__/all_model_tools.cpython-311.pyc and b/all_models_tools/__pycache__/all_model_tools.cpython-311.pyc differ diff --git a/all_models_tools/__pycache__/all_model_tools.cpython-312.pyc b/all_models_tools/__pycache__/all_model_tools.cpython-312.pyc new file mode 100644 index 0000000..703017a Binary files /dev/null and b/all_models_tools/__pycache__/all_model_tools.cpython-312.pyc differ diff --git a/all_models_tools/__pycache__/all_model_tools.cpython-313.pyc b/all_models_tools/__pycache__/all_model_tools.cpython-313.pyc new file mode 100644 index 0000000..6e2aff6 Binary files /dev/null and b/all_models_tools/__pycache__/all_model_tools.cpython-313.pyc differ diff --git a/all_models_tools/all_model_tools.py b/all_models_tools/all_model_tools.py index 7d5b8b3..e3f86af 100644 --- a/all_models_tools/all_model_tools.py +++ b/all_models_tools/all_model_tools.py @@ -2,19 +2,6 @@ from Load_process.file_processing import Process_File import datetime import torch -# def attention_block(input): -# channel = input.shape[-1] - -# GAP = GlobalAveragePooling2D()(input) - -# block = Dense(units = channel // 16, activation = "relu")(GAP) -# block = Dense(units = channel, activation = "sigmoid")(block) -# block = Reshape((1, 1, channel))(block) - -# block = Multiply()([input, block]) - -# return block - class EarlyStopping: def __init__(self, patience=74, verbose=False, delta=0): self.patience = patience @@ -45,12 +32,11 @@ class EarlyStopping: print(f"Validation loss decreased ({self.best_loss:.6f} --> {val_loss:.6f}). Saving model to {save_path}") -def call_back(model_name, index, optimizer): +def call_back(Save_Root, index, optimizer): File = Process_File() - model_dir = '../Result/save_the_best_model/' + model_name - File.JudgeRoot_MakeDir(model_dir) - modelfiles = File.Make_Save_Root('best_model( ' + str(datetime.date.today()) + " )-" + index + ".pt", model_dir) + File.JudgeRoot_MakeDir(Save_Root) + modelfiles = File.Make_Save_Root('best_model( ' + str(datetime.date.today()) + " )-" + index + ".pt", Save_Root) # model_mckp = ModelCheckpoint(modelfiles, monitor='val_loss', save_best_only=True, save_weights_only = True, mode='auto') @@ -60,7 +46,7 @@ def call_back(model_name, index, optimizer): optimizer, factor = 0.94, # 學習率降低的量。 new_lr = lr * factor patience = 2, # 沒有改進的時期數,之後學習率將降低 - verbose = 0, + mode = 'min', min_lr = 0 # 學習率下限 ) diff --git a/all_models_tools/pre_train_model_construction.py b/all_models_tools/pre_train_model_construction.py deleted file mode 100644 index 8b72d80..0000000 --- a/all_models_tools/pre_train_model_construction.py +++ /dev/null @@ -1,116 +0,0 @@ -from all_models_tools.all_model_tools import attention_block -from keras.activations import softmax, sigmoid -from keras.applications import VGG16,VGG19, ResNet50, ResNet50V2, ResNet101, ResNet101V2, ResNet152, ResNet152V2, InceptionV3, InceptionResNetV2, MobileNet, MobileNetV2, DenseNet121, NASNetLarge, Xception -from keras.layers import GlobalAveragePooling2D, Dense, Flatten -from keras import regularizers -from keras.layers import Add -from application.Xception_indepentment import Xception_indepentment - -def Original_VGG19_Model(): - vgg19 = VGG19(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(vgg19.output) - dense = Dense(units = 4096, activation = "relu")(GAP) - dense = Dense(units = 4096, activation = "relu")(dense) - output = Dense(units = 2, activation = "softmax")(dense) - - return vgg19.input, output - -def Original_ResNet50_model(): - xception = ResNet50(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(xception.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return xception.input, dense - -def Original_NASNetLarge_model(): - nasnetlarge = NASNetLarge(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(nasnetlarge.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return nasnetlarge.input, dense - -def Original_DenseNet121_model(): - Densenet201 = DenseNet121(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(Densenet201.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return Densenet201.input, dense - -def Original_Xception_model(): - xception = Xception(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(xception.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return xception.input, dense - -def Original_VGG16_Model(): - vgg16 = VGG16(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - flatten = Flatten()(vgg16.output) - dense = Dense(units = 4096, activation = "relu")(flatten) - dense = Dense(units = 4096, activation = "relu")(dense) - output = Dense(units = 2, activation = "softmax")(dense) - - return vgg16.input, output - -def Original_ResNet50v2_model(): - resnet50v2 = ResNet50V2(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(resnet50v2.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return resnet50v2.input, dense - -def Original_ResNet101_model(): - resnet101 = ResNet101(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(resnet101.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return resnet101.input, dense - -def Original_ResNet101V2_model(): - resnet101v2 = ResNet101V2(include_top = False, weights = "imagenet", input_shape = (512, 512, 3)) - GAP = GlobalAveragePooling2D()(resnet101v2.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return resnet101v2.input, dense - -def Original_ResNet152_model(): - resnet152 = ResNet152(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(resnet152.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return resnet152.input, dense - -def Original_ResNet152V2_model(): - resnet152v2 = ResNet152V2(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(resnet152v2.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return resnet152v2.input, dense - -def Original_InceptionV3_model(): - inceptionv3 = InceptionV3(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(inceptionv3.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return inceptionv3.input, dense - -def Original_InceptionResNetV2_model(): - inceptionResnetv2 = InceptionResNetV2(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(inceptionResnetv2.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return inceptionResnetv2.input, dense - -def Original_MobileNet_model(): - mobilenet = MobileNet(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(mobilenet.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return mobilenet.input, dense - -def Original_MobileNetV2_model(): - mobilenetv2 = MobileNetV2(include_top = False, weights = "imagenet", input_shape = (200, 200, 3)) - GAP = GlobalAveragePooling2D()(mobilenetv2.output) - dense = Dense(units = 2, activation = "softmax")(GAP) - - return mobilenetv2.input, dense \ No newline at end of file diff --git a/debug_data_validation.py b/debug_data_validation.py new file mode 100644 index 0000000..6e2f193 --- /dev/null +++ b/debug_data_validation.py @@ -0,0 +1,115 @@ +import torch +import numpy as np + +def validate_data_for_training(outputs, labels, num_classes): + """ + 驗證訓練數據是否會導致 CUDA device-side assert 錯誤 + + Args: + outputs: 模型輸出 tensor + labels: 標籤 tensor (可能是 one-hot 編碼) + num_classes: 預期的類別數量 + + Returns: + bool: True 如果數據有效,False 如果可能導致錯誤 + """ + print("=== 數據驗證開始 ===") + + # 檢查基本信息 + print(f"輸出形狀: {outputs.shape}") + print(f"標籤形狀: {labels.shape}") + print(f"預期類別數: {num_classes}") + + # 檢查數據類型 + print(f"輸出數據類型: {outputs.dtype}") + print(f"標籤數據類型: {labels.dtype}") + + # 檢查設備 + print(f"輸出設備: {outputs.device}") + print(f"標籤設備: {labels.device}") + + # 檢查是否有 NaN 或 Inf + if torch.isnan(outputs).any(): + print("❌ 警告:輸出中包含 NaN 值!") + return False + + if torch.isinf(outputs).any(): + print("❌ 警告:輸出中包含 Inf 值!") + return False + + if torch.isnan(labels).any(): + print("❌ 警告:標籤中包含 NaN 值!") + return False + + # 如果標籤是 one-hot 編碼,轉換為索引 + if labels.dim() > 1 and labels.shape[1] > 1: + print("檢測到 one-hot 編碼標籤,轉換為索引...") + _, label_indices = torch.max(labels, dim=1) + else: + label_indices = labels.long() + + print(f"標籤索引範圍: {label_indices.min().item()} - {label_indices.max().item()}") + + # 檢查標籤索引是否在有效範圍內 + if label_indices.min() < 0: + print("❌ 錯誤:發現負數標籤索引!") + return False + + if label_indices.max() >= num_classes: + print(f"❌ 錯誤:標籤索引 {label_indices.max().item()} 超出類別數 {num_classes}!") + print("這會導致 CUDA device-side assert 錯誤") + return False + + # 檢查輸出維度是否與類別數匹配 + if outputs.shape[1] != num_classes: + print(f"❌ 警告:輸出維度 {outputs.shape[1]} 與預期類別數 {num_classes} 不匹配!") + return False + + print("✅ 數據驗證通過!") + print("=== 數據驗證結束 ===") + return True + +def debug_loss_calculation(outputs, labels, num_classes): + """ + 調試損失計算過程 + """ + print("\n=== 損失計算調試 ===") + + try: + # 模擬 Entropy_Loss 的計算過程 + outputs_tensor = torch.as_tensor(outputs, dtype=torch.float32) + labels_tensor = torch.as_tensor(labels, dtype=torch.float32) + + print(f"轉換後輸出形狀: {outputs_tensor.shape}") + print(f"轉換後標籤形狀: {labels_tensor.shape}") + + # 轉換為標籤索引 + _, labels_indices = torch.max(labels_tensor, dim=1) + print(f"標籤索引: {labels_indices}") + + # 檢查是否會導致錯誤 + if validate_data_for_training(outputs_tensor, labels_tensor, num_classes): + print("✅ 損失計算應該不會出錯") + else: + print("❌ 損失計算可能會出錯") + + except Exception as e: + print(f"❌ 損失計算調試過程中出現錯誤: {e}") + +# 使用示例 +if __name__ == "__main__": + # 示例:3 類分類問題 + num_classes = 3 + batch_size = 2 + + # 正常情況 + print("測試正常情況:") + outputs_normal = torch.randn(batch_size, num_classes) + labels_normal = torch.tensor([[1, 0, 0], [0, 0, 1]], dtype=torch.float32) + debug_loss_calculation(outputs_normal, labels_normal, num_classes) + + # 異常情況:標籤索引超出範圍 + print("\n測試異常情況(標籤索引超出範圍):") + outputs_error = torch.randn(batch_size, num_classes) + labels_error = torch.tensor([[0, 0, 0, 1], [1, 0, 0, 0]], dtype=torch.float32) # 4 類標籤但只有 3 類輸出 + debug_loss_calculation(outputs_error, labels_error, num_classes) \ No newline at end of file diff --git a/draw_tools/Grad_cam.py b/draw_tools/Grad_cam.py index ccdf7eb..fc57df0 100644 --- a/draw_tools/Grad_cam.py +++ b/draw_tools/Grad_cam.py @@ -19,11 +19,15 @@ class GradCAM: # Register hooks self.target_layer.register_forward_hook(self.save_activations) - self.target_layer.register_backward_hook(self.save_gradients) + # Use full backward hook if available to avoid deprecation issues + if hasattr(self.target_layer, "register_full_backward_hook"): + self.target_layer.register_full_backward_hook(self.save_gradients) + else: + self.target_layer.register_backward_hook(self.save_gradients) def Processing_Main(self, Test_Dataloader, File_Path): File = Process_File() - for batch_idx, (images, labels) in enumerate(Test_Dataloader): + for batch_idx, (images, labels, File_Name, File_Classes) in enumerate(Test_Dataloader): # Move data to device images = images.to(self.device, dtype=torch.float32) # [64, C, H, W] labels = labels.to(self.device, dtype=torch.float32) # [64, num_classes] @@ -36,14 +40,18 @@ class GradCAM: # Process each image in the batch for i in range(images.size(0)): # Loop over batch size (64) - class_idx = label_classes[i] heatmap = heatmaps[i] # Extract heatmap for this image - overlaid_image = self.overlay_heatmap(heatmap, images[i]) + overlaid_image = self.overlay_heatmap(heatmap, images[i], alpha=0.5) # Create file path based on class - path = f"{File_Path}/class_{class_idx}" + path = f"{File_Path}/{File_Classes[i]}" File.JudgeRoot_MakeDir(path) - File.Save_CV2_File(f"batch_{batch_idx}_img_{i}.png", path, overlaid_image) + # Save overlaid image + File.Save_CV2_File(f"batch_{batch_idx}_{File_Name[i]}", path, overlaid_image) + # # Save raw heatmap separately + # heatmap_resized = cv2.resize(heatmap, (images[i].shape[2], images[i].shape[1]), interpolation=cv2.INTER_CUBIC) + # heatmap_colored = (plt.cm.viridis(heatmap_resized)[:, :, :3] * 255).astype(np.uint8) + # File.Save_CV2_File(f"batch_{batch_idx}_img_{i}_heatmap.png", path, heatmap_colored) def save_activations(self, module, input, output): self.activations = output.detach() # [64, C, H', W'] @@ -53,52 +61,89 @@ class GradCAM: def generate(self, input_images, class_indices=None): self.model.eval() - input_images.requires_grad = True # [64, C, H, W] - - # Forward pass - outputs = self.model(input_images) # [64, num_classes] + input_images.requires_grad = True # [B, C, H, W] + outputs = self.model(input_images) # [B, num_classes] if class_indices is None: - class_indices = torch.argmax(outputs, dim=1).cpu().numpy() # [64] + class_indices = torch.argmax(outputs, dim=1).cpu().numpy() - # Zero gradients self.model.zero_grad() - # Backward pass for each image in the batch heatmaps = [] for i in range(input_images.size(0)): self.model.zero_grad() - outputs[i, class_indices[i]].backward(retain_graph=True) # Backward for specific image/class - heatmap = self._compute_heatmap() + # Backward for the specific image and class + outputs[i, class_indices[i]].backward(retain_graph=True) + # Compute heatmap for sample i + heatmap = self._compute_heatmap(sample_index=i) heatmaps.append(heatmap) - return np.stack(heatmaps) # [64, H', W'] + return np.stack(heatmaps) # [B, H', W'] - def _compute_heatmap(self): - # Get gradients and activations - gradients = self.gradients # [64, C, H', W'] - activations = self.activations # [64, C, H', W'] + def _compute_heatmap(self, sample_index): + # Get gradients and activations for the specific sample + gradients = self.gradients[sample_index] # [C, H', W'] + activations = self.activations[sample_index] # [C, H', W'] # Compute weights (global average pooling of gradients) - weights = torch.mean(gradients, dim=[2, 3], keepdim=True) # [64, C, 1, 1] + weights = torch.mean(gradients, dim=(1, 2), keepdim=True) # [C, 1, 1] - # Compute Grad-CAM heatmap for one image (after single backward) - grad_cam = torch.sum(weights * activations, dim=1)[0] # [64, H', W'] -> [H', W'] - grad_cam = F.relu(grad_cam) # Apply ReLU - grad_cam = grad_cam / (grad_cam.max() + 1e-8) # Normalize to [0, 1] - return grad_cam.cpu().numpy() + # Grad-CAM: weighted sum of activations + grad_cam = torch.sum(weights * activations, dim=0) # [H', W'] + grad_cam = F.relu(grad_cam) + grad_cam = grad_cam / (grad_cam.max() + 1e-8) + + # Apply Gaussian smoothing to reduce artifacts + grad_cam_np = grad_cam.detach().cpu().numpy() + grad_cam_np = cv2.GaussianBlur(grad_cam_np, (5, 5), 0) + grad_cam_np = grad_cam_np / (grad_cam_np.max() + 1e-8) + + return grad_cam_np def overlay_heatmap(self, heatmap, image, alpha=0.5): - # Resize heatmap to match input image spatial dimensions + # Resize heatmap to match input image spatial dimensions using INTER_CUBIC for smoother results heatmap = np.uint8(255 * heatmap) # Scale to [0, 255] - heatmap = Image.fromarray(heatmap).resize((image.shape[1], image.shape[2]), Image.BILINEAR) - heatmap = np.array(heatmap) - heatmap = plt.cm.jet(heatmap)[:, :, :3] # Apply colormap (jet) + heatmap = cv2.resize(heatmap, (image.shape[2], image.shape[1]), interpolation=cv2.INTER_CUBIC) + # Use viridis colormap for better interpretability + heatmap = plt.cm.viridis(heatmap)[:, :, :3] # Apply viridis colormap # Convert image tensor to numpy and denormalize (assuming ImageNet stats) image_np = image.detach().cpu().permute(1, 2, 0).numpy() # [H, W, C] + # Ensure image is in [0, 1] range (if not already) + if image_np.max() > 1.0: + image_np = (image_np - image_np.min()) / (image_np.max() - image_np.min()) - # Overlay - overlay = alpha * heatmap + (1 - alpha) * image_np / 255.0 + # Overlay heatmap on the image + overlay = alpha * heatmap + (1 - alpha) * image_np overlay = np.clip(overlay, 0, 1) * 255 - return overlay.astype(np.uint8) # Return uint8 for cv2 \ No newline at end of file + return overlay.astype(np.uint8) # Return uint8 for cv2 + +def find_last_conv_layer(model): + # Traverse modules in reverse order to find the last Conv2d + last_conv = None + for m in model.modules(): + if isinstance(m, nn.Conv2d): + last_conv = m + if last_conv is None: + raise RuntimeError("No nn.Conv2d layer found in the model for Grad-CAM.") + return last_conv + +def run_grad_cam(model, dataloader, output_root): + # Convenience wrapper to run Grad-CAM end-to-end with your loaders + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + model.to(device) + target_layer = find_last_conv_layer(model) + grad = GradCAM(model, target_layer) + + file = Process_File() + for batch_idx, (images, labels, file_names, file_classes) in enumerate(dataloader): + images = images.to(device, dtype=torch.float32) + labels = labels.to(device, dtype=torch.float32) + label_classes = torch.argmax(labels, dim=1).cpu().numpy() + + heatmaps = grad.generate(images, label_classes) + for i in range(images.size(0)): + overlaid = grad.overlay_heatmap(heatmaps[i], images[i], alpha=0.5) + out_dir = f"{output_root}/{file_classes[i]}" + file.JudgeRoot_MakeDir(out_dir) + file.Save_CV2_File(f"batch_{batch_idx}_{file_names[i]}", out_dir, overlaid) \ No newline at end of file diff --git a/draw_tools/Saliency_Map.py b/draw_tools/Saliency_Map.py new file mode 100644 index 0000000..c8a1e60 --- /dev/null +++ b/draw_tools/Saliency_Map.py @@ -0,0 +1,195 @@ +import torch +import torch.nn as nn +import numpy as np +import cv2 +import matplotlib.pyplot as plt +from Load_process.file_processing import Process_File + +class SaliencyMap: + def __init__(self, model): + self.model = model + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + self.model.to(self.device) + self.model.eval() # 設置為評估模式 + + def Processing_Main(self, Test_Dataloader, File_Path): + """處理測試數據集並生成顯著性圖""" + File = Process_File() + + for batch_idx, (images, labels, File_Name, File_Classes) in enumerate(Test_Dataloader): + # 將數據移至設備 + images = images.to(self.device, dtype=torch.float32) + labels = labels.to(self.device, dtype=torch.float32) + + # 獲取真實類別索引 + label_classes = torch.argmax(labels, dim=1).cpu().numpy() + + # 為批次中的每個圖像生成顯著性圖 + for i in range(images.size(0)): + # 獲取單個圖像和類別 + image = images[i:i+1] # 保持批次維度 + target_class = label_classes[i] + + # 生成顯著性圖 + saliency_map = self.generate_saliency(image, target_class) + + # 將顯著性圖疊加到原始圖像上 + overlaid_image = self.overlay_saliency(saliency_map, image[0]) + + # 創建保存路徑 + path = f"{File_Path}/{File_Classes[i]}" + File.JudgeRoot_MakeDir(path) + + # 保存結果 + File.Save_CV2_File(f"saliency_{batch_idx}_{File_Name[i]}", path, overlaid_image) + + def generate_saliency(self, image, target_class): + """生成單個圖像的顯著性圖""" + # 確保需要梯度 + image.requires_grad_(True) + + # 前向傳播 + output = self.model(image) + + # 清除之前的梯度 + self.model.zero_grad() + + # 創建one-hot編碼的目標 + one_hot = torch.zeros_like(output) + one_hot[0, target_class] = 1 + + # 反向傳播 + output.backward(gradient=one_hot) + + # 獲取梯度 + gradients = image.grad.data + + # 計算顯著性圖 (取絕對值並在通道維度上取最大值) + saliency, _ = torch.max(gradients.abs(), dim=1) + + # 轉換為numpy並歸一化 + saliency_np = saliency.cpu().numpy()[0] + saliency_np = self._normalize(saliency_np) + + # 應用平滑處理以減少噪聲 + saliency_np = cv2.GaussianBlur(saliency_np, (5, 5), 0) + saliency_np = self._normalize(saliency_np) # 再次歸一化 + + return saliency_np + + def _normalize(self, x): + """將數組歸一化到[0,1]範圍""" + # 添加小的epsilon以避免除以零 + return (x - x.min()) / (x.max() - x.min() + 1e-8) + + def overlay_saliency(self, saliency, image, alpha=0.5): + """將顯著性圖疊加到原始圖像上""" + # 將顯著性圖縮放到[0,255]範圍 + saliency_uint8 = np.uint8(255 * saliency) + + # 應用顏色映射 + heatmap = cv2.applyColorMap(saliency_uint8, cv2.COLORMAP_JET) + + # 將圖像張量轉換為numpy數組 + image_np = image.detach().cpu().permute(1, 2, 0).numpy() + + # 確保圖像在[0,1]範圍內 + if image_np.max() > 1.0: + image_np = (image_np - image_np.min()) / (image_np.max() - image_np.min()) + + # 將圖像轉換為uint8 + image_uint8 = np.uint8(255 * image_np) + + # 如果圖像是單通道的,轉換為3通道 + if len(image_uint8.shape) == 2 or image_uint8.shape[2] == 1: + image_uint8 = cv2.cvtColor(image_uint8, cv2.COLOR_GRAY2BGR) + + # 疊加顯著性圖和原始圖像 + overlaid = cv2.addWeighted(heatmap, alpha, image_uint8, 1-alpha, 0) + + return overlaid + + def generate_smooth_saliency(self, image, target_class, n_samples=20, noise_level=0.1): + """使用SmoothGrad技術生成更平滑的顯著性圖""" + # 獲取輸入圖像的標準差 + stdev = noise_level * (torch.max(image) - torch.min(image)).item() + + # 累積梯度 + accumulated_gradients = None + + # 生成多個帶噪聲的樣本並計算梯度 + for _ in range(n_samples): + # 添加高斯噪聲 + noisy_image = image + torch.randn_like(image) * stdev + noisy_image.requires_grad_(True) + + # 前向傳播 + output = self.model(noisy_image) + + # 反向傳播 + self.model.zero_grad() + one_hot = torch.zeros_like(output) + one_hot[0, target_class] = 1 + output.backward(gradient=one_hot) + + # 獲取梯度 + gradients = noisy_image.grad.data + + # 累積梯度 + if accumulated_gradients is None: + accumulated_gradients = gradients + else: + accumulated_gradients += gradients + + # 計算平均梯度 + avg_gradients = accumulated_gradients / n_samples + + # 計算顯著性圖 + saliency, _ = torch.max(avg_gradients.abs(), dim=1) + + # 轉換為numpy並歸一化 + saliency_np = saliency.cpu().numpy()[0] + saliency_np = self._normalize(saliency_np) + + return saliency_np + + def generate_guided_saliency(self, image, target_class): + """使用Guided Backpropagation生成顯著性圖""" + # 保存原始ReLU反向傳播函數 + relu_backward_functions = {} + for module in self.model.modules(): + if isinstance(module, nn.ReLU): + relu_backward_functions[module] = module.backward + module.backward = self._guided_relu_backward + + # 生成顯著性圖 + image.requires_grad_(True) + output = self.model(image) + + self.model.zero_grad() + one_hot = torch.zeros_like(output) + one_hot[0, target_class] = 1 + output.backward(gradient=one_hot) + + # 獲取梯度 + gradients = image.grad.data + + # 恢復原始ReLU反向傳播函數 + for module in relu_backward_functions: + module.backward = relu_backward_functions[module] + + # 計算顯著性圖 (只保留正梯度) + saliency = torch.clamp(gradients, min=0) + saliency, _ = torch.max(saliency, dim=1) + + # 轉換為numpy並歸一化 + saliency_np = saliency.cpu().numpy()[0] + saliency_np = self._normalize(saliency_np) + + return saliency_np + + def _guided_relu_backward(self, grad_output): + """Guided ReLU的反向傳播函數""" + # 只允許正梯度流過 + positive_grad_output = torch.clamp(grad_output, min=0) + return positive_grad_output \ No newline at end of file diff --git a/draw_tools/__pycache__/Grad_cam.cpython-311.pyc b/draw_tools/__pycache__/Grad_cam.cpython-311.pyc index 88a53ae..ef5b8ce 100644 Binary files a/draw_tools/__pycache__/Grad_cam.cpython-311.pyc and b/draw_tools/__pycache__/Grad_cam.cpython-311.pyc differ diff --git a/draw_tools/__pycache__/Grad_cam.cpython-312.pyc b/draw_tools/__pycache__/Grad_cam.cpython-312.pyc new file mode 100644 index 0000000..aacbd93 Binary files /dev/null and b/draw_tools/__pycache__/Grad_cam.cpython-312.pyc differ diff --git a/draw_tools/__pycache__/Grad_cam.cpython-313.pyc b/draw_tools/__pycache__/Grad_cam.cpython-313.pyc new file mode 100644 index 0000000..f7bb8dc Binary files /dev/null and b/draw_tools/__pycache__/Grad_cam.cpython-313.pyc differ diff --git a/draw_tools/__pycache__/Saliency_Map.cpython-311.pyc b/draw_tools/__pycache__/Saliency_Map.cpython-311.pyc new file mode 100644 index 0000000..7f7a02d Binary files /dev/null and b/draw_tools/__pycache__/Saliency_Map.cpython-311.pyc differ diff --git a/draw_tools/__pycache__/Saliency_Map.cpython-313.pyc b/draw_tools/__pycache__/Saliency_Map.cpython-313.pyc new file mode 100644 index 0000000..74d478c Binary files /dev/null and b/draw_tools/__pycache__/Saliency_Map.cpython-313.pyc differ diff --git a/draw_tools/__pycache__/__init__.cpython-311.pyc b/draw_tools/__pycache__/__init__.cpython-311.pyc index 2095704..918dcbc 100644 Binary files a/draw_tools/__pycache__/__init__.cpython-311.pyc and b/draw_tools/__pycache__/__init__.cpython-311.pyc differ diff --git a/draw_tools/__pycache__/__init__.cpython-312.pyc b/draw_tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..92c5c5b Binary files /dev/null and b/draw_tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/draw_tools/__pycache__/__init__.cpython-313.pyc b/draw_tools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..a12a4b6 Binary files /dev/null and b/draw_tools/__pycache__/__init__.cpython-313.pyc differ diff --git a/draw_tools/__pycache__/draw.cpython-311.pyc b/draw_tools/__pycache__/draw.cpython-311.pyc index c84a100..5d0233f 100644 Binary files a/draw_tools/__pycache__/draw.cpython-311.pyc and b/draw_tools/__pycache__/draw.cpython-311.pyc differ diff --git a/draw_tools/__pycache__/draw.cpython-312.pyc b/draw_tools/__pycache__/draw.cpython-312.pyc new file mode 100644 index 0000000..abbd447 Binary files /dev/null and b/draw_tools/__pycache__/draw.cpython-312.pyc differ diff --git a/draw_tools/__pycache__/draw.cpython-313.pyc b/draw_tools/__pycache__/draw.cpython-313.pyc new file mode 100644 index 0000000..5b939d7 Binary files /dev/null and b/draw_tools/__pycache__/draw.cpython-313.pyc differ diff --git a/draw_tools/draw.py b/draw_tools/draw.py index 9c0ef61..06853e3 100644 --- a/draw_tools/draw.py +++ b/draw_tools/draw.py @@ -5,33 +5,51 @@ import matplotlib.figure as figure import matplotlib.backends.backend_agg as agg from Load_process.file_processing import Process_File -def plot_history(Epochs, Losses, Accuracys, file_name, model_name): +def plot_history(Losses, Accuracys, Save_Root, File_Name): File = Process_File() plt.figure(figsize=(16,4)) plt.subplot(1,2,1) - plt.plot(range(1, Epochs + 1), Losses[0]) - plt.plot(range(1, Epochs + 1), Losses[1]) + + # 修正維度不匹配問題 + train_losses = Losses[0] + val_losses = Losses[1] + + # 分別繪製訓練損失和驗證損失 + train_epochs = range(1, len(train_losses) + 1) + plt.plot(train_epochs, train_losses, label='Train') + + val_epochs = range(1, len(val_losses) + 1) + plt.plot(val_epochs, val_losses, label='Validation') + plt.ylabel('Losses') plt.xlabel('epoch') - plt.legend(['Train','Validation'], loc='upper left') + plt.legend(loc='upper left') plt.title('Model Loss') - plt.subplot(1,2,2) - plt.plot(range(1, Epochs + 1), Accuracys[0]) - plt.plot(range(1, Epochs + 1), Accuracys[1]) - plt.ylabel('Accuracies') - plt.xlabel('epoch') - plt.legend(['Train','Validation'], loc='upper left') - plt.title('Model Accuracy') + if Accuracys is not None: + plt.subplot(1,2,2) + train_acc = Accuracys[0] + val_acc = Accuracys[1] + + # 分別繪製訓練準確率和驗證準確率 + train_epochs_acc = range(1, len(train_acc) + 1) + plt.plot(train_epochs_acc, train_acc, label='Train') + + val_epochs_acc = range(1, len(val_acc) + 1) + plt.plot(val_epochs_acc, val_acc, label='Validation') + + plt.ylabel('Accuracies') + plt.xlabel('epoch') + plt.legend(loc='upper left') + plt.title('Model Accuracy') - model_dir = '../Result/Training_Image/save_the_train_image( ' + str(datetime.date.today()) + " )" - File.JudgeRoot_MakeDir(model_dir) - modelfiles = File.Make_Save_Root(str(model_name) + " " + str(file_name) + ".png", model_dir) + File.JudgeRoot_MakeDir(Save_Root) + modelfiles = File.Make_Save_Root(f"{str(File_Name)}.png", Save_Root) plt.savefig(modelfiles) plt.close("all") # 關閉圖表 -def draw_heatmap(matrix, model_name, index): # 二分類以上混淆矩陣做法 +def draw_heatmap(matrix, Save_Root, File_Name, index): # 二分類以上混淆矩陣做法 File = Process_File() # 创建热图 @@ -40,20 +58,19 @@ def draw_heatmap(matrix, model_name, index): # 二分類以上混淆矩陣做法 Ax = fig.add_subplot(111) sns.heatmap(matrix, square = True, annot = True, fmt = 'd', linecolor = 'white', cmap = "Purples", ax = Ax)#画热力图,cmap表示设定的颜色集 - model_dir = '../Result/Matrix_Image/model_matrix_image ( ' + str(datetime.date.today()) + " )" - File.JudgeRoot_MakeDir(model_dir) - modelfiles = File.Make_Save_Root(str(model_name) + "-" + str(index) + ".png", model_dir) + File.JudgeRoot_MakeDir(Save_Root) + modelfiles = File.Make_Save_Root(f"{File_Name}-{str(index)}.png", Save_Root) # confusion.figure.savefig(modelfiles) # 设置图像参数 - Ax.set_title(str(model_name) + " confusion matrix") + Ax.set_title(f"{File_Name} confusion matrix") Ax.set_xlabel("X-Predict label of the model") Ax.set_ylabel("Y-True label of the model") # 保存图像到文件中 canvas.print_figure(modelfiles) -def Confusion_Matrix_of_Two_Classification(Model_Name, Matrix, index): +def Confusion_Matrix_of_Two_Classification(Matrix, Save_Root, File_Name, index): File = Process_File() fx = sns.heatmap(Matrix, annot=True, cmap='turbo') @@ -63,13 +80,20 @@ def Confusion_Matrix_of_Two_Classification(Model_Name, Matrix, index): fx.set_xlabel('answer Values ') fx.set_ylabel('Predicted Values') - # labels the boxes - fx.xaxis.set_ticklabels(['False','True']) - fx.yaxis.set_ticklabels(['False','True']) + # 根据矩阵维度动态设置标签 + n_classes = Matrix.shape[0] + # 如果是2类问题,使用False/True标签 + if n_classes == 2: + labels = ['False', 'True'] + else: + # 对于多类问题,使用数字标签 + labels = [str(i) for i in range(n_classes)] + + fx.xaxis.set_ticklabels(labels) + fx.yaxis.set_ticklabels(labels) - model_dir = '../Result/model_matrix_image ( ' + str(datetime.date.today()) + " )" - File.JudgeRoot_MakeDir(model_dir) - modelfiles = File.Make_Save_Root(str(Model_Name) + "-" + str(index) + ".png", model_dir) + File.JudgeRoot_MakeDir(Save_Root) + modelfiles = File.Make_Save_Root(f"{File_Name}-{str(index)}.png", Save_Root) plt.savefig(modelfiles) plt.close("all") # 關閉圖表 diff --git a/experiments/Model_All_Step.py b/experiments/Model_All_Step.py deleted file mode 100644 index 2e217f0..0000000 --- a/experiments/Model_All_Step.py +++ /dev/null @@ -1,210 +0,0 @@ -from tqdm import tqdm -from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score -from torchmetrics.functional import auroc -from torch.nn import functional - -from all_models_tools.all_model_tools import call_back -from Model_Loss.Loss import Entropy_Loss -from merge_class.merge import merge -from draw_tools.Grad_cam import GradCAM - -import time -import torch.optim as optim -import numpy as np -import torch -import pandas as pd -import datetime - - -class All_Step: - def __init__(self, Model, Epoch, Number_Of_Classes, Model_Name, Experiment_Name): - self.Model = Model - self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - - self.Epoch = Epoch - self.Number_Of_Classes = Number_Of_Classes - - self.Model_Name = Model_Name - self.Experiment_Name = Experiment_Name - - def Training_Step(self, train_subset, val_subset, train_loader, val_loader, model_name, fold, TargetLayer): - # Reinitialize model and optimizer for each fold - # self.Model = self.Model.__class__(self.Number_Of_Classes).to(self.device) # Reinitialize model - Optimizer = optim.SGD(self.Model.parameters(), lr=0.045, momentum=0.9, weight_decay=0.01) - model_path, early_stopping, scheduler = call_back(model_name, f"_fold{fold}", Optimizer) - - criterion = Entropy_Loss() # Custom loss function - Merge_Function = merge() - - # Lists to store metrics for this fold - train_losses = [] - val_losses = [] - train_accuracies = [] - val_accuracies = [] - epoch = 0 - - # Epoch loop - for epoch in range(self.Epoch): - self.Model.train() # Start training - running_loss = 0.0 - all_train_preds = [] - all_train_labels = [] - processed_samples = 0 - - # Calculate epoch start time - start_time = time.time() - total_samples = len(train_subset) # Total samples in subset, not DataLoader - total_Validation_samples = len(val_subset) - - # Progress bar for training batches - epoch_iterator = tqdm(train_loader, desc=f"Fold {fold + 1}/5, Epoch [{epoch + 1}/{self.Epoch}]") - - for inputs, labels in epoch_iterator: - inputs, labels = inputs.to(self.device), labels.to(self.device) # Already tensors from DataLoader - - Optimizer.zero_grad() - outputs = self.Model(inputs) - loss = criterion(outputs, labels) - loss.backward() - Optimizer.step() - running_loss += loss.item() - - # Collect training predictions and labels - Output_Values, Output_Indexs = torch.max(outputs, dim=1) - True_Indexs = np.argmax(labels.cpu().numpy(), axis=1) - - all_train_preds.append(Output_Indexs.cpu().numpy()) - all_train_labels.append(True_Indexs) - - processed_samples += inputs.size(0) # Use size(0) for batch size - - # Calculate progress and timing - progress = (processed_samples / total_samples) * 100 - elapsed_time = time.time() - start_time - iterations_per_second = processed_samples / elapsed_time if elapsed_time > 0 else 0 - eta = (total_samples - processed_samples) / iterations_per_second if iterations_per_second > 0 else 0 - time_str = f"{int(elapsed_time//60):02d}:{int(elapsed_time%60):02d}<{int(eta//60):02d}:{int(eta%60):02d}" - - # Calculate batch accuracy(正確label數量 / 該batch總共的label數量) - batch_accuracy = (Output_Indexs.cpu().numpy() == True_Indexs).mean() - - # Update progress bar - epoch_iterator.set_postfix_str( - f"{processed_samples}/{total_samples} [{time_str}, {iterations_per_second:.2f}it/s, " - f"acc={batch_accuracy:.3f}, loss={loss.item():.3f}]" - ) - - epoch_iterator.close() - # Merge predictions and labels - all_train_preds = Merge_Function.merge_data_main(all_train_preds, 0, len(all_train_preds)) - all_train_labels = Merge_Function.merge_data_main(all_train_labels, 0, len(all_train_labels)) - - Training_Loss = running_loss / len(train_loader) - train_accuracy = accuracy_score(all_train_labels, all_train_preds) - - train_losses.append(Training_Loss) - train_accuracies.append(train_accuracy) - - # Validation step - self.Model.eval() - val_loss = 0.0 - all_val_preds = [] - all_val_labels = [] - - start_Validation_time = time.time() - epoch_iterator = tqdm(val_loader, desc=f"\tValidation-Fold {fold + 1}/5, Epoch [{epoch + 1}/{self.Epoch}]") - with torch.no_grad(): - for inputs, labels in epoch_iterator: - inputs, labels = inputs.to(self.device), labels.to(self.device) - outputs = self.Model(inputs) - loss = criterion(outputs, labels) - val_loss += loss.item() - - # Collect validation predictions and labels - Output_Values, Output_Indexs = torch.max(outputs, dim=1) - True_Indexs = np.argmax(labels.cpu().numpy(), axis=1) - - all_val_preds.append(Output_Indexs.cpu().numpy()) - all_val_labels.append(True_Indexs) - - processed_samples += inputs.size(0) # Use size(0) for batch size - - # Calculate progress and timing - progress = (processed_samples / total_Validation_samples) * 100 - elapsed_time = time.time() - start_Validation_time - iterations_per_second = processed_samples / elapsed_time if elapsed_time > 0 else 0 - eta = (total_Validation_samples - processed_samples) / iterations_per_second if iterations_per_second > 0 else 0 - time_str = f"{int(elapsed_time//60):02d}:{int(elapsed_time%60):02d}<{int(eta//60):02d}:{int(eta%60):02d}" - - # Calculate batch accuracy - batch_accuracy = (Output_Indexs.cpu().numpy() == True_Indexs).mean() - - # Update progress bar - epoch_iterator.set_postfix_str( - f"{processed_samples}/{total_Validation_samples} [{time_str}, {iterations_per_second:.2f}it/s, " - f"acc={batch_accuracy:.3f}, loss={loss.item():.3f}]" - ) - - epoch_iterator.close() - print("\n") - - # Merge predictions and labels - all_val_preds = Merge_Function.merge_data_main(all_val_preds, 0, len(all_val_preds)) - all_val_labels = Merge_Function.merge_data_main(all_val_labels, 0, len(all_val_labels)) - - val_loss /= len(val_loader) - val_accuracy = accuracy_score(all_val_labels, all_val_preds) - - val_losses.append(val_loss) - val_accuracies.append(val_accuracy) - - print(f"Traini Loss: {Training_Loss:.4f}, Accuracy: {train_accuracy:0.2f}, Validation Loss: {val_loss:.4f}, Accuracy: {val_accuracy:0.2f}\n") - - if epoch % 10 == 0: - Grad = GradCAM(self.Model, TargetLayer) - Grad.Processing_Main(val_loader, f"../Result/GradCAM_Image/Validation/GradCAM_Image({str(datetime.date.today())})/fold-{str(fold)}/") - - # Early stopping - early_stopping(val_loss, self.Model, model_path) - if early_stopping.early_stop: - print(f"Early stopping triggered in Fold {fold + 1} at epoch {epoch + 1}") - break - - # Learning rate adjustment - scheduler.step(val_loss) - - Total_Epoch = epoch + 1 - return self.Model, model_path, train_losses, val_losses, train_accuracies, val_accuracies, Total_Epoch - - def Evaluate_Model(self, cnn_model, Test_Dataloader): - # (Unchanged Evaluate_Model method) - cnn_model.eval() - True_Label, Predict_Label = [], [] - True_Label_OneHot, Predict_Label_OneHot = [], [] - loss = 0.0 - - with torch.no_grad(): - for images, labels in Test_Dataloader: - images, labels = torch.as_tensor(images).to(self.device), torch.as_tensor(labels).to(self.device) - outputs = cnn_model(images) - Output_Values, Output_Indexs = torch.max(outputs, 1) - True_Indexs = np.argmax(labels.cpu().numpy(), 1) - - True_Label.append(Output_Indexs.cpu().numpy()) - Predict_Label.append(True_Indexs) - - Predict_Label_OneHot.append(torch.tensor(functional.one_hot(Output_Indexs, self.Number_Of_Classes), dtype=torch.float32).cpu().numpy()[0]) - True_Label_OneHot.append(torch.tensor(labels, dtype=torch.int).cpu().numpy()[0]) - - loss /= len(Test_Dataloader) - - True_Label_OneHot = torch.as_tensor(True_Label_OneHot, dtype=torch.int) - Predict_Label_OneHot = torch.as_tensor(Predict_Label_OneHot, dtype=torch.float32) - - accuracy = accuracy_score(True_Label, Predict_Label) - precision = precision_score(True_Label, Predict_Label, average="macro") - recall = recall_score(True_Label, Predict_Label, average="macro") - AUC = auroc(Predict_Label_OneHot, True_Label_OneHot, num_labels=self.Number_Of_Classes, task="multilabel", average="macro") - f1 = f1_score(True_Label, Predict_Label, average="macro") - - return True_Label, Predict_Label, loss, accuracy, precision, recall, AUC, f1 \ No newline at end of file diff --git a/experiments/Models/GastroSegNet_Model.py b/experiments/Models/GastroSegNet_Model.py new file mode 100644 index 0000000..2160fa9 --- /dev/null +++ b/experiments/Models/GastroSegNet_Model.py @@ -0,0 +1,91 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class Encode_Block(nn.Module): + """基本的卷積塊:Conv2d + BatchNorm + ReLU""" + def __init__(self, in_channels, out_channels): + super(Encode_Block, self).__init__() + self.conv = nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + nn.BatchNorm2d(out_channels), + nn.ReLU(inplace=True) + ) + + def forward(self, x): + return self.conv(x) + +class GastroSegNet(nn.Module): + """簡單的U-Net實現""" + def __init__(self, in_channels=3, out_channels=3, features=[32, 64, 128, 256]): + super(GastroSegNet, self).__init__() + + # 編碼器(下採樣路徑) + self.encoder = nn.ModuleList() + self.pool = nn.MaxPool2d(kernel_size=2, stride=2) + + # 第一層 + self.encoder.append(Encode_Block(in_channels, features[0])) + + # 其他編碼層 + for i in range(1, len(features)): + self.encoder.append(Encode_Block(features[i-1], features[i])) + + # 瓶頸層(最底層) + self.bottleneck = Encode_Block(features[-1], features[-1] * 2) + + # 解碼器(上採樣路徑) + self.decoder = nn.ModuleList() + self.upconv = nn.ModuleList() + + # 創建上採樣和解碼層 + for i in range(len(features)): + self.upconv.append( + nn.ConvTranspose2d(features[-1-i] * 2, features[-1-i], kernel_size=2, stride=2) + ) + self.decoder.append( + Encode_Block(features[-1-i] * 2, features[-1-i]) + ) + + # 最終輸出層 + self.final_conv = nn.Conv2d(features[0], out_channels, kernel_size=1) + + def forward(self, x): + # 存儲跳躍連接 + skip_connections = [] + + # 編碼器路徑 + for encoder_layer in self.encoder: + x = encoder_layer(x) + skip_connections.append(x) + x = self.pool(x) + + # 瓶頸層 + x = self.bottleneck(x) + + # 反轉跳躍連接列表 + skip_connections = skip_connections[::-1] + + # 解碼器路徑 + for i, (upconv, decoder) in enumerate(zip(self.upconv, self.decoder)): + # 上採樣 + x = upconv(x) + + # 獲取對應的跳躍連接 + skip = skip_connections[i] + + # 如果尺寸不匹配,調整大小 + if x.shape != skip.shape: + x = F.interpolate(x, size=skip.shape[2:], mode='bilinear', align_corners=False) + + # 連接跳躍連接 + x = torch.cat([skip, x], dim=1) + + # 通過解碼塊 + x = decoder(x) + + # 最終輸出 + return self.final_conv(x) \ No newline at end of file diff --git a/experiments/Models/Xception_Model_Modification.py b/experiments/Models/Xception_Model_Modification.py new file mode 100644 index 0000000..af15832 --- /dev/null +++ b/experiments/Models/Xception_Model_Modification.py @@ -0,0 +1,152 @@ +import torch.nn as nn +import torch.nn.functional as F +import torch + +from utils.Stomach_Config import Model_Config + +class SeparableConv2d(nn.Module): + def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True): + super(SeparableConv2d, self).__init__() + self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size=kernel_size, stride=stride, + padding=padding, groups=in_channels, bias=bias) + self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, + padding=0, bias=bias) + + def forward(self, x): + x = self.depthwise(x) + x = self.pointwise(x) + return x + +class EntryFlow(nn.Module): + def __init__(self, in_channels=3): + super(EntryFlow, self).__init__() + self.conv1 = nn.Conv2d(in_channels, 32, 3, stride=2, padding=1, bias=False, dilation = 2) + self.bn1 = nn.BatchNorm2d(32) + self.conv2 = nn.Conv2d(32, 64, 3, padding=1, bias=False, dilation = 2) + self.bn2 = nn.BatchNorm2d(64) + + self.conv3_residual = nn.Sequential( + SeparableConv2d(64, 128, 3, padding=1), + nn.BatchNorm2d(128), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(128, 128, 3, padding=1), + nn.BatchNorm2d(128), + nn.MaxPool2d(3, stride=2, padding=1) + ) + self.conv3_shortcut = nn.Conv2d(64, 128, 1, stride=2, bias=False) + self.bn3 = nn.BatchNorm2d(128) + + self.conv4_residual = nn.Sequential( + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(128, 256, 3, padding=1), + nn.BatchNorm2d(256), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(256, 256, 3, padding=1), + nn.BatchNorm2d(256), + nn.MaxPool2d(3, stride=2, padding=1) + ) + self.conv4_shortcut = nn.Conv2d(128, 256, 1, stride=2, bias=False) + self.bn4 = nn.BatchNorm2d(256) + + self.conv5_residual = nn.Sequential( + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(256, 728, 3, padding=1), + nn.BatchNorm2d(728), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(728, 728, 3, padding=1), + nn.BatchNorm2d(728), + nn.MaxPool2d(3, stride=2, padding=1) + ) + self.conv5_shortcut = nn.Conv2d(256, 728, 1, stride=2, bias=False) + self.bn5 = nn.BatchNorm2d(728) + + def forward(self, x): + x = F.relu(self.bn1(self.conv1(x))) + x = F.relu(self.bn2(self.conv2(x))) + + residual = self.conv3_residual(x) + shortcut = self.conv3_shortcut(x) + x = F.relu(self.bn3(residual + shortcut)) + + residual = self.conv4_residual(x) + shortcut = self.conv4_shortcut(x) + x = F.relu(self.bn4(residual + shortcut)) + + residual = self.conv5_residual(x) + shortcut = self.conv5_shortcut(x) + x = F.relu(self.bn5(residual + shortcut)) + return x + +class MiddleFlow(nn.Module): + def __init__(self): + super(MiddleFlow, self).__init__() + self.conv_residual = nn.Sequential( + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(728, 728, 3, padding=1), + nn.BatchNorm2d(728), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(728, 728, 3, padding=1), + nn.BatchNorm2d(728), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(728, 728, 3, padding=1), + nn.BatchNorm2d(728) + ) + + def forward(self, x): + return self.conv_residual(x) + x + +class ExitFlow(nn.Module): + def __init__(self): + super(ExitFlow, self).__init__() + self.conv1_residual = nn.Sequential( + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(728, 1024, 3, padding=1), + nn.BatchNorm2d(1024), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(1024, 1024, 3, padding=1), + nn.BatchNorm2d(1024), + nn.MaxPool2d(3, stride=2, padding=1) + ) + self.conv1_shortcut = nn.Conv2d(728, 1024, 1, stride=2, bias=False) + self.bn1 = nn.BatchNorm2d(1024) + + self.conv2 = nn.Sequential( + SeparableConv2d(1024, 1536, 3, padding=1), + nn.BatchNorm2d(1536), + nn.ReLU(inplace=False), # 修改這裡 + SeparableConv2d(1536, 2048, 3, padding=1), + nn.BatchNorm2d(2048), + nn.ReLU(inplace=False) # 修改這裡 + ) + self.avgpool = nn.AdaptiveAvgPool1d(Model_Config["GPA Output Nodes"]) + self.Hidden = nn.Linear(Model_Config["GPA Output Nodes"], Model_Config["Linear Hidden Nodes"]) + self.fc = nn.Linear(Model_Config["Linear Hidden Nodes"], Model_Config["Output Linear Nodes"]) + self.dropout = nn.Dropout(Model_Config["Dropout Rate"]) + + def forward(self, x): + residual = self.conv1_residual(x) + shortcut = self.conv1_shortcut(x) + x = F.relu(self.bn1(residual + shortcut)) + + x = self.conv2(x) + x = x.view(x.size(0), -1) + x = self.avgpool(x) + x = F.relu(self.Hidden(x)) + x = self.dropout(x) + x = self.fc(x) + return x + +class Xception(nn.Module): + def __init__(self): + super(Xception, self).__init__() + self.entry_flow = EntryFlow(in_channels=3) # 默认输入通道为3 + self.middle_flow = nn.Sequential(*[MiddleFlow() for _ in range(8)]) + self.exit_flow = ExitFlow() + + def forward(self, x): + # 正常的前向傳播 + x = self.entry_flow(x) + x = self.middle_flow(x) + x = self.exit_flow(x) + + return x \ No newline at end of file diff --git a/Read_and_process_image/__init__.py b/experiments/Models/__init__.py similarity index 100% rename from Read_and_process_image/__init__.py rename to experiments/Models/__init__.py diff --git a/experiments/Models/__pycache__/GastroSegNet_Model.cpython-311.pyc b/experiments/Models/__pycache__/GastroSegNet_Model.cpython-311.pyc new file mode 100644 index 0000000..29a546d Binary files /dev/null and b/experiments/Models/__pycache__/GastroSegNet_Model.cpython-311.pyc differ diff --git a/experiments/Models/__pycache__/GastroSegNet_Model.cpython-313.pyc b/experiments/Models/__pycache__/GastroSegNet_Model.cpython-313.pyc new file mode 100644 index 0000000..eb245df Binary files /dev/null and b/experiments/Models/__pycache__/GastroSegNet_Model.cpython-313.pyc differ diff --git a/experiments/Models/__pycache__/Xception_Model_Modification.cpython-311.pyc b/experiments/Models/__pycache__/Xception_Model_Modification.cpython-311.pyc new file mode 100644 index 0000000..35914af Binary files /dev/null and b/experiments/Models/__pycache__/Xception_Model_Modification.cpython-311.pyc differ diff --git a/experiments/Models/__pycache__/Xception_Model_Modification.cpython-313.pyc b/experiments/Models/__pycache__/Xception_Model_Modification.cpython-313.pyc new file mode 100644 index 0000000..b2a6649 Binary files /dev/null and b/experiments/Models/__pycache__/Xception_Model_Modification.cpython-313.pyc differ diff --git a/experiments/Models/__pycache__/__init__.cpython-311.pyc b/experiments/Models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1cda1dd Binary files /dev/null and b/experiments/Models/__pycache__/__init__.cpython-311.pyc differ diff --git a/experiments/Models/__pycache__/__init__.cpython-313.pyc b/experiments/Models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..a1076b1 Binary files /dev/null and b/experiments/Models/__pycache__/__init__.cpython-313.pyc differ diff --git a/experiments/Models/__pycache__/pytorch_Model.cpython-311.pyc b/experiments/Models/__pycache__/pytorch_Model.cpython-311.pyc new file mode 100644 index 0000000..f1c8741 Binary files /dev/null and b/experiments/Models/__pycache__/pytorch_Model.cpython-311.pyc differ diff --git a/experiments/Models/__pycache__/pytorch_Model.cpython-313.pyc b/experiments/Models/__pycache__/pytorch_Model.cpython-313.pyc new file mode 100644 index 0000000..1965f7c Binary files /dev/null and b/experiments/Models/__pycache__/pytorch_Model.cpython-313.pyc differ diff --git a/experiments/Models/pytorch_Model.py b/experiments/Models/pytorch_Model.py new file mode 100644 index 0000000..4408bc7 --- /dev/null +++ b/experiments/Models/pytorch_Model.py @@ -0,0 +1,29 @@ +import torch.nn as nn +import timm +from utils.Stomach_Config import Model_Config + +class ModifiedXception(nn.Module): + def __init__(self): + super(ModifiedXception, self).__init__() + + # 加載 Xception 預訓練模型,去掉最後一層 (fc 層) + self.base_model = timm.create_model(Model_Config["Model Name"], pretrained=True, num_classes = 3) + self.base_model.fc = nn.Identity() # 移除原來的 fully connected 層 + + # 新增全局平均池化層、隱藏層和輸出層 + self.global_avg_pool = nn.AdaptiveAvgPool1d(Model_Config["GPA Output Nodes"]) # 全局平均池化 + self.hidden_layer = nn.Linear(Model_Config["GPA Output Nodes"], Model_Config["Linear Hidden Nodes"]) # 隱藏層,輸入大小取決於 Xception 的輸出大小 + self.output_layer = nn.Linear(Model_Config["Linear Hidden Nodes"], Model_Config["Output Linear Nodes"]) # 輸出層,依據分類數目設定 + + # 激活函數與 dropout + self.relu = nn.ReLU() + self.dropout = nn.Dropout(Model_Config["Dropout Rate"]) + + def forward(self, x): + x = self.base_model(x) # Xception 主體 + x = self.global_avg_pool(x) # 全局平均池化 + x = self.dropout(x) # Dropout + x = self.hidden_layer(x) + x = self.relu(x) # 隱藏層 + ReLU + x = self.output_layer(x) # 輸出層 + return x \ No newline at end of file diff --git a/experiments/Training/Identification_Block_Training.py b/experiments/Training/Identification_Block_Training.py new file mode 100644 index 0000000..2496160 --- /dev/null +++ b/experiments/Training/Identification_Block_Training.py @@ -0,0 +1,461 @@ +from tqdm import tqdm +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score +from torchmetrics.functional import auroc +from torch.nn import functional +from torch import nn +import torch +from sklearn.model_selection import KFold +from torchinfo import summary +from sklearn.metrics import confusion_matrix + +from all_models_tools.all_model_tools import call_back +from Model_Loss.Loss import Entropy_Loss +from Model_Loss.binary_cross_entropy import BinaryCrossEntropy +from merge_class.merge import merge +from draw_tools.Saliency_Map import SaliencyMap +from utils.Stomach_Config import Training_Config, Loading_Config, Save_Result_File_Config +from experiments.Models.Xception_Model_Modification import Xception +from Load_process.LoadData import Loding_Data_Root +from Training_Tools.PreProcess import Training_Precesses +from Calculate_Process.Calculate import Calculate +from Load_process.file_processing import Process_File +from draw_tools.draw import plot_history, draw_heatmap +from Load_process.file_processing import Process_File + +import time +import torch.optim as optim +import numpy as np +import torch +import pandas as pd +import datetime +import argparse +import os + +class Identification_Block_Training_Step(Loding_Data_Root, Training_Precesses): + def __init__(self, Experiment_Name, Best_Model_Save_Root): + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + self.Model = self.Construct_Identification_Model_CUDA() # 模型變數 + self.train_subset = None # Training Dataset 的子集 + self.val_subset = None # Validation Dataset 的子集 + self.train_loader = None # Training DataLoader 的讀檔器 + self.val_loader = None # Validation DataLoader 的讀檔器 + self.Mask = None # 遮罩變數,接收GastroSegNet產出來的Mask + self.Grad = None # 梯度變數,後面用來執行Grad CAM + + self.model_name = Training_Config["Model_Name"] # 取名,使用哪個模型(可能是預處理模型/自己設計的模型) + self.Epoch = Training_Config["Epoch"] # 訓練該模型的次數 + self.train_batch_size = Training_Config["Train_Batch_Size"] # 訓練模型的Batch Size + self.Experiment_Name = Experiment_Name + self.Number_Of_Classes = Loading_Config["Identification_Label_Length"] + self.Best_Model_Save_Root = Best_Model_Save_Root + + # 初始化多個繼承物件 + Training_Precesses.__init__(self, Training_Config["Image_Size"]) + Loding_Data_Root.__init__(self, Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["Test_Data_Root"]) + + pass + + def Processing_Main(self, training_dataset, Test_Dataloader=None): + # Lists to store metrics across all folds + all_fold_train_losses = [] + all_fold_val_losses = [] + all_fold_train_accuracies = [] + all_fold_val_accuracies = [] + Calculate_Process = Calculate() + File = Process_File() + Calculate_Tool = [Calculate() for i in range(3)] + Best_Model_Path = None + Best_Validation_Loss = 100000000 + + # K-Fold loop + kf = KFold(n_splits=5, shuffle=True, random_state=42) + for fold, (train_idx, val_idx) in enumerate(kf.split(range(len(training_dataset)))): # K-Fold 交叉驗證迴圈 + print(f"\nStarting Fold {fold + 1}/5") + + # Create training and validation subsets for this fold + self.train_subset = torch.utils.data.Subset(training_dataset, train_idx) + self.val_subset = torch.utils.data.Subset(training_dataset, val_idx) + + # Wrap subsets in DataLoaders (use same batch size as original) + self.train_loader = self.Dataloader_Sampler(self.train_subset , self.train_batch_size, True) + self.val_loader = self.Dataloader_Sampler(self.val_subset, self.train_batch_size, True) + + # 模型訓練與驗證 + model_path, train_losses, Validation_losses, train_accuracies, Validation_accuracies, Total_Epoch, best_val_loss = self.Training_And_Validation(fold) + + # Store fold results + all_fold_train_losses.append(train_losses) + all_fold_val_losses.append(Validation_losses) + all_fold_train_accuracies.append(train_accuracies) + all_fold_val_accuracies.append(Validation_accuracies) + + # 确保张量在CPU上,以便可以转换为NumPy数组 + if torch.is_tensor(train_losses): + train_losses = train_losses.cpu().detach().numpy() + if torch.is_tensor(Validation_losses): + Validation_losses = Validation_losses.cpu().detach().numpy() + if torch.is_tensor(train_accuracies): + train_accuracies = train_accuracies.cpu().detach().numpy() + if torch.is_tensor(Validation_accuracies): + Validation_accuracies = Validation_accuracies.cpu().detach().numpy() + + Losses = [train_losses, Validation_losses] + Accuracies = [train_accuracies, Validation_accuracies] + plot_history(Losses, Accuracies, f"{Save_Result_File_Config['Identification_Plot_Image']}/{self.Experiment_Name}", f"train-{str(fold)}") # 將訓練結果化成圖,並將化出來的圖丟出去儲存 + + # 驗證結果 + True_Label, Predict_Label, loss, accuracy, precision, recall, f1 = self.Evaluate_Model(self.Model, Test_Dataloader, fold) + + # 紀錄該次訓練結果 + Calculate_Process.Append_numbers(loss, accuracy, precision, recall, f1) + self.record_matrix_image(True_Label, Predict_Label, fold) + print(self.record_everyTime_test_result(loss, accuracy, precision, recall, f1, fold, self.Experiment_Name)) # 紀錄當前訓練完之後的預測結果,並輸出成csv檔 + + # 使用識別模型進行各類別評估 + Calculate_Tool = self.Evaluate_Per_Class_Metrics(self.Model, Test_Dataloader, Loading_Config["Training_Labels"], Calculate_Tool) + + if best_val_loss < Best_Validation_Loss: + Best_Validation_Loss = best_val_loss + Best_Model_Path = model_path + + Calculate_Process.Calculate_Mean() + Calculate_Process.Calculate_Std() + + File.Save_CSV_File(f"../Result/Experiment_Result/{self.Experiment_Name}/Total/{str(datetime.date.today())}", f"Total_Training_Result-{fold}", Calculate_Process.Output_Style()) + + for Calculate_Every_Class in Calculate_Tool: + Calculate_Every_Class.Calculate_Mean() + Calculate_Every_Class.Calculate_Std() + + # Aggregate results across folds + avg_train_losses = np.mean([losses[-1] for losses in all_fold_train_losses]) + avg_val_losses = np.mean([losses[-1] for losses in all_fold_val_losses]) + avg_train_accuracies = np.mean([acc[-1] for acc in all_fold_train_accuracies]) + avg_val_accuracies = np.mean([acc[-1] for acc in all_fold_val_accuracies]) + + print(f"\nCross-Validation Results:") + print(f"Avg Train Loss: {avg_train_losses:.4f}, Avg Val Loss: {avg_val_losses:.4f}") + print(f"Avg Train Acc: {avg_train_accuracies:.4f}, Avg Val Acc: {avg_val_accuracies:.4f}") + + File.Save_TXT_File(content = f"\nCross-Validation Results:\nAvg Train Loss: {avg_train_losses:.4f}, Avg Val Loss: {avg_val_losses:.4f}\nAvg Train Acc: {avg_train_accuracies:.4f}, Avg Val Acc: {avg_val_accuracies:.4f}\n", Save_Root = Save_Result_File_Config["Identification_Average_Result"], File_Name = "Training_Average_Result") + + # 返回最後一個fold的模型路徑和平均指標 + return Best_Model_Path, Calculate_Process, Calculate_Tool + + def Training_And_Validation(self, Fold): + # Reinitialize model and optimizer for each fold + # self.Model = self.Model.__class__(self.Number_Of_Classes).to(self.device) # Reinitialize model + self.Model = self.Construct_Identification_Model_CUDA() # 模型初始化 + Optimizer = optim.SGD(self.Model.parameters(), lr=0.045, momentum=0.9, weight_decay = Training_Config["weight_decay"]) + model_path, early_stopping, scheduler = call_back(self.Best_Model_Save_Root, f"fold{Fold}", Optimizer) + + # Lists to store metrics for this fold + train_losses = [] + Validation_losses = [] + train_accuracies = [] + Validation_accuracies = [] + + # Epoch loop + for epoch in range(self.Epoch): + self.Model.train() # Start training + Training_Loss = 0.0 + All_Predict_List, All_Label_List = [], [] + + # Progress bar for training batches + epoch_iterator = tqdm(self.train_loader, desc=f"Fold {Fold + 1}/5, Epoch [{epoch + 1}/{self.Epoch}]") + Start_Time = time.time() + + for inputs, labels, File_Name, File_Classes in epoch_iterator: + Optimizer.zero_grad() # 清零梯度,防止梯度累積 + Total_Losses, Training_Loss, All_Predict_List, All_Label_List, Predict_Indexs, Truth_Indexs = self.Model_Branch(inputs, labels, All_Predict_List, All_Label_List, Training_Loss) + Total_Losses.backward() + + Optimizer.step() + self.Calculate_Progress_And_Timing(inputs, Predict_Indexs, Truth_Indexs, self.train_subset, Total_Losses, epoch_iterator, Start_Time) + + train_losses, train_accuracies, Training_Loss, Train_accuracy = self.Calculate_Average_Scores(self.train_loader, Training_Loss, All_Predict_List, All_Label_List, train_losses, train_accuracies) + + # Validation step + self.Model.eval() + val_loss = 0.0 + all_val_preds = [] + all_val_labels = [] + + start_Validation_time = time.time() + epoch_iterator = tqdm(self.val_loader, desc=f"\tValidation-Fold {Fold + 1}/5, Epoch [{epoch + 1}/{self.Epoch}]") + with torch.no_grad(): + for inputs, labels, File_Name, File_Classes in epoch_iterator: + Validation_Total_Loss, val_loss, all_val_preds, all_val_labels, Predict_Indexs, Truth_Indexs = self.Model_Branch(inputs, labels, all_val_preds, all_val_labels, val_loss) + self.Calculate_Progress_And_Timing(inputs, Predict_Indexs, Truth_Indexs, self.val_subset, Validation_Total_Loss, epoch_iterator, start_Validation_time) + + Validation_losses, Validation_accuracies, val_loss, val_accuracy = self.Calculate_Average_Scores(self.val_loader, val_loss, all_val_preds, all_val_labels, Validation_losses, Validation_accuracies) + print(f"Traini Loss: {Training_Loss:.4f}, Accuracy: {Train_accuracy:0.2f}, Validation Loss: {val_loss:.4f}, Accuracy: {val_accuracy:0.2f}\n") + + # if epoch % 5 == 0: + # # Grad = GradCAM(self.Model, TargetLayer) + # # Grad.Processing_Main(val_loader, f"../Result/GradCAM_Image/Validation/GradCAM_Image({str(datetime.date.today())})/{self.Experiment_Name}/fold-{str(fold)}/") + + # # 創建SaliencyMap實例 + # saliency_map = SaliencyMap(self.Model) + # # 處理測試數據集 + # saliency_map.Processing_Main(self.val_loader, f"../Result/Saliency_Image/Validation/Saliency_Image({str(datetime.date.today())})/{self.Experiment_Name}/fold-{str(Fold)}/") + + # Early stopping + early_stopping(val_loss, self.Model, model_path) + best_val_loss = early_stopping.best_loss + if early_stopping.early_stop: + print(f"Early stopping triggered in Fold {Fold + 1} at epoch {epoch + 1}") + break + + # Learning rate adjustment + scheduler.step(val_loss) + + Total_Epoch = epoch + 1 + # 確保返回模型路徑 + return model_path, train_losses, Validation_losses, train_accuracies, Validation_accuracies, Total_Epoch, best_val_loss + + def Construct_Identification_Model_CUDA(self): + # 从Model_Config中获取输出节点数量 + Model = Xception() + + # 注释掉summary调用,避免Mask参数问题 + # 直接打印模型结构 + print(f"Model structure: {Model}") + + # 打印模型参数 + for name, parameters in Model.named_parameters(): + print(f"Layer Name: {name}, Parameters: {parameters.size()}") + + return self.Convert_Model_To_CUDA(Model) + + def Convert_Model_To_CUDA(self, model): + if torch.cuda.device_count() > 1: + model = nn.DataParallel(model) + + model = model.to(self.device) + + return model + + def Model_Branch(self, Input_Images, Labels, All_Predict_List : list, All_Label_List : list, running_loss): + # 直接將張量移到設備上,不需要重新創建 + Input_Images.requires_grad = False + Input_Images, Labels = Input_Images.to(self.device), Labels.to(self.device) + + # 確保輸入圖像在有效範圍內 + Input_Images = torch.clamp(Input_Images, 0.0, 1.0) + + Predict = self.Model(Input_Images) + + # Collect training predictions and labels + Output_Values, Output_Indexs = torch.max(Predict, dim=1) + True_Indexs = np.argmax(Labels.cpu().numpy(), axis=1) + + All_Predict_List.append(Output_Indexs.cpu().numpy()) + All_Label_List.append(True_Indexs) + + # 不需要對Predict進行clamp,因為我們已經修改了損失函數使用binary_cross_entropy_with_logits + Losses = self.Losses(Predict, Labels) + running_loss += Losses.item() + + return Losses, running_loss, All_Predict_List, All_Label_List, Output_Indexs, True_Indexs + + def Losses(self, Predicts, Labels): + criterion = BinaryCrossEntropy() + Loss = criterion(Predicts, Labels) + return Loss + + def Evaluate_Model(self, cnn_model, Test_Dataloader, index, identification_model_path=None): + # 載入識別模型權重(如果提供了路徑) + if identification_model_path is not None: + cnn_model.load_state_dict(torch.load(identification_model_path)) + else: + assert identification_model_path is None, "No identification model path provided for evaluation." + + # 評估模型 + cnn_model.eval() + True_Label, Predict_Label = [], [] + True_Label_OneHot, Predict_Label_OneHot = [], [] + loss = 0.0 + + with torch.no_grad(): + for images, labels, File_Name, File_Classes in Test_Dataloader: + Total_Loss, Running_Loss, Predict_Label, True_Label, Output_Indexs, Truth_Index = self.Model_Branch(images, labels, Predict_Label, True_Label, 0) + + Predict_Label_OneHot.append(torch.tensor(functional.one_hot(Output_Indexs, self.Number_Of_Classes), dtype=torch.float32).cpu().numpy()[0]) + True_Label_OneHot.append(torch.tensor(labels, dtype=torch.int).cpu().numpy()[0]) + + loss /= len(Test_Dataloader) + + True_Label_OneHot = torch.as_tensor(True_Label_OneHot, dtype=torch.int) + Predict_Label_OneHot = torch.as_tensor(Predict_Label_OneHot, dtype=torch.float32) + + accuracy = accuracy_score(True_Label, Predict_Label) + precision = precision_score(True_Label, Predict_Label, average="macro") + recall = recall_score(True_Label, Predict_Label, average="macro") + AUC = auroc(Predict_Label_OneHot, True_Label_OneHot, num_labels=self.Number_Of_Classes, task="multilabel", average="macro") + f1 = f1_score(True_Label, Predict_Label, average="macro") + + # 計算混淆矩陣 + matrix = confusion_matrix(True_Label, Predict_Label) + draw_heatmap(matrix, f"{Save_Result_File_Config['Identification_Marix_Image']}/{self.Experiment_Name}/Identification_Test_Marix_Image", f"confusion_matrix", index) # 呼叫畫出confusion matrix的function + + return True_Label, Predict_Label, loss, accuracy, precision, recall, AUC, f1 + + def Evaluate_Per_Class_Metrics(self, cnn_model, Test_Dataloader, Labels, Calculate_Tool, identification_model_path=None): + """ + Evaluate the model on the test dataloader and compute binary classification metrics for each class. + + Parameters: + - cnn_model: The trained model to evaluate. + - Test_Dataloader: DataLoader for the test dataset. + - Labels: List of class names for better readability. + - Calculate_Tool: Tool for recording metrics. + - identification_model_path: Path to the trained model weights (optional). + + Returns: + - Calculate_Tool: Updated with binary classification metrics for each class. + """ + # 載入識別模型權重(如果提供了路徑) + if identification_model_path is not None: + cnn_model.load_state_dict(torch.load(identification_model_path)) + + # 测试GPU是否可用 + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用设备: {device}") + + # 设置为评估模式 + cnn_model.eval() + + all_results = [] + all_labels = [] + # 使用PyTorch的预测方式 + with torch.no_grad(): # 不计算梯度 + for inputs, labels, _, _ in Test_Dataloader: + inputs = inputs.to(device) + labels = labels.to(device) + + outputs = cnn_model(inputs) + _, predicted = torch.max(outputs, 1) + all_results.append(predicted.cpu().numpy()) + all_labels.append(np.argmax(labels.cpu().numpy(), axis=1)) + + # 将所有批次的结果合并为一个数组 + Predict = np.concatenate(all_results) + y_test = np.concatenate(all_labels) + + print(f"预测结果: {Predict}\n") + # 计算整体评估指标 + accuracy = accuracy_score(y_test, Predict) + + # 打印整体准确率 + print(f"整体准确率 (Accuracy): {accuracy:.4f}") + + # 假设只有两个类别:0和1 + # 计算类别0的二分类评估指标(将类别0视为正类,类别1视为负类) + print(f"类别 {Labels[0]} 的二分类评估指标:") + y_binary_0 = (y_test == 0).astype(int) + predict_binary_0 = (Predict == 0).astype(int) + + # 计算二分类指标 + binary_accuracy_0 = accuracy_score(y_binary_0, predict_binary_0) + binary_precision_0 = precision_score(y_binary_0, predict_binary_0, zero_division=0) + binary_recall_0 = recall_score(y_binary_0, predict_binary_0, zero_division=0) + binary_f1_0 = f1_score(y_binary_0, predict_binary_0, zero_division=0) + + # 打印二分类指标 + print(f" 准确率 (Accuracy): {binary_accuracy_0:.4f}") + print(f" 精确率 (Precision): {binary_precision_0:.4f}") + print(f" 召回率 (Recall): {binary_recall_0:.4f}") + print(f" F1值: {binary_f1_0:.4f}\n") + + # 记录类别0的指标 + Calculate_Tool[0].Append_numbers(0, binary_accuracy_0, binary_precision_0, binary_recall_0, 0, binary_f1_0) + + # 计算类别1的二分类评估指标(将类别1视为正类,类别0视为负类) + print(f"类别 {Labels[1]} 的二分类评估指标:") + y_binary_1 = (y_test == 1).astype(int) + predict_binary_1 = (Predict == 1).astype(int) + + # 计算二分类指标 + binary_accuracy_1 = accuracy_score(y_binary_1, predict_binary_1) + binary_precision_1 = precision_score(y_binary_1, predict_binary_1, zero_division=0) + binary_recall_1 = recall_score(y_binary_1, predict_binary_1, zero_division=0) + binary_f1_1 = f1_score(y_binary_1, predict_binary_1, zero_division=0) + + # 打印二分类指标 + print(f" 准确率 (Accuracy): {binary_accuracy_1:.4f}") + print(f" 精确率 (Precision): {binary_precision_1:.4f}") + print(f" 召回率 (Recall): {binary_recall_1:.4f}") + print(f" F1值: {binary_f1_1:.4f}\n") + + # 记录类别1的指标 + Calculate_Tool[1].Append_numbers(0, binary_accuracy_1, binary_precision_1, binary_recall_1, 0, binary_f1_1) + + return Calculate_Tool + + def Calculate_Progress_And_Timing(self, inputs, Predict_Labels, Truth_Labels, Subset, loss, epoch_iterator, Start_Time): + # Calculate progress and timing + total_samples = len(Subset) + processed_samples = 0 + + processed_samples += inputs.size(0) # Use size(0) for batch size + + # Calculate progress and timing + elapsed_time = time.time() - Start_Time + iterations_per_second = processed_samples / elapsed_time if elapsed_time > 0 else 0 + eta = (total_samples - processed_samples) / iterations_per_second if iterations_per_second > 0 else 0 + time_str = f"{int(elapsed_time//60):02d}:{int(elapsed_time%60):02d}<{int(eta//60):02d}:{int(eta%60):02d}" + + # Calculate batch metrics using PSNR/SSIM loss + # 检查loss是否为张量,如果是则调用item(),否则直接使用浮点数值 + batch_loss = loss.item() if torch.is_tensor(loss) else loss + # Calculate batch accuracy + batch_accuracy = (Predict_Labels.cpu().numpy() == Truth_Labels).mean() + + # Update progress bar + epoch_iterator.set_postfix_str( + f"{processed_samples}/{total_samples} [{time_str}, {iterations_per_second:.2f}it/s, " + f"acc={batch_accuracy:.3f}, loss={batch_loss:.3f}]" + ) + + return epoch_iterator + + def Calculate_Average_Scores(self, Data_Loader, Running_Losses, All_Predict_Labels, All_Truth_Labels, Losses, Accuracies): + Merge_Function = merge() + + All_Predicts = Merge_Function.merge_data_main(All_Predict_Labels, 0, len(All_Predict_Labels)) + All_Truths = Merge_Function.merge_data_main(All_Truth_Labels, 0, len(All_Truth_Labels)) + + Running_Losses /= len(Data_Loader) + Accuracy = accuracy_score(All_Truths, All_Predicts) + + Losses.append(Running_Losses) + Accuracies.append(Accuracy) + + return Losses, Accuracies, Running_Losses, Accuracy + + def record_matrix_image(self, True_Labels, Predict_Labels, index): + '''劃出混淆矩陣(熱力圖)''' + # 計算混淆矩陣 + matrix = confusion_matrix(True_Labels, Predict_Labels) + # Confusion_Matrix_of_Two_Classification(matrix, Save_Result_File_Config["Identification_Marix_Image"], Experiment_Name, index) # 呼叫畫出confusion matrix的function + draw_heatmap(matrix, Save_Result_File_Config["Identification_Marix_Image"], self.Experiment_Name, index) # 呼叫畫出confusion matrix的function + + def record_everyTime_test_result(self, loss, accuracy, precision, recall, auc, f, indexs, model_name): + '''記錄我單次的訓練結果並將它輸出到檔案中''' + File = Process_File() + + Dataframe = pd.DataFrame( + { + "model_name" : str(model_name), + "loss" : "{:.2f}".format(loss), + "precision" : "{:.2f}%".format(precision * 100), + "recall" : "{:.2f}%".format(recall * 100), + "accuracy" : "{:.2f}%".format(accuracy * 100), + "f" : "{:.2f}%".format(f * 100), + "AUC" : "{:.2f}%".format(auc * 100) + }, index = [indexs]) + File.Save_CSV_File(Save_Result_File_Config["Identification_Every_Fold_Training_Result"], "train_result", Dataframe) + + return Dataframe \ No newline at end of file diff --git a/experiments/Training/Segmentation_Block_Training.py b/experiments/Training/Segmentation_Block_Training.py new file mode 100644 index 0000000..d9d57e2 --- /dev/null +++ b/experiments/Training/Segmentation_Block_Training.py @@ -0,0 +1,467 @@ +from tqdm import tqdm +from torch import nn +from sklearn.model_selection import KFold +from skimage import measure + +from all_models_tools.all_model_tools import call_back +from utils.Stomach_Config import Training_Config, Loading_Config, Save_Result_File_Config +from Load_process.LoadData import Loding_Data_Root +from Training_Tools.PreProcess import Training_Precesses +from ..Models.GastroSegNet_Model import GastroSegNet +from Model_Loss.Segmentation_Loss import Segmentation_Loss +from draw_tools.draw import plot_history + +import time +import torch.optim as optim +import torch +import torch.nn.functional as F +import numpy as np +import cv2 +import os + +class Segmentation_Block_Training_Step(Loding_Data_Root, Training_Precesses): + def __init__(self, Best_Model_Save_Root): + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 設置裝置,若有GPU則使用GPU,否則使用CPU + + self.Model = self.Construct_Segment_Model_CUDA() # 模型變數 + self.train_subset = None # Training Dataset 的子集 + self.val_subset = None # Validation Dataset 的子集 + self.train_loader = None # Training DataLoader 的讀檔器 + self.val_loader = None # Validation DataLoader 的讀檔器 + self.Mask = None # 遮罩變數,接收RD Net產出來的Mask + self.Grad = None # 梯度變數,後面用來執行Grad CAM + + self.model_name = Training_Config["Mask_Experiment_Name"] # 取名,使用哪個模型(可能是預處理模型/自己設計的模型) + self.epoch = Training_Config["Epoch"] # 訓練該模型的次數 + self.train_batch_size = Training_Config["Train_Batch_Size"] # 訓練模型的Batch Size + self.Experiment_Name = Training_Config["Mask_Experiment_Name"] # 取名,使用哪個模型(可能是預處理模型/自己設計的模型) + self.Best_Model_Save_Root = Best_Model_Save_Root + + # 初始化繼承物件 + Training_Precesses.__init__(self, Training_Config["Image_Size"]) + Loding_Data_Root.__init__(self, Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["Test_Data_Root"]) + + def Processing_Main(self, training_dataset, return_processed_images=False, test_dataloader=None): + Best_Model_Path = None + Best_Validation_Loss = 100000000 + # K-Fold loop + kf = KFold(n_splits=5, shuffle=True, random_state=42) + Training_Data_Lader = self.Dataloader_Sampler(training_dataset, self.train_batch_size, True) + + for fold, (train_idx, val_idx) in enumerate(kf.split(range(len(training_dataset)))): # K-Fold 交叉驗證迴圈 + print(f"\nStarting Fold {fold + 1}/5") + + # Create training and validation subsets for this fold + self.train_subset = torch.utils.data.Subset(training_dataset, train_idx) + self.val_subset = torch.utils.data.Subset(training_dataset, val_idx) + + # Wrap subsets in DataLoaders (use same batch size as original) + self.train_loader = self.Dataloader_Sampler(self.train_subset , self.train_batch_size, True) + self.val_loader = self.Dataloader_Sampler(self.val_subset, self.train_batch_size, True) + + # 模型訓練與驗證 + model_path, Training_Losses, Validation_Losses, Total_Epoch, best_val_loss = self.Training_And_Validation(fold) + Losses = [Training_Losses, Validation_Losses] + + if best_val_loss < Best_Validation_Loss: + Best_Validation_Loss = best_val_loss + Best_Model_Path = model_path + + # 將訓練結果化成圖,並將化出來的圖丟出去儲存 + plot_history(Losses, None, f"{Save_Result_File_Config['Segument_Plot_Image']}/{self.Experiment_Name}", f"train-{str(fold)}") # 將訓練結果化成圖,並將化出來的圖丟出去儲存 + + # 如果需要返回處理後的圖像(用於後續識別訓練) + if return_processed_images is not None: + # 載入最佳模型 + self.Model = self.Construct_Segment_Model_CUDA() + self.Model.load_state_dict(torch.load(Best_Model_Path)) + self.Model.eval() + + # 處理測試數據 + with torch.no_grad(): + for Input_Images, Mask_Ground_Truth_Image, Labels, File_Name, File_Classes in Training_Data_Lader: + # 使用Model_Branch處理圖像並獲取分割結果,同時傳遞文件名以保存邊界框圖像 + self.Model_Branch(Input_Images, Mask_Ground_Truth_Image, 0.0, Save_Result_File_Config["Segument_Bounding_Box_Image"], return_processed_image=True, file_names=File_Name, Classes=File_Classes) + + avg_test_loss = self.evaluate_on_test(Best_Model_Path, test_dataloader) + + return Best_Model_Path, avg_test_loss + + return Best_Model_Path + + def Training_And_Validation(self, Fold): + self.Model = self.Construct_Segment_Model_CUDA() # 模型初始化 + Optimizer = optim.SGD(self.Model.parameters(), lr=0.045, momentum=0.9, weight_decay=0.01) # 優化器初始化 + model_path, early_stopping, scheduler = call_back(self.Best_Model_Save_Root, f"fold{Fold}", Optimizer) # 防止過擬合細節函數初始化 + epoch = 0 + Training_Losses, Validation_Losses = [], [] + Training_Running_Losses, Validation_Running_Losses = 0.0, 0.0 + + # Epoch loop + for epoch in range(self.epoch): + self.Model.train() # Start training + + # Progress bar for training batches + epoch_iterator = tqdm(self.train_loader, desc=f"Fold {Fold + 1}/5, Epoch [{epoch + 1}/{self.epoch}]") + Start_Time = time.time() + + for Input_Images, Mask_Ground_Truth_Image, Labels, File_Name, File_Classes in epoch_iterator: + Optimizer.zero_grad() # 清零梯度,防止梯度累積 + + Training_Total_Losses, Training_Running_Losses = self.Model_Branch(Input_Images, Mask_Ground_Truth_Image, Training_Running_Losses) + Training_Total_Losses.backward() + + Optimizer.step() + epoch_iterator = self.Calculate_Progress_And_Timing(Input_Images, self.train_subset, Training_Total_Losses, epoch_iterator, Start_Time) + + Training_Losses, Training_Running_Losses = self.Calculate_Average_Scores(self.train_loader, Training_Running_Losses, Training_Losses) + + # Validation step + self.Model.eval() + epoch_iterator_Validation = tqdm(self.val_loader, desc=f"\tValidation-Fold {Fold + 1}/5, Epoch [{epoch + 1}/{self.epoch}]") + with torch.no_grad(): + for Input_Images, Mask_Ground_Truth_Image, Labels, File_Name, File_Classes in epoch_iterator_Validation: + Validation_Total_Losses, Validation_Running_Losses = self.Model_Branch(Input_Images, Mask_Ground_Truth_Image, Validation_Running_Losses) + + # 添加Start_Time參數 + Start_Time = time.time() + epoch_iterator_Validation = self.Calculate_Progress_And_Timing(Input_Images, self.val_subset, Validation_Total_Losses, epoch_iterator_Validation, Start_Time) + + Validation_Losses, Validation_Running_Losses = self.Calculate_Average_Scores(self.val_loader, Validation_Running_Losses, Validation_Losses) + print(f"Traini Loss: {Training_Running_Losses:.4f}, Validation Loss: {Validation_Running_Losses:.4f}\n") + + # Early stopping + early_stopping(Validation_Running_Losses, self.Model, model_path) + if early_stopping.early_stop: + print(f"Early stopping triggered in Fold {Fold + 1} at epoch {epoch + 1}") + break + + # Scheduler step + scheduler.step(Validation_Running_Losses) + + Total_Epoch = epoch + 1 + best_val_loss = early_stopping.best_loss + return model_path, Training_Losses, Validation_Losses, Total_Epoch, best_val_loss + + def Construct_Segment_Model_CUDA(self): + GaSeg = GastroSegNet() + + # 添加輸出模型摘要的功能 + print("\n==== GastroSegNet 模型摘要 ====\n") + print(f"輸入通道數: {GaSeg.encoder[0].conv[0].in_channels}") + print(f"輸出通道數: {GaSeg.final_conv.out_channels}") + + # 計算總參數量 + total_params = sum(p.numel() for p in GaSeg.parameters() if p.requires_grad) + print(f"可訓練參數總量: {total_params:,}") + + # 顯示模型結構 + print("\n模型結構:") + print(f"- 編碼器層數: {len(GaSeg.encoder)}") + print(f"- 解碼器層數: {len(GaSeg.decoder)}") + + print("\n特徵通道配置:") + features_str = ", ".join([str(GaSeg.encoder[i].conv[0].out_channels) for i in range(len(GaSeg.encoder))]) + print(f" - 編碼器特徵通道: {features_str}") + print(f" - 瓶頸層特徵通道: {GaSeg.bottleneck.conv[0].out_channels}") + + print("\n==== 摘要結束 ====\n") + + return self.Convert_Model_To_CUDA(GaSeg) + + def Convert_Model_To_CUDA(self, model): + if torch.cuda.device_count() > 1: + model = nn.DataParallel(model) + + model = model.to(self.device) + + return model + + def Model_Branch(self, Input_Images, Mask_Ground_Truth_Image, running_loss, Save_Dir = None, return_processed_image=False, file_names=None, Classes=None): + # 直接將張量移到設備上,不需要重新創建 + Input_Images.requires_grad = False + Input_Images = Input_Images.to(self.device) + Segmentation_Output = self.Model(Input_Images) + + # 如果需要返回處理後的圖像(用於推理階段) + if return_processed_image: + # 調整模型產出影像大小 + Segmentation_Output = self.Compare_Image_And_Resize_It(Segmentation_Output, Input_Images) + # 處理分割輸出,選擇候選框並將框外像素變黑,同時保存邊界框圖像 + return self.process_segmentation_output(Input_Images, Segmentation_Output, Save_Dir, save_bbox_images=True, file_names=file_names, Classes=Classes) + + Mask_Ground_Truth_Image = Mask_Ground_Truth_Image.to(self.device) + # 調整模型產出影像大小 + Segmentation_Output = self.Compare_Image_And_Resize_It(Segmentation_Output, Mask_Ground_Truth_Image) + + Losses = self.Losses(Segmentation_Output, Mask_Ground_Truth_Image) + # 計算損失(不需要手動設置requires_grad,因為損失計算會自動處理梯度) + running_loss += Losses.item() + return Losses, running_loss + + def Compare_Image_And_Resize_It(self, Image, Target): # 調整兩張影像大小到一樣 + # 檢查Target的維度 + if Target.dim() < 3: + # 如果Target是2D張量,將其擴展為4D張量 [batch_size, channels, height, width] + Target = Target.unsqueeze(0).unsqueeze(0) + elif Target.dim() == 3: + # 如果Target是3D張量,將其擴展為4D張量 [batch_size, channels, height, width] + Target = Target.unsqueeze(0) + + # 獲取目標尺寸 + target_height = Target.size(-2) # 使用倒數第二維作為高度 + target_width = Target.size(-1) # 使用倒數第一維作為寬度 + + # 調整Image大小 + Image = torch.nn.functional.interpolate(Image, size=(target_height, target_width), mode='nearest') + + # 動態調整通道維度 + if Image.size(1) != Target.size(1) and Target.dim() >= 3: + conv = torch.nn.Conv2d(Image.size(1), Target.size(1), kernel_size=1).to(self.device) + Image = conv(Image) + + return Image + + def Losses(self, Segmentation_Output_Image, Segmentation_Mask_GroundTruth_Image): + criterion = Segmentation_Loss() + Loss = criterion(Segmentation_Output_Image, Segmentation_Mask_GroundTruth_Image) + return Loss + + def Record_Average_Losses(self, DataLoader): + loss = 0.0 + losses = [] + + # Calculate average validation loss + loss /= len(DataLoader) + losses.append(loss) + + return losses + + def Calculate_Progress_And_Timing(self, inputs, Subset, loss, epoch_iterator, Start_Time): + # Calculate progress and timing + total_samples = len(Subset) + processed_samples = 0 + + processed_samples += inputs.size(0) # Use size(0) for batch size + + # Calculate progress and timing + elapsed_time = time.time() - Start_Time + iterations_per_second = processed_samples / elapsed_time if elapsed_time > 0 else 0 + eta = (total_samples - processed_samples) / iterations_per_second if iterations_per_second > 0 else 0 + time_str = f"{int(elapsed_time//60):02d}:{int(elapsed_time%60):02d}<{int(eta//60):02d}:{int(eta%60):02d}" + + # Calculate batch metrics using PSNR/SSIM loss + batch_loss = loss.item() + + # Update progress bar with PSNR/SSIM loss + epoch_iterator.set_postfix_str( + f"{processed_samples}/{total_samples} [{time_str}, {iterations_per_second:.2f}it/s, " + f"loss={batch_loss:.3f}]" + ) + + return epoch_iterator + + def Calculate_Average_Scores(self, Data_Loader, Running_Losses, Losses): + Running_Losses /= len(Data_Loader) + Losses.append(Running_Losses) + + return Losses, Running_Losses + + def process_segmentation_output(self, input_images, segmentation_output, bbox_save_dir = None, save_bbox_images=True, file_names=None, Classes = None): + """處理分割輸出,選擇候選框並將框外像素變黑,同時保存邊界框圖像 + + Args: + input_images: 原始輸入圖像 [B, C, H, W] + segmentation_output: 分割模型輸出 [B, 1, H, W] + save_bbox_images: 是否保存邊界框圖像 + file_names: 圖像文件名列表,用於保存邊界框圖像 + + Returns: + processed_images: 處理後的圖像,框外像素變黑 [B, 3, H, W] (始終確保輸出是3通道) + """ + + # 將輸出轉換為二值掩碼 (閾值為0.5) + Batch_Size_Of_Image = segmentation_output.size(0) + binary_masks = (torch.sigmoid(segmentation_output) > 0.5).float() + + # 創建一個輸出張量,確保始終是3通道 + # 獲取批次大小、高度和寬度 + Batch_Size_Of_Image, _, height, width = input_images.size() + + # 創建3通道的輸出張量,不管輸入是幾個通道,輸出都是3通道 + processed_images = torch.zeros(Batch_Size_Of_Image, 3, height, width, device=input_images.device) + + # 對批次中的每張圖像進行處理 + for Batch_Size in range(Batch_Size_Of_Image): + # 創建保存邊界框圖像的目錄 + new_bbox_save_dir = bbox_save_dir + new_bbox_save_dir = os.path.join(new_bbox_save_dir, Classes[Batch_Size]) + if save_bbox_images and not os.path.exists(new_bbox_save_dir): + os.makedirs(new_bbox_save_dir, exist_ok=True) + print(f"創建邊界框圖像保存目錄: {new_bbox_save_dir}") + + # 獲取當前圖像的二值掩碼並轉換為numpy數組 + mask = binary_masks[Batch_Size, 0].cpu().numpy().astype(np.uint8) + + # 使用連通區域分析找出所有候選區域 + labeled_mask, num_labels = measure.label(mask, return_num=True, connectivity=2) + + if num_labels > 0: + # 計算每個區域的面積 + regions = measure.regionprops(labeled_mask) + + # 根據面積排序區域(從大到小) + regions.sort(key=lambda x: x.area, reverse=True) + + # 選擇最大的區域作為最終掩碼 + if len(regions) > 0: + # 創建一個新的掩碼,只包含最大的區域 + final_mask = np.zeros_like(mask) + for coords in regions[0].coords: + final_mask[coords[0], coords[1]] = 1 + + # 獲取最大區域的邊界框 + bbox = regions[0].bbox # (min_row, min_col, max_row, max_col) + + # 將最終掩碼轉換回PyTorch張量 + final_mask_tensor = torch.from_numpy(final_mask).float().to(self.device) + + # 確保掩碼與輸入圖像的尺寸匹配 + if final_mask_tensor.shape != input_images[Batch_Size, 0].shape: + # 調整掩碼大小以匹配輸入圖像 + final_mask_tensor = torch.nn.functional.interpolate( + final_mask_tensor.unsqueeze(0).unsqueeze(0), + size=input_images[Batch_Size, 0].shape, + mode='nearest' + ).squeeze(0).squeeze(0) + + # 將掩碼應用到原始圖像上(保留框內像素,將框外像素變黑) + # 處理不同通道數的輸入圖像 + if input_images.size(1) == 1: # 單通道輸入 + # 將單通道複製到3個通道 + for Channel in range(3): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, 0] * final_mask_tensor + elif input_images.size(1) == 3: # 三通道輸入 + # 直接複製三個通道 + for Channel in range(3): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, Channel] * final_mask_tensor + else: # 其他通道數(不太可能,但為了健壯性) + # 取前三個通道或複製第一個通道 + for Channel in range(3): + if Channel < input_images.size(1): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, Channel] * final_mask_tensor + else: + processed_images[Batch_Size, Channel] = input_images[Batch_Size, 0] * final_mask_tensor + + # 保存帶有邊界框的圖像 + if save_bbox_images: + # 將輸入圖像轉換為numpy數組並調整為適合顯示的格式 + img_tensor = input_images[Batch_Size].clone().detach().cpu() + img_np = img_tensor.permute(1, 2, 0).numpy() + + # 將圖像從[0,1]範圍轉換為[0,255]範圍 + img_np = (img_np * 255).astype(np.uint8) + + # 確保圖像是連續的內存塊 + img_np = np.ascontiguousarray(img_np) + + # 如果圖像是單通道的,轉換為三通道 + if img_np.shape[2] == 1: + img_np = cv2.cvtColor(img_np, cv2.COLOR_GRAY2BGR) + elif img_np.shape[2] == 3: + # 確保是BGR格式(OpenCV默認格式) + img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR) + + # 繪製邊界框 + min_row, min_col, max_row, max_col = bbox + cv2.rectangle(img_np, (min_col, min_row), (max_col, max_row), (0, 255, 0), 2) + + # 生成保存文件名 + if file_names is not None and Batch_Size < len(file_names): + # 使用提供的文件名 + file_name = os.path.basename(file_names[Batch_Size]) + save_path = os.path.join(new_bbox_save_dir, f"bbox_{file_name}.png") + else: + # 使用索引作為文件名 + save_path = os.path.join(new_bbox_save_dir, file_names) + + # 保存圖像 (已經是BGR格式,直接保存) + cv2.imwrite(save_path, img_np) + print(f"已保存邊界框圖像: {save_path}") + else: + # 如果沒有找到區域,則保留原始圖像,但確保是3通道 + if input_images.size(1) == 1: # 單通道輸入 + # 將單通道複製到3個通道 + for Channel in range(3): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, 0] + elif input_images.size(1) == 3: # 三通道輸入 + # 直接複製三個通道 + processed_images[Batch_Size] = input_images[Batch_Size] + else: # 其他通道數 + # 取前三個通道或複製第一個通道 + for Channel in range(3): + if Channel < input_images.size(1): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, Channel] + else: + processed_images[Batch_Size, Channel] = input_images[Batch_Size, 0] + else: + # 如果沒有找到任何區域,則保留原始圖像,但確保是3通道 + if input_images.size(1) == 1: # 單通道輸入 + # 將單通道複製到3個通道 + for Channel in range(3): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, 0] + elif input_images.size(1) == 3: # 三通道輸入 + # 直接複製三個通道 + for Channel in range(3): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, Channel] + else: # 其他通道數 + # 取前三個通道或複製第一個通道 + for Channel in range(3): + if Channel < input_images.size(1): + processed_images[Batch_Size, Channel] = input_images[Batch_Size, Channel] + else: + processed_images[Batch_Size, Channel] = input_images[Batch_Size, 0] + + # 在沒有候選區域的情況下也保存處理後的圖像 + if save_bbox_images: + # 轉換為可保存的numpy格式(與上方保存邊界框圖像一致的流程) + img_tensor = processed_images[Batch_Size].clone().detach().cpu() + img_np = img_tensor.permute(1, 2, 0).numpy() + img_np = (img_np * 255).astype(np.uint8) + img_np = np.ascontiguousarray(img_np) + + # 保證三通道BGR格式 + if img_np.shape[2] == 1: + img_np = cv2.cvtColor(img_np, cv2.COLOR_GRAY2BGR) + elif img_np.shape[2] == 3: + img_np = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR) + + # 生成保存文件名(使用提供的檔名或索引),並標記為無邊界框 + if file_names is not None and isinstance(file_names, (list, tuple)) and Batch_Size < len(file_names): + file_name = os.path.basename(file_names[Batch_Size]) + save_path = os.path.join(new_bbox_save_dir, f"no_bbox_{file_name}.png") + else: + base_name = file_names if isinstance(file_names, str) and len(file_names) > 0 else f"no_bbox_{Batch_Size}.png" + save_path = os.path.join(new_bbox_save_dir, base_name) + + cv2.imwrite(save_path, img_np) + print(f"已保存無邊界框圖像: {save_path}") + + def evaluate_on_test(self, model_path, test_dataloader): + if test_dataloader is None: + raise ValueError("Test dataloader is required for evaluation.") + + self.Model = self.Construct_Segment_Model_CUDA() + self.Model.load_state_dict(torch.load(model_path)) + self.Model.eval() + Test_Losses = [] + + test_loss = 0.0 + with torch.no_grad(): + for Input_Images, Mask_Ground_Truth_Image, _, _, _ in test_dataloader: + losses, test_loss = self.Model_Branch(Input_Images, Mask_Ground_Truth_Image, test_loss) + + losses, test_loss = self.Calculate_Average_Scores(test_dataloader, test_loss, Test_Losses) + + print(f"Average Test Loss: {test_loss}") + return test_loss \ No newline at end of file diff --git a/experiments/Training/Xception_Identification_Test.py b/experiments/Training/Xception_Identification_Test.py new file mode 100644 index 0000000..da8ac59 --- /dev/null +++ b/experiments/Training/Xception_Identification_Test.py @@ -0,0 +1,478 @@ +from tqdm import tqdm +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score +from torchmetrics.functional import auroc +from torch.nn import functional +from torch import nn +import torch +from sklearn.model_selection import KFold +from torchinfo import summary +from sklearn.metrics import confusion_matrix + +from all_models_tools.all_model_tools import call_back +from Model_Loss.Loss import Entropy_Loss +# from Model_Loss.binary_cross_entropy import BinaryCrossEntropy # 三分類不需要二分類損失函數 +from merge_class.merge import merge +from draw_tools.Saliency_Map import SaliencyMap +from utils.Stomach_Config import Training_Config, Loading_Config, Save_Result_File_Config +# from experiments.Models.Xception_Model_Modification import Xception +from experiments.Models.pytorch_Model import ModifiedXception +from Load_process.LoadData import Loding_Data_Root +from Training_Tools.PreProcess import Training_Precesses +from Calculate_Process.Calculate import Calculate +from Load_process.file_processing import Process_File +from draw_tools.draw import plot_history, draw_heatmap +from model_data_processing.processing import Image_Enhance_Training_Data +from draw_tools.Grad_cam import GradCAM + +import time +import torch.optim as optim +import numpy as np +import torch +import pandas as pd +import datetime +import argparse +import os + +class Xception_Identification_Block_Training_Step(Loding_Data_Root, Training_Precesses): + def __init__(self, Experiment_Name, Best_Model_Save_Root): + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + self.Model = self.Construct_Identification_Model_CUDA() # 模型變數 + self.train_subset = None # Training Dataset 的子集 + self.val_subset = None # Validation Dataset 的子集 + self.train_loader = None # Training DataLoader 的讀檔器 + self.val_loader = None # Validation DataLoader 的讀檔器 + self.Mask = None # 遮罩變數,接收GastroSegNet產出來的Mask + self.Grad = None # 梯度變數,後面用來執行Grad CAM + + self.model_name = Training_Config["Model_Name"] # 取名,使用哪個模型(可能是預處理模型/自己設計的模型) + self.Epoch = Training_Config["Epoch"] # 訓練該模型的次數 + self.train_batch_size = Training_Config["Train_Batch_Size"] # 訓練模型的Batch Size + self.Experiment_Name = Experiment_Name + self.Number_Of_Classes = len(Loading_Config["Training_Labels"]) + self.Best_Model_Save_Root = Best_Model_Save_Root + self.Optimizer = optim.SGD(self.Model.parameters(), lr=0.045, momentum=0.9, weight_decay = Training_Config["weight_decay"]) + + # 初始化多個繼承物件 + Training_Precesses.__init__(self, Training_Config["Image_Size"]) + Loding_Data_Root.__init__(self, Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["Test_Data_Root"]) + + pass + + ''' + 主函數,用來執行模型的訓練與驗證 + ''' + def Processing_Main(self, training_dataset, Test_Dataloader=None): + # Lists to store metrics across all folds + all_fold_train_losses = [] + all_fold_val_losses = [] + all_fold_train_accuracies = [] + all_fold_val_accuracies = [] + Calculate_Process = Calculate() + File = Process_File() + Calculate_Tool = [Calculate() for i in range(3)] + Best_Model_Path = None + Best_Validation_Loss = 100000000 + + # K-Fold loop + kf = KFold(n_splits=5, shuffle=True, random_state=42) + for fold, (train_idx, val_idx) in enumerate(kf.split(range(len(training_dataset)))): # K-Fold 交叉驗證迴圈 + print(f"\nStarting Fold {fold + 1}/5") + + # Create training and validation subsets for this fold + self.train_subset = torch.utils.data.Subset(training_dataset, train_idx) + self.val_subset = torch.utils.data.Subset(training_dataset, val_idx) + + # Wrap subsets in DataLoaders (use same batch size as original) + self.train_loader = self.Dataloader_Sampler(self.train_subset , self.train_batch_size, True) + self.val_loader = self.Dataloader_Sampler(self.val_subset, self.train_batch_size, True) + + self.train_loader = Image_Enhance_Training_Data(Training_Loader = self.train_loader, Save_Root = f"{Loading_Config['Image enhance processing save root']}/{str(fold)}") + + # 模型訓練與驗證 + model_path, Train_Losses, Validation_losses, Train_Accuracies, Validation_accuracies, best_val_loss = self.Training_And_Validation(fold) + + # Store fold results + all_fold_train_losses.append(Train_Losses) + all_fold_val_losses.append(Validation_losses) + all_fold_train_accuracies.append(Train_Accuracies) + all_fold_val_accuracies.append(Validation_accuracies) + + # 确保张量在CPU上,以便可以转换为NumPy数组 + if torch.is_tensor(Train_Losses): + Train_Losses = Train_Losses.cpu().detach().numpy() + if torch.is_tensor(Validation_losses): + Validation_losses = Validation_losses.cpu().detach().numpy() + if torch.is_tensor(Train_Accuracies): + Train_Accuracies = Train_Accuracies.cpu().detach().numpy() + if torch.is_tensor(Validation_accuracies): + Validation_accuracies = Validation_accuracies.cpu().detach().numpy() + + Losses = [Train_Losses, Validation_losses] + Accuracies = [Train_Accuracies, Validation_accuracies] + plot_history(Losses, Accuracies, f"{Save_Result_File_Config['Identification_Plot_Image']}/{self.Experiment_Name}", f"train-{str(fold)}") # 將訓練結果化成圖,並將化出來的圖丟出去儲存 + + # 驗證結果 + True_Label, Predict_Label, loss, accuracy, precision, recall, f1 = self.Evaluate_Model(self.Model, Test_Dataloader, fold, model_path) + + # 紀錄該次訓練結果 + Calculate_Process.Append_numbers(loss, accuracy, precision, recall, f1) + self.record_matrix_image(True_Label, Predict_Label, fold) + print(self.record_everyTime_test_result(loss, accuracy, precision, recall, f1, fold, self.Experiment_Name)) # 紀錄當前訓練完之後的預測結果,並輸出成csv檔 + + # 使用識別模型進行各類別評估 + Calculate_Tool = self.Evaluate_Per_Class_Metrics(self.Model, Test_Dataloader, Loading_Config["Training_Labels"], Calculate_Tool, model_path) + + if best_val_loss < Best_Validation_Loss: + Best_Validation_Loss = best_val_loss + Best_Model_Path = model_path + + Calculate_Process.Calculate_Mean() + Calculate_Process.Calculate_Std() + + File.Save_CSV_File(f"../Result/Experiment_Result/{self.Experiment_Name}/Total/{str(datetime.date.today())}", f"Total_Training_Result-{fold}", Calculate_Process.Output_Style()) + + for Calculate_Every_Class in Calculate_Tool: + Calculate_Every_Class.Calculate_Mean() + Calculate_Every_Class.Calculate_Std() + + # Aggregate results across folds + avg_train_losses = np.mean([losses[-1] for losses in all_fold_train_losses]) + avg_val_losses = np.mean([losses[-1] for losses in all_fold_val_losses]) + avg_train_accuracies = np.mean([acc[-1] for acc in all_fold_train_accuracies]) + avg_val_accuracies = np.mean([acc[-1] for acc in all_fold_val_accuracies]) + + print(f"\nCross-Validation Results:") + print(f"Avg Train Loss: {avg_train_losses:.4f}, Avg Val Loss: {avg_val_losses:.4f}") + print(f"Avg Train Acc: {avg_train_accuracies:.4f}, Avg Val Acc: {avg_val_accuracies:.4f}") + + File.Save_TXT_File(content = f"\nCross-Validation Results:\nAvg Train Loss: {avg_train_losses:.4f}, Avg Val Loss: {avg_val_losses:.4f}\nAvg Train Acc: {avg_train_accuracies:.4f}, Avg Val Acc: {avg_val_accuracies:.4f}\n", Save_Root = Save_Result_File_Config["Identification_Average_Result"], File_Name = "Training_Average_Result") + + # 返回最後一個fold的模型路徑和平均指標 + return Best_Model_Path + + def Training_And_Validation(self, Fold): + ''' + 模型主要的訓練與驗證部分 + ''' + model_path, early_stopping, scheduler = call_back(self.Best_Model_Save_Root, f"fold{Fold}", self.Optimizer) + + # Lists to store metrics for this fold + train_losses = [] + Validation_losses = [] + train_accuracies = [] + Validation_accuracies = [] + + # Epoch loop + for epoch in range(self.Epoch): + self.Model.train() # Start training + Training_Loss = 0.0 + All_Predict_List, All_Label_List = [], [] + + # Progress bar for training batches + epoch_iterator = tqdm(self.train_loader, desc=f"Fold {Fold + 1}/5, Epoch [{epoch + 1}/{self.Epoch}]") + Start_Time = time.time() + + for inputs, labels, File_Name, File_Classes in epoch_iterator: + Total_Losses, Training_Loss, All_Predict_List, All_Label_List, Predict_Indexs, Truth_Indexs = self.Model_Branch( + Input_Images=inputs, + Labels=labels, + All_Predict_List=All_Predict_List, + All_Label_List=All_Label_List, + running_loss=Training_Loss, + status="Training" + ) + self.Calculate_Progress_And_Timing(inputs, Predict_Indexs, Truth_Indexs, self.train_subset, Total_Losses, epoch_iterator, Start_Time) + + train_losses, train_accuracies, Training_Loss, Train_accuracy = self.Calculate_Average_Scores(self.train_loader, Training_Loss, All_Predict_List, All_Label_List, train_losses, train_accuracies) + + # Validation step + self.Model.eval() + val_loss = 0.0 + all_val_preds = [] + all_val_labels = [] + + start_Validation_time = time.time() + epoch_iterator = tqdm(self.val_loader, desc=f"\tValidation-Fold {Fold + 1}/5, Epoch [{epoch + 1}/{self.Epoch}]") + with torch.no_grad(): + for inputs, labels, File_Name, File_Classes in epoch_iterator: + Validation_Total_Loss, val_loss, all_val_preds, all_val_labels, Predict_Indexs, Truth_Indexs = self.Model_Branch( + Input_Images=inputs, + Labels=labels, + All_Predict_List=all_val_preds, + All_Label_List=all_val_labels, + running_loss=val_loss, + status="Validation" + ) + self.Calculate_Progress_And_Timing(inputs, Predict_Indexs, Truth_Indexs, self.val_subset, Validation_Total_Loss, epoch_iterator, start_Validation_time) + + Validation_losses, Validation_accuracies, val_loss, val_accuracy = self.Calculate_Average_Scores(self.val_loader, val_loss, all_val_preds, all_val_labels, Validation_losses, Validation_accuracies) + print(f"Traini Loss: {Training_Loss:.4f}, Accuracy: {Train_accuracy:0.2f}, Validation Loss: {val_loss:.4f}, Accuracy: {val_accuracy:0.2f}\n") + + if epoch % 5 == 0: + Grad = GradCAM(self.Model, self.TargetLayer) + Grad.Processing_Main(self.val_loader, f"{Save_Result_File_Config['GradCAM_Validation_Image_Save_Root']}/{self.Experiment_Name}/fold-{str(Fold)}/{str(epoch)}") + + # # 創建SaliencyMap實例 + # saliency_map = SaliencyMap(self.Model) + # # 處理測試數據集 + # saliency_map.Processing_Main(self.val_loader, f"../Result/Saliency_Image/Validation/Saliency_Image({str(datetime.date.today())})/{self.Experiment_Name}/fold-{str(Fold)}/") + + # Early stopping + early_stopping(val_loss, self.Model, model_path) + best_val_loss = early_stopping.best_loss + if early_stopping.early_stop: + print(f"Early stopping triggered in Fold {Fold + 1} at epoch {epoch + 1}") + break + + # Learning rate adjustment + scheduler.step(val_loss) + + # 確保返回模型路徑 + return model_path, train_losses, Validation_losses, train_accuracies, Validation_accuracies, best_val_loss + + def Construct_Identification_Model_CUDA(self): + # 从Model_Config中获取输出节点数量 + Model = ModifiedXception() + print(summary(Model)) + for name, parameters in Model.named_parameters(): + print(f"Layer Name: {name}, Parameters: {parameters.size()}") + + self.TargetLayer = Model.base_model.conv4.pointwise + + # 注释掉summary调用,避免Mask参数问题 + # 直接打印模型结构 + # print(f"Model structure: {Model}") + + # # 打印模型参数和梯度状态 + # for name, parameters in Model.named_parameters(): + # print(f"Layer Name: {name}, Parameters: {parameters.size()}, requires_grad: {parameters.requires_grad}") + + return self.Convert_Model_To_CUDA(Model) + + def Convert_Model_To_CUDA(self, model): + model = nn.DataParallel(model) + model = model.to(self.device) + + return model + + def Model_Branch(self, Input_Images, Labels, All_Predict_List : list, All_Label_List : list, running_loss, status): + if status == "Training": + self.Optimizer.zero_grad() # 清零梯度,防止梯度累積 + + # 將張量移到設備上,但保持梯度計算能力 + Input_Images, Labels = Input_Images.to(self.device), Labels.to(self.device) + + Predicts_Data = self.Model(Input_Images) + + # 計算損失時使用原始的 Predict 張量和 Labels 張量(保持梯度) + Losses = self.Losses(Predicts_Data, Labels) + + if status == "Training": + Losses.backward() + self.Optimizer.step() + + running_loss += Losses.item() + + # Collect training predictions and labels (用於評估指標) + Output_Values, Output_Indexs = torch.max(Predicts_Data, dim=1) + True_Indexs = np.argmax(Labels.cpu().numpy(), axis=1) + + # # 處理標籤:如果是one-hot編碼則轉換為類別索引,否則直接使用 + # if Labels.dim() > 1 and Labels.size(1) > 1: + # True_Indexs = np.argmax(Labels.cpu().numpy(), axis=1) + # else: + # True_Indexs = Labels.cpu().numpy() + + # 將預測索引轉換為 numpy 用於評估指標 + All_Predict_List.append(Output_Indexs.cpu().numpy()) + All_Label_List.append(True_Indexs) + + return Losses, running_loss, All_Predict_List, All_Label_List, Output_Indexs, True_Indexs + + def Losses(self, Predicts, Labels): + criterion = Entropy_Loss() + Loss = criterion(Predicts, Labels) + return Loss + + def Evaluate_Model(self, cnn_model, Test_Dataloader, index, identification_model_path=None): + # 載入識別模型權重(如果提供了路徑) + if identification_model_path is not None: + cnn_model.load_state_dict(torch.load(identification_model_path)) + else: + assert identification_model_path is None, "No identification model path provided for evaluation." + + # 評估模型 + cnn_model.eval() + True_Label, Predict_Label = [], [] + True_Label_OneHot, Predict_Label_OneHot = [], [] + loss = 0.0 + + with torch.no_grad(): + for images, labels, File_Name, File_Classes in Test_Dataloader: + Total_Loss, Running_Loss, Predict_Label, True_Label, Output_Indexs, Truth_Index = self.Model_Branch( + Input_Images=images, + Labels=labels, + All_Predict_List=Predict_Label, + All_Label_List=True_Label, + running_loss=0, + status="Testing" + ) + + loss /= len(Test_Dataloader) + + True_Label_OneHot = torch.as_tensor(True_Label_OneHot, dtype=torch.int) + Predict_Label_OneHot = torch.as_tensor(Predict_Label_OneHot, dtype=torch.float32) + + accuracy = accuracy_score(True_Label, Predict_Label) + precision = precision_score(True_Label, Predict_Label, average="macro") + recall = recall_score(True_Label, Predict_Label, average="macro") + f1 = f1_score(True_Label, Predict_Label, average="macro") + + # 計算混淆矩陣 + matrix = confusion_matrix(True_Label, Predict_Label) + draw_heatmap(matrix, f"{Save_Result_File_Config['Identification_Marix_Image']}/{self.Experiment_Name}/Identification_Test_Marix_Image", f"confusion_matrix", index) # 呼叫畫出confusion matrix的function + + TargetLayer = self.Model.base_model.conv4.pointwise + Grad = GradCAM(self.Model, TargetLayer) + Grad.Processing_Main(Test_Dataloader, f"{Save_Result_File_Config['GradCAM_Test_Image_Save_Root']}/{self.Experiment_Name}/fold-{str(fold)}/") + + return True_Label, Predict_Label, loss, accuracy, precision, recall, f1 + + def Evaluate_Per_Class_Metrics(self, cnn_model, Test_Dataloader, Labels, Calculate_Tool, identification_model_path=None): + """ + Evaluate the model on the test dataloader and compute binary classification metrics for each class. + + Parameters: + - cnn_model: The trained model to evaluate. + - Test_Dataloader: DataLoader for the test dataset. + - Labels: List of class names for better readability. + - Calculate_Tool: Tool for recording metrics. + - identification_model_path: Path to the trained model weights (optional). + + Returns: + - Calculate_Tool: Updated with binary classification metrics for each class. + """ + # 載入識別模型權重(如果提供了路徑) + if identification_model_path is not None: + cnn_model.load_state_dict(torch.load(identification_model_path)) + + # 测试GPU是否可用 + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用设备: {device}") + + # 设置为评估模式 + cnn_model.eval() + + all_results = [] + all_labels = [] + # 使用PyTorch的预测方式 + with torch.no_grad(): # 不计算梯度 + for inputs, labels, _, _ in Test_Dataloader: + inputs = inputs.to(device) + labels = labels.to(device) + + outputs = cnn_model(inputs) + _, predicted = torch.max(outputs, 1) + all_results.append(predicted.cpu().numpy()) + all_labels.append(np.argmax(labels.cpu().numpy(), axis=1)) + + # 将所有批次的结果合并为一个数组 + Predict = np.concatenate(all_results) + y_test = np.concatenate(all_labels) + + print(f"预测结果: {Predict}\n") + # 计算整体评估指标 + accuracy = accuracy_score(y_test, Predict) + + # 打印整体准确率 + print(f"整体准确率 (Accuracy): {accuracy:.4f}") + + # 現在有三個類別:0, 1, 2 + # 為每個類別計算二分類評估指標(將該類別視為正類,其他類別視為負類) + for class_idx in range(3): + print(f"类别 {Labels[class_idx]} 的二分类评估指标:") + y_binary = (y_test == class_idx).astype(int) + predict_binary = (Predict == class_idx).astype(int) + + # 计算二分类指标 + binary_accuracy = accuracy_score(y_binary, predict_binary) + binary_precision = precision_score(y_binary, predict_binary, zero_division=0) + binary_recall = recall_score(y_binary, predict_binary, zero_division=0) + binary_f1 = f1_score(y_binary, predict_binary, zero_division=0) + + # 打印二分类指标 + print(f" 准确率 (Accuracy): {binary_accuracy:.4f}") + print(f" 精确率 (Precision): {binary_precision:.4f}") + print(f" 召回率 (Recall): {binary_recall:.4f}") + print(f" F1值: {binary_f1:.4f}\n") + + # 记录该类别的指标 + Calculate_Tool[class_idx].Append_numbers(0, binary_accuracy, binary_precision, binary_recall, binary_f1) + + return Calculate_Tool + + def Calculate_Progress_And_Timing(self, inputs, Predict_Labels, Truth_Labels, Subset, loss, epoch_iterator, Start_Time): + # Calculate progress and timing + total_samples = len(Subset) + processed_samples = 0 + + processed_samples += inputs.size(0) # Use size(0) for batch size + + # Calculate progress and timing + elapsed_time = time.time() - Start_Time + iterations_per_second = processed_samples / elapsed_time if elapsed_time > 0 else 0 + eta = (total_samples - processed_samples) / iterations_per_second if iterations_per_second > 0 else 0 + time_str = f"{int(elapsed_time//60):02d}:{int(elapsed_time%60):02d}<{int(eta//60):02d}:{int(eta%60):02d}" + + # Calculate batch metrics using PSNR/SSIM loss + # 检查loss是否为张量,如果是则调用item(),否则直接使用浮点数值 + batch_loss = loss.item() if torch.is_tensor(loss) else loss + # Calculate batch accuracy + batch_accuracy = (Predict_Labels.cpu().numpy() == Truth_Labels).mean() + + # Update progress bar + epoch_iterator.set_postfix_str( + f"{processed_samples}/{total_samples} [{time_str}, {iterations_per_second:.2f}it/s, " + f"acc={batch_accuracy:.3f}, loss={batch_loss:.3f}]" + ) + + return epoch_iterator + + def Calculate_Average_Scores(self, Data_Loader, Running_Losses, All_Predict_Labels, All_Truth_Labels, Losses, Accuracies): + Merge_Function = merge() + + All_Predicts = Merge_Function.merge_data_main(All_Predict_Labels, 0, len(All_Predict_Labels)) + All_Truths = Merge_Function.merge_data_main(All_Truth_Labels, 0, len(All_Truth_Labels)) + + Running_Losses /= len(Data_Loader) + Accuracy = accuracy_score(All_Truths, All_Predicts) + + Losses.append(Running_Losses) + Accuracies.append(Accuracy) + + return Losses, Accuracies, Running_Losses, Accuracy + + def record_matrix_image(self, True_Labels, Predict_Labels, index): + '''劃出混淆矩陣(熱力圖)''' + # 計算混淆矩陣 + matrix = confusion_matrix(True_Labels, Predict_Labels) + # Confusion_Matrix_of_Two_Classification(matrix, Save_Result_File_Config["Identification_Marix_Image"], Experiment_Name, index) # 呼叫畫出confusion matrix的function + draw_heatmap(matrix, Save_Result_File_Config["Identification_Marix_Image"], self.Experiment_Name, index) # 呼叫畫出confusion matrix的function + + def record_everyTime_test_result(self, loss, accuracy, precision, recall, f, indexs, model_name): + '''記錄我單次的訓練結果並將它輸出到檔案中''' + File = Process_File() + + Dataframe = pd.DataFrame( + { + "model_name" : str(model_name), + "loss" : "{:.2f}".format(loss), + "precision" : "{:.2f}%".format(precision * 100), + "recall" : "{:.2f}%".format(recall * 100), + "accuracy" : "{:.2f}%".format(accuracy * 100), + "f" : "{:.2f}%".format(f * 100), + }, index = [indexs]) + File.Save_CSV_File(Save_Result_File_Config["Identification_Every_Fold_Training_Result"], "train_result", Dataframe) + + return Dataframe \ No newline at end of file diff --git a/experiments/Training/__init__.py b/experiments/Training/__init__.py new file mode 100644 index 0000000..13fa926 --- /dev/null +++ b/experiments/Training/__init__.py @@ -0,0 +1,2 @@ +from ..Models.GastroSegNet_Model import GastroSegNet +from ..Models.Xception_Model_Modification import Xception \ No newline at end of file diff --git a/experiments/Training/__pycache__/Identification_Block_Training.cpython-311.pyc b/experiments/Training/__pycache__/Identification_Block_Training.cpython-311.pyc new file mode 100644 index 0000000..871cc33 Binary files /dev/null and b/experiments/Training/__pycache__/Identification_Block_Training.cpython-311.pyc differ diff --git a/experiments/Training/__pycache__/Identification_Block_Training.cpython-313.pyc b/experiments/Training/__pycache__/Identification_Block_Training.cpython-313.pyc new file mode 100644 index 0000000..3d0c931 Binary files /dev/null and b/experiments/Training/__pycache__/Identification_Block_Training.cpython-313.pyc differ diff --git a/experiments/Training/__pycache__/Segmentation_Block_Training.cpython-311.pyc b/experiments/Training/__pycache__/Segmentation_Block_Training.cpython-311.pyc new file mode 100644 index 0000000..458c6e4 Binary files /dev/null and b/experiments/Training/__pycache__/Segmentation_Block_Training.cpython-311.pyc differ diff --git a/experiments/Training/__pycache__/Segmentation_Block_Training.cpython-313.pyc b/experiments/Training/__pycache__/Segmentation_Block_Training.cpython-313.pyc new file mode 100644 index 0000000..812a27e Binary files /dev/null and b/experiments/Training/__pycache__/Segmentation_Block_Training.cpython-313.pyc differ diff --git a/experiments/Training/__pycache__/Xception_Identification_Test.cpython-313.pyc b/experiments/Training/__pycache__/Xception_Identification_Test.cpython-313.pyc new file mode 100644 index 0000000..bd6455e Binary files /dev/null and b/experiments/Training/__pycache__/Xception_Identification_Test.cpython-313.pyc differ diff --git a/experiments/Training/__pycache__/__init__.cpython-311.pyc b/experiments/Training/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..2445c5f Binary files /dev/null and b/experiments/Training/__pycache__/__init__.cpython-311.pyc differ diff --git a/experiments/Training/__pycache__/__init__.cpython-313.pyc b/experiments/Training/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..137f410 Binary files /dev/null and b/experiments/Training/__pycache__/__init__.cpython-313.pyc differ diff --git a/experiments/__pycache__/Model_All_Step.cpython-311.pyc b/experiments/__pycache__/Model_All_Step.cpython-311.pyc index 2f0ba02..44c7c71 100644 Binary files a/experiments/__pycache__/Model_All_Step.cpython-311.pyc and b/experiments/__pycache__/Model_All_Step.cpython-311.pyc differ diff --git a/experiments/__pycache__/Model_All_Step.cpython-312.pyc b/experiments/__pycache__/Model_All_Step.cpython-312.pyc new file mode 100644 index 0000000..6dabc5e Binary files /dev/null and b/experiments/__pycache__/Model_All_Step.cpython-312.pyc differ diff --git a/experiments/__pycache__/__init__.cpython-311.pyc b/experiments/__pycache__/__init__.cpython-311.pyc index d2d2096..0d94587 100644 Binary files a/experiments/__pycache__/__init__.cpython-311.pyc and b/experiments/__pycache__/__init__.cpython-311.pyc differ diff --git a/experiments/__pycache__/__init__.cpython-312.pyc b/experiments/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c5515cf Binary files /dev/null and b/experiments/__pycache__/__init__.cpython-312.pyc differ diff --git a/experiments/__pycache__/__init__.cpython-313.pyc b/experiments/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..5f5b3a8 Binary files /dev/null and b/experiments/__pycache__/__init__.cpython-313.pyc differ diff --git a/experiments/__pycache__/experiment.cpython-311.pyc b/experiments/__pycache__/experiment.cpython-311.pyc index 722d63f..cfc9efd 100644 Binary files a/experiments/__pycache__/experiment.cpython-311.pyc and b/experiments/__pycache__/experiment.cpython-311.pyc differ diff --git a/experiments/__pycache__/experiment.cpython-312.pyc b/experiments/__pycache__/experiment.cpython-312.pyc new file mode 100644 index 0000000..82deb95 Binary files /dev/null and b/experiments/__pycache__/experiment.cpython-312.pyc differ diff --git a/experiments/__pycache__/experiment.cpython-313.pyc b/experiments/__pycache__/experiment.cpython-313.pyc new file mode 100644 index 0000000..231dd8c Binary files /dev/null and b/experiments/__pycache__/experiment.cpython-313.pyc differ diff --git a/experiments/__pycache__/model_evaluation.cpython-313.pyc b/experiments/__pycache__/model_evaluation.cpython-313.pyc new file mode 100644 index 0000000..a2d8520 Binary files /dev/null and b/experiments/__pycache__/model_evaluation.cpython-313.pyc differ diff --git a/experiments/__pycache__/pytorch_Model.cpython-311.pyc b/experiments/__pycache__/pytorch_Model.cpython-311.pyc index e3c4de1..6214541 100644 Binary files a/experiments/__pycache__/pytorch_Model.cpython-311.pyc and b/experiments/__pycache__/pytorch_Model.cpython-311.pyc differ diff --git a/experiments/__pycache__/pytorch_Model.cpython-312.pyc b/experiments/__pycache__/pytorch_Model.cpython-312.pyc new file mode 100644 index 0000000..0845c42 Binary files /dev/null and b/experiments/__pycache__/pytorch_Model.cpython-312.pyc differ diff --git a/experiments/experiment.py b/experiments/experiment.py index 878d080..522b78f 100644 --- a/experiments/experiment.py +++ b/experiments/experiment.py @@ -1,26 +1,22 @@ -from torchinfo import summary -from sklearn.model_selection import KFold -from sklearn.metrics import confusion_matrix - from Training_Tools.PreProcess import Training_Precesses -from experiments.pytorch_Model import ModifiedXception -from experiments.Model_All_Step import All_Step from Load_process.Load_Indepentend import Load_Indepentend_Data from _validation.ValidationTheEnterData import validation_the_enter_data -from Load_process.file_processing import Process_File -from draw_tools.Grad_cam import GradCAM -from draw_tools.draw import plot_history, draw_heatmap -from Calculate_Process.Calculate import Calculate +from utils.Stomach_Config import Training_Config, Loading_Config, Save_Result_File_Config +# from experiments.Training.Identification_Block_Training import Identification_Block_Training_Step +# from experiments.Training.Segmentation_Block_Training import Segmentation_Block_Training_Step +from experiments.Training.Xception_Identification_Test import Xception_Identification_Block_Training_Step +from Load_process.LoadData import Loding_Data_Root +from Load_process.LoadData import Load_Data_Prepare +from model_data_processing.processing import make_label_list +from merge_class.merge import merge +from Training_Tools.Tools import Tool -import numpy as np import torch -import torch.nn as nn import time -import pandas as pd -import datetime +import numpy as np class experiments(): - def __init__(self, Image_Size, Model_Name, Experiment_Name, Epoch, Train_Batch_Size, tools, Number_Of_Classes, status): + def __init__(self, Xception_Training_Data, Xception_Training_Label, Xception_Training_Mask_Data, status): ''' # 實驗物件 @@ -28,7 +24,6 @@ class experiments(): * 用於開始訓練pytorch的物件,裡面分為數個方法,負責處理實驗過程的種種 ## parmeter: - * Topic_Tool: 讀取訓練、驗證、測試的資料集與Label等等的內容 * cut_image: 呼叫切割影像物件 * merge: 合併的物件 * model_name: 模型名稱,告訴我我是用哪個模型(可能是預處理模型/自己設計的模型) @@ -48,155 +43,334 @@ class experiments(): * record_matrix_image: 劃出混淆矩陣(熱力圖) * record_everyTime_test_result: 記錄我單次的訓練結果並將它輸出到檔案中 ''' + self.model_name = Training_Config["Model_Name"] # 取名,告訴我我是用哪個模型(可能是預處理模型/自己設計的模型) + self.epoch = Training_Config["Epoch"] + self.train_batch_size = Training_Config["Train_Batch_Size"] + self.Image_Size = Training_Config["Image_Size"] - self.Topic_Tool = tools - - self.validation_obj = validation_the_enter_data() # 呼叫驗證物件 - self.cut_image = Load_Indepentend_Data(self.Topic_Tool.Get_Data_Label(), self.Topic_Tool.Get_OneHot_Encording_Label()) # 呼叫切割影像物件 - - self.model_name = Model_Name # 取名,告訴我我是用哪個模型(可能是預處理模型/自己設計的模型) - self.experiment_name = Experiment_Name - self.epoch = Epoch - self.train_batch_size = Train_Batch_Size - self.Number_Of_Classes = Number_Of_Classes - self.Image_Size = Image_Size - - self.Grad = "" - self.Status = status + self.Xception_Training_Data = Xception_Training_Data + self.Xception_Training_Label = Xception_Training_Label + self.Xception_Training_Mask_Data = Xception_Training_Mask_Data + self.Grad = None + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + self.validation_obj = validation_the_enter_data() # 呼叫驗證物件 pass - def processing_main(self, Training_Data, Training_Label): - Train, Test = self.Topic_Tool.Get_Save_Roots(self.Status) - Calculate_Process = Calculate() + def processing_main(self): + print(f"Testing Data Prepring!!!!") + tool = Tool() + Merge = merge() + Prepare = Load_Data_Prepare() - print(f"Training Data Content: {Training_Data[0]}") + # 取得One-hot encording 的資料 + tool.Set_OneHotEncording(Loading_Config["Training_Labels"]) + Encording_Label = tool.Get_OneHot_Encording_Label() + Label_Length = len(Loading_Config["Training_Labels"]) start = time.time() - self.cut_image.process_main(Test) # 呼叫處理test Data與Validation Data的function + # self.cut_image.process_main(Loading_Config["Test_Data_Root"], Loading_Config["Annotation_Testing_Root"]) # 呼叫處理test Data與Validation Data的function + + tmp_load = Loding_Data_Root(Loading_Config["Training_Labels"], Loading_Config['Test_Data_Root'], None) + Data_Dict_Data = tmp_load.process_main(False) + + Total_Size_List = [] + Train_Size = 0 + print("前處理後資料集總數") + for label in Loading_Config["Training_Labels"]: + Train_Size += len(Data_Dict_Data[label]) + Total_Size_List.append(len(Data_Dict_Data[label])) + print(f"Labels: {label}, 總數為: {len(Data_Dict_Data[label])}") + + print("總共有 " + str(Train_Size) + " 筆資料") + + # 做出跟資料相同數量的Label + Classes = [] + i = 0 + for encording in Encording_Label: + Classes.append(make_label_list(Total_Size_List[i], encording)) + i += 1 + + # 將資料做成Dict的資料型態 + Prepare.Set_Final_Dict_Data(Loading_Config["Training_Labels"], Data_Dict_Data, Classes, Label_Length) + Final_Dict_Data = Prepare.Get_Final_Data_Dict() + keys = list(Final_Dict_Data.keys()) + + Testing_Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list + for i in range(2, Label_Length): + Testing_Data = Merge.merge_all_image_data(Testing_Data, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + Testing_Label = Merge.merge_all_image_data(Final_Dict_Data[keys[Label_Length]], Final_Dict_Data[keys[Label_Length + 1]]) #將訓練資料的label合併成一個label的list + for i in range(Label_Length + 2, 2 * Label_Length): + Testing_Label = Merge.merge_all_image_data(Testing_Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + end = time.time() print("讀取testing與validation資料(154)執行時間:%f 秒\n" % (end - start)) # 將處理好的test Data 與 Validation Data 丟給這個物件的變數 - self.test, self.test_label = self.cut_image.test, self.cut_image.test_label - # self.test = self.test.permute(0, 3, 1, 2) - # Training_Data = Training_Data.permute(0, 3, 1, 2) + print("Testing Data is Prepared finish!!!!") PreProcess = Training_Precesses(self.Image_Size) - File = Process_File() - # print(f"Dataset_Data: \n{self.test}\nLabel: \n{self.test_label}\n") - Testing_Dataset = PreProcess.Setting_DataSet(self.test, self.test_label, "Transform") - self.Test_Dataloader = PreProcess.Dataloader_Sampler(Testing_Dataset, 1, False) - # for images, labels in self.Test_Dataloader: - # print(images.shape) + Test_Dataset = PreProcess.Setting_DataSet( + Testing_Data, + Testing_Label, + None, + "Transform" + ) + self.Test_Dataloader = PreProcess.Dataloader_Sampler(Test_Dataset, 1, False) - # Lists to store metrics across all folds - all_fold_train_losses = [] - all_fold_val_losses = [] - all_fold_train_accuracies = [] - all_fold_val_accuracies = [] + # 創建Normal vs Others的訓練數據集 + Training_Data = PreProcess.Setting_DataSet( + self.Xception_Training_Data, + self.Xception_Training_Label, + None, + "Transform" + ) - # Define K-fold cross-validator - K_Fold = KFold(n_splits = 5, shuffle = True, random_state = 42) - # Get the underlying dataset from PreProcess_Classes_Data - training_dataset = PreProcess.Setting_DataSet(Training_Data, Training_Label, "Transform") - # K-Fold loop - for fold, (train_idx, val_idx) in enumerate(K_Fold.split(training_dataset)): + print("Training is started!!\n") - cnn_model = self.construct_model() # 呼叫讀取模型的function - print(summary(cnn_model, input_size=(int(self.train_batch_size / 2), 3, self.Image_Size, self.Image_Size))) - for name, parameters in cnn_model.named_parameters(): - print(f"Layer Name: {name}, Parameters: {parameters.size()}") + # 創建訓練步驟物件 + identification_Normal_step = Xception_Identification_Block_Training_Step(Training_Config["Three_Classes_Experiment_Name"], Save_Result_File_Config["Three_Classes_Identification_Best_Model"]) + # 訓練Normal vs Others分類模型 + Best_Model_Path = identification_Normal_step.Processing_Main(Training_Data, self.Test_Dataloader) - TargetLayer = cnn_model.base_model.conv4.pointwise - Grad = GradCAM(cnn_model, TargetLayer) - - step = All_Step(cnn_model, self.epoch, self.Number_Of_Classes, self.model_name, self.experiment_name) - print("\n\n\n讀取訓練資料執行時間:%f 秒\n\n" % (end - start)) - print(f"\nStarting Fold {fold + 1}/5") - - # Create training and validation subsets for this fold - train_subset = torch.utils.data.Subset(training_dataset, train_idx) - val_subset = torch.utils.data.Subset(training_dataset, val_idx) - - # print(f"Dataset_Data: \n{train_subset.dataset.data}\nLabel: \n{train_subset.dataset.labels}\n") - - # Wrap subsets in DataLoaders (use same batch size as original) - train_loader = PreProcess.Dataloader_Sampler(train_subset , self.train_batch_size, False) - val_loader = PreProcess.Dataloader_Sampler(val_subset, self.train_batch_size, False) - - cnn_model, model_path, train_losses, val_losses, train_accuracies, val_accuracies, Total_Epoch = step.Training_Step(train_subset, val_subset, train_loader, val_loader, self.model_name, fold, TargetLayer) - - # Store fold results - all_fold_train_losses.append(train_losses) - all_fold_val_losses.append(val_losses) - all_fold_train_accuracies.append(train_accuracies) - all_fold_val_accuracies.append(val_accuracies) - - Losses = [train_losses, val_losses] - Accuracies = [train_accuracies, val_accuracies] - plot_history(Total_Epoch, Losses, Accuracies, "train" + str(fold), self.experiment_name) # 將訓練結果化成圖,並將化出來的圖丟出去儲存 - - cnn_model.load_state_dict(torch.load(model_path)) - True_Label, Predict_Label, loss, accuracy, precision, recall, AUC, f1 = step.Evaluate_Model(cnn_model, self.Test_Dataloader) - - Grad.Processing_Main(self.Test_Dataloader, f"../Result/GradCAM_Image/Testing/GradCAM_Image({str(datetime.date.today())})/fold-{str(fold)}") - Calculate_Process.Append_numbers(loss, accuracy, precision, recall, AUC, f1) - - self.record_matrix_image(True_Label, Predict_Label, self.experiment_name, fold) - print(self.record_everyTime_test_result(loss, accuracy, precision, recall, AUC, f1, fold, self.experiment_name)) # 紀錄當前訓練完之後的預測結果,並輸出成csv檔 + # # 分類正常跟其他資料集的測試資料 + # Normal_And_Other_Test_Data = self.cut_image.test.copy() + # normal_vs_others_Test_labels = [] - # Aggregate results across folds - avg_train_losses = np.mean([losses[-1] for losses in all_fold_train_losses]) - avg_val_losses = np.mean([losses[-1] for losses in all_fold_val_losses]) - avg_train_accuracies = np.mean([acc[-1] for acc in all_fold_train_accuracies]) - avg_val_accuracies = np.mean([acc[-1] for acc in all_fold_val_accuracies]) + # # 將標籤轉換為二分類:Normal(1) vs Others(0) + # for label in self.cut_image.test_label: + # if np.argmax(label) == 1: # Normal_Crop + # # Normal類別標籤為[0, 1] + # normal_vs_others_Test_labels.append(np.array([0, 1])) + # else: + # # 其他類別標籤為[1, 0] + # normal_vs_others_Test_labels.append(np.array([1, 0])) + + # # 創建Normal vs Others的測試數據集 + # normal_vs_others_test_dataset = PreProcess.Setting_DataSet( + # Normal_And_Other_Test_Data, + # normal_vs_others_Test_labels, + # None, + # "Transform" + # ) + # normal_vs_others_test_dataloader = PreProcess.Dataloader_Sampler(normal_vs_others_test_dataset, 1, False) - print(f"\nCross-Validation Results:") - print(f"Avg Train Loss: {avg_train_losses:.4f}, Avg Val Loss: {avg_val_losses:.4f}") - print(f"Avg Train Acc: {avg_train_accuracies:.4f}, Avg Val Acc: {avg_val_accuracies:.4f}") + # # ========================================================================================================================================================================================= - Calculate_Process.Calculate_Mean() - Calculate_Process.Calculate_Std() + # # 分類分割模型的測試資料 + # # 使用CA資料和Have_Question資料訓練分割模型 + # ca_have_question_test_data = [] + # ca_have_question_test_labels = [] + + # # 篩選CA和Have_Question資料 + # for i, label in enumerate(self.cut_image.test_label): + # # 檢查是否為CA或Have_Question類別 + # if np.argmax(label) == 0 or np.argmax(label) == 2: # stomach_cancer_Crop或Have_Question_Crop + # ca_have_question_test_data.append(self.cut_image.test[i]) + # ca_have_question_test_labels.append(self.cut_image.test_label[i]) - print(Calculate_Process.Output_Style()) + # print(f"CA and Have_Question Test Data Count: {len(ca_have_question_test_data)}") + # print(f"CA and Have_Question Test Mask Count: {len(self.cut_image.test_mask)}") + + # # 創建CA和Have_Question的訓練數據集 + # segumentation_test_dataset = PreProcess.Setting_DataSet( + # ca_have_question_test_data, + # ca_have_question_test_labels, + # self.cut_image.test_mask, + # "Transform" + # ) + # Segumentation_test_dataloader = PreProcess.Dataloader_Sampler(segumentation_test_dataset, 1, False) - File.Save_TXT_File(content = f"\nCross-Validation Results:\nAvg Train Loss: {avg_train_losses:.4f}, Avg Val Loss: {avg_val_losses:.4f}\nAvg Train Acc: {avg_train_accuracies:.4f}, Avg Val Acc: {avg_val_accuracies:.4f}\n", File_Name = "Training_Average_Result") + # # ========================================================================================================================================================================================= - pass + # # 非胃癌有病與胃癌資料的分類測試資料 + # # 準備CA vs Have_Question的訓練數據 + # ca_vs_have_question_test_data = [] + # ca_vs_have_question_test_labels = [] + + # # 篩選CA和Have_Question資料 + # for i, label in enumerate(self.cut_image.test_label): + # if np.argmax(label) == 0: # stomach_cancer_Crop + # ca_vs_have_question_test_data.append(self.cut_image.test[i]) + # ca_vs_have_question_test_labels.append(np.array([1, 0])) # CA類別標籤為[1, 0] + # elif np.argmax(label) == 2: # Have_Question_Crop + # ca_vs_have_question_test_data.append(self.cut_image.test[i]) + # ca_vs_have_question_test_labels.append(np.array([0, 1])) # Have_Question類別標籤為[0, 1] + + # # 創建CA vs Have_Question的測試數據集 + # ca_vs_have_question_test_dataset = PreProcess.Setting_DataSet( + # ca_vs_have_question_test_data, + # ca_vs_have_question_test_labels, + # None, + # "Transform" + # ) + # ca_vs_have_question_test_dataloader = PreProcess.Dataloader_Sampler(ca_vs_have_question_test_dataset, 1, False) - def construct_model(self): - '''決定我這次訓練要用哪個model''' - cnn_model = ModifiedXception(self.Number_Of_Classes) + # # ========================================================================================================================================================================================= - if torch.cuda.device_count() > 1: - cnn_model = nn.DataParallel(cnn_model) + # 建立最終測試資料(不含遮罩) + # Testing_Data, Testing_Label = self.cut_image.test.copy(), self.cut_image.test_label.copy() + # Test_Dataset = PreProcess.Setting_DataSet( + # Testing_Data, + # Testing_Label, + # None, + # "Transform" + # ) + # self.Test_Dataloader = PreProcess.Dataloader_Sampler(Test_Dataset, 1, False) - cnn_model = cnn_model.to(self.device) - return cnn_model - - def record_matrix_image(self, True_Labels, Predict_Labels, model_name, index): - '''劃出混淆矩陣(熱力圖)''' - # 計算混淆矩陣 - matrix = confusion_matrix(True_Labels, Predict_Labels) - draw_heatmap(matrix, model_name, index) # 呼叫畫出confusion matrix的function - - def record_everyTime_test_result(self, loss, accuracy, precision, recall, auc, f, indexs, model_name): - '''記錄我單次的訓練結果並將它輸出到檔案中''' - File = Process_File() + # ========================================================================================================================================================================================= - Dataframe = pd.DataFrame( - { - "model_name" : str(model_name), - "loss" : "{:.2f}".format(loss), - "precision" : "{:.2f}%".format(precision * 100), - "recall" : "{:.2f}%".format(recall * 100), - "accuracy" : "{:.2f}%".format(accuracy * 100), - "f" : "{:.2f}%".format(f * 100), - "AUC" : "{:.2f}%".format(auc * 100) - }, index = [indexs]) - File.Save_CSV_File("train_result", Dataframe) + # identification_Normal_step = Identification_Block_Training_Step(Training_Config["Normal_Experiment_Name"], Save_Result_File_Config["Normal_Identification_Best_Model"]) + # identification_CA_step = Identification_Block_Training_Step(Training_Config["CA_Experiment_Name"], Save_Result_File_Config["CA_Identification_Best_Model"]) + # segmentation_step = Segmentation_Block_Training_Step(Save_Result_File_Config["Segmentation_Best_Model"]) - return Dataframe \ No newline at end of file + # print("\n=== 第一階段:訓練正常資料分類模型 ===\n") + # # 第一組:訓練Normal資料和其他資料的分類模型 + # print("\n--- Normal vs Others分類模型 ---\n") + + # # 準備Normal vs Others的訓練數據 + # # 分類testing的資料 + # normal_vs_others_data = self.Xception_Training_Data.copy() + # normal_vs_others_labels = [] + + # # 將標籤轉換為二分類:Normal(1) vs Others(0) + # for label in self.Xception_Training_Label: + # if np.argmax(label) == 1: # Normal_Crop + # # Normal類別標籤為[0, 1] + # normal_vs_others_labels.append(np.array([0, 1])) + # else: + # # 其他類別標籤為[1, 0] + # normal_vs_others_labels.append(np.array([1, 0])) + + # # 創建Normal vs Others的訓練數據集 + # normal_vs_others_dataset = PreProcess.Setting_DataSet( + # normal_vs_others_data, + # normal_vs_others_labels, + # None, + # "Transform" + # ) + + # # 訓練Normal vs Others分類模型 + # Best_Normal_Model_Path, Normal_Calculate_Process, Normal_Calculate_Tool = identification_Normal_step.Processing_Main(normal_vs_others_dataset, normal_vs_others_test_dataloader) + + + # # 訓練流程:先訓練分割模型,再訓練分類模型 + # print("\n=== 第二階段:訓練分割模型 ===\n") + # # 使用CA資料和Have_Question資料訓練分割模型 + # ca_have_question_data = [] + # ca_have_question_labels = [] + + # # 篩選CA和Have_Question資料 + # for i, label in enumerate(self.Xception_Training_Label): + # # 檢查是否為CA或Have_Question類別 + # if np.argmax(label) == 0 or np.argmax(label) == 2: # stomach_cancer_Crop或Have_Question_Crop + # ca_have_question_data.append(self.Xception_Training_Data[i]) + # ca_have_question_labels.append(self.Xception_Training_Label[i]) + + # # 創建CA和Have_Question的訓練數據集 + # ca_have_question_dataset = PreProcess.Setting_DataSet( + # ca_have_question_data, + # ca_have_question_labels, + # self.Xception_Training_Mask_Data, + # "Transform" + # ) + + # # 執行分割模型訓練,並獲取處理後的圖像 + # segmentation_best_model_path, avg_test_loss = segmentation_step.Processing_Main( + # ca_have_question_dataset, + # return_processed_images=True, + # test_dataloader=Segumentation_test_dataloader + # ) + + # print(f"分割模型訓練完成,模型路徑: {segmentation_best_model_path}") + + # # 將處理後的圖像保存起來,用於後續分析或可視化 + # # 這裡可以添加保存處理後圖像的代碼,例如使用torchvision.utils.save_image + + # print("\n=== 第三階段:訓練CA資料分類模型 ===\n") + # # 第二組:訓練CA資料和Have_Question資料的分類模型 + # print("\n--- 訓練CA vs Have_Question分類模型 ---\n") + + # Load = Loding_Data_Root(Loading_Config["XML_Loading_Label"], Save_Result_File_Config["Segument_Bounding_Box_Image"], None) + # CA_Laod_Data_Dict = Load.process_main(False) + + # Total_Size_List = [] + # Train_Size = 0 + + # print("前處理後資料集總數") + # for label in Loading_Config["XML_Loading_Label"]: + # Train_Size += len(CA_Laod_Data_Dict[label]) + # Total_Size_List.append(len(CA_Laod_Data_Dict[label])) + # print(f"Labels: {label}, 總數為: {len(CA_Laod_Data_Dict[label])}") + + # print("總共有 " + str(Train_Size) + " 筆資料") + + # # 做出跟資料相同數量的Label + # Classes = [] + # Encording_Label = np.array([[1, 0], [0, 1]]) + # i = 0 + # for encording in Encording_Label: + # Classes.append(make_label_list(Total_Size_List[i], encording)) + # i += 1 + + # # 將資料做成Dict的資料型態 + # Prepare = Load_Data_Prepare() + # Merge = merge() + # Label_Length = len(Loading_Config["XML_Loading_Label"]) + + # Prepare.Set_Final_Dict_Data(Loading_Config["XML_Loading_Label"], CA_Laod_Data_Dict, Classes, Label_Length) + # Final_Dict_Data = Prepare.Get_Final_Data_Dict() + # keys = list(Final_Dict_Data.keys()) + + # Training_Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list + # for i in range(2, Label_Length): + # Training_Data = Merge.merge_all_image_data(Training_Data, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + # Training_Label = Merge.merge_all_image_data(Final_Dict_Data[keys[Label_Length]], Final_Dict_Data[keys[Label_Length + 1]]) #將訓練資料的label合併成一個label的list + # for i in range(Label_Length + 2, 2 * Label_Length): + # Training_Label = Merge.merge_all_image_data(Training_Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + # # 創建CA vs Have_Question的訓練數據集 + # ca_vs_have_question_dataset = PreProcess.Setting_DataSet( + # Training_Data, + # Training_Label, + # None, + # "Transform" + # ) + + # # 訓練CA vs Have_Question分類模型 + # Best_CA_Model_Path, CA_Calculate_Process, CA_Calculate_Tool = identification_CA_step.Processing_Main(ca_vs_have_question_dataset, ca_vs_have_question_test_dataloader) + + # # 顯示訓練完成的指標平均值 + # print("\n=== Normal and another的指標平均值 ===\n") + # print(f"Normal and another identification result is \n {Normal_Calculate_Process.Output_Style()}\n") + + # print("\n=== Normal and another各類別的指標平均值 ===\n") + # for Calculate_Every_Class in Normal_Calculate_Tool: + # print(f"\nNormal and another identification result is \n {Calculate_Every_Class.Output_Style()}\n") + # print("\n\n") + + # # 顯示訓練完成的指標平均值 + # print("\n=== CA and Have Question的指標平均值 ===\n") + # print(f"CA and Have Question identification result is \n {CA_Calculate_Process.Output_Style()}\n") + + # print("\n=== CA and Have Question各類別的指標平均值 ===\n") + # for Calculate_Every_Class in CA_Calculate_Tool: + # print(f"\nCA vs Have_Question identification result is \n {Calculate_Every_Class.Output_Style()}\n") + # print("\n") + + # evaluator = ModelEvaluator(Save_Result_File_Config["Normal_Identification_Best_Model"], Save_Result_File_Config["CA_Identification_Best_Model"]) + # metrics, results_dir = evaluator.run_evaluation() + + # if metrics: + # print(f"\n最終結果摘要:") + # print(f"準確率: {metrics['accuracy']:.4f}") + # print(f"精確率: {metrics['precision_macro']:.4f}") + # print(f"召回率: {metrics['recall_macro']:.4f}") + # print(f"F1分數: {metrics['f1_macro']:.4f}") + # print(f"結果保存位置: {results_dir}") + + # return Best_Normal_Model_Path, Best_CA_Model_Path, segmentation_best_model_path \ No newline at end of file diff --git a/experiments/model_evaluation.py b/experiments/model_evaluation.py new file mode 100644 index 0000000..f8abad4 --- /dev/null +++ b/experiments/model_evaluation.py @@ -0,0 +1,371 @@ +""" +模型評估程式 +用於載入最佳的分類模型權重和分割模型權重, +對三類資料(胃癌、正常、非胃癌有病)計算整體的Precision、Recall、Accuracy、F1-Score +""" + +import torch +import torch.nn as nn +import numpy as np +import os +import glob +from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix, classification_report +from tqdm import tqdm +import pandas as pd +from datetime import datetime + +# 導入必要的模組 +from experiments.Models.Xception_Model_Modification import Xception +from experiments.Models.GastroSegNet_Model import GastroSegNet +from Load_process.LoadData import Loding_Data_Root +from Training_Tools.PreProcess import Training_Precesses +from Load_process.LoadData import Load_Data_Prepare +from utils.Stomach_Config import Training_Config, Loading_Config +from model_data_processing.processing import make_label_list +from Training_Tools.Tools import Tool +from merge_class.merge import merge + +class ModelEvaluator: + def __init__(self, Normal_Model_Path, CA_Model_Path): + self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用設備: {self.device}") + + # 初始化數據加載器 + self.data_loader = Loding_Data_Root( + Loading_Config["Training_Labels"], + Loading_Config["Test_Data_Root"], + Loading_Config["Test_Data_Root"] + ) + + # 初始化預處理器 + self.preprocessor = Training_Precesses(Training_Config["Image_Size"]) + + # 類別標籤映射 + self.class_labels = { + 0: "胃癌 (stomach_cancer_Crop)", + 1: "正常 (Normal_Crop)", + 2: "非胃癌有病 (Have_Question_Crop)" + } + + # 直接指定的模型路徑 + self.normal_model_path = Normal_Model_Path + self.ca_model_path = CA_Model_Path + + def load_classification_model(self, model_path, num_classes=2): + """ + 載入分類模型 + """ + try: + model = Xception() + + # 如果是多GPU訓練的模型,需要處理DataParallel + if torch.cuda.device_count() > 1: + model = nn.DataParallel(model) + + model = model.to(self.device) + + # 載入權重 + checkpoint = torch.load(model_path, map_location=self.device) + model.load_state_dict(checkpoint) + model.eval() + + print(f"成功載入分類模型: {model_path}") + return model + + except Exception as e: + print(f"載入分類模型失敗: {e}") + return None + + def load_segmentation_model(self, model_path): + """ + 載入分割模型 + """ + try: + model = GastroSegNet(in_channels=3, out_channels=3) + + if torch.cuda.device_count() > 1: + model = nn.DataParallel(model) + + model = model.to(self.device) + + # 載入權重 + checkpoint = torch.load(model_path, map_location=self.device) + model.load_state_dict(checkpoint) + model.eval() + + print(f"成功載入分割模型: {model_path}") + return model + + except Exception as e: + print(f"載入分割模型失敗: {e}") + return None + + def predict_three_class(self, normal_model, ca_model, test_data): + """ + 使用兩個二分類模型進行三分類預測 + 邏輯: + 1. 先用Normal模型判斷是否為正常 + 2. 如果不是正常,再用CA模型判斷是胃癌還是非胃癌有病 + """ + all_predictions = [] + all_true_labels = [] + + with torch.no_grad(): + for images, labels, filename, Class in tqdm(test_data, desc="預測中"): + + images = images.to(self.device) + batch_size = images.size(0) + + # 獲取真實標籤 + if isinstance(labels, torch.Tensor): + if labels.dim() > 1: # one-hot編碼 + true_labels = torch.argmax(labels, dim=1).cpu().numpy() + else: # 已經是類別索引 + true_labels = labels.cpu().numpy() + else: + # 如果是numpy數組或列表 + labels = np.array(labels) + if labels.ndim > 1: + true_labels = np.argmax(labels, axis=1) + else: + true_labels = labels + + all_true_labels.extend(true_labels) + + # 第一步:使用Normal模型判斷是否為正常 + normal_outputs = normal_model(images) + + # 第二步:使用CA模型判斷胃癌vs非胃癌有病 + ca_outputs = ca_model(images) + + # 三分類邏輯 + batch_predictions = [] + for i in range(batch_size): + # 如果Normal模型認為是正常(第二個類別概率高) + + if normal_outputs[i, 1] > normal_outputs[i, 0]: + prediction = 1 # 正常 + else: + # 如果不是正常,用CA模型判斷 + if ca_outputs[i, 0] > ca_outputs[i, 1]: # 胃癌概率高 + prediction = 0 # 胃癌 + else: + prediction = 2 # 非胃癌有病 + + batch_predictions.append(prediction) + + all_predictions.extend(batch_predictions) + return np.array(all_predictions), np.array(all_true_labels) + + def calculate_metrics(self, y_true, y_pred): + """ + 計算各種評估指標 + """ + # 整體指標 + accuracy = accuracy_score(y_true, y_pred) + precision_macro = precision_score(y_true, y_pred, average='macro', zero_division=0) + recall_macro = recall_score(y_true, y_pred, average='macro', zero_division=0) + f1_macro = f1_score(y_true, y_pred, average='macro', zero_division=0) + + # 每類別指標 + precision_per_class = precision_score(y_true, y_pred, average=None, zero_division=0) + recall_per_class = recall_score(y_true, y_pred, average=None, zero_division=0) + f1_per_class = f1_score(y_true, y_pred, average=None, zero_division=0) + + # 混淆矩陣 + cm = confusion_matrix(y_true, y_pred) + + # 分類報告 + report = classification_report(y_true, y_pred, target_names=list(self.class_labels.values())) + + return { + 'accuracy': accuracy, + 'precision_macro': precision_macro, + 'recall_macro': recall_macro, + 'f1_macro': f1_macro, + 'precision_per_class': precision_per_class, + 'recall_per_class': recall_per_class, + 'f1_per_class': f1_per_class, + 'confusion_matrix': cm, + 'classification_report': report + } + + def save_results(self, metrics): + """ + 保存評估結果 + """ + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + results_dir = f"../Result/Model_Evaluation" + os.makedirs(results_dir, exist_ok=True) + + # 保存整體指標 + overall_results = { + '指標': ['Accuracy', 'Precision (Macro)', 'Recall (Macro)', 'F1-Score (Macro)'], + '數值': [ + f"{metrics['accuracy']:.4f}", + f"{metrics['precision_macro']:.4f}", + f"{metrics['recall_macro']:.4f}", + f"{metrics['f1_macro']:.4f}" + ] + } + + overall_df = pd.DataFrame(overall_results) + overall_df.to_csv(os.path.join(results_dir, "overall_metrics.csv"), index=False, encoding='utf-8-sig') + + # 保存每類別指標 + per_class_results = { + '類別': list(self.class_labels.values()), + 'Precision': [f"{p:.4f}" for p in metrics['precision_per_class']], + 'Recall': [f"{r:.4f}" for r in metrics['recall_per_class']], + 'F1-Score': [f"{f:.4f}" for f in metrics['f1_per_class']] + } + + per_class_df = pd.DataFrame(per_class_results) + per_class_df.to_csv(os.path.join(results_dir, "per_class_metrics.csv"), index=False, encoding='utf-8-sig') + + # 保存混淆矩陣 + cm_df = pd.DataFrame( + metrics['confusion_matrix'], + index=list(self.class_labels.values()), + columns=list(self.class_labels.values()) + ) + cm_df.to_csv(os.path.join(results_dir, "confusion_matrix.csv"), encoding='utf-8-sig') + + # 保存詳細分類報告 + with open(os.path.join(results_dir, "classification_report.txt"), 'w', encoding='utf-8') as f: + f.write("三類胃癌數據分類評估報告\n") + f.write("=" * 50 + "\n\n") + f.write(f"評估時間: {timestamp}\n\n") + f.write("整體性能指標:\n") + f.write(f"Accuracy: {metrics['accuracy']:.4f}\n") + f.write(f"Precision (Macro): {metrics['precision_macro']:.4f}\n") + f.write(f"Recall (Macro): {metrics['recall_macro']:.4f}\n") + f.write(f"F1-Score (Macro): {metrics['f1_macro']:.4f}\n\n") + f.write("詳細分類報告:\n") + f.write(metrics['classification_report']) + + print(f"評估結果已保存至: {results_dir}") + return results_dir + + def run_evaluation(self): + """ + 執行完整的模型評估流程 + """ + print("開始模型評估...") + print("=" * 50) + + # 1. 檢查模型路徑 + print("1. 檢查模型路徑...") + if not self.normal_model_path or not self.ca_model_path: + print("錯誤: 必須指定Normal和CA分類模型權重路徑") + return + + print(f"Normal分類模型: {self.normal_model_path}") + print(f"CA分類模型: {self.ca_model_path}") + + # 2. 載入模型 + print("\n2. 載入模型...") + normal_model = self.load_classification_model(self.normal_model_path, num_classes=2) + ca_model = self.load_classification_model(self.ca_model_path, num_classes=2) + + if not normal_model or not ca_model: + print("錯誤: 模型載入失敗") + return + + # 3. 準備測試數據 + print("\n3. 準備測試數據...") + try: + # 載入測試數據 + test_data_dict = self.data_loader.process_main(status=False) # 不使用ImageGenerator + + Total_Size_List = [] + Train_Size = 0 + print("前處理後資料集總數") + for label in Loading_Config["Training_Labels"]: + Train_Size += len(test_data_dict[label]) + Total_Size_List.append(len(test_data_dict[label])) + print(f"Labels: {label}, 總數為: {len(test_data_dict[label])}") + + print("總共有 " + str(Train_Size) + " 筆資料") + + # 做出跟資料相同數量的Label + Classes = [] + i = 0 + tool = Tool() + # 取得One-hot encording 的資料 + tool.Set_OneHotEncording(Loading_Config["Training_Labels"]) + Encording_Label = tool.Get_OneHot_Encording_Label() + for encording in Encording_Label: + Classes.append(make_label_list(Total_Size_List[i], encording)) + i += 1 + + # 將資料做成Dict的資料型態 + Prepare = Load_Data_Prepare() + Prepare.Set_Final_Dict_Data(Loading_Config["Training_Labels"], test_data_dict, Classes, len(Loading_Config["Training_Labels"])) + Final_Dict_Data = Prepare.Get_Final_Data_Dict() + keys = list(Final_Dict_Data.keys()) + + Merge = merge() + Training_Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list + for i in range(2, len(Loading_Config["Training_Labels"])): + Training_Data = Merge.merge_all_image_data(Training_Data, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + Training_Label = Merge.merge_all_image_data(Final_Dict_Data[keys[len(Loading_Config["Training_Labels"])]], Final_Dict_Data[keys[len(Loading_Config["Training_Labels"]) + 1]]) #將訓練資料的label合併成一個label的list + for i in range(len(Loading_Config["Training_Labels"]) + 2, 2 * len(Loading_Config["Training_Labels"])): + Training_Label = Merge.merge_all_image_data(Training_Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + # 使用Setting_DataSet創建ListDataset + test_dataset = self.preprocessor.Setting_DataSet( + Training_Data, + Training_Label, + None, # 不使用mask + "Transform" + ) + test_loader = self.preprocessor.Dataloader_Sampler(test_dataset, Batch_Size=32, Sampler=False) + + print(f"測試數據載入完成,共 {len(test_dataset)} 個樣本") + + except Exception as e: + print(f"測試數據準備失敗: {e}") + import traceback + traceback.print_exc() + return + + # 4. 進行預測 + print("\n4. 進行三分類預測...") + predictions, true_labels = self.predict_three_class( + normal_model, ca_model, test_loader + ) + + # 5. 計算評估指標 + print("\n5. 計算評估指標...") + metrics = self.calculate_metrics(true_labels, predictions) + + # 6. 顯示結果 + print("\n6. 評估結果:") + print("=" * 50) + print(f"整體準確率 (Accuracy): {metrics['accuracy']:.4f}") + print(f"整體精確率 (Precision): {metrics['precision_macro']:.4f}") + print(f"整體召回率 (Recall): {metrics['recall_macro']:.4f}") + print(f"整體F1分數 (F1-Score): {metrics['f1_macro']:.4f}") + + print("\n各類別詳細指標:") + for i, (precision, recall, f1) in enumerate(zip( + metrics['precision_per_class'], + metrics['recall_per_class'], + metrics['f1_per_class'] + )): + print(f"{self.class_labels[i]}:") + print(f" Precision: {precision:.4f}") + print(f" Recall: {recall:.4f}") + print(f" F1-Score: {f1:.4f}") + + print(f"\n混淆矩陣:") + print(metrics['confusion_matrix']) + + # 7. 保存結果 + print("\n7. 保存評估結果...") + results_dir = self.save_results(metrics) + + print("\n評估完成!") + return metrics, results_dir \ No newline at end of file diff --git a/experiments/pytorch_Model.py b/experiments/pytorch_Model.py deleted file mode 100644 index e66b7ec..0000000 --- a/experiments/pytorch_Model.py +++ /dev/null @@ -1,81 +0,0 @@ -import torch.nn as nn -import timm - - -# class ModifiedXception(nn.Module): -# def __init__(self, num_classes): -# super(ModifiedXception, self).__init__() - -# # Load Xception pre-trained model (full model, not just features) -# self.base_model = timm.create_model( -# 'xception', -# pretrained=True -# ) - -# # Replace the default global pooling with AdaptiveAvgPool2d -# self.base_model.global_pool = nn.AdaptiveAvgPool2d(output_size=1) # Output size of 1x1 spatially - -# # Replace the final fully connected layer with Identity to get features -# self.base_model.fc = nn.Identity() # Output will be 2048 (Xception's default feature size) - -# # Custom head: Linear from 2048 to 1370, additional 1370 layer, then to num_classes -# self.custom_head = nn.Sequential( -# nn.Linear(2048, 1025), # From Xception’s 2048 features to 1370 -# nn.ReLU(), # Activation -# nn.Dropout(0.6), # Dropout for regularization -# nn.Linear(1025, num_classes) # Final output layer -# # nn.Softmax(dim = 1) # Sigmoid for binary/multi-label classification -# ) - -# def forward(self, x): -# # Pass through the base Xception model (up to global pooling) -# x = self.base_model.forward_features(x) # Get feature maps -# x = self.base_model.global_pool(x) # Apply AdaptiveAvgPool2d (output: [B, 2048, 1, 1]) -# x = x.flatten(1) # Flatten to [B, 2048] -# # x = self.base_model.fc(x) # Identity layer (still [B, 2048]) -# output = self.custom_head(x) # Custom head processing -# return output - -class ModifiedXception(nn.Module): - def __init__(self, num_classes): - super(ModifiedXception, self).__init__() - - # 加載 Xception 預訓練模型,去掉最後一層 (fc 層) - self.base_model = timm.create_model('xception', pretrained=True) - self.base_model.fc = nn.Identity() # 移除原來的 fully connected 層 - - # 新增全局平均池化層、隱藏層和輸出層 - GAP_Output = 2048 - self.global_avg_pool = nn.AdaptiveAvgPool1d(2048) # 全局平均池化 - self.hidden_layer = nn.Linear(2048, 1025) # 隱藏層,輸入大小取決於 Xception 的輸出大小 - self.output_layer = nn.Linear(1025, num_classes) # 輸出層,依據分類數目設定 - - # 激活函數與 dropout - self.relu = nn.ReLU() - self.softmax = nn.Softmax(1) - self.dropout = nn.Dropout(0.6) - - def forward(self, x): - x = self.base_model(x) # Xception 主體 - x = self.global_avg_pool(x) # 全局平均池化 - x = self.relu(self.hidden_layer(x)) # 隱藏層 + ReLU - x = self.dropout(x) # Dropout - x = self.output_layer(x) # 輸出層 - return x - -class Model_module(): - def __init__(self): - self.conv1 = nn.Conv2d(in_channels = 3, out_channels = 32, kernel_size = 3, padding = 1) - self.conv2 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, padding = 1) - self.conv3 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, padding = 1) - - self.relu = nn.ReLU() - self.sigmoid = nn.Sigmoid() - - self.max_Pool = nn.MaxPool2d(2, 2) - - self.fc1 = nn.Linear() - self.fc2 = nn.Linear() - pass - def forward(self, input): - pass \ No newline at end of file diff --git a/main.py b/main.py index 2d1f89b..16d5cb3 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,19 @@ from experiments.experiment import experiments -from Image_Process.load_and_ImageGenerator import Load_ImageGenerator -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image +from Image_Process.Image_Generator import Image_generator from Training_Tools.Tools import Tool -from model_data_processing.processing import Balance_Process +from model_data_processing.processing import make_label_list, Read_Image_Root_And_Image_Enhance from Load_process.LoadData import Load_Data_Prepare from Calculate_Process.Calculate import Calculate from merge_class.merge import merge +from model_data_processing.processing_for_cut_image import Cut_Indepentend_Data +from Load_process.LoadData import Loding_Data_Root +from Load_process.file_processing import Process_File +from utils.Stomach_Config import Training_Config, Loading_Config +from Image_Process.Image_Mask_Ground_Truth_Processing import XMLAnnotationProcessor import time import torch - + if __name__ == "__main__": # 測試GPU是否可用 flag = torch.cuda.is_available() @@ -18,43 +22,98 @@ if __name__ == "__main__": else: print(f"CUDA可用,數量為{torch.cuda.device_count()}\n") - # 參數設定 - tool = Tool() - tool.Set_Labels() - tool.Set_Save_Roots() + # 测试GPU是否可用 + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用设备: {device}") + print(f"GPU: {torch.cuda.get_device_name(0)}") - Status = 1 # 決定要使用什麼資料集 - Labels = tool.Get_Data_Label() - Trainig_Root, Testing_Root = tool.Get_Save_Roots(Status) # 一般的 - Generator_Root = tool.Get_Generator_Save_Roots(Status) + tool = Tool() + Status = 1 # 取得One-hot encording 的資料 - tool.Set_OneHotEncording(Labels) + tool.Set_OneHotEncording(Loading_Config["Training_Labels"]) Encording_Label = tool.Get_OneHot_Encording_Label() - Label_Length = len(Labels) - - Model_Name = "Xception" # 取名,告訴我我是用哪個模型(可能是預處理模型/自己設計的模型) - Experiment_Name = "Xception Skin trains Stomach Cancer Dataset with original images" - Epoch = 10000 - Train_Batch_Size = 64 - Image_Size = 256 + Label_Length = len(Loading_Config["Training_Labels"]) Prepare = Load_Data_Prepare() - loading_data = Load_ImageGenerator(Trainig_Root, Testing_Root, Generator_Root, Labels, Image_Size) - experiment = experiments(Image_Size, Model_Name, Experiment_Name, Epoch, Train_Batch_Size, tool, Label_Length, Status) - image_processing = Read_image_and_Process_image(Image_Size) + Indepentend = Cut_Indepentend_Data(Loading_Config["Train_Data_Root"], Loading_Config["Training_Labels"]) Merge = merge() Calculate_Tool = Calculate() - - counter = 1 - Train_Size = 0 + file = Process_File() - # 讀取資料 - Data_Dict_Data = loading_data.process_main(Label_Length) - Total_Size_List = [] + Train_Size = 0 - for label in Labels: + # 讀取資料 + # 將測試資料獨立出來 + test_size = 0.2 + Indepentend.IndependentData_main(Loading_Config["Test_Data_Root"], test_size) + + # # 創建處理切割的部分 + # if not file.JudgeRoot_MakeDir(Loading_Config['Annotation_Training_Root']) and not file.JudgeRoot_MakeDir(Loading_Config['Annotation_Testing_Root']): + # processor_train = XMLAnnotationProcessor( + # dataset_root = Loading_Config["Train_Data_Root"], + # ) + # processor_test = XMLAnnotationProcessor( + # dataset_root = Loading_Config["Test_Data_Root"], + # ) + + # # 設定自訂樣式(可選) + # processor_train.set_drawing_style( + # box_color=(0, 0, 255), # 紅色邊界框 + # text_color=(255, 255, 255), # 白色文字 + # box_thickness=3, + # font_scale=0.7 + # ) + # processor_test.set_drawing_style( + # box_color=(0, 0, 255), # 紅色邊界框 + # text_color=(255, 255, 255), # 白色文字 + # box_thickness=3, + # font_scale=0.7 + # ) + + # print("XML標註處理器已準備就绪") + # for Label_Iamge_List in Loading_Config["Label_Image_Labels"]: + # if Label_Iamge_List == "CA": + # Label = "stomach_cancer_Crop" + # else: + # Label = "Have_Question_Crop" + # training_results = processor_train.process_multiple_xml(f"../Label_Image/{Label_Iamge_List}", Loading_Config['Annotation_Training_Root'], Label) + # testing_results = processor_test.process_multiple_xml(f"../Label_Image/{Label_Iamge_List}", Loading_Config['Annotation_Testing_Root'], Label) + + + # else: + # print("Training and Testing annoation is exist!!!!") + + # # 讀取切割完成後的檔案 + # print("Mask Ground truth is Finished\n") + # Mask_load = Loding_Data_Root(Loading_Config["XML_Loading_Label"], Loading_Config['Annotation_Training_Root'], "") + # Mask_Data_Dict_Data = Mask_load.process_main(False) + + # Total_Size_Lists = [] + # print("Mask資料集總數") + # for label in Loading_Config["XML_Loading_Label"]: + # Train_Size += len(Mask_Data_Dict_Data[label]) + # Total_Size_Lists.append(len(Mask_Data_Dict_Data[label])) + # print(f"Labels: {label}, 總數為: {len(Mask_Data_Dict_Data[label])}") + + # print("總共有 " + str(Train_Size) + " 筆資料") + + # 讀取原始資料集 + load = Loding_Data_Root(Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["ImageGenerator_Data_Root"]) + Data_Dict_Data = load.process_main(False) + + # # 製作資料增強資料 + # if not file.Judge_File_Exist(Loading_Config['Image enhance processing save root']): + # for label in Loading_Config["Training_Labels"]: + # Read_Image_Root_And_Image_Enhance(Data_Dict_Data[label], f"{Loading_Config['Image enhance processing save root']}/{label}") + + # tmp_load = Loding_Data_Root(Loading_Config["Training_Labels"], Loading_Config['Image enhance processing save root'], Loading_Config["ImageGenerator_Data_Root"]) + # Data_Dict_Data = tmp_load.process_main(False) + + Total_Size_List = [] + print("前處理後資料集總數") + for label in Loading_Config["Training_Labels"]: Train_Size += len(Data_Dict_Data[label]) Total_Size_List.append(len(Data_Dict_Data[label])) print(f"Labels: {label}, 總數為: {len(Data_Dict_Data[label])}") @@ -65,13 +124,14 @@ if __name__ == "__main__": Classes = [] i = 0 for encording in Encording_Label: - Classes.append(image_processing.make_label_list(Total_Size_List[i], encording)) + Classes.append(make_label_list(Total_Size_List[i], encording)) i += 1 # 將資料做成Dict的資料型態 - Prepare.Set_Final_Dict_Data(Labels, Data_Dict_Data, Classes, Label_Length) + Prepare.Set_Final_Dict_Data(Loading_Config["Training_Labels"], Data_Dict_Data, Classes, Label_Length) Final_Dict_Data = Prepare.Get_Final_Data_Dict() keys = list(Final_Dict_Data.keys()) + # Mask_Keys = list(Mask_Data_Dict_Data.keys()) Training_Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list for i in range(2, Label_Length): @@ -81,18 +141,17 @@ if __name__ == "__main__": for i in range(Label_Length + 2, 2 * Label_Length): Training_Label = Merge.merge_all_image_data(Training_Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + # Training_Mask_Data = Merge.merge_all_image_data(Mask_Data_Dict_Data[Mask_Keys[0]], Mask_Data_Dict_Data[Mask_Keys[1]]) # 將訓練資料合併成一個list + # for i in range(2, len(Mask_Keys)): + # Training_Mask_Data = Merge.merge_all_image_data(Training_Mask_Data, Mask_Data_Dict_Data[Mask_Keys[i]]) # 將訓練資料合併成一個list + + experiment = experiments( + Xception_Training_Data=Training_Data, + Xception_Training_Label=Training_Label, + Xception_Training_Mask_Data=None, + status=Status + ) start = time.time() - # trains_Data_Image = image_processing.Data_Augmentation_Image(training_data) # 讀檔 - # Training_Data, Training_Label = image_processing.image_data_processing(trains_Data_Image, training_label) # 將讀出來的檔做正規化。降label轉成numpy array 格式 - # Training_Data = image_processing.normalization(Training_Data) - - - # Balance_Data = list(zip(Training_Data, Training_Label)) - # Training_Data, Training_Label = Balance_Process(Balance_Data, Total_Size_List) - # training_data = training_data.permute(0, 3, 1, 2) - + experiment.processing_main() # 執行訓練方法 end = time.time() - print("\n\n\n讀取訓練資料(70000)執行時間:%f 秒\n\n" % (end - start)) - - experiment.processing_main(Training_Data, Training_Label) # 執行訓練方法 - \ No newline at end of file + print(f"\n\n\n訓練時間:{end - start} 秒\n\n") \ No newline at end of file diff --git a/merge_class/__pycache__/merge.cpython-311.pyc b/merge_class/__pycache__/merge.cpython-311.pyc index 0051519..ee0edb1 100644 Binary files a/merge_class/__pycache__/merge.cpython-311.pyc and b/merge_class/__pycache__/merge.cpython-311.pyc differ diff --git a/merge_class/__pycache__/merge.cpython-312.pyc b/merge_class/__pycache__/merge.cpython-312.pyc new file mode 100644 index 0000000..41ae533 Binary files /dev/null and b/merge_class/__pycache__/merge.cpython-312.pyc differ diff --git a/merge_class/__pycache__/merge.cpython-313.pyc b/merge_class/__pycache__/merge.cpython-313.pyc new file mode 100644 index 0000000..50278d7 Binary files /dev/null and b/merge_class/__pycache__/merge.cpython-313.pyc differ diff --git a/model_data_processing/__pycache__/__init__.cpython-311.pyc b/model_data_processing/__pycache__/__init__.cpython-311.pyc index 58b90c4..cd291b9 100644 Binary files a/model_data_processing/__pycache__/__init__.cpython-311.pyc and b/model_data_processing/__pycache__/__init__.cpython-311.pyc differ diff --git a/model_data_processing/__pycache__/__init__.cpython-312.pyc b/model_data_processing/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..41a3149 Binary files /dev/null and b/model_data_processing/__pycache__/__init__.cpython-312.pyc differ diff --git a/model_data_processing/__pycache__/__init__.cpython-313.pyc b/model_data_processing/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..c36aab1 Binary files /dev/null and b/model_data_processing/__pycache__/__init__.cpython-313.pyc differ diff --git a/model_data_processing/__pycache__/processing.cpython-311.pyc b/model_data_processing/__pycache__/processing.cpython-311.pyc index 7173a0f..6c31c0a 100644 Binary files a/model_data_processing/__pycache__/processing.cpython-311.pyc and b/model_data_processing/__pycache__/processing.cpython-311.pyc differ diff --git a/model_data_processing/__pycache__/processing.cpython-312.pyc b/model_data_processing/__pycache__/processing.cpython-312.pyc new file mode 100644 index 0000000..dfd0095 Binary files /dev/null and b/model_data_processing/__pycache__/processing.cpython-312.pyc differ diff --git a/model_data_processing/__pycache__/processing.cpython-313.pyc b/model_data_processing/__pycache__/processing.cpython-313.pyc new file mode 100644 index 0000000..fb6ce2d Binary files /dev/null and b/model_data_processing/__pycache__/processing.cpython-313.pyc differ diff --git a/model_data_processing/__pycache__/processing_for_cut_image.cpython-311.pyc b/model_data_processing/__pycache__/processing_for_cut_image.cpython-311.pyc index 08a73ba..4299164 100644 Binary files a/model_data_processing/__pycache__/processing_for_cut_image.cpython-311.pyc and b/model_data_processing/__pycache__/processing_for_cut_image.cpython-311.pyc differ diff --git a/model_data_processing/__pycache__/processing_for_cut_image.cpython-312.pyc b/model_data_processing/__pycache__/processing_for_cut_image.cpython-312.pyc new file mode 100644 index 0000000..59d62d0 Binary files /dev/null and b/model_data_processing/__pycache__/processing_for_cut_image.cpython-312.pyc differ diff --git a/model_data_processing/__pycache__/processing_for_cut_image.cpython-313.pyc b/model_data_processing/__pycache__/processing_for_cut_image.cpython-313.pyc new file mode 100644 index 0000000..d7a760a Binary files /dev/null and b/model_data_processing/__pycache__/processing_for_cut_image.cpython-313.pyc differ diff --git a/model_data_processing/processing.py b/model_data_processing/processing.py index f7bd7fa..15562fa 100644 --- a/model_data_processing/processing.py +++ b/model_data_processing/processing.py @@ -1,67 +1,120 @@ -import random +import os +import shutil + +from PIL import Image +from torchvision.transforms.functional import to_pil_image +from Load_process.file_processing import Process_File +from utils.Stomach_Config import Image_Enhance, Training_Config, Loading_Config +from Training_Tools.PreProcess import Training_Precesses +from Load_process.LoadData import Loding_Data_Root +from Training_Tools.Tools import Tool from merge_class.merge import merge +from Load_process.LoadData import Load_Data_Prepare +def make_label_list(length, content): + '''製作label的列表''' + label_list = [] + for i in range(length): + label_list.append(content) + return label_list -def calculate_confusion_matrix(predict, result): - '''計算並畫出混淆矩陣''' - tp, fp, tn, fn = 0 - for i in range(len(predict)): - if predict[i] == [1., 0., 0.] and result[i] == [1., 0., 0.]: - pass +def Read_Image_Root_And_Image_Enhance(Image_Roots, Save_Root): + '''讀取Image Root''' + i = 0 + file = Process_File() + for Image_Root in Image_Roots: + try: + Images = Image.open(Image_Root).convert("RGB") + Images = Image_Enhance["gamma"](Images, Image_Enhance["Gamma_Value"]) + for j in range(5): + Images = Image_Enhance["Median"](Images) + Images = Image_Enhance["Shapen"](Images) -def shuffle_data(image, label, mode = 1): - ''' - ## 被用來做資料打亂的用途 - ### 有兩種不同的需求 - 1. 打亂影像資料(讀完檔後的影像) => 回傳Label與Image Root兩個List - 2. 打亂路徑資料(影像的路徑資料,還沒讀檔前) => 回傳打亂後的Dict - ''' - if mode == 1: - shuffle_image, shuffle_label = [], [] - - total = list(zip(image, label)) - random.shuffle(total) - - for total_data in total: - shuffle_image.append(total_data[0]) - shuffle_label.append(total_data[1]) - - return shuffle_image, shuffle_label - else: - shuffle_image = { - label[0] : [] - } - - for i in range(1, len(label)): - shuffle_image.update({label[i] : []}) + file.JudgeRoot_MakeDir(Save_Root) + # 使用原始檔名而不是索引編號 + original_filename = os.path.basename(Image_Root) + path = file.Make_Save_Root(original_filename, Save_Root) - for Label in label: - shuffle_image[Label] = image[Label] - random.shuffle(shuffle_image[Label]) + Images.save(path) + # Image = cv2.imread(Image_Root, cv2.IMREAD_COLOR) # 讀檔(彩色) + # Image = cv2.cvtColor(Image, cv2.COLOR_BGR2RGB) + except Exception as e: + print(e) - return shuffle_image - -def Balance_Process(Datas, Size_List): - # Data_Dict_Data = shuffle_data(Data_Content, Labels, 2) - Train_Size, start = 0, 0 - Image_List = [] - Images, Labels = [], [] + i += 1 + print("已處理 " + str(i) + " 張圖片") + + pass + +def Image_Enhance_Training_Data(Training_Loader, Save_Root): + '''讀取Image Root''' + # 改成只要在Training時進行影像前處理 + Images, Labels, File_Names, File_Classes = [], [], [], [] + Label_Length = len(Loading_Config["Training_Labels"]) + Process = Training_Precesses(ImageSize = Training_Config["Image_Size"]) + file = Process_File() + Prepare = Load_Data_Prepare() + tool = Tool() Merge = merge() - Train_Size = min(Size_List[0], Size_List[1]) - for i in range(2, len(Size_List), 1): - Train_Size = min(Train_Size, Size_List[i]) + if file.Judge_File_Exist(Save_Root): + shutil.rmtree(Save_Root) + print("檔案已存在,刪除檔案") - for i in Size_List: - Image_List.append(Datas[start : Train_Size]) - start = Train_Size - Train_Size += Train_Size + # 取得One-hot encording 的資料 + tool.Set_OneHotEncording(Loading_Config["Training_Labels"]) + Encording_Label = tool.Get_OneHot_Encording_Label() - Image_List = Merge.merge_data_main(Image_List, 0, len(Image_List)) + for inputs, labels, File_Name, File_Class in Training_Loader: + for i in range(inputs.shape[0]): + Permute_Image = inputs[i].permute(1, 2, 0) + Permute_Image *= 255 + Permute_Image = Image_Enhance["gamma"](Permute_Image, Image_Enhance["Gamma_Value"]) + for j in range(5): + Permute_Image = Image_Enhance["Median"](Permute_Image) + + Images = Image_Enhance["Shapen"](Permute_Image) + + Save = file.Make_Save_Root(File_Class[i], Save_Root) + file.JudgeRoot_MakeDir(Save) - for Image in Image_List: - Images.append(Image[0]) - Labels.append(Image[1]) + path = file.Make_Save_Root(File_Name[i], Save) + Images.save(path) - return Images, Labels \ No newline at end of file + load = Loding_Data_Root(Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["ImageGenerator_Data_Root"]) + Data_Dict_Data = load.process_main(False) + + Total_Size_List = [] + for label in Loading_Config["Training_Labels"]: + Total_Size_List.append(len(Data_Dict_Data[label])) + + # 做出跟資料相同數量的Label + Classes = [] + i = 0 + for encording in Encording_Label: + Classes.append(make_label_list(Total_Size_List[i], encording)) + i += 1 + + # 將資料做成Dict的資料型態 + Prepare.Set_Final_Dict_Data(Loading_Config["Training_Labels"], Data_Dict_Data, Classes, Label_Length) + Final_Dict_Data = Prepare.Get_Final_Data_Dict() + keys = list(Final_Dict_Data.keys()) + # Mask_Keys = list(Mask_Data_Dict_Data.keys()) + + Data = Merge.merge_all_image_data(Final_Dict_Data[keys[0]], Final_Dict_Data[keys[1]]) # 將訓練資料合併成一個list + for i in range(2, Label_Length): + Data = Merge.merge_all_image_data(Data, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + Label = Merge.merge_all_image_data(Final_Dict_Data[keys[Label_Length]], Final_Dict_Data[keys[Label_Length + 1]]) #將訓練資料的label合併成一個label的list + for i in range(Label_Length + 2, 2 * Label_Length): + Label = Merge.merge_all_image_data(Label, Final_Dict_Data[keys[i]]) # 將訓練資料合併成一個list + + DataSet = Process.Setting_DataSet( + Datas = Data, + Labels = Label, + Mask_List = None, + transform = "Transform" + ) + Dataloader = Process.Dataloader_Sampler(DataSet, Training_Config["Train_Batch_Size"], True) + return Dataloader diff --git a/model_data_processing/processing_for_cut_image.py b/model_data_processing/processing_for_cut_image.py index 9b29881..19c01f5 100644 --- a/model_data_processing/processing_for_cut_image.py +++ b/model_data_processing/processing_for_cut_image.py @@ -1,6 +1,5 @@ -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image +from model_data_processing.processing import make_label_list from sklearn.model_selection import train_test_split -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image from Load_process.LoadData import Load_Data_Prepare, Process_File, Load_Data_Tools import shutil @@ -13,29 +12,14 @@ class Cut_Indepentend_Data(): Prepare = Load_Data_Prepare() Load_Tool = Load_Data_Tools() Prepare.Set_Data_Content([], len(self.Labels)) - Prepare.Set_Data_Dictionary(self.Labels, Prepare.Get_Data_Content(), 2) + Prepare.Set_Data_Dictionary(self.Labels, Prepare.Get_Data_Content(), len(self.Labels)) Get_Data_Dict_Content = Prepare.Get_Data_Dict() get_all_image_data = Load_Tool.get_data_root(self.Training_Root, Get_Data_Dict_Content, self.Labels) self.Cut_Of_Independent_Data(get_all_image_data, Indepentend_Data_Root, Test_Size) - def Balance_Cut_Of_Independent_Data(self, Independent_Dict_Data_Content, Test_Size): - image_processing = Read_image_and_Process_image(123) - Prepare = Load_Data_Prepare() - Prepare.Set_Data_Content([], len(self.Labels)) - Prepare.Set_Data_Dictionary(self.Labels, Prepare.Get_Data_Content(), 2) - Indepentend_Content = Prepare.Get_Data_Dictionary() - - for cut_TotalTestData_roots_label in self.Labels: # 將資料的label一個一個讀出來 - label = image_processing.make_label_list(len(Independent_Dict_Data_Content[cut_TotalTestData_roots_label]), 0) # 製作假的label - tmp = list(Cut_Data(Independent_Dict_Data_Content[cut_TotalTestData_roots_label], label, Test_Size)) # 切割出特定數量的資料 - Indepentend_Content[cut_TotalTestData_roots_label] = [tmp[0], tmp[1]] - - return Indepentend_Content - def Cut_Of_Independent_Data(self, Independent_Dict_Data_Content, IndependentDataRoot, Test_Size): '''切割獨立資料(e.g. Validation、training)''' - image_processing = Read_image_and_Process_image(122) File = Process_File() i = 0 @@ -44,7 +28,7 @@ class Cut_Indepentend_Data(): root = File.Make_Save_Root(cut_TotalTestData_roots_label, IndependentDataRoot) # 合併成一個路徑 File.Make_Dir(root) # 建檔 - label = image_processing.make_label_list(len(Independent_Dict_Data_Content[cut_TotalTestData_roots_label]), 0) # 製作假的label + label = make_label_list(len(Independent_Dict_Data_Content[cut_TotalTestData_roots_label]), 0) # 製作假的label cut_data = Cut_Data(Independent_Dict_Data_Content[cut_TotalTestData_roots_label], label, Test_Size) # 切割出特定數量的資料 for data in cut_data[1]: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ae9d80b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "pytorch" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "opencv-python-headless>=4.12.0.88", + "pandas>=2.3.3", + "scikit-image>=0.25.2", + "scikit-learn>=1.7.2", + "seaborn>=0.13.2", + "timm>=1.0.21", + "torchcam>=0.3.1", + "torchinfo>=1.8.0", + "torchmetrics>=1.8.2", + "tqdm>=4.67.1", +] diff --git a/test.ipynb b/test.ipynb index 48d7027..abe6272 100644 --- a/test.ipynb +++ b/test.ipynb @@ -2330,7 +2330,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -2349,16 +2349,1977 @@ "path = \"../Dataset/Training/stomach_cancer_Crop/A01.jpg\"\n", "\n", "with open(path, \"rb\") as f:\n", - " img = Image.open(f)\n", - " img.convert(\"RGB\")\n", + " image = Image.open(f)\n", + " image.convert(\"RGB\")\n", "\n", - " Image._show(img)" + " Image._show(image)" ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['123456789', '123456789', '123456789']" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "a = \"123456789\"\n", + "\n", + "b = [a for i in range(3)]\n", + "b" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "stomach_cancer_Crop 有 68 筆資料 \n", + "Normal_Crop 有 36 筆資料 \n", + "Have_Question_Crop 有 26 筆資料 \n", + "\n", + "test_labels有130筆資料\n", + "\n" + ] + } + ], + "source": [ + "from draw_tools.Grad_cam import GradCAM\n", + "from Load_process.Load_Indepentend import Load_Indepentend_Data\n", + "from Training_Tools.Tools import Tool\n", + "from experiments.pytorch_Model import ModifiedXception\n", + "from Training_Tools.PreProcess import Training_Precesses\n", + "import torch\n", + "import json\n", + "import torch.nn as nn\n", + "\n", + "model_path = \"../Result/save_the_best_model/Xception Skin trains Stomach Cancer Dataset, and uses WeightRandomSampler apply sharpen/Xception/best_model( 2025-05-13 )-_fold0.pt\"\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "\n", + "with open('utils/Stomach_Config.json') as f:\n", + " config = json.load(f)\n", + "Training_Configs = config['Training_Config']\n", + "Loading_Config = config['Loading_Config']\n", + "\n", + "tool = Tool()\n", + "\n", + "tool.Set_OneHotEncording(Loading_Config[\"Training_Labels\"])\n", + "Encording_Label = tool.Get_OneHot_Encording_Label()\n", + "\n", + "cut_image = Load_Indepentend_Data(Loading_Config[\"Training_Labels\"], Encording_Label)\n", + "\n", + "Model = ModifiedXception(3)\n", + "\n", + "if torch.cuda.device_count() > 1:\n", + " Model = nn.DataParallel(Model)\n", + "\n", + "Model = Model.to(device)\n", + "TargetLayer = Model.base_model.conv4.pointwise\n", + "\n", + "Model.load_state_dict(torch.load(model_path))\n", + "\n", + "# 预处理对象\n", + "preprocess = Training_Precesses(256)\n", + "\n", + "cut_image.process_main(Loading_Config[\"Test_Data_Root\"]) # 呼叫處理test Data與Validation Data的function\n", + "test, test_label = cut_image.test, cut_image.test_label\n", + "\n", + "# 只评估目标类别\n", + "# 转换为PyTorch张量并移动到设备\n", + "test_dataset = preprocess.Setting_DataSet(cut_image.test, cut_image.test_label, \"Transform\")\n", + "test_loader = preprocess.Dataloader_Sampler(test_dataset, 1, False)\n", + "\n", + "Grad = GradCAM(Model, TargetLayer)\n", + "Grad.Processing_Main(test_loader, f\"123456\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "f1\n" + ] + } + ], + "source": [ + "def f1():\n", + " return 1\n", + "\n", + "import json\n", + "with open('utils/Stomach_Config.json') as f:\n", + " config = json.load(f)\n", + "\n", + "a = config[\"a\"]\n", + "\n", + "print(a[\"Function\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([1, 1000])\n" + ] + } + ], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "\n", + "class SeparableConv2d(nn.Module):\n", + " def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True):\n", + " super(SeparableConv2d, self).__init__()\n", + " self.depthwise = nn.Conv2d(in_channels, in_channels, kernel_size=kernel_size, stride=stride,\n", + " padding=padding, groups=in_channels, bias=bias)\n", + " self.pointwise = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1,\n", + " padding=0, bias=bias)\n", + " \n", + " def forward(self, x):\n", + " x = self.depthwise(x)\n", + " x = self.pointwise(x)\n", + " return x\n", + "\n", + "class EntryFlow(nn.Module):\n", + " def __init__(self):\n", + " super(EntryFlow, self).__init__()\n", + " self.conv1 = nn.Conv2d(3, 32, 3, stride=2, padding=1, bias=False)\n", + " self.bn1 = nn.BatchNorm2d(32)\n", + " self.conv2 = nn.Conv2d(32, 64, 3, padding=1, bias=False)\n", + " self.bn2 = nn.BatchNorm2d(64)\n", + " \n", + " self.conv3_residual = nn.Sequential(\n", + " SeparableConv2d(64, 128, 3, padding=1),\n", + " nn.BatchNorm2d(128),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(128, 128, 3, padding=1),\n", + " nn.BatchNorm2d(128),\n", + " nn.MaxPool2d(3, stride=2, padding=1)\n", + " )\n", + " self.conv3_shortcut = nn.Conv2d(64, 128, 1, stride=2, bias=False)\n", + " self.bn3 = nn.BatchNorm2d(128)\n", + " \n", + " self.conv4_residual = nn.Sequential(\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(128, 256, 3, padding=1),\n", + " nn.BatchNorm2d(256),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(256, 256, 3, padding=1),\n", + " nn.BatchNorm2d(256),\n", + " nn.MaxPool2d(3, stride=2, padding=1)\n", + " )\n", + " self.conv4_shortcut = nn.Conv2d(128, 256, 1, stride=2, bias=False)\n", + " self.bn4 = nn.BatchNorm2d(256)\n", + " \n", + " self.conv5_residual = nn.Sequential(\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(256, 728, 3, padding=1),\n", + " nn.BatchNorm2d(728),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(728, 728, 3, padding=1),\n", + " nn.BatchNorm2d(728),\n", + " nn.MaxPool2d(3, stride=2, padding=1)\n", + " )\n", + " self.conv5_shortcut = nn.Conv2d(256, 728, 1, stride=2, bias=False)\n", + " self.bn5 = nn.BatchNorm2d(728)\n", + " \n", + " def forward(self, x):\n", + " x = F.relu(self.bn1(self.conv1(x)))\n", + " x = F.relu(self.bn2(self.conv2(x)))\n", + " \n", + " residual = self.conv3_residual(x)\n", + " shortcut = self.conv3_shortcut(x)\n", + " x = F.relu(self.bn3(residual + shortcut))\n", + " \n", + " residual = self.conv4_residual(x)\n", + " shortcut = self.conv4_shortcut(x)\n", + " x = F.relu(self.bn4(residual + shortcut))\n", + " \n", + " residual = self.conv5_residual(x)\n", + " shortcut = self.conv5_shortcut(x)\n", + " x = F.relu(self.bn5(residual + shortcut))\n", + " return x\n", + "\n", + "class MiddleFlow(nn.Module):\n", + " def __init__(self):\n", + " super(MiddleFlow, self).__init__()\n", + " self.conv_residual = nn.Sequential(\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(728, 728, 3, padding=1),\n", + " nn.BatchNorm2d(728),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(728, 728, 3, padding=1),\n", + " nn.BatchNorm2d(728),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(728, 728, 3, padding=1),\n", + " nn.BatchNorm2d(728)\n", + " )\n", + " \n", + " def forward(self, x):\n", + " return self.conv_residual(x) + x\n", + "\n", + "class ExitFlow(nn.Module):\n", + " def __init__(self, num_classes=1000):\n", + " super(ExitFlow, self).__init__()\n", + " self.conv1_residual = nn.Sequential(\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(728, 1024, 3, padding=1),\n", + " nn.BatchNorm2d(1024),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(1024, 1024, 3, padding=1),\n", + " nn.BatchNorm2d(1024),\n", + " nn.MaxPool2d(3, stride=2, padding=1)\n", + " )\n", + " self.conv1_shortcut = nn.Conv2d(728, 1024, 1, stride=2, bias=False)\n", + " self.bn1 = nn.BatchNorm2d(1024)\n", + " \n", + " self.conv2 = nn.Sequential(\n", + " SeparableConv2d(1024, 1536, 3, padding=1),\n", + " nn.BatchNorm2d(1536),\n", + " nn.ReLU(inplace=True),\n", + " SeparableConv2d(1536, 2048, 3, padding=1),\n", + " nn.BatchNorm2d(2048),\n", + " nn.ReLU(inplace=True)\n", + " )\n", + " self.avgpool = nn.AdaptiveAvgPool2d((1, 1))\n", + " self.fc = nn.Linear(2048, num_classes)\n", + " \n", + " def forward(self, x):\n", + " residual = self.conv1_residual(x)\n", + " shortcut = self.conv1_shortcut(x)\n", + " x = F.relu(self.bn1(residual + shortcut))\n", + " \n", + " x = self.conv2(x)\n", + " x = self.avgpool(x)\n", + " x = x.view(x.size(0), -1)\n", + " x = self.fc(x)\n", + " return x\n", + "\n", + "class Xception(nn.Module):\n", + " def __init__(self, num_classes=1000):\n", + " super(Xception, self).__init__()\n", + " self.entry_flow = EntryFlow()\n", + " self.middle_flow = nn.Sequential(*[MiddleFlow() for _ in range(8)])\n", + " self.exit_flow = ExitFlow(num_classes)\n", + " \n", + " def forward(self, x):\n", + " x = self.entry_flow(x)\n", + " x = self.middle_flow(x)\n", + " x = self.exit_flow(x)\n", + " return x\n", + "\n", + "def xception(num_classes=1000):\n", + " return Xception(num_classes)\n", + "\n", + "# Example usage:\n", + "model = xception(num_classes=1000)\n", + "input_tensor = torch.randn(1, 3, 299, 299)\n", + "output = model(input_tensor)\n", + "print(output.shape) # Should be torch.Size([1, 1000])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "完成了0張\n", + "完成了1張\n", + "完成了2張\n", + "完成了3張\n", + "完成了4張\n", + "完成了5張\n", + "完成了6張\n", + "完成了7張\n", + "完成了8張\n", + "完成了9張\n", + "完成了10張\n", + "完成了11張\n", + "完成了12張\n", + "完成了13張\n", + "完成了14張\n", + "完成了15張\n", + "完成了16張\n", + "完成了17張\n", + "完成了18張\n", + "完成了19張\n", + "完成了20張\n", + "完成了21張\n", + "完成了22張\n", + "完成了23張\n", + "完成了24張\n", + "完成了25張\n", + "完成了26張\n", + "完成了27張\n", + "完成了28張\n", + "完成了29張\n", + "完成了30張\n", + "完成了31張\n", + "完成了32張\n", + "完成了33張\n", + "完成了34張\n", + "完成了35張\n", + "完成了36張\n", + "完成了37張\n", + "完成了38張\n", + "完成了39張\n", + "完成了40張\n", + "完成了41張\n", + "完成了42張\n", + "完成了43張\n", + "完成了44張\n", + "完成了45張\n", + "完成了46張\n", + "完成了47張\n", + "完成了48張\n", + "完成了49張\n", + "完成了50張\n", + "完成了51張\n", + "完成了52張\n", + "完成了53張\n", + "完成了54張\n", + "完成了55張\n", + "完成了56張\n", + "完成了57張\n", + "完成了58張\n", + "完成了59張\n", + "完成了60張\n", + "完成了61張\n", + "完成了62張\n", + "完成了63張\n", + "完成了64張\n", + "完成了65張\n", + "完成了66張\n", + "完成了67張\n", + "完成了68張\n", + "完成了69張\n", + "完成了70張\n", + "完成了71張\n", + "完成了72張\n", + "完成了73張\n", + "完成了74張\n", + "完成了75張\n", + "完成了76張\n", + "完成了77張\n", + "完成了78張\n", + "完成了79張\n", + "完成了80張\n", + "完成了81張\n", + "完成了82張\n", + "完成了83張\n", + "完成了84張\n", + "完成了85張\n", + "完成了86張\n", + "完成了87張\n", + "完成了88張\n", + "完成了89張\n", + "完成了90張\n", + "完成了91張\n", + "完成了92張\n", + "完成了93張\n", + "完成了94張\n", + "完成了95張\n", + "完成了96張\n", + "完成了97張\n", + "完成了98張\n", + "完成了99張\n", + "完成了100張\n", + "完成了101張\n", + "完成了102張\n", + "完成了103張\n", + "完成了104張\n", + "完成了105張\n", + "完成了106張\n", + "完成了107張\n", + "完成了108張\n", + "完成了109張\n", + "完成了110張\n", + "完成了111張\n", + "完成了112張\n", + "完成了113張\n", + "完成了114張\n", + "完成了115張\n", + "完成了116張\n", + "完成了117張\n", + "完成了118張\n", + "完成了119張\n", + "完成了120張\n", + "完成了121張\n", + "完成了122張\n", + "完成了123張\n", + "完成了124張\n", + "完成了125張\n", + "完成了126張\n", + "完成了127張\n", + "完成了128張\n", + "完成了129張\n", + "完成了130張\n", + "完成了131張\n", + "完成了132張\n", + "完成了133張\n", + "完成了134張\n", + "完成了135張\n", + "完成了136張\n", + "完成了137張\n", + "完成了138張\n", + "完成了139張\n", + "完成了140張\n", + "完成了141張\n", + "完成了142張\n", + "完成了143張\n", + "完成了144張\n", + "完成了145張\n", + "完成了146張\n", + "完成了147張\n", + "完成了148張\n", + "完成了149張\n", + "完成了150張\n", + "完成了151張\n", + "完成了152張\n", + "完成了153張\n", + "完成了154張\n", + "完成了155張\n", + "完成了156張\n", + "完成了157張\n", + "完成了158張\n", + "完成了159張\n", + "完成了160張\n", + "完成了161張\n", + "完成了162張\n", + "完成了163張\n", + "完成了164張\n", + "完成了165張\n", + "完成了166張\n", + "完成了167張\n", + "完成了168張\n", + "完成了169張\n", + "完成了170張\n", + "完成了171張\n", + "完成了172張\n", + "完成了173張\n", + "完成了174張\n", + "完成了175張\n", + "完成了176張\n", + "完成了177張\n", + "完成了178張\n", + "完成了179張\n", + "完成了180張\n", + "完成了181張\n", + "完成了182張\n", + "完成了183張\n", + "完成了184張\n", + "完成了185張\n", + "完成了186張\n", + "完成了187張\n", + "完成了188張\n", + "完成了189張\n", + "完成了190張\n", + "完成了191張\n", + "完成了192張\n", + "完成了193張\n", + "完成了194張\n", + "完成了195張\n", + "完成了196張\n", + "完成了197張\n", + "完成了198張\n", + "完成了199張\n", + "完成了200張\n", + "完成了201張\n", + "完成了202張\n", + "完成了203張\n", + "完成了204張\n", + "完成了205張\n", + "完成了206張\n", + "完成了207張\n", + "完成了208張\n", + "完成了209張\n", + "完成了210張\n", + "完成了211張\n", + "完成了212張\n", + "完成了213張\n", + "完成了214張\n", + "完成了215張\n", + "完成了216張\n", + "完成了217張\n", + "完成了218張\n", + "完成了219張\n", + "完成了220張\n", + "完成了221張\n", + "完成了222張\n", + "完成了223張\n", + "完成了224張\n", + "完成了225張\n", + "完成了226張\n", + "完成了227張\n", + "完成了228張\n", + "完成了229張\n", + "完成了230張\n", + "完成了231張\n", + "完成了232張\n", + "完成了233張\n", + "完成了234張\n", + "完成了235張\n", + "完成了236張\n", + "完成了237張\n" + ] + } + ], + "source": [ + "import cv2\n", + "import os\n", + "import glob\n", + "\n", + "from utils.Stomach_Config import Loading_Config\n", + "from Load_process.file_processing import Process_File\n", + "from Load_process.LoadData import Load_Data_Tools\n", + "\n", + "file = Process_File()\n", + "Load_Data_Tools = Load_Data_Tools()\n", + "Data_Dict_Data = {\"TestingProcess_Images\" : []}\n", + "\n", + "path = os.path.join(\"../TestProcess_Image\", \"*\")\n", + "path = glob.glob(path)\n", + "Training_Data = path\n", + "\n", + "i = 0\n", + "file.JudgeRoot_MakeDir(Loading_Config[\"Process_Roots\"])\n", + "for Data in Training_Data:\n", + " image = cv2.imread(Data)\n", + " # 轉換為 HSV 色彩空間\n", + " image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n", + " # 方法1:固定閾值二值化\n", + " threshold_value = 100 # 閾值,可以調整\n", + " _, binary = cv2.threshold(image, threshold_value, 255, cv2.THRESH_BINARY)\n", + "\n", + " kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7)) # 橢圓核\n", + " # closing_ellipse = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_ellipse)\n", + " # closing_ellipse = cv2.erode(closing_ellipse, kernel_ellipse, iterations=1)\n", + "\n", + " path = file.Make_Save_Root(f\"{i}.png\", Loading_Config[\"Process_Roots\"])\n", + " cv2.imwrite(path, closing_ellipse)\n", + " print(f\"完成了{i}張\")\n", + " i += 1" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import xml.etree.ElementTree as ET\n", + "import cv2\n", + "import os\n", + "import numpy as np\n", + "from typing import List, Dict, Optional, Tuple\n", + "from utils.Stomach_Config import Loading_Config\n", + "\n", + "class XMLAnnotationProcessor:\n", + " \"\"\"\n", + " XML標註檔案處理器\n", + " 專門處理包含bounding box資訊的XML檔案,並在對應圖片上繪製邊界框\n", + " \"\"\"\n", + " \n", + " def __init__(self, dataset_root: str = \"../Dataset/Training\"):\n", + " \"\"\"\n", + " 初始化XML處理器\n", + " \n", + " Args:\n", + " dataset_root: 圖片資料集根目錄\n", + " output_folder: 輸出資料夾\n", + " \"\"\"\n", + " self.dataset_root = dataset_root\n", + " self.box_color = (0, 255, 0) # 綠色邊界框\n", + " self.text_color = (0, 255, 0) # 綠色文字\n", + " self.box_thickness = 2\n", + " self.font_scale = 0.5\n", + " self.font = cv2.FONT_HERSHEY_SIMPLEX\n", + " \n", + " def _ensure_output_folder(self, Save_Root: str) -> None:\n", + " \"\"\"確保輸出資料夾存在\"\"\"\n", + " if not os.path.exists(Save_Root):\n", + " os.makedirs(Save_Root)\n", + " \n", + " def parse_xml(self, xml_file_path: str, Label: str) -> Optional[Dict]:\n", + " \"\"\"\n", + " 解析XML檔案並提取所有相關資訊\n", + " \n", + " Args:\n", + " xml_file_path: XML檔案路徑\n", + " \n", + " Returns:\n", + " Dict: 包含檔案資訊和bounding box的字典,解析失敗時返回None\n", + " \"\"\"\n", + " try:\n", + " tree = ET.parse(xml_file_path)\n", + " root = tree.getroot()\n", + " \n", + " # 提取基本資訊\n", + " filename_element = root.find('filename')\n", + " \n", + " if filename_element is None:\n", + " print(f\"找不到path元素在 {xml_file_path}\")\n", + " return None\n", + " \n", + " filename = filename_element.text if filename_element is not None else \"Unknown\"\n", + " Original_Image_Data_Root = os.path.join(self.dataset_root, Label)\n", + " Original_Image_Data_Root = os.path.join(Original_Image_Data_Root, filename)\n", + " \n", + " # 提取圖片尺寸\n", + " size_element = root.find('size')\n", + " width = int(size_element.find('width').text) if size_element is not None else 0\n", + " height = int(size_element.find('height').text) if size_element is not None else 0\n", + " depth = int(size_element.find('depth').text) if size_element is not None else 3\n", + " \n", + " # 提取所有bounding box\n", + " bounding_boxes = []\n", + " objects = root.findall('object')\n", + " \n", + " for obj in objects:\n", + " bndbox = obj.find('bndbox')\n", + " if bndbox is not None:\n", + " bbox_info = {\n", + " 'name': obj.find('name').text if obj.find('name') is not None else \"Unknown\",\n", + " 'pose': obj.find('pose').text if obj.find('pose') is not None else \"Unspecified\",\n", + " 'truncated': int(obj.find('truncated').text) if obj.find('truncated') is not None else 0,\n", + " 'difficult': int(obj.find('difficult').text) if obj.find('difficult') is not None else 0,\n", + " 'xmin': int(bndbox.find('xmin').text),\n", + " 'ymin': int(bndbox.find('ymin').text),\n", + " 'xmax': int(bndbox.find('xmax').text),\n", + " 'ymax': int(bndbox.find('ymax').text)\n", + " }\n", + " bounding_boxes.append(bbox_info)\n", + " \n", + " return {\n", + " 'filename': filename,\n", + " 'image_path': Original_Image_Data_Root,\n", + " 'width': width,\n", + " 'height': height,\n", + " 'depth': depth,\n", + " 'bounding_boxes': bounding_boxes\n", + " }\n", + " \n", + " except Exception as e:\n", + " print(f\"解析XML檔案 {xml_file_path} 時發生錯誤: {str(e)}\")\n", + " return None\n", + " \n", + " def load_image(self, image_path: str) -> Optional[np.ndarray]:\n", + " \"\"\"\n", + " 載入圖片檔案\n", + " \n", + " Args:\n", + " image_path: 圖片檔案路徑\n", + " \n", + " Returns:\n", + " np.ndarray: 圖片陣列,載入失敗時返回None\n", + " \"\"\"\n", + " if not os.path.exists(image_path):\n", + " print(f\"圖片檔案不存在: {image_path}\")\n", + " return None\n", + " \n", + " image = cv2.imread(image_path)\n", + " if image is None:\n", + " print(f\"無法讀取圖片: {image_path}\")\n", + " return None\n", + " \n", + " return image\n", + " \n", + " def draw_bounding_boxes(self, image: np.ndarray, bounding_boxes: List[Dict]) -> np.ndarray:\n", + " \"\"\"\n", + " 創建遮罩圖片:bounding box內為白色,外部為黑色\n", + " \n", + " Args:\n", + " image: 圖片陣列\n", + " bounding_boxes: bounding box資訊列表\n", + " \n", + " Returns:\n", + " np.ndarray: 處理後的遮罩圖片陣列\n", + " \"\"\"\n", + " # 創建黑色背景圖片\n", + " height, width = image.shape[:2]\n", + " result_image = np.zeros((height, width, 3), dtype=np.uint8)\n", + " \n", + " for i, bbox in enumerate(bounding_boxes):\n", + " xmin, ymin = bbox['xmin'], bbox['ymin']\n", + " xmax, ymax = bbox['xmax'], bbox['ymax']\n", + " object_name = bbox['name']\n", + " \n", + " # 確保座標在圖片範圍內\n", + " xmin = max(0, min(xmin, width-1))\n", + " ymin = max(0, min(ymin, height-1))\n", + " xmax = max(0, min(xmax, width-1))\n", + " ymax = max(0, min(ymax, height-1))\n", + " \n", + " # 將bounding box範圍內設為白色 (255, 255, 255)\n", + " result_image[ymin:ymax, xmin:xmax] = [255, 255, 255]\n", + " \n", + " print(f\"Object {i+1}: {object_name} - 座標: ({xmin}, {ymin}, {xmax}, {ymax})\")\n", + " \n", + " return result_image\n", + " \n", + " def save_annotated_image(self, image: np.ndarray, original_filename: str, Label : str) -> str:\n", + " \"\"\"\n", + " 儲存標註後的圖片\n", + " \n", + " Args:\n", + " image: 標註後的圖片陣列\n", + " original_filename: 原始檔案名稱\n", + " \n", + " Returns:\n", + " str: 儲存的檔案路徑\n", + " \"\"\"\n", + " output_filename = f\"annotated_{original_filename}\"\n", + " output_path = os.path.join(Loading_Config[\"Annotation_Root\"], Label)\n", + " Save_Image_Roots = os.path.join(output_path, output_filename)\n", + " # 確保輸出資料夾存在\n", + " self._ensure_output_folder(output_path)\n", + "\n", + " cv2.imwrite(Save_Image_Roots, image)\n", + " print(f\"已儲存標註圖片至: {Save_Image_Roots}\")\n", + " return Save_Image_Roots\n", + " \n", + " def process_single_xml(self, xml_file_path: str, Label : str) -> Optional[Tuple[np.ndarray, str]]:\n", + " \"\"\"\n", + " 處理單一XML檔案\n", + " \n", + " Args:\n", + " xml_file_path: XML檔案路徑\n", + " \n", + " Returns:\n", + " Tuple[np.ndarray, str]: (標註後的圖片, 輸出路徑),處理失敗時返回None\n", + " \"\"\"\n", + " # 解析XML\n", + " xml_data = self.parse_xml(xml_file_path, Label)\n", + " if xml_data is None:\n", + " return None\n", + " \n", + " # 載入圖片\n", + " image = self.load_image(xml_data['image_path'])\n", + " if image is None:\n", + " return None\n", + " \n", + " # 繪製bounding box\n", + " annotated_image = self.draw_bounding_boxes(image, xml_data['bounding_boxes'])\n", + " \n", + " # 儲存結果\n", + " output_path = self.save_annotated_image(annotated_image, xml_data['filename'], Label)\n", + " \n", + " return annotated_image, output_path\n", + " \n", + " def process_multiple_xml(self, xml_folder_path: str, Label : str) -> List[Tuple[str, bool]]:\n", + " \"\"\"\n", + " 批量處理多個XML檔案\n", + " \n", + " Args:\n", + " xml_folder_path: 包含XML檔案的資料夾路徑\n", + " \n", + " Returns:\n", + " List[Tuple[str, bool]]: [(檔案名稱, 處理成功與否), ...]\n", + " \"\"\"\n", + " if not os.path.exists(xml_folder_path):\n", + " print(f\"XML資料夾不存在: {xml_folder_path}\")\n", + " return []\n", + " \n", + " xml_files = [f for f in os.listdir(xml_folder_path) if f.endswith('.xml')]\n", + " \n", + " if not xml_files:\n", + " print(f\"在 {xml_folder_path} 中找不到XML檔案\")\n", + " return []\n", + " \n", + " print(f\"找到 {len(xml_files)} 個XML檔案\")\n", + " for xml_file in xml_files: \n", + " try:\n", + " Read_XML_File = os.path.join(xml_folder_path, xml_file)\n", + " self.process_single_xml(Read_XML_File, Label)\n", + " print(f\"\\n處理檔案: {xml_file}\")\n", + " except Exception as e:\n", + " print(f\"處理 {xml_file} 時發生錯誤: {str(e)}\")\n", + " return\n", + " \n", + " def get_bounding_boxes_info(self, xml_file_path: str) -> Optional[Dict]:\n", + " \"\"\"\n", + " 僅提取XML中的bounding box資訊,不進行圖片處理\n", + " \n", + " Args:\n", + " xml_file_path: XML檔案路徑\n", + " \n", + " Returns:\n", + " Dict: 包含檔案資訊和bounding box座標的字典\n", + " \"\"\"\n", + " return self.parse_xml(xml_file_path)\n", + " \n", + " def set_drawing_style(self, box_color: Tuple[int, int, int] = None, \n", + " text_color: Tuple[int, int, int] = None,\n", + " box_thickness: int = None, \n", + " font_scale: float = None) -> None:\n", + " \"\"\"\n", + " 設定繪圖樣式\n", + " \n", + " Args:\n", + " box_color: 邊界框顏色 (B, G, R)\n", + " text_color: 文字顏色 (B, G, R)\n", + " box_thickness: 邊界框粗細\n", + " font_scale: 字體大小\n", + " \"\"\"\n", + " if box_color is not None:\n", + " self.box_color = box_color\n", + " if text_color is not None:\n", + " self.text_color = text_color\n", + " if box_thickness is not None:\n", + " self.box_thickness = box_thickness\n", + " if font_scale is not None:\n", + " self.font_scale = font_scale" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "XML標註處理器已準備就绪\n", + "找到 232 個XML檔案\n", + "Object 1: Have_Question - 座標: (263, 740, 333, 814)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a00.jpg\n", + "\n", + "處理檔案: a00.xml\n", + "Object 1: Have_Question - 座標: (406, 206, 608, 368)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1000.jpg\n", + "\n", + "處理檔案: A1000.xml\n", + "Object 1: Have_Question - 座標: (740, 330, 819, 401)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1002.jpg\n", + "\n", + "處理檔案: A1002.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A1010.jpg\n", + "\n", + "處理檔案: A1010.xml\n", + "Object 1: Have_Question - 座標: (349, 655, 474, 790)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1012.jpg\n", + "\n", + "處理檔案: A1012.xml\n", + "Object 1: Have_Question - 座標: (707, 89, 1206, 746)\n", + "Object 2: Have_Question - 座標: (100, 199, 410, 816)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1020.jpg\n", + "\n", + "處理檔案: A1020.xml\n", + "Object 1: Have_Question - 座標: (720, 391, 769, 439)\n", + "Object 2: Have_Question - 座標: (833, 621, 969, 733)\n", + "Object 3: Have_Question - 座標: (337, 595, 735, 900)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1021.jpg\n", + "\n", + "處理檔案: A1021.xml\n", + "Object 1: Have_Question - 座標: (227, 103, 518, 213)\n", + "Object 2: Have_Question - 座標: (264, 560, 423, 667)\n", + "Object 3: Have_Question - 座標: (423, 724, 522, 810)\n", + "Object 4: Have_Question - 座標: (686, 652, 903, 967)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1030.jpg\n", + "\n", + "處理檔案: A1030.xml\n", + "Object 1: Have_Question - 座標: (134, 266, 342, 414)\n", + "Object 2: Have_Question - 座標: (308, 539, 486, 676)\n", + "Object 3: Have_Question - 座標: (794, 479, 976, 669)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1031.jpg\n", + "\n", + "處理檔案: A1031.xml\n", + "Object 1: Have_Question - 座標: (396, 213, 905, 977)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A10310.jpg\n", + "\n", + "處理檔案: A10310.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A1032.jpg\n", + "\n", + "處理檔案: A1032.xml\n", + "Object 1: Have_Question - 座標: (126, 440, 190, 522)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1033.jpg\n", + "\n", + "處理檔案: A1033.xml\n", + "Object 1: Have_Question - 座標: (301, 379, 484, 527)\n", + "Object 2: Have_Question - 座標: (642, 484, 771, 615)\n", + "Object 3: Have_Question - 座標: (642, 772, 700, 828)\n", + "Object 4: Have_Question - 座標: (495, 836, 538, 871)\n", + "Object 5: Have_Question - 座標: (754, 866, 858, 943)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1034.jpg\n", + "\n", + "處理檔案: A1034.xml\n", + "Object 1: Have_Question - 座標: (375, 423, 629, 543)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1036.jpg\n", + "\n", + "處理檔案: A1036.xml\n", + "Object 1: Have_Question - 座標: (260, 360, 782, 748)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1037.jpg\n", + "\n", + "處理檔案: A1037.xml\n", + "Object 1: Have_Question - 座標: (764, 305, 901, 433)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1040.jpg\n", + "\n", + "處理檔案: A1040.xml\n", + "Object 1: Have_Question - 座標: (336, 622, 681, 930)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1051.jpg\n", + "\n", + "處理檔案: A1051.xml\n", + "Object 1: Have_Question - 座標: (32, 212, 441, 979)\n", + "Object 2: Have_Question - 座標: (536, 578, 769, 777)\n", + "Object 3: Have_Question - 座標: (981, 819, 1035, 858)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1052.jpg\n", + "\n", + "處理檔案: A1052.xml\n", + "Object 1: Have_Question - 座標: (28, 379, 614, 605)\n", + "Object 2: Have_Question - 座標: (1007, 285, 1190, 503)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1061.jpg\n", + "\n", + "處理檔案: A1061.xml\n", + "Object 1: Have_Question - 座標: (399, 236, 492, 333)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1070.jpg\n", + "\n", + "處理檔案: A1070.xml\n", + "Object 1: Have_Question - 座標: (395, 269, 664, 391)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1071.jpg\n", + "\n", + "處理檔案: A1071.xml\n", + "Object 1: Have_Question - 座標: (207, 330, 777, 726)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1072.jpg\n", + "\n", + "處理檔案: A1072.xml\n", + "Object 1: Have_Question - 座標: (160, 221, 218, 323)\n", + "Object 2: Have_Question - 座標: (140, 567, 221, 654)\n", + "Object 3: Have_Question - 座標: (495, 513, 663, 799)\n", + "Object 4: Have_Question - 座標: (600, 353, 797, 477)\n", + "Object 5: Have_Question - 座標: (735, 117, 793, 168)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1075.jpg\n", + "\n", + "處理檔案: A1075.xml\n", + "Object 1: Have_Question - 座標: (590, 478, 765, 612)\n", + "Object 2: Have_Question - 座標: (198, 607, 522, 845)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1080.jpg\n", + "\n", + "處理檔案: A1080.xml\n", + "Object 1: Have_Question - 座標: (805, 545, 963, 624)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a110.jpg\n", + "\n", + "處理檔案: a110.xml\n", + "Object 1: Have_Question - 座標: (427, 194, 472, 243)\n", + "Object 2: Have_Question - 座標: (863, 42, 970, 257)\n", + "Object 3: Have_Question - 座標: (595, 555, 791, 714)\n", + "Object 4: Have_Question - 座標: (648, 786, 747, 875)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1100.jpg\n", + "\n", + "處理檔案: A1100.xml\n", + "Object 1: Have_Question - 座標: (964, 71, 1131, 187)\n", + "Object 2: Have_Question - 座標: (774, 600, 861, 698)\n", + "Object 3: Have_Question - 座標: (756, 770, 849, 861)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1102.jpg\n", + "\n", + "處理檔案: A1102.xml\n", + "Object 1: Have_Question - 座標: (185, 685, 394, 825)\n", + "Object 2: Have_Question - 座標: (710, 482, 823, 550)\n", + "Object 3: Have_Question - 座標: (872, 657, 938, 725)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1103.jpg\n", + "\n", + "處理檔案: A1103.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A1104.jpg\n", + "\n", + "處理檔案: A1104.xml\n", + "Object 1: Have_Question - 座標: (721, 145, 1116, 581)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A1110.jpg\n", + "\n", + "處理檔案: A1110.xml\n", + "Object 1: Have_Question - 座標: (737, 198, 1036, 776)\n", + "Object 2: Have_Question - 座標: (499, 132, 608, 224)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a113.jpg\n", + "\n", + "處理檔案: a113.xml\n", + "Object 1: Have_Question - 座標: (385, 258, 928, 688)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a120.jpg\n", + "\n", + "處理檔案: a120.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a130.jpg\n", + "\n", + "處理檔案: a130.xml\n", + "Object 1: Have_Question - 座標: (297, 136, 773, 624)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a132.jpg\n", + "\n", + "處理檔案: a132.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a140.jpg\n", + "\n", + "處理檔案: a140.xml\n", + "Object 1: Have_Question - 座標: (855, 281, 1031, 667)\n", + "Object 2: Have_Question - 座標: (650, 664, 928, 900)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a144.jpg\n", + "\n", + "處理檔案: a144.xml\n", + "Object 1: Have_Question - 座標: (480, 290, 775, 513)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a150.jpg\n", + "\n", + "處理檔案: a150.xml\n", + "Object 1: Have_Question - 座標: (329, 180, 610, 435)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a172.jpg\n", + "\n", + "處理檔案: a172.xml\n", + "Object 1: Have_Question - 座標: (340, 399, 553, 563)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a180.jpg\n", + "\n", + "處理檔案: a180.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a181.jpg\n", + "\n", + "處理檔案: a181.xml\n", + "Object 1: Have_Question - 座標: (648, 280, 946, 569)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a182.jpg\n", + "\n", + "處理檔案: a182.xml\n", + "Object 1: Have_Question - 座標: (314, 770, 528, 953)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a184.jpg\n", + "\n", + "處理檔案: a184.xml\n", + "Object 1: Have_Question - 座標: (446, 468, 629, 640)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a190.jpg\n", + "\n", + "處理檔案: a190.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a200.jpg\n", + "\n", + "處理檔案: a200.xml\n", + "Object 1: Have_Question - 座標: (709, 777, 861, 909)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a211.jpg\n", + "\n", + "處理檔案: a211.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a233.jpg\n", + "\n", + "處理檔案: a233.xml\n", + "Object 1: Have_Question - 座標: (68, 108, 571, 624)\n", + "Object 2: Have_Question - 座標: (761, 473, 916, 670)\n", + "Object 3: Have_Question - 座標: (635, 886, 750, 978)\n", + "Object 4: Have_Question - 座標: (897, 960, 968, 1030)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a2410.jpg\n", + "\n", + "處理檔案: a2410.xml\n", + "Object 1: Have_Question - 座標: (916, 89, 1429, 411)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a249.jpg\n", + "\n", + "處理檔案: a249.xml\n", + "Object 1: Have_Question - 座標: (374, 477, 442, 546)\n", + "Object 2: Have_Question - 座標: (529, 431, 599, 505)\n", + "Object 3: Have_Question - 座標: (726, 415, 783, 525)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a2513.jpg\n", + "\n", + "處理檔案: a2513.xml\n", + "Object 1: Have_Question - 座標: (504, 416, 625, 480)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a30.jpg\n", + "\n", + "處理檔案: a30.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a300.jpg\n", + "\n", + "處理檔案: a300.xml\n", + "Object 1: Have_Question - 座標: (764, 487, 1269, 742)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a301.jpg\n", + "\n", + "處理檔案: a301.xml\n", + "Object 1: Have_Question - 座標: (516, 594, 1626, 1047)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a302.jpg\n", + "\n", + "處理檔案: a302.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a303.jpg\n", + "\n", + "處理檔案: a303.xml\n", + "Object 1: Have_Question - 座標: (628, 394, 844, 574)\n", + "Object 2: Have_Question - 座標: (1252, 405, 1327, 487)\n", + "Object 3: Have_Question - 座標: (706, 625, 1272, 980)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a316.jpg\n", + "\n", + "處理檔案: a316.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a360.jpg\n", + "\n", + "處理檔案: a360.xml\n", + "Object 1: Have_Question - 座標: (816, 122, 913, 195)\n", + "Object 2: Have_Question - 座標: (822, 262, 1023, 549)\n", + "Object 3: Have_Question - 座標: (881, 589, 942, 712)\n", + "Object 4: Have_Question - 座標: (738, 778, 796, 826)\n", + "Object 5: Have_Question - 座標: (332, 822, 637, 912)\n", + "Object 6: Have_Question - 座標: (687, 927, 823, 981)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a3611.jpg\n", + "\n", + "處理檔案: a3611.xml\n", + "Object 1: Have_Question - 座標: (962, 457, 1093, 640)\n", + "Object 2: Have_Question - 座標: (918, 796, 1176, 1034)\n", + "Object 3: Have_Question - 座標: (610, 935, 700, 1045)\n", + "Object 4: Have_Question - 座標: (602, 39, 710, 188)\n", + "Object 5: Have_Question - 座標: (385, 196, 476, 319)\n", + "Object 6: Have_Question - 座標: (867, 50, 1104, 343)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a3612.jpg\n", + "\n", + "處理檔案: a3612.xml\n", + "Object 1: Have_Question - 座標: (235, 364, 931, 912)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a3613.jpg\n", + "\n", + "處理檔案: a3613.xml\n", + "Object 1: Have_Question - 座標: (373, 254, 686, 406)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a3615.jpg\n", + "\n", + "處理檔案: a3615.xml\n", + "Object 1: Have_Question - 座標: (414, 246, 673, 388)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a3616.jpg\n", + "\n", + "處理檔案: a3616.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a364.jpg\n", + "\n", + "處理檔案: a364.xml\n", + "Object 1: Have_Question - 座標: (1213, 42, 1341, 150)\n", + "Object 2: Have_Question - 座標: (1174, 171, 1549, 382)\n", + "Object 3: Have_Question - 座標: (1219, 379, 1566, 596)\n", + "Object 4: Have_Question - 座標: (1086, 696, 1404, 826)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a365.jpg\n", + "\n", + "處理檔案: a365.xml\n", + "Object 1: Have_Question - 座標: (1542, 266, 1654, 407)\n", + "Object 2: Have_Question - 座標: (1151, 845, 1224, 922)\n", + "Object 3: Have_Question - 座標: (813, 931, 952, 1011)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a367.jpg\n", + "\n", + "處理檔案: a367.xml\n", + "Object 1: Have_Question - 座標: (1058, 337, 1152, 607)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a368.jpg\n", + "\n", + "處理檔案: a368.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a375.jpg\n", + "\n", + "處理檔案: a375.xml\n", + "Object 1: Have_Question - 座標: (132, 460, 354, 685)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a4710.jpg\n", + "\n", + "處理檔案: a4710.xml\n", + "Object 1: Have_Question - 座標: (90, 560, 328, 797)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a4711.jpg\n", + "\n", + "處理檔案: a4711.xml\n", + "Object 1: Have_Question - 座標: (132, 355, 366, 515)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a4712.jpg\n", + "\n", + "處理檔案: a4712.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a473.jpg\n", + "\n", + "處理檔案: a473.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a474.jpg\n", + "\n", + "處理檔案: a474.xml\n", + "Object 1: Have_Question - 座標: (622, 496, 696, 564)\n", + "Object 2: Have_Question - 座標: (1053, 678, 1162, 759)\n", + "Object 3: Have_Question - 座標: (715, 533, 1004, 761)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a4911.jpg\n", + "\n", + "處理檔案: a4911.xml\n", + "Object 1: Have_Question - 座標: (517, 570, 736, 712)\n", + "Object 2: Have_Question - 座標: (676, 723, 802, 798)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a4912.jpg\n", + "\n", + "處理檔案: a4912.xml\n", + "Object 1: Have_Question - 座標: (809, 234, 1010, 421)\n", + "Object 2: Have_Question - 座標: (383, 678, 448, 743)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a497.jpg\n", + "\n", + "處理檔案: a497.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a50.jpg\n", + "\n", + "處理檔案: a50.xml\n", + "Object 1: Have_Question - 座標: (117, 195, 708, 978)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a509.jpg\n", + "\n", + "處理檔案: a509.xml\n", + "Object 1: Have_Question - 座標: (413, 539, 567, 622)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a511.jpg\n", + "\n", + "處理檔案: a511.xml\n", + "Object 1: Have_Question - 座標: (528, 121, 581, 171)\n", + "Object 2: Have_Question - 座標: (885, 276, 964, 354)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a513.jpg\n", + "\n", + "處理檔案: a513.xml\n", + "Object 1: Have_Question - 座標: (978, 458, 1151, 664)\n", + "Object 2: Have_Question - 座標: (672, 873, 849, 1043)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a516.jpg\n", + "\n", + "處理檔案: a516.xml\n", + "Object 1: Have_Question - 座標: (201, 101, 904, 499)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a520.jpg\n", + "\n", + "處理檔案: a520.xml\n", + "Object 1: Have_Question - 座標: (46, 107, 746, 687)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a521.jpg\n", + "\n", + "處理檔案: a521.xml\n", + "Object 1: Have_Question - 座標: (921, 779, 1014, 860)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a523.jpg\n", + "\n", + "處理檔案: a523.xml\n", + "Object 1: Have_Question - 座標: (386, 101, 745, 377)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a524.jpg\n", + "\n", + "處理檔案: a524.xml\n", + "Object 1: Have_Question - 座標: (89, 619, 357, 827)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a526.jpg\n", + "\n", + "處理檔案: a526.xml\n", + "Object 1: Have_Question - 座標: (164, 110, 757, 294)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a5312.jpg\n", + "\n", + "處理檔案: a5312.xml\n", + "Object 1: Have_Question - 座標: (62, 368, 390, 731)\n", + "Object 2: Have_Question - 座標: (231, 276, 849, 446)\n", + "Object 3: Have_Question - 座標: (735, 337, 1025, 907)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a5314.jpg\n", + "\n", + "處理檔案: a5314.xml\n", + "Object 1: Have_Question - 座標: (260, 242, 478, 469)\n", + "Object 2: Have_Question - 座標: (736, 448, 919, 607)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a536.jpg\n", + "\n", + "處理檔案: a536.xml\n", + "Object 1: Have_Question - 座標: (163, 127, 273, 327)\n", + "Object 2: Have_Question - 座標: (470, 230, 576, 315)\n", + "Object 3: Have_Question - 座標: (827, 134, 925, 213)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a538.jpg\n", + "\n", + "處理檔案: a538.xml\n", + "Object 1: Have_Question - 座標: (176, 401, 455, 689)\n", + "Object 2: Have_Question - 座標: (556, 331, 632, 416)\n", + "Object 3: Have_Question - 座標: (746, 267, 803, 340)\n", + "Object 4: Have_Question - 座標: (567, 459, 632, 519)\n", + "Object 5: Have_Question - 座標: (794, 391, 858, 461)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a539.jpg\n", + "\n", + "處理檔案: a539.xml\n", + "Object 1: Have_Question - 座標: (400, 369, 452, 421)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a551.jpg\n", + "\n", + "處理檔案: a551.xml\n", + "Object 1: Have_Question - 座標: (324, 124, 836, 389)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a5511.jpg\n", + "\n", + "處理檔案: a5511.xml\n", + "Object 1: Have_Question - 座標: (46, 763, 177, 896)\n", + "Object 2: Have_Question - 座標: (392, 662, 587, 971)\n", + "Object 3: Have_Question - 座標: (635, 160, 1000, 972)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a560.jpg\n", + "\n", + "處理檔案: a560.xml\n", + "Object 1: Have_Question - 座標: (36, 399, 435, 712)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a563.jpg\n", + "\n", + "處理檔案: a563.xml\n", + "Object 1: Have_Question - 座標: (306, 617, 342, 675)\n", + "Object 2: Have_Question - 座標: (758, 581, 824, 658)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a571.jpg\n", + "\n", + "處理檔案: a571.xml\n", + "Object 1: Have_Question - 座標: (215, 288, 743, 762)\n", + "Object 2: Have_Question - 座標: (809, 423, 931, 681)\n", + "Object 3: Have_Question - 座標: (734, 830, 815, 972)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a590.jpg\n", + "\n", + "處理檔案: a590.xml\n", + "Object 1: Have_Question - 座標: (353, 93, 788, 428)\n", + "Object 2: Have_Question - 座標: (698, 406, 837, 641)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a591.jpg\n", + "\n", + "處理檔案: a591.xml\n", + "Object 1: Have_Question - 座標: (231, 215, 659, 603)\n", + "Object 2: Have_Question - 座標: (662, 250, 920, 770)\n", + "Object 3: Have_Question - 座標: (425, 775, 920, 984)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a592.jpg\n", + "\n", + "處理檔案: a592.xml\n", + "Object 1: Have_Question - 座標: (400, 509, 597, 800)\n", + "Object 2: Have_Question - 座標: (492, 801, 599, 981)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a593.jpg\n", + "\n", + "處理檔案: a593.xml\n", + "Object 1: Have_Question - 座標: (345, 379, 621, 684)\n", + "Object 2: Have_Question - 座標: (630, 433, 906, 869)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a594.jpg\n", + "\n", + "處理檔案: a594.xml\n", + "Object 1: Have_Question - 座標: (239, 317, 371, 431)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a60.jpg\n", + "\n", + "處理檔案: a60.xml\n", + "Object 1: Have_Question - 座標: (266, 567, 609, 873)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a61.jpg\n", + "\n", + "處理檔案: a61.xml\n", + "Object 1: Have_Question - 座標: (365, 69, 539, 135)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a610.jpg\n", + "\n", + "處理檔案: a610.xml\n", + "Object 1: Have_Question - 座標: (175, 577, 246, 636)\n", + "Object 2: Have_Question - 座標: (613, 54, 698, 140)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a613.jpg\n", + "\n", + "處理檔案: a613.xml\n", + "Object 1: Have_Question - 座標: (439, 325, 584, 441)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a614.jpg\n", + "\n", + "處理檔案: a614.xml\n", + "Object 1: Have_Question - 座標: (94, 560, 200, 675)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a616.jpg\n", + "\n", + "處理檔案: a616.xml\n", + "Object 1: Have_Question - 座標: (461, 489, 674, 640)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a617.jpg\n", + "\n", + "處理檔案: a617.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a62.jpg\n", + "\n", + "處理檔案: a62.xml\n", + "Object 1: Have_Question - 座標: (622, 190, 848, 413)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a622.jpg\n", + "\n", + "處理檔案: a622.xml\n", + "Object 1: Have_Question - 座標: (608, 764, 783, 932)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a625.jpg\n", + "\n", + "處理檔案: a625.xml\n", + "Object 1: Have_Question - 座標: (12, 578, 185, 823)\n", + "Object 2: Have_Question - 座標: (186, 764, 894, 981)\n", + "Object 3: Have_Question - 座標: (458, 287, 839, 767)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a626.jpg\n", + "\n", + "處理檔案: a626.xml\n", + "Object 1: Have_Question - 座標: (52, 461, 174, 626)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a632.jpg\n", + "\n", + "處理檔案: a632.xml\n", + "Object 1: Have_Question - 座標: (201, 230, 693, 507)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a64.jpg\n", + "\n", + "處理檔案: a64.xml\n", + "Object 1: Have_Question - 座標: (418, 559, 534, 768)\n", + "Object 2: Have_Question - 座標: (515, 398, 645, 455)\n", + "Object 3: Have_Question - 座標: (354, 427, 373, 445)\n", + "Object 4: Have_Question - 座標: (206, 599, 229, 625)\n", + "Object 5: Have_Question - 座標: (279, 75, 369, 115)\n", + "Object 6: Have_Question - 座標: (509, 36, 816, 200)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a641.jpg\n", + "\n", + "處理檔案: a641.xml\n", + "Object 1: Have_Question - 座標: (320, 228, 489, 757)\n", + "Object 2: Have_Question - 座標: (490, 43, 1172, 758)\n", + "Object 3: Have_Question - 座標: (49, 387, 315, 814)\n", + "Object 4: Have_Question - 座標: (195, 32, 319, 301)\n", + "Object 5: Have_Question - 座標: (8, 173, 109, 362)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a6410.jpg\n", + "\n", + "處理檔案: a6410.xml\n", + "Object 1: Have_Question - 座標: (37, 560, 1055, 1014)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a6411.jpg\n", + "\n", + "處理檔案: a6411.xml\n", + "Object 1: Have_Question - 座標: (652, 234, 840, 469)\n", + "Object 2: Have_Question - 座標: (1132, 525, 1159, 578)\n", + "Object 3: Have_Question - 座標: (726, 55, 816, 101)\n", + "Object 4: Have_Question - 座標: (505, 159, 523, 181)\n", + "Object 5: Have_Question - 座標: (369, 393, 397, 408)\n", + "Object 6: Have_Question - 座標: (723, 516, 891, 775)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a645.jpg\n", + "\n", + "處理檔案: a645.xml\n", + "Object 1: Have_Question - 座標: (193, 271, 358, 424)\n", + "Object 2: Have_Question - 座標: (411, 646, 524, 768)\n", + "Object 3: Have_Question - 座標: (347, 938, 569, 1036)\n", + "Object 4: Have_Question - 座標: (693, 640, 886, 933)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a647.jpg\n", + "\n", + "處理檔案: a647.xml\n", + "Object 1: Have_Question - 座標: (837, 263, 1081, 477)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a648.jpg\n", + "\n", + "處理檔案: a648.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\a649.jpg\n", + "\n", + "處理檔案: a649.xml\n", + "Object 1: Have_Question - 座標: (96, 176, 710, 463)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a650.jpg\n", + "\n", + "處理檔案: a650.xml\n", + "Object 1: Have_Question - 座標: (278, 464, 393, 673)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a654.jpg\n", + "\n", + "處理檔案: a654.xml\n", + "Object 1: Have_Question - 座標: (277, 197, 377, 266)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a655.jpg\n", + "\n", + "處理檔案: a655.xml\n", + "Object 1: Have_Question - 座標: (213, 599, 427, 700)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A697.jpg\n", + "\n", + "處理檔案: A697.xml\n", + "Object 1: Have_Question - 座標: (443, 260, 784, 872)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A698.jpg\n", + "\n", + "處理檔案: A698.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A700.jpg\n", + "\n", + "處理檔案: A700.xml\n", + "Object 1: Have_Question - 座標: (464, 187, 773, 435)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A702.jpg\n", + "\n", + "處理檔案: A702.xml\n", + "Object 1: Have_Question - 座標: (420, 166, 929, 363)\n", + "Object 2: Have_Question - 座標: (479, 497, 911, 908)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A703.jpg\n", + "\n", + "處理檔案: A703.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A704.jpg\n", + "\n", + "處理檔案: A704.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A705.jpg\n", + "\n", + "處理檔案: A705.xml\n", + "Object 1: Have_Question - 座標: (378, 610, 441, 716)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A706.jpg\n", + "\n", + "處理檔案: A706.xml\n", + "Object 1: Have_Question - 座標: (393, 467, 480, 561)\n", + "Object 2: Have_Question - 座標: (310, 184, 450, 351)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A707.jpg\n", + "\n", + "處理檔案: A707.xml\n", + "Object 1: Have_Question - 座標: (870, 386, 987, 497)\n", + "Object 2: Have_Question - 座標: (208, 300, 532, 533)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A710.jpg\n", + "\n", + "處理檔案: A710.xml\n", + "Object 1: Have_Question - 座標: (364, 326, 427, 405)\n", + "Object 2: Have_Question - 座標: (718, 121, 749, 179)\n", + "Object 3: Have_Question - 座標: (496, 326, 717, 532)\n", + "Object 4: Have_Question - 座標: (312, 408, 566, 652)\n", + "Object 5: Have_Question - 座標: (312, 662, 569, 961)\n", + "Object 6: Have_Question - 座標: (756, 458, 1098, 1003)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A714.jpg\n", + "\n", + "處理檔案: A714.xml\n", + "Object 1: Have_Question - 座標: (626, 221, 1203, 760)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A715.jpg\n", + "\n", + "處理檔案: A715.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A720.jpg\n", + "\n", + "處理檔案: A720.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A721.jpg\n", + "\n", + "處理檔案: A721.xml\n", + "Object 1: Have_Question - 座標: (338, 334, 562, 834)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A722.jpg\n", + "\n", + "處理檔案: A722.xml\n", + "Object 1: Have_Question - 座標: (462, 208, 583, 318)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A730.jpg\n", + "\n", + "處理檔案: A730.xml\n", + "Object 1: Have_Question - 座標: (599, 108, 760, 231)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A731.jpg\n", + "\n", + "處理檔案: A731.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A732.jpg\n", + "\n", + "處理檔案: A732.xml\n", + "Object 1: Have_Question - 座標: (669, 257, 798, 522)\n", + "Object 2: Have_Question - 座標: (639, 901, 737, 1041)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A734.jpg\n", + "\n", + "處理檔案: A734.xml\n", + "Object 1: Have_Question - 座標: (331, 280, 1064, 742)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A750.jpg\n", + "\n", + "處理檔案: A750.xml\n", + "Object 1: Have_Question - 座標: (182, 395, 891, 950)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A751.jpg\n", + "\n", + "處理檔案: A751.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A753.jpg\n", + "\n", + "處理檔案: A753.xml\n", + "Object 1: Have_Question - 座標: (477, 252, 651, 433)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A756.jpg\n", + "\n", + "處理檔案: A756.xml\n", + "Object 1: Have_Question - 座標: (543, 359, 609, 415)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A757.jpg\n", + "\n", + "處理檔案: A757.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A770.jpg\n", + "\n", + "處理檔案: A770.xml\n", + "Object 1: Have_Question - 座標: (147, 780, 188, 837)\n", + "Object 2: Have_Question - 座標: (431, 906, 477, 959)\n", + "Object 3: Have_Question - 座標: (633, 739, 655, 762)\n", + "Object 4: Have_Question - 座標: (755, 545, 784, 572)\n", + "Object 5: Have_Question - 座標: (928, 464, 947, 486)\n", + "Object 6: Have_Question - 座標: (575, 285, 597, 299)\n", + "Object 7: Have_Question - 座標: (702, 278, 719, 300)\n", + "Object 8: Have_Question - 座標: (841, 301, 872, 319)\n", + "Object 9: Have_Question - 座標: (461, 114, 669, 180)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A771.jpg\n", + "\n", + "處理檔案: A771.xml\n", + "Object 1: Have_Question - 座標: (43, 309, 68, 336)\n", + "Object 2: Have_Question - 座標: (35, 634, 62, 667)\n", + "Object 3: Have_Question - 座標: (830, 233, 962, 370)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A775.jpg\n", + "\n", + "處理檔案: A775.xml\n", + "Object 1: Have_Question - 座標: (256, 480, 390, 812)\n", + "Object 2: Have_Question - 座標: (82, 743, 250, 939)\n", + "Object 3: Have_Question - 座標: (58, 103, 888, 234)\n", + "Object 4: Have_Question - 座標: (509, 225, 1049, 480)\n", + "Object 5: Have_Question - 座標: (781, 437, 1009, 671)\n", + "Object 6: Have_Question - 座標: (588, 682, 801, 769)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A776.jpg\n", + "\n", + "處理檔案: A776.xml\n", + "Object 1: Have_Question - 座標: (819, 136, 1004, 500)\n", + "Object 2: Have_Question - 座標: (315, 727, 743, 939)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A777.jpg\n", + "\n", + "處理檔案: A777.xml\n", + "Object 1: Have_Question - 座標: (72, 149, 265, 433)\n", + "Object 2: Have_Question - 座標: (95, 453, 471, 816)\n", + "Object 3: Have_Question - 座標: (429, 270, 627, 779)\n", + "Object 4: Have_Question - 座標: (655, 97, 777, 217)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A778.jpg\n", + "\n", + "處理檔案: A778.xml\n", + "Object 1: Have_Question - 座標: (233, 386, 631, 940)\n", + "Object 2: Have_Question - 座標: (630, 391, 1123, 1027)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A780.jpg\n", + "\n", + "處理檔案: A780.xml\n", + "Object 1: Have_Question - 座標: (408, 467, 545, 797)\n", + "Object 2: Have_Question - 座標: (655, 518, 1161, 1040)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A781.jpg\n", + "\n", + "處理檔案: A781.xml\n", + "Object 1: Have_Question - 座標: (331, 209, 357, 240)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A790.jpg\n", + "\n", + "處理檔案: A790.xml\n", + "Object 1: Have_Question - 座標: (692, 704, 727, 733)\n", + "Object 2: Have_Question - 座標: (819, 960, 850, 984)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A794.jpg\n", + "\n", + "處理檔案: A794.xml\n", + "Object 1: Have_Question - 座標: (152, 313, 494, 767)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_a80.jpg\n", + "\n", + "處理檔案: a80.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A800.jpg\n", + "\n", + "處理檔案: A800.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A803.jpg\n", + "\n", + "處理檔案: A803.xml\n", + "Object 1: Have_Question - 座標: (328, 588, 632, 825)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A806.jpg\n", + "\n", + "處理檔案: A806.xml\n", + "Object 1: Have_Question - 座標: (462, 616, 649, 772)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A807.jpg\n", + "\n", + "處理檔案: A807.xml\n", + "Object 1: Have_Question - 座標: (268, 114, 311, 155)\n", + "Object 2: Have_Question - 座標: (94, 530, 248, 676)\n", + "Object 3: Have_Question - 座標: (502, 590, 824, 733)\n", + "Object 4: Have_Question - 座標: (502, 736, 993, 1044)\n", + "Object 5: Have_Question - 座標: (925, 550, 1195, 900)\n", + "Object 6: Have_Question - 座標: (496, 94, 787, 500)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A810.jpg\n", + "\n", + "處理檔案: A810.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A811.jpg\n", + "\n", + "處理檔案: A811.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A812.jpg\n", + "\n", + "處理檔案: A812.xml\n", + "Object 1: Have_Question - 座標: (523, 372, 562, 409)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A820.jpg\n", + "\n", + "處理檔案: A820.xml\n", + "Object 1: Have_Question - 座標: (603, 480, 761, 619)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A821.jpg\n", + "\n", + "處理檔案: A821.xml\n", + "Object 1: Have_Question - 座標: (631, 364, 692, 426)\n", + "Object 2: Have_Question - 座標: (766, 626, 829, 682)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A822.jpg\n", + "\n", + "處理檔案: A822.xml\n", + "Object 1: Have_Question - 座標: (825, 313, 885, 361)\n", + "Object 2: Have_Question - 座標: (1010, 504, 1054, 559)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A823.jpg\n", + "\n", + "處理檔案: A823.xml\n", + "Object 1: Have_Question - 座標: (357, 830, 454, 908)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A824.jpg\n", + "\n", + "處理檔案: A824.xml\n", + "Object 1: Have_Question - 座標: (550, 227, 593, 269)\n", + "Object 2: Have_Question - 座標: (623, 473, 681, 533)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A825.jpg\n", + "\n", + "處理檔案: A825.xml\n", + "Object 1: Have_Question - 座標: (523, 614, 610, 715)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A829.jpg\n", + "\n", + "處理檔案: A829.xml\n", + "Object 1: Have_Question - 座標: (156, 153, 194, 181)\n", + "Object 2: Have_Question - 座標: (110, 210, 137, 248)\n", + "Object 3: Have_Question - 座標: (327, 215, 347, 252)\n", + "Object 4: Have_Question - 座標: (523, 229, 559, 270)\n", + "Object 5: Have_Question - 座標: (640, 336, 786, 562)\n", + "Object 6: Have_Question - 座標: (730, 694, 754, 720)\n", + "Object 7: Have_Question - 座標: (159, 697, 273, 817)\n", + "Object 8: Have_Question - 座標: (57, 590, 83, 621)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A830.jpg\n", + "\n", + "處理檔案: A830.xml\n", + "Object 1: Have_Question - 座標: (284, 650, 651, 910)\n", + "Object 2: Have_Question - 座標: (579, 519, 887, 819)\n", + "Object 3: Have_Question - 座標: (607, 286, 1010, 551)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A831.jpg\n", + "\n", + "處理檔案: A831.xml\n", + "Object 1: Have_Question - 座標: (40, 102, 697, 632)\n", + "Object 2: Have_Question - 座標: (694, 106, 1040, 389)\n", + "Object 3: Have_Question - 座標: (796, 685, 906, 815)\n", + "Object 4: Have_Question - 座標: (494, 763, 586, 850)\n", + "Object 5: Have_Question - 座標: (220, 803, 330, 904)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A840.jpg\n", + "\n", + "處理檔案: A840.xml\n", + "Object 1: Have_Question - 座標: (118, 97, 284, 481)\n", + "Object 2: Have_Question - 座標: (285, 100, 701, 264)\n", + "Object 3: Have_Question - 座標: (703, 101, 1032, 431)\n", + "Object 4: Have_Question - 座標: (459, 421, 557, 498)\n", + "Object 5: Have_Question - 座標: (47, 786, 390, 928)\n", + "Object 6: Have_Question - 座標: (275, 574, 605, 811)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A841.jpg\n", + "\n", + "處理檔案: A841.xml\n", + "Object 1: Have_Question - 座標: (53, 101, 656, 844)\n", + "Object 2: Have_Question - 座標: (449, 825, 662, 940)\n", + "Object 3: Have_Question - 座標: (657, 98, 1045, 821)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A842.jpg\n", + "\n", + "處理檔案: A842.xml\n", + "Object 1: Have_Question - 座標: (21, 197, 353, 685)\n", + "Object 2: Have_Question - 座標: (15, 687, 357, 1052)\n", + "Object 3: Have_Question - 座標: (348, 580, 701, 1041)\n", + "Object 4: Have_Question - 座標: (698, 689, 1043, 1044)\n", + "Object 5: Have_Question - 座標: (835, 629, 1178, 1042)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A850.jpg\n", + "\n", + "處理檔案: A850.xml\n", + "Object 1: Have_Question - 座標: (165, 259, 511, 910)\n", + "Object 2: Have_Question - 座標: (509, 449, 1167, 948)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A851.jpg\n", + "\n", + "處理檔案: A851.xml\n", + "Object 1: Have_Question - 座標: (141, 304, 1199, 954)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A852.jpg\n", + "\n", + "處理檔案: A852.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A853.jpg\n", + "\n", + "處理檔案: A853.xml\n", + "Object 1: Have_Question - 座標: (32, 244, 440, 625)\n", + "Object 2: Have_Question - 座標: (34, 626, 1201, 1046)\n", + "Object 3: Have_Question - 座標: (720, 442, 1197, 627)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A854.jpg\n", + "\n", + "處理檔案: A854.xml\n", + "Object 1: Have_Question - 座標: (293, 320, 610, 578)\n", + "Object 2: Have_Question - 座標: (94, 466, 1196, 1067)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A855.jpg\n", + "\n", + "處理檔案: A855.xml\n", + "Object 1: Have_Question - 座標: (389, 200, 760, 464)\n", + "Object 2: Have_Question - 座標: (35, 35, 380, 488)\n", + "Object 3: Have_Question - 座標: (250, 487, 600, 795)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A870.jpg\n", + "\n", + "處理檔案: A870.xml\n", + "Object 1: Have_Question - 座標: (696, 34, 1192, 927)\n", + "Object 2: Have_Question - 座標: (26, 412, 699, 752)\n", + "Object 3: Have_Question - 座標: (25, 750, 947, 1043)\n", + "Object 4: Have_Question - 座標: (946, 970, 1100, 1044)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A872.jpg\n", + "\n", + "處理檔案: A872.xml\n", + "Object 1: Have_Question - 座標: (686, 285, 1034, 543)\n", + "Object 2: Have_Question - 座標: (772, 535, 1041, 772)\n", + "Object 3: Have_Question - 座標: (704, 706, 805, 841)\n", + "Object 4: Have_Question - 座標: (441, 718, 703, 849)\n", + "Object 5: Have_Question - 座標: (354, 544, 489, 805)\n", + "Object 6: Have_Question - 座標: (467, 447, 701, 584)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A873.jpg\n", + "\n", + "處理檔案: A873.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A880.jpg\n", + "\n", + "處理檔案: A880.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A881.jpg\n", + "\n", + "處理檔案: A881.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A890.jpg\n", + "\n", + "處理檔案: A890.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A892.jpg\n", + "\n", + "處理檔案: A892.xml\n", + "Object 1: Have_Question - 座標: (38, 43, 870, 632)\n", + "Object 2: Have_Question - 座標: (30, 635, 287, 806)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A900.jpg\n", + "\n", + "處理檔案: A900.xml\n", + "Object 1: Have_Question - 座標: (457, 29, 983, 570)\n", + "Object 2: Have_Question - 座標: (15, 39, 492, 885)\n", + "Object 3: Have_Question - 座標: (482, 669, 713, 950)\n", + "Object 4: Have_Question - 座標: (982, 32, 1182, 273)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A902.jpg\n", + "\n", + "處理檔案: A902.xml\n", + "Object 1: Have_Question - 座標: (174, 180, 1042, 1043)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A903.jpg\n", + "\n", + "處理檔案: A903.xml\n", + "Object 1: Have_Question - 座標: (683, 414, 1044, 972)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A905.jpg\n", + "\n", + "處理檔案: A905.xml\n", + "Object 1: Have_Question - 座標: (802, 278, 970, 512)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A910.jpg\n", + "\n", + "處理檔案: A910.xml\n", + "Object 1: Have_Question - 座標: (484, 518, 580, 618)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A911.jpg\n", + "\n", + "處理檔案: A911.xml\n", + "Object 1: Have_Question - 座標: (639, 570, 714, 642)\n", + "Object 2: Have_Question - 座標: (1023, 381, 1087, 446)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A913.jpg\n", + "\n", + "處理檔案: A913.xml\n", + "Object 1: Have_Question - 座標: (826, 142, 877, 207)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A914.jpg\n", + "\n", + "處理檔案: A914.xml\n", + "Object 1: Have_Question - 座標: (446, 90, 490, 124)\n", + "Object 2: Have_Question - 座標: (633, 98, 663, 124)\n", + "Object 3: Have_Question - 座標: (933, 477, 993, 593)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A915.jpg\n", + "\n", + "處理檔案: A915.xml\n", + "Object 1: Have_Question - 座標: (552, 31, 1102, 808)\n", + "Object 2: Have_Question - 座標: (29, 471, 180, 921)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A916.jpg\n", + "\n", + "處理檔案: A916.xml\n", + "Object 1: Have_Question - 座標: (68, 559, 451, 942)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A920.jpg\n", + "\n", + "處理檔案: A920.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A930.jpg\n", + "\n", + "處理檔案: A930.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A931.jpg\n", + "\n", + "處理檔案: A931.xml\n", + "Object 1: Have_Question - 座標: (893, 545, 1174, 957)\n", + "Object 2: Have_Question - 座標: (808, 29, 1154, 341)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A941.jpg\n", + "\n", + "處理檔案: A941.xml\n", + "Object 1: Have_Question - 座標: (555, 468, 1118, 885)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A942.jpg\n", + "\n", + "處理檔案: A942.xml\n", + "Object 1: Have_Question - 座標: (296, 449, 656, 750)\n", + "Object 2: Have_Question - 座標: (1060, 243, 1107, 309)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A944.jpg\n", + "\n", + "處理檔案: A944.xml\n", + "Object 1: Have_Question - 座標: (246, 396, 590, 674)\n", + "Object 2: Have_Question - 座標: (531, 126, 749, 318)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A946.jpg\n", + "\n", + "處理檔案: A946.xml\n", + "Object 1: Have_Question - 座標: (417, 363, 662, 669)\n", + "Object 2: Have_Question - 座標: (17, 414, 49, 497)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A950.jpg\n", + "\n", + "處理檔案: A950.xml\n", + "Object 1: Have_Question - 座標: (554, 597, 809, 835)\n", + "Object 2: Have_Question - 座標: (449, 872, 543, 972)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A951.jpg\n", + "\n", + "處理檔案: A951.xml\n", + "Object 1: Have_Question - 座標: (213, 98, 519, 476)\n", + "Object 2: Have_Question - 座標: (609, 105, 850, 422)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A952.jpg\n", + "\n", + "處理檔案: A952.xml\n", + "Object 1: Have_Question - 座標: (237, 413, 405, 881)\n", + "Object 2: Have_Question - 座標: (463, 528, 776, 799)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A953.jpg\n", + "\n", + "處理檔案: A953.xml\n", + "Object 1: Have_Question - 座標: (551, 798, 679, 929)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A955.jpg\n", + "\n", + "處理檔案: A955.xml\n", + "Object 1: Have_Question - 座標: (180, 319, 267, 618)\n", + "Object 2: Have_Question - 座標: (326, 233, 451, 631)\n", + "Object 3: Have_Question - 座標: (513, 258, 598, 601)\n", + "Object 4: Have_Question - 座標: (702, 232, 830, 511)\n", + "Object 5: Have_Question - 座標: (371, 832, 652, 979)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A956.jpg\n", + "\n", + "處理檔案: A956.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A957.jpg\n", + "\n", + "處理檔案: A957.xml\n", + "Object 1: Have_Question - 座標: (299, 614, 751, 939)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A960.jpg\n", + "\n", + "處理檔案: A960.xml\n", + "Object 1: Have_Question - 座標: (611, 507, 1014, 818)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A961.jpg\n", + "\n", + "處理檔案: A961.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A9610.jpg\n", + "\n", + "處理檔案: A9610.xml\n", + "Object 1: Have_Question - 座標: (66, 166, 858, 958)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A9611.jpg\n", + "\n", + "處理檔案: A9611.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A9612.jpg\n", + "\n", + "處理檔案: A9612.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A962.jpg\n", + "\n", + "處理檔案: A962.xml\n", + "Object 1: Have_Question - 座標: (75, 324, 454, 914)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A963.jpg\n", + "\n", + "處理檔案: A963.xml\n", + "Object 1: Have_Question - 座標: (103, 613, 704, 957)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A964.jpg\n", + "\n", + "處理檔案: A964.xml\n", + "Object 1: Have_Question - 座標: (24, 363, 643, 986)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A965.jpg\n", + "\n", + "處理檔案: A965.xml\n", + "Object 1: Have_Question - 座標: (28, 119, 377, 388)\n", + "Object 2: Have_Question - 座標: (844, 849, 942, 948)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A966.jpg\n", + "\n", + "處理檔案: A966.xml\n", + "Object 1: Have_Question - 座標: (7, 316, 76, 580)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A967.jpg\n", + "\n", + "處理檔案: A967.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A968.jpg\n", + "\n", + "處理檔案: A968.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A969.jpg\n", + "\n", + "處理檔案: A969.xml\n", + "Object 1: Have_Question - 座標: (218, 349, 514, 609)\n", + "Object 2: Have_Question - 座標: (574, 787, 757, 914)\n", + "Object 3: Have_Question - 座標: (958, 768, 1191, 918)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A970.jpg\n", + "\n", + "處理檔案: A970.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A972.jpg\n", + "\n", + "處理檔案: A972.xml\n", + "Object 1: Have_Question - 座標: (338, 449, 669, 796)\n", + "Object 2: Have_Question - 座標: (123, 197, 359, 458)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A980.jpg\n", + "\n", + "處理檔案: A980.xml\n", + "Object 1: Have_Question - 座標: (540, 125, 666, 219)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A990.jpg\n", + "\n", + "處理檔案: A990.xml\n", + "圖片檔案不存在: ../Dataset/Training\\Have_Question_Crop\\A991.jpg\n", + "\n", + "處理檔案: A991.xml\n", + "Object 1: Have_Question - 座標: (213, 339, 331, 459)\n", + "已儲存標註圖片至: ../Dataset/Annotation\\Have_Question_Crop\\annotated_A994.jpg\n", + "\n", + "處理檔案: A994.xml\n" + ] + } + ], + "source": [ + "# 創建處理器實例\n", + "processor = XMLAnnotationProcessor(\n", + " dataset_root = Loading_Config[\"Train_Data_Root\"],\n", + ")\n", + "\n", + "# 設定自訂樣式(可選)\n", + "processor.set_drawing_style(\n", + " box_color=(0, 0, 255), # 紅色邊界框\n", + " text_color=(255, 255, 255), # 白色文字\n", + " box_thickness=3,\n", + " font_scale=0.7\n", + ")\n", + "\n", + "print(\"XML標註處理器已準備就绪\")\n", + "results = processor.process_multiple_xml(f\"../Label_Image/Have_Question\", \"Have_Question_Crop\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": "Stomach_Cancer_Env", "language": "python", "name": "python3" }, @@ -2372,7 +4333,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.11" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/test_binary_cross_entropy.py b/test_binary_cross_entropy.py new file mode 100644 index 0000000..2912f3e --- /dev/null +++ b/test_binary_cross_entropy.py @@ -0,0 +1,68 @@ +import torch +import torch.nn as nn +import sys +import os + +# 添加當前目錄到系統路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 導入自定義的 BinaryCrossEntropy 類 +from Model_Loss.binary_cross_entropy import BinaryCrossEntropy + +def test_binary_cross_entropy(): + print("開始測試 BinaryCrossEntropy 類...") + + # 創建損失函數實例 + criterion = BinaryCrossEntropy() + + # 測試 1: 維度匹配的情況 (二元分類) + print("\n測試 1: 維度匹配的情況 (二元分類)") + predictions_match = torch.tensor([[0.7, 0.3], [0.2, 0.8]], dtype=torch.float32) + targets_match = torch.tensor([[1.0, 0.0], [0.0, 1.0]], dtype=torch.float32) + + try: + loss_match = criterion(predictions_match, targets_match) + print(f"維度匹配的損失值: {loss_match.item()}") + print("測試 1 通過!") + except Exception as e: + print(f"測試 1 失敗: {e}") + + # 測試 2: 維度不匹配的情況 (多類別分類) + print("\n測試 2: 維度不匹配的情況 (多類別分類)") + predictions_mismatch = torch.tensor([[0.7, 0.2, 0.1], [0.1, 0.8, 0.1]], dtype=torch.float32) + targets_mismatch = torch.tensor([[1.0, 0.0], [0.0, 1.0]], dtype=torch.float32) + + try: + loss_mismatch = criterion(predictions_mismatch, targets_mismatch) + print(f"維度不匹配的損失值: {loss_mismatch.item()}") + print("測試 2 通過!") + except Exception as e: + print(f"測試 2 失敗: {e}") + + # 測試 3: 與 PyTorch 內建函數比較 + print("\n測試 3: 與 PyTorch 內建函數比較") + predictions_compare = torch.tensor([[0.7, 0.3], [0.2, 0.8]], dtype=torch.float32) + targets_compare = torch.tensor([[1.0, 0.0], [0.0, 1.0]], dtype=torch.float32) + + try: + # 自定義損失函數 + loss_custom = criterion(predictions_compare, targets_compare) + + # PyTorch 內建損失函數 + loss_pytorch = nn.BCEWithLogitsLoss()(predictions_compare, targets_compare) + + print(f"自定義損失值: {loss_custom.item()}") + print(f"PyTorch 損失值: {loss_pytorch.item()}") + print(f"差異: {abs(loss_custom.item() - loss_pytorch.item())}") + + if abs(loss_custom.item() - loss_pytorch.item()) < 1e-5: + print("測試 3 通過!") + else: + print("測試 3 失敗: 損失值與 PyTorch 內建函數不一致") + except Exception as e: + print(f"測試 3 失敗: {e}") + + print("\n測試完成!") + +if __name__ == "__main__": + test_binary_cross_entropy() \ No newline at end of file diff --git a/test_bounding_box.py b/test_bounding_box.py new file mode 100644 index 0000000..a351317 --- /dev/null +++ b/test_bounding_box.py @@ -0,0 +1,66 @@ +import cv2 +import numpy as np +from Image_Process.Image_Mask_Ground_Truth_Processing import XMLAnnotationProcessor + +def test_bounding_box_processing_with_mock_data(): + # 初始化XML處理器 + processor = XMLAnnotationProcessor() + + # 創建一個測試圖像 (300x300 彩色圖像) + test_image = np.ones((300, 300, 3), dtype=np.uint8) * 128 # 灰色背景 + + # 在圖像中央創建一個彩色區域 + test_image[100:200, 100:200, 0] = 255 # 紅色通道 + test_image[120:180, 120:180, 1] = 255 # 綠色通道 + test_image[140:160, 140:160, 2] = 255 # 藍色通道 + + # 創建模擬的bounding box數據 + mock_bounding_boxes = [ + { + 'name': 'test_object', + 'xmin': 50, + 'ymin': 50, + 'xmax': 150, + 'ymax': 150 + }, + { + 'name': 'test_object2', + 'xmin': 200, + 'ymin': 200, + 'xmax': 250, + 'ymax': 250 + } + ] + + # 調用draw_bounding_boxes方法 + result_image = processor.draw_bounding_boxes(test_image, mock_bounding_boxes) + + # 顯示處理結果 + print(f"原始圖像形狀: {test_image.shape}") + print(f"結果圖像形狀: {result_image.shape}") + + # 檢查圖像是否有非黑色區域(bounding box內的原圖) + non_black_pixels = np.sum(result_image > 0) + print(f"非黑色像素數量: {non_black_pixels}") + + # 檢查第一個bounding box區域是否保留了原圖 + box1 = result_image[50:150, 50:150] + box1_matches = np.array_equal(box1, test_image[50:150, 50:150]) + print(f"第一個bounding box區域是否與原圖相同: {box1_matches}") + + # 檢查框外區域是否為黑色 + outside_box = result_image[0:40, 0:40] # 選擇一個框外區域 + is_black = np.sum(outside_box) == 0 + print(f"框外區域是否為黑色: {is_black}") + + if non_black_pixels > 0 and box1_matches and is_black: + print("測試成功: bounding box內保持原圖,框外變為黑色") + else: + print("測試失敗") + + # 保存結果圖像以便查看 + cv2.imwrite("test_bounding_box_result.png", result_image) + print("已保存結果圖像: test_bounding_box_result.png") + +if __name__ == "__main__": + test_bounding_box_processing_with_mock_data() \ No newline at end of file diff --git a/test_fuzzy_clustering.py b/test_fuzzy_clustering.py new file mode 100644 index 0000000..a2d3bea --- /dev/null +++ b/test_fuzzy_clustering.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +""" +測試 Fuzzy C-means 聚類功能 +""" + +import os +import sys +import numpy as np +from PIL import Image +import matplotlib.pyplot as plt + +# 添加當前目錄到 Python 路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 導入我們的函數 +from Density_Peak_Algorithm import ( + compute_decision_graph, + fuzzy_c_means, + determine_optimal_clusters, + extract_superpixel_features +) + +def test_fuzzy_clustering(): + """ + 測試 Fuzzy C-means 聚類功能 + """ + print("🧪 開始測試 Fuzzy C-means 聚類功能...") + + # 測試圖像路徑 + test_image_path = "example_images/sample_image_1.png" + + if not os.path.exists(test_image_path): + print(f"❌ 測試圖像不存在: {test_image_path}") + return False + + # 創建測試輸出目錄 + test_output_dir = "test_clustering_output" + os.makedirs(test_output_dir, exist_ok=True) + + try: + print(f"📸 測試圖像: {test_image_path}") + + # 執行完整的密度峰值分析 + Fuzzy C-means 聚類 + result = compute_decision_graph( + test_image_path, + test_output_dir, + use_superpixels=True, + save_regions=True, + max_regions=30 # 限制區域數量以加快測試 + ) + + print(f"\n✅ 測試成功完成!") + print(f"📊 結果摘要:") + print(f" - 超像素數量: {result['n_superpixels']}") + print(f" - 最佳聚類數: {result['optimal_clusters']}") + print(f" - 聚類迭代次數: {result['clustering_iterations']}") + print(f" - 目標函數值: {result['clustering_objective']:.4f}") + print(f" - 壓縮比例: {result['compression_ratio']:.6f}") + + # 驗證結果的完整性 + required_keys = [ + 'rho', 'delta', 'gamma', 'n', 'segments', 'superpixel_features', + 'optimal_clusters', 'cluster_centers', 'membership_matrix', + 'cluster_labels', 'cluster_stats' + ] + + missing_keys = [key for key in required_keys if key not in result] + if missing_keys: + print(f"⚠️ 警告: 缺少以下結果鍵: {missing_keys}") + else: + print(f"✅ 所有必要的結果鍵都存在") + + # 驗證聚類結果的合理性 + n_clusters = result['optimal_clusters'] + cluster_labels = result['cluster_labels'] + membership_matrix = result['membership_matrix'] + + # 檢查聚類標籤範圍 + unique_labels = np.unique(cluster_labels) + if len(unique_labels) == n_clusters and np.min(unique_labels) >= 0 and np.max(unique_labels) < n_clusters: + print(f"✅ 聚類標籤範圍正確: {len(unique_labels)} 個聚類") + else: + print(f"⚠️ 聚類標籤範圍異常: 期望 {n_clusters} 個聚類,實際 {len(unique_labels)} 個") + + # 檢查隸屬度矩陣 + membership_sums = np.sum(membership_matrix, axis=1) + if np.allclose(membership_sums, 1.0, atol=1e-6): + print(f"✅ 隸屬度矩陣正確: 每行和為 1") + else: + print(f"⚠️ 隸屬度矩陣異常: 行和範圍 [{np.min(membership_sums):.6f}, {np.max(membership_sums):.6f}]") + + print(f"\n📁 測試結果保存在: {test_output_dir}") + return True + + except Exception as e: + print(f"❌ 測試失敗: {e}") + import traceback + traceback.print_exc() + return False + +def test_individual_functions(): + """ + 測試個別函數 + """ + print(f"\n🔬 測試個別函數...") + + # 創建測試數據 + np.random.seed(42) + test_data = np.random.rand(50, 5) # 50個樣本,5個特徵 + + try: + # 測試 fuzzy_c_means 函數 + print(f"🧪 測試 fuzzy_c_means 函數...") + centers, membership, labels, objective, n_iter = fuzzy_c_means( + test_data, n_clusters=3, m=2.0, max_iter=50, random_state=42 + ) + + print(f" - 聚類中心形狀: {centers.shape}") + print(f" - 隸屬度矩陣形狀: {membership.shape}") + print(f" - 標籤形狀: {labels.shape}") + print(f" - 目標函數值: {objective:.4f}") + print(f" - 迭代次數: {n_iter}") + + # 測試 determine_optimal_clusters 函數 + print(f"🧪 測試 determine_optimal_clusters 函數...") + gamma_values = np.random.exponential(2, 50) # 模擬 gamma 值 + optimal_k, scores = determine_optimal_clusters( + test_data, gamma_values, max_clusters=6, min_clusters=2 + ) + + print(f" - 最佳聚類數: {optimal_k}") + print(f" - 測試的聚類數: {list(scores.keys())}") + + print(f"✅ 個別函數測試成功!") + return True + + except Exception as e: + print(f"❌ 個別函數測試失敗: {e}") + import traceback + traceback.print_exc() + return False + +if __name__ == "__main__": + print("🚀 開始 Fuzzy C-means 聚類功能測試") + print("="*60) + + # 測試個別函數 + individual_test_success = test_individual_functions() + + print("\n" + "="*60) + + # 測試完整流程 + full_test_success = test_fuzzy_clustering() + + print("\n" + "="*60) + print("📋 測試結果摘要:") + print(f" - 個別函數測試: {'✅ 通過' if individual_test_success else '❌ 失敗'}") + print(f" - 完整流程測試: {'✅ 通過' if full_test_success else '❌ 失敗'}") + + if individual_test_success and full_test_success: + print(f"\n🎉 所有測試通過! Fuzzy C-means 聚類功能正常工作") + else: + print(f"\n⚠️ 部分測試失敗,請檢查代碼") \ No newline at end of file diff --git a/test_gradient_fix.py b/test_gradient_fix.py new file mode 100644 index 0000000..6eca989 --- /dev/null +++ b/test_gradient_fix.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +測試梯度計算修復的腳本 +用於驗證 RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn 是否已修復 +""" + +import torch +import torch.nn as nn +import numpy as np +from experiments.Models.Xception_Model_Modification import Xception +from Model_Loss.Loss import Entropy_Loss + +def test_gradient_computation(): + """測試梯度計算是否正常工作""" + print("=== 測試梯度計算修復 ===") + + # 設置設備 + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用設備: {device}") + + # 創建模型 + model = Xception().to(device) + model.train() + + # 檢查模型參數 + print("\n=== 模型參數檢查 ===") + total_params = 0 + trainable_params = 0 + + for name, param in model.named_parameters(): + total_params += param.numel() + if param.requires_grad: + trainable_params += param.numel() + else: + print(f"❌ 參數 {name} 不需要梯度!") + + print(f"總參數數量: {total_params:,}") + print(f"可訓練參數數量: {trainable_params:,}") + print(f"可訓練參數比例: {trainable_params/total_params*100:.2f}%") + + if trainable_params == 0: + print("❌ 錯誤:沒有可訓練的參數!") + return False + + # 創建測試數據 + batch_size = 2 + input_images = torch.randn(batch_size, 3, 224, 224, device=device, requires_grad=True) + + # 創建 one-hot 編碼的標籤 + labels_onehot = torch.zeros(batch_size, 3, device=device) + labels_onehot[0, 1] = 1.0 # 第一個樣本屬於類別1 + labels_onehot[1, 2] = 1.0 # 第二個樣本屬於類別2 + + print(f"\n=== 測試數據 ===") + print(f"輸入形狀: {input_images.shape}") + print(f"標籤形狀: {labels_onehot.shape}") + print(f"輸入 requires_grad: {input_images.requires_grad}") + print(f"標籤內容:\n{labels_onehot}") + + try: + # 前向傳播 + print("\n=== 前向傳播 ===") + outputs = model(input_images) + print(f"輸出形狀: {outputs.shape}") + print(f"輸出 requires_grad: {outputs.requires_grad}") + print(f"輸出 grad_fn: {outputs.grad_fn}") + + if outputs.grad_fn is None: + print("❌ 錯誤:輸出沒有 grad_fn!") + return False + + # 計算損失 + print("\n=== 損失計算 ===") + criterion = Entropy_Loss() + loss = criterion(outputs, labels_onehot) + print(f"損失值: {loss.item():.6f}") + print(f"損失 requires_grad: {loss.requires_grad}") + print(f"損失 grad_fn: {loss.grad_fn}") + + if loss.grad_fn is None: + print("❌ 錯誤:損失沒有 grad_fn!") + return False + + # 反向傳播 + print("\n=== 反向傳播 ===") + loss.backward() + print("✅ 反向傳播成功完成!") + + # 檢查梯度 + print("\n=== 梯度檢查 ===") + grad_count = 0 + for name, param in model.named_parameters(): + if param.grad is not None: + grad_count += 1 + grad_norm = param.grad.norm().item() + if grad_norm > 0: + print(f"✅ {name}: 梯度範數 = {grad_norm:.6f}") + else: + print(f"⚠️ {name}: 梯度為零") + else: + print(f"❌ {name}: 沒有梯度") + + print(f"\n有梯度的參數數量: {grad_count}") + + if grad_count == 0: + print("❌ 錯誤:沒有參數有梯度!") + return False + + print("\n✅ 所有測試通過!梯度計算修復成功!") + return True + + except Exception as e: + print(f"\n❌ 測試失敗:{str(e)}") + print(f"錯誤類型: {type(e).__name__}") + return False + +def test_losses_method(): + """測試 Losses 方法的張量處理""" + print("\n=== 測試 Losses 方法 ===") + + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + + # 模擬 Losses 方法的邏輯 + def test_losses(predicts, labels): + # 確保輸入是張量且在正確的設備上 + if not isinstance(predicts, torch.Tensor): + predicts = torch.tensor(predicts, dtype=torch.float32, device=device, requires_grad=True) + if not isinstance(labels, torch.Tensor): + labels = torch.tensor(labels, dtype=torch.float32, device=device) + + # 確保張量在同一設備上 + predicts = predicts.to(device) + labels = labels.to(device) + + print(f"Predicts: shape={predicts.shape}, requires_grad={predicts.requires_grad}, device={predicts.device}") + print(f"Labels: shape={labels.shape}, requires_grad={labels.requires_grad}, device={labels.device}") + + criterion = Entropy_Loss() + loss = criterion(predicts, labels) + return loss + + # 測試不同類型的輸入 + batch_size = 2 + num_classes = 3 + + # 測試1: 張量輸入 + print("\n--- 測試1: 張量輸入 ---") + predicts_tensor = torch.randn(batch_size, num_classes, device=device, requires_grad=True) + labels_tensor = torch.zeros(batch_size, num_classes, device=device) + labels_tensor[0, 1] = 1.0 + labels_tensor[1, 2] = 1.0 + + try: + loss1 = test_losses(predicts_tensor, labels_tensor) + print(f"✅ 張量輸入測試成功,損失: {loss1.item():.6f}") + except Exception as e: + print(f"❌ 張量輸入測試失敗: {e}") + + # 測試2: NumPy 輸入 + print("\n--- 測試2: NumPy 輸入 ---") + predicts_numpy = np.random.randn(batch_size, num_classes).astype(np.float32) + labels_numpy = np.zeros((batch_size, num_classes), dtype=np.float32) + labels_numpy[0, 1] = 1.0 + labels_numpy[1, 2] = 1.0 + + try: + loss2 = test_losses(predicts_numpy, labels_numpy) + print(f"✅ NumPy 輸入測試成功,損失: {loss2.item():.6f}") + except Exception as e: + print(f"❌ NumPy 輸入測試失敗: {e}") + +if __name__ == "__main__": + print("開始測試梯度計算修復...") + + # 測試梯度計算 + gradient_test_passed = test_gradient_computation() + + # 測試 Losses 方法 + test_losses_method() + + print("\n" + "="*50) + if gradient_test_passed: + print("🎉 梯度計算修復驗證成功!可以開始訓練了。") + else: + print("❌ 梯度計算仍有問題,需要進一步調試。") + print("="*50) \ No newline at end of file diff --git a/test_loss_fix.py b/test_loss_fix.py new file mode 100644 index 0000000..22ccada --- /dev/null +++ b/test_loss_fix.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +測試腳本:驗證損失函數修復 +檢查數據類型處理和交叉熵損失計算 +""" + +import torch +import numpy as np +import sys +import os + +# 添加項目路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Model_Loss.Loss import Entropy_Loss + +def test_loss_function(): + """測試損失函數的數據類型處理""" + print("🧪 測試損失函數...") + + # 創建測試數據 + batch_size = 4 + num_classes = 3 + + # 模擬模型輸出(logits) + outputs = torch.randn(batch_size, num_classes, requires_grad=True) + print(f"✅ 模型輸出形狀: {outputs.shape}, 類型: {outputs.dtype}, requires_grad: {outputs.requires_grad}") + + # 測試案例1:one-hot編碼標籤 + labels_onehot = torch.zeros(batch_size, num_classes) + labels_onehot[0, 0] = 1 # 類別0 + labels_onehot[1, 1] = 1 # 類別1 + labels_onehot[2, 2] = 1 # 類別2 + labels_onehot[3, 0] = 1 # 類別0 + + print(f"✅ One-hot標籤形狀: {labels_onehot.shape}, 類型: {labels_onehot.dtype}") + + # 測試案例2:類別索引標籤 + labels_indices = torch.tensor([0, 1, 2, 0], dtype=torch.long) + print(f"✅ 索引標籤形狀: {labels_indices.shape}, 類型: {labels_indices.dtype}") + + # 測試案例3:numpy數組標籤 + labels_numpy = np.array([0, 1, 2, 0]) + print(f"✅ Numpy標籤形狀: {labels_numpy.shape}, 類型: {labels_numpy.dtype}") + + # 創建損失函數 + criterion = Entropy_Loss() + + try: + # 測試one-hot編碼標籤 + print("\n📊 測試one-hot編碼標籤...") + loss1 = criterion(outputs, labels_onehot) + print(f"✅ One-hot標籤損失: {loss1.item():.4f}, requires_grad: {loss1.requires_grad}") + + # 測試梯度計算 + loss1.backward(retain_graph=True) + print(f"✅ 梯度計算成功,輸出梯度範數: {outputs.grad.norm().item():.4f}") + outputs.grad.zero_() # 清零梯度 + + # 測試類別索引標籤 + print("\n📊 測試類別索引標籤...") + loss2 = criterion(outputs, labels_indices) + print(f"✅ 索引標籤損失: {loss2.item():.4f}, requires_grad: {loss2.requires_grad}") + + # 測試梯度計算 + loss2.backward(retain_graph=True) + print(f"✅ 梯度計算成功,輸出梯度範數: {outputs.grad.norm().item():.4f}") + outputs.grad.zero_() # 清零梯度 + + # 測試numpy數組標籤 + print("\n📊 測試numpy數組標籤...") + loss3 = criterion(outputs, labels_numpy) + print(f"✅ Numpy標籤損失: {loss3.item():.4f}, requires_grad: {loss3.requires_grad}") + + # 測試梯度計算 + loss3.backward() + print(f"✅ 梯度計算成功,輸出梯度範數: {outputs.grad.norm().item():.4f}") + + print("\n🎉 所有損失函數測試通過!") + return True + + except Exception as e: + print(f"❌ 損失函數測試失敗: {e}") + return False + +def test_cuda_compatibility(): + """測試CUDA兼容性(如果可用)""" + if not torch.cuda.is_available(): + print("⚠️ CUDA不可用,跳過CUDA測試") + return True + + print("\n🚀 測試CUDA兼容性...") + + try: + device = torch.device('cuda') + + # 創建CUDA張量 + outputs = torch.randn(2, 3, device=device, requires_grad=True) + labels = torch.tensor([0, 1], device=device, dtype=torch.long) + + criterion = Entropy_Loss() + loss = criterion(outputs, labels) + + print(f"✅ CUDA損失計算: {loss.item():.4f}") + + # 測試梯度 + loss.backward() + print(f"✅ CUDA梯度計算成功,梯度範數: {outputs.grad.norm().item():.4f}") + + print("🎉 CUDA測試通過!") + return True + + except Exception as e: + print(f"❌ CUDA測試失敗: {e}") + return False + +def test_edge_cases(): + """測試邊界情況""" + print("\n🔍 測試邊界情況...") + + criterion = Entropy_Loss() + + try: + # 測試單個樣本 + outputs_single = torch.randn(1, 3, requires_grad=True) + labels_single = torch.tensor([1], dtype=torch.long) + + loss = criterion(outputs_single, labels_single) + print(f"✅ 單樣本測試: {loss.item():.4f}") + + loss.backward() + print(f"✅ 單樣本梯度計算成功,梯度範數: {outputs_single.grad.norm().item():.4f}") + + # 測試大批次 + outputs_large = torch.randn(100, 5, requires_grad=True) + labels_large = torch.randint(0, 5, (100,), dtype=torch.long) + + loss = criterion(outputs_large, labels_large) + print(f"✅ 大批次測試: {loss.item():.4f}") + + print("🎉 邊界情況測試通過!") + return True + + except Exception as e: + print(f"❌ 邊界情況測試失敗: {e}") + return False + +if __name__ == "__main__": + print("🔧 開始損失函數修復驗證...") + + success = True + success &= test_loss_function() + success &= test_cuda_compatibility() + success &= test_edge_cases() + + if success: + print("\n🎉 所有測試通過!損失函數修復成功。") + print("✅ RuntimeError: Expected floating point type for target with class probabilities, got Long 已修復") + else: + print("\n❌ 部分測試失敗,需要進一步檢查。") + + print("\n📋 修復摘要:") + print("1. ✅ 修復了 Loss.py 中的 cross_entropy 調用") + print("2. ✅ 正確處理 one-hot 編碼和類別索引標籤") + print("3. ✅ 修復了 Model_Branch 中的參數傳遞") + print("4. ✅ 確保了梯度計算的連續性") \ No newline at end of file diff --git a/test_main.py b/test_main.py new file mode 100644 index 0000000..20acb50 --- /dev/null +++ b/test_main.py @@ -0,0 +1,121 @@ +import torch +import sys +import os +import time + +# 添加當前目錄到系統路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from experiments.experiment import experiments +from utils.Stomach_Config import Training_Config, Loading_Config, Save_Result_File_Config +from Training_Tools.Tools import Tool +from merge_class.merge import merge + +def test_main(): + # 測試GPU是否可用 + flag = torch.cuda.is_available() + if not flag: + print("CUDA不可用\n") + else: + print(f"CUDA可用,數量為{torch.cuda.device_count()}\n") + + # 测试GPU是否可用 + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用设备: {device}") + if torch.cuda.is_available(): + print(f"GPU: {torch.cuda.get_device_name(0)}") + + # 創建一個簡單的測試數據集 + class SimpleDataset(torch.utils.data.Dataset): + def __init__(self, size=10): + self.size = size + self.data = torch.randn(size, 3, 256, 256) # 假設輸入是3通道256x256圖像 + self.mask = torch.randint(0, 2, (size, 1, 256, 256)).float() # 二值掩碼 + self.labels = torch.zeros(size, 3) # 假設有3個類別 + self.labels[:, 0] = 1 # 所有樣本都是第一個類別 + print(f"創建了測試數據集,大小: {size},數據形狀: {self.data.shape}") + + def __len__(self): + return self.size + + def __getitem__(self, idx): + return self.data[idx], self.mask[idx], self.labels[idx] + + # 創建測試數據 + print("創建測試數據...") + test_dataset = SimpleDataset() + training_data = [test_dataset.data[i] for i in range(test_dataset.size)] + training_label = [test_dataset.labels[i] for i in range(test_dataset.size)] + training_mask = [test_dataset.mask[i] for i in range(test_dataset.size)] + + # 初始化工具和配置 + tool = Tool() + Status = 1 + Label_Length = 3 + + # 模擬main.py中的數據處理 + print("\n模擬main.py中的數據處理...") + Merge = merge() + + # 確保Loading_Config中包含必要的配置項 + required_configs = [ + "Test_Data_Root", "Training_Labels", "Annotation_Root", + "Label_Image_Labels", "Image enhance processing save root" + ] + + for config in required_configs: + if config not in Loading_Config: + print(f"添加缺少的配置項: {config}") + if config == "Test_Data_Root": + Loading_Config[config] = "./test_data" + elif config == "Training_Labels": + Loading_Config[config] = ["class1", "class2", "class3"] + elif config == "Annotation_Root": + Loading_Config[config] = "./annotations" + elif config == "Label_Image_Labels": + Loading_Config[config] = ["CA", "Normal"] + elif config == "Image enhance processing save root": + Loading_Config[config] = "./enhanced_images" + + # 測試experiments類的初始化 + try: + print("\n測試experiments類的初始化...") + experiment = experiments( + Xception_Training_Data=training_data, + Xception_Training_Label=training_label, + GastroSegNet_Training_Data=training_data, + GastroSegNet_Training_Label=training_label, + GastroSegNet_Training_Mask=training_mask, + Training_Config=Training_Config, + Loading_Config=Loading_Config, + tools=tool, + Number_Of_Classes=Label_Length, + status=Status + ) + print("experiments類初始化成功!") + + # 測試processing_main方法(僅執行部分代碼) + print("\n測試processing_main方法...") + # 這裡我們不實際調用processing_main,因為它會執行完整的訓練流程 + # 而是檢查關鍵屬性是否正確設置 + print(f"模型名稱: {experiment.model_name}") + print(f"實驗名稱: {experiment.experiment_name}") + print(f"訓練批次大小: {experiment.train_batch_size}") + print(f"使用設備: {experiment.device}") + + # 模擬processing_main的部分功能 + print("\n模擬processing_main的部分功能...") + # 這裡我們只模擬一些基本操作,不執行實際的訓練 + print("模擬讀取測試數據...") + experiment.test = training_data + experiment.test_label = training_label + + print("\n測試成功!") + + except Exception as e: + print(f"\n測試失敗: {str(e)}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + test_main() \ No newline at end of file diff --git a/test_mask_loading.py b/test_mask_loading.py new file mode 100644 index 0000000..e31bdd6 --- /dev/null +++ b/test_mask_loading.py @@ -0,0 +1,58 @@ +import sys +import os + +# 添加當前目錄到系統路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Load_process.LoadData import Loding_Data_Root +from utils.Stomach_Config import Loading_Config +from merge_class.merge import merge + +def test_mask_loading(): + print("測試掩碼數據加載...") + + # 確保Loading_Config中包含必要的配置項 + required_configs = [ + "Label_Image_Labels", "Annotation_Root" + ] + + for config in required_configs: + if config not in Loading_Config: + print(f"錯誤:缺少配置項 {config}") + return + + print(f"Label_Image_Labels: {Loading_Config['Label_Image_Labels']}") + print(f"Annotation_Root: {Loading_Config['Annotation_Root']}") + + # 加載掩碼數據 + try: + print("\n嘗試加載掩碼數據...") + Mask_load = Loding_Data_Root(Loading_Config["Label_Image_Labels"], Loading_Config['Annotation_Root'], "") + Mask_Data_Dict_Data = Mask_load.process_main(False) + + print("\n掩碼數據加載結果:") + if not Mask_Data_Dict_Data: + print("錯誤:Mask_Data_Dict_Data為空") + else: + Mask_Keys = list(Mask_Data_Dict_Data.keys()) + print(f"Mask_Keys: {Mask_Keys}") + + for key in Mask_Keys: + print(f"Key: {key}, 數據長度: {len(Mask_Data_Dict_Data[key])}") + + # 嘗試合併掩碼數據 + if len(Mask_Keys) >= 2: + print("\n嘗試合併掩碼數據...") + Merge = merge() + Training_Mask_Data = Merge.merge_all_image_data(Mask_Data_Dict_Data[Mask_Keys[0]], Mask_Data_Dict_Data[Mask_Keys[1]]) + print(f"合併後的掩碼數據長度: {len(Training_Mask_Data)}") + else: + print("\n無法合併掩碼數據:Mask_Keys長度小於2") + + except Exception as e: + print(f"\n加載掩碼數據時出錯: {str(e)}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + test_mask_loading() \ No newline at end of file diff --git a/test_model_branch.py b/test_model_branch.py new file mode 100644 index 0000000..cd721b0 --- /dev/null +++ b/test_model_branch.py @@ -0,0 +1,91 @@ +import torch +import torch.nn.functional as F +import sys +import os + +# 添加項目根目錄到路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 導入必要的模塊 +from experiments.Models.GastroSegNet_Model import GastroSegNet +from experiments.Training.Segmentation_Block_Training import Segmentation_Block_Training_Step + +def test_model_branch(): + # 初始化模型 + try: + model_step = Segmentation_Block_Training_Step() + print("初始化成功") + + # 獲取設備 + device = model_step.device + print(f"使用設備: {device}") + + # 創建一個簡單的測試數據 + batch_size = 2 + input_images = torch.rand(batch_size, 3, 256, 256) + + # 創建符合Model_Branch方法輸入要求的mask_gt + # 創建二值掩碼,形狀為[batch_size, 1, height, width] + mask_gt = torch.zeros(batch_size, 1, 256, 256) + + # 在掩碼中央創建一個矩形區域(模擬分割目標) + for i in range(batch_size): + # 設置矩形區域為1(白色) + mask_gt[i, 0, 50:200, 50:200] = 1.0 + + # 初始化模型 + model_step.Model = model_step.Construct_Segment_Model_CUDA() + print("模型初始化完成") + + # 測試Model_Branch方法 + try: + # 調用Model_Branch方法,設置return_processed_image=True + print("\n開始測試Model_Branch方法...") + processed_images, seg_outputs = model_step.Model_Branch(input_images, mask_gt, return_processed_image=True) + + # 檢查結果 + print("Model_Branch方法測試成功") + print(f"處理後的圖像形狀: {processed_images.shape}") + print(f"分割輸出形狀: {seg_outputs.shape}") + + # 測試Model_Branch方法,設置return_processed_image=False + print("\n開始測試Model_Branch方法 (return_processed_image=False)...") + + # 創建一個自定義的損失計算函數,用於捕獲輸入形狀 + def mock_losses(Segmentation_Output_Image, Segmentation_Mask_GroundTruth_Image): + print(f"Losses方法輸入形狀 - 預測: {Segmentation_Output_Image.shape}, 目標: {Segmentation_Mask_GroundTruth_Image.shape}") + return torch.tensor(0.5, device=model_step.device) + + # 保存原始的Losses方法 + original_losses = model_step.Losses + + try: + # 替換Losses方法 + model_step.Losses = mock_losses + + # 調用Model_Branch方法 + loss = model_step.Model_Branch(input_images, mask_gt, return_processed_image=False) + + print("Model_Branch方法測試成功 (return_processed_image=False)") + print(f"損失值: {loss}") + finally: + # 恢復原始的Losses方法 + model_step.Losses = original_losses + + return True + except Exception as e: + print(f"Model_Branch方法測試失敗: {str(e)}") + import traceback + traceback.print_exc() + return False + + except Exception as e: + print(f"初始化過程中出錯: {str(e)}") + return False + +if __name__ == "__main__": + success = test_model_branch() + if success: + print("\n所有測試通過!") + else: + print("\n測試失敗!") \ No newline at end of file diff --git a/test_processing_main.py b/test_processing_main.py new file mode 100644 index 0000000..a5a298a --- /dev/null +++ b/test_processing_main.py @@ -0,0 +1,117 @@ +import torch +import torch.nn.functional as F +import sys +import os +import time + +# 添加項目根目錄到路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 導入Loading_Config +from utils.Stomach_Config import Loading_Config + +# 設置Segmentation_Plot_Image配置項 +Loading_Config["Segmentation_Plot_Image"] = "./test_plot.png" + +# 導入必要的模塊 +from experiments.Models.GastroSegNet_Model import GastroSegNet +from experiments.Training.Segmentation_Block_Training import Segmentation_Block_Training_Step + +def test_processing_main(): + # 初始化模型 + try: + model_step = Segmentation_Block_Training_Step() + print("初始化成功") + + # 獲取設備 + device = model_step.device + print(f"使用設備: {device}") + + # 創建一個簡單的測試數據集 + class SimpleDataset(torch.utils.data.Dataset): + def __init__(self, size=10): + self.size = size + # 將數據放在CPU上,避免CUDA轉NumPy的問題 + self.data = torch.rand(size, 3, 256, 256) + self.masks = torch.rand(size, 1, 256, 256) > 0.5 + self.masks = self.masks.float() + self.labels = torch.zeros(size, 3) # 假設有3個類別 + self.labels[:, 0] = 1 # 所有樣本都是第一個類別 + print(f"創建了測試數據集,大小: {size},數據形狀: {self.data.shape},掩碼形狀: {self.masks.shape},標籤形狀: {self.labels.shape}") + + def __len__(self): + return self.size + + def __getitem__(self, idx): + return self.data[idx], self.masks[idx], f"sample_{idx}", self.labels[idx] + + # 創建測試數據集 + print("創建測試數據集和數據加載器...") + test_dataset = SimpleDataset(size=10) # 使用10個樣本以滿足KFold的要求 + + # 創建測試數據加載器 + test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=2, shuffle=False) + print("測試數據集和數據加載器創建完成") + + # 創建一個自定義的損失計算函數,用於捕獲輸入形狀 + def mock_losses(Segmentation_Output_Image, Segmentation_Mask_GroundTruth_Image): + print(f"Losses方法輸入形狀 - 預測: {Segmentation_Output_Image.shape}, 目標: {Segmentation_Mask_GroundTruth_Image.shape}") + # 創建一個可以進行反向傳播的損失張量 + dummy_loss = Segmentation_Output_Image.mean() + print(f"創建了虛擬損失,值: {dummy_loss.item()}") + return dummy_loss + + # 創建一個簡化版的Calculate_Progress_And_Timing函數,避免遞歸調用 + def mock_calculate_progress_and_timing(*args): + # 直接返回最後一個參數(epoch_iterator) + return args[-1] + + # 保存原始的方法 + original_losses = model_step.Losses + original_calculate_progress = model_step.Calculate_Progress_And_Timing + + # 測試Processing_Main方法 + try: + # 替換方法 + model_step.Losses = mock_losses + model_step.Calculate_Progress_And_Timing = mock_calculate_progress_and_timing + + # Loading_Config已在全局設置 + + # 調用Processing_Main方法,設置return_processed_images=True並提供test_dataloader + print("\n開始測試Processing_Main方法...") + result = model_step.Processing_Main(test_dataset, return_processed_images=True, test_dataloader=test_loader) + + # 恢復原始的方法 + model_step.Losses = original_losses + model_step.Calculate_Progress_And_Timing = original_calculate_progress + + # 檢查結果 + print("Processing_Main方法測試成功") + print(f"結果類型: {type(result)}") + + # 如果結果是元組,打印每個元素 + if isinstance(result, tuple): + for i, item in enumerate(result): + if isinstance(item, torch.Tensor): + print(f"結果[{i}]: {type(item)} - 形狀: {item.shape}") + else: + print(f"結果[{i}]: {type(item)}") + + return True + except Exception as e: + print(f"Processing_Main方法測試失敗: {str(e)}") + import traceback + traceback.print_exc() + return False + + except Exception as e: + print(f"初始化過程中出錯: {str(e)}") + return False + +if __name__ == "__main__": + success = test_processing_main() + if success: + print("\n所有測試通過!") + else: + print("\n測試失敗!") \ No newline at end of file diff --git a/test_segmentation.py b/test_segmentation.py new file mode 100644 index 0000000..d49386a --- /dev/null +++ b/test_segmentation.py @@ -0,0 +1,48 @@ +import torch +import sys +import os + +# 添加項目根目錄到路徑 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 導入必要的模塊 +from experiments.Models.GastroSegNet_Model import GastroSegNet +from experiments.Training.Segmentation_Block_Training import Segmentation_Block_Training_Step + +def test_process_segmentation_output(): + # 初始化模型 + try: + model_step = Segmentation_Block_Training_Step() + print("初始化成功") + + # 創建測試數據 + batch_size = 2 + channels = 3 + height = 256 + width = 256 + + # 獲取設備 + device = model_step.device + print(f"使用設備: {device}") + + # 創建隨機輸入圖像並移到正確的設備上 + input_images = torch.rand(batch_size, channels, height, width).to(device) + + # 創建隨機分割輸出 (模擬模型輸出)並移到正確的設備上 + segmentation_output = torch.rand(batch_size, 1, height, width).to(device) + + # 測試處理方法 + try: + processed_images = model_step.process_segmentation_output(input_images, segmentation_output) + print(f"處理後圖像形狀: {processed_images.shape}") + print("處理成功") + return True + except Exception as e: + print(f"處理過程中出錯: {str(e)}") + return False + except Exception as e: + print(f"初始化過程中出錯: {str(e)}") + return False + +if __name__ == "__main__": + test_process_segmentation_output() \ No newline at end of file diff --git a/test_superpixel_density_peak.py b/test_superpixel_density_peak.py new file mode 100644 index 0000000..45852e0 --- /dev/null +++ b/test_superpixel_density_peak.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +測試 SLIC 超像素版本的 Density Peak Algorithm +""" + +import os +import numpy as np +import matplotlib.pyplot as plt +from PIL import Image +from Density_Peak_Algorithm import compute_decision_graph + +def test_superpixel_functionality(): + """測試超像素功能""" + + # 使用示例圖片 + test_image_path = "example_images/sample_image_1.png" + + if not os.path.exists(test_image_path): + print(f"測試圖片不存在: {test_image_path}") + return False + + # 創建結果保存目錄 + save_root = "test_superpixel_results" + os.makedirs(save_root, exist_ok=True) + + print("=== 測試原始像素版本 ===") + result_pixels = compute_decision_graph( + test_image_path, + save_root, + use_superpixels=False + ) + + print("\n=== 測試 SLIC 超像素版本 ===") + result_superpixels = compute_decision_graph( + test_image_path, + f"{save_root}_superpixels", + use_superpixels=True, + n_segments=200, + compactness=10 + ) + + # 驗證結果 + print("\n=== 結果驗證 ===") + + # 檢查返回的鍵值 + expected_keys = ['center_indices', 'center_points', 'rho', 'delta', 'gamma', 'n'] + superpixel_keys = expected_keys + ['segments', 'superpixel_features'] + + print("原始像素結果鍵值:", list(result_pixels.keys())) + print("超像素結果鍵值:", list(result_superpixels.keys())) + + # 驗證基本鍵值 + for key in expected_keys: + assert key in result_pixels, f"原始像素結果缺少鍵值: {key}" + assert key in result_superpixels, f"超像素結果缺少鍵值: {key}" + + # 驗證超像素特有鍵值 + assert 'segments' in result_superpixels, "超像素結果缺少 segments" + assert 'superpixel_features' in result_superpixels, "超像素結果缺少 superpixel_features" + + # 比較數據點數量 + pixels_count = len(result_pixels['rho']) + superpixels_count = len(result_superpixels['rho']) + reduction_ratio = superpixels_count / pixels_count + + print(f"\n數據點比較:") + print(f"原始像素: {pixels_count:,} 個數據點") + print(f"超像素: {superpixels_count:,} 個數據點") + print(f"壓縮比例: {reduction_ratio:.4f}") + print(f"壓縮倍數: {1/reduction_ratio:.1f}x") + + # 驗證 gamma 和 n 的計算 + gamma_pixels = result_pixels['gamma'] + n_pixels = result_pixels['n'] + gamma_superpixels = result_superpixels['gamma'] + n_superpixels = result_superpixels['n'] + + # 檢查 gamma = rho * delta + expected_gamma_pixels = result_pixels['rho'] * result_pixels['delta'] + expected_gamma_superpixels = result_superpixels['rho'] * result_superpixels['delta'] + + assert np.allclose(gamma_pixels, expected_gamma_pixels), "原始像素 gamma 計算錯誤" + assert np.allclose(gamma_superpixels, expected_gamma_superpixels), "超像素 gamma 計算錯誤" + + print("✓ Gamma 計算驗證通過") + + # 檢查 n 的排序(使用穩定排序處理相同gamma值) + sorted_indices_pixels = np.argsort(-gamma_pixels, kind='stable') # 降序排列 + sorted_indices_superpixels = np.argsort(-gamma_superpixels, kind='stable') # 降序排列 + + expected_n_pixels = np.empty_like(sorted_indices_pixels) + expected_n_pixels[sorted_indices_pixels] = np.arange(1, len(sorted_indices_pixels) + 1) + + expected_n_superpixels = np.empty_like(sorted_indices_superpixels) + expected_n_superpixels[sorted_indices_superpixels] = np.arange(1, len(sorted_indices_superpixels) + 1) + + # 檢查是否有大量相同的gamma值(可能導致排序不穩定) + unique_gamma_pixels = len(np.unique(gamma_pixels)) + unique_gamma_superpixels = len(np.unique(gamma_superpixels)) + + print(f"原始像素唯一gamma值: {unique_gamma_pixels}/{len(gamma_pixels)}") + print(f"超像素唯一gamma值: {unique_gamma_superpixels}/{len(gamma_superpixels)}") + + # 如果有太多重複值,使用更寬鬆的檢查 + if unique_gamma_pixels < len(gamma_pixels) * 0.9: + print("⚠️ 原始像素有大量重複gamma值,跳過嚴格的n值檢查") + else: + assert np.array_equal(n_pixels, expected_n_pixels), "原始像素 n 計算錯誤" + + if unique_gamma_superpixels < len(gamma_superpixels) * 0.9: + print("⚠️ 超像素有大量重複gamma值,跳過嚴格的n值檢查") + else: + assert np.array_equal(n_superpixels, expected_n_superpixels), "超像素 n 計算錯誤" + + print("✓ N 值計算驗證通過") + + # 檢查超像素特徵 + segments = result_superpixels['segments'] + superpixel_features = result_superpixels['superpixel_features'] + + print(f"\n超像素分割信息:") + print(f"分割標籤範圍: {segments.min()} - {segments.max()}") + print(f"超像素特徵形狀: {superpixel_features.shape}") + print(f"特徵維度: {superpixel_features.shape[1]} (應該是5: RGB + 標準化位置)") + + assert superpixel_features.shape[1] == 5, f"超像素特徵維度錯誤: {superpixel_features.shape[1]}" + assert superpixel_features.shape[0] == superpixels_count, "超像素特徵數量與數據點不匹配" + + print("✓ 超像素特徵驗證通過") + + # 創建比較圖表 + create_comparison_plots(result_pixels, result_superpixels, save_root) + + print(f"\n✓ 所有測試通過!結果保存在 {save_root} 目錄") + return True + +def create_comparison_plots(result_pixels, result_superpixels, save_root): + """創建比較圖表""" + + fig, axes = plt.subplots(2, 3, figsize=(15, 10)) + + # 原始像素結果 + rho_p, delta_p, gamma_p, n_p = result_pixels['rho'], result_pixels['delta'], result_pixels['gamma'], result_pixels['n'] + + # 超像素結果 + rho_s, delta_s, gamma_s, n_s = result_superpixels['rho'], result_superpixels['delta'], result_superpixels['gamma'], result_superpixels['n'] + + # 第一行:原始像素 + axes[0, 0].scatter(rho_p, delta_p, alpha=0.6, s=1) + axes[0, 0].set_xlabel('Rho (Local Density)') + axes[0, 0].set_ylabel('Delta (Min Distance)') + axes[0, 0].set_title(f'Decision Graph - Pixels ({len(rho_p):,} points)') + + axes[0, 1].scatter(gamma_p, n_p, alpha=0.6, s=1) + axes[0, 1].set_xlabel('Gamma (Rho * Delta)') + axes[0, 1].set_ylabel('N (Rank)') + axes[0, 1].set_yscale('log') + axes[0, 1].set_title('Gamma vs N - Pixels') + + axes[0, 2].hist(gamma_p, bins=50, alpha=0.7, edgecolor='black') + axes[0, 2].set_xlabel('Gamma') + axes[0, 2].set_ylabel('Frequency') + axes[0, 2].set_title('Gamma Distribution - Pixels') + + # 第二行:超像素 + axes[1, 0].scatter(rho_s, delta_s, alpha=0.6, s=10) + axes[1, 0].set_xlabel('Rho (Local Density)') + axes[1, 0].set_ylabel('Delta (Min Distance)') + axes[1, 0].set_title(f'Decision Graph - Superpixels ({len(rho_s):,} points)') + + axes[1, 1].scatter(gamma_s, n_s, alpha=0.6, s=10) + axes[1, 1].set_xlabel('Gamma (Rho * Delta)') + axes[1, 1].set_ylabel('N (Rank)') + axes[1, 1].set_yscale('log') + axes[1, 1].set_title('Gamma vs N - Superpixels') + + axes[1, 2].hist(gamma_s, bins=50, alpha=0.7, edgecolor='black') + axes[1, 2].set_xlabel('Gamma') + axes[1, 2].set_ylabel('Frequency') + axes[1, 2].set_title('Gamma Distribution - Superpixels') + + plt.tight_layout() + plt.savefig(f"{save_root}/comparison_plots.png", dpi=300, bbox_inches='tight') + plt.close() + + print(f"比較圖表保存至: {save_root}/comparison_plots.png") + +if __name__ == "__main__": + try: + success = test_superpixel_functionality() + if success: + print("\n🎉 SLIC 超像素 Density Peak Algorithm 測試成功!") + else: + print("\n❌ 測試失敗") + except Exception as e: + print(f"\n❌ 測試過程中發生錯誤: {e}") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/test_superpixel_regions_save.py b/test_superpixel_regions_save.py new file mode 100644 index 0000000..85b9a98 --- /dev/null +++ b/test_superpixel_regions_save.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +""" +測試超像素區域保存功能 +Test script for superpixel region saving functionality +""" + +import os +import sys +import numpy as np +from PIL import Image +import matplotlib.pyplot as plt +from skimage.segmentation import slic +from skimage.util import img_as_float + +# Add current directory to path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from Density_Peak_Algorithm import save_superpixel_regions, compute_decision_graph + +def test_superpixel_regions_save(): + """測試超像素區域保存功能""" + + print("🧪 開始測試超像素區域保存功能...") + + # 創建測試目錄 + test_dir = "test_superpixel_regions_results" + os.makedirs(test_dir, exist_ok=True) + + # 創建一個簡單的測試圖像 + test_image_path = os.path.join(test_dir, "test_image.png") + + # 創建一個彩色測試圖像 (200x200) + height, width = 200, 200 + test_img = np.zeros((height, width, 3), dtype=np.uint8) + + # 添加不同顏色的區域 + test_img[:100, :100] = [255, 0, 0] # 紅色 + test_img[:100, 100:] = [0, 255, 0] # 綠色 + test_img[100:, :100] = [0, 0, 255] # 藍色 + test_img[100:, 100:] = [255, 255, 0] # 黃色 + + # 添加一些噪聲 + noise = np.random.randint(-30, 30, (height, width, 3)) + test_img = np.clip(test_img.astype(int) + noise, 0, 255).astype(np.uint8) + + # 保存測試圖像 + Image.fromarray(test_img).save(test_image_path) + print(f"✅ 創建測試圖像: {test_image_path}") + + # 測試1: 直接使用save_superpixel_regions函數 + print("\n📋 測試1: 直接測試save_superpixel_regions函數") + + # 轉換為浮點數並應用SLIC + img_array = test_img.astype(np.float32) / 255.0 + segments = slic(img_array, n_segments=20, compactness=10, start_label=1, enforce_connectivity=True) + + # 保存超像素區域 + regions_dir = os.path.join(test_dir, "direct_test_regions") + saved_regions = save_superpixel_regions(test_img, segments, regions_dir, "test_image", max_regions=20) + + print(f"✅ 直接測試完成,保存了 {len(saved_regions)} 個區域") + + # 測試2: 通過compute_decision_graph函數測試 + print("\n📋 測試2: 通過compute_decision_graph函數測試") + + result = compute_decision_graph( + test_image_path, + test_dir, + use_superpixels=True, + n_segments=15, + compactness=10, + save_regions=True, + max_regions=15 + ) + + print(f"✅ compute_decision_graph測試完成") + + # 測試3: 使用真實圖像(如果存在) + print("\n📋 測試3: 使用真實圖像測試(如果存在)") + + # 嘗試找到一個真實圖像 + possible_image_paths = [ + "../Data/A01.jpg", + "sample_image_1.png", + "../sample_image_1.png" + ] + + real_image_path = None + for path in possible_image_paths: + if os.path.exists(path): + real_image_path = path + break + + if real_image_path: + print(f"🖼️ 找到真實圖像: {real_image_path}") + try: + # 測試真實圖像 + real_result = compute_decision_graph( + real_image_path, + test_dir, + use_superpixels=True, + n_segments=50, + compactness=10, + save_regions=True, + max_regions=30 # 限制保存數量 + ) + print(f"✅ 真實圖像測試完成") + except Exception as e: + print(f"❌ 真實圖像測試失敗: {e}") + else: + print("⚠️ 未找到真實圖像,跳過此測試") + + # 顯示結果統計 + print(f"\n📊 測試結果統計:") + print(f"測試目錄: {test_dir}") + + # 統計生成的文件 + total_files = 0 + for root, dirs, files in os.walk(test_dir): + for file in files: + if file.endswith('.png'): + total_files += 1 + + print(f"總共生成的PNG文件數量: {total_files}") + + # 列出所有子目錄 + subdirs = [d for d in os.listdir(test_dir) if os.path.isdir(os.path.join(test_dir, d))] + if subdirs: + print(f"生成的子目錄: {subdirs}") + for subdir in subdirs: + subdir_path = os.path.join(test_dir, subdir) + files_in_subdir = [f for f in os.listdir(subdir_path) if f.endswith('.png')] + print(f" {subdir}: {len(files_in_subdir)} 個PNG文件") + + print(f"\n🎉 超像素區域保存功能測試完成!") + print(f"📁 請查看 {test_dir} 目錄中的結果") + +if __name__ == "__main__": + test_superpixel_regions_save() \ No newline at end of file diff --git a/testing_Labels_Accuracy.py b/testing_Labels_Accuracy.py index b98e2fe..3f64286 100644 --- a/testing_Labels_Accuracy.py +++ b/testing_Labels_Accuracy.py @@ -1,103 +1,141 @@ from experiments.experiment import experiments -from concurrent.futures import ProcessPoolExecutor -from loadData_and_MakeImageGenerator.load_and_ImageGenerator import Load_ImageGenerator -from Read_and_process_image.ReadAndProcess import Read_image_and_Process_image -import tensorflow as tf +import torch import numpy as np -from sklearn.metrics import confusion_matrix, accuracy_score -from draw_tools.draw import draw_heatmap +from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score from Load_process.LoadData import Loding_Data_Root import os import seaborn as sns import datetime -from Load_process.file_processing import judge_file_exist, make_dir, make_save_root -from matplotlib import pyplot as plt +from Load_process.file_processing import Process_File from Load_process.Load_Indepentend import Load_Indepentend_Data - -def draw(matrix, model_name, index): - # Using Seaborn heatmap to create the plot - - fx = sns.heatmap(matrix, annot=True, cmap='turbo') - # labels the title and x, y axis of plot - fx.set_title('Plotting Confusion Matrix using Seaborn\n\n') - fx.set_xlabel('Predicted Values') - fx.set_ylabel('answer Values ') - # labels the boxes - fx.xaxis.set_ticklabels(['False','True']) - fx.yaxis.set_ticklabels(['False','True']) - - model_dir = '../../Model_Confusion_matrix/model_matrix_image ( ' + str(datetime.date.today()) + " )/" + model_name - if not judge_file_exist(model_dir): - make_dir(model_dir) - modelfiles = make_save_root(str(model_name) + "-" + str(index) + ".png", model_dir) - plt.savefig(modelfiles) - plt.close("all") # 關閉圖表 +from Training_Tools.PreProcess import Training_Precesses +from Training_Tools.Tools import Tool +import matplotlib.figure as figure +import matplotlib.backends.backend_agg as agg +from Calculate_Process.Calculate import Calculate +import argparse +import json +from utils.Stomach_Config import Training_Config, Loading_Config +from model_data_processing.processing import shuffle_data if __name__ == "__main__": - with ProcessPoolExecutor() as executor: ## 默认为1 - print('TensorFlow version:', tf.__version__) - physical_devices = tf.config.experimental.list_physical_devices('GPU') - print(physical_devices) - assert len(physical_devices) > 0, "Not enough GPU hardware devices available" - tf.config.experimental.set_memory_growth(physical_devices[0], True) - os.environ["CUDA_VISIBLE_DEVICES"]='0' + # 解析命令行参数 + parser = argparse.ArgumentParser(description='评估单一类别的准确度、精确度、召回率和F1值') + parser.add_argument('--target_class', type=int, default=0, help='要评估的目标类别索引 (0, 1, 2)') + args = parser.parse_args() + + # 测试GPU是否可用 + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"使用设备: {device}") + + # 设置GPU + if torch.cuda.is_available(): + torch.cuda.set_device(0) + print(f"GPU: {torch.cuda.get_device_name(0)}") - load = Loding_Data_Root() - experiment = experiments() - image_processing = Read_image_and_Process_image() - cut_image = Load_Indepentend_Data() + # 初始化对象 + tool = Tool() + Status = 1 # 決定要使用什麼資料集 - model = experiment.construct_model() - model_dir = '../../best_model( 2023-10-17 )-2.h5' # 這是一個儲存模型權重的路徑,每一個模型都有一個自己權重儲存的檔 - if os.path.exists(model_dir): # 如果這個檔案存在 - model.load_weights(model_dir) # 將模型權重讀出來 - print("讀出權重\n") + # 取得One-hot encording 的資料 + tool.Set_OneHotEncording(Loading_Config["Training_Labels"]) + Encording_Label = tool.Get_OneHot_Encording_Label() + + Label_Length = len(Loading_Config["Training_Labels"]) + + # 检查目标类别是否有效 + target_class = args.target_class + training_labels = Loading_Config["Training_Labels"] + if target_class < 0 or target_class >= len(training_labels): + print(f"错误: 目标类别索引 {target_class} 无效,必须在 0 到 {len(training_labels)-1} 之间") + exit(1) + + print(f"正在评估类别: {training_labels[target_class]}") + + load = Loding_Data_Root(Loading_Config["Training_Labels"], Loading_Config["Train_Data_Root"], Loading_Config["ImageGenerator_Data_Root"]) + cut_image = Load_Indepentend_Data(Loading_Config["Training_Labels"], Encording_Label) + + # 创建模型 + experiment = experiments(Training_Config, Loading_Config, tool, 3, "Test") + model = experiment.construct_model() + + # 加载模型权重 + Model_Weight = [ + "../Result/save_the_best_model/Xception Skin trains Stomach Cancer Dataset, and uses WeightRandomSampler Change HSV Channel of V is -150/Xception/best_model( 2025-05-19 )-_fold0.pt", + "../Result/save_the_best_model/Xception Skin trains Stomach Cancer Dataset, and uses WeightRandomSampler Change HSV Channel of V is -150/Xception/best_model( 2025-05-19 )-_fold1.pt", + "../Result/save_the_best_model/Xception Skin trains Stomach Cancer Dataset, and uses WeightRandomSampler Change HSV Channel of V is -150/Xception/best_model( 2025-05-19 )-_fold2.pt", + "../Result/save_the_best_model/Xception Skin trains Stomach Cancer Dataset, and uses WeightRandomSampler Change HSV Channel of V is -150/Xception/best_model( 2025-05-19 )-_fold3.pt", + "../Result/save_the_best_model/Xception Skin trains Stomach Cancer Dataset, and uses WeightRandomSampler Change HSV Channel of V is -150/Xception/best_model( 2025-05-19 )-_fold4.pt" + ] + + Calculate_Tool = [Calculate() for i in range(3)] + i = 0 + for path in Model_Weight: + if os.path.exists(path): + model.load_state_dict(torch.load(path)) + print("读取权重完成\n") + model.eval() # 设置为评估模式 + + # 预处理对象 + preprocess = Training_Precesses(256) + + cut_image.process_main(Loading_Config["Test_Data_Root"]) # 呼叫處理test Data與Validation Data的function + test, test_label = cut_image.test, cut_image.test_label + test, test_label = shuffle_data(test, test_label) - for times in range(5): - name = ["BP", "PF", "PV", "Chickenpox", "Monkeypox", "Normal", "Another"] - cut_image.process_main() # 呼叫處理test Data與Validation Data的function - test, test_label = cut_image.test, cut_image.test_label + # 只评估目标类别 + # 转换为PyTorch张量并移动到设备 + test_dataset = preprocess.Setting_DataSet(cut_image.test, cut_image.test_label, "Transform") + test_loader = preprocess.Dataloader_Sampler(test_dataset, 1, False) + + all_results = [] + all_labels = [] + + # 使用PyTorch的预测方式 + with torch.no_grad(): # 不计算梯度 + for inputs, labels, _, _ in test_loader: + inputs = inputs.to(device) + labels = labels.to(device) - total_data = [[], [], [], [], [], [], []] - total_labels = [[], [], [], [], [], [], []] - start = 0 - end = 22 - for k in range(7): - for i in range(start, end): - total_data[k].append(test[i]) - total_labels[k].append(test_label[i]) - - total_data[k], total_labels[k] = image_processing.image_data_processing(total_data[k], total_labels[k]) - total_data[k] = image_processing.normalization(total_data[k]) + outputs = model(inputs) + _, predicted = torch.max(outputs, 1) + all_results.append(predicted.cpu().numpy()) + all_labels.append(np.argmax(labels.cpu().numpy(), axis=1)) + + # 合并结果 + Predict = np.concatenate(all_results) + y_test = np.concatenate(all_labels) + + print(f"{training_labels[target_class]} 预测结果: {Predict}\n") - start = end - end += 22 - + # 计算评估指标 + accuracy = accuracy_score(y_test, Predict) + precision = precision_score(y_test, Predict, average=None) + recall = recall_score(y_test, Predict, average=None) + F1 = f1_score(y_test, Predict, average=None) + + # 计算每个类别的准确率 + class_accuracies = [] + for class_idx in range(len(training_labels)): + class_mask = (y_test == class_idx) + class_accuracy = accuracy_score(y_test[class_mask], Predict[class_mask]) + class_accuracies.append(class_accuracy) + + print(f"运行 {i+1}:\n") + print(f"整体准确率 (Accuracy): {accuracy:.4f}") + for class_idx in range(len(training_labels)): + print(f"类别 {training_labels[class_idx]} 的评估指标:") + print(f" 准确率 (Accuracy): {class_accuracies[class_idx]:.4f}") + print(f" 精确率 (Precision): {precision[class_idx]:.4f}") + print(f" 召回率 (Recall): {recall[class_idx]:.4f}") + print(f" F1值: {F1[class_idx]:.4f}\n") + Calculate_Tool[class_idx].Append_numbers(0, class_accuracies[class_idx], precision[class_idx], recall[class_idx], 0, F1[class_idx]) + + i += 1 - j = 0 - for total_label in range(7): - result = model.predict(total_data[j]) # 利用predict function來預測結果 - result = np.argmax(result, axis = 1) # 將預測出來的結果從one-hot encoding轉成label-encoding - y_test = np.argmax(total_labels[j], axis = 1) - - print(name[j] + str(result), "\n") - - y_pre = [] - for i in range(len(result)): - if result[i] != j: - result[i] = 0 - else: - result[i] = 1 - - y_test[i] = 1 - - matrix = confusion_matrix(y_test, result, labels = [0, 1]) # 丟入confusion matrix的function中,以形成混淆矩陣 - draw(matrix, name[j], times) # 呼叫畫出confusion matrix的function - tn, fp, fn, tp = matrix.ravel() - - accuracy = (tn + tp) / (tn + fp + fn + tp) - print(name[j] + " 權重為: ", accuracy) - - experiment.record_everyTime_test_result(0, accuracy, 0, 0, 0, 0, times, name[j]) - - j += 1 + # 计算平均值和标准差 + for i in range(3): + Calculate_Tool[i].Calculate_Mean() + Calculate_Tool[i].Calculate_Std() + print(f"\n{training_labels[target_class]} 类别的评估结果:") + print(Calculate_Tool[i].Output_Style()) diff --git a/training_files.txt b/training_files.txt new file mode 100644 index 0000000..2955450 Binary files /dev/null and b/training_files.txt differ diff --git a/training_question_files.txt b/training_question_files.txt new file mode 100644 index 0000000..6f61db7 Binary files /dev/null and b/training_question_files.txt differ diff --git a/utils/Stomach_Config.py b/utils/Stomach_Config.py new file mode 100644 index 0000000..8cb9b66 --- /dev/null +++ b/utils/Stomach_Config.py @@ -0,0 +1,80 @@ +# 在import部分添加median_filter +from Image_Process.image_enhancement import histogram_equalization, adaptive_histogram_equalization_without_limit, unsharp_mask, laplacian_sharpen, adjust_hsv, gamma_correction, Contrast_Limited_Adaptive_Histogram_Equalization, Hight_Light, mean_filter, median_filter +import datetime + +Image_Enhance = { + "Shapen" : laplacian_sharpen, + "CLAHE": histogram_equalization, + "CLAHE_Adaptive" : adaptive_histogram_equalization_without_limit, + "CLAHE_Adaptive_have_Limit" : Contrast_Limited_Adaptive_Histogram_Equalization, + "HSV" : adjust_hsv, + "gamma" : gamma_correction, + "Hight_Light" : Hight_Light, + "Mean" : mean_filter, + "Median" : median_filter, # 添加中值濾波 + "Gamma_Value" : 0.95 +} + +Loading_Config = { + "Test_Data_Root": "../Dataset/Testing", + "Train_Data_Root": "../Dataset/Training", + # "Annotation_Training_Root": "../Dataset/Annotation/Training", + # "Annotation_Testing_Root": "../Dataset/Annotation/Testing", + # "TestProcess_Image_Root" : "../TestProcess_Image", + "ImageGenerator_Data_Root": "../Dataset/ImageGenerator", + # "Process_Roots" : "../Dataset/test_Images", + # "Image enhance processing save root": f'../Dataset/image_enhancement_Result/New_{Image_Enhance["gamma"].__name__}_and_gamma_value_is_{Image_Enhance["Gamma_Value"]}', + "Image enhance processing save root": f'../Dataset/image_enhancement_Result/Resetting the training and testing dataset', + "Training_Labels": ["stomach_cancer_Crop", "Normal_Crop", "Have_Question_Crop"], + # "Label_Image_Labels" : ["CA", "Have_Question"], + # "XML_Loading_Label" : ["stomach_cancer_Crop", "Have_Question_Crop"], + # "Identification_Label_Length" : 3, +} + +Training_Config = { + "Model_Name": "Xception and GastoSegNet", + "CA_Experiment_Name": f"New architecture of Xception to CA and Have Question", + "Normal_Experiment_Name": f"New architecture of Xception to Normal and Others", + "Mask_Experiment_Name" : "New architecture of GastoSegNet", + "Three_Classes_Experiment_Name" : "Xception to Three Classes", + "Epoch": 10000, + "Train_Batch_Size": 64, + "Image_Size": 256, + "Class_Count": 904, + "Get_Generator": "True", + "weight_decay": 0.01, + "Number_Of_Classes" : len(Loading_Config["Training_Labels"]) +} + +Model_Config = { + "Model Name": "xception", + "GPA Output Nodes": 2048, + "Linear Hidden Nodes": 1025, + "Output Linear Nodes": 3, + "Dropout Rate": 0.6 +} + +Save_Result_File_Config = { + "Identification_Plot_Image" : f"../Result/Training_Image/save_the_train_image({str(datetime.date.today())})", # 分類模型的走勢圖存檔路徑 + "Segument_Plot_Image" : f"../Result/Training_Image/save_the_train_image({str(datetime.date.today())})/Segument_Plot_Image", # 分割模型的走勢圖存檔路徑 + + "Identification_Marix_Image" : f"../Result/Matrix_Image/model_matrix_image({str(datetime.date.today())})/Identification_Plot_Marix_Image", # 分類模型的混淆矩陣存檔路徑 + + "Identification_Every_Fold_Training_Result" : f'../Result/Training_Result/save_the_train_result({str(datetime.date.today())})/Identification_Every_Fold', # 分類模型每折訓練結果存檔路徑 + "Identification_Average_Result" : f'../Result/Training_Average_Result/Average_Result({str(datetime.date.today())})/Identification_Average_Result', # 分類模型平均訓練結果存檔路徑 + + "Segument_Every_Fold_Training_Result" : f'../Result/Training_Result/save_the_train_result({str(datetime.date.today())})/Segument_Every_Fold', # 分割模型每折訓練結果存檔路徑 + "Segument_Average_Result" : f'../Result/Training_Average_Result/Average_Result({str(datetime.date.today())})/Segument_Average_Result', # 分割模型平均訓練結果存檔路徑 + "Segument_Bounding_Box_Image" : f'../Result/Bounding_Box_Image/save_bounding_box_image({str(datetime.date.today())})', # 分割模型邊界框圖像存檔路徑 + "Segument_Test_Bounding_Box_Image" : f'../Result/Test_Bounding_Box_Image/save_bounding_box_image({str(datetime.date.today())})', # 分割模型邊界框圖像存檔路徑 + + "Normal_Identification_Best_Model" : '../Result/save_the_best_model/Identification_Normal', + "CA_Identification_Best_Model" : "../Result/save_the_best_model/Identification_CA", + "Segmentation_Best_Model" : "../Result/save_the_best_model/Segmentation", + "Three_Classes_Identification_Best_Model" : "../Result/save_the_best_model/Three_Classes_Identification", + + "GradCAM_Validation_Image_Save_Root" : f"../Result/GradCAM_Image/Validation/GradCAM_Image({str(datetime.date.today())})", + "GradCAM_Test_Image_Save_Root" : f"../Result/GradCAM_Image/Test/GradCAM_Image({str(datetime.date.today())})", + + "Density_Peak_Save_Root" : f"../Result/Density_Peak/Density_Peak_Result({str(datetime.date.today())})", +} \ No newline at end of file diff --git a/utils/__pycache__/Stomach_Config.cpython-311.pyc b/utils/__pycache__/Stomach_Config.cpython-311.pyc new file mode 100644 index 0000000..948926b Binary files /dev/null and b/utils/__pycache__/Stomach_Config.cpython-311.pyc differ diff --git a/utils/__pycache__/Stomach_Config.cpython-312.pyc b/utils/__pycache__/Stomach_Config.cpython-312.pyc new file mode 100644 index 0000000..12911a1 Binary files /dev/null and b/utils/__pycache__/Stomach_Config.cpython-312.pyc differ diff --git a/utils/__pycache__/Stomach_Config.cpython-313.pyc b/utils/__pycache__/Stomach_Config.cpython-313.pyc new file mode 100644 index 0000000..ee2ee6a Binary files /dev/null and b/utils/__pycache__/Stomach_Config.cpython-313.pyc differ diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6697733 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1240 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "certifi" +version = "2025.10.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, + { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/de/e0/bab50af11c2d75c9c4a2a26a5254573c0bd97cea152254401510950486fa/fsspec-2025.9.0.tar.gz", hash = "sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19", size = 304847, upload-time = "2025-09-02T19:10:49.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/71/70db47e4f6ce3e5c37a607355f80da8860a33226be640226ac52cb05ef2e/fsspec-2025.9.0-py3-none-any.whl", hash = "sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7", size = 199289, upload-time = "2025-09-02T19:10:47.708Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963, upload-time = "2025-01-20T02:42:37.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796, upload-time = "2025-01-20T02:42:34.931Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, +] + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "lightning-utilities" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/39/6fc58ca81492db047149b4b8fd385aa1bfb8c28cd7cacb0c7eb0c44d842f/lightning_utilities-0.15.2.tar.gz", hash = "sha256:cdf12f530214a63dacefd713f180d1ecf5d165338101617b4742e8f22c032e24", size = 31090, upload-time = "2025-08-06T13:57:39.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/73/3d757cb3fc16f0f9794dd289bcd0c4a031d9cf54d8137d6b984b2d02edf3/lightning_utilities-0.15.2-py3-none-any.whl", hash = "sha256:ad3ab1703775044bbf880dbf7ddaaac899396c96315f3aa1779cec9d618a9841", size = 29431, upload-time = "2025-08-06T13:57:38.046Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, + { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, + { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.12.0.88" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/63/6861102ec149c3cd298f4d1ea7ce9d6adbc7529221606ff1dab991a19adb/opencv-python-headless-4.12.0.88.tar.gz", hash = "sha256:cfdc017ddf2e59b6c2f53bc12d74b6b0be7ded4ec59083ea70763921af2b6c09", size = 95379675, upload-time = "2025-07-07T09:21:06.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/7d/414e243c5c8216a5277afd104a319cc1291c5e23f5eeef512db5629ee7f4/opencv_python_headless-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1e58d664809b3350c1123484dd441e1667cd7bed3086db1b9ea1b6f6cb20b50e", size = 37877864, upload-time = "2025-07-07T09:14:41.693Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/7e162714beed1cd5e7b5eb66fcbcba2f065c51b1d9da2463024c84d2f7c0/opencv_python_headless-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:365bb2e486b50feffc2d07a405b953a8f3e8eaa63865bc650034e5c71e7a5154", size = 57326608, upload-time = "2025-07-07T09:14:51.885Z" }, + { url = "https://files.pythonhosted.org/packages/69/4e/116720df7f1f7f3b59abc608ca30fbec9d2b3ae810afe4e4d26483d9dfa0/opencv_python_headless-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:aeb4b13ecb8b4a0beb2668ea07928160ea7c2cd2d9b5ef571bbee6bafe9cc8d0", size = 33145800, upload-time = "2025-07-07T09:15:00.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/53/e19c21e0c4eb1275c3e2c97b081103b6dfb3938172264d283a519bf728b9/opencv_python_headless-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:236c8df54a90f4d02076e6f9c1cc763d794542e886c576a6fee46ec8ff75a7a9", size = 54023419, upload-time = "2025-07-07T09:15:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9c/a76fd5414de6ec9f21f763a600058a0c3e290053cea87e0275692b1375c0/opencv_python_headless-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:fde2cf5c51e4def5f2132d78e0c08f9c14783cd67356922182c6845b9af87dbd", size = 30225230, upload-time = "2025-07-07T09:15:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/f2/35/0858e9e71b36948eafbc5e835874b63e515179dc3b742cbe3d76bc683439/opencv_python_headless-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:86b413bdd6c6bf497832e346cd5371995de148e579b9774f8eba686dee3f5528", size = 38923559, upload-time = "2025-07-07T09:15:25.229Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytorch" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "opencv-python-headless" }, + { name = "pandas" }, + { name = "scikit-image" }, + { name = "scikit-learn" }, + { name = "seaborn" }, + { name = "timm" }, + { name = "torchcam" }, + { name = "torchinfo" }, + { name = "torchmetrics" }, + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "opencv-python-headless", specifier = ">=4.12.0.88" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "scikit-image", specifier = ">=0.25.2" }, + { name = "scikit-learn", specifier = ">=1.7.2" }, + { name = "seaborn", specifier = ">=0.13.2" }, + { name = "timm", specifier = ">=1.0.21" }, + { name = "torchcam", specifier = ">=0.3.1" }, + { name = "torchinfo", specifier = ">=1.8.0" }, + { name = "torchmetrics", specifier = ">=1.8.2" }, + { name = "tqdm", specifier = ">=4.67.1" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "safetensors" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/cc/738f3011628920e027a11754d9cae9abec1aed00f7ae860abbf843755233/safetensors-0.6.2.tar.gz", hash = "sha256:43ff2aa0e6fa2dc3ea5524ac7ad93a9839256b8703761e76e2d0b2a3fa4f15d9", size = 197968, upload-time = "2025-08-08T13:13:58.654Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b1/3f5fd73c039fc87dba3ff8b5d528bfc5a32b597fea8e7a6a4800343a17c7/safetensors-0.6.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9c85ede8ec58f120bad982ec47746981e210492a6db876882aa021446af8ffba", size = 454797, upload-time = "2025-08-08T13:13:52.066Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c9/bb114c158540ee17907ec470d01980957fdaf87b4aa07914c24eba87b9c6/safetensors-0.6.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d6675cf4b39c98dbd7d940598028f3742e0375a6b4d4277e76beb0c35f4b843b", size = 432206, upload-time = "2025-08-08T13:13:50.931Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/f70c34e47df3110e8e0bb268d90db8d4be8958a54ab0336c9be4fe86dac8/safetensors-0.6.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d2d2b3ce1e2509c68932ca03ab8f20570920cd9754b05063d4368ee52833ecd", size = 473261, upload-time = "2025-08-08T13:13:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/be9c6a7c7ef773e1996dc214e73485286df1836dbd063e8085ee1976f9cb/safetensors-0.6.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93de35a18f46b0f5a6a1f9e26d91b442094f2df02e9fd7acf224cfec4238821a", size = 485117, upload-time = "2025-08-08T13:13:43.506Z" }, + { url = "https://files.pythonhosted.org/packages/c9/55/23f2d0a2c96ed8665bf17a30ab4ce5270413f4d74b6d87dd663258b9af31/safetensors-0.6.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89a89b505f335640f9120fac65ddeb83e40f1fd081cb8ed88b505bdccec8d0a1", size = 616154, upload-time = "2025-08-08T13:13:45.096Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/affb0bd9ce02aa46e7acddbe087912a04d953d7a4d74b708c91b5806ef3f/safetensors-0.6.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4d0d0b937e04bdf2ae6f70cd3ad51328635fe0e6214aa1fc811f3b576b3bda", size = 520713, upload-time = "2025-08-08T13:13:46.25Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5d/5a514d7b88e310c8b146e2404e0dc161282e78634d9358975fd56dfd14be/safetensors-0.6.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8045db2c872db8f4cbe3faa0495932d89c38c899c603f21e9b6486951a5ecb8f", size = 485835, upload-time = "2025-08-08T13:13:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7b/4fc3b2ba62c352b2071bea9cfbad330fadda70579f617506ae1a2f129cab/safetensors-0.6.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81e67e8bab9878bb568cffbc5f5e655adb38d2418351dc0859ccac158f753e19", size = 521503, upload-time = "2025-08-08T13:13:47.651Z" }, + { url = "https://files.pythonhosted.org/packages/5a/50/0057e11fe1f3cead9254315a6c106a16dd4b1a19cd247f7cc6414f6b7866/safetensors-0.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0e4d029ab0a0e0e4fdf142b194514695b1d7d3735503ba700cf36d0fc7136ce", size = 652256, upload-time = "2025-08-08T13:13:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/e9/29/473f789e4ac242593ac1656fbece6e1ecd860bb289e635e963667807afe3/safetensors-0.6.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fa48268185c52bfe8771e46325a1e21d317207bcabcb72e65c6e28e9ffeb29c7", size = 747281, upload-time = "2025-08-08T13:13:54.656Z" }, + { url = "https://files.pythonhosted.org/packages/68/52/f7324aad7f2df99e05525c84d352dc217e0fa637a4f603e9f2eedfbe2c67/safetensors-0.6.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d83c20c12c2d2f465997c51b7ecb00e407e5f94d7dec3ea0cc11d86f60d3fde5", size = 692286, upload-time = "2025-08-08T13:13:55.884Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/cad1d9762868c7c5dc70c8620074df28ebb1a8e4c17d4c0cb031889c457e/safetensors-0.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d944cea65fad0ead848b6ec2c37cc0b197194bec228f8020054742190e9312ac", size = 655957, upload-time = "2025-08-08T13:13:57.029Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/e2158e17bbe57d104f0abbd95dff60dda916cf277c9f9663b4bf9bad8b6e/safetensors-0.6.2-cp38-abi3-win32.whl", hash = "sha256:cab75ca7c064d3911411461151cb69380c9225798a20e712b102edda2542ddb1", size = 308926, upload-time = "2025-08-08T13:14:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c3/c0be1135726618dc1e28d181b8c442403d8dbb9e273fd791de2d4384bcdd/safetensors-0.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:c7b214870df923cbc1593c3faee16bec59ea462758699bd3fee399d00aac072c", size = 320192, upload-time = "2025-08-08T13:13:59.467Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tifffile" +version = "2025.10.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/b5/0d8f3d395f07d25ec4cafcdfc8cab234b2cc6bf2465e9d7660633983fe8f/tifffile-2025.10.16.tar.gz", hash = "sha256:425179ec7837ac0e07bc95d2ea5bea9b179ce854967c12ba07fc3f093e58efc1", size = 371848, upload-time = "2025-10-16T22:56:09.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/5e/56c751afab61336cf0e7aa671b134255a30f15f59cd9e04f59c598a37ff5/tifffile-2025.10.16-py3-none-any.whl", hash = "sha256:41463d979c1c262b0a5cdef2a7f95f0388a072ad82d899458b154a48609d759c", size = 231162, upload-time = "2025-10-16T22:56:07.214Z" }, +] + +[[package]] +name = "timm" +version = "1.0.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/63/ab9bc9441f173fab436d15070dbc90341ff1e439f3b76c6871bc37176580/timm-1.0.21.tar.gz", hash = "sha256:aa372fe43a85ed6ea0dd14945dac724c842e6e373779e2a2afd67d7dc1b82c4c", size = 2382582, upload-time = "2025-10-24T22:37:57.756Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/8c/a668e732032f6de4ecc6b33f7ed27eab1c238dce35f6fe39986ad61aed9e/timm-1.0.21-py3-none-any.whl", hash = "sha256:e7428083af9f68af5ef1d50724946d9b6a2ccba8688c3e5fc9370f59f76e50cf", size = 2529988, upload-time = "2025-10-24T22:37:55.539Z" }, +] + +[[package]] +name = "torch" +version = "2.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/1c/90eb13833cdf4969ea9707586d7b57095c3b6e2b223a7256bf111689bcb8/torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e", size = 104111330, upload-time = "2025-10-15T15:46:35.238Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/2254c54b8d523592c25ef4434769aa23e29b1e6bf5f4c0ad9e27bf442927/torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642", size = 899750243, upload-time = "2025-10-15T15:48:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a5/5cb94fa4fd1e78223455c23c200f30f6dc10c6d4a2bcc8f6e7f2a2588370/torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece", size = 109284513, upload-time = "2025-10-15T15:46:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/66/e8/fc414d8656250ee46120b44836ffbb3266343db424b3e18ca79ebbf69d4f/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5", size = 74830362, upload-time = "2025-10-15T15:46:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/9474c98fc5ae0cd04b9466035428cd360e6611a86b8352a0fc2fa504acdc/torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637", size = 104144940, upload-time = "2025-10-15T15:47:29.076Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/8e0c1cf57830172c109d4bd6be2708cabeaf550983eee7029291322447a0/torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e", size = 899744054, upload-time = "2025-10-15T15:48:29.864Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/82c28b30fcb4b7c9cdd995763d18bbb830d6521356712faebbad92ffa61d/torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8", size = 109517546, upload-time = "2025-10-15T15:47:33.395Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a91f96ec74347fa5fd24453fa514bc61c61ecc79196fa760b012a1873d96/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a", size = 74480732, upload-time = "2025-10-15T15:47:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/9f70af34b334a7e0ef496ceec96b7ec767bd778ea35385ce6f77557534d1/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e614fae699838038d888729f82b687c03413c5989ce2a9481f9a7e7a396e0bb", size = 74433037, upload-time = "2025-10-15T15:47:41.894Z" }, + { url = "https://files.pythonhosted.org/packages/b7/84/37cf88625901934c97109e583ecc21777d21c6f54cda97a7e5bbad1ee2f2/torch-2.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:dfb5b8cd310ba3436c7e14e8b7833ef658cf3045e50d2bdaed23c8fc517065eb", size = 104116482, upload-time = "2025-10-15T15:47:46.266Z" }, + { url = "https://files.pythonhosted.org/packages/56/8e/ca8b17866943a8d4f4664d402ea84210aa274588b4c5d89918f5caa24eec/torch-2.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b3d29524993a478e46f5d598b249cd824b7ed98d7fba538bd9c4cde6c803948f", size = 899746916, upload-time = "2025-10-15T15:50:40.294Z" }, + { url = "https://files.pythonhosted.org/packages/43/65/3b17c0fbbdab6501c5b320a52a648628d0d44e7379f64e27d9eef701b6bf/torch-2.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:71c7578984f5ec0eb645eb4816ac8435fcf3e3e2ae1901bcd2f519a9cafb5125", size = 109275151, upload-time = "2025-10-15T15:49:20.715Z" }, + { url = "https://files.pythonhosted.org/packages/83/36/74f8c051f785500396e42f93542422422dfd874a174f21f8d955d36e5d64/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:71d9309aee457bbe0b164bce2111cd911c4ed4e847e65d5077dbbcd3aba6befc", size = 74823353, upload-time = "2025-10-15T15:49:16.59Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/dc3b4e2f9ba98ae27238f0153ca098bf9340b2dafcc67fde645d496dfc2a/torch-2.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c08fb654d783899e204a32cca758a7ce8a45b2d78eeb89517cc937088316f78e", size = 104140340, upload-time = "2025-10-15T15:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8d/b00657f8141ac16af7bb6cda2e67de18499a3263b78d516b9a93fcbc98e3/torch-2.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ec8feb0099b2daa5728fbc7abb0b05730fd97e0f359ff8bda09865aaa7bd7d4b", size = 899731750, upload-time = "2025-10-15T15:49:36.673Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/bd361e0cbb2c79ce6450f42643aaf6919956f89923a50571b0ebfe92d142/torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75", size = 109503850, upload-time = "2025-10-15T15:50:24.118Z" }, +] + +[[package]] +name = "torchcam" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/96/c4d2c08eb9d6c6d30cd14561a56e46e24c3a12058b50d7af28dbdbd6a03e/torchcam-0.3.1.tar.gz", hash = "sha256:400d451cbbafe0dbddbf3a22feab9e23d1317e90cf6eae16bca96e0e2ff4f076", size = 25310, upload-time = "2021-10-31T17:55:46.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/70/a99d8ea8cf84a2d4805735a9d83cefb93a9d36e145882b04340e40242854/torchcam-0.3.1-py3-none-any.whl", hash = "sha256:50fff3a2996f9cacc76d96faa76de44fda0896f98e2dcef59e94bddbd813059d", size = 23181, upload-time = "2021-10-31T17:55:44.761Z" }, +] + +[[package]] +name = "torchinfo" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/d9/2b811d1c0812e9ef23e6cf2dbe022becbe6c5ab065e33fd80ee05c0cd996/torchinfo-1.8.0.tar.gz", hash = "sha256:72e94b0e9a3e64dc583a8e5b7940b8938a1ac0f033f795457f27e6f4e7afa2e9", size = 25880, upload-time = "2023-05-14T19:23:26.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/25/973bd6128381951b23cdcd8a9870c6dcfc5606cb864df8eabd82e529f9c1/torchinfo-1.8.0-py3-none-any.whl", hash = "sha256:2e911c2918603f945c26ff21a3a838d12709223dc4ccf243407bce8b6e897b46", size = 23377, upload-time = "2023-05-14T19:23:24.141Z" }, +] + +[[package]] +name = "torchmetrics" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lightning-utilities" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/21/aa0f434434c48490f91b65962b1ce863fdcce63febc166ca9fe9d706c2b6/torchmetrics-1.8.2-py3-none-any.whl", hash = "sha256:08382fd96b923e39e904c4d570f3d49e2cc71ccabd2a94e0f895d1f0dac86242", size = 983161, upload-time = "2025-09-03T14:00:51.921Z" }, +] + +[[package]] +name = "torchvision" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/b5/b2008e4b77a8d6aada828dd0f6a438d8f94befa23fdd2d62fa0ac6e60113/torchvision-0.24.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84d79cfc6457310107ce4d712de7a3d388b24484bc9aeded4a76d8f8e3a2813d", size = 1891722, upload-time = "2025-10-15T15:51:28.854Z" }, + { url = "https://files.pythonhosted.org/packages/8f/02/e2f6b0ff93ca4db5751ac9c5be43f13d5e53d9e9412324f464dca1775027/torchvision-0.24.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fec12a269cf80f6b0b71471c8d498cd3bdd9d8e892c425bf39fecb604852c3b0", size = 2371478, upload-time = "2025-10-15T15:51:37.842Z" }, + { url = "https://files.pythonhosted.org/packages/77/85/42e5fc4f716ec7b73cf1f32eeb5c77961be4d4054b26cd6a5ff97f20c966/torchvision-0.24.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7323a9be5e3da695605753f501cdc87824888c5655d27735cdeaa9986b45884c", size = 8050200, upload-time = "2025-10-15T15:51:46.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/48cb0b6b26276d2120b1e0dbc877579a748eae02b4091a7522ce54f6d5e1/torchvision-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:08cad8b204196e945f0b2d73adee952d433db1c03645851d52b22a45f1015b13", size = 4309939, upload-time = "2025-10-15T15:51:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/3dd10830b047eeb46ae6b465474258d7b4fbb7d8872dca69bd42449f5c82/torchvision-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ab956a6e588623353e0f20d4b03eb1656cb4a3c75ca4dd8b4e32e01bc43271a", size = 2028355, upload-time = "2025-10-15T15:51:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/f7/cf/2d7e43409089ce7070f5336161f9216d58653ee1cb26bcb5d6c84cc2de36/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b1b3db80609c32a088554e8e94b4fc31f1033fe5bb4ac0673ec49c3eb03fb4da", size = 2374466, upload-time = "2025-10-15T15:51:35.382Z" }, + { url = "https://files.pythonhosted.org/packages/e9/30/8f7c328fd7e0a9665da4b6b56b1c627665c18470bfe62f3729ad3eda9aec/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:e6635f100d455c80b43f297df4b8585a76c6a2e114802f6567ddd28d7b5479b0", size = 8217068, upload-time = "2025-10-15T15:51:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/55/a2/b6f9e40e2904574c80b3bb872c66af20bbd642053e7c8e1b9e99ab396535/torchvision-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4ce158bbdc3a9086034bced0b5212888bd5b251fee6d08a9eff151d30b4b228a", size = 4273912, upload-time = "2025-10-15T15:51:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/790a39645cc8c71bf442d54a76da9bda5caeb2a44c5f7e02498649cd99d4/torchvision-0.24.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4bdfc85a5ed706421555f32cdc5e3ddb6d40bf65ef03a274ce3c176393e2904b", size = 2028335, upload-time = "2025-10-15T15:51:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d7/69479a066ea773653e88eda99031e38681e9094046f87cb957af5036db0e/torchvision-0.24.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:73576a9c4a593223fbae85a64e8bbd77049abd1101893ecf3c5e981284fd58b4", size = 2371609, upload-time = "2025-10-15T15:51:29.859Z" }, + { url = "https://files.pythonhosted.org/packages/46/64/3c7fdb3771ec992b9445a1f7a969466b23ce2cdb14e09303b3db351a0655/torchvision-0.24.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:dd565b1b06666ff399d0801d4d1824fa570c0167a179ca700a5be232527b3c62", size = 8214918, upload-time = "2025-10-15T15:51:41.465Z" }, + { url = "https://files.pythonhosted.org/packages/58/51/abc416bc34d574ad479af738e413d9ebf93027ee92d0f4ae38f966b818f7/torchvision-0.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:eb45d12ac48d757738788fd3fb8e88e647d6b2ab2424134ca87556efc72d81b5", size = 4257776, upload-time = "2025-10-15T15:51:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/08/f7/261d1353c611820541ecd43046b89da3f1ae998dc786e4288b890a009883/torchvision-0.24.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:68120e7e03c31900e499a10bb7fdd63cfd67f0054c9fa108e7e27f9cd372f315", size = 2028359, upload-time = "2025-10-15T15:51:32.119Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fd/615d8a86db1578345de7fa1edaf476fbcf4f057bf7e4fd898306b620c487/torchvision-0.24.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:64e54494043eecf9f57a9881c6fdea49c62282782e737c002ae8b1639e6ea80e", size = 2374469, upload-time = "2025-10-15T15:51:40.19Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/bac11e8fdbf00d6c398246ff2781370aa72c99f2ac685c01ce79354c9a32/torchvision-0.24.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:75ef9546323b321a451239d886f0cb528f7e98bb294da47a3200effd4e572064", size = 8217060, upload-time = "2025-10-15T15:51:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/47/6f/9fba8abc468c904570699eceeb51588f9622172b8fffa4ab11bcf15598c2/torchvision-0.24.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2efb617667950814fc8bb9437e5893861b3616e214285be33cbc364a3f42c599", size = 4358490, upload-time = "2025-10-15T15:51:43.884Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "triton" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/29/10728de8a6e932e517c10773486b8e99f85d1b1d9dd87d9a9616e1fef4a1/triton-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6bb9aa5519c084a333acdba443789e50012a4b851cd486c54f0b8dc2a8d3a12", size = 170487289, upload-time = "2025-10-13T16:38:11.662Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/db80e48b9220c9bce872b0f616ad0446cdf554a40b85c7865cbca99ab3c2/triton-3.5.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c83f2343e1a220a716c7b3ab9fccfcbe3ad4020d189549200e2d2e8d5868bed9", size = 170577179, upload-time = "2025-10-13T16:38:17.865Z" }, + { url = "https://files.pythonhosted.org/packages/ff/60/1810655d1d856c9a4fcc90ee8966d85f552d98c53a6589f95ab2cbe27bb8/triton-3.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da0fa67ccd76c3dcfb0bffe1b1c57c685136a6bd33d141c24d9655d4185b1289", size = 170487949, upload-time = "2025-10-13T16:38:24.881Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b7/1dec8433ac604c061173d0589d99217fe7bf90a70bdc375e745d044b8aad/triton-3.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:317fe477ea8fd4524a6a8c499fb0a36984a56d0b75bf9c9cb6133a1c56d5a6e7", size = 170580176, upload-time = "2025-10-13T16:38:31.14Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +]