├── .gitignore ├── LICENSE ├── README.md ├── data_list └── Deepfakes_c0_test.txt ├── dataset ├── __init__.py ├── mydataset.py └── transform.py ├── detect_from_video.py ├── download-FaceForensics_v3.py ├── network ├── __init__.py ├── mesonet.py ├── models.py └── xception.py ├── requirements.txt ├── test_CNN.py ├── train_CNN.py └── videos └── 003_000.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deepfake-Detection 2 | ------------------ 3 | The Pytorch implemention of Deepfake Detection based on [Faceforensics++](https://github.com/ondyari/FaceForensics) 4 | 5 | The Backbone net is XceptionNet, and we also reproduced the [MesoNet with pytorch version](https://github.com/HongguLiu/MesoNet-Pytorch), and you can use the mesonet network in this project. 6 | 7 | ## Install & Requirements 8 | The code has been tested on pytorch=1.3.1 and python 3.6, please refer to `requirements.txt` for more details. 9 | ### To install the python packages 10 | `python -m pip install -r requirements.txt` 11 | 12 | Although you can install all dependencies at a time. But it is easy to install dlib via `conda install -c conda-forge dlib` 13 | 14 | 15 | ## Dataset 16 | If you want to use the opensource dataset [Faceforensics++](https://github.com/ondyari/FaceForensics), you can use the script './download-FaceForensics_v3.py' to download the dataset accroding the instructions of [download section](https://github.com/ondyari/FaceForensics/blob/master/dataset/README.md). 17 | 18 | You can train the model with full images, but we suggest you take only face region as input. 19 | 20 | ## Pretrained Model 21 | The model provided just be used to test the effectiveness of our code. We suggest you train you own models based on your dataset. 22 | 23 | And we will upload models which have better performance as soon as possible. 24 | 25 | we provide some [pretrained model](https://drive.google.com/drive/folders/1GNtk3hLq6sUGZCGx8fFttvyNYH8nrQS8?usp=sharing) based on FaceForensics++ 26 | - FF++\_c23.pth 27 | - FF++\_c40.pth 28 | 29 | ## Usage 30 | **To test with videos** 31 | 32 | `python detect_from_video.py --video_path ./videos/003_000.mp4 --model_path ./pretrained_model/df_c0_best.pkl -o ./output --cuda` 33 | 34 | **To test with images** 35 | 36 | `python test_CNN.py -bz 32 --test_list ./data_list/Deepfakes_c0_299.txt --model_path ./pretrained_model/df_c0_best.pkl` 37 | 38 | **To train a model** 39 | 40 | `python train_CNN.py` 41 | (Please set the arguments after read the code) 42 | 43 | ## About 44 | If our project is helpful to you, we hope you can star and fork it. If there are any questions and suggestions, please feel free to contact us. 45 | 46 | Thanks for your support. 47 | ## License 48 | The provided implementation is strictly for academic purposes only. Should you be interested in using our technology for any commercial use, please feel free to contact us. 49 | -------------------------------------------------------------------------------- /dataset/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HongguLiu/Deepfake-Detection/cecd5ab4b50a482ccf5e64b1057b7cfbb6094fc9/dataset/__init__.py -------------------------------------------------------------------------------- /dataset/mydataset.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Honggu Liu 3 | 4 | """ 5 | 6 | from PIL import Image 7 | from torch.utils.data import Dataset 8 | import os 9 | import random 10 | 11 | 12 | class MyDataset(Dataset): 13 | def __init__(self, txt_path, transform=None, target_transform=None): 14 | fh = open(txt_path, 'r') 15 | imgs = [] 16 | for line in fh: 17 | line = line.rstrip() 18 | words = line.split() 19 | imgs.append((words[0], int(words[1]))) 20 | 21 | self.imgs = imgs 22 | self.transform = transform 23 | self.target_transform = target_transform 24 | 25 | def __getitem__(self, index): 26 | fn, label = self.imgs[index] 27 | img = Image.open(fn).convert('RGB') 28 | 29 | if self.transform is not None: 30 | img = self.transform(img) 31 | 32 | return img, label 33 | 34 | def __len__(self): 35 | return len(self.imgs) 36 | 37 | -------------------------------------------------------------------------------- /dataset/transform.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Andreas Rössler 3 | """ 4 | from torchvision import transforms 5 | 6 | xception_default_data_transforms = { 7 | 'train': transforms.Compose([ 8 | transforms.Resize((299, 299)), 9 | transforms.ToTensor(), 10 | transforms.Normalize([0.5]*3, [0.5]*3) 11 | ]), 12 | 'val': transforms.Compose([ 13 | transforms.Resize((299, 299)), 14 | transforms.ToTensor(), 15 | transforms.Normalize([0.5] * 3, [0.5] * 3) 16 | ]), 17 | 'test': transforms.Compose([ 18 | transforms.Resize((299, 299)), 19 | transforms.ToTensor(), 20 | transforms.Normalize([0.5] * 3, [0.5] * 3) 21 | ]), 22 | } 23 | xception_default_data_transforms_256 = { 24 | 'train': transforms.Compose([ 25 | transforms.Resize((256, 256)), 26 | transforms.ToTensor(), 27 | transforms.Normalize([0.5]*3, [0.5]*3) 28 | ]), 29 | 'val': transforms.Resize((256, 256)), 30 | 'test': transforms.Compose([ 31 | transforms.Resize((256, 256)), 32 | transforms.ToTensor(), 33 | transforms.Normalize([0.5] * 3, [0.5] * 3) 34 | ]), 35 | } 36 | transforms_224 = { 37 | 'train': transforms.Compose([ 38 | transforms.Resize((224, 224)), 39 | transforms.ToTensor(), 40 | transforms.Normalize([0.5]*3, [0.5]*3) 41 | ]), 42 | 'val': transforms.Compose([ 43 | transforms.Resize((224, 224)), 44 | transforms.ToTensor(), 45 | transforms.Normalize([0.5]*3, [0.5]*3) 46 | ]), 47 | 'test': transforms.Compose([ 48 | transforms.Resize((224, 224)), 49 | transforms.ToTensor(), 50 | transforms.Normalize([0.5] * 3, [0.5] * 3) 51 | ]), 52 | } -------------------------------------------------------------------------------- /detect_from_video.py: -------------------------------------------------------------------------------- 1 | """ 2 | Evaluates a folder of video files or a single file with a xception binary 3 | classification network. 4 | 5 | Usage: 6 | python detect_from_video.py 7 | -i 8 | -m 9 | -o 10 | 11 | Author: Andreas Rössler 12 | """ 13 | import os 14 | import argparse 15 | from os.path import join 16 | import cv2 17 | import dlib 18 | import torch 19 | import torch.nn as nn 20 | from PIL import Image as pil_image 21 | from tqdm import tqdm 22 | 23 | from network.models import model_selection 24 | from dataset.transform import xception_default_data_transforms 25 | 26 | 27 | def get_boundingbox(face, width, height, scale=1.3, minsize=None): 28 | """ 29 | Expects a dlib face to generate a quadratic bounding box. 30 | :param face: dlib face class 31 | :param width: frame width 32 | :param height: frame height 33 | :param scale: bounding box size multiplier to get a bigger face region 34 | :param minsize: set minimum bounding box size 35 | :return: x, y, bounding_box_size in opencv form 36 | """ 37 | x1 = face.left() 38 | y1 = face.top() 39 | x2 = face.right() 40 | y2 = face.bottom() 41 | size_bb = int(max(x2 - x1, y2 - y1) * scale) 42 | if minsize: 43 | if size_bb < minsize: 44 | size_bb = minsize 45 | center_x, center_y = (x1 + x2) // 2, (y1 + y2) // 2 46 | 47 | # Check for out of bounds, x-y top left corner 48 | x1 = max(int(center_x - size_bb // 2), 0) 49 | y1 = max(int(center_y - size_bb // 2), 0) 50 | # Check for too big bb size for given x, y 51 | size_bb = min(width - x1, size_bb) 52 | size_bb = min(height - y1, size_bb) 53 | 54 | return x1, y1, size_bb 55 | 56 | 57 | def preprocess_image(image, cuda=True): 58 | """ 59 | Preprocesses the image such that it can be fed into our network. 60 | During this process we envoke PIL to cast it into a PIL image. 61 | 62 | :param image: numpy image in opencv form (i.e., BGR and of shape 63 | :return: pytorch tensor of shape [1, 3, image_size, image_size], not 64 | necessarily casted to cuda 65 | """ 66 | # Revert from BGR 67 | image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 68 | # Preprocess using the preprocessing function used during training and 69 | # casting it to PIL image 70 | preprocess = xception_default_data_transforms['test'] 71 | preprocessed_image = preprocess(pil_image.fromarray(image)) 72 | # Add first dimension as the network expects a batch 73 | preprocessed_image = preprocessed_image.unsqueeze(0) 74 | if cuda: 75 | preprocessed_image = preprocessed_image.cuda() 76 | return preprocessed_image 77 | 78 | 79 | def predict_with_model(image, model, post_function=nn.Softmax(dim=1), 80 | cuda=True): 81 | """ 82 | Predicts the label of an input image. Preprocesses the input image and 83 | casts it to cuda if required 84 | 85 | :param image: numpy image 86 | :param model: torch model with linear layer at the end 87 | :param post_function: e.g., softmax 88 | :param cuda: enables cuda, must be the same parameter as the model 89 | :return: prediction (1 = fake, 0 = real) 90 | """ 91 | # Preprocess 92 | preprocessed_image = preprocess_image(image, cuda) 93 | 94 | # Model prediction 95 | output = model(preprocessed_image) 96 | output = post_function(output) 97 | 98 | # Cast to desired 99 | _, prediction = torch.max(output, 1) # argmax 100 | prediction = float(prediction.cpu().numpy()) 101 | 102 | return int(prediction), output 103 | 104 | 105 | def test_full_image_network(video_path, model_path, output_path, 106 | start_frame=0, end_frame=None, cuda=True): 107 | """ 108 | Reads a video and evaluates a subset of frames with the a detection network 109 | that takes in a full frame. Outputs are only given if a face is present 110 | and the face is highlighted using dlib. 111 | :param video_path: path to video file 112 | :param model_path: path to model file (should expect the full sized image) 113 | :param output_path: path where the output video is stored 114 | :param start_frame: first frame to evaluate 115 | :param end_frame: last frame to evaluate 116 | :param cuda: enable cuda 117 | :return: 118 | """ 119 | print('Starting: {}'.format(video_path)) 120 | 121 | # Read and write 122 | reader = cv2.VideoCapture(video_path) 123 | 124 | video_fn = video_path.split('/')[-1].split('.')[0]+'.avi' 125 | os.makedirs(output_path, exist_ok=True) 126 | fourcc = cv2.VideoWriter_fourcc(*'MJPG') 127 | fps = reader.get(cv2.CAP_PROP_FPS) 128 | num_frames = int(reader.get(cv2.CAP_PROP_FRAME_COUNT)) 129 | writer = None 130 | 131 | # Face detector 132 | face_detector = dlib.get_frontal_face_detector() 133 | 134 | # Load model 135 | model = model_selection(modelname='xception', num_out_classes=2, dropout=0.5) 136 | model.load_state_dict(torch.load(model_path)) 137 | if isinstance(model, torch.nn.DataParallel): 138 | model = model.module 139 | if cuda: 140 | model = model.cuda() 141 | 142 | # Text variables 143 | font_face = cv2.FONT_HERSHEY_SIMPLEX 144 | thickness = 2 145 | font_scale = 1 146 | 147 | # Frame numbers and length of output video 148 | frame_num = 0 149 | assert start_frame < num_frames - 1 150 | end_frame = end_frame if end_frame else num_frames 151 | pbar = tqdm(total=end_frame-start_frame) 152 | 153 | while reader.isOpened(): 154 | _, image = reader.read() 155 | if image is None: 156 | break 157 | frame_num += 1 158 | 159 | if frame_num < start_frame: 160 | continue 161 | pbar.update(1) 162 | 163 | # Image size 164 | height, width = image.shape[:2] 165 | 166 | # Init output writer 167 | if writer is None: 168 | writer = cv2.VideoWriter(join(output_path, video_fn), fourcc, fps, 169 | (height, width)[::-1]) 170 | 171 | # 2. Detect with dlib 172 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 173 | faces = face_detector(gray, 1) 174 | if len(faces): 175 | # For now only take biggest face 176 | face = faces[0] 177 | 178 | # --- Prediction --------------------------------------------------- 179 | # Face crop with dlib and bounding box scale enlargement 180 | x, y, size = get_boundingbox(face, width, height) 181 | cropped_face = image[y:y+size, x:x+size] 182 | 183 | # Actual prediction using our model 184 | prediction, output = predict_with_model(cropped_face, model, 185 | cuda=cuda) 186 | # ------------------------------------------------------------------ 187 | 188 | # Text and bb 189 | x = face.left() 190 | y = face.top() 191 | w = face.right() - x 192 | h = face.bottom() - y 193 | label = 'fake' if prediction == 1 else 'real' 194 | color = (0, 255, 0) if prediction == 0 else (0, 0, 255) 195 | output_list = ['{0:.2f}'.format(float(x)) for x in 196 | output.detach().cpu().numpy()[0]] 197 | cv2.putText(image, str(output_list)+'=>'+label, (x, y+h+30), 198 | font_face, font_scale, 199 | color, thickness, 2) 200 | # draw box over face 201 | cv2.rectangle(image, (x, y), (x + w, y + h), color, 2) 202 | 203 | if frame_num >= end_frame: 204 | break 205 | 206 | # Show 207 | cv2.imshow('test', image) 208 | cv2.waitKey(33) # About 30 fps 209 | writer.write(image) 210 | pbar.close() 211 | if writer is not None: 212 | writer.release() 213 | print('Finished! Output saved under {}'.format(output_path)) 214 | else: 215 | print('Input video file was empty') 216 | 217 | 218 | if __name__ == '__main__': 219 | p = argparse.ArgumentParser( 220 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 221 | p.add_argument('--video_path', '-i', type=str) 222 | p.add_argument('--model_path', '-mi', type=str, default=None) 223 | p.add_argument('--output_path', '-o', type=str, 224 | default='.') 225 | p.add_argument('--start_frame', type=int, default=0) 226 | p.add_argument('--end_frame', type=int, default=None) 227 | p.add_argument('--cuda', action='store_true') 228 | args = p.parse_args() 229 | 230 | video_path = args.video_path 231 | if video_path.endswith('.mp4') or video_path.endswith('.avi'): 232 | test_full_image_network(**vars(args)) 233 | else: 234 | videos = os.listdir(video_path) 235 | for video in videos: 236 | args.video_path = join(video_path, video) 237 | test_full_image_network(**vars(args)) 238 | -------------------------------------------------------------------------------- /download-FaceForensics_v3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Downloads FaceForensics++ public data release v3 3 | Example usage: 4 | see -h or https://github.com/ondyari/FaceForensics 5 | """ 6 | # -*- coding: utf-8 -*- 7 | import argparse 8 | import os 9 | import urllib 10 | import urllib.request 11 | import tempfile 12 | import time 13 | import sys 14 | import json 15 | import random 16 | from tqdm import tqdm 17 | from os.path import join 18 | 19 | # URLs 20 | SERVER_URL = 'http://kaldir.vc.in.tum.de/FaceForensics/' 21 | TOS_URL = SERVER_URL + 'webpage/FaceForensics_TOS.pdf' 22 | BASE_URL = SERVER_URL + 'v3/' 23 | FILELIST_URL = BASE_URL + 'misc/filelist.json' 24 | DEEPFAKES_MODEL_NAMES = ['decoder_A.h5', 'decoder_A.h5.bk', 25 | 'decoder_B.h5', 'decoder_B.h5.bk', 26 | 'encoder.h5', 'encoder.h5.bk'] 27 | DEEPFAKES_MODEL_URL = SERVER_URL + 'v2/manipulated_sequences/Deepfakes/raw/' \ 28 | 'models/' 29 | 30 | # Types 31 | DATASETS = { 32 | 'original_youtube_videos': BASE_URL + 'misc/downloaded_youtube_videos.zip', 33 | 'original_youtube_videos_info': BASE_URL + 'misc/downloaded_youtube_videos_info.zip', 34 | 'original': 'original_sequences', 35 | 'Deepfakes': 'manipulated_sequences/Deepfakes', 36 | 'Face2Face': 'manipulated_sequences/Face2Face', 37 | 'FaceSwap': 'manipulated_sequences/FaceSwap', 38 | 'NeuralTextures': 'manipulated_sequences/NeuralTextures' 39 | } 40 | ALL_DATASETS = {'original', 'Deepfakes', 'Face2Face', 'FaceSwap', 'NeuralTextures'} 41 | COMPRESSION = ['c0', 'c23', 'c40'] 42 | TYPE = ['videos', 'masks', 'models'] 43 | 44 | 45 | def download_files(filenames, base_url, output_path, report_progress=True): 46 | os.makedirs(output_path, exist_ok=True) 47 | if report_progress: 48 | filenames = tqdm(filenames) 49 | for filename in filenames: 50 | download_file(base_url + filename, join(output_path, filename)) 51 | 52 | 53 | def reporthook(count, block_size, total_size): 54 | global start_time 55 | if count == 0: 56 | start_time = time.time() 57 | return 58 | duration = time.time() - start_time 59 | progress_size = int(count * block_size) 60 | speed = int(progress_size / (1024 * duration)) 61 | percent = int(count * block_size * 100 / total_size) 62 | sys.stdout.write("\rProgress: %d%%, %d MB, %d KB/s, %d seconds passed" % 63 | (percent, progress_size / (1024 * 1024), speed, duration)) 64 | sys.stdout.flush() 65 | 66 | 67 | def download_file(url, out_file, report_progress=False): 68 | out_dir = os.path.dirname(out_file) 69 | if not os.path.isfile(out_file): 70 | fh, out_file_tmp = tempfile.mkstemp(dir=out_dir) 71 | f = os.fdopen(fh, 'w') 72 | f.close() 73 | if report_progress: 74 | urllib.request.urlretrieve(url, out_file_tmp, 75 | reporthook=reporthook) 76 | else: 77 | urllib.request.urlretrieve(url, out_file_tmp) 78 | os.rename(out_file_tmp, out_file) 79 | else: 80 | tqdm.write('WARNING: skipping download of existing file ' + out_file) 81 | 82 | 83 | def main(): 84 | parser = argparse.ArgumentParser( 85 | description='Downloads FaceForensics v2 public data release.', 86 | formatter_class=argparse.ArgumentDefaultsHelpFormatter 87 | ) 88 | parser.add_argument('output_path', type=str, help='Output directory.') 89 | parser.add_argument('-d', '--dataset', type=str, default='all', 90 | help='Which dataset to download, either pristine or ' 91 | 'manipulated data or the downloaded youtube ' 92 | 'videos.', 93 | choices=list(DATASETS.keys()) + ['all'] 94 | ) 95 | parser.add_argument('-c', '--compression', type=str, default='raw', 96 | help='Which compression degree. All videos ' 97 | 'have been generated with h264 with a varying ' 98 | 'codec. Raw (c0) videos are lossless compressed.', 99 | choices=COMPRESSION 100 | ) 101 | parser.add_argument('-t', '--type', type=str, default='videos', 102 | help='Which file type, i.e. videos, masks, for our ' 103 | 'manipulation methods, models, for Deepfakes.', 104 | choices=TYPE 105 | ) 106 | args = parser.parse_args() 107 | 108 | # TOS 109 | print('By pressing any key to continue you confirm that you have agreed '\ 110 | 'to the FaceForensics terms of use as described at:') 111 | print(TOS_URL) 112 | print('***') 113 | print('Press any key to continue, or CTRL-C to exit.') 114 | #_ = input('') 115 | 116 | # Extract arguments 117 | c_datasets = [args.dataset] if args.dataset != 'all' else ALL_DATASETS 118 | c_type = args.type 119 | c_compression = args.compression 120 | output_path = args.output_path 121 | 122 | # Check for special dataset cases 123 | for dataset in c_datasets: 124 | dataset_path = DATASETS[dataset] 125 | # Special cases 126 | if 'youtube' in dataset_path: 127 | # Here we download the original youtube videos zip file 128 | print('Downloading original youtube videos.') 129 | if not 'info' in dataset_path: 130 | print('Please be patient, this may take a while (~40gb)') 131 | download_file(dataset_path, 132 | out_file=join(output_path, 133 | 'downloaded_videos.zip'), 134 | report_progress=True) 135 | return 136 | 137 | # Else: regular datasets 138 | print('Downloading {} of dataset "{}"'.format( 139 | c_type, dataset_path 140 | )) 141 | 142 | # Get filelists and video lenghts list from server 143 | if 'original' in dataset_path: 144 | filelist = ['{:03d}'.format(i) for i in range(1000)] 145 | else: 146 | # Load filelist from server 147 | file_pairs = json.loads(urllib.request.urlopen(FILELIST_URL).read().decode("utf-8")) 148 | # Get filelist 149 | filelist = [] 150 | for pair in file_pairs: 151 | filelist.append('_'.join(pair)) 152 | if c_type != 'models': 153 | filelist.append('_'.join(pair[::-1])) 154 | 155 | # Server and local paths 156 | dataset_videos_url = BASE_URL + '{}/{}/{}/'.format( 157 | dataset_path, c_compression, c_type) 158 | dataset_mask_url = BASE_URL + '{}/{}/videos/'.format(dataset_path, 'masks', 159 | c_type) 160 | 161 | if c_type == 'videos': 162 | dataset_output_path = join(output_path, dataset_path, c_compression, 163 | c_type) 164 | print('Output path: {}'.format(dataset_output_path)) 165 | filelist = [filename + '.mp4' for filename in filelist] 166 | download_files(filelist, dataset_videos_url, dataset_output_path) 167 | elif c_type == 'masks': 168 | dataset_output_path = join(output_path, dataset_path, c_type, 169 | 'videos') 170 | print('Output path: {}'.format(dataset_output_path)) 171 | if dataset == 'original': 172 | if args.dataset != 'all': 173 | print('Only videos available for original data. Aborting.') 174 | return 175 | else: 176 | print('Only videos available for original data. ' 177 | 'Skipping original.\n') 178 | continue 179 | filelist = [filename + '.mp4' for filename in filelist] 180 | download_files(filelist, dataset_mask_url, dataset_output_path) 181 | 182 | # Else: models for deepfakes 183 | else: 184 | if dataset != 'Deepfakes' and c_type == 'models': 185 | print('Models only available for Deepfakes. Aborting') 186 | return 187 | dataset_output_path = join(output_path, dataset_path, c_type) 188 | print('Output path: {}'.format(dataset_output_path)) 189 | print(DEEPFAKES_MODEL_URL) 190 | 191 | # Get Deepfakes models 192 | for folder in tqdm(filelist): 193 | folder_filelist = DEEPFAKES_MODEL_NAMES 194 | 195 | # Folder paths 196 | folder_base_url = DEEPFAKES_MODEL_URL + folder + '/' 197 | folder_dataset_output_path = join(dataset_output_path, 198 | folder) 199 | download_files(folder_filelist, folder_base_url, 200 | folder_dataset_output_path, 201 | report_progress=False) # already done 202 | 203 | 204 | if __name__ == "__main__": 205 | main() -------------------------------------------------------------------------------- /network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HongguLiu/Deepfake-Detection/cecd5ab4b50a482ccf5e64b1057b7cfbb6094fc9/network/__init__.py -------------------------------------------------------------------------------- /network/mesonet.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | 4 | 5 | import torch 6 | import torch.nn as nn 7 | import torch.nn.functional as F 8 | import math 9 | import torchvision 10 | 11 | class Meso4(nn.Module): 12 | """ 13 | Pytorch Implemention of Meso4 14 | Autor: Honggu Liu 15 | Date: July 4, 2019 16 | """ 17 | def __init__(self, num_classes=2): 18 | super(Meso4, self).__init__() 19 | self.num_classes = num_classes 20 | self.conv1 = nn.Conv2d(3, 8, 3, padding=1, bias=False) 21 | self.bn1 = nn.BatchNorm2d(8) 22 | self.relu = nn.ReLU(inplace=True) 23 | self.leakyrelu = nn.LeakyReLU(0.1) 24 | 25 | self.conv2 = nn.Conv2d(8, 8, 5, padding=2, bias=False) 26 | self.bn2 = nn.BatchNorm2d(16) 27 | self.conv3 = nn.Conv2d(8, 16, 5, padding=2, bias=False) 28 | self.conv4 = nn.Conv2d(16, 16, 5, padding=2, bias=False) 29 | self.maxpooling1 = nn.MaxPool2d(kernel_size=(2, 2)) 30 | self.maxpooling2 = nn.MaxPool2d(kernel_size=(4, 4)) 31 | #flatten: x = x.view(x.size(0), -1) 32 | self.dropout = nn.Dropout2d(0.5) 33 | self.fc1 = nn.Linear(16*8*8, 16) 34 | self.fc2 = nn.Linear(16, num_classes) 35 | 36 | def forward(self, input): 37 | x = self.conv1(input) #(8, 256, 256) 38 | x = self.relu(x) 39 | x = self.bn1(x) 40 | x = self.maxpooling1(x) #(8, 128, 128) 41 | 42 | x = self.conv2(x) #(8, 128, 128) 43 | x = self.relu(x) 44 | x = self.bn1(x) 45 | x = self.maxpooling1(x) #(8, 64, 64) 46 | 47 | x = self.conv3(x) #(16, 64, 64) 48 | x = self.relu(x) 49 | x = self.bn2(x) 50 | x = self.maxpooling1(x) #(16, 32, 32) 51 | 52 | x = self.conv4(x) #(16, 32, 32) 53 | x = self.relu(x) 54 | x = self.bn2(x) 55 | x = self.maxpooling2(x) #(16, 8, 8) 56 | 57 | x = x.view(x.size(0), -1) #(Batch, 16*8*8) 58 | x = self.dropout(x) 59 | x = self.fc1(x) #(Batch, 16) 60 | x = self.leakyrelu(x) 61 | x = self.dropout(x) 62 | x = self.fc2(x) 63 | 64 | return x 65 | 66 | 67 | class MesoInception4(nn.Module): 68 | """ 69 | Pytorch Implemention of MesoInception4 70 | Author: Honggu Liu 71 | Date: July 7, 2019 72 | """ 73 | def __init__(self, num_classes=2): 74 | super(MesoInception4, self).__init__() 75 | self.num_classes = num_classes 76 | #InceptionLayer1 77 | self.Incption1_conv1 = nn.Conv2d(3, 1, 1, padding=0, bias=False) 78 | self.Incption1_conv2_1 = nn.Conv2d(3, 4, 1, padding=0, bias=False) 79 | self.Incption1_conv2_2 = nn.Conv2d(4, 4, 3, padding=1, bias=False) 80 | self.Incption1_conv3_1 = nn.Conv2d(3, 4, 1, padding=0, bias=False) 81 | self.Incption1_conv3_2 = nn.Conv2d(4, 4, 3, padding=2, dilation=2, bias=False) 82 | self.Incption1_conv4_1 = nn.Conv2d(3, 2, 1, padding=0, bias=False) 83 | self.Incption1_conv4_2 = nn.Conv2d(2, 2, 3, padding=3, dilation=3, bias=False) 84 | self.Incption1_bn = nn.BatchNorm2d(11) 85 | 86 | 87 | #InceptionLayer2 88 | self.Incption2_conv1 = nn.Conv2d(11, 2, 1, padding=0, bias=False) 89 | self.Incption2_conv2_1 = nn.Conv2d(11, 4, 1, padding=0, bias=False) 90 | self.Incption2_conv2_2 = nn.Conv2d(4, 4, 3, padding=1, bias=False) 91 | self.Incption2_conv3_1 = nn.Conv2d(11, 4, 1, padding=0, bias=False) 92 | self.Incption2_conv3_2 = nn.Conv2d(4, 4, 3, padding=2, dilation=2, bias=False) 93 | self.Incption2_conv4_1 = nn.Conv2d(11, 2, 1, padding=0, bias=False) 94 | self.Incption2_conv4_2 = nn.Conv2d(2, 2, 3, padding=3, dilation=3, bias=False) 95 | self.Incption2_bn = nn.BatchNorm2d(12) 96 | 97 | #Normal Layer 98 | self.conv1 = nn.Conv2d(12, 16, 5, padding=2, bias=False) 99 | self.relu = nn.ReLU(inplace=True) 100 | self.leakyrelu = nn.LeakyReLU(0.1) 101 | self.bn1 = nn.BatchNorm2d(16) 102 | self.maxpooling1 = nn.MaxPool2d(kernel_size=(2, 2)) 103 | 104 | self.conv2 = nn.Conv2d(16, 16, 5, padding=2, bias=False) 105 | self.maxpooling2 = nn.MaxPool2d(kernel_size=(4, 4)) 106 | 107 | self.dropout = nn.Dropout2d(0.5) 108 | self.fc1 = nn.Linear(16*8*8, 16) 109 | self.fc2 = nn.Linear(16, num_classes) 110 | 111 | 112 | #InceptionLayer 113 | def InceptionLayer1(self, input): 114 | x1 = self.Incption1_conv1(input) 115 | x2 = self.Incption1_conv2_1(input) 116 | x2 = self.Incption1_conv2_2(x2) 117 | x3 = self.Incption1_conv3_1(input) 118 | x3 = self.Incption1_conv3_2(x3) 119 | x4 = self.Incption1_conv4_1(input) 120 | x4 = self.Incption1_conv4_2(x4) 121 | y = torch.cat((x1, x2, x3, x4), 1) 122 | y = self.Incption1_bn(y) 123 | y = self.maxpooling1(y) 124 | 125 | return y 126 | 127 | def InceptionLayer2(self, input): 128 | x1 = self.Incption2_conv1(input) 129 | x2 = self.Incption2_conv2_1(input) 130 | x2 = self.Incption2_conv2_2(x2) 131 | x3 = self.Incption2_conv3_1(input) 132 | x3 = self.Incption2_conv3_2(x3) 133 | x4 = self.Incption2_conv4_1(input) 134 | x4 = self.Incption2_conv4_2(x4) 135 | y = torch.cat((x1, x2, x3, x4), 1) 136 | y = self.Incption2_bn(y) 137 | y = self.maxpooling1(y) 138 | 139 | return y 140 | 141 | def forward(self, input): 142 | x = self.InceptionLayer1(input) #(Batch, 11, 128, 128) 143 | x = self.InceptionLayer2(x) #(Batch, 12, 64, 64) 144 | 145 | x = self.conv1(x) #(Batch, 16, 64 ,64) 146 | x = self.relu(x) 147 | x = self.bn1(x) 148 | x = self.maxpooling1(x) #(Batch, 16, 32, 32) 149 | 150 | x = self.conv2(x) #(Batch, 16, 32, 32) 151 | x = self.relu(x) 152 | x = self.bn1(x) 153 | x = self.maxpooling2(x) #(Batch, 16, 8, 8) 154 | 155 | x = x.view(x.size(0), -1) #(Batch, 16*8*8) 156 | x = self.dropout(x) 157 | x = self.fc1(x) #(Batch, 16) 158 | x = self.leakyrelu(x) 159 | x = self.dropout(x) 160 | x = self.fc2(x) 161 | 162 | return x 163 | 164 | -------------------------------------------------------------------------------- /network/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Author: Andreas Rössler 4 | """ 5 | import os 6 | import argparse 7 | 8 | 9 | import torch 10 | #import pretrainedmodels 11 | import torch.nn as nn 12 | import torch.nn.functional as F 13 | from network.xception import xception, xception_concat 14 | import math 15 | import torchvision 16 | 17 | 18 | def return_pytorch04_xception(pretrained=False): 19 | # Raises warning "src not broadcastable to dst" but thats fine 20 | model = xception(pretrained=False) 21 | if pretrained: 22 | # Load model in torch 0.4+ 23 | model.fc = model.last_linear 24 | del model.last_linear 25 | state_dict = torch.load( 26 | '/public/liuhonggu/.torch/models/xception-b5690688.pth') 27 | for name, weights in state_dict.items(): 28 | if 'pointwise' in name: 29 | state_dict[name] = weights.unsqueeze(-1).unsqueeze(-1) 30 | model.load_state_dict(state_dict) 31 | model.last_linear = model.fc 32 | del model.fc 33 | return model 34 | 35 | 36 | class TransferModel(nn.Module): 37 | """ 38 | Simple transfer learning model that takes an imagenet pretrained model with 39 | a fc layer as base model and retrains a new fc layer for num_out_classes 40 | """ 41 | def __init__(self, modelchoice, num_out_classes=2, dropout=0.5): 42 | super(TransferModel, self).__init__() 43 | self.modelchoice = modelchoice 44 | if modelchoice == 'xception': 45 | self.model = return_pytorch04_xception(pretrained=False) 46 | # Replace fc 47 | num_ftrs = self.model.last_linear.in_features 48 | if not dropout: 49 | self.model.last_linear = nn.Linear(num_ftrs, num_out_classes) 50 | else: 51 | print('Using dropout', dropout) 52 | self.model.last_linear = nn.Sequential( 53 | nn.Dropout(p=dropout), 54 | nn.Linear(num_ftrs, num_out_classes) 55 | ) 56 | elif modelchoice == 'xception_concat': 57 | self.model = xception_concat() 58 | num_ftrs = self.model.last_linear.in_features 59 | if not dropout: 60 | self.model.last_linear = nn.Linear(num_ftrs, num_out_classes) 61 | else: 62 | print('Using dropout', dropout) 63 | self.model.last_linear = nn.Sequential( 64 | nn.Dropout(p=dropout), 65 | nn.Linear(num_ftrs, num_out_classes) 66 | ) 67 | elif modelchoice == 'resnet50' or modelchoice == 'resnet18': 68 | if modelchoice == 'resnet50': 69 | self.model = torchvision.models.resnet50(pretrained=True) 70 | if modelchoice == 'resnet18': 71 | self.model = torchvision.models.resnet18(pretrained=True) 72 | # Replace fc 73 | num_ftrs = self.model.fc.in_features 74 | if not dropout: 75 | self.model.fc = nn.Linear(num_ftrs, num_out_classes) 76 | else: 77 | self.model.fc = nn.Sequential( 78 | nn.Dropout(p=dropout), 79 | nn.Linear(num_ftrs, num_out_classes) 80 | ) 81 | else: 82 | raise Exception('Choose valid model, e.g. resnet50') 83 | 84 | def set_trainable_up_to(self, boolean, layername="Conv2d_4a_3x3"): 85 | """ 86 | Freezes all layers below a specific layer and sets the following layers 87 | to true if boolean else only the fully connected final layer 88 | :param boolean: 89 | :param layername: depends on network, for inception e.g. Conv2d_4a_3x3 90 | :return: 91 | """ 92 | # Stage-1: freeze all the layers 93 | if layername is None: 94 | for i, param in self.model.named_parameters(): 95 | param.requires_grad = True 96 | return 97 | else: 98 | for i, param in self.model.named_parameters(): 99 | param.requires_grad = False 100 | if boolean: 101 | # Make all layers following the layername layer trainable 102 | ct = [] 103 | found = False 104 | for name, child in self.model.named_children(): 105 | if layername in ct: 106 | found = True 107 | for params in child.parameters(): 108 | params.requires_grad = True 109 | ct.append(name) 110 | if not found: 111 | raise Exception('Layer not found, cant finetune!'.format( 112 | layername)) 113 | else: 114 | if self.modelchoice == 'xception': 115 | # Make fc trainable 116 | for param in self.model.last_linear.parameters(): 117 | param.requires_grad = True 118 | 119 | else: 120 | # Make fc trainable 121 | for param in self.model.fc.parameters(): 122 | param.requires_grad = True 123 | 124 | def forward(self, x): 125 | x = self.model(x) 126 | return x 127 | 128 | 129 | def model_selection(modelname, num_out_classes, 130 | dropout=None): 131 | """ 132 | :param modelname: 133 | :return: model, image size, pretraining, input_list 134 | """ 135 | if modelname == 'xception': 136 | return TransferModel(modelchoice='xception', 137 | num_out_classes=num_out_classes) 138 | # , 299, \True, ['image'], None 139 | elif modelname == 'resnet18': 140 | return TransferModel(modelchoice='resnet18', dropout=dropout, 141 | num_out_classes=num_out_classes) 142 | # , \224, True, ['image'], None 143 | elif modelname == 'xception_concat': 144 | return TransferModel(modelchoice='xception_concat', 145 | num_out_classes=num_out_classes) 146 | else: 147 | raise NotImplementedError(modelname) 148 | 149 | 150 | if __name__ == '__main__': 151 | model, image_size, *_ = model_selection('xception', num_out_classes=2) 152 | print(model) 153 | model = model.cuda() 154 | from torchsummary import summary 155 | input_s = (3, image_size, image_size) 156 | print(summary(model, input_s)) 157 | -------------------------------------------------------------------------------- /network/xception.py: -------------------------------------------------------------------------------- 1 | """ 2 | Ported to pytorch thanks to [tstandley](https://github.com/tstandley/Xception-PyTorch) 3 | 4 | @author: tstandley 5 | Adapted by cadene 6 | 7 | Creates an Xception Model as defined in: 8 | 9 | Francois Chollet 10 | Xception: Deep Learning with Depthwise Separable Convolutions 11 | https://arxiv.org/pdf/1610.02357.pdf 12 | 13 | This weights ported from the Keras implementation. Achieves the following performance on the validation set: 14 | 15 | Loss:0.9173 Prec@1:78.892 Prec@5:94.292 16 | 17 | REMEMBER to set your image size to 3x299x299 for both test and validation 18 | 19 | normalize = transforms.Normalize(mean=[0.5, 0.5, 0.5], 20 | std=[0.5, 0.5, 0.5]) 21 | 22 | The resize parameter of the validation transform should be 333, and make sure to center crop at 299x299 23 | """ 24 | import math 25 | import torch 26 | import torch.nn as nn 27 | import torch.nn.functional as F 28 | import torch.utils.model_zoo as model_zoo 29 | from torch.nn import init 30 | 31 | pretrained_settings = { 32 | 'xception': { 33 | 'imagenet': { 34 | 'url': 'http://data.lip6.fr/cadene/pretrainedmodels/xception-b5690688.pth', 35 | 'input_space': 'RGB', 36 | 'input_size': [3, 299, 299], 37 | 'input_range': [0, 1], 38 | 'mean': [0.5, 0.5, 0.5], 39 | 'std': [0.5, 0.5, 0.5], 40 | 'num_classes': 1000, 41 | 'scale': 0.8975 # The resize parameter of the validation transform should be 333, and make sure to center crop at 299x299 42 | } 43 | } 44 | } 45 | 46 | 47 | class SeparableConv2d(nn.Module): 48 | def __init__(self,in_channels,out_channels,kernel_size=1,stride=1,padding=0,dilation=1,bias=False): 49 | super(SeparableConv2d,self).__init__() 50 | 51 | self.conv1 = nn.Conv2d(in_channels,in_channels,kernel_size,stride,padding,dilation,groups=in_channels,bias=bias) 52 | self.pointwise = nn.Conv2d(in_channels,out_channels,1,1,0,1,1,bias=bias) 53 | 54 | def forward(self,x): 55 | x = self.conv1(x) 56 | x = self.pointwise(x) 57 | return x 58 | 59 | 60 | class Block(nn.Module): 61 | def __init__(self,in_filters,out_filters,reps,strides=1,start_with_relu=True,grow_first=True): 62 | super(Block, self).__init__() 63 | 64 | if out_filters != in_filters or strides!=1: 65 | self.skip = nn.Conv2d(in_filters,out_filters,1,stride=strides, bias=False) 66 | self.skipbn = nn.BatchNorm2d(out_filters) 67 | else: 68 | self.skip=None 69 | 70 | self.relu = nn.ReLU(inplace=True) 71 | rep=[] 72 | 73 | filters=in_filters 74 | if grow_first: 75 | rep.append(self.relu) 76 | rep.append(SeparableConv2d(in_filters,out_filters,3,stride=1,padding=1,bias=False)) 77 | rep.append(nn.BatchNorm2d(out_filters)) 78 | filters = out_filters 79 | 80 | for i in range(reps-1): 81 | rep.append(self.relu) 82 | rep.append(SeparableConv2d(filters,filters,3,stride=1,padding=1,bias=False)) 83 | rep.append(nn.BatchNorm2d(filters)) 84 | 85 | if not grow_first: 86 | rep.append(self.relu) 87 | rep.append(SeparableConv2d(in_filters,out_filters,3,stride=1,padding=1,bias=False)) 88 | rep.append(nn.BatchNorm2d(out_filters)) 89 | 90 | if not start_with_relu: 91 | rep = rep[1:] 92 | else: 93 | rep[0] = nn.ReLU(inplace=False) 94 | 95 | if strides != 1: 96 | rep.append(nn.MaxPool2d(3,strides,1)) 97 | self.rep = nn.Sequential(*rep) 98 | 99 | def forward(self,inp): 100 | x = self.rep(inp) 101 | 102 | if self.skip is not None: 103 | skip = self.skip(inp) 104 | skip = self.skipbn(skip) 105 | else: 106 | skip = inp 107 | 108 | x+=skip 109 | return x 110 | 111 | 112 | class Xception(nn.Module): 113 | """ 114 | Xception optimized for the ImageNet dataset, as specified in 115 | https://arxiv.org/pdf/1610.02357.pdf 116 | """ 117 | def __init__(self, num_classes=1000): 118 | """ Constructor 119 | Args: 120 | num_classes: number of classes 121 | """ 122 | super(Xception, self).__init__() 123 | self.num_classes = num_classes 124 | 125 | #self.conv1 = nn.Conv2d(15,32,3,2,0,bias=False) 126 | self.conv1 = nn.Conv2d(3,32,3,2,0,bias=False) 127 | self.bn1 = nn.BatchNorm2d(32) 128 | self.relu = nn.ReLU(inplace=True) 129 | 130 | self.conv2 = nn.Conv2d(32,64,3,bias=False) 131 | self.bn2 = nn.BatchNorm2d(64) 132 | #do relu here 133 | 134 | self.block1=Block(64,128,2,2,start_with_relu=False,grow_first=True) 135 | self.block2=Block(128,256,2,2,start_with_relu=True,grow_first=True) 136 | self.block3=Block(256,728,2,2,start_with_relu=True,grow_first=True) 137 | 138 | self.block4=Block(728,728,3,1,start_with_relu=True,grow_first=True) 139 | self.block5=Block(728,728,3,1,start_with_relu=True,grow_first=True) 140 | self.block6=Block(728,728,3,1,start_with_relu=True,grow_first=True) 141 | self.block7=Block(728,728,3,1,start_with_relu=True,grow_first=True) 142 | 143 | self.block8=Block(728,728,3,1,start_with_relu=True,grow_first=True) 144 | self.block9=Block(728,728,3,1,start_with_relu=True,grow_first=True) 145 | self.block10=Block(728,728,3,1,start_with_relu=True,grow_first=True) 146 | self.block11=Block(728,728,3,1,start_with_relu=True,grow_first=True) 147 | 148 | self.block12=Block(728,1024,2,2,start_with_relu=True,grow_first=False) 149 | 150 | self.conv3 = SeparableConv2d(1024,1536,3,1,1) 151 | self.bn3 = nn.BatchNorm2d(1536) 152 | 153 | #do relu here 154 | self.conv4 = SeparableConv2d(1536,2048,3,1,1) 155 | self.bn4 = nn.BatchNorm2d(2048) 156 | 157 | self.fc = nn.Linear(2048, num_classes) 158 | 159 | # #------- init weights -------- 160 | # for m in self.modules(): 161 | # if isinstance(m, nn.Conv2d): 162 | # n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels 163 | # m.weight.data.normal_(0, math.sqrt(2. / n)) 164 | # elif isinstance(m, nn.BatchNorm2d): 165 | # m.weight.data.fill_(1) 166 | # m.bias.data.zero_() 167 | # #----------------------------- 168 | 169 | def features(self, input): 170 | x = self.conv1(input) #(32, 299, 299) 171 | x = self.bn1(x) 172 | x = self.relu(x) 173 | 174 | x = self.conv2(x) #(64, 299, 299) 175 | x = self.bn2(x) 176 | x = self.relu(x) 177 | 178 | x = self.block1(x) 179 | x = self.block2(x) 180 | x = self.block3(x) 181 | x = self.block4(x) 182 | x = self.block5(x) 183 | x = self.block6(x) 184 | x = self.block7(x) 185 | x = self.block8(x) 186 | x = self.block9(x) 187 | x = self.block10(x) 188 | x = self.block11(x) 189 | x = self.block12(x) #(1024, 299, 299) 190 | 191 | x = self.conv3(x) #(1536, 299, 299) 192 | x = self.bn3(x) 193 | x = self.relu(x) 194 | 195 | x = self.conv4(x) #(2048, 299, 299) 196 | x = self.bn4(x) 197 | return x 198 | 199 | def logits(self, features): 200 | x = self.relu(features) 201 | 202 | x = F.adaptive_avg_pool2d(x, (1, 1)) 203 | x = x.view(x.size(0), -1) 204 | x = self.last_linear(x) 205 | return x 206 | 207 | def forward(self, input): 208 | x = self.features(input) 209 | x = self.logits(x) 210 | return x 211 | 212 | 213 | 214 | class Xception_concat(nn.Module): 215 | """ 216 | Xception optimized for the ImageNet dataset, as specified in 217 | https://arxiv.org/pdf/1610.02357.pdf 218 | """ 219 | def __init__(self, num_classes=1000): 220 | """ Constructor 221 | Args: 222 | num_classes: number of classes 223 | """ 224 | super(Xception_concat, self).__init__() 225 | self.num_classes = num_classes 226 | 227 | self.conv1 = nn.Conv2d(15,32,3,2,0,bias=False) 228 | self.bn1 = nn.BatchNorm2d(32) 229 | self.relu = nn.ReLU(inplace=True) 230 | 231 | self.conv2 = nn.Conv2d(32,64,3,bias=False) 232 | self.bn2 = nn.BatchNorm2d(64) 233 | #do relu here 234 | 235 | self.block1=Block(64,128,2,2,start_with_relu=False,grow_first=True) 236 | self.block2=Block(128,256,2,2,start_with_relu=True,grow_first=True) 237 | self.block3=Block(256,728,2,2,start_with_relu=True,grow_first=True) 238 | 239 | self.block4=Block(728,728,3,1,start_with_relu=True,grow_first=True) 240 | self.block5=Block(728,728,3,1,start_with_relu=True,grow_first=True) 241 | self.block6=Block(728,728,3,1,start_with_relu=True,grow_first=True) 242 | self.block7=Block(728,728,3,1,start_with_relu=True,grow_first=True) 243 | 244 | self.block8=Block(728,728,3,1,start_with_relu=True,grow_first=True) 245 | self.block9=Block(728,728,3,1,start_with_relu=True,grow_first=True) 246 | self.block10=Block(728,728,3,1,start_with_relu=True,grow_first=True) 247 | self.block11=Block(728,728,3,1,start_with_relu=True,grow_first=True) 248 | 249 | self.block12=Block(728,1024,2,2,start_with_relu=True,grow_first=False) 250 | 251 | self.conv3 = SeparableConv2d(1024,1536,3,1,1) 252 | self.bn3 = nn.BatchNorm2d(1536) 253 | 254 | #do relu here 255 | self.conv4 = SeparableConv2d(1536,2048,3,1,1) 256 | self.bn4 = nn.BatchNorm2d(2048) 257 | 258 | self.fc = nn.Linear(2048, num_classes) 259 | 260 | # #------- init weights -------- 261 | # for m in self.modules(): 262 | # if isinstance(m, nn.Conv2d): 263 | # n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels 264 | # m.weight.data.normal_(0, math.sqrt(2. / n)) 265 | # elif isinstance(m, nn.BatchNorm2d): 266 | # m.weight.data.fill_(1) 267 | # m.bias.data.zero_() 268 | # #----------------------------- 269 | 270 | def features(self, input): 271 | x = self.conv1(input) #(32, 299, 299) 272 | x = self.bn1(x) 273 | x = self.relu(x) 274 | 275 | x = self.conv2(x) #(64, 299, 299) 276 | x = self.bn2(x) 277 | x = self.relu(x) 278 | 279 | x = self.block1(x) 280 | x = self.block2(x) 281 | x = self.block3(x) 282 | x = self.block4(x) 283 | x = self.block5(x) 284 | x = self.block6(x) 285 | x = self.block7(x) 286 | x = self.block8(x) 287 | x = self.block9(x) 288 | x = self.block10(x) 289 | x = self.block11(x) 290 | x = self.block12(x) #(1024, 299, 299) 291 | 292 | x = self.conv3(x) #(1536, 299, 299) 293 | x = self.bn3(x) 294 | x = self.relu(x) 295 | 296 | x = self.conv4(x) #(2048, 299, 299) 297 | x = self.bn4(x) 298 | return x 299 | 300 | def logits(self, features): 301 | x = self.relu(features) 302 | 303 | x = F.adaptive_avg_pool2d(x, (1, 1)) 304 | x = x.view(x.size(0), -1) 305 | x = self.last_linear(x) 306 | return x 307 | 308 | def forward(self, input): 309 | x = self.features(input) 310 | x = self.logits(x) 311 | return x 312 | 313 | ''' 314 | def features(self, input): 315 | x = self.conv1(input) 316 | x = self.bn1(x) 317 | x = self.relu(x) 318 | 319 | x = self.conv2(x) 320 | x = self.bn2(x) 321 | x = self.relu(x) 322 | 323 | x = self.block1(x) 324 | x = self.block2(x) 325 | x = self.block3(x) 326 | x = self.block4(x) 327 | x = self.block5(x) 328 | x = self.block6(x) 329 | x = self.block7(x) 330 | x = self.block8(x) 331 | x = self.block9(x) 332 | x = self.block10(x) 333 | x = self.block11(x) 334 | x = self.block12(x) 335 | 336 | x = self.conv3(x) 337 | x = self.bn3(x) 338 | x = self.relu(x) 339 | 340 | conv4_x = self.conv4(x) 341 | x = self.bn4(conv4_x) 342 | return x, conv4_x 343 | 344 | def logits(self, features): 345 | x = self.relu(features) 346 | 347 | x = F.adaptive_avg_pool2d(x, (1, 1)) 348 | x = x.view(x.size(0), -1) 349 | x = self.last_linear(x) 350 | return x 351 | 352 | def forward(self, input): 353 | #x = self.features(input) 354 | x, conv4_x = self.features(input) 355 | x = self.logits(x) 356 | #x = self.logits(x) 357 | return x, conv4_x 358 | ''' 359 | 360 | 361 | def xception(num_classes=1000, pretrained='imagenet'): 362 | model = Xception(num_classes=num_classes) 363 | if pretrained: 364 | settings = pretrained_settings['xception'][pretrained] 365 | assert num_classes == settings['num_classes'], \ 366 | "num_classes should be {}, but is {}".format(settings['num_classes'], num_classes) 367 | 368 | model = Xception(num_classes=num_classes) 369 | model.load_state_dict(model_zoo.load_url(settings['url'])) 370 | 371 | model.input_space = settings['input_space'] 372 | model.input_size = settings['input_size'] 373 | model.input_range = settings['input_range'] 374 | model.mean = settings['mean'] 375 | model.std = settings['std'] 376 | 377 | # TODO: ugly 378 | model.last_linear = model.fc 379 | del model.fc 380 | return model 381 | 382 | def xception_concat(num_classes=1000): 383 | model = Xception_concat(num_classes=num_classes) 384 | # TODO: ugly 385 | model.last_linear = model.fc 386 | del model.fc 387 | return model -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | opencv==3.4.2 2 | dlib==19.18.0 3 | numpy==1.22.0 4 | pillow>=6.2.2 5 | -------------------------------------------------------------------------------- /test_CNN.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torchvision 4 | from torch.utils.data import DataLoader 5 | import torch.optim as optim 6 | from torch.optim import lr_scheduler 7 | import argparse 8 | import os 9 | import cv2 10 | from network.models import model_selection 11 | from dataset.transform import xception_default_data_transforms 12 | from dataset.mydataset import MyDataset 13 | def main(): 14 | args = parse.parse_args() 15 | test_list = args.test_list 16 | batch_size = args.batch_size 17 | model_path = args.model_path 18 | torch.backends.cudnn.benchmark=True 19 | test_dataset = MyDataset(txt_path=test_list, transform=xception_default_data_transforms['test']) 20 | test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=batch_size, shuffle=True, drop_last=True, num_workers=8) 21 | test_dataset_size = len(test_dataset) 22 | corrects = 0 23 | acc = 0 24 | #model = torchvision.models.densenet121(num_classes=2) 25 | model = model_selection(modelname='xception', num_out_classes=2, dropout=0.5) 26 | model.load_state_dict(torch.load(model_path)) 27 | if isinstance(model, torch.nn.DataParallel): 28 | model = model.module 29 | model = model.cuda() 30 | model.eval() 31 | with torch.no_grad(): 32 | for (image, labels) in test_loader: 33 | image = image.cuda() 34 | labels = labels.cuda() 35 | outputs = model(image) 36 | _, preds = torch.max(outputs.data, 1) 37 | corrects += torch.sum(preds == labels.data).to(torch.float32) 38 | print('Iteration Acc {:.4f}'.format(torch.sum(preds == labels.data).to(torch.float32)/batch_size)) 39 | acc = corrects / test_dataset_size 40 | print('Test Acc: {:.4f}'.format(acc)) 41 | 42 | 43 | 44 | if __name__ == '__main__': 45 | parse = argparse.ArgumentParser( 46 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 47 | parse.add_argument('--batch_size', '-bz', type=int, default=32) 48 | parse.add_argument('--test_list', '-tl', type=str, default='./data_list/Deepfakes_c0_test.txt') 49 | parse.add_argument('--model_path', '-mp', type=str, default='./pretrained_model/df_c0_best.pkl') 50 | main() 51 | print('Hello world!!!') -------------------------------------------------------------------------------- /train_CNN.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torchvision 4 | from torch.utils.data import DataLoader 5 | import torch.optim as optim 6 | from torch.optim import lr_scheduler 7 | import argparse 8 | import os 9 | import cv2 10 | 11 | from network.models import model_selection 12 | from network.mesonet import Meso4, MesoInception4 13 | from dataset.transform import xception_default_data_transforms 14 | from dataset.mydataset import MyDataset 15 | def main(): 16 | args = parse.parse_args() 17 | name = args.name 18 | continue_train = args.continue_train 19 | train_list = args.train_list 20 | val_list = args.val_list 21 | epoches = args.epoches 22 | batch_size = args.batch_size 23 | model_name = args.model_name 24 | model_path = args.model_path 25 | output_path = os.path.join('./output', name) 26 | if not os.path.exists(output_path): 27 | os.mkdir(output_path) 28 | torch.backends.cudnn.benchmark=True 29 | train_dataset = MyDataset(txt_path=train_list, transform=xception_default_data_transforms['train']) 30 | val_dataset = MyDataset(txt_path=val_list, transform=xception_default_data_transforms['val']) 31 | train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=False, num_workers=8) 32 | val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=True, drop_last=False, num_workers=8) 33 | train_dataset_size = len(train_dataset) 34 | val_dataset_size = len(val_dataset) 35 | model = model_selection(modelname='xception', num_out_classes=2, dropout=0.5) 36 | if continue_train: 37 | model.load_state_dict(torch.load(model_path)) 38 | model = model.cuda() 39 | criterion = nn.CrossEntropyLoss() 40 | optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999), eps=1e-08) 41 | scheduler = lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.5) 42 | model = nn.DataParallel(model) 43 | best_model_wts = model.state_dict() 44 | best_acc = 0.0 45 | iteration = 0 46 | for epoch in range(epoches): 47 | print('Epoch {}/{}'.format(epoch+1, epoches)) 48 | print('-'*10) 49 | model.train() 50 | train_loss = 0.0 51 | train_corrects = 0.0 52 | val_loss = 0.0 53 | val_corrects = 0.0 54 | for (image, labels) in train_loader: 55 | iter_loss = 0.0 56 | iter_corrects = 0.0 57 | image = image.cuda() 58 | labels = labels.cuda() 59 | optimizer.zero_grad() 60 | outputs = model(image) 61 | _, preds = torch.max(outputs.data, 1) 62 | loss = criterion(outputs, labels) 63 | loss.backward() 64 | optimizer.step() 65 | iter_loss = loss.data.item() 66 | train_loss += iter_loss 67 | iter_corrects = torch.sum(preds == labels.data).to(torch.float32) 68 | train_corrects += iter_corrects 69 | iteration += 1 70 | if not (iteration % 20): 71 | print('iteration {} train loss: {:.4f} Acc: {:.4f}'.format(iteration, iter_loss / batch_size, iter_corrects / batch_size)) 72 | epoch_loss = train_loss / train_dataset_size 73 | epoch_acc = train_corrects / train_dataset_size 74 | print('epoch train loss: {:.4f} Acc: {:.4f}'.format(epoch_loss, epoch_acc)) 75 | 76 | model.eval() 77 | with torch.no_grad(): 78 | for (image, labels) in val_loader: 79 | image = image.cuda() 80 | labels = labels.cuda() 81 | outputs = model(image) 82 | _, preds = torch.max(outputs.data, 1) 83 | loss = criterion(outputs, labels) 84 | val_loss += loss.data.item() 85 | val_corrects += torch.sum(preds == labels.data).to(torch.float32) 86 | epoch_loss = val_loss / val_dataset_size 87 | epoch_acc = val_corrects / val_dataset_size 88 | print('epoch val loss: {:.4f} Acc: {:.4f}'.format(epoch_loss, epoch_acc)) 89 | if epoch_acc > best_acc: 90 | best_acc = epoch_acc 91 | best_model_wts = model.state_dict() 92 | scheduler.step() 93 | #if not (epoch % 40): 94 | torch.save(model.module.state_dict(), os.path.join(output_path, str(epoch) + '_' + model_name)) 95 | print('Best val Acc: {:.4f}'.format(best_acc)) 96 | model.load_state_dict(best_model_wts) 97 | torch.save(model.module.state_dict(), os.path.join(output_path, "best.pkl")) 98 | 99 | 100 | 101 | 102 | if __name__ == '__main__': 103 | parse = argparse.ArgumentParser( 104 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 105 | parse.add_argument('--name', '-n', type=str, default='fs_xception_c0_299') 106 | parse.add_argument('--train_list', '-tl' , type=str, default = './data_list/FaceSwap_c0_train.txt') 107 | parse.add_argument('--val_list', '-vl' , type=str, default = './data_list/FaceSwap_c0_val.txt') 108 | parse.add_argument('--batch_size', '-bz', type=int, default=64) 109 | parse.add_argument('--epoches', '-e', type=int, default='20') 110 | parse.add_argument('--model_name', '-mn', type=str, default='fs_c0_299.pkl') 111 | parse.add_argument('--continue_train', type=bool, default=False) 112 | parse.add_argument('--model_path', '-mp', type=str, default='./output/df_xception_c0_299/1_df_c0_299.pkl') 113 | main() 114 | -------------------------------------------------------------------------------- /videos/003_000.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HongguLiu/Deepfake-Detection/cecd5ab4b50a482ccf5e64b1057b7cfbb6094fc9/videos/003_000.mp4 --------------------------------------------------------------------------------