File size: 16,034 Bytes
e2d24e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
import os
import numpy as np
import torch
import cv2
from pycocotools.coco import COCO
from pycocotools import mask as maskUtils
from tqdm import tqdm
from segment_anything import sam_model_registry, SamAutomaticMaskGenerator
import matplotlib.pyplot as plt
import json
from scipy.ndimage import zoom

def fast_hist(label_true, label_pred, num_classes):
    """Fast histogram calculation"""
    mask = (label_true >= 0) & (label_true < num_classes)
    hist = np.bincount(
        num_classes * label_true[mask].astype(int) + label_pred[mask],
        minlength=num_classes ** 2,
        ).reshape(num_classes, num_classes)
    return hist

class SAMEvaluator:
    def __init__(self, sam_checkpoint, model_type="vit_b", device="cuda", num_classes=21):
        """Initialize SAM model and evaluation metrics"""
        self.device = device
        self.num_classes = num_classes
        print(f"Loading SAM model from {sam_checkpoint}")
        self.sam = sam_model_registry[model_type](checkpoint=sam_checkpoint)
        self.sam.to(device=device)
        self.mask_generator = SamAutomaticMaskGenerator(self.sam)
        self.hist = np.zeros((num_classes, num_classes))

    # [Previous methods remain the same until calculate_metrics]

    def load_coco_dataset(self, annotation_path, image_dir, subset_size=2):
        """Load COCO dataset with proper error handling"""
        print(f"Loading COCO annotations from {annotation_path}")
        try:
            # Load JSON manually first to handle encoding issues
            with open(annotation_path, 'rb') as f:
                dataset = json.load(f)

            # Initialize COCO API
            self.coco = COCO(annotation_path)
            self.image_dir = image_dir

            # Get images that have annotations
            self.image_ids = []
            for img_id in self.coco.getImgIds():
                ann_ids = self.coco.getAnnIds(imgIds=img_id)
                if len(ann_ids) > 0:
                    self.image_ids.append(img_id)

            print(f"Found {len(self.image_ids)} images with annotations")

            # Sample subset if specified
            self.image_ids = self.image_ids[:subset_size]

            img_info = self.coco.loadImgs(self.image_ids[0])[0]
            print("img_info", img_info)
        except UnicodeDecodeError as e:
            print(f"Error reading annotation file: {e}")
            raise
        except json.JSONDecodeError as e:
            print(f"Error parsing JSON: {e}")
            raise
        except Exception as e:
            print(f"Unexpected error: {e}")
            raise

    def visualize_evaluation(self, image_id, save_dir='vis'):
        """Visualize ground truth vs predictions"""
        os.makedirs(save_dir, exist_ok=True)

        try:
            # Load image
            img_info = self.coco.loadImgs(image_id)[0]
            image_path = os.path.join(self.image_dir, img_info['file_name'])
            image = cv2.imread(image_path)
            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

            # Get predictions and ground truth
            sam_masks = self.mask_generator.generate(image)
            ann_ids = self.coco.getAnnIds(imgIds=image_id)
            anns = self.coco.loadAnns(ann_ids)

            # Create visualization
            fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))

            # Original image with GT masks
            ax1.imshow(image)
            for ann in anns:
                mask = self.coco.annToMask(ann)
                color_mask = np.random.random(3)
                masked_image = np.ones_like(image) * color_mask.reshape(1, 1, 3)
                ax1.imshow(np.dstack((masked_image, mask * 0.35)))
            ax1.set_title('Ground Truth')
            ax1.axis('off')

            # Original image with SAM predictions
            ax2.imshow(image)
            for mask in sam_masks:
                m = mask['segmentation']
                color_mask = np.random.random(3)
                masked_image = np.ones_like(image) * color_mask.reshape(1, 1, 3)
                ax2.imshow(np.dstack((masked_image, m * 0.35)))
            ax2.set_title('SAM Predictions')
            ax2.axis('off')

            # Save the plot
            plt.savefig(os.path.join(save_dir, f'eval_vis_{image_id}.png'))
            plt.close()

        except Exception as e:
            print(f"Error visualizing image {image_id}: {e}")

    def calculate_additional_metrics(self, gt_masks, pred_masks):
        """Calculate additional segmentation metrics"""
        # Convert masks to label format
        label_trues = []
        label_preds = []

        # Process each pair of ground truth and predicted masks
        for gt_mask, pred_mask in zip(gt_masks, pred_masks):
            # Convert boolean masks to integer labels
            gt_labels = gt_mask.astype(np.int32)
            pred_labels = pred_mask.astype(np.int32)

            label_trues.append(gt_labels)
            label_preds.append(pred_labels)

        # Calculate histogram and metrics
        hist, metrics = self.scores(label_trues, label_preds, self.hist, self.num_classes)
        self.hist = hist  # Update the histogram

        return metrics

    def scores(self, label_trues, label_preds, hist, num_classes=21):
        """Calculate various segmentation metrics"""
        for lt, lp in zip(label_trues, label_preds):
            hist += fast_hist(lt.flatten(), lp.flatten(), num_classes)

        # Pixel Accuracy
        acc = np.diag(hist).sum() / hist.sum()

        # Mean Pixel Accuracy
        acc_cls = np.diag(hist) / hist.sum(axis=1)
        acc_cls = np.nanmean(acc_cls)

        # Mean IoU
        iu = np.diag(hist) / (hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist))
        valid = hist.sum(axis=1) > 0
        mean_iu = np.nanmean(iu[valid])

        # Per-class IoU
        cls_iu = dict(zip(range(num_classes), iu))

        return hist, {
            "pixel_accuracy": acc,
            "mean_pixel_accuracy": acc_cls,
            "mean_iou": mean_iu,
            "per_class_iou": cls_iu,
        }
    def calculate_iou(self, mask1, mask2):
        """Calculate IoU between two masks"""
        intersection = np.logical_and(mask1, mask2).sum()
        union = np.logical_or(mask1, mask2).sum()
        return intersection / (union + 1e-6)

    def calculate_cam_score(self, pred_mask, gt_mask):
        """Calculate Class Activation Map score"""
        # Normalize masks to [0, 1]
        pred_mask = pred_mask.astype(float)
        gt_mask = gt_mask.astype(float)

        # Calculate activation map correlation
        intersection = np.sum(pred_mask * gt_mask)
        union = np.sqrt(np.sum(pred_mask**2)) * np.sqrt(np.sum(gt_mask**2))
        cam_score = intersection / (union + 1e-6)

        return cam_score

    def calculate_segs_score(self, pred_mask, gt_mask):
        """Calculate SEGS (Segment Everything) score"""
        # Calculate boundary accuracy
        pred_boundary = cv2.Canny(pred_mask.astype(np.uint8), 100, 200)
        gt_boundary = cv2.Canny(gt_mask.astype(np.uint8), 100, 200)

        # Calculate boundary overlap
        boundary_overlap = np.sum(np.logical_and(pred_boundary > 0, gt_boundary > 0))
        boundary_union = np.sum(np.logical_or(pred_boundary > 0, gt_boundary > 0))

        boundary_score = boundary_overlap / (boundary_union + 1e-6)

        # Combine with region similarity
        region_score = self.calculate_iou(pred_mask, gt_mask)
        segs_score = 0.5 * (boundary_score + region_score)

        return segs_score

    def calculate_msc_segs_score(self, pred_mask, gt_mask):
        """Calculate Multi-Scale Consistency SEGS score"""
        scales = [0.5, 1.0, 1.5]  # Multiple scales to evaluate
        msc_scores = []

        original_shape = pred_mask.shape
        for scale in scales:
            # Resize masks to different scales
            if scale != 1.0:
                scaled_pred = zoom(pred_mask, scale, order=0)
                scaled_gt = zoom(gt_mask, scale, order=0)

                # Resize back to original size for comparison
                scaled_pred = zoom(scaled_pred, 1/scale, order=0)
                scaled_gt = zoom(scaled_gt, 1/scale, order=0)

                # Ensure masks are same size as original
                if scaled_pred.shape != original_shape:
                    scaled_pred = cv2.resize(scaled_pred, original_shape[::-1])
                if scaled_gt.shape != original_shape:
                    scaled_gt = cv2.resize(scaled_gt, original_shape[::-1])
            else:
                scaled_pred = pred_mask
                scaled_gt = gt_mask

            # Calculate SEGS score at this scale
            segs_score = self.calculate_segs_score(scaled_pred, scaled_gt)
            msc_scores.append(segs_score)

        # Return average score across scales
        return np.mean(msc_scores)

    def calculate_metrics(self, gt_masks, pred_masks):
        """Calculate all metrics for one image"""
        if not gt_masks or not pred_masks:
            return {
                'iou': 0, 'precision': 0, 'recall': 0,
                'cam_score': 0, 'segs_score': 0, 'msc_segs_score': 0,
                'pixel_accuracy': 0, 'mean_pixel_accuracy': 0,
                'mean_iou': 0, 'per_class_iou': {}
            }

        # Calculate original metrics
        best_scores = {
            'ious': [], 'cam_scores': [],
            'segs_scores': [], 'msc_segs_scores': []
        }

        for gt_mask in gt_masks:
            mask_scores = {
                'ious': [], 'cam_scores': [],
                'segs_scores': [], 'msc_segs_scores': []
            }

            for pred_mask in pred_masks:
                iou = self.calculate_iou(gt_mask, pred_mask)
                cam = self.calculate_cam_score(pred_mask, gt_mask)
                segs = self.calculate_segs_score(pred_mask, gt_mask)
                msc_segs = self.calculate_msc_segs_score(pred_mask, gt_mask)

                mask_scores['ious'].append(iou)
                mask_scores['cam_scores'].append(cam)
                mask_scores['segs_scores'].append(segs)
                mask_scores['msc_segs_scores'].append(msc_segs)

            best_scores['ious'].append(max(mask_scores['ious']) if mask_scores['ious'] else 0)
            best_scores['cam_scores'].append(max(mask_scores['cam_scores']) if mask_scores['cam_scores'] else 0)
            best_scores['segs_scores'].append(max(mask_scores['segs_scores']) if mask_scores['segs_scores'] else 0)
            best_scores['msc_segs_scores'].append(max(mask_scores['msc_segs_scores']) if mask_scores['msc_segs_scores'] else 0)

        # Calculate additional metrics
        additional_metrics = self.calculate_additional_metrics(gt_masks, pred_masks)

        # Combine all metrics
        metrics = {
            'iou': np.mean(best_scores['ious']),
            'precision': sum(iou > 0.5 for iou in best_scores['ious']) / len(pred_masks) if pred_masks else 0,
            'recall': sum(iou > 0.5 for iou in best_scores['ious']) / len(gt_masks) if gt_masks else 0,
            'cam_score': np.mean(best_scores['cam_scores']),
            'segs_score': np.mean(best_scores['segs_scores']),
            'msc_segs_score': np.mean(best_scores['msc_segs_scores']),
            **additional_metrics
        }

        return metrics

    def evaluate_image(self, image_id):
        """Evaluate single image"""
        try:
            # Load image
            print(image_id)
            img_info = self.coco.loadImgs(image_id)[0]
            print(img_info)
            image_path = os.path.join(self.image_dir, img_info['file_name'])
            print(image_path)

            if not os.path.exists(image_path):
                print(f"Image not found: {image_path}")
                return None

            image = cv2.imread(image_path)
            if image is None:
                print(f"Failed to load image: {image_path}")
                return None

            image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

            # Get ground truth masks
            ann_ids = self.coco.getAnnIds(imgIds=image_id)
            anns = self.coco.loadAnns(ann_ids)

            gt_masks = []
            for ann in anns:
                try:
                    if isinstance(ann['segmentation'], dict):
                        # RLE format
                        mask = self.coco.annToMask(ann)
                        gt_masks.append(mask)
                    elif isinstance(ann['segmentation'], list):
                        # Polygon format
                        rle = maskUtils.frPyObjects(ann['segmentation'],
                                                    img_info['height'],
                                                    img_info['width'])
                        mask = maskUtils.decode(rle)
                        if len(mask.shape) > 2:
                            mask = mask.sum(axis=2) > 0
                        gt_masks.append(mask)
                except Exception as e:
                    print(f"Error processing annotation: {e}")
                    continue

            if not gt_masks:
                print(f"No valid masks found for image {image_id}")
                return None

            # Generate SAM predictions
            sam_masks = self.mask_generator.generate(image)
            pred_masks = [mask['segmentation'] for mask in sam_masks]

            return self.calculate_metrics(gt_masks, pred_masks)

        except Exception as e:
            print(f"Error evaluating image {image_id}: {e}")
            return None

    def evaluate_dataset(self):
        """Evaluate entire dataset"""
        total_metrics = {
            'iou': 0, 'precision': 0, 'recall': 0,
            'cam_score': 0, 'segs_score': 0, 'msc_segs_score': 0,
            'pixel_accuracy': 0, 'mean_pixel_accuracy': 0,
            'mean_iou': 0
        }
        valid_images = 0

        for image_id in tqdm(self.image_ids, desc="Evaluating images"):
            metrics = self.evaluate_image(image_id)
            if metrics is not None:
                valid_images += 1
                for k in total_metrics:
                    if k != 'per_class_iou':
                        total_metrics[k] += metrics[k]

        if valid_images == 0:
            print("No valid images were evaluated!")
            return total_metrics

        # Calculate averages
        avg_metrics = {k: v/valid_images for k, v in total_metrics.items()}
        print(f"Successfully evaluated {valid_images} images")
        return avg_metrics

