File size: 36,490 Bytes
b91f38a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
import os
import glob
import json
import torch
import pandas as pd
import numpy as np
from PIL import Image
from torch.utils.data import Dataset
import torchvision.transforms as transforms
from functools import partial
import torch.nn.functional as F
import nibabel as nib
from tqdm import tqdm
import random
import math
from monai.transforms import Compose, RandRotate, RandFlip, RandZoom, RandAffine, RandGaussianNoise, RandScaleIntensity, RandAdjustContrast,RandCoarseDropout
from monai.data.meta_tensor import MetaTensor
import nibabel as nib


def safe_load_image(path):
    """
    Robust image loader that supports both .nii.gz and .npy files.
    If .nii.gz is missing, automatically falls back to _compressed.npy.
    """
    try:
        if os.path.exists(path):
            if path.endswith('.nii.gz'):
                data = nib.load(path).get_fdata()
                return data
            elif path.endswith('.npy'):
                data = np.load(path)
                return data

        # fallback to compressed npy
        npy_fallback = path.replace('.nii.gz', '_compressed.npy')
        if os.path.exists(npy_fallback):
            data = np.load(npy_fallback)
            print(f"[Info] Fallback to compressed file: {npy_fallback}")
            return data

        # all failed
        raise FileNotFoundError(f"{path} and fallback {npy_fallback} not found")

    except Exception as e:
        print(f"[Warning] Error loading {path}: {e}")
        return None


def resize_array_to_tensor(array, current_spacing, target_spacing):
    """
    Resize the array to match the target spacing.

    Args:
    array (torch.Tensor): Input array to be resized.
    current_spacing (tuple): Current voxel spacing (z_spacing, xy_spacing, xy_spacing).
    target_spacing (tuple): Target voxel spacing (target_z_spacing, target_x_spacing, target_y_spacing).

    Returns:
    np.ndarray: Resized array.
    """
    # Calculate new dimensions
    original_shape = array.shape[2:]
    scaling_factors = [
        current_spacing[i] / target_spacing[i] for i in range(len(original_shape))
    ]
    new_shape = [
        int(original_shape[i] * scaling_factors[i]) for i in range(len(original_shape))
    ]
    # Resize the array
    resized_array = F.interpolate(array, size=new_shape, mode='trilinear', align_corners=False)
    return resized_array

def resize_array(array, current_spacing, target_spacing):
    """
    Resize the array to match the target spacing.

    Args:
    array (torch.Tensor): Input array to be resized.
    current_spacing (tuple): Current voxel spacing (z_spacing, xy_spacing, xy_spacing).
    target_spacing (tuple): Target voxel spacing (target_z_spacing, target_x_spacing, target_y_spacing).

    Returns:
    np.ndarray: Resized array.
    """
    # Calculate new dimensions
    original_shape = array.shape[2:]
    scaling_factors = [
        current_spacing[i] / target_spacing[i] for i in range(len(original_shape))
    ]
    new_shape = [
        int(original_shape[i] * scaling_factors[i]) for i in range(len(original_shape))
    ]
    # Resize the array
    resized_array = F.interpolate(array, size=new_shape, mode='trilinear', align_corners=False).cpu().numpy()
    return resized_array

