zxiao commited on
Commit
7ccb334
·
1 Parent(s): 913e058

update evaluation and fix psnr

Browse files
Files changed (3) hide show
  1. README.md +1 -0
  2. calculate_fid.py +277 -0
  3. compute_video_psnr.py +0 -195
README.md CHANGED
@@ -139,6 +139,7 @@ python calculate_fid.py --videos_dir outputs/2025-11-30/00-02-42/videos/test_vis
139
  | FID | 15.13 |
140
 
141
  *Note: FID is computed over 5000 frames.*
 
142
  *Note: Previous versions incorrectly used `data_range=2.0` for PSNR calculation, but the decoded video data is in the range [0, 1], so `data_range=1.0` should be used. This bug inflated PSNR values by approximately 6 dB. We have now corrected this in the latest version by clipping predictions to [0, 1] before metric computation. The relative performance comparisons and conclusions remain unchanged.*
143
 
144
  ---
 
139
  | FID | 15.13 |
140
 
141
  *Note: FID is computed over 5000 frames.*
142
+
143
  *Note: Previous versions incorrectly used `data_range=2.0` for PSNR calculation, but the decoded video data is in the range [0, 1], so `data_range=1.0` should be used. This bug inflated PSNR values by approximately 6 dB. We have now corrected this in the latest version by clipping predictions to [0, 1] before metric computation. The relative performance comparisons and conclusions remain unchanged.*
144
 
145
  ---