# Modified main function to display new metrics
def main():
    evaluator = SAMEvaluator(
        sam_checkpoint='model/sam_vit_b_01ec64.pth',
        device="cuda" if torch.cuda.is_available() else "cpu"
    )

    coco_annotation_path = 'data/annotations/instances_val2017.json'
    coco_image_dir = 'data/val2017'

    print("Starting evaluation...")
    try:
        evaluator.load_coco_dataset(coco_annotation_path, coco_image_dir, subset_size=20)

        metrics = evaluator.evaluate_dataset()
        print("\nEvaluation Results:")
        print(f"Mean IoU: {metrics['iou']:.3f}")
        print(f"Mean Precision: {metrics['precision']:.3f}")
        print(f"Mean Recall: {metrics['recall']:.3f}")
        print(f"Mean CAM Score: {metrics['cam_score']:.3f}")
        print(f"Mean SEGS Score: {metrics['segs_score']:.3f}")
        print(f"Mean MSC SEGS Score: {metrics['msc_segs_score']:.3f}")
        print(f"Pixel Accuracy: {metrics['pixel_accuracy']:.3f}")
        print(f"Mean Pixel Accuracy: {metrics['mean_pixel_accuracy']:.3f}")
        print(f"Mean IoU (Additional): {metrics['mean_iou']:.3f}")

        if evaluator.image_ids:
            random_image_id = np.random.choice(evaluator.image_ids)
            print(f"\nGenerating visualization for image {random_image_id}")
            evaluator.visualize_evaluation(random_image_id)
    except Exception as e:
        print(f"Evaluation failed: {e}")

if __name__ == "__main__":
    main()