File size: 9,200 Bytes
23c10f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import os
import glob
import cv2
import numpy as np
import tifffile
import pandas as pd
import matplotlib.subplots as plt_subplots
import matplotlib.pyplot as plt
from scipy.ndimage import median_filter
from skimage.filters import gaussian
from skimage import exposure
from huggingface_hub import snapshot_download
from csbdeep.utils import normalize
from stardist.models import Config2D, StarDist2D
from stardist import fill_label_holes, calculate_extents, random_label_cmap
from stardist.matching import matching

# --- 1. Data Loading & Preparation ---
def load_and_split_dataset(repo_id="champ7/celldatamag", test_size=10):
    print(f"Downloading dataset from Hugging Face: {repo_id}...")
    local_dir = snapshot_download(repo_id=repo_id, repo_type="dataset")
    
    # Target specifically _w2.TIF based on the dataset structure
    search_path = os.path.join(local_dir, "**", "*_w2.TIF")
    image_paths = sorted(glob.glob(search_path, recursive=True))
    
    X, Y = [], []
    for img_path in image_paths:
        # Map _w2.TIF to _w3_seg.npy based on the provided screenshot
        mask_path = img_path.replace("_w2.TIF", "_w3_seg.npy")
        
        if os.path.exists(mask_path):
            img = tifffile.imread(img_path)
            mask = np.load(mask_path, allow_pickle=True)
            
            if mask.ndim == 0 and isinstance(mask.item(), dict):
                mask = mask.item().get('masks', mask.item())
            
            img = np.squeeze(img)
            mask = np.squeeze(mask)
                
            mask = fill_label_holes(mask.astype(np.int32))
            X.append(img)
            Y.append(mask)

    print(f"Loaded {len(X)} valid 2D image-mask pairs.")
    
    if len(X) <= test_size:
        raise ValueError("Not enough images to create a split. Reduce test_size or check the dataset.")

    X_test, Y_test = X[:test_size], Y[:test_size]
    X_train, Y_train = X[test_size:], Y[test_size:]
    
    return X_train, Y_train, X_test, Y_test

# --- 2. Preprocessing Definitions ---
def process_w2(image_obj):
    """ W2: Organelles (Red Fluorescence) -> CLAHE & Top-Hat preprocessing """
    if image_obj.ndim == 3: 
        image_obj = image_obj[0]
    p1, p99 = np.percentile(image_obj, (1, 99.9))
    robust_img = exposure.rescale_intensity(image_obj, in_range=(p1, p99), out_range=(0, 255)).astype(np.uint8)
    clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8))
    clahe_img = clahe.apply(robust_img)
    return clahe_img

def preprocess_2(img):
    """ Standard StarDist Percentile Normalization """
    return normalize(img, 1, 99.8, axis=(0,1))

def preprocess_none(img):
    """ Basic Min-Max scaling to prevent float crashes on raw inputs """
    return (img - np.min(img)) / (np.ptp(img) + 1e-8)

def apply_preprocessing(img, mode="none", to_rgb=False):
    """ Router for preprocessing modes """
    if mode == "prep1":
        out = process_w2(img)
    elif mode == "prep2":
        out = preprocess_2(img)
    elif mode == "prep1_then_prep2":
        out = process_w2(img)
        out = preprocess_2(out)
    else:
        out = preprocess_none(img)
        
    out = out.astype(np.float32)

    if to_rgb and out.ndim == 2:
        out = np.stack((out,) * 3, axis=-1)
        
    return out

# --- 3. Visualization Function ---
def plot_test_results(X_test, Y_test, Y_pred, X_input, experiment_name, mode_label, num_images=2):
    """Plots 3 columns: Original+GT, Model Input, Model Input+Pred"""
    print(f"\nGenerating plots for: {experiment_name}")
    lbl_cmap = random_label_cmap()
    
    for i in range(min(num_images, len(X_test))):
        fig, axes = plt.subplots(1, 3, figsize=(18, 6))
        
        # Column 1: Original Image + GT
        axes[0].imshow(X_test[i], cmap='gray')
        axes[0].imshow(Y_test[i], cmap=lbl_cmap, alpha=0.5)
        axes[0].set_title(f"Image {i+1}: Original + Ground Truth")
        axes[0].axis('off')
        
        # Column 2: The actual input fed to the model
        disp_img = X_input[i]
        if disp_img.ndim == 3: 
            disp_img = disp_img[..., 0] 
            
        axes[1].imshow(disp_img, cmap='gray')
        axes[1].set_title(f"Image {i+1}: Model Input ({mode_label})")
        axes[1].axis('off')
        
        # Column 3: Model Input + Prediction
        axes[2].imshow(disp_img, cmap='gray')
        axes[2].imshow(Y_pred[i], cmap=lbl_cmap, alpha=0.5)
        axes[2].set_title(f"Image {i+1}: Model Input + Prediction")
        axes[2].axis('off')
        
        plt.tight_layout()
        plt.show()