calculate_fid.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Calculate FID (Fréchet Inception Distance) between predicted and ground truth videos.
4
+
5
+ Usage:
6
+ python calculate_fid.py --videos_dir /path/to/videos
7
+ python calculate_fid.py --videos_dir /path/to/videos --batch_size 32
8
+ """
9
+
10
+ import torch
11
+ import numpy as np
12
+ from pathlib import Path
13
+ from tqdm import tqdm
14
+ import argparse
15
+ import cv2
16
+ from torchmetrics.image.fid import FrechetInceptionDistance
17
+
18
+
19
+ def load_video_frames(video_path, max_frames=None):
20
+ """
21
+ Load frames from a video file.
22
+
23
+ Args:
24
+ video_path: Path to the video file
25
+ max_frames: Maximum number of frames to load (None = all frames)
26
+
27
+ Returns:
28
+ torch.Tensor: Video frames with shape (T, C, H, W) in range [0, 255]
29
+ """
30
+ cap = cv2.VideoCapture(str(video_path))
31
+ frames = []
32
+ frame_count = 0
33
+
34
+ while True:
35
+ ret, frame = cap.read()
36
+ if not ret:
37
+ break
38
+
39
+ # Convert BGR to RGB
40
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
41
+ frames.append(frame)
42
+ frame_count += 1
43
+
44
+ if max_frames and frame_count >= max_frames:
45
+ break
46
+
47
+ cap.release()
48
+
49
+ if len(frames) == 0:
50
+ raise ValueError(f"No frames loaded from {video_path}")
51
+
52
+ # Convert to tensor: (T, H, W, C) -> (T, C, H, W)
53
+ frames = np.stack(frames, axis=0)
54
+ frames = torch.from_numpy(frames).permute(0, 3, 1, 2)
55
+
56
+ return frames
57
+
58
+
59
+ def load_videos_from_directory(video_dir, max_frames_per_video=None, max_videos=None):
60
+ """
61
+ Load all videos from a directory.
62
+
63
+ Args:
64
+ video_dir: Directory containing .mp4 files
65
+ max_frames_per_video: Maximum frames to load per video
66
+ max_videos: Maximum number of videos to load
67
+
68
+ Returns:
69
+ torch.Tensor: All frames concatenated with shape (N, C, H, W)
70
+ """
71
+ video_dir = Path(video_dir)
72
+ video_paths = sorted(list(video_dir.glob("**/*.mp4")))
73
+
74
+ if max_videos:
75
+ video_paths = video_paths[:max_videos]
76
+
77
+ all_frames = []
78
+
79
+ print(f"Loading videos from {video_dir}")
80
+ print(f"Found {len(video_paths)} videos")
81
+
82
+ for video_path in tqdm(video_paths, desc="Loading videos"):
83
+ try:
84
+ frames = load_video_frames(video_path, max_frames=max_frames_per_video)
85
+ all_frames.append(frames)
86
+ except Exception as e:
87
+ print(f"\nWarning: Failed to load {video_path.name}: {e}")
88
+ continue
89
+
90
+ if len(all_frames) == 0:
91
+ raise ValueError(f"No videos loaded from {video_dir}")
92
+
93
+ # Concatenate all frames: (N_videos, T, C, H, W) -> (N_total_frames, C, H, W)
94
+ all_frames = torch.cat(all_frames, dim=0)
95
+
96
+ print(f"Loaded {all_frames.shape[0]} frames total")
97
+ print(f"Frame shape: {all_frames.shape[1:]}")
98
+
99
+ return all_frames
100
+
101
+
102
+ def calculate_fid(pred_dir, gt_dir, batch_size=32, device='cuda',
103
+ max_frames_per_video=None, max_videos=None):
104
+ """
105
+ Calculate FID between predicted and ground truth videos.
106
+
107
+ Args:
108
+ pred_dir: Directory containing predicted videos
109
+ gt_dir: Directory containing ground truth videos
110
+ batch_size: Batch size for FID calculation
111
+ device: Device to use ('cuda' or 'cpu')
112
+ max_frames_per_video: Maximum frames to load per video
113
+ max_videos: Maximum number of videos to load from each directory
114
+
115
+ Returns:
116
+ float: FID score
117
+ """
118
+ print("="*60)
119
+ print("FID Calculation")
120
+ print("="*60)
121
+ print(f"Pred directory: {pred_dir}")
122
+ print(f"GT directory: {gt_dir}")
123
+ print(f"Device: {device}")
124
+ print(f"Batch size: {batch_size}")
125
+ print("="*60 + "\n")
126
+
127
+ # Check if directories exist
128
+ pred_dir = Path(pred_dir)
129
+ gt_dir = Path(gt_dir)
130
+
131
+ if not pred_dir.exists():
132
+ raise ValueError(f"Pred directory does not exist: {pred_dir}")
133
+ if not gt_dir.exists():
134
+ raise ValueError(f"GT directory does not exist: {gt_dir}")
135
+
136
+ # Load videos
137
+ print("\n[1/3] Loading predicted videos...")
138
+ pred_frames = load_videos_from_directory(
139
+ pred_dir,
140
+ max_frames_per_video=max_frames_per_video,
141
+ max_videos=max_videos
142
+ )
143
+
144
+ print("\n[2/3] Loading ground truth videos...")
145
+ gt_frames = load_videos_from_directory(
146
+ gt_dir,
147
+ max_frames_per_video=max_frames_per_video,
148
+ max_videos=max_videos
149
+ )
150
+
151
+ # Initialize FID model
152
+ print("\n[3/3] Calculating FID...")
153
+ fid_model = FrechetInceptionDistance(normalize=True).to(device)
154
+
155
+ # Process pred frames in batches
156
+ print("Processing predicted frames...")
157
+ num_pred_frames = pred_frames.shape[0]
158
+ for i in tqdm(range(0, num_pred_frames, batch_size)):
159
+ batch = pred_frames[i:i+batch_size]
160
+ batch = batch.to(device)
161
+ fid_model.update(batch, real=False)
162
+
163
+ # Process gt frames in batches
164
+ print("Processing ground truth frames...")
165
+ num_gt_frames = gt_frames.shape[0]
166
+ for i in tqdm(range(0, num_gt_frames, batch_size)):
167
+ batch = gt_frames[i:i+batch_size]
168
+ batch = batch.to(device)
169
+ fid_model.update(batch, real=True)
170
+
171
+ # Compute FID
172
+ fid_score = fid_model.compute().item()
173
+
174
+ return fid_score
175
+
176
+
177
+ def main():
178
+ parser = argparse.ArgumentParser(
179
+ description="Calculate FID between predicted and ground truth videos"
180
+ )
181
+ parser.add_argument(
182
+ "--videos_dir",
183
+ type=str,
184
+ default="/mnt/worldmem_valid/outputs/2025-12-01/08-09-46/videos/test_vis",
185
+ help="Base directory containing 'pred' and 'gt' subdirectories"
186
+ )
187
+ parser.add_argument(
188
+ "--pred_dir",
189
+ type=str,
190
+ default=None,
191
+ help="Override pred directory (default: {videos_dir}/pred)"
192
+ )
193
+ parser.add_argument(
194
+ "--gt_dir",
195
+ type=str,
196
+ default=None,
197
+ help="Override gt directory (default: {videos_dir}/gt)"
198
+ )
199
+ parser.add_argument(
200
+ "--batch_size",
201
+ type=int,
202
+ default=32,
203
+ help="Batch size for FID calculation (default: 32)"
204
+ )
205
+ parser.add_argument(
206
+ "--device",
207
+ type=str,
208
+ default="cuda" if torch.cuda.is_available() else "cpu",
209
+ help="Device to use (default: cuda if available)"
210
+ )
211
+ parser.add_argument(
212
+ "--max_frames_per_video",
213
+ type=int,
214
+ default=None,
215
+ help="Maximum frames to load per video (default: None, load all)"
216
+ )
217
+ parser.add_argument(
218
+ "--max_videos",
219
+ type=int,
220
+ default=50,
221
+ help="Maximum number of videos to load (default: None, load all)"
222
+ )
223
+
224
+ args = parser.parse_args()
225
+
226
+ # Determine pred and gt directories
227
+ videos_dir = Path(args.videos_dir)
228
+
229
+ if args.pred_dir:
230
+ pred_dir = Path(args.pred_dir)
231
+ else:
232
+ pred_dir = videos_dir / "pred"
233
+
234
+ if args.gt_dir:
235
+ gt_dir = Path(args.gt_dir)
236
+ else:
237
+ gt_dir = videos_dir / "gt"
238
+
239
+ # Calculate FID
240
+ try:
241
+ fid_score = calculate_fid(
242
+ pred_dir=pred_dir,
243
+ gt_dir=gt_dir,
244
+ batch_size=args.batch_size,
245
+ device=args.device,
246
+ max_frames_per_video=args.max_frames_per_video,
247
+ max_videos=args.max_videos
248
+ )
249
+
250
+ # Print results
251
+ print("\n" + "="*60)
252
+ print("RESULTS")
253
+ print("="*60)
254
+ print(f"FID Score: {fid_score:.4f}")
255
+ print("="*60)
256
+
257
+ # Save results to file
258
+ output_file = videos_dir / "fid_results.txt"
259
+ with open(output_file, 'w') as f:
260
+ f.write(f"FID Score: {fid_score:.4f}\n")
261
+ f.write(f"Pred directory: {pred_dir}\n")
262
+ f.write(f"GT directory: {gt_dir}\n")
263
+
264
+ print(f"\nResults saved to: {output_file}")
265
+
266
+ except Exception as e:
267
+ print(f"\n✗ Error: {e}")
268
+ import traceback
269
+ traceback.print_exc()
270
+ return 1
271
+
272
+ return 0
273
+
274
+
275
+ if __name__ == "__main__":
276
+ exit(main())
277
+
compute_video_psnr.py DELETED
@@ -1,195 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Compute PSNR for each video pair in gt and pred directories.
4
- """
5
- import os
6
- import numpy as np
7
- from pathlib import Path
8
- from tqdm import tqdm
9
- import pandas as pd
10
- import imageio
11
-
12
-
13
- def load_video(video_path):
14
- """
15
- Load a video file and return frames as a numpy array.
16
-
17
- Args:
18
- video_path: Path to the video file
19
-
20
- Returns:
21
- np.ndarray: Video frames of shape (T, C, H, W) normalized to [0, 1]
22
- """
23
- reader = imageio.get_reader(str(video_path))
24
- frames = []
25
- for frame in reader:
26
- # frame is already RGB
27
- frame = frame.astype(np.float32) / 255.0
28
- frames.append(frame)
29
- reader.close()
30
-
31
- if len(frames) == 0:
32
- raise ValueError(f"No frames loaded from {video_path}")
33
-
34
- # Convert to numpy array: (T, H, W, C) -> (T, C, H, W)
35
- frames = np.stack(frames, axis=0)
36
- frames = np.transpose(frames, (0, 3, 1, 2))
37
-
38
- return frames
39
-
40
-
41
- def calculate_psnr(pred, gt, data_range=1.0):
42
- """
43
- Calculate PSNR between two images/videos.
44
-
45
- Args:
46
- pred: Predicted frames
47
- gt: Ground truth frames
48
- data_range: Data range (default: 1.0)
49
-
50
- Returns:
51
- float: PSNR value in dB
52
- """
53
- mse = np.mean((pred - gt) ** 2)
54
- if mse == 0:
55
- return float('inf')
56
- return 10 * np.log10((data_range ** 2) / mse)
57
-
58
-
59
- def compute_psnr(pred_frames, gt_frames, data_range=1.0):
60
- """
61
- Compute PSNR between predicted and ground truth frames.
62
-
63
- Args:
64
- pred_frames: Predicted frames array (T, C, H, W)
65
- gt_frames: Ground truth frames array (T, C, H, W)
66
- data_range: Data range of the frames (default: 1.0 for [0, 1] range)
67
-
68
- Returns:
69
- dict: Dictionary containing overall PSNR and frame-wise PSNR
70
- """
71
- # Ensure same shape
72
- assert pred_frames.shape == gt_frames.shape, \
73
- f"Shape mismatch: pred {pred_frames.shape} vs gt {gt_frames.shape}"
74
-
75
- T, C, H, W = pred_frames.shape
76
-
77
- # Compute frame-wise PSNR
78
- frame_wise_psnr = []
79
- for t in range(T):
80
- psnr = calculate_psnr(pred_frames[t], gt_frames[t], data_range=data_range)
81
- frame_wise_psnr.append(psnr)
82
-
83
- # Compute overall PSNR
84
- overall_psnr = calculate_psnr(pred_frames, gt_frames, data_range=data_range)
85
-
86
- return {
87
- 'overall_psnr': overall_psnr,
88
- 'frame_wise_psnr': frame_wise_psnr,
89
- 'mean_frame_psnr': np.mean(frame_wise_psnr),
90
- 'std_frame_psnr': np.std(frame_wise_psnr),
91
- 'num_frames': T
92
- }
93
-
94
-
95
- def main():
96
- # Directories
97
- gt_dir = Path("/mnt/WorldMem/outputs/2025-12-01/23-39-44/videos/test_vis/gt")
98
- pred_dir = Path("/mnt/WorldMem/outputs/2025-12-01/23-39-44/videos/test_vis/pred")
99
-
100
- # Get all video files in gt directory
101
- gt_videos = sorted(gt_dir.glob("*.mp4"))
102
-
103
- print(f"Found {len(gt_videos)} videos to process")
104
- print("=" * 80)
105
-
106
- results = []
107
-
108
- for gt_path in tqdm(gt_videos, desc="Computing PSNR"):
109
- video_name = gt_path.name
110
- pred_path = pred_dir / video_name
111
-
112
- if not pred_path.exists():
113
- print(f"Warning: Prediction video not found for {video_name}")
114
- continue
115
-
116
- try:
117
- # Load videos
118
- gt_frames = load_video(gt_path)
119
- pred_frames = load_video(pred_path)
120
-
121
- # Compute PSNR
122
- psnr_results = compute_psnr(pred_frames, gt_frames, data_range=1.0)
123
-
124
- # Store results
125
- result = {
126
- 'video_name': video_name,
127
- 'overall_psnr': psnr_results['overall_psnr'],
128
- 'mean_frame_psnr': psnr_results['mean_frame_psnr'],
129
- 'std_frame_psnr': psnr_results['std_frame_psnr'],
130
- 'num_frames': psnr_results['num_frames']
131
- }
132
- results.append(result)
133
-
134
- except Exception as e:
135
- print(f"Error processing {video_name}: {str(e)}")
136
- continue
137
-
138
- # Create DataFrame for better visualization
139
- df = pd.DataFrame(results)
140
-
141
- # Print results
142
- print("\n" + "=" * 80)
143
- print("PSNR Results for Each Video:")
144
- print("=" * 80)
145
- print(df.to_string(index=False))
146
-
147
- # Print summary statistics
148
- print("\n" + "=" * 80)
149
- print("Summary Statistics:")
150
- print("=" * 80)
151
- print(f"Average PSNR across all videos: {df['overall_psnr'].mean():.4f} ± {df['overall_psnr'].std():.4f}")
152
- print(f"Min PSNR: {df['overall_psnr'].min():.4f} ({df.loc[df['overall_psnr'].idxmin(), 'video_name']})")
153
- print(f"Max PSNR: {df['overall_psnr'].max():.4f} ({df.loc[df['overall_psnr'].idxmax(), 'video_name']})")
154
- print(f"Median PSNR: {df['overall_psnr'].median():.4f}")
155
-
156
- # Group by video ID (video_0, video_1, etc.) and rank
157
- df['video_id'] = df['video_name'].str.extract(r'(video_\d+)')[0]
158
- df['rank'] = df['video_name'].str.extract(r'rank(\d+)')[0].astype(int)
159
-
160
- print("\n" + "=" * 80)
161
- print("Average PSNR by Video ID:")
162
- print("=" * 80)
163
- video_group = df.groupby('video_id')['overall_psnr'].agg(['mean', 'std', 'count'])
164
- print(video_group.to_string())
165
-
166
- print("\n" + "=" * 80)
167
- print("Average PSNR by Rank:")
168
- print("=" * 80)
169
- rank_group = df.groupby('rank')['overall_psnr'].agg(['mean', 'std', 'count'])
170
- print(rank_group.to_string())
171
-
172
- # Save results to CSV
173
- output_csv = gt_dir.parent / "psnr_results.csv"
174
- df.to_csv(output_csv, index=False)
175
- print(f"\nResults saved to: {output_csv}")
176
-
177
- # Save summary to text file
178
- output_txt = gt_dir.parent / "psnr_summary.txt"
179
- with open(output_txt, 'w') as f:
180
- f.write("PSNR Results Summary\n")
181
- f.write("=" * 80 + "\n\n")
182
- f.write(f"Average PSNR: {df['overall_psnr'].mean():.4f} ± {df['overall_psnr'].std():.4f}\n")
183
- f.write(f"Min PSNR: {df['overall_psnr'].min():.4f}\n")
184
- f.write(f"Max PSNR: {df['overall_psnr'].max():.4f}\n")
185
- f.write(f"Median PSNR: {df['overall_psnr'].median():.4f}\n")
186
- f.write("\n" + "=" * 80 + "\n")
187
- f.write("Full Results:\n")
188
- f.write("=" * 80 + "\n")
189
- f.write(df.to_string(index=False))
190
- print(f"Summary saved to: {output_txt}")
191
-
192
-
193
- if __name__ == "__main__":
194
- main()
195
-