├── .gitignore ├── LICENSE ├── README.md ├── environment.yml ├── example.gif ├── freeze_model.py ├── generate_train_data.py ├── reduce_model.py └── run_webcam.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | .venv/ 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Custom 95 | .idea/ 96 | face2face-model*/ 97 | face2face-reduced-model*/ 98 | landmarks 99 | original 100 | angela_merkel_speech.mp4 101 | shape_predictor_68_face_landmarks.dat 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dat Tran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # face2face-demo 2 | 3 | This is a pix2pix demo that learns from facial landmarks and translates this into a face. A webcam-enabled application is also provided that translates your face to the trained face in real-time. 4 | 5 | ## Getting Started 6 | 7 | #### 1. Prepare Environment 8 | 9 | ``` 10 | # Clone this repo 11 | git clone git@github.com:datitran/face2face-demo.git 12 | 13 | # Create the conda environment from file (Mac OSX) 14 | conda env create -f environment.yml 15 | ``` 16 | 17 | #### 2. Generate Training Data 18 | 19 | ``` 20 | python generate_train_data.py --file angela_merkel_speech.mp4 --num 400 --landmark-model shape_predictor_68_face_landmarks.dat 21 | ``` 22 | 23 | Input: 24 | 25 | - `file` is the name of the video file from which you want to create the data set. 26 | - `num` is the number of train data to be created. 27 | - `landmark-model` is the facial landmark model that is used to detect the landmarks. A pre-trained facial landmark model is provided [here](http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2). 28 | 29 | Output: 30 | 31 | - Two folders `original` and `landmarks` will be created. 32 | 33 | If you want to download my dataset, here is also the [video file](https://dl.dropboxusercontent.com/s/2g04onlkmkq9c69/angela_merkel_speech.mp4) that I used and the generated [training dataset](https://dl.dropboxusercontent.com/s/pfm8b0yogmum63w/dataset.zip) (400 images already split into training and validation). 34 | 35 | #### 3. Train Model 36 | 37 | ``` 38 | # Clone the repo from Christopher Hesse's pix2pix TensorFlow implementation 39 | git clone https://github.com/affinelayer/pix2pix-tensorflow.git 40 | 41 | # Move the original and landmarks folder into the pix2pix-tensorflow folder 42 | mv face2face-demo/landmarks face2face-demo/original pix2pix-tensorflow/photos 43 | 44 | # Go into the pix2pix-tensorflow folder 45 | cd pix2pix-tensorflow/ 46 | 47 | # Resize original images 48 | python tools/process.py \ 49 | --input_dir photos/original \ 50 | --operation resize \ 51 | --output_dir photos/original_resized 52 | 53 | # Resize landmark images 54 | python tools/process.py \ 55 | --input_dir photos/landmarks \ 56 | --operation resize \ 57 | --output_dir photos/landmarks_resized 58 | 59 | # Combine both resized original and landmark images 60 | python tools/process.py \ 61 | --input_dir photos/landmarks_resized \ 62 | --b_dir photos/original_resized \ 63 | --operation combine \ 64 | --output_dir photos/combined 65 | 66 | # Split into train/val set 67 | python tools/split.py \ 68 | --dir photos/combined 69 | 70 | # Train the model on the data 71 | python pix2pix.py \ 72 | --mode train \ 73 | --output_dir face2face-model \ 74 | --max_epochs 200 \ 75 | --input_dir photos/combined/train \ 76 | --which_direction AtoB 77 | ``` 78 | 79 | For more information around training, have a look at Christopher Hesse's [pix2pix-tensorflow](https://github.com/affinelayer/pix2pix-tensorflow) implementation. 80 | 81 | #### 4. Export Model 82 | 83 | 1. First, we need to reduce the trained model so that we can use an image tensor as input: 84 | ``` 85 | python reduce_model.py --model-input face2face-model --model-output face2face-reduced-model 86 | ``` 87 | 88 | Input: 89 | 90 | - `model-input` is the model folder to be imported. 91 | - `model-output` is the model (reduced) folder to be exported. 92 | 93 | Output: 94 | 95 | - It returns a reduced model with less weights file size than the original model. 96 | 97 | 2. Second, we freeze the reduced model to a single file. 98 | ``` 99 | python freeze_model.py --model-folder face2face-reduced-model 100 | ``` 101 | 102 | Input: 103 | 104 | - `model-folder` is the model folder of the reduced model. 105 | 106 | Output: 107 | 108 | - It returns a frozen model file `frozen_model.pb` in the model folder. 109 | 110 | I have uploaded a pre-trained frozen model [here](https://dl.dropboxusercontent.com/s/rzfaoeb3e2ta343/face2face_model_epoch_200.zip). This model is trained on 400 images with epoch 200. 111 | 112 | #### 5. Run Demo 113 | 114 | ``` 115 | python run_webcam.py --source 0 --show 0 --landmark-model shape_predictor_68_face_landmarks.dat --tf-model face2face-reduced-model/frozen_model.pb 116 | ``` 117 | 118 | Input: 119 | 120 | - `source` is the device index of the camera (default=0). 121 | - `show` is an option to either display the normal input (0) or the facial landmark (1) alongside the generated image (default=0). 122 | - `landmark-model` is the facial landmark model that is used to detect the landmarks. 123 | - `tf-model` is the frozen model file. 124 | 125 | Example: 126 | 127 | ![example](example.gif) 128 | 129 | ## Requirements 130 | - [Anaconda / Python 3.5](https://www.continuum.io/downloads) 131 | - [TensorFlow 1.2](https://www.tensorflow.org/) 132 | - [OpenCV 3.0](http://opencv.org/) 133 | - [Dlib 19.4](http://dlib.net/) 134 | 135 | ## Acknowledgments 136 | Kudos to [Christopher Hesse](https://github.com/christopherhesse) for his amazing pix2pix TensorFlow implementation and [Gene Kogan](http://genekogan.com/) for his inspirational workshop. 137 | 138 | ## Copyright 139 | 140 | See [LICENSE](LICENSE) for details. 141 | Copyright (c) 2017 [Dat Tran](http://www.dat-tran.com/). 142 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: face2face-demo 2 | channels: !!python/tuple 3 | - menpo 4 | - defaults 5 | dependencies: 6 | - bzip2=1.0.6=3 7 | - menpo::opencv3=3.1.0=py35_0 8 | - jpeg=9b=0 9 | - libpng=1.6.27=0 10 | - menpo::boost=1.59.0=py35_0 11 | - menpo::dlib=19.4=py35_0 12 | - conda-forge::tbb 13 | - mkl=2017.0.3=0 14 | - numpy=1.13.0=py35_0 15 | - openssl=1.0.2l=0 16 | - pip=9.0.1=py35_1 17 | - python=3.5.3=1 18 | - readline=6.2=2 19 | - setuptools=27.2.0=py35_0 20 | - sqlite=3.13.0=0 21 | - tk=8.5.18=0 22 | - wheel=0.29.0=py35_0 23 | - xz=5.2.2=1 24 | - zlib=1.2.8=3 25 | - pip: 26 | - backports.weakref==1.0rc1 27 | - bleach==1.5.0 28 | - html5lib==0.9999999 29 | - imutils==0.4.3 30 | - markdown==2.6.8 31 | - protobuf==3.3.0 32 | - six==1.10.0 33 | - tensorflow==1.2.1 34 | - werkzeug==0.12.2 35 | prefix: /Users/datitran/anaconda/envs/face2face-demo 36 | 37 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datitran/face2face-demo/19d916a35d2432e745d6d47ab0dca23d5c02ea18/example.gif -------------------------------------------------------------------------------- /freeze_model.py: -------------------------------------------------------------------------------- 1 | import os, argparse 2 | import tensorflow as tf 3 | from tensorflow.python.framework import graph_util 4 | 5 | dir = os.path.dirname(os.path.realpath(__file__)) 6 | 7 | 8 | def freeze_graph(model_folder): 9 | # We retrieve our checkpoint fullpath 10 | checkpoint = tf.train.get_checkpoint_state(model_folder) 11 | input_checkpoint = checkpoint.model_checkpoint_path 12 | 13 | # We precise the file fullname of our freezed graph 14 | absolute_model_folder = '/'.join(input_checkpoint.split('/')[:-1]) 15 | output_graph = absolute_model_folder + '/frozen_model.pb' 16 | 17 | # Before exporting our graph, we need to precise what is our output node 18 | # This is how TF decides what part of the Graph he has to keep and what part it can dump 19 | # NOTE: this variable is plural, because you can have multiple output nodes 20 | output_node_names = 'generate_output/output' 21 | 22 | # We clear devices to allow TensorFlow to control on which device it will load operations 23 | clear_devices = True 24 | 25 | # We import the meta graph and retrieve a Saver 26 | saver = tf.train.import_meta_graph(input_checkpoint + '.meta', clear_devices=clear_devices) 27 | 28 | # We retrieve the protobuf graph definition 29 | graph = tf.get_default_graph() 30 | input_graph_def = graph.as_graph_def() 31 | 32 | # We start a session and restore the graph weights 33 | with tf.Session() as sess: 34 | saver.restore(sess, input_checkpoint) 35 | 36 | # We use a built-in TF helper to export variables to constants 37 | output_graph_def = graph_util.convert_variables_to_constants( 38 | sess, # The session is used to retrieve the weights 39 | input_graph_def, # The graph_def is used to retrieve the nodes 40 | output_node_names.split(",") # The output node names are used to select the usefull nodes 41 | ) 42 | 43 | # Finally we serialize and dump the output graph to the filesystem 44 | with tf.gfile.GFile(output_graph, 'wb') as f: 45 | f.write(output_graph_def.SerializeToString()) 46 | print('%d ops in the final graph.' % len(output_graph_def.node)) 47 | 48 | 49 | if __name__ == '__main__': 50 | parser = argparse.ArgumentParser() 51 | parser.add_argument('--model-folder', type=str, help='Model folder to export') 52 | args = parser.parse_args() 53 | 54 | freeze_graph(args.model_folder) 55 | -------------------------------------------------------------------------------- /generate_train_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import dlib 4 | import time 5 | import argparse 6 | import numpy as np 7 | from imutils import video 8 | 9 | DOWNSAMPLE_RATIO = 4 10 | 11 | 12 | def reshape_for_polyline(array): 13 | return np.array(array, np.int32).reshape((-1, 1, 2)) 14 | 15 | 16 | def main(): 17 | os.makedirs('original', exist_ok=True) 18 | os.makedirs('landmarks', exist_ok=True) 19 | 20 | cap = cv2.VideoCapture(args.filename) 21 | fps = video.FPS().start() 22 | 23 | count = 0 24 | while cap.isOpened(): 25 | ret, frame = cap.read() 26 | 27 | frame_resize = cv2.resize(frame, None, fx=1 / DOWNSAMPLE_RATIO, fy=1 / DOWNSAMPLE_RATIO) 28 | gray = cv2.cvtColor(frame_resize, cv2.COLOR_BGR2GRAY) 29 | faces = detector(gray, 1) 30 | black_image = np.zeros(frame.shape, np.uint8) 31 | 32 | t = time.time() 33 | 34 | # Perform if there is a face detected 35 | if len(faces) == 1: 36 | for face in faces: 37 | detected_landmarks = predictor(gray, face).parts() 38 | landmarks = [[p.x * DOWNSAMPLE_RATIO, p.y * DOWNSAMPLE_RATIO] for p in detected_landmarks] 39 | 40 | jaw = reshape_for_polyline(landmarks[0:17]) 41 | left_eyebrow = reshape_for_polyline(landmarks[22:27]) 42 | right_eyebrow = reshape_for_polyline(landmarks[17:22]) 43 | nose_bridge = reshape_for_polyline(landmarks[27:31]) 44 | lower_nose = reshape_for_polyline(landmarks[30:35]) 45 | left_eye = reshape_for_polyline(landmarks[42:48]) 46 | right_eye = reshape_for_polyline(landmarks[36:42]) 47 | outer_lip = reshape_for_polyline(landmarks[48:60]) 48 | inner_lip = reshape_for_polyline(landmarks[60:68]) 49 | 50 | color = (255, 255, 255) 51 | thickness = 3 52 | 53 | cv2.polylines(black_image, [jaw], False, color, thickness) 54 | cv2.polylines(black_image, [left_eyebrow], False, color, thickness) 55 | cv2.polylines(black_image, [right_eyebrow], False, color, thickness) 56 | cv2.polylines(black_image, [nose_bridge], False, color, thickness) 57 | cv2.polylines(black_image, [lower_nose], True, color, thickness) 58 | cv2.polylines(black_image, [left_eye], True, color, thickness) 59 | cv2.polylines(black_image, [right_eye], True, color, thickness) 60 | cv2.polylines(black_image, [outer_lip], True, color, thickness) 61 | cv2.polylines(black_image, [inner_lip], True, color, thickness) 62 | 63 | # Display the resulting frame 64 | count += 1 65 | print(count) 66 | cv2.imwrite("original/{}.png".format(count), frame) 67 | cv2.imwrite("landmarks/{}.png".format(count), black_image) 68 | fps.update() 69 | 70 | print('[INFO] elapsed time: {:.2f}'.format(time.time() - t)) 71 | 72 | if count == args.number: # only take 400 photos 73 | break 74 | elif cv2.waitKey(1) & 0xFF == ord('q'): 75 | break 76 | else: 77 | print("No face detected") 78 | 79 | fps.stop() 80 | print('[INFO] elapsed time (total): {:.2f}'.format(fps.elapsed())) 81 | print('[INFO] approx. FPS: {:.2f}'.format(fps.fps())) 82 | 83 | cap.release() 84 | cv2.destroyAllWindows() 85 | 86 | 87 | if __name__ == '__main__': 88 | parser = argparse.ArgumentParser() 89 | parser.add_argument('--file', dest='filename', type=str, help='Name of the video file.') 90 | parser.add_argument('--num', dest='number', type=int, help='Number of train data to be created.') 91 | parser.add_argument('--landmark-model', dest='face_landmark_shape_file', type=str, help='Face landmark model file.') 92 | args = parser.parse_args() 93 | 94 | # Create the face predictor and landmark predictor 95 | detector = dlib.get_frontal_face_detector() 96 | predictor = dlib.shape_predictor(args.face_landmark_shape_file) 97 | 98 | main() 99 | -------------------------------------------------------------------------------- /reduce_model.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import tensorflow as tf 3 | 4 | CROP_SIZE = 256 # scale_size = CROP_SIZE 5 | ngf = 64 6 | ndf = 64 7 | 8 | 9 | def preprocess(image): 10 | with tf.name_scope('preprocess'): 11 | # [0, 1] => [-1, 1] 12 | return image * 2 - 1 13 | 14 | 15 | def deprocess(image): 16 | with tf.name_scope('deprocess'): 17 | # [-1, 1] => [0, 1] 18 | return (image + 1) / 2 19 | 20 | 21 | def gen_conv(batch_input, out_channels): 22 | # [batch, in_height, in_width, in_channels] => [batch, out_height, out_width, out_channels] 23 | initializer = tf.random_normal_initializer(0, 0.02) 24 | # if a.separable_conv: 25 | # return tf.layers.separable_conv2d(batch_input, out_channels, kernel_size=4, strides=(2, 2), padding="same", depthwise_initializer=initializer, pointwise_initializer=initializer) 26 | # else: 27 | return tf.layers.conv2d(batch_input, out_channels, kernel_size=4, strides=(2, 2), padding="same", kernel_initializer=initializer) 28 | 29 | 30 | def lrelu(x, a): 31 | with tf.name_scope('lrelu'): 32 | # adding these together creates the leak part and linear part 33 | # then cancels them out by subtracting/adding an absolute value term 34 | # leak: a*x/2 - a*abs(x)/2 35 | # linear: x/2 + abs(x)/2 36 | 37 | # this block looks like it has 2 inputs on the graph unless we do this 38 | x = tf.identity(x) 39 | return (0.5 * (1 + a)) * x + (0.5 * (1 - a)) * tf.abs(x) 40 | 41 | 42 | def batchnorm(inputs): 43 | return tf.layers.batch_normalization(inputs, axis=3, epsilon=1e-5, momentum=0.1, training=True, gamma_initializer=tf.random_normal_initializer(1.0, 0.02)) 44 | # with tf.variable_scope('batchnorm'): 45 | # # this block looks like it has 3 inputs on the graph unless we do this 46 | # input = tf.identity(input) 47 | # 48 | # channels = input.get_shape()[3] 49 | # offset = tf.get_variable('offset', [channels], dtype=tf.float32, initializer=tf.zeros_initializer()) 50 | # scale = tf.get_variable('scale', [channels], dtype=tf.float32, 51 | # initializer=tf.random_normal_initializer(1.0, 0.02)) 52 | # mean, variance = tf.nn.moments(input, axes=[0, 1, 2], keep_dims=False) 53 | # variance_epsilon = 1e-5 54 | # normalized = tf.nn.batch_normalization(input, mean, variance, offset, scale, variance_epsilon=variance_epsilon) 55 | # return normalized 56 | 57 | 58 | def gen_deconv(batch_input, out_channels): 59 | # [batch, in_height, in_width, in_channels] => [batch, out_height, out_width, out_channels] 60 | initializer = tf.random_normal_initializer(0, 0.02) 61 | # if a.separable_conv: 62 | # _b, h, w, _c = batch_input.shape 63 | # resized_input = tf.image.resize_images(batch_input, [h * 2, w * 2], method=tf.image.ResizeMethod.NEAREST_NEIGHBOR) 64 | # return tf.layers.separable_conv2d(resized_input, out_channels, kernel_size=4, strides=(1, 1), padding="same", depthwise_initializer=initializer, pointwise_initializer=initializer) 65 | # else: 66 | return tf.layers.conv2d_transpose(batch_input, out_channels, kernel_size=4, strides=(2, 2), padding="same", kernel_initializer=initializer) 67 | 68 | 69 | def process_image(x): 70 | with tf.name_scope('load_images'): 71 | raw_input = tf.image.convert_image_dtype(x, dtype=tf.float32) 72 | 73 | raw_input.set_shape([None, None, 3]) 74 | 75 | # break apart image pair and move to range [-1, 1] 76 | width = tf.shape(raw_input)[1] # [height, width, channels] 77 | a_images = preprocess(raw_input[:, :width // 2, :]) 78 | b_images = preprocess(raw_input[:, width // 2:, :]) 79 | 80 | inputs, targets = [a_images, b_images] 81 | 82 | # synchronize seed for image operations so that we do the same operations to both 83 | # input and output images 84 | def transform(image): 85 | r = image 86 | 87 | # area produces a nice downscaling, but does nearest neighbor for upscaling 88 | # assume we're going to be doing downscaling here 89 | r = tf.image.resize_images(r, [CROP_SIZE, CROP_SIZE], method=tf.image.ResizeMethod.AREA) 90 | 91 | return r 92 | 93 | with tf.name_scope('input_images'): 94 | input_images = tf.expand_dims(transform(inputs), 0) 95 | 96 | with tf.name_scope('target_images'): 97 | target_images = tf.expand_dims(transform(targets), 0) 98 | 99 | return input_images, target_images 100 | 101 | # Tensor('batch:1', shape=(1, 256, 256, 3), dtype=float32) -> 1 batch size 102 | 103 | 104 | def create_generator(generator_inputs, generator_outputs_channels): 105 | layers = [] 106 | 107 | # encoder_1: [batch, 256, 256, in_channels] => [batch, 128, 128, ngf] 108 | with tf.variable_scope('encoder_1'): 109 | output = gen_conv(generator_inputs, ngf) 110 | layers.append(output) 111 | 112 | layer_specs = [ 113 | ngf * 2, # encoder_2: [batch, 128, 128, ngf] => [batch, 64, 64, ngf * 2] 114 | ngf * 4, # encoder_3: [batch, 64, 64, ngf * 2] => [batch, 32, 32, ngf * 4] 115 | ngf * 8, # encoder_4: [batch, 32, 32, ngf * 4] => [batch, 16, 16, ngf * 8] 116 | ngf * 8, # encoder_5: [batch, 16, 16, ngf * 8] => [batch, 8, 8, ngf * 8] 117 | ngf * 8, # encoder_6: [batch, 8, 8, ngf * 8] => [batch, 4, 4, ngf * 8] 118 | ngf * 8, # encoder_7: [batch, 4, 4, ngf * 8] => [batch, 2, 2, ngf * 8] 119 | ngf * 8, # encoder_8: [batch, 2, 2, ngf * 8] => [batch, 1, 1, ngf * 8] 120 | ] 121 | 122 | for out_channels in layer_specs: 123 | with tf.variable_scope('encoder_%d' % (len(layers) + 1)): 124 | rectified = lrelu(layers[-1], 0.2) 125 | # [batch, in_height, in_width, in_channels] => [batch, in_height/2, in_width/2, out_channels] 126 | convolved = gen_conv(rectified, out_channels) 127 | output = batchnorm(convolved) 128 | layers.append(output) 129 | 130 | layer_specs = [ 131 | (ngf * 8, 0.5), # decoder_8: [batch, 1, 1, ngf * 8] => [batch, 2, 2, ngf * 8 * 2] 132 | (ngf * 8, 0.5), # decoder_7: [batch, 2, 2, ngf * 8 * 2] => [batch, 4, 4, ngf * 8 * 2] 133 | (ngf * 8, 0.5), # decoder_6: [batch, 4, 4, ngf * 8 * 2] => [batch, 8, 8, ngf * 8 * 2] 134 | (ngf * 8, 0.0), # decoder_5: [batch, 8, 8, ngf * 8 * 2] => [batch, 16, 16, ngf * 8 * 2] 135 | (ngf * 4, 0.0), # decoder_4: [batch, 16, 16, ngf * 8 * 2] => [batch, 32, 32, ngf * 4 * 2] 136 | (ngf * 2, 0.0), # decoder_3: [batch, 32, 32, ngf * 4 * 2] => [batch, 64, 64, ngf * 2 * 2] 137 | (ngf, 0.0), # decoder_2: [batch, 64, 64, ngf * 2 * 2] => [batch, 128, 128, ngf * 2] 138 | ] 139 | 140 | num_encoder_layers = len(layers) 141 | for decoder_layer, (out_channels, dropout) in enumerate(layer_specs): 142 | skip_layer = num_encoder_layers - decoder_layer - 1 143 | with tf.variable_scope('decoder_%d' % (skip_layer + 1)): 144 | if decoder_layer == 0: 145 | # first decoder layer doesn't have skip connections 146 | # since it is directly connected to the skip_layer 147 | input = layers[-1] 148 | else: 149 | input = tf.concat([layers[-1], layers[skip_layer]], axis=3) 150 | 151 | rectified = tf.nn.relu(input) 152 | # [batch, in_height, in_width, in_channels] => [batch, in_height*2, in_width*2, out_channels] 153 | output = gen_deconv(rectified, out_channels) 154 | output = batchnorm(output) 155 | 156 | if dropout > 0.0: 157 | output = tf.nn.dropout(output, keep_prob=1 - dropout) 158 | 159 | layers.append(output) 160 | 161 | # decoder_1: [batch, 128, 128, ngf * 2] => [batch, 256, 256, generator_outputs_channels] 162 | with tf.variable_scope('decoder_1'): 163 | input = tf.concat([layers[-1], layers[0]], axis=3) 164 | rectified = tf.nn.relu(input) 165 | output = gen_deconv(rectified, generator_outputs_channels) 166 | output = tf.tanh(output) 167 | layers.append(output) 168 | 169 | return layers[-1] 170 | 171 | 172 | def create_model(inputs, targets): 173 | with tf.variable_scope('generator'): # as scope 174 | out_channels = int(targets.get_shape()[-1]) 175 | outputs = create_generator(inputs, out_channels) 176 | 177 | return outputs 178 | 179 | 180 | def convert(image): 181 | return tf.image.convert_image_dtype(image, dtype=tf.uint8, saturate=True, name='output') # output tensor 182 | 183 | 184 | def generate_output(x): 185 | with tf.name_scope('generate_output'): 186 | test_inputs, test_targets = process_image(x) 187 | 188 | # inputs and targets are [batch_size, height, width, channels] 189 | model = create_model(test_inputs, test_targets) 190 | 191 | # deprocess files 192 | outputs = deprocess(model) 193 | 194 | # reverse any processing on images so they can be written to disk or displayed to user 195 | converted_outputs = convert(outputs) 196 | return converted_outputs 197 | 198 | 199 | if __name__ == '__main__': 200 | parser = argparse.ArgumentParser() 201 | parser.add_argument('--model-input', dest='input_folder', type=str, help='Model folder to import.') 202 | parser.add_argument('--model-output', dest='output_folder', type=str, help='Model (reduced) folder to export.') 203 | args = parser.parse_args() 204 | 205 | x = tf.placeholder(tf.uint8, shape=(256, 512, 3), name='image_tensor') # input tensor 206 | y = generate_output(x) 207 | 208 | with tf.Session() as sess: 209 | # Restore original model 210 | saver = tf.train.Saver() 211 | checkpoint = tf.train.latest_checkpoint(args.input_folder) 212 | saver.restore(sess, checkpoint) 213 | 214 | # Export reduced model used for prediction 215 | saver = tf.train.Saver() 216 | saver.save(sess, '{}/reduced_model'.format(args.output_folder)) 217 | print("Model is exported to {}".format(checkpoint)) 218 | -------------------------------------------------------------------------------- /run_webcam.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import cv2 3 | import dlib 4 | import numpy as np 5 | import tensorflow as tf 6 | from imutils import video 7 | 8 | CROP_SIZE = 256 9 | DOWNSAMPLE_RATIO = 4 10 | 11 | 12 | def reshape_for_polyline(array): 13 | """Reshape image so that it works with polyline.""" 14 | return np.array(array, np.int32).reshape((-1, 1, 2)) 15 | 16 | 17 | def resize(image): 18 | """Crop and resize image for pix2pix.""" 19 | height, width, _ = image.shape 20 | if height != width: 21 | # crop to correct ratio 22 | size = min(height, width) 23 | oh = (height - size) // 2 24 | ow = (width - size) // 2 25 | cropped_image = image[oh:(oh + size), ow:(ow + size)] 26 | image_resize = cv2.resize(cropped_image, (CROP_SIZE, CROP_SIZE)) 27 | return image_resize 28 | 29 | 30 | def load_graph(frozen_graph_filename): 31 | """Load a (frozen) Tensorflow model into memory.""" 32 | graph = tf.Graph() 33 | with graph.as_default(): 34 | od_graph_def = tf.GraphDef() 35 | with tf.gfile.GFile(frozen_graph_filename, 'rb') as fid: 36 | serialized_graph = fid.read() 37 | od_graph_def.ParseFromString(serialized_graph) 38 | tf.import_graph_def(od_graph_def, name='') 39 | return graph 40 | 41 | 42 | def main(): 43 | # TensorFlow 44 | graph = load_graph(args.frozen_model_file) 45 | image_tensor = graph.get_tensor_by_name('image_tensor:0') 46 | output_tensor = graph.get_tensor_by_name('generate_output/output:0') 47 | sess = tf.Session(graph=graph) 48 | 49 | # OpenCV 50 | cap = cv2.VideoCapture(args.video_source) 51 | fps = video.FPS().start() 52 | 53 | while True: 54 | ret, frame = cap.read() 55 | 56 | # resize image and detect face 57 | frame_resize = cv2.resize(frame, None, fx=1 / DOWNSAMPLE_RATIO, fy=1 / DOWNSAMPLE_RATIO) 58 | gray = cv2.cvtColor(frame_resize, cv2.COLOR_BGR2GRAY) 59 | faces = detector(gray, 1) 60 | black_image = np.zeros(frame.shape, np.uint8) 61 | 62 | for face in faces: 63 | detected_landmarks = predictor(gray, face).parts() 64 | landmarks = [[p.x * DOWNSAMPLE_RATIO, p.y * DOWNSAMPLE_RATIO] for p in detected_landmarks] 65 | 66 | jaw = reshape_for_polyline(landmarks[0:17]) 67 | left_eyebrow = reshape_for_polyline(landmarks[22:27]) 68 | right_eyebrow = reshape_for_polyline(landmarks[17:22]) 69 | nose_bridge = reshape_for_polyline(landmarks[27:31]) 70 | lower_nose = reshape_for_polyline(landmarks[30:35]) 71 | left_eye = reshape_for_polyline(landmarks[42:48]) 72 | right_eye = reshape_for_polyline(landmarks[36:42]) 73 | outer_lip = reshape_for_polyline(landmarks[48:60]) 74 | inner_lip = reshape_for_polyline(landmarks[60:68]) 75 | 76 | color = (255, 255, 255) 77 | thickness = 3 78 | 79 | cv2.polylines(black_image, [jaw], False, color, thickness) 80 | cv2.polylines(black_image, [left_eyebrow], False, color, thickness) 81 | cv2.polylines(black_image, [right_eyebrow], False, color, thickness) 82 | cv2.polylines(black_image, [nose_bridge], False, color, thickness) 83 | cv2.polylines(black_image, [lower_nose], True, color, thickness) 84 | cv2.polylines(black_image, [left_eye], True, color, thickness) 85 | cv2.polylines(black_image, [right_eye], True, color, thickness) 86 | cv2.polylines(black_image, [outer_lip], True, color, thickness) 87 | cv2.polylines(black_image, [inner_lip], True, color, thickness) 88 | 89 | # generate prediction 90 | combined_image = np.concatenate([resize(black_image), resize(frame_resize)], axis=1) 91 | image_rgb = cv2.cvtColor(combined_image, cv2.COLOR_BGR2RGB) # OpenCV uses BGR instead of RGB 92 | generated_image = sess.run(output_tensor, feed_dict={image_tensor: image_rgb}) 93 | image_bgr = cv2.cvtColor(np.squeeze(generated_image), cv2.COLOR_RGB2BGR) 94 | image_normal = np.concatenate([resize(frame_resize), image_bgr], axis=1) 95 | image_landmark = np.concatenate([resize(black_image), image_bgr], axis=1) 96 | 97 | if args.display_landmark == 0: 98 | cv2.imshow('frame', image_normal) 99 | else: 100 | cv2.imshow('frame', image_landmark) 101 | 102 | fps.update() 103 | if cv2.waitKey(1) & 0xFF == ord('q'): 104 | break 105 | 106 | fps.stop() 107 | print('[INFO] elapsed time (total): {:.2f}'.format(fps.elapsed())) 108 | print('[INFO] approx. FPS: {:.2f}'.format(fps.fps())) 109 | 110 | sess.close() 111 | cap.release() 112 | cv2.destroyAllWindows() 113 | 114 | 115 | if __name__ == '__main__': 116 | parser = argparse.ArgumentParser() 117 | parser.add_argument('-src', '--source', dest='video_source', type=int, 118 | default=0, help='Device index of the camera.') 119 | parser.add_argument('--show', dest='display_landmark', type=int, default=0, choices=[0, 1], 120 | help='0 shows the normal input and 1 the facial landmark.') 121 | parser.add_argument('--landmark-model', dest='face_landmark_shape_file', type=str, help='Face landmark model file.') 122 | parser.add_argument('--tf-model', dest='frozen_model_file', type=str, help='Frozen TensorFlow model file.') 123 | 124 | args = parser.parse_args() 125 | 126 | # Create the face predictor and landmark predictor 127 | detector = dlib.get_frontal_face_detector() 128 | predictor = dlib.shape_predictor(args.face_landmark_shape_file) 129 | 130 | main() 131 | --------------------------------------------------------------------------------