# --- 4. Training Function ---
def train_model(X_train, Y_train, model_name, prep_mode="none"):
    print(f"\n--- Training Custom Model ONCE: {model_name} (Training Data Preprocessing: {prep_mode}) ---")
    
    X_trn_proc = [apply_preprocessing(x, mode=prep_mode, to_rgb=False) for x in X_train]
    
    rng = np.random.RandomState(42)
    ind = rng.permutation(len(X_trn_proc))
    n_val = max(1, int(round(0.15 * len(ind))))
    ind_train, ind_val = ind[:-n_val], ind[-n_val:]
    
    X_trn = [X_trn_proc[i] for i in ind_train]
    Y_trn = [Y_train[i] for i in ind_train]
    X_val = [X_trn_proc[i] for i in ind_val]
    Y_val = [Y_train[i] for i in ind_val]
    
    conf = Config2D(
        n_rays=32,
        grid=(2,2),
        n_channel_in=1,
        train_patch_size=(256,256),
        train_batch_size=4,
    )
    model = StarDist2D(conf, name=model_name, basedir='models')
    
    median_size = calculate_extents(list(Y_train), np.median)
    fov = np.array(model._axes_tile_overlap('YX'))
    if any(median_size > fov):
        print("WARNING: Median object size is larger than the field of view.")
    
    model.train(X_trn, Y_trn, validation_data=(X_val, Y_val), epochs=30, steps_per_epoch=50)
    model.optimize_thresholds(X_val, Y_val)
    
    return model

# --- 5. Evaluation Loop ---
def run_evaluation(model, X_test, Y_test, experiment_name, prep_mode, is_pretrained_he=False):
    print(f"\nEvaluating: {experiment_name}")
    metrics = []
    
    Y_pred_list = []
    X_input_list = []
    
    for i, (img, gt) in enumerate(zip(X_test, Y_test)):
        img_input = apply_preprocessing(img, mode=prep_mode, to_rgb=is_pretrained_he)
        X_input_list.append(img_input)
        
        pred_mask, _ = model.predict_instances(img_input, prob_thresh=0.5, nms_thresh=0.3)
        Y_pred_list.append(pred_mask)
        
        # IoU Threshold 0.0 for matching calculation
        res = matching(gt, pred_mask, thresh=0.0)
        
        metrics.append({
            "Image": i+1, 
            "F1": res.f1, 
            "IoU": res.mean_matched_score 
        })
    
    df = pd.DataFrame(metrics)
    mean_f1 = df['F1'].mean()
    mean_iou = df['IoU'].mean()
    
    print(f"Results for {experiment_name}:")
    print(f"Mean F1-Score: {mean_f1:.4f}")
    print(f"Mean IoU:      {mean_iou:.4f}\n")
    
    plot_test_results(X_test, Y_test, Y_pred_list, X_input_list, experiment_name, prep_mode, num_images=2)
    
    return mean_f1, mean_iou

# --- Main Execution Block ---
if __name__ == "__main__":
    np.random.seed(42) 
    
    # 1. Load Data with updated repo
    X_train, Y_train, X_test, Y_test = load_and_split_dataset(repo_id="champ7/celldatamag", test_size=10)
    
    all_results = []
    
    # 2. Preload the versatile H&E model
    print("\nLoading pre-trained '2D_versatile_he' model...")
    pretrained_model = StarDist2D.from_pretrained('2D_versatile_he')

    # 3. Train the custom model EXACTLY ONCE (using 'none' as the baseline training preprocessing)
    custom_trained_model = train_model(X_train, Y_train, model_name="single_custom_model", prep_mode="none")

    # 4. Define all 8 experiments
    experiments = [
        # (Experiment Name, Preprocessing Mode, Model to Evaluate, is_pretrained_flag)
        ("Exp 1: Pretrained, No Preprocessing", "none", pretrained_model, True),
        ("Exp 2: Pretrained, Preprocessing 1", "prep1", pretrained_model, True),
        ("Exp 3: Pretrained, Preprocessing 2", "prep2", pretrained_model, True),
        ("Exp 4: Pretrained, Prep 1 -> Prep 2", "prep1_then_prep2", pretrained_model, True),
        
        ("Exp 5: Trained, No Preprocessing", "none", custom_trained_model, False),
        ("Exp 6: Trained, Preprocessing 1", "prep1", custom_trained_model, False),
        ("Exp 7: Trained, Preprocessing 2", "prep2", custom_trained_model, False),
        ("Exp 8: Trained, Prep 1 -> Prep 2", "prep1_then_prep2", custom_trained_model, False),
    ]

    # 5. Run through all inference/evaluation states
    for exp_name, prep_mode, model_to_eval, is_pretrained in experiments:
        f1, iou = run_evaluation(model_to_eval, X_test, Y_test, exp_name, prep_mode, is_pretrained_he=is_pretrained)
        all_results.append({
            "Experiment": exp_name,
            "Mean F1-Score": round(f1, 4),
            "Mean IoU": round(iou, 4)
        })

    # --- 6. Final Summary Table ---
    print("\n" + "="*70)
    print("FINAL METRICS SUMMARY (IoU Threshold = 0.0)".center(70))
    print("="*70)
    
    results_df = pd.DataFrame(all_results)
    print(results_df.to_string(index=False, justify='left'))
    print("="*70)