PixelNavigation commited on
Commit
44f320f
·
1 Parent(s): ce19294

Rewriting the complete backend with gradcam and explainability support

Browse files
backend/Deepfake-Detection/detect_from_video.py ADDED
File without changes
backend/app.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from flask_cors import CORS
3
+ from werkzeug.utils import secure_filename
4
+ import os
5
+ import sys
6
+ import torch
7
+ import re
8
+ import numpy as np
9
+ from PIL import Image
10
+
11
+ # Add the dfdc_deepfake_challenge directory to the path
12
+ sys.path.append('dfdc_model')
13
+
14
+ from dfdc_model.kernel_utils import VideoReader, FaceExtractor, confident_strategy, predict_on_video
15
+ from dfdc_model.training.zoo.classifiers import DeepFakeClassifier
16
+ from enhanced_prediction import enhanced_predict_on_video, enhanced_predict_on_image
17
+
18
+ app = Flask(__name__)
19
+ CORS(app)
20
+ app.config['UPLOAD_FOLDER'] = 'uploads/'
21
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
22
+
23
+ # Initialize the model and preprocessing pipeline
24
+ def load_models():
25
+ models = []
26
+ weights_dir = "dfdc_deepfake_challenge/weights"
27
+ # Update these model names based on your downloaded weights
28
+ model_files = [
29
+ "final_888_DeepFakeClassifier_tf_efficientnet_b7_ns_0_40",
30
+ # Add more model files if you have them
31
+ ]
32
+
33
+ device = "cuda" if torch.cuda.is_available() else "cpu"
34
+
35
+ for model_file in model_files:
36
+ model_path = os.path.join(weights_dir, model_file)
37
+ if os.path.exists(model_path):
38
+ model = DeepFakeClassifier(encoder="tf_efficientnet_b7_ns").to(device)
39
+ print(f"Loading state dict {model_path}")
40
+ checkpoint = torch.load(model_path, map_location="cpu")
41
+ state_dict = checkpoint.get("state_dict", checkpoint)
42
+ model.load_state_dict({re.sub("^module.", "", k): v for k, v in state_dict.items()}, strict=True)
43
+ model.eval()
44
+ del checkpoint
45
+ if device == "cuda":
46
+ models.append(model.half())
47
+ else:
48
+ models.append(model)
49
+
50
+ return models
51
+
52
+ # Initialize the preprocessing pipeline
53
+ def initialize_preprocessing():
54
+ frames_per_video = 32
55
+ video_reader = VideoReader()
56
+ video_read_fn = lambda x: video_reader.read_frames(x, num_frames=frames_per_video)
57
+ face_extractor = FaceExtractor(video_read_fn)
58
+ input_size = 380
59
+ return face_extractor, input_size, frames_per_video
60
+
61
+ # Load models and initialize preprocessing
62
+ print("Loading models...")
63
+ models = load_models()
64
+ face_extractor, input_size, frames_per_video = initialize_preprocessing()
65
+ print(f"Loaded {len(models)} models")
66
+
67
+ def predict_on_image(image_path):
68
+ """Predict on a single image using face extraction and model inference"""
69
+ try:
70
+ from dfdc_model.kernel_utils import isotropically_resize_image, put_to_center, normalize_transform
71
+ import numpy as np
72
+ import cv2
73
+
74
+ # Read the image
75
+ image = cv2.imread(image_path)
76
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
77
+
78
+ # Use MTCNN to detect faces
79
+ from PIL import Image as PILImage
80
+ img_pil = PILImage.fromarray(image)
81
+ img_pil = img_pil.resize(size=[s // 2 for s in img_pil.size])
82
+
83
+ device = "cuda" if torch.cuda.is_available() else "cpu"
84
+ detector = face_extractor.detector
85
+
86
+ batch_boxes, probs = detector.detect(img_pil, landmarks=False)
87
+
88
+ if batch_boxes is None:
89
+ return 0.5 # No faces detected, return neutral
90
+
91
+ # Process the largest/most confident face
92
+ best_idx = 0
93
+ if len(probs) > 1:
94
+ best_idx = np.argmax(probs)
95
+
96
+ bbox = batch_boxes[best_idx]
97
+ if bbox is not None:
98
+ xmin, ymin, xmax, ymax = [int(b * 2) for b in bbox]
99
+ w = xmax - xmin
100
+ h = ymax - ymin
101
+ p_h = h // 3
102
+ p_w = w // 3
103
+ crop = image[max(ymin - p_h, 0):ymax + p_h, max(xmin - p_w, 0):xmax + p_w]
104
+
105
+ # Resize and center the face
106
+ resized_face = isotropically_resize_image(crop, input_size)
107
+ resized_face = put_to_center(resized_face, input_size)
108
+
109
+ # Prepare for model inference
110
+ x = torch.tensor(resized_face, device=device).float().unsqueeze(0)
111
+ x = x.permute((0, 3, 1, 2))
112
+ x = normalize_transform(x[0] / 255.).unsqueeze(0)
113
+
114
+ # Run prediction
115
+ with torch.no_grad():
116
+ preds = []
117
+ for model in models:
118
+ if device == "cuda":
119
+ y_pred = model(x.half())
120
+ else:
121
+ y_pred = model(x)
122
+ y_pred = torch.sigmoid(y_pred.squeeze())
123
+ preds.append(y_pred.cpu().numpy())
124
+ return np.mean(preds)
125
+
126
+ return 0.5
127
+ except Exception as e:
128
+ print(f"Error predicting on image: {e}")
129
+ return 0.5
130
+
131
+ @app.route('/')
132
+ def index():
133
+ return jsonify({'message': 'Deepfake Detection Flask API is running.'})
134
+
135
+ @app.route('/analyze/image', methods=['POST'])
136
+ def analyze_image():
137
+ if 'file' not in request.files:
138
+ return jsonify({'error': 'No file part'}), 400
139
+ file = request.files['file']
140
+ if file.filename == '':
141
+ return jsonify({'error': 'No selected file'}), 400
142
+ filename = secure_filename(file.filename)
143
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
144
+ file.save(filepath)
145
+
146
+ try:
147
+ # Use enhanced image prediction with Grad-CAM
148
+ result = enhanced_predict_on_image(filepath, models, input_size)
149
+
150
+ label = 'fake' if result['prediction'] > 0.5 else 'real'
151
+ response = {
152
+ 'label': label,
153
+ 'confidence': float(result['prediction']),
154
+ 'details': f'Image analyzed using {len(models)} models with face detection and Grad-CAM',
155
+ 'faces_detected': 1,
156
+ 'processing_time': 'N/A',
157
+ 'gradcam_image': result['gradcam_image'],
158
+ 'fake_frames': [] # Not applicable for images
159
+ }
160
+ return jsonify(response)
161
+ except Exception as e:
162
+ return jsonify({'error': str(e)}), 500
163
+
164
+ @app.route('/analyze/video', methods=['POST'])
165
+ def analyze_video():
166
+ if 'file' not in request.files:
167
+ return jsonify({'error': 'No file part'}), 400
168
+ file = request.files['file']
169
+ if file.filename == '':
170
+ return jsonify({'error': 'No selected file'}), 400
171
+ filename = secure_filename(file.filename)
172
+ filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
173
+ file.save(filepath)
174
+
175
+ try:
176
+ # Use enhanced video prediction with frame analysis and Grad-CAM
177
+ result = enhanced_predict_on_video(
178
+ face_extractor=face_extractor,
179
+ video_path=filepath,
180
+ batch_size=frames_per_video,
181
+ input_size=input_size,
182
+ models=models,
183
+ strategy=confident_strategy,
184
+ apply_compression=False,
185
+ return_frame_predictions=True
186
+ )
187
+
188
+ prediction = result['prediction']
189
+ label = 'fake' if prediction > 0.5 else 'real'
190
+
191
+ response = {
192
+ 'label': label,
193
+ 'confidence': float(prediction),
194
+ 'details': f'Video analyzed using {len(models)} models with confident strategy and frame analysis',
195
+ 'faces_detected': len(result.get('frame_predictions', [])),
196
+ 'processing_time': 'N/A',
197
+ 'fake_frames': result.get('fake_frames', []),
198
+ 'gradcam_image': result.get('gradcam_images', [None])[0], # Return first Grad-CAM image
199
+ 'frame_predictions': result.get('frame_predictions', [])
200
+ }
201
+ return jsonify(response)
202
+ except Exception as e:
203
+ return jsonify({'error': str(e)}), 500
204
+
205
+ if __name__ == '__main__':
206
+ app.run(host='0.0.0.0', port=8000)
backend/deepfake_integration.py ADDED
File without changes
backend/dfdc_model/__pycache__/kernel_utils.cpython-312.pyc ADDED
Binary file (18 kB). View file
 
backend/dfdc_model/__pycache__/kernel_utils.cpython-313.pyc ADDED
Binary file (17.7 kB). View file
 
backend/dfdc_model/configs/b5.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "network": "DeepFakeClassifier",
3
+ "encoder": "tf_efficientnet_b5_ns",
4
+ "batches_per_epoch": 2500,
5
+ "size": 380,
6
+ "fp16": true,
7
+ "optimizer": {
8
+ "batch_size": 20,
9
+ "type": "SGD",
10
+ "momentum": 0.9,
11
+ "weight_decay": 1e-4,
12
+ "learning_rate": 0.01,
13
+ "nesterov": true,
14
+ "schedule": {
15
+ "type": "poly",
16
+ "mode": "step",
17
+ "epochs": 30,
18
+ "params": {"max_iter": 75100}
19
+ }
20
+ },
21
+ "normalize": {
22
+ "mean": [0.485, 0.456, 0.406],
23
+ "std": [0.229, 0.224, 0.225]
24
+ },
25
+ "losses": {
26
+ "BinaryCrossentropy": 1
27
+ }
28
+ }
backend/dfdc_model/configs/b7.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "network": "DeepFakeClassifier",
3
+ "encoder": "tf_efficientnet_b7_ns",
4
+ "batches_per_epoch": 2500,
5
+ "size": 380,
6
+ "fp16": true,
7
+ "optimizer": {
8
+ "batch_size": 12,
9
+ "type": "SGD",
10
+ "momentum": 0.9,
11
+ "weight_decay": 1e-4,
12
+ "learning_rate": 0.01,
13
+ "nesterov": true,
14
+ "schedule": {
15
+ "type": "poly",
16
+ "mode": "step",
17
+ "epochs": 40,
18
+ "params": {"max_iter": 100500}
19
+ }
20
+ },
21
+ "normalize": {
22
+ "mean": [0.485, 0.456, 0.406],
23
+ "std": [0.229, 0.224, 0.225]
24
+ },
25
+ "losses": {
26
+ "BinaryCrossentropy": 1
27
+ }
28
+ }
29
+
backend/dfdc_model/kernel_utils.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import cv2
4
+ import numpy as np
5
+ import torch
6
+ from PIL import Image
7
+ from albumentations.augmentations.functional import image_compression
8
+ from facenet_pytorch.models.mtcnn import MTCNN
9
+ from concurrent.futures import ThreadPoolExecutor
10
+
11
+ from torchvision.transforms import Normalize
12
+
13
+ mean = [0.485, 0.456, 0.406]
14
+ std = [0.229, 0.224, 0.225]
15
+ normalize_transform = Normalize(mean, std)
16
+
17
+
18
+ class VideoReader:
19
+ """Helper class for reading one or more frames from a video file."""
20
+
21
+ def __init__(self, verbose=True, insets=(0, 0)):
22
+ """Creates a new VideoReader.
23
+
24
+ Arguments:
25
+ verbose: whether to print warnings and error messages
26
+ insets: amount to inset the image by, as a percentage of
27
+ (width, height). This lets you "zoom in" to an image
28
+ to remove unimportant content around the borders.
29
+ Useful for face detection, which may not work if the
30
+ faces are too small.
31
+ """
32
+ self.verbose = verbose
33
+ self.insets = insets
34
+
35
+ def read_frames(self, path, num_frames, jitter=0, seed=None):
36
+ """Reads frames that are always evenly spaced throughout the video.
37
+
38
+ Arguments:
39
+ path: the video file
40
+ num_frames: how many frames to read, -1 means the entire video
41
+ (warning: this will take up a lot of memory!)
42
+ jitter: if not 0, adds small random offsets to the frame indices;
43
+ this is useful so we don't always land on even or odd frames
44
+ seed: random seed for jittering; if you set this to a fixed value,
45
+ you probably want to set it only on the first video
46
+ """
47
+ assert num_frames > 0
48
+
49
+ capture = cv2.VideoCapture(path)
50
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
51
+ if frame_count <= 0: return None
52
+
53
+ frame_idxs = np.linspace(0, frame_count - 1, num_frames, endpoint=True, dtype=int)
54
+ if jitter > 0:
55
+ np.random.seed(seed)
56
+ jitter_offsets = np.random.randint(-jitter, jitter, len(frame_idxs))
57
+ frame_idxs = np.clip(frame_idxs + jitter_offsets, 0, frame_count - 1)
58
+
59
+ result = self._read_frames_at_indices(path, capture, frame_idxs)
60
+ capture.release()
61
+ return result
62
+
63
+ def read_random_frames(self, path, num_frames, seed=None):
64
+ """Picks the frame indices at random.
65
+
66
+ Arguments:
67
+ path: the video file
68
+ num_frames: how many frames to read, -1 means the entire video
69
+ (warning: this will take up a lot of memory!)
70
+ """
71
+ assert num_frames > 0
72
+ np.random.seed(seed)
73
+
74
+ capture = cv2.VideoCapture(path)
75
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
76
+ if frame_count <= 0: return None
77
+
78
+ frame_idxs = sorted(np.random.choice(np.arange(0, frame_count), num_frames))
79
+ result = self._read_frames_at_indices(path, capture, frame_idxs)
80
+
81
+ capture.release()
82
+ return result
83
+
84
+ def read_frames_at_indices(self, path, frame_idxs):
85
+ """Reads frames from a video and puts them into a NumPy array.
86
+
87
+ Arguments:
88
+ path: the video file
89
+ frame_idxs: a list of frame indices. Important: should be
90
+ sorted from low-to-high! If an index appears multiple
91
+ times, the frame is still read only once.
92
+
93
+ Returns:
94
+ - a NumPy array of shape (num_frames, height, width, 3)
95
+ - a list of the frame indices that were read
96
+
97
+ Reading stops if loading a frame fails, in which case the first
98
+ dimension returned may actually be less than num_frames.
99
+
100
+ Returns None if an exception is thrown for any reason, or if no
101
+ frames were read.
102
+ """
103
+ assert len(frame_idxs) > 0
104
+ capture = cv2.VideoCapture(path)
105
+ result = self._read_frames_at_indices(path, capture, frame_idxs)
106
+ capture.release()
107
+ return result
108
+
109
+ def _read_frames_at_indices(self, path, capture, frame_idxs):
110
+ try:
111
+ frames = []
112
+ idxs_read = []
113
+ for frame_idx in range(frame_idxs[0], frame_idxs[-1] + 1):
114
+ # Get the next frame, but don't decode if we're not using it.
115
+ ret = capture.grab()
116
+ if not ret:
117
+ if self.verbose:
118
+ print("Error grabbing frame %d from movie %s" % (frame_idx, path))
119
+ break
120
+
121
+ # Need to look at this frame?
122
+ current = len(idxs_read)
123
+ if frame_idx == frame_idxs[current]:
124
+ ret, frame = capture.retrieve()
125
+ if not ret or frame is None:
126
+ if self.verbose:
127
+ print("Error retrieving frame %d from movie %s" % (frame_idx, path))
128
+ break
129
+
130
+ frame = self._postprocess_frame(frame)
131
+ frames.append(frame)
132
+ idxs_read.append(frame_idx)
133
+
134
+ if len(frames) > 0:
135
+ return np.stack(frames), idxs_read
136
+ if self.verbose:
137
+ print("No frames read from movie %s" % path)
138
+ return None
139
+ except:
140
+ if self.verbose:
141
+ print("Exception while reading movie %s" % path)
142
+ return None
143
+
144
+ def read_middle_frame(self, path):
145
+ """Reads the frame from the middle of the video."""
146
+ capture = cv2.VideoCapture(path)
147
+ frame_count = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
148
+ result = self._read_frame_at_index(path, capture, frame_count // 2)
149
+ capture.release()
150
+ return result
151
+
152
+ def read_frame_at_index(self, path, frame_idx):
153
+ """Reads a single frame from a video.
154
+
155
+ If you just want to read a single frame from the video, this is more
156
+ efficient than scanning through the video to find the frame. However,
157
+ for reading multiple frames it's not efficient.
158
+
159
+ My guess is that a "streaming" approach is more efficient than a
160
+ "random access" approach because, unless you happen to grab a keyframe,
161
+ the decoder still needs to read all the previous frames in order to
162
+ reconstruct the one you're asking for.
163
+
164
+ Returns a NumPy array of shape (1, H, W, 3) and the index of the frame,
165
+ or None if reading failed.
166
+ """
167
+ capture = cv2.VideoCapture(path)
168
+ result = self._read_frame_at_index(path, capture, frame_idx)
169
+ capture.release()
170
+ return result
171
+
172
+ def _read_frame_at_index(self, path, capture, frame_idx):
173
+ capture.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
174
+ ret, frame = capture.read()
175
+ if not ret or frame is None:
176
+ if self.verbose:
177
+ print("Error retrieving frame %d from movie %s" % (frame_idx, path))
178
+ return None
179
+ else:
180
+ frame = self._postprocess_frame(frame)
181
+ return np.expand_dims(frame, axis=0), [frame_idx]
182
+
183
+ def _postprocess_frame(self, frame):
184
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
185
+
186
+ if self.insets[0] > 0:
187
+ W = frame.shape[1]
188
+ p = int(W * self.insets[0])
189
+ frame = frame[:, p:-p, :]
190
+
191
+ if self.insets[1] > 0:
192
+ H = frame.shape[1]
193
+ q = int(H * self.insets[1])
194
+ frame = frame[q:-q, :, :]
195
+
196
+ return frame
197
+
198
+
199
+ class FaceExtractor:
200
+ def __init__(self, video_read_fn):
201
+ self.video_read_fn = video_read_fn
202
+ self.detector = MTCNN(margin=0, thresholds=[0.7, 0.8, 0.8], device="cuda")
203
+
204
+ def process_videos(self, input_dir, filenames, video_idxs):
205
+ videos_read = []
206
+ frames_read = []
207
+ frames = []
208
+ results = []
209
+ for video_idx in video_idxs:
210
+ # Read the full-size frames from this video.
211
+ filename = filenames[video_idx]
212
+ video_path = os.path.join(input_dir, filename)
213
+ result = self.video_read_fn(video_path)
214
+ # Error? Then skip this video.
215
+ if result is None: continue
216
+
217
+ videos_read.append(video_idx)
218
+
219
+ # Keep track of the original frames (need them later).
220
+ my_frames, my_idxs = result
221
+
222
+ frames.append(my_frames)
223
+ frames_read.append(my_idxs)
224
+ for i, frame in enumerate(my_frames):
225
+ h, w = frame.shape[:2]
226
+ img = Image.fromarray(frame.astype(np.uint8))
227
+ img = img.resize(size=[s // 2 for s in img.size])
228
+
229
+ batch_boxes, probs = self.detector.detect(img, landmarks=False)
230
+
231
+ faces = []
232
+ scores = []
233
+ if batch_boxes is None:
234
+ continue
235
+ for bbox, score in zip(batch_boxes, probs):
236
+ if bbox is not None:
237
+ xmin, ymin, xmax, ymax = [int(b * 2) for b in bbox]
238
+ w = xmax - xmin
239
+ h = ymax - ymin
240
+ p_h = h // 3
241
+ p_w = w // 3
242
+ crop = frame[max(ymin - p_h, 0):ymax + p_h, max(xmin - p_w, 0):xmax + p_w]
243
+ faces.append(crop)
244
+ scores.append(score)
245
+
246
+ frame_dict = {"video_idx": video_idx,
247
+ "frame_idx": my_idxs[i],
248
+ "frame_w": w,
249
+ "frame_h": h,
250
+ "faces": faces,
251
+ "scores": scores}
252
+ results.append(frame_dict)
253
+
254
+ return results
255
+
256
+ def process_video(self, video_path):
257
+ """Convenience method for doing face extraction on a single video."""
258
+ input_dir = os.path.dirname(video_path)
259
+ filenames = [os.path.basename(video_path)]
260
+ return self.process_videos(input_dir, filenames, [0])
261
+
262
+
263
+
264
+ def confident_strategy(pred, t=0.8):
265
+ pred = np.array(pred)
266
+ sz = len(pred)
267
+ fakes = np.count_nonzero(pred > t)
268
+ # 11 frames are detected as fakes with high probability
269
+ if fakes > sz // 2.5 and fakes > 11:
270
+ return np.mean(pred[pred > t])
271
+ elif np.count_nonzero(pred < 0.2) > 0.9 * sz:
272
+ return np.mean(pred[pred < 0.2])
273
+ else:
274
+ return np.mean(pred)
275
+
276
+ strategy = confident_strategy
277
+
278
+
279
+ def put_to_center(img, input_size):
280
+ img = img[:input_size, :input_size]
281
+ image = np.zeros((input_size, input_size, 3), dtype=np.uint8)
282
+ start_w = (input_size - img.shape[1]) // 2
283
+ start_h = (input_size - img.shape[0]) // 2
284
+ image[start_h:start_h + img.shape[0], start_w: start_w + img.shape[1], :] = img
285
+ return image
286
+
287
+
288
+ def isotropically_resize_image(img, size, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC):
289
+ h, w = img.shape[:2]
290
+ if max(w, h) == size:
291
+ return img
292
+ if w > h:
293
+ scale = size / w
294
+ h = h * scale
295
+ w = size
296
+ else:
297
+ scale = size / h
298
+ w = w * scale
299
+ h = size
300
+ interpolation = interpolation_up if scale > 1 else interpolation_down
301
+ resized = cv2.resize(img, (int(w), int(h)), interpolation=interpolation)
302
+ return resized
303
+
304
+
305
+ def predict_on_video(face_extractor, video_path, batch_size, input_size, models, strategy=np.mean,
306
+ apply_compression=False):
307
+ batch_size *= 4
308
+ try:
309
+ faces = face_extractor.process_video(video_path)
310
+ if len(faces) > 0:
311
+ x = np.zeros((batch_size, input_size, input_size, 3), dtype=np.uint8)
312
+ n = 0
313
+ for frame_data in faces:
314
+ for face in frame_data["faces"]:
315
+ resized_face = isotropically_resize_image(face, input_size)
316
+ resized_face = put_to_center(resized_face, input_size)
317
+ if apply_compression:
318
+ resized_face = image_compression(resized_face, quality=90, image_type=".jpg")
319
+ if n + 1 < batch_size:
320
+ x[n] = resized_face
321
+ n += 1
322
+ else:
323
+ pass
324
+ if n > 0:
325
+ x = torch.tensor(x, device="cuda").float()
326
+ # Preprocess the images.
327
+ x = x.permute((0, 3, 1, 2))
328
+ for i in range(len(x)):
329
+ x[i] = normalize_transform(x[i] / 255.)
330
+ # Make a prediction, then take the average.
331
+ with torch.no_grad():
332
+ preds = []
333
+ for model in models:
334
+ y_pred = model(x[:n].half())
335
+ y_pred = torch.sigmoid(y_pred.squeeze())
336
+ bpred = y_pred[:n].cpu().numpy()
337
+ preds.append(strategy(bpred))
338
+ return np.mean(preds)
339
+ except Exception as e:
340
+ print("Prediction error on video %s: %s" % (video_path, str(e)))
341
+
342
+ return 0.5
343
+
344
+
345
+ def predict_on_video_set(face_extractor, videos, input_size, num_workers, test_dir, frames_per_video, models,
346
+ strategy=np.mean,
347
+ apply_compression=False):
348
+ def process_file(i):
349
+ filename = videos[i]
350
+ y_pred = predict_on_video(face_extractor=face_extractor, video_path=os.path.join(test_dir, filename),
351
+ input_size=input_size,
352
+ batch_size=frames_per_video,
353
+ models=models, strategy=strategy, apply_compression=apply_compression)
354
+ return y_pred
355
+
356
+ with ThreadPoolExecutor(max_workers=num_workers) as ex:
357
+ predictions = ex.map(process_file, range(len(videos)))
358
+ return list(predictions)
359
+
backend/dfdc_model/preprocessing/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from .face_detector import *
backend/dfdc_model/preprocessing/face_detector.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ["MKL_NUM_THREADS"] = "1"
3
+ os.environ["NUMEXPR_NUM_THREADS"] = "1"
4
+ os.environ["OMP_NUM_THREADS"] = "1"
5
+
6
+ from abc import ABC, abstractmethod
7
+ from collections import OrderedDict
8
+ from typing import List
9
+
10
+ import cv2
11
+ cv2.ocl.setUseOpenCL(False)
12
+ cv2.setNumThreads(0)
13
+
14
+ from PIL import Image
15
+ from facenet_pytorch.models.mtcnn import MTCNN
16
+ from torch.utils.data import Dataset
17
+
18
+
19
+ class VideoFaceDetector(ABC):
20
+
21
+ def __init__(self, **kwargs) -> None:
22
+ super().__init__()
23
+
24
+ @property
25
+ @abstractmethod
26
+ def _batch_size(self) -> int:
27
+ pass
28
+
29
+ @abstractmethod
30
+ def _detect_faces(self, frames) -> List:
31
+ pass
32
+
33
+
34
+ class FacenetDetector(VideoFaceDetector):
35
+
36
+ def __init__(self, device="cuda:0") -> None:
37
+ super().__init__()
38
+ self.detector = MTCNN(margin=0,thresholds=[0.85, 0.95, 0.95], device=device)
39
+
40
+ def _detect_faces(self, frames) -> List:
41
+ batch_boxes, *_ = self.detector.detect(frames, landmarks=False)
42
+ return [b.tolist() if b is not None else None for b in batch_boxes]
43
+
44
+ @property
45
+ def _batch_size(self):
46
+ return 32
47
+
48
+
49
+ class VideoDataset(Dataset):
50
+
51
+ def __init__(self, videos) -> None:
52
+ super().__init__()
53
+ self.videos = videos
54
+
55
+ def __getitem__(self, index: int):
56
+ video = self.videos[index]
57
+ capture = cv2.VideoCapture(video)
58
+ frames_num = int(capture.get(cv2.CAP_PROP_FRAME_COUNT))
59
+ frames = OrderedDict()
60
+ for i in range(frames_num):
61
+ capture.grab()
62
+ success, frame = capture.retrieve()
63
+ if not success:
64
+ continue
65
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
66
+ frame = Image.fromarray(frame)
67
+ frame = frame.resize(size=[s // 2 for s in frame.size])
68
+ frames[i] = frame
69
+ return video, list(frames.keys()), list(frames.values())
70
+
71
+ def __len__(self) -> int:
72
+ return len(self.videos)
backend/dfdc_model/preprocessing/utils.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from glob import glob
4
+ from pathlib import Path
5
+
6
+
7
+ def get_original_video_paths(root_dir, basename=False):
8
+ originals = set()
9
+ originals_v = set()
10
+ for json_path in glob(os.path.join(root_dir, "*/metadata.json")):
11
+ dir = Path(json_path).parent
12
+ with open(json_path, "r") as f:
13
+ metadata = json.load(f)
14
+ for k, v in metadata.items():
15
+ original = v.get("original", None)
16
+ if v["label"] == "REAL":
17
+ original = k
18
+ originals_v.add(original)
19
+ originals.add(os.path.join(dir, original))
20
+ originals = list(originals)
21
+ originals_v = list(originals_v)
22
+ print(len(originals))
23
+ return originals_v if basename else originals
24
+
25
+
26
+ def get_original_with_fakes(root_dir):
27
+ pairs = []
28
+ for json_path in glob(os.path.join(root_dir, "*/metadata.json")):
29
+ with open(json_path, "r") as f:
30
+ metadata = json.load(f)
31
+ for k, v in metadata.items():
32
+ original = v.get("original", None)
33
+ if v["label"] == "FAKE":
34
+ pairs.append((original[:-4], k[:-4] ))
35
+
36
+ return pairs
37
+
38
+
39
+ def get_originals_and_fakes(root_dir):
40
+ originals = []
41
+ fakes = []
42
+ for json_path in glob(os.path.join(root_dir, "*/metadata.json")):
43
+ with open(json_path, "r") as f:
44
+ metadata = json.load(f)
45
+ for k, v in metadata.items():
46
+ if v["label"] == "FAKE":
47
+ fakes.append(k[:-4])
48
+ else:
49
+ originals.append(k[:-4])
50
+
51
+ return originals, fakes
backend/dfdc_model/training/__init__.py ADDED
File without changes
backend/dfdc_model/training/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (164 Bytes). View file
 
backend/dfdc_model/training/transforms/__init__.py ADDED
File without changes
backend/dfdc_model/training/transforms/albu.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+
3
+ import cv2
4
+ import numpy as np
5
+ from albumentations import DualTransform, ImageOnlyTransform
6
+ from albumentations.augmentations.functional import crop
7
+
8
+
9
+ def isotropically_resize_image(img, size, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC):
10
+ h, w = img.shape[:2]
11
+ if max(w, h) == size:
12
+ return img
13
+ if w > h:
14
+ scale = size / w
15
+ h = h * scale
16
+ w = size
17
+ else:
18
+ scale = size / h
19
+ w = w * scale
20
+ h = size
21
+ interpolation = interpolation_up if scale > 1 else interpolation_down
22
+ resized = cv2.resize(img, (int(w), int(h)), interpolation=interpolation)
23
+ return resized
24
+
25
+
26
+ class IsotropicResize(DualTransform):
27
+ def __init__(self, max_side, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC,
28
+ always_apply=False, p=1):
29
+ super(IsotropicResize, self).__init__(always_apply, p)
30
+ self.max_side = max_side
31
+ self.interpolation_down = interpolation_down
32
+ self.interpolation_up = interpolation_up
33
+
34
+ def apply(self, img, interpolation_down=cv2.INTER_AREA, interpolation_up=cv2.INTER_CUBIC, **params):
35
+ return isotropically_resize_image(img, size=self.max_side, interpolation_down=interpolation_down,
36
+ interpolation_up=interpolation_up)
37
+
38
+ def apply_to_mask(self, img, **params):
39
+ return self.apply(img, interpolation_down=cv2.INTER_NEAREST, interpolation_up=cv2.INTER_NEAREST, **params)
40
+
41
+ def get_transform_init_args_names(self):
42
+ return ("max_side", "interpolation_down", "interpolation_up")
43
+
44
+
45
+ class Resize4xAndBack(ImageOnlyTransform):
46
+ def __init__(self, always_apply=False, p=0.5):
47
+ super(Resize4xAndBack, self).__init__(always_apply, p)
48
+
49
+ def apply(self, img, **params):
50
+ h, w = img.shape[:2]
51
+ scale = random.choice([2, 4])
52
+ img = cv2.resize(img, (w // scale, h // scale), interpolation=cv2.INTER_AREA)
53
+ img = cv2.resize(img, (w, h),
54
+ interpolation=random.choice([cv2.INTER_CUBIC, cv2.INTER_LINEAR, cv2.INTER_NEAREST]))
55
+ return img
56
+
57
+
58
+ class RandomSizedCropNonEmptyMaskIfExists(DualTransform):
59
+
60
+ def __init__(self, min_max_height, w2h_ratio=[0.7, 1.3], always_apply=False, p=0.5):
61
+ super(RandomSizedCropNonEmptyMaskIfExists, self).__init__(always_apply, p)
62
+
63
+ self.min_max_height = min_max_height
64
+ self.w2h_ratio = w2h_ratio
65
+
66
+ def apply(self, img, x_min=0, x_max=0, y_min=0, y_max=0, **params):
67
+ cropped = crop(img, x_min, y_min, x_max, y_max)
68
+ return cropped
69
+
70
+ @property
71
+ def targets_as_params(self):
72
+ return ["mask"]
73
+
74
+ def get_params_dependent_on_targets(self, params):
75
+ mask = params["mask"]
76
+ mask_height, mask_width = mask.shape[:2]
77
+ crop_height = int(mask_height * random.uniform(self.min_max_height[0], self.min_max_height[1]))
78
+ w2h_ratio = random.uniform(*self.w2h_ratio)
79
+ crop_width = min(int(crop_height * w2h_ratio), mask_width - 1)
80
+ if mask.sum() == 0:
81
+ x_min = random.randint(0, mask_width - crop_width + 1)
82
+ y_min = random.randint(0, mask_height - crop_height + 1)
83
+ else:
84
+ mask = mask.sum(axis=-1) if mask.ndim == 3 else mask
85
+ non_zero_yx = np.argwhere(mask)
86
+ y, x = random.choice(non_zero_yx)
87
+ x_min = x - random.randint(0, crop_width - 1)
88
+ y_min = y - random.randint(0, crop_height - 1)
89
+ x_min = np.clip(x_min, 0, mask_width - crop_width)
90
+ y_min = np.clip(y_min, 0, mask_height - crop_height)
91
+
92
+ x_max = x_min + crop_height
93
+ y_max = y_min + crop_width
94
+ y_max = min(mask_height, y_max)
95
+ x_max = min(mask_width, x_max)
96
+ return {"x_min": x_min, "x_max": x_max, "y_min": y_min, "y_max": y_max}
97
+
98
+ def get_transform_init_args_names(self):
99
+ return "min_max_height", "height", "width", "w2h_ratio"
backend/dfdc_model/training/zoo/__init__.py ADDED
File without changes
backend/dfdc_model/training/zoo/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (168 Bytes). View file
 
backend/dfdc_model/training/zoo/__pycache__/classifiers.cpython-312.pyc ADDED
Binary file (8.5 kB). View file
 
backend/dfdc_model/training/zoo/classifiers.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import partial
2
+
3
+ import numpy as np
4
+ import torch
5
+ from timm.models.efficientnet import tf_efficientnet_b4_ns, tf_efficientnet_b3_ns, \
6
+ tf_efficientnet_b5_ns, tf_efficientnet_b2_ns, tf_efficientnet_b6_ns, tf_efficientnet_b7_ns
7
+ from torch import nn
8
+ from torch.nn.modules.dropout import Dropout
9
+ from torch.nn.modules.linear import Linear
10
+ from torch.nn.modules.pooling import AdaptiveAvgPool2d
11
+
12
+ encoder_params = {
13
+ "tf_efficientnet_b3_ns": {
14
+ "features": 1536,
15
+ "init_op": partial(tf_efficientnet_b3_ns, pretrained=True, drop_path_rate=0.2)
16
+ },
17
+ "tf_efficientnet_b2_ns": {
18
+ "features": 1408,
19
+ "init_op": partial(tf_efficientnet_b2_ns, pretrained=False, drop_path_rate=0.2)
20
+ },
21
+ "tf_efficientnet_b4_ns": {
22
+ "features": 1792,
23
+ "init_op": partial(tf_efficientnet_b4_ns, pretrained=True, drop_path_rate=0.5)
24
+ },
25
+ "tf_efficientnet_b5_ns": {
26
+ "features": 2048,
27
+ "init_op": partial(tf_efficientnet_b5_ns, pretrained=True, drop_path_rate=0.2)
28
+ },
29
+ "tf_efficientnet_b4_ns_03d": {
30
+ "features": 1792,
31
+ "init_op": partial(tf_efficientnet_b4_ns, pretrained=True, drop_path_rate=0.3)
32
+ },
33
+ "tf_efficientnet_b5_ns_03d": {
34
+ "features": 2048,
35
+ "init_op": partial(tf_efficientnet_b5_ns, pretrained=True, drop_path_rate=0.3)
36
+ },
37
+ "tf_efficientnet_b5_ns_04d": {
38
+ "features": 2048,
39
+ "init_op": partial(tf_efficientnet_b5_ns, pretrained=True, drop_path_rate=0.4)
40
+ },
41
+ "tf_efficientnet_b6_ns": {
42
+ "features": 2304,
43
+ "init_op": partial(tf_efficientnet_b6_ns, pretrained=True, drop_path_rate=0.2)
44
+ },
45
+ "tf_efficientnet_b7_ns": {
46
+ "features": 2560,
47
+ "init_op": partial(tf_efficientnet_b7_ns, pretrained=True, drop_path_rate=0.2)
48
+ },
49
+ "tf_efficientnet_b6_ns_04d": {
50
+ "features": 2304,
51
+ "init_op": partial(tf_efficientnet_b6_ns, pretrained=True, drop_path_rate=0.4)
52
+ },
53
+ }
54
+
55
+
56
+ def setup_srm_weights(input_channels: int = 3) -> torch.Tensor:
57
+ """Creates the SRM kernels for noise analysis."""
58
+ # note: values taken from Zhou et al., "Learning Rich Features for Image Manipulation Detection", CVPR2018
59
+ srm_kernel = torch.from_numpy(np.array([
60
+ [ # srm 1/2 horiz
61
+ [0., 0., 0., 0., 0.], # noqa: E241,E201
62
+ [0., 0., 0., 0., 0.], # noqa: E241,E201
63
+ [0., 1., -2., 1., 0.], # noqa: E241,E201
64
+ [0., 0., 0., 0., 0.], # noqa: E241,E201
65
+ [0., 0., 0., 0., 0.], # noqa: E241,E201
66
+ ], [ # srm 1/4
67
+ [0., 0., 0., 0., 0.], # noqa: E241,E201
68
+ [0., -1., 2., -1., 0.], # noqa: E241,E201
69
+ [0., 2., -4., 2., 0.], # noqa: E241,E201
70
+ [0., -1., 2., -1., 0.], # noqa: E241,E201
71
+ [0., 0., 0., 0., 0.], # noqa: E241,E201
72
+ ], [ # srm 1/12
73
+ [-1., 2., -2., 2., -1.], # noqa: E241,E201
74
+ [2., -6., 8., -6., 2.], # noqa: E241,E201
75
+ [-2., 8., -12., 8., -2.], # noqa: E241,E201
76
+ [2., -6., 8., -6., 2.], # noqa: E241,E201
77
+ [-1., 2., -2., 2., -1.], # noqa: E241,E201
78
+ ]
79
+ ])).float()
80
+ srm_kernel[0] /= 2
81
+ srm_kernel[1] /= 4
82
+ srm_kernel[2] /= 12
83
+ return srm_kernel.view(3, 1, 5, 5).repeat(1, input_channels, 1, 1)
84
+
85
+
86
+ def setup_srm_layer(input_channels: int = 3) -> torch.nn.Module:
87
+ """Creates a SRM convolution layer for noise analysis."""
88
+ weights = setup_srm_weights(input_channels)
89
+ conv = torch.nn.Conv2d(input_channels, out_channels=3, kernel_size=5, stride=1, padding=2, bias=False)
90
+ with torch.no_grad():
91
+ conv.weight = torch.nn.Parameter(weights, requires_grad=False)
92
+ return conv
93
+
94
+
95
+ class DeepFakeClassifierSRM(nn.Module):
96
+ def __init__(self, encoder, dropout_rate=0.5) -> None:
97
+ super().__init__()
98
+ self.encoder = encoder_params[encoder]["init_op"]()
99
+ self.avg_pool = AdaptiveAvgPool2d((1, 1))
100
+ self.srm_conv = setup_srm_layer(3)
101
+ self.dropout = Dropout(dropout_rate)
102
+ self.fc = Linear(encoder_params[encoder]["features"], 1)
103
+
104
+ def forward(self, x):
105
+ noise = self.srm_conv(x)
106
+ x = self.encoder.forward_features(noise)
107
+ x = self.avg_pool(x).flatten(1)
108
+ x = self.dropout(x)
109
+ x = self.fc(x)
110
+ return x
111
+
112
+
113
+ class GlobalWeightedAvgPool2d(nn.Module):
114
+ """
115
+ Global Weighted Average Pooling from paper "Global Weighted Average
116
+ Pooling Bridges Pixel-level Localization and Image-level Classification"
117
+ """
118
+
119
+ def __init__(self, features: int, flatten=False):
120
+ super().__init__()
121
+ self.conv = nn.Conv2d(features, 1, kernel_size=1, bias=True)
122
+ self.flatten = flatten
123
+
124
+ def fscore(self, x):
125
+ m = self.conv(x)
126
+ m = m.sigmoid().exp()
127
+ return m
128
+
129
+ def norm(self, x: torch.Tensor):
130
+ return x / x.sum(dim=[2, 3], keepdim=True)
131
+
132
+ def forward(self, x):
133
+ input_x = x
134
+ x = self.fscore(x)
135
+ x = self.norm(x)
136
+ x = x * input_x
137
+ x = x.sum(dim=[2, 3], keepdim=not self.flatten)
138
+ return x
139
+
140
+
141
+ class DeepFakeClassifier(nn.Module):
142
+ def __init__(self, encoder, dropout_rate=0.0) -> None:
143
+ super().__init__()
144
+ self.encoder = encoder_params[encoder]["init_op"]()
145
+ self.avg_pool = AdaptiveAvgPool2d((1, 1))
146
+ self.dropout = Dropout(dropout_rate)
147
+ self.fc = Linear(encoder_params[encoder]["features"], 1)
148
+
149
+ def forward(self, x):
150
+ x = self.encoder.forward_features(x)
151
+ x = self.avg_pool(x).flatten(1)
152
+ x = self.dropout(x)
153
+ x = self.fc(x)
154
+ return x
155
+
156
+
157
+
158
+
159
+ class DeepFakeClassifierGWAP(nn.Module):
160
+ def __init__(self, encoder, dropout_rate=0.5) -> None:
161
+ super().__init__()
162
+ self.encoder = encoder_params[encoder]["init_op"]()
163
+ self.avg_pool = GlobalWeightedAvgPool2d(encoder_params[encoder]["features"])
164
+ self.dropout = Dropout(dropout_rate)
165
+ self.fc = Linear(encoder_params[encoder]["features"], 1)
166
+
167
+ def forward(self, x):
168
+ x = self.encoder.forward_features(x)
169
+ x = self.avg_pool(x).flatten(1)
170
+ x = self.dropout(x)
171
+ x = self.fc(x)
172
+ return x
backend/dfdc_model/training/zoo/unet.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import partial
2
+
3
+ import torch
4
+ from timm.models.efficientnet import tf_efficientnet_b3_ns, tf_efficientnet_b5_ns
5
+ from torch import nn
6
+ from torch.nn import Dropout2d, Conv2d
7
+ from torch.nn.modules.dropout import Dropout
8
+ from torch.nn.modules.linear import Linear
9
+ from torch.nn.modules.pooling import AdaptiveAvgPool2d
10
+ from torch.nn.modules.upsampling import UpsamplingBilinear2d
11
+
12
+ encoder_params = {
13
+ "tf_efficientnet_b3_ns": {
14
+ "features": 1536,
15
+ "filters": [40, 32, 48, 136, 1536],
16
+ "decoder_filters": [64, 128, 256, 256],
17
+ "init_op": partial(tf_efficientnet_b3_ns, pretrained=True, drop_path_rate=0.2)
18
+ },
19
+ "tf_efficientnet_b5_ns": {
20
+ "features": 2048,
21
+ "filters": [48, 40, 64, 176, 2048],
22
+ "decoder_filters": [64, 128, 256, 256],
23
+ "init_op": partial(tf_efficientnet_b5_ns, pretrained=True, drop_path_rate=0.2)
24
+ },
25
+ }
26
+
27
+
28
+ class DecoderBlock(nn.Module):
29
+ def __init__(self, in_channels, out_channels):
30
+ super().__init__()
31
+ self.layer = nn.Sequential(
32
+ nn.Upsample(scale_factor=2),
33
+ nn.Conv2d(in_channels, out_channels, 3, padding=1),
34
+ nn.ReLU(inplace=True)
35
+ )
36
+
37
+ def forward(self, x):
38
+ return self.layer(x)
39
+
40
+
41
+ class ConcatBottleneck(nn.Module):
42
+ def __init__(self, in_channels, out_channels):
43
+ super().__init__()
44
+ self.seq = nn.Sequential(
45
+ nn.Conv2d(in_channels, out_channels, 3, padding=1),
46
+ nn.ReLU(inplace=True)
47
+ )
48
+
49
+ def forward(self, dec, enc):
50
+ x = torch.cat([dec, enc], dim=1)
51
+ return self.seq(x)
52
+
53
+
54
+ class Decoder(nn.Module):
55
+ def __init__(self, decoder_filters, filters, upsample_filters=None,
56
+ decoder_block=DecoderBlock, bottleneck=ConcatBottleneck, dropout=0):
57
+ super().__init__()
58
+ self.decoder_filters = decoder_filters
59
+ self.filters = filters
60
+ self.decoder_block = decoder_block
61
+ self.decoder_stages = nn.ModuleList([self._get_decoder(idx) for idx in range(0, len(decoder_filters))])
62
+ self.bottlenecks = nn.ModuleList([bottleneck(self.filters[-i - 2] + f, f)
63
+ for i, f in enumerate(reversed(decoder_filters))])
64
+ self.dropout = Dropout2d(dropout) if dropout > 0 else None
65
+ self.last_block = None
66
+ if upsample_filters:
67
+ self.last_block = decoder_block(decoder_filters[0], out_channels=upsample_filters)
68
+ else:
69
+ self.last_block = UpsamplingBilinear2d(scale_factor=2)
70
+
71
+ def forward(self, encoder_results: list):
72
+ x = encoder_results[0]
73
+ bottlenecks = self.bottlenecks
74
+ for idx, bottleneck in enumerate(bottlenecks):
75
+ rev_idx = - (idx + 1)
76
+ x = self.decoder_stages[rev_idx](x)
77
+ x = bottleneck(x, encoder_results[-rev_idx])
78
+ if self.last_block:
79
+ x = self.last_block(x)
80
+ if self.dropout:
81
+ x = self.dropout(x)
82
+ return x
83
+
84
+ def _get_decoder(self, layer):
85
+ idx = layer + 1
86
+ if idx == len(self.decoder_filters):
87
+ in_channels = self.filters[idx]
88
+ else:
89
+ in_channels = self.decoder_filters[idx]
90
+ return self.decoder_block(in_channels, self.decoder_filters[max(layer, 0)])
91
+
92
+
93
+ def _initialize_weights(module):
94
+ for m in module.modules():
95
+ if isinstance(m, nn.Conv2d) or isinstance(m, nn.ConvTranspose2d) or isinstance(m, nn.Linear):
96
+ m.weight.data = nn.init.kaiming_normal_(m.weight.data)
97
+ if m.bias is not None:
98
+ m.bias.data.zero_()
99
+ elif isinstance(m, nn.BatchNorm2d):
100
+ m.weight.data.fill_(1)
101
+ m.bias.data.zero_()
102
+
103
+
104
+ class EfficientUnetClassifier(nn.Module):
105
+ def __init__(self, encoder, dropout_rate=0.5) -> None:
106
+ super().__init__()
107
+ self.decoder = Decoder(decoder_filters=encoder_params[encoder]["decoder_filters"],
108
+ filters=encoder_params[encoder]["filters"])
109
+ self.avg_pool = AdaptiveAvgPool2d((1, 1))
110
+ self.dropout = Dropout(dropout_rate)
111
+ self.fc = Linear(encoder_params[encoder]["features"], 1)
112
+ self.final = Conv2d(encoder_params[encoder]["decoder_filters"][0], out_channels=1, kernel_size=1, bias=False)
113
+ _initialize_weights(self)
114
+ self.encoder = encoder_params[encoder]["init_op"]()
115
+
116
+ def get_encoder_features(self, x):
117
+ encoder_results = []
118
+ x = self.encoder.conv_stem(x)
119
+ x = self.encoder.bn1(x)
120
+ x = self.encoder.act1(x)
121
+ encoder_results.append(x)
122
+ x = self.encoder.blocks[:2](x)
123
+ encoder_results.append(x)
124
+ x = self.encoder.blocks[2:3](x)
125
+ encoder_results.append(x)
126
+ x = self.encoder.blocks[3:5](x)
127
+ encoder_results.append(x)
128
+ x = self.encoder.blocks[5:](x)
129
+ x = self.encoder.conv_head(x)
130
+ x = self.encoder.bn2(x)
131
+ x = self.encoder.act2(x)
132
+ encoder_results.append(x)
133
+ encoder_results = list(reversed(encoder_results))
134
+ return encoder_results
135
+
136
+ def forward(self, x):
137
+ encoder_results = self.get_encoder_features(x)
138
+ seg = self.final(self.decoder(encoder_results))
139
+ x = encoder_results[0]
140
+ x = self.avg_pool(x).flatten(1)
141
+ x = self.dropout(x)
142
+ x = self.fc(x)
143
+ return x, seg
144
+
145
+
146
+ if __name__ == '__main__':
147
+ model = EfficientUnetClassifier("tf_efficientnet_b5_ns")
148
+ model.eval()
149
+ with torch.no_grad():
150
+ input = torch.rand(4, 3, 224, 224)
151
+ print(model(input))
backend/download_models.py ADDED
File without changes
backend/enhanced_prediction.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ from dfdc_model.kernel_utils import isotropically_resize_image, put_to_center, normalize_transform
4
+ from gradcam import GradCAM, create_gradcam_image, image_to_base64
5
+
6
+ def enhanced_predict_on_video(face_extractor, video_path, batch_size, input_size, models, strategy=np.mean,
7
+ apply_compression=False, return_frame_predictions=True):
8
+ """Enhanced video prediction that returns frame-level predictions and Grad-CAM"""
9
+ # Reduce batch size for lower memory usage
10
+ batch_size = max(8, batch_size * 2)
11
+ frame_predictions = []
12
+ fake_frames = []
13
+ gradcam_images = []
14
+
15
+ try:
16
+ faces = face_extractor.process_video(video_path)
17
+ # Sample every Nth frame to reduce memory usage
18
+ frame_sample_rate = 3 # Only process every 3rd frame
19
+ if len(faces) > 0:
20
+ x = np.zeros((batch_size, input_size, input_size, 3), dtype=np.uint8)
21
+ frame_data_list = []
22
+ n = 0
23
+ for idx, frame_data in enumerate(faces):
24
+ if idx % frame_sample_rate != 0:
25
+ continue
26
+ for face in frame_data["faces"]:
27
+ resized_face = isotropically_resize_image(face, input_size)
28
+ resized_face = put_to_center(resized_face, input_size)
29
+ if apply_compression:
30
+ from albumentations.augmentations.functional import image_compression
31
+ resized_face = image_compression(resized_face, quality=90, image_type=".jpg")
32
+ if n + 1 < batch_size:
33
+ x[n] = resized_face
34
+ frame_data_list.append({
35
+ 'frame_idx': frame_data["frame_idx"],
36
+ 'original_face': face,
37
+ 'processed_face': resized_face
38
+ })
39
+ n += 1
40
+ else:
41
+ break
42
+ if n > 0:
43
+ device = "cuda" if torch.cuda.is_available() else "cpu"
44
+ x = torch.tensor(x, device=device).float()
45
+ # Preprocess the images.
46
+ x = x.permute((0, 3, 1, 2))
47
+ for i in range(len(x)):
48
+ x[i] = normalize_transform(x[i] / 255.)
49
+
50
+ # Make predictions for each frame
51
+ all_preds = []
52
+ for model in models:
53
+ if device == "cuda":
54
+ y_pred = model(x[:n].half())
55
+ else:
56
+ y_pred = model(x[:n])
57
+ y_pred = torch.sigmoid(y_pred.squeeze())
58
+ frame_preds = y_pred.detach().cpu().numpy()
59
+ all_preds.append(frame_preds)
60
+
61
+ # Average predictions across models
62
+ avg_frame_preds = np.mean(all_preds, axis=0)
63
+
64
+ # Identify fake frames (threshold > 0.5)
65
+ for i, pred in enumerate(avg_frame_preds):
66
+ if i < len(frame_data_list):
67
+ frame_idx = frame_data_list[i]['frame_idx']
68
+ frame_predictions.append({
69
+ 'frame_idx': frame_idx,
70
+ 'prediction': float(pred)
71
+ })
72
+
73
+ if pred > 0.5: # Fake frame
74
+ fake_frames.append(frame_idx)
75
+
76
+ # Generate Grad-CAM for the most suspicious frame
77
+ if len(fake_frames) > 0 and len(models) > 0:
78
+ # Find the frame with highest fake probability
79
+ max_pred_idx = np.argmax(avg_frame_preds)
80
+ if max_pred_idx < len(frame_data_list):
81
+ try:
82
+ # Get the target layer (last convolutional layer)
83
+ model = models[0]
84
+ target_layer = None
85
+ for name, module in model.named_modules():
86
+ if isinstance(module, torch.nn.Conv2d):
87
+ target_layer = module
88
+
89
+ if target_layer is not None:
90
+ gradcam = GradCAM(model, target_layer)
91
+ input_tensor = x[max_pred_idx:max_pred_idx+1].clone().detach()
92
+ input_tensor.requires_grad = True
93
+ if device == "cuda":
94
+ input_tensor = input_tensor.half()
95
+ heatmap = gradcam.generate_cam(input_tensor)
96
+ # Create visualization
97
+ original_face = frame_data_list[max_pred_idx]['processed_face']
98
+ # Ensure heatmap is detached before converting to numpy
99
+ if isinstance(heatmap, torch.Tensor):
100
+ heatmap = heatmap.detach().cpu().numpy()
101
+ gradcam_image = create_gradcam_image(original_face, heatmap)
102
+ gradcam_base64 = image_to_base64(gradcam_image)
103
+ gradcam_images.append(gradcam_base64)
104
+ except Exception as e:
105
+ print(f"Grad-CAM generation error: {e}")
106
+
107
+ # Return overall prediction using strategy
108
+ overall_prediction = strategy(avg_frame_preds)
109
+
110
+ return {
111
+ 'prediction': overall_prediction,
112
+ 'frame_predictions': frame_predictions,
113
+ 'fake_frames': fake_frames,
114
+ 'gradcam_images': gradcam_images
115
+ }
116
+
117
+ except Exception as e:
118
+ print("Enhanced prediction error on video %s: %s" % (video_path, str(e)))
119
+
120
+ return {
121
+ 'prediction': 0.5,
122
+ 'frame_predictions': [],
123
+ 'fake_frames': [],
124
+ 'gradcam_images': []
125
+ }
126
+
127
+ def enhanced_predict_on_image(image_path, models, input_size=380):
128
+ """Enhanced image prediction with Grad-CAM"""
129
+ try:
130
+ from dfdc_model.kernel_utils import isotropically_resize_image, put_to_center, normalize_transform
131
+ from facenet_pytorch.models.mtcnn import MTCNN
132
+ import cv2
133
+
134
+ device = "cuda" if torch.cuda.is_available() else "cpu"
135
+ detector = MTCNN(margin=0, thresholds=[0.7, 0.8, 0.8], device=device)
136
+
137
+ # Read and process image
138
+ image = cv2.imread(image_path)
139
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
140
+
141
+ # Detect faces
142
+ from PIL import Image as PILImage
143
+ img_pil = PILImage.fromarray(image)
144
+ img_pil = img_pil.resize(size=[s // 2 for s in img_pil.size])
145
+
146
+ batch_boxes, probs = detector.detect(img_pil, landmarks=False)
147
+
148
+ if batch_boxes is None:
149
+ return {
150
+ 'prediction': 0.5,
151
+ 'gradcam_image': None
152
+ }
153
+
154
+ # Process the best face
155
+ best_idx = 0
156
+ if len(probs) > 1:
157
+ best_idx = np.argmax(probs)
158
+
159
+ bbox = batch_boxes[best_idx]
160
+ if bbox is not None:
161
+ xmin, ymin, xmax, ymax = [int(b * 2) for b in bbox]
162
+ w = xmax - xmin
163
+ h = ymax - ymin
164
+ p_h = h // 3
165
+ p_w = w // 3
166
+ crop = image[max(ymin - p_h, 0):ymax + p_h, max(xmin - p_w, 0):xmax + p_w]
167
+
168
+ # Resize and center
169
+ resized_face = isotropically_resize_image(crop, input_size)
170
+ resized_face = put_to_center(resized_face, input_size)
171
+
172
+ # Prepare tensor
173
+ x = torch.tensor(resized_face, device=device).float().unsqueeze(0)
174
+ x = x.permute((0, 3, 1, 2))
175
+ x = normalize_transform(x[0] / 255.).unsqueeze(0)
176
+
177
+ # Predict
178
+ preds = []
179
+ for model in models:
180
+ if device == "cuda":
181
+ y_pred = model(x.half())
182
+ else:
183
+ y_pred = model(x)
184
+ y_pred = torch.sigmoid(y_pred.squeeze())
185
+ preds.append(y_pred.detach().cpu().numpy())
186
+
187
+ prediction = np.mean(preds)
188
+
189
+ # Generate Grad-CAM if fake
190
+ gradcam_image = None
191
+ if prediction > 0.5 and len(models) > 0:
192
+ try:
193
+ model = models[0]
194
+ target_layer = None
195
+ for name, module in model.named_modules():
196
+ if isinstance(module, torch.nn.Conv2d):
197
+ target_layer = module
198
+
199
+ if target_layer is not None:
200
+ gradcam = GradCAM(model, target_layer)
201
+ input_tensor = x.clone().detach()
202
+ input_tensor.requires_grad = True
203
+ if device == "cuda":
204
+ input_tensor = input_tensor.half()
205
+ heatmap = gradcam.generate_cam(input_tensor)
206
+ gradcam_vis = create_gradcam_image(resized_face, heatmap)
207
+ gradcam_image = image_to_base64(gradcam_vis)
208
+ except Exception as e:
209
+ print(f"Grad-CAM generation error: {e}")
210
+
211
+ return {
212
+ 'prediction': prediction,
213
+ 'gradcam_image': gradcam_image
214
+ }
215
+
216
+ return {
217
+ 'prediction': 0.5,
218
+ 'gradcam_image': None
219
+ }
220
+
221
+ except Exception as e:
222
+ print(f"Enhanced image prediction error: {e}")
223
+ return {
224
+ 'prediction': 0.5,
225
+ 'gradcam_image': None
226
+ }
backend/gradcam.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import cv2
4
+ import numpy as np
5
+ from PIL import Image
6
+ import io
7
+ import base64
8
+
9
+ class GradCAM:
10
+ def __init__(self, model, target_layer):
11
+ self.model = model
12
+ self.target_layer = target_layer
13
+ self.gradients = None
14
+ self.activations = None
15
+ self.hook_layers()
16
+
17
+ def hook_layers(self):
18
+ def backward_hook(module, grad_input, grad_output):
19
+ self.gradients = grad_output[0]
20
+
21
+ def forward_hook(module, input, output):
22
+ self.activations = output
23
+
24
+ self.target_layer.register_forward_hook(forward_hook)
25
+ self.target_layer.register_full_backward_hook(backward_hook)
26
+
27
+ def generate_cam(self, input_tensor, class_idx=None):
28
+ # Forward pass
29
+ output = self.model(input_tensor)
30
+ if class_idx is None:
31
+ # For single output models, use index 0
32
+ if len(output.shape) == 2 and output.shape[1] == 1:
33
+ class_idx = 0
34
+ else:
35
+ class_idx = torch.argmax(output, dim=1)
36
+ # Backward pass
37
+ self.model.zero_grad()
38
+ if len(output.shape) == 2 and output.shape[1] == 1:
39
+ # Single output case
40
+ output[0, 0].backward(retain_graph=True)
41
+ else:
42
+ output[0, class_idx].backward(retain_graph=True)
43
+ # Generate CAM
44
+ gradients = self.gradients[0]
45
+ activations = self.activations[0]
46
+ # Pool the gradients across the spatial dimensions
47
+ pooled_gradients = torch.mean(gradients, dim=[1, 2])
48
+ # Weight the channels by corresponding gradients
49
+ for i in range(activations.shape[0]):
50
+ activations[i, :, :] *= pooled_gradients[i]
51
+ # Average the channels of the activations
52
+ heatmap = torch.mean(activations, dim=0)
53
+ # ReLU on top of the heatmap
54
+ heatmap = F.relu(heatmap)
55
+ # Normalize the heatmap more aggressively for better visibility
56
+ heatmap_min = torch.min(heatmap)
57
+ heatmap_max = torch.max(heatmap)
58
+ if heatmap_max > heatmap_min:
59
+ heatmap = (heatmap - heatmap_min) / (heatmap_max - heatmap_min)
60
+ # Apply power function to enhance high values (make them more visible)
61
+ heatmap = torch.pow(heatmap, 0.5) # Square root for better contrast
62
+ else:
63
+ heatmap = torch.zeros_like(heatmap)
64
+ # Detach before converting to numpy
65
+ return heatmap.detach().cpu().numpy()
66
+
67
+ def create_gradcam_image(original_image, heatmap):
68
+ """Create a Grad-CAM visualization overlaid on the original image"""
69
+ # Resize heatmap to match original image size
70
+ if isinstance(original_image, torch.Tensor):
71
+ original_image = original_image.cpu().numpy()
72
+ if original_image.shape[0] == 3: # CHW format
73
+ original_image = np.transpose(original_image, (1, 2, 0))
74
+
75
+ # Validate heatmap
76
+ if heatmap is None or not isinstance(heatmap, np.ndarray) or heatmap.size == 0:
77
+ print("Invalid heatmap for Grad-CAM visualization")
78
+ return original_image.astype(np.uint8)
79
+
80
+ # Ensure heatmap is 2D
81
+ if len(heatmap.shape) > 2:
82
+ heatmap = np.squeeze(heatmap)
83
+
84
+ # Check for NaN or infinite values
85
+ if np.any(np.isnan(heatmap)) or np.any(np.isinf(heatmap)):
86
+ print("Heatmap contains NaN or infinite values")
87
+ return original_image.astype(np.uint8)
88
+
89
+ h, w = original_image.shape[:2]
90
+
91
+ # Ensure heatmap has valid dimensions before resizing
92
+ if heatmap.shape[0] == 0 or heatmap.shape[1] == 0:
93
+ print("Heatmap has zero dimensions, skipping Grad-CAM")
94
+ return original_image.astype(np.uint8)
95
+
96
+ # Convert heatmap to float32 for better OpenCV compatibility
97
+ heatmap_float = heatmap.astype(np.float32)
98
+
99
+ try:
100
+ heatmap_resized = cv2.resize(heatmap_float, (w, h), interpolation=cv2.INTER_LINEAR)
101
+ except cv2.error as e:
102
+ print(f"OpenCV resize error: {e}")
103
+ print(f"Heatmap shape: {heatmap.shape}, Target size: ({w}, {h})")
104
+ print(f"Heatmap dtype: {heatmap.dtype}, min: {np.min(heatmap)}, max: {np.max(heatmap)}")
105
+ # Fallback: use numpy resize
106
+ from scipy import ndimage
107
+ try:
108
+ zoom_factors = (h / heatmap.shape[0], w / heatmap.shape[1])
109
+ heatmap_resized = ndimage.zoom(heatmap, zoom_factors, order=1)
110
+ except:
111
+ print("Fallback resize also failed, returning original image")
112
+ return original_image.astype(np.uint8)
113
+
114
+ # Normalize heatmap to 0-255 with better contrast
115
+ if np.max(heatmap_resized) > 0:
116
+ heatmap_resized = heatmap_resized / np.max(heatmap_resized) # Normalize to 0-1
117
+ # Apply gamma correction for better visibility
118
+ heatmap_resized = np.power(heatmap_resized, 0.7) # Enhance contrast
119
+
120
+ heatmap_resized = np.uint8(255 * heatmap_resized)
121
+
122
+ # Apply colormap (JET gives red for high values)
123
+ heatmap_colored = cv2.applyColorMap(heatmap_resized, cv2.COLORMAP_JET)
124
+
125
+ # Convert original image to uint8 if needed
126
+ if original_image.max() <= 1.0:
127
+ original_image = (original_image * 255).astype(np.uint8)
128
+
129
+ # Ensure both images are in RGB format
130
+ if len(original_image.shape) == 3:
131
+ original_image = np.uint8(original_image)
132
+ heatmap_colored = cv2.cvtColor(heatmap_colored, cv2.COLOR_BGR2RGB)
133
+
134
+ # Create a mask for significant heatmap values (threshold to only show strong activations)
135
+ mask = heatmap_resized > 50 # Only overlay where heatmap is significant
136
+
137
+ # Create the overlay
138
+ superimposed = original_image.copy()
139
+ superimposed[mask] = (0.3 * original_image[mask] + 0.7 * heatmap_colored[mask]).astype(np.uint8)
140
+ superimposed = np.uint8(superimposed)
141
+
142
+ return superimposed
143
+
144
+ def image_to_base64(image):
145
+ """Convert numpy array image to base64 string"""
146
+ if isinstance(image, np.ndarray):
147
+ pil_image = Image.fromarray(image)
148
+ buffer = io.BytesIO()
149
+ pil_image.save(buffer, format='PNG')
150
+ img_str = base64.b64encode(buffer.getvalue()).decode()
151
+ return img_str
152
+ return None
backend/requirements.txt ADDED
File without changes
backend/simple_server.py ADDED
File without changes