modality_dict = {'3D':0,'2D':1}
class Conditional_ImageReportDataset_Train(Dataset):
    def __init__(self, local_batch_size, jsonl_file, csv_file_dir, npy_file_dir, anatomy_filter, positive_threshold, negative_threshold, max_samples=30000,modality='CT', need_aug=True,modal_embedding=False,is_train=True):
        self.anatomy_filter = anatomy_filter
        self.is_train = is_train
        self.modal_embedding = modal_embedding
        self.csv_file_dir = csv_file_dir
        self.npy_file_dir = npy_file_dir
        self.max_samples = max_samples
        self.modality = modality
        self.local_batch_size = local_batch_size    # for each anatomy, how many samples to be selected
        
        if positive_threshold == 0 and negative_threshold != 1: # >0 & <0.4
            positive_threshold = negative_threshold
        if positive_threshold != 1 and negative_threshold == 1: # >0.7 & <1
            negative_threshold = positive_threshold
            
        self.positive_threshold = positive_threshold
        self.negative_threshold = negative_threshold
        
        self.id2image_path = self.prepare_image_paths(jsonl_file)
        
        self.prepare_anatomy_data()
        
        self.anatomy_ls = list(self.anatomy2id_ls.keys())
        self.anatomy_weight = [round(math.log(len(self.anatomy2id_ls[anatomy]))) for anatomy in self.anatomy_ls]   # weight 与 sqrt(size) 相关
        self.anatomy_weight = [w/sum(self.anatomy_weight) for w in self.anatomy_weight]  # NOTE: Balancing between anatomys
        # self.anatomy_weight = [1/len(self.anatomy_ls) for anatomy in self.anatomy_ls]
        
        if need_aug:
            if '3D' in self.modality:
                self.augmentator = Compose([
                        RandRotate(range_x=15, range_y=15, range_z=15, prob=0.3, keep_size=True),  # 随机旋转
                        RandFlip(prob=0.5, spatial_axis=0),  # 随机翻转(沿深度轴)
                        RandZoom(min_zoom=0.85, max_zoom=1.15, prob=0.3, keep_size=True),  # 随机缩放
                        RandAffine(prob=0.3, translate_range=(24, 48, 48)),  # 随机平移
                        RandGaussianNoise(prob=0.3, mean=0.0, std=0.05),  # 添加高斯噪声
                        RandScaleIntensity(factors=[-0.1, 0.1], prob=0.3),
                        RandAdjustContrast(prob=0.3, gamma=(0.9, 1.1)),  # 增加对比度调整
                        RandCoarseDropout(prob=0.2, holes=8, spatial_size=3)  # 模拟局部缺失
                ])
            elif '2D' in self.modality:
                self.augmentator = Compose([
                    RandRotate(range_x=15, range_y=15, prob=0.5, keep_size=True),  # 随机旋转(仅x和y轴)
                    RandFlip(prob=0.5, spatial_axis=(0,1)),  # 随机翻转(仅水平和垂直轴)
                    RandZoom(min_zoom=0.8, max_zoom=1.1, prob=0.5, keep_size=True),  # 随机缩放
                    RandAffine(prob=0.5, translate_range=(32, 32)),  # 随机平移(仅2D平移)
                    RandGaussianNoise(prob=0.5, mean=0.0, std=0.1),  # 添加高斯噪声
                    RandScaleIntensity(factors=[-0.15, 0.15], prob=0.5),  # 随机调整像素强度
                    RandAdjustContrast(prob=0.4, gamma=(0.85, 1.15))  # 增加对比度变化
                    ])
        else:
            self.augmentator = None
        
        if int(os.environ.get("RANK", 0)) == 0:
            print(f'** DATA ** Load {len(self.id2image_path)} images.')
            print(f'** DATA ** Load {len(self.anatomy2id_ls)} anatomy.')

    def prepare_image_paths(self, jsonl_file):
        id2image_path = {}
        
        with open(jsonl_file, 'r') as f:
            data = f.readlines()
        data = [json.loads(l) for l in data]
        for d in data:
            # 这里有问题的
            data_id = d['name']   # valid_692_a_1.nii.gz
            id2image_path[data_id] = d['img_path']

        return id2image_path
    
    def prepare_anatomy_data(self):
        self.anatomy2id_ls = {} # 'lung': ['valid_692_a_1.nii.gz', ...]
        self.anatomy2simi_tab = {} # 'lung': a tensor with shape NxN
        
        for csv_file in os.listdir(self.csv_file_dir):   
            anatomy_name = csv_file.replace('.csv', '') # lung.csv -> lung
            
            # DEBUG
            if anatomy_name not in self.anatomy_filter:
                continue
            # DEBUG
            
            self.anatomy2id_ls[anatomy_name] = []
            df = pd.read_csv(os.path.join(self.csv_file_dir, csv_file))
            print(f"{csv_file} has {len(df)} rows.")
            if len(df) > self.max_samples:
                # 随机抽取max_samples个样本,并按照原csv顺序排序,以保持与simi_tab索引对应
                sampled_df = df.sample(n=self.max_samples, random_state=42).sort_index()
                sampled_indices = sampled_df.index.tolist() # 例如抽取[0, 3, 5]行
            else:
                sampled_df = df
                sampled_indices = sampled_df.index.tolist()
            for _, row in sampled_df.iterrows():
                self.anatomy2id_ls[anatomy_name].append(row['File Path'])
                
            npy_file = csv_file.replace('.csv', '.npy')
            full_simi_tab = np.load(os.path.join(self.npy_file_dir, npy_file))
            # 截取原csv对应的大小
            full_simi_tab = full_simi_tab[:len(df), :len(df)]
            full_simi_tab = np.clip(full_simi_tab, 0, 1)
            # 根据抽样的索引构造子矩阵,确保与csv中样本一一对应
            sampled_simi_tab = full_simi_tab[np.ix_(sampled_indices, sampled_indices)] # 例如只保留[0, 3, 5]行/列
            simi_tab_scaled = np.round(sampled_simi_tab * 100).astype(np.uint8)
            self.anatomy2simi_tab[anatomy_name] = simi_tab_scaled
            
        self.check_integrity()

    def check_integrity(self):
        id_keys = set(self.anatomy2id_ls.keys())
        simi_keys = set(self.anatomy2simi_tab.keys())
        if id_keys != simi_keys:
            missing_in_simi = id_keys - simi_keys
            missing_in_id = simi_keys - id_keys
            raise ValueError(f"Inconsistent keys detected. Missing in similarity table: {missing_in_simi}. Missing in anatomy data: {missing_in_id}.")
        # 修改
        if self.modality == '3D':
            for anatomy in id_keys:
                if not (len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[0] and len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[1]):
                    raise ValueError(f"Length of anatomy data {len(self.anatomy2id_ls[anatomy])} and similarity table {self.anatomy2simi_tab[anatomy].shape} do not match for {anatomy}.")
                
                # Here we check if all samples are readable
                filtered_sample_id_ls = []
                valid_indices = []
                for i, sample_id in enumerate(self.anatomy2id_ls[anatomy]):
                    # image_file_name = sample_id.replace('.nii.gz', '.npz')
                    if sample_id in self.id2image_path and os.path.exists(self.id2image_path[sample_id]):
                        filtered_sample_id_ls.append(sample_id)
                        valid_indices.append(i)
                        continue
                    else:
                        print(f'{sample_id} is missing, removed')
                        # # Remove the invalid sample from self.anatomy2id_ls and update simi_tab accordingly
                        # self.anatomy2simi_tab[anatomy] = np.delete(self.anatomy2simi_tab[anatomy], i, axis=0)
                        # self.anatomy2simi_tab[anatomy] = np.delete(self.anatomy2simi_tab[anatomy], i, axis=1)
                self.anatomy2simi_tab[anatomy] = self.anatomy2simi_tab[anatomy][np.ix_(valid_indices, valid_indices)]  # 保留有效索引对应的子矩阵
                self.anatomy2id_ls[anatomy] = filtered_sample_id_ls
                
                assert len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[0] and len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[1]
        elif self.modality == '2D':
            for anatomy in id_keys:
                if not (len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[0] and len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[1]):
                    raise ValueError(f"Length of anatomy data {len(self.anatomy2id_ls[anatomy])} and similarity table {self.anatomy2simi_tab[anatomy].shape} do not match for {anatomy}.")
                
                # Here we check if all samples are readable
                filtered_sample_id_ls = []
                valid_indices = []
                for i, sample_id in enumerate(self.anatomy2id_ls[anatomy]):
                    if sample_id in self.id2image_path and os.path.exists(self.id2image_path[sample_id]):
                        filtered_sample_id_ls.append(sample_id)
                        valid_indices.append(i)
                        continue
                    else:
                        print(f'{sample_id} is missing, removed')
                        # # Remove the invalid sample from self.anatomy2id_ls and update simi_tab accordingly
                        # self.anatomy2simi_tab[anatomy] = np.delete(self.anatomy2simi_tab[anatomy], i, axis=0)
                        # self.anatomy2simi_tab[anatomy] = np.delete(self.anatomy2simi_tab[anatomy], i, axis=1)
                self.anatomy2simi_tab[anatomy] = self.anatomy2simi_tab[anatomy][np.ix_(valid_indices, valid_indices)]  # 保留有效索引对应的子矩阵
                self.anatomy2id_ls[anatomy] = filtered_sample_id_ls
                
                assert len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[0] and len(self.anatomy2id_ls[anatomy]) == self.anatomy2simi_tab[anatomy].shape[1]
        else:
            raise   ValueError(f"Unsupported modality: {self.modality}. Supported modalities are '3D' and '2D'.")
    def __len__(self):
        return 100000
    
    def MLS_nii_img_to_tensor(self, path):

        nii_img = safe_load_image(sample["image"])
        img_data = nii_img.get_fdata()
        img_data = np.flip(img_data, axis=0)
        img_data = np.flip(img_data, axis=1)

        # WARNING Respacing
        img_data = img_data.transpose(2, 0, 1)
        img_data = np.copy(img_data)
        tensor = torch.tensor(img_data)
        tensor = tensor.unsqueeze(0).unsqueeze(0)
        
        target_x_spacing = 0.75
        target_y_spacing = 0.75
        target_z_spacing = 1.5
        current = (3, 1, 1)   # this is all set to 3 1 1
        target = (target_z_spacing, target_x_spacing, target_y_spacing)
        
        img_data = resize_array(tensor, current, target)
        img_data = img_data[0][0]
        img_data= np.transpose(img_data, (1, 2, 0))

        # WARNING Normalization 2
        hu_min, hu_max = -1000, 1000
        img_data = np.clip(img_data, hu_min, hu_max)
        img_data = (((img_data ) / 1000)).astype(np.float32)

        tensor = torch.tensor(img_data)
        
        # WARNING Padding or Crop
        
        # Get the dimensions of the input tensor
        target_shape = (480,480,240)    # h w d
        
        # Extract dimensions
        h, w, d = tensor.shape
        
        # Calculate cropping/padding values for height, width, and depth
        dh, dw, dd = target_shape
        h_start = max((h - dh) // 2, 0)
        h_end = min(h_start + dh, h)
        w_start = max((w - dw) // 2, 0)
        w_end = min(w_start + dw, w)
        d_start = max((d - dd) // 2, 0)
        d_end = min(d_start + dd, d)

        # Crop or pad the tensor
        tensor = tensor[h_start:h_end, w_start:w_end, d_start:d_end]

        pad_h_before = (dh - tensor.size(0)) // 2
        pad_h_after = dh - tensor.size(0) - pad_h_before

        pad_w_before = (dw - tensor.size(1)) // 2
        pad_w_after = dw - tensor.size(1) - pad_w_before

        pad_d_before = (dd - tensor.size(2)) // 2
        pad_d_after = dd - tensor.size(2) - pad_d_before

        tensor = torch.nn.functional.pad(tensor, (pad_d_before, pad_d_after, pad_w_before, pad_w_after, pad_h_before, pad_h_after), value=-1)

        tensor = tensor.permute(2, 0, 1)    # d h w

        tensor = tensor.unsqueeze(0)    # 1 d h w

        return tensor

    def nii_img_to_tensor(self, path):

        nii_img = safe_load_image(sample["image"])
        img_data = nii_img.get_fdata()
        img_data = np.flip(img_data, axis=0)
        img_data = np.flip(img_data, axis=1)

        # WARNING Respacing
        img_data = img_data.transpose(2, 0, 1)
        img_data = np.copy(img_data)
        tensor = torch.tensor(img_data)
        tensor = tensor.unsqueeze(0).unsqueeze(0)
        
        target_x_spacing = 0.75
        target_y_spacing = 0.75
        target_z_spacing = 1.5
        current = (3, 1, 1)   # this is all set to 3 1 1
        target = (target_z_spacing, target_x_spacing, target_y_spacing)
        
        img_data = resize_array(tensor, current, target)
        img_data = img_data[0][0]
        img_data= np.transpose(img_data, (1, 2, 0))
        
        # WARNING Normalization 2
        hu_min, hu_max = -1000, 1000
        img_data = np.clip(img_data, hu_min, hu_max)
        img_data = (((img_data ) / 1000)).astype(np.float32)

        tensor = torch.tensor(img_data)
        target_shape = (480,480,240)    # h w d
        
        # Extract dimensions
        h, w, d = tensor.shape
        
        # Calculate cropping/padding values for height, width, and depth
        dh, dw, dd = target_shape
        h_start = max((h - dh) // 2, 0)
        h_end = min(h_start + dh, h)
        w_start = max((w - dw) // 2, 0)
        w_end = min(w_start + dw, w)
        d_start = max((d - dd) // 2, 0)
        d_end = min(d_start + dd, d)
        
        # # Add random shift to the crop region
        # hw_max_shift = 128
        # z_max_shift = 64
        # if z_max_shift > 0 or hw_max_shift > 0:
        #     # Generate random shifts within [-max_shift, max_shift]
        #     h_shift = np.random.randint(-hw_max_shift, hw_max_shift + 1)
        #     w_shift = np.random.randint(-hw_max_shift, hw_max_shift + 1)
        #     d_shift = np.random.randint(-z_max_shift, z_max_shift + 1)

        #     # Apply shifts, ensuring the crop region remains within the image bounds
        #     h_start = max(h_start + h_shift, 0)
        #     h_end = min(h_start + dh, h)
        #     w_start = max(w_start + w_shift, 0)
        #     w_end = min(w_start + dw, w)
        #     d_start = max(d_start + d_shift, 0)
        #     d_end = min(d_start + dd, d)

        # Crop or pad the tensor
        tensor = tensor[h_start:h_end, w_start:w_end, d_start:d_end]

        pad_h_before = (dh - tensor.size(0)) // 2
        pad_h_after = dh - tensor.size(0) - pad_h_before

        pad_w_before = (dw - tensor.size(1)) // 2
        pad_w_after = dw - tensor.size(1) - pad_w_before

        pad_d_before = (dd - tensor.size(2)) // 2
        pad_d_after = dd - tensor.size(2) - pad_d_before

        tensor = torch.nn.functional.pad(tensor, (pad_d_before, pad_d_after, pad_w_before, pad_w_after, pad_h_before, pad_h_after), value=-1)

        tensor = tensor.permute(2, 0, 1)    # d h w

        tensor = tensor.unsqueeze(0)    # 1 d h w

        return tensor
    
    def load_2d_image_to_tensor(self, image_path,resize=False):
            """
            Load a 2D grayscale image and convert it to a tensor with dimensions [1, 1, 480, 480].
            
            Args:
                image_path (str): Path to the image file.
                
            Returns:
                tensor (torch.Tensor): Tensor with shape [1, 1, 480, 480].
            """
            # Load the image as grayscale
            image = Image.open(image_path).convert('L')
            
            # Resize to 480x480 if needed
            if resize:
                if image.size != (480, 480):
                    image = image.resize((480, 480), Image.BILINEAR)
            else:
                if image.size != (480, 480):
                    # 计算可以开始裁剪的最大左上角坐标
                    width, height = image.size
                    crop_size = 480
                    if width < crop_size or height < crop_size:
                        raise ValueError(f"Image size {image.size} is smaller than the crop size {crop_size}.")
                    max_left = width - crop_size
                    max_top = height - crop_size
                    
                    # 随机选择裁剪的左上角坐标
                    left = random.randint(0, max(0, max_left))
                    top = random.randint(0, max(0, max_top))
                    
                    # 裁剪图像
                    image = image.crop((left, top, left + crop_size, top + crop_size))
                

            # Convert to numpy array and normalize to [0, 1]
            img_array = np.array(image, dtype=np.float32) / 255.0
            
            # Convert to tensor
            tensor = torch.from_numpy(img_array)
            
            # Add batch and channel dimensions
            tensor = tensor.unsqueeze(0).unsqueeze(0)  # [1, 1, 480, 480]
            
            return tensor

    def __getitem__(self, index):
        """
        Returns:
            video_tensor (tensor): N, D, H, W
            input_text (List of str): N
        """
        anatomy = np.random.choice(self.anatomy_ls, size=1, p=self.anatomy_weight)[0]
        similarity = self.anatomy2simi_tab[anatomy]
        num_samples = similarity.shape[0]
        
        # Get upper-triangular indices
        rows, cols = np.triu_indices(num_samples, k=1)
        pos_mask = (similarity[rows, cols] > self.positive_threshold) & (rows != cols)
        neg_mask = (similarity[rows, cols] < self.negative_threshold) & (rows != cols)
        pos_candidate_pairs = list(zip(rows[pos_mask], cols[pos_mask]))
        neg_candidate_pairs = list(zip(rows[neg_mask], cols[neg_mask]))
        
        # Sampling
        pos_pair_count = neg_pair_count = self.local_batch_size // 4
        if self.local_batch_size % 4 > 1:
            pos_pair_count += 1
        if len(pos_candidate_pairs) >= pos_pair_count:
            selected_pos = random.sample(pos_candidate_pairs, pos_pair_count)
        else:
            selected_pos = random.sample(list(zip(rows, cols)), pos_pair_count)
        if len(neg_candidate_pairs) >= neg_pair_count:
            selected_neg = random.sample(neg_candidate_pairs, neg_pair_count)
        else:
            selected_neg = random.sample(list(zip(rows, cols)), neg_pair_count)
        sampled_indexes = [idx for pair in (selected_pos + selected_neg) for idx in pair]
        
        # If local_batch_size is odd, add one more sample
        if self.local_batch_size % 2 == 1:
            remaining = list(set(range(num_samples)) - set(sampled_indexes))
            extra = random.choice(remaining) if remaining else random.choice(list(range(num_samples)))
            sampled_indexes.append(extra)
        
        assert len(sampled_indexes) == self.local_batch_size

        sampled_ids = [self.anatomy2id_ls[anatomy][idx] for idx in sampled_indexes]
        # sampled_ids = [sample_id for sample_id in sampled_ids if sample_id in self.id2image_path]   # WARNING 假如有一个sample_id的image不存在,那么会导致stacked_video_tensor后面的元素整体向前移动一位,和similarity_tab对不上
        
        similarity_tab = np.zeros(
            (self.local_batch_size, self.local_batch_size), 
            dtype=np.uint8  # 使用 uint8 类型
        )
                
        # 修改赋值逻辑(需要反向缩放)
        for local_i, global_i in enumerate(sampled_indexes):
            for local_j, global_j in enumerate(sampled_indexes):
                raw_value = max(
                    self.anatomy2simi_tab[anatomy][global_i, global_j],
                    self.anatomy2simi_tab[anatomy][global_j, global_i]
                )
                # 直接使用存储的 uint8 值(无需转换)
                similarity_tab[local_i, local_j] = raw_value  # 已经是 0~100 的整数
        # # 修改
        video_tensors = []

        if '3D' in self.modality:
            for sampled_id in sampled_ids:
                try:
                    # 返回的 video_tensor 已经是 [1, D, H, W]
                    if self.is_train:
                        video_tensor = self.nii_img_to_tensor(self.id2image_path[sampled_id])
                    else:
                        video_tensor = self.MLS_nii_img_to_tensor(self.id2image_path[sampled_id])
                    assert video_tensor.dim() == 4, f"Expected 4D tensor, got {video_tensor.dim()}D tensor for {sampled_id}"
                    if self.augmentator is not None and self.is_train:
                        video_tensor = self.augmentator(video_tensor)
                    video_tensors.append(video_tensor)

                except Exception as e:
                    print(f"Error processing {sampled_id}: {e}. Skipping.")
                    # 如果一个样本失败,为了保持批次大小,可以加载一个全零张量或重复上一个
                    if video_tensors:
                        video_tensors.append(torch.zeros_like(video_tensors[-1]))
                    else: # 如果第一个就失败了
                        video_tensors.append(torch.zeros((1, 240, 480, 480), dtype=torch.float32))
            
            # 此时 video_tensors 是一个列表,包含 N 个 [1, D, H, W] 的张量
            stacked_video_tensor = torch.cat(video_tensors, dim=0) # -> [N, D, H, W]
            
            # if self.augmentator is not None:
            #     stacked_video_tensor = self.augmentator(stacked_video_tensor) # MONAI增广可以直接处理 [N, D, H, W]
            stacked_video_tensor = stacked_video_tensor.unsqueeze(1) # -> [N, 1, D, H, W]
            
        elif '2D' in self.modality:
            for sampled_id in sampled_ids:
                try:
                    # 返回的 video_tensor 已经是 [1, 1, H, W]
                    video_tensor = self.load_2d_image_to_tensor(self.id2image_path[sampled_id],resize=True)
                    video_tensor = video_tensor.squeeze().unsqueeze(0)  # 确保是 [1, H, W]
                    if self.augmentator is not None and self.is_train:
                        video_tensor = self.augmentator(video_tensor)
                    
                    video_tensor = video_tensor.unsqueeze(0)  # 变成 [1, 1, H, W]
                    video_tensors.append(video_tensor)
                except Exception as e:
                    print(f"Error processing {sampled_id}: {e}. Skipping.")
                    # 如果一个样本失败,为了保持批次大小,可以加载一个全零张量或重复上一个
                    if video_tensors:
                        video_tensors.append(torch.zeros_like(video_tensors[-1]))
                    else:
                        video_tensors.append(torch.zeros((1, 1, 480, 480), dtype=torch.float32))
            # 此时 video_tensors 是一个列表,包含 N 个 [1, 1, H, W] 的张量
            stacked_video_tensor = torch.cat(video_tensors, dim=0) # -> [N, 1, H, W]
            
            # 确保最终形状符合预期
            stacked_video_tensor = stacked_video_tensor.unsqueeze(1) # -> [N, 1, 1, H, W]
        if isinstance(stacked_video_tensor, MetaTensor):
            stacked_video_tensor = stacked_video_tensor.as_tensor()
        if self.modal_embedding:
            return {'video_tensor': stacked_video_tensor, 'similarity_tab': torch.tensor(similarity_tab), 'anatomy': anatomy, 'sampled_ids':sampled_ids, 'modality':modality_dict[self.modality]}
        else:
            return {'video_tensor': stacked_video_tensor, 'similarity_tab': torch.tensor(similarity_tab), 'anatomy': anatomy, 'sampled_ids':sampled_ids}

def collate_fn(batch):
    # batch 是一个列表,其中每个元素是 __getitem__ 返回的字典
    
    # 使用列表推导式和 torch.stack 高效地组合批次
    videos = torch.stack([item['video_tensor'] for item in batch], dim=0)
    similarity_tabs = torch.stack([item['similarity_tab'] for item in batch], dim=0)
    
    # 其他非张量数据正常收集
    anatomies = [item['anatomy'] for item in batch]
    sampled_ids_list = [item['sampled_ids'] for item in batch]
    if 'modality' in batch[0]:
        modalities = torch.tensor([item['modality'] for item in batch])
        return videos, similarity_tabs, anatomies, sampled_ids_list, modalities
    else:
        return videos, similarity_tabs, anatomies, sampled_ids_list

# if __name__ == '__main__':
#     from torch.utils.data import Dataset, DataLoader, random_split
#     print('** CXR **')
#     dataset = Conditional_CTReportDataset_Train(
#         local_batch_size=8,
#         modality='2D',
#         jsonl_file='/mnt/petrelfs/zhangtengfei/RadIR/dataset/CXR/MIMIC_CXR/train.jsonl',
#         csv_file_dir='/mnt/petrelfs/zhangtengfei/RadIR/dataset/CXR/MIMIC_CXR/train_entity',
#         npy_file_dir='/mnt/petrelfs/zhangtengfei/RadIR/dataset/CXR/MIMIC_CXR/train_ratescore',
#         positive_threshold=0.75,
#         negative_threshold=1,
#         anatomy_filter=['left hemidiaphragm', 'airspace']
#     )
    
#     dl = DataLoader(
#         dataset,
#         num_workers=8,
#         batch_size=1,
#         shuffle = False,
#         drop_last = False,
#         pin_memory = False,
#         collate_fn = collate_fn
#     )        

#     dl_iter = iter(dl)
    
#     for threshold in [0.1, 0.15, 0.2, 0.25, 0.3]:
    
#         count = 0
#         triplet_count = 0
        
#         while True:
            
#             count += 1
#             if count > 50:
#                 break
            
#             video, similarity_tab, anatomy, batched_sampled_ids = next(dl_iter)   # 1 8 8
            
#             print(batched_sampled_ids)
#             print(similarity_tab)
                    
#             mask = (similarity_tab.unsqueeze(3) - similarity_tab.unsqueeze(2)) > 0.2   # N N N
#             triplet_count += torch.sum(mask)
            
#         print(f'** {threshold} **')
#         print(similarity_tab.shape)
#         print(triplet_count/count)
    
#     # > 0.1 时 8x8x8 的 matrix 上平均有 _ 个有效对
#     # > 0.15 时 8x8x8 的 matrix 上平均有 _ 个有效对
#     # > 0.2 时 8x8x8 的 matrix 上平均有 _ 个有效对
#     # > 0.25 时 8x8x8 的 matrix 上平均有 _ 个有效对
#     # > 0.3 时 8x8x8 的 matrix 上平均有 _ 个有效对
#     print('** CT **')
#     dataset = Conditional_CTReportDataset_Train(
#         local_batch_size=8,
#         modality='3D',
#         jsonl_file='/mnt/petrelfs/zhangtengfei/RadIR/dataset/CT_RATE/train_replaced.jsonl',
#         csv_file_dir='/mnt/petrelfs/zhangtengfei/RadIR/dataset/CT_RATE/anatomy/train_entity',
#         npy_file_dir='/mnt/petrelfs/zhangtengfei/RadIR/dataset/CT_RATE/anatomy/train_ratescore',
#         positive_threshold=0.75,
#         negative_threshold=1,
#         anatomy_filter=['right rib', 'thyroid gland']
#     )
    
#     dl = DataLoader(
#         dataset,
#         num_workers=8,
#         batch_size=1,
#         shuffle = False,
#         drop_last = False,
#         pin_memory = False,
#         collate_fn = collate_fn
#     )        
    
#     dl_iter = iter(dl)
    
#     for threshold in [0.1, 0.15, 0.2, 0.25, 0.3]:
    
#         count = 0
#         triplet_count = 0
        
#         while True:
            
#             count += 1
#             if count > 50:
#                 break
            
#             video, similarity_tab, anatomy, batched_sampled_ids = next(dl_iter)   # 1 8 8
            
#             print(batched_sampled_ids)
#             print(similarity_tab)
                    
#             mask = (similarity_tab.unsqueeze(3) - similarity_tab.unsqueeze(2)) > 0.2   # N N N
#             triplet_count += torch.sum(mask)
            
#         print(f'** {threshold} **')
#         print(similarity_tab.shape)
#         print(triplet_count/count)
    
if __name__ == '__main__':
    from torch.utils.data import Dataset, DataLoader, random_split
    import time
    from tqdm import tqdm
    
    print('** 测试数据加载耗时 **')
    
    # 创建CXR数据集
    print('** 创建CXR数据集 **')
    cxr_dataset = Conditional_CTReportDataset_Train(
        local_batch_size=35,
        modality='2D',
        
        jsonl_file=None,  # ⚠️ 你没有 .jsonl 文件,直接设为 None 或删除此参数
        csv_file_dir='/mnt/data-3/users/zhangzeyu/HMClip/RadIR/dataset/MIMIC-IR/anatomy/train_caption',
        npy_file_dir='/mnt/data-3/users/zhangzeyu/HMClip/RadIR/dataset/MIMIC-IR/anatomy/train_ratescore',
        
        positive_threshold=0.75,
        negative_threshold=1,
        anatomy_filter=['lungs', 'ventricle', 'right upper lobe'],
        need_aug=True
    )
    
    # 创建CT数据集
    print('** 创建CT数据集 **')
    ct_dataset = Conditional_CTReportDataset_Train(
        local_batch_size=7,
        modality='3D',
        jsonl_file   = '/mnt/data-3/users/zhangzeyu/HMClip/RadIR/dataset/CTRATE-IR/train.jsonl',
        csv_file_dir = '/mnt/data-3/users/zhangzeyu/HMClip/RadIR/dataset/CTRATE-IR/anatomy/train_entity',
        npy_file_dir = '/mnt/data-3/users/zhangzeyu/HMClip/RadIR/dataset/CTRATE-IR/anatomy/train_ratescore_compressed',
        positive_threshold=0.75,
        negative_threshold=1,
        anatomy_filter=['bone', 'pulmonary artery', 'stomach'],
        need_aug=True
    )
    
    # 测试不同的工作线程数量
    for num_workers in [2, 4,6, 8,12]:
        print(f"\n使用 {num_workers} 个工作线程")
        
        # 测试CXR数据集
        dl_cxr = DataLoader(
            cxr_dataset,
            num_workers=num_workers,
            batch_size=2,
            shuffle=False,
            drop_last=False,
            pin_memory=True,
            collate_fn=collate_fn
        )
        
        # 预热(初始化可能很慢)
        print("预热CXR数据加载器...")
        for _ in range(5):
            next(iter(dl_cxr))
        
        # 测量CXR数据加载时间
        print("开始测量CXR数据加载时间...")
        start_time = time.time()
        total_batches = min(20, len(dl_cxr))  # 限制测试数量
        
        for i, _ in enumerate(tqdm(dl_cxr, total=total_batches)):
            if i >= total_batches - 1:
                break
        
        cxr_time = time.time() - start_time
        print(f"CXR数据集: {total_batches}个批次耗时 {cxr_time:.2f}秒, 平均每批次 {cxr_time/total_batches:.4f}秒")
        
        # 测试CT数据集
        dl_ct = DataLoader(
            ct_dataset,
            num_workers=num_workers,
            batch_size=1,
            shuffle=False,
            drop_last=False,
            pin_memory=True,
            collate_fn=collate_fn
        )
        
        # 预热
        print("预热CT数据加载器...")
        for _ in range(5):
            next(iter(dl_ct))
        
        # 测量CT数据加载时间
        print("开始测量CT数据加载时间...")
        start_time = time.time()
        total_batches = min(20, len(dl_ct))  # 限制测试数量
        
        for i, _ in enumerate(tqdm(dl_ct, total=total_batches)):
            if i >= total_batches - 1:
                break
        
        ct_time = time.time() - start_time
        print(f"CT数据集: {total_batches}个批次耗时 {ct_time:.2f}秒, 平均每批次 {ct_time/total_batches:.4f}秒")

    # # 测试不同的batch_size
    # num_workers = 8  # 使用固定的工作线程数
    # print(f"\n使用固定的{num_workers}个工作线程,测试不同batch_size")
    
    # for batch_size in [1, 2, 4, 8, 16]:
    #     print(f"\nbatch_size = {batch_size}")
        
    #     # CXR数据集
    #     dl_cxr = DataLoader(
    #         cxr_dataset,
    #         num_workers=num_workers,
    #         batch_size=batch_size,
    #         shuffle=False,
    #         drop_last=False,
    #         pin_memory=True,
    #         collate_fn=collate_fn
    #     )
        
    #     # 预热
    #     print("预热CXR数据加载器...")
    #     for _ in range(3):
    #         next(iter(dl_cxr))
        
    #     # 测量CXR数据加载时间
    #     print("开始测量CXR数据加载时间...")
    #     start_time = time.time()
    #     total_batches = min(50, len(dl_cxr))  # 限制测试数量
        
    #     for i, _ in enumerate(tqdm(dl_cxr, total=total_batches)):
    #         if i >= total_batches - 1:
    #             break
        
    #     cxr_time = time.time() - start_time
    #     print(f"CXR数据集(batch_size={batch_size}): {total_batches}个批次耗时 {cxr_time:.2f}秒, "
    #           f"平均每批次 {cxr_time/total_batches:.4f}秒, "
    #           f"平均每样本 {cxr_time/(total_batches*batch_size):.4f}秒")