├── LICENSE ├── README.md ├── images ├── cats.png ├── celeb.jpg ├── celeb2.jpg ├── dogs.jpg ├── fig1.png ├── fig3.png ├── fig6.png ├── fig8.png ├── monkeys.jpg ├── selfie.jpg ├── smileys.jpg └── zombies.jpg ├── matconvnet_hr101_to_pickle.py ├── networks └── ResNet101.pdf ├── tiny_face_eval.py ├── tiny_face_model.py └── util.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cydonia 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny Face Detector in TensorFlow 2 | 3 | A TensorFlow port(inference only) of Tiny Face Detector from [authors' MatConvNet codes](https://github.com/peiyunh/tiny)[1]. 4 | 5 | # Requirements 6 | 7 | Codes are written in Python. At first install [Anaconda](https://docs.anaconda.com/anaconda/install.html). 8 | Then install [OpenCV](https://github.com/opencv/opencv), [TensorFlow](https://www.tensorflow.org/). 9 | 10 | # Usage 11 | 12 | ## Converting a pretrained model 13 | 14 | `matconvnet_hr101_to_pickle` reads weights of the MatConvNet pretrained model and 15 | write back to a pickle file which is used in a TensorFlow model as initial weights. 16 | 17 | 1. Download a [ResNet101-based pretrained model(hr_res101.mat)](https://www.cs.cmu.edu/%7Epeiyunh/tiny/hr_res101.mat) 18 | from the authors' repo. 19 | 20 | 2. Convert the model to a pickle file by: 21 | ``` 22 | python matconvnet_hr101_to_pickle.py 23 | --matlab_model_path /path/to/pretrained_model 24 | --weight_file_path /path/to/pickle_file 25 | ``` 26 | 27 | ## Tesing Tiny Face Detector in TensorFlow 28 | 29 | 1. Prepare images in a directory. 30 | 31 | 2. `tiny_face_eval.py` reads images one by one from the image directory and 32 | write images to an output directory with bounding boxes of detected faces. 33 | ``` 34 | python tiny_face_eval.py 35 | --weight_file_path /path/to/pickle_file 36 | --data_dir /path/to/input_image_directory 37 | --output_dir /path/to/output_directory 38 | ``` 39 | 40 | # Neural network diagram 41 | 42 | [This](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/networks/ResNet101.pdf)(pdf) is 43 | a network diagram of the ResNet101-based model used here for an input image(height: 1150, width: 2048, channel: 3). 44 | 45 | 46 | # Examples 47 | 48 | Though this model is developed to detect tiny faces, I apply this to several types of images including 'faces' 49 | as experiments. 50 | 51 | ### selfie with many people 52 | This is the same image as one in [the authors' repo](https://github.com/peiyunh/tiny)[1]. 53 | 54 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/selfie.jpg?raw=true) 55 | 56 | [Original image](https://github.com/peiyunh/tiny/blob/master/data/demo/selfie.jpg) 57 | 58 | ### selfie of celebrities 59 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/celeb.jpg?raw=true) 60 | 61 | [Original image](https://twitter.com/thesimpsons/status/441000198995582976) 62 | 63 | ### selfie of "celebrities" 64 | Homer and "Meryl Streep" are missed. 65 | 66 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/celeb2.jpg?raw=true) 67 | 68 | [Original image](https://twitter.com/thesimpsons/status/441000198995582976) 69 | 70 | ### zombies 71 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/zombies.jpg?raw=true) 72 | 73 | [Original image](http://www.talkingwalkingdead.com/2012/03/walk-on-by.html) 74 | 75 | ### monkeys 76 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/monkeys.jpg?raw=true) 77 | 78 | [Original image](http://intisari.grid.id/index.php/Techno/Science/Manusia-Saling-Mengenal-Wajah-Simpanse-Saling-Mengenal-Pantat) 79 | 80 | ### dogs 81 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/dogs.jpg?raw=true) 82 | 83 | [Original image](http://www.socialitelife.com/photos/sweet-crazy-woman-adopts-1500-dogs-200-cats/some-may-think-shes-barking-mad-but-one-chinese-woman-adopted-1500-stray-dogs) 84 | 85 | ### cats 86 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/cats.png?raw=true) 87 | 88 | [Original image](http://kodex.me/clanak/80268/na-ovom-ostrvu-macke-su-najbrojniji-stanovnici) 89 | 90 | ### figure1 from a paper[2] 91 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/fig1.png?raw=true) 92 | 93 | ### figure8 from a paper[2]. 94 | Facebook's face detector failed to detect these faces(as of the paper publication date[14 Feb 2016]). 95 | 96 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/fig8.png?raw=true) 97 | 98 | ### figure3 from a paper[2] 99 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/fig3.png?raw=true) 100 | 101 | ### figure6 from a paper[2] 102 | ![selfie](https://github.com/cydonia999/Tiny_Faces_in_Tensorflow/blob/master/images/fig6.png?raw=true) 103 | 104 | # Acknowledgments 105 | 106 | - Many python codes are borrowed from [chinakook's MXNet tiny face detector](https://github.com/chinakook/hr101_mxnet) 107 | - parula colormap table is borrowed from [`fake_parula.py`](https://github.com/BIDS/colormap/blob/master/fake_parula.py). 108 | 109 | # Disclaimer 110 | 111 | Codes are tested only on CPUs, not GPUs. 112 | 113 | # References 114 | 115 | 1. Hu, Peiyun and Ramanan, Deva, 116 | Finding Tiny Faces, 117 | The IEEE Conference on Computer Vision and Pattern Recognition (CVPR 2017). 118 | [project page](https://www.cs.cmu.edu/~peiyunh/tiny/), [arXiv](https://arxiv.org/abs/1612.04402) 119 | 120 | 2. Michael J. Wilber, Vitaly Shmatikov, Serge Belongie, 121 | Can we still avoid automatic face detection, 2016. 122 | [arXiv](https://arxiv.org/abs/1602.04504) 123 | 124 | -------------------------------------------------------------------------------- /images/cats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/cats.png -------------------------------------------------------------------------------- /images/celeb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/celeb.jpg -------------------------------------------------------------------------------- /images/celeb2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/celeb2.jpg -------------------------------------------------------------------------------- /images/dogs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/dogs.jpg -------------------------------------------------------------------------------- /images/fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/fig1.png -------------------------------------------------------------------------------- /images/fig3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/fig3.png -------------------------------------------------------------------------------- /images/fig6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/fig6.png -------------------------------------------------------------------------------- /images/fig8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/fig8.png -------------------------------------------------------------------------------- /images/monkeys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/monkeys.jpg -------------------------------------------------------------------------------- /images/selfie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/selfie.jpg -------------------------------------------------------------------------------- /images/smileys.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/smileys.jpg -------------------------------------------------------------------------------- /images/zombies.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/images/zombies.jpg -------------------------------------------------------------------------------- /matconvnet_hr101_to_pickle.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | 6 | import numpy as np 7 | import scipy.io as sio 8 | import os 9 | import pickle 10 | from argparse import ArgumentParser 11 | 12 | argparse = ArgumentParser() 13 | argparse.add_argument('--matlab_model_path', type=str, help='Matlab pretrained model.', 14 | default='/path/to/hr_res101.mat') 15 | argparse.add_argument('--weight_file_path', type=str, help='Weight file for Tensorflow.', 16 | default='/path/to/mat2tf.pkl') 17 | 18 | args = argparse.parse_args() 19 | 20 | # check arguments 21 | assert os.path.exists(args.matlab_model_path), \ 22 | "Matlab pretrained model: " + args.matlab_model_path + " not found." 23 | assert os.path.exists(os.path.dirname((args.weight_file_path))),\ 24 | "Directory for weight file for Tensorflow: " + args.weight_file_path + " not found." 25 | 26 | mat_params_dict = {} 27 | mat_blocks_dict = {} 28 | 29 | f = sio.loadmat(args.matlab_model_path) 30 | net = f['net'] 31 | clusters = np.copy(net['meta'][0][0][0][0][6]) 32 | average_image = np.copy(net['meta'][0][0][0][0][2][0][0][2])[:, 0] 33 | mat_params_dict["clusters"] = clusters 34 | mat_params_dict["average_image"] = average_image 35 | 36 | layers = net['layers'][0][0][0] 37 | mat_params = net['params'][0][0][0] 38 | for p in mat_params: 39 | mat_params_dict[p[0][0]] = p[1] 40 | 41 | for k, layer in enumerate(layers): 42 | type_string = '' 43 | param_string = '' 44 | 45 | layer_name, layer_type = layer[0][0], layer[1][0] 46 | layer_inputs = [] 47 | layer_outputs = [] 48 | layer_params = [] 49 | 50 | layer_inputs_count = layer[2][0].shape[0] 51 | for i in range(layer_inputs_count): 52 | layer_inputs.append(layer[2][0][i][0]) 53 | 54 | layer_outputs_count = layer[3][0].shape[0] 55 | for i in range(layer_outputs_count): 56 | layer_outputs.append(layer[3][0][i][0]) 57 | 58 | if layer[4].shape[0] > 0: 59 | layer_params_count = layer[4][0].shape[0] 60 | for i in range(layer_params_count): 61 | layer_params.append(layer[4][0][i][0]) 62 | 63 | mat_blocks_dict[layer_name + '_type'] = layer_type 64 | mat_params_dict[layer_name + '_type'] = layer_type 65 | if layer_type == u'dagnn.Conv': 66 | nchw = layer[5][0][0][0][0] 67 | has_bias = layer[5][0][0][1][0][0] 68 | pad = layer[5][0][0][3][0] 69 | stride = layer[5][0][0][4][0] 70 | dilate = layer[5][0][0][5][0] 71 | mat_blocks_dict[layer_name + '_nchw'] = nchw 72 | mat_blocks_dict[layer_name + '_has_bias'] = has_bias 73 | mat_blocks_dict[layer_name + '_pad'] = pad 74 | mat_blocks_dict[layer_name + '_stride'] = stride 75 | mat_blocks_dict[layer_name + '_dilate'] = dilate 76 | if has_bias: 77 | bias = mat_params_dict[layer_name + '_bias'][0] # (1, N) -> (N,) 78 | mat_params_dict[layer_name + '_bias'] = bias 79 | elif layer_type == u'dagnn.BatchNorm': 80 | epsilon = layer[5][0][0][1][0][0] 81 | gamma = mat_params_dict[layer_name + '_mult'][:, 0] # (N, 1) -> (N,) 82 | beta = mat_params_dict[layer_name + '_bias'][:, 0] # (N, 1) -> (N,) 83 | moments = mat_params_dict[layer_name + '_moments'] # (N, 2) -> (N,), (N,) 84 | moving_mean = moments[:, 0] 85 | moving_var = moments[:, 1] * moments[:, 1] - epsilon 86 | 87 | mat_blocks_dict[layer_name + '_variance_epsilon'] = epsilon 88 | mat_params_dict[layer_name + '_scale'] = gamma 89 | mat_params_dict[layer_name + '_offset'] = beta 90 | mat_params_dict[layer_name + '_mean'] = moving_mean 91 | mat_params_dict[layer_name + '_variance'] = moving_var 92 | elif layer_type == u'dagnn.ConvTranspose': 93 | nchw = layer[5][0][0][0][0] 94 | has_bias = layer[5][0][0][1][0][0] 95 | upsample = layer[5][0][0][2][0] 96 | crop = layer[5][0][0][3][0] 97 | mat_blocks_dict[layer_name + '_nchw'] = nchw 98 | mat_blocks_dict[layer_name + '_has_bias'] = has_bias 99 | mat_blocks_dict[layer_name + '_upsample'] = upsample 100 | mat_blocks_dict[layer_name + '_crop'] = crop 101 | wmat = mat_params_dict[layer_name + 'f'] 102 | mat_params_dict[layer_name + '_filter'] = wmat 103 | elif layer_type == u'dagnn.Pooling': 104 | method = layer[5][0][0][0][0] 105 | pool_size = layer[5][0][0][1][0] 106 | pad = layer[5][0][0][3][0] 107 | stride = layer[5][0][0][4][0] 108 | mat_blocks_dict[layer_name + '_method'] = method 109 | mat_blocks_dict[layer_name + '_pool_size'] = pool_size 110 | mat_blocks_dict[layer_name + '_pad'] = pad 111 | mat_blocks_dict[layer_name + '_stride'] = stride 112 | elif layer_type == u'dagnn.ReLU': 113 | pass 114 | elif layer_type == u'dagnn.Sum': 115 | pass 116 | else: 117 | pass 118 | 119 | with open(args.weight_file_path, 'wb') as f: 120 | pickle.dump([mat_blocks_dict, mat_params_dict], f, pickle.HIGHEST_PROTOCOL) 121 | -------------------------------------------------------------------------------- /networks/ResNet101.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cydonia999/Tiny_Faces_in_Tensorflow/feccef440fafd8cf9f9d1d49f1824f8993439d4b/networks/ResNet101.pdf -------------------------------------------------------------------------------- /tiny_face_eval.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | 6 | import tensorflow as tf 7 | import tiny_face_model 8 | import util 9 | from argparse import ArgumentParser 10 | import cv2 11 | import scipy.io 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | import cv2 15 | import pickle 16 | 17 | import pylab as pl 18 | import time 19 | import os 20 | import sys 21 | from scipy.special import expit 22 | import glob 23 | 24 | MAX_INPUT_DIM = 5000.0 25 | 26 | def overlay_bounding_boxes(raw_img, refined_bboxes, lw): 27 | """Overlay bounding boxes of face on images. 28 | Args: 29 | raw_img: 30 | A target image. 31 | refined_bboxes: 32 | Bounding boxes of detected faces. 33 | lw: 34 | Line width of bounding boxes. If zero specified, 35 | this is determined based on confidence of each detection. 36 | Returns: 37 | None. 38 | """ 39 | 40 | # Overlay bounding boxes on an image with the color based on the confidence. 41 | for r in refined_bboxes: 42 | _score = expit(r[4]) 43 | cm_idx = int(np.ceil(_score * 255)) 44 | rect_color = [int(np.ceil(x * 255)) for x in util.cm_data[cm_idx]] # parula 45 | _lw = lw 46 | if lw == 0: # line width of each bounding box is adaptively determined. 47 | bw, bh = r[2] - r[0] + 1, r[3] - r[0] + 1 48 | _lw = 1 if min(bw, bh) <= 20 else max(2, min(3, min(bh / 20, bw / 20))) 49 | _lw = int(np.ceil(_lw * _score)) 50 | 51 | _r = [int(x) for x in r[:4]] 52 | cv2.rectangle(raw_img, (_r[0], _r[1]), (_r[2], _r[3]), rect_color, _lw) 53 | 54 | 55 | def evaluate(weight_file_path, data_dir, output_dir, prob_thresh=0.5, nms_thresh=0.1, lw=3, display=False): 56 | """Detect faces in images. 57 | Args: 58 | prob_thresh: 59 | The threshold of detection confidence. 60 | nms_thresh: 61 | The overlap threshold of non maximum suppression 62 | weight_file_path: 63 | A pretrained weight file in the pickle format 64 | generated by matconvnet_hr101_to_tf.py. 65 | data_dir: 66 | A directory which contains images. 67 | output_dir: 68 | A directory into which images with detected faces are output. 69 | lw: 70 | Line width of bounding boxes. If zero specified, 71 | this is determined based on confidence of each detection. 72 | display: 73 | Display tiny face images on window. 74 | Returns: 75 | None. 76 | """ 77 | 78 | # placeholder of input images. Currently batch size of one is supported. 79 | x = tf.placeholder(tf.float32, [1, None, None, 3]) # n, h, w, c 80 | 81 | # Create the tiny face model which weights are loaded from a pretrained model. 82 | model = tiny_face_model.Model(weight_file_path) 83 | score_final = model.tiny_face(x) 84 | 85 | # Find image files in data_dir. 86 | filenames = [] 87 | for ext in ('*.png', '*.gif', '*.jpg', '*.jpeg'): 88 | filenames.extend(glob.glob(os.path.join(data_dir, ext))) 89 | 90 | # Load an average image and clusters(reference boxes of templates). 91 | with open(weight_file_path, "rb") as f: 92 | _, mat_params_dict = pickle.load(f) 93 | 94 | average_image = model.get_data_by_key("average_image") 95 | clusters = model.get_data_by_key("clusters") 96 | clusters_h = clusters[:, 3] - clusters[:, 1] + 1 97 | clusters_w = clusters[:, 2] - clusters[:, 0] + 1 98 | normal_idx = np.where(clusters[:, 4] == 1) 99 | 100 | # main 101 | with tf.Session() as sess: 102 | sess.run(tf.global_variables_initializer()) 103 | 104 | for filename in filenames: 105 | fname = filename.split(os.sep)[-1] 106 | raw_img = cv2.imread(filename) 107 | raw_img = cv2.cvtColor(raw_img, cv2.COLOR_BGR2RGB) 108 | raw_img_f = raw_img.astype(np.float32) 109 | 110 | def _calc_scales(): 111 | raw_h, raw_w = raw_img.shape[0], raw_img.shape[1] 112 | min_scale = min(np.floor(np.log2(np.max(clusters_w[normal_idx] / raw_w))), 113 | np.floor(np.log2(np.max(clusters_h[normal_idx] / raw_h)))) 114 | max_scale = min(1.0, -np.log2(max(raw_h, raw_w) / MAX_INPUT_DIM)) 115 | scales_down = pl.frange(min_scale, 0, 1.) 116 | scales_up = pl.frange(0.5, max_scale, 0.5) 117 | scales_pow = np.hstack((scales_down, scales_up)) 118 | scales = np.power(2.0, scales_pow) 119 | return scales 120 | 121 | scales = _calc_scales() 122 | start = time.time() 123 | 124 | # initialize output 125 | bboxes = np.empty(shape=(0, 5)) 126 | 127 | # process input at different scales 128 | for s in scales: 129 | print("Processing {} at scale {:.4f}".format(fname, s)) 130 | img = cv2.resize(raw_img_f, (0, 0), fx=s, fy=s, interpolation=cv2.INTER_LINEAR) 131 | img = img - average_image 132 | img = img[np.newaxis, :] 133 | 134 | # we don't run every template on every scale ids of templates to ignore 135 | tids = list(range(4, 12)) + ([] if s <= 1.0 else list(range(18, 25))) 136 | ignoredTids = list(set(range(0, clusters.shape[0])) - set(tids)) 137 | 138 | # run through the net 139 | score_final_tf = sess.run(score_final, feed_dict={x: img}) 140 | 141 | # collect scores 142 | score_cls_tf, score_reg_tf = score_final_tf[:, :, :, :25], score_final_tf[:, :, :, 25:125] 143 | prob_cls_tf = expit(score_cls_tf) 144 | prob_cls_tf[0, :, :, ignoredTids] = 0.0 145 | 146 | def _calc_bounding_boxes(): 147 | # threshold for detection 148 | _, fy, fx, fc = np.where(prob_cls_tf > prob_thresh) 149 | 150 | # interpret heatmap into bounding boxes 151 | cy = fy * 8 - 1 152 | cx = fx * 8 - 1 153 | ch = clusters[fc, 3] - clusters[fc, 1] + 1 154 | cw = clusters[fc, 2] - clusters[fc, 0] + 1 155 | 156 | # extract bounding box refinement 157 | Nt = clusters.shape[0] 158 | tx = score_reg_tf[0, :, :, 0:Nt] 159 | ty = score_reg_tf[0, :, :, Nt:2*Nt] 160 | tw = score_reg_tf[0, :, :, 2*Nt:3*Nt] 161 | th = score_reg_tf[0, :, :, 3*Nt:4*Nt] 162 | 163 | # refine bounding boxes 164 | dcx = cw * tx[fy, fx, fc] 165 | dcy = ch * ty[fy, fx, fc] 166 | rcx = cx + dcx 167 | rcy = cy + dcy 168 | rcw = cw * np.exp(tw[fy, fx, fc]) 169 | rch = ch * np.exp(th[fy, fx, fc]) 170 | 171 | scores = score_cls_tf[0, fy, fx, fc] 172 | tmp_bboxes = np.vstack((rcx - rcw / 2, rcy - rch / 2, rcx + rcw / 2, rcy + rch / 2)) 173 | tmp_bboxes = np.vstack((tmp_bboxes / s, scores)) 174 | tmp_bboxes = tmp_bboxes.transpose() 175 | return tmp_bboxes 176 | 177 | tmp_bboxes = _calc_bounding_boxes() 178 | bboxes = np.vstack((bboxes, tmp_bboxes)) # : (5265, 5) 179 | 180 | 181 | print("time {:.2f} secs for {}".format(time.time() - start, fname)) 182 | 183 | # non maximum suppression 184 | # refind_idx = util.nms(bboxes, nms_thresh) 185 | refind_idx = tf.image.non_max_suppression(tf.convert_to_tensor(bboxes[:, :4], dtype=tf.float32), 186 | tf.convert_to_tensor(bboxes[:, 4], dtype=tf.float32), 187 | max_output_size=bboxes.shape[0], iou_threshold=nms_thresh) 188 | refind_idx = sess.run(refind_idx) 189 | refined_bboxes = bboxes[refind_idx] 190 | overlay_bounding_boxes(raw_img, refined_bboxes, lw) 191 | 192 | if display: 193 | # plt.axis('off') 194 | plt.imshow(raw_img) 195 | plt.show() 196 | 197 | # save image with bounding boxes 198 | raw_img = cv2.cvtColor(raw_img, cv2.COLOR_RGB2BGR) 199 | cv2.imwrite(os.path.join(output_dir, fname), raw_img) 200 | 201 | def main(): 202 | 203 | argparse = ArgumentParser() 204 | argparse.add_argument('--weight_file_path', type=str, help='Pretrained weight file.', default="/path/to/mat2tf.pkl") 205 | argparse.add_argument('--data_dir', type=str, help='Image data directory.', default="/path/to/input_image_directory") 206 | argparse.add_argument('--output_dir', type=str, help='Output directory for images with faces detected.', default="/path/to/output_directory") 207 | argparse.add_argument('--prob_thresh', type=float, help='The threshold of detection confidence(default: 0.5).', default=0.5) 208 | argparse.add_argument('--nms_thresh', type=float, help='The overlap threshold of non maximum suppression(default: 0.1).', default=0.1) 209 | argparse.add_argument('--line_width', type=int, help='Line width of bounding boxes(0: auto).', default=3) 210 | argparse.add_argument('--display', type=bool, help='Display each image on window.', default=False) 211 | 212 | args = argparse.parse_args() 213 | 214 | # check arguments 215 | assert os.path.exists(args.weight_file_path), "weight file: " + args.weight_file_path + " not found." 216 | assert os.path.exists(args.data_dir), "data directory: " + args.data_dir + " not found." 217 | assert os.path.exists(args.output_dir), "output directory: " + args.output_dir + " not found." 218 | assert args.line_width >= 0, "line_width should be >= 0." 219 | 220 | with tf.Graph().as_default(): 221 | evaluate( 222 | weight_file_path=args.weight_file_path, data_dir=args.data_dir, output_dir=args.output_dir, 223 | prob_thresh=args.prob_thresh, nms_thresh=args.nms_thresh, 224 | lw=args.line_width, display=args.display) 225 | 226 | if __name__ == '__main__': 227 | main() 228 | -------------------------------------------------------------------------------- /tiny_face_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | 6 | import tensorflow as tf 7 | import numpy as np 8 | import pickle 9 | 10 | class Model(): 11 | def __init__(self, weight_file_path): 12 | """Overlay bounding boxes of face on images. 13 | Args: 14 | weight_file_path: 15 | A pretrained weight file in the pickle format 16 | generated by matconvnet_hr101_to_tf.py. 17 | Returns: 18 | None. 19 | """ 20 | self.dtype = tf.float32 21 | self.weight_file_path = weight_file_path 22 | with open(self.weight_file_path, "rb") as f: 23 | self.mat_blocks_dict, self.mat_params_dict = pickle.load(f) 24 | 25 | def get_data_by_key(self, key): 26 | """Helper to access a pretrained model data through a key.""" 27 | assert key in self.mat_params_dict, "key: " + key + " not found." 28 | return self.mat_params_dict[key] 29 | 30 | def _weight_variable_on_cpu(self, name, shape): 31 | """Helper to create a weight Variable stored on CPU memory. 32 | 33 | Args: 34 | name: name of the variable. 35 | shape: list of ints: (height, width, channel, filter). 36 | 37 | Returns: 38 | initializer for Variable. 39 | """ 40 | assert len(shape) == 4 41 | 42 | weights = self.get_data_by_key(name + "_filter") # (h, w, channel, filter) 43 | assert list(weights.shape) == shape 44 | initializer = tf.constant_initializer(weights, dtype=self.dtype) 45 | 46 | with tf.device('/cpu:0'): 47 | var = tf.get_variable(name + "_w", shape, initializer=initializer, dtype=self.dtype) 48 | return var 49 | 50 | def _bias_variable_on_cpu(self, name, shape): 51 | """Helper to create a bias Variable stored on CPU memory. 52 | 53 | Args: 54 | name: name of the variable. 55 | shape: int, filter size. 56 | 57 | Returns: 58 | initializer for Variable. 59 | """ 60 | assert isinstance(shape, int) 61 | bias = self.get_data_by_key(name + "_bias") 62 | assert len(bias) == shape 63 | initializer = tf.constant_initializer(bias, dtype=self.dtype) 64 | 65 | with tf.device('/cpu:0'): 66 | var = tf.get_variable(name + "_b", shape, initializer=initializer, dtype=self.dtype) 67 | return var 68 | 69 | 70 | def _bn_variable_on_cpu(self, name, shape): 71 | """Helper to create a batch normalization Variable stored on CPU memory. 72 | 73 | Args: 74 | name: name of the variable. 75 | shape: int, filter size. 76 | 77 | Returns: 78 | initializer for Variable. 79 | """ 80 | assert isinstance(shape, int) 81 | 82 | name2 = "bn" + name[3:] 83 | if name.startswith("conv"): 84 | name2 = "bn_" + name 85 | 86 | scale = self.get_data_by_key(name2 + '_scale') 87 | offset = self.get_data_by_key(name2 + '_offset') 88 | mean = self.get_data_by_key(name2 + '_mean') 89 | variance = self.get_data_by_key(name2 + '_variance') 90 | 91 | with tf.device('/cpu:0'): 92 | initializer = tf.constant_initializer(scale, dtype=self.dtype) 93 | scale = tf.get_variable(name2 + "_scale", shape, initializer=initializer, dtype=self.dtype) 94 | initializer = tf.constant_initializer(offset, dtype=self.dtype) 95 | offset = tf.get_variable(name2 + "_offset", shape, initializer=initializer, dtype=self.dtype) 96 | initializer = tf.constant_initializer(mean, dtype=self.dtype) 97 | mean = tf.get_variable(name2 + "_mean", shape, initializer=initializer, dtype=self.dtype) 98 | initializer = tf.constant_initializer(variance, dtype=self.dtype) 99 | variance = tf.get_variable(name2 + "_variance", shape, initializer=initializer, dtype=self.dtype) 100 | 101 | return scale, offset, mean, variance 102 | 103 | 104 | def conv_block(self, bottom, name, shape, strides=[1,1,1,1], padding="SAME", 105 | has_bias=False, add_relu=True, add_bn=True, eps=1.0e-5): 106 | """Create a block composed of multiple layers: 107 | a conv layer 108 | a batch normalization layer 109 | an activation layer 110 | 111 | Args: 112 | bottom: A layer before this block. 113 | name: Name of the block. 114 | shape: List of ints: (height, width, channel, filter). 115 | strides: Strides of conv layer. 116 | padding: Padding of conv layer. 117 | has_bias: Whether a bias term is added. 118 | add_relu: Whether a ReLU layer is added. 119 | add_bn: Whether a batch normalization layer is added. 120 | eps: A small float number to avoid dividing by 0, used in a batch normalization layer. 121 | Returns: 122 | a block of layers 123 | """ 124 | assert len(shape) == 4 125 | 126 | weight = self._weight_variable_on_cpu(name, shape) 127 | conv = tf.nn.conv2d(bottom, weight, strides, padding=padding) 128 | if has_bias: 129 | bias = self._bias_variable_on_cpu(name, shape[3]) 130 | 131 | pre_activation = tf.nn.bias_add(conv, bias) if has_bias else conv 132 | 133 | if add_bn: 134 | # scale, offset, mean, variance = self._bn_variable_on_cpu("bn_" + name, shape[-1]) 135 | scale, offset, mean, variance = self._bn_variable_on_cpu(name, shape[-1]) 136 | pre_activation = tf.nn.batch_normalization(pre_activation, mean, variance, offset, scale, variance_epsilon=eps) 137 | 138 | relu = tf.nn.relu(pre_activation) if add_relu else pre_activation 139 | 140 | return relu 141 | 142 | 143 | def conv_trans_layer(self, bottom, name, shape, strides=[1,1,1,1], padding="SAME", has_bias=False): 144 | """Create a block composed of multiple layers: 145 | a transpose of conv layer 146 | an activation layer 147 | 148 | Args: 149 | bottom: A layer before this block. 150 | name: Name of the block. 151 | shape: List of ints: (height, width, channel, filter). 152 | strides: Strides of conv layer. 153 | padding: Padding of conv layer. 154 | has_bias: Whether a bias term is added. 155 | add_relu: Whether a ReLU layer is added. 156 | Returns: 157 | a block of layers 158 | """ 159 | assert len(shape) == 4 160 | 161 | weight = self._weight_variable_on_cpu(name, shape) 162 | nb, h, w, nc = tf.split(tf.shape(bottom), num_or_size_splits=4) 163 | output_shape = tf.stack([nb, (h - 1) * strides[1] - 3 + shape[0], (w - 1) * strides[2] - 3 + shape[1], nc])[:, 0] 164 | conv = tf.nn.conv2d_transpose(bottom, weight, output_shape, strides, padding=padding) 165 | if has_bias: 166 | bias = self._bias_variable_on_cpu(name, shape[3]) 167 | 168 | conv = tf.nn.bias_add(conv, bias) if has_bias else conv 169 | 170 | return conv 171 | 172 | def residual_block(self, bottom, name, in_channel, neck_channel, out_channel, trunk): 173 | """Create a residual block. 174 | 175 | Args: 176 | bottom: A layer before this block. 177 | name: Name of the block. 178 | in_channel: number of channels in a input tensor. 179 | neck_channel: number of channels in a bottleneck block. 180 | out_channel: number of channels in a output tensor. 181 | trunk: a tensor in a identity path. 182 | Returns: 183 | a block of layers 184 | """ 185 | _strides = [1, 2, 2, 1] if name.startswith("res3a") or name.startswith("res4a") else [1, 1, 1, 1] 186 | res = self.conv_block(bottom, name + '_branch2a', shape=[1, 1, in_channel, neck_channel], 187 | strides=_strides, padding="VALID", add_relu=True) 188 | res = self.conv_block(res, name + '_branch2b', shape=[3, 3, neck_channel, neck_channel], 189 | padding="SAME", add_relu=True) 190 | res = self.conv_block(res, name + '_branch2c', shape=[1, 1, neck_channel, out_channel], 191 | padding="VALID", add_relu=False) 192 | 193 | res = trunk + res 194 | res = tf.nn.relu(res) 195 | 196 | return res 197 | 198 | def tiny_face(self, image): 199 | """Create a tiny face model. 200 | 201 | Args: 202 | image: an input image. 203 | Returns: 204 | a score tensor 205 | """ 206 | img = tf.pad(image, [[0, 0], [3, 3], [3, 3], [0, 0]], "CONSTANT") 207 | conv = self.conv_block(img, 'conv1', shape=[7, 7, 3, 64], strides=[1, 2, 2, 1], padding="VALID", add_relu=True) 208 | pool1 = tf.nn.max_pool(conv, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME') 209 | 210 | res2a_branch1 = self.conv_block(pool1, 'res2a_branch1', shape=[1, 1, 64, 256], padding="VALID", add_relu=False) 211 | res2a = self.residual_block(pool1, 'res2a', 64, 64, 256, res2a_branch1) 212 | res2b = self.residual_block(res2a, 'res2b', 256, 64, 256, res2a) 213 | res2c = self.residual_block(res2b, 'res2c', 256, 64, 256, res2b) 214 | 215 | res3a_branch1 = self.conv_block(res2c, 'res3a_branch1', shape=[1, 1, 256, 512], strides=[1, 2, 2, 1], padding="VALID", add_relu=False) 216 | res3a = self.residual_block(res2c, 'res3a', 256, 128, 512, res3a_branch1) 217 | 218 | res3b1 = self.residual_block(res3a, 'res3b1', 512, 128, 512, res3a) 219 | res3b2 = self.residual_block(res3b1, 'res3b2', 512, 128, 512, res3b1) 220 | res3b3 = self.residual_block(res3b2, 'res3b3', 512, 128, 512, res3b2) 221 | 222 | res4a_branch1 = self.conv_block(res3b3, 'res4a_branch1', shape=[1, 1, 512, 1024], strides=[1, 2, 2, 1], padding="VALID", add_relu=False) 223 | res4a = self.residual_block(res3b3, 'res4a', 512, 256, 1024, res4a_branch1) 224 | 225 | res4b = res4a 226 | for i in range(1, 23): 227 | res4b = self.residual_block(res4b, 'res4b' + str(i), 1024, 256, 1024, res4b) 228 | 229 | score_res4 = self.conv_block(res4b, 'score_res4', shape=[1, 1, 1024, 125], padding="VALID", 230 | has_bias=True, add_relu=False, add_bn=False) 231 | score4 = self.conv_trans_layer(score_res4, 'score4', shape=[4, 4, 125, 125], strides=[1, 2, 2, 1], padding="SAME") 232 | score_res3 = self.conv_block(res3b3, 'score_res3', shape=[1, 1, 512, 125], padding="VALID", 233 | has_bias=True, add_bn=False, add_relu=False) 234 | 235 | bs, height, width = tf.split(tf.shape(score4), num_or_size_splits=4)[0:3] 236 | _size = tf.convert_to_tensor([height[0], width[0]]) 237 | _offsets = tf.zeros([bs[0], 2]) 238 | score_res3c = tf.image.extract_glimpse(score_res3, _size, _offsets, centered=True, normalized=False) 239 | 240 | score_final = score4 + score_res3c 241 | return score_final 242 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | 6 | import numpy as np 7 | 8 | def nms(dets, prob_thresh): 9 | x1 = dets[:, 0] 10 | y1 = dets[:, 1] 11 | x2 = dets[:, 2] 12 | y2 = dets[:, 3] 13 | scores = dets[:, 4] 14 | 15 | areas = (x2 - x1 + 1) * (y2 - y1 + 1) 16 | 17 | order = scores.argsort()[::-1] 18 | 19 | keep = [] 20 | while order.size > 0: 21 | i = order[0] 22 | keep.append(i) 23 | xx1 = np.maximum(x1[i], x1[order[1:]]) 24 | yy1 = np.maximum(y1[i], y1[order[1:]]) 25 | xx2 = np.minimum(x2[i], x2[order[1:]]) 26 | yy2 = np.minimum(y2[i], y2[order[1:]]) 27 | w = np.maximum(0.0, xx2 - xx1 + 1) 28 | h = np.maximum(0.0, yy2 - yy1 + 1) 29 | inter = w * h 30 | 31 | ovr = inter / (areas[i] + areas[order[1:]] - inter) 32 | inds = np.where(ovr <= prob_thresh)[0] 33 | 34 | order = order[inds + 1] 35 | return keep 36 | 37 | # colormap parula borrowed from 38 | # https://github.com/BIDS/colormap/blob/master/fake_parula.py 39 | cm_data = [[ 0.26710521, 0.03311059, 0.6188155 ], 40 | [ 0.26493929, 0.04780926, 0.62261795], 41 | [ 0.26260545, 0.06084214, 0.62619176], 42 | [ 0.26009691, 0.07264411, 0.62951561], 43 | [ 0.25740785, 0.08360391, 0.63256745], 44 | [ 0.25453369, 0.09395358, 0.63532497], 45 | [ 0.25147146, 0.10384228, 0.6377661 ], 46 | [ 0.24822014, 0.11337029, 0.6398697 ], 47 | [ 0.24478105, 0.12260661, 0.64161629], 48 | [ 0.24115816, 0.131599 , 0.6429888 ], 49 | [ 0.23735836, 0.14038009, 0.64397346], 50 | [ 0.23339166, 0.14897137, 0.64456048], 51 | [ 0.22927127, 0.15738602, 0.64474476], 52 | [ 0.22501278, 0.16563165, 0.64452595], 53 | [ 0.22063349, 0.17371215, 0.64390834], 54 | [ 0.21616055, 0.18162302, 0.64290515], 55 | [ 0.21161851, 0.18936156, 0.64153295], 56 | [ 0.20703353, 0.19692415, 0.63981287], 57 | [ 0.20243273, 0.20430706, 0.63776986], 58 | [ 0.19784363, 0.211507 , 0.63543183], 59 | [ 0.19329361, 0.21852157, 0.63282872], 60 | [ 0.18880937, 0.2253495 , 0.62999156], 61 | [ 0.18442119, 0.23198815, 0.62695569], 62 | [ 0.18014936, 0.23844124, 0.62374886], 63 | [ 0.17601569, 0.24471172, 0.62040016], 64 | [ 0.17204028, 0.25080356, 0.61693715], 65 | [ 0.16824123, 0.25672163, 0.6133854 ], 66 | [ 0.16463462, 0.26247158, 0.60976836], 67 | [ 0.16123449, 0.26805963, 0.60610723], 68 | [ 0.15805279, 0.27349243, 0.60242099], 69 | [ 0.15509948, 0.27877688, 0.59872645], 70 | [ 0.15238249, 0.28392004, 0.59503836], 71 | [ 0.14990781, 0.28892902, 0.59136956], 72 | [ 0.14767951, 0.29381086, 0.58773113], 73 | [ 0.14569979, 0.29857245, 0.58413255], 74 | [ 0.1439691 , 0.30322055, 0.58058191], 75 | [ 0.14248613, 0.30776167, 0.57708599], 76 | [ 0.14124797, 0.31220208, 0.57365049], 77 | [ 0.14025018, 0.31654779, 0.57028011], 78 | [ 0.13948691, 0.32080454, 0.5669787 ], 79 | [ 0.13895174, 0.32497744, 0.56375063], 80 | [ 0.13863958, 0.32907012, 0.56060453], 81 | [ 0.138537 , 0.3330895 , 0.55753513], 82 | [ 0.13863384, 0.33704026, 0.55454374], 83 | [ 0.13891931, 0.34092684, 0.55163126], 84 | [ 0.13938212, 0.34475344, 0.54879827], 85 | [ 0.14001061, 0.34852402, 0.54604503], 86 | [ 0.14079292, 0.35224233, 0.54337156], 87 | [ 0.14172091, 0.35590982, 0.54078769], 88 | [ 0.14277848, 0.35953205, 0.53828312], 89 | [ 0.14395358, 0.36311234, 0.53585661], 90 | [ 0.1452346 , 0.36665374, 0.5335074 ], 91 | [ 0.14661019, 0.3701591 , 0.5312346 ], 92 | [ 0.14807104, 0.37363011, 0.52904278], 93 | [ 0.1496059 , 0.3770697 , 0.52692951], 94 | [ 0.15120289, 0.3804813 , 0.52488853], 95 | [ 0.15285214, 0.38386729, 0.52291854], 96 | [ 0.15454421, 0.38722991, 0.52101815], 97 | [ 0.15627225, 0.39056998, 0.5191937 ], 98 | [ 0.15802555, 0.39389087, 0.5174364 ], 99 | [ 0.15979549, 0.39719482, 0.51574311], 100 | [ 0.16157425, 0.40048375, 0.51411214], 101 | [ 0.16335571, 0.40375871, 0.51254622], 102 | [ 0.16513234, 0.40702178, 0.51104174], 103 | [ 0.1668964 , 0.41027528, 0.50959299], 104 | [ 0.16864151, 0.41352084, 0.50819797], 105 | [ 0.17036277, 0.41675941, 0.50685814], 106 | [ 0.1720542 , 0.41999269, 0.50557008], 107 | [ 0.17370932, 0.42322271, 0.50432818], 108 | [ 0.17532301, 0.42645082, 0.50313007], 109 | [ 0.17689176, 0.42967776, 0.50197686], 110 | [ 0.17841013, 0.43290523, 0.5008633 ], 111 | [ 0.17987314, 0.43613477, 0.49978492], 112 | [ 0.18127676, 0.43936752, 0.49873901], 113 | [ 0.18261885, 0.44260392, 0.49772638], 114 | [ 0.18389409, 0.44584578, 0.49673978], 115 | [ 0.18509911, 0.44909409, 0.49577605], 116 | [ 0.18623135, 0.4523496 , 0.494833 ], 117 | [ 0.18728844, 0.45561305, 0.49390803], 118 | [ 0.18826671, 0.45888565, 0.49299567], 119 | [ 0.18916393, 0.46216809, 0.49209268], 120 | [ 0.18997879, 0.46546084, 0.49119678], 121 | [ 0.19070881, 0.46876472, 0.49030328], 122 | [ 0.19135221, 0.47208035, 0.48940827], 123 | [ 0.19190791, 0.47540815, 0.48850845], 124 | [ 0.19237491, 0.47874852, 0.4876002 ], 125 | [ 0.19275204, 0.48210192, 0.48667935], 126 | [ 0.19303899, 0.48546858, 0.48574251], 127 | [ 0.19323526, 0.48884877, 0.48478573], 128 | [ 0.19334062, 0.49224271, 0.48380506], 129 | [ 0.19335574, 0.49565037, 0.4827974 ], 130 | [ 0.19328143, 0.49907173, 0.48175948], 131 | [ 0.19311664, 0.50250719, 0.48068559], 132 | [ 0.192864 , 0.50595628, 0.47957408], 133 | [ 0.19252521, 0.50941877, 0.47842186], 134 | [ 0.19210087, 0.51289469, 0.47722441], 135 | [ 0.19159194, 0.516384 , 0.47597744], 136 | [ 0.19100267, 0.51988593, 0.47467988], 137 | [ 0.19033595, 0.52340005, 0.47332894], 138 | [ 0.18959113, 0.5269267 , 0.47191795], 139 | [ 0.18877336, 0.530465 , 0.47044603], 140 | [ 0.18788765, 0.53401416, 0.46891178], 141 | [ 0.18693822, 0.53757359, 0.46731272], 142 | [ 0.18592276, 0.54114404, 0.46563962], 143 | [ 0.18485204, 0.54472367, 0.46389595], 144 | [ 0.18373148, 0.5483118 , 0.46207951], 145 | [ 0.18256585, 0.55190791, 0.4601871 ], 146 | [ 0.18135481, 0.55551253, 0.45821002], 147 | [ 0.18011172, 0.55912361, 0.45615277], 148 | [ 0.17884392, 0.56274038, 0.45401341], 149 | [ 0.17755858, 0.56636217, 0.45178933], 150 | [ 0.17625543, 0.56998972, 0.44946971], 151 | [ 0.174952 , 0.57362064, 0.44706119], 152 | [ 0.17365805, 0.57725408, 0.44456198], 153 | [ 0.17238403, 0.58088916, 0.4419703 ], 154 | [ 0.17113321, 0.58452637, 0.43927576], 155 | [ 0.1699221 , 0.58816399, 0.43648119], 156 | [ 0.1687662 , 0.5918006 , 0.43358772], 157 | [ 0.16767908, 0.59543526, 0.43059358], 158 | [ 0.16667511, 0.59906699, 0.42749697], 159 | [ 0.16575939, 0.60269653, 0.42428344], 160 | [ 0.16495764, 0.6063212 , 0.42096245], 161 | [ 0.16428695, 0.60993988, 0.41753246], 162 | [ 0.16376481, 0.61355147, 0.41399151], 163 | [ 0.16340924, 0.61715487, 0.41033757], 164 | [ 0.16323549, 0.62074951, 0.40656329], 165 | [ 0.16326148, 0.62433443, 0.40266378], 166 | [ 0.16351136, 0.62790748, 0.39864431], 167 | [ 0.16400433, 0.63146734, 0.39450263], 168 | [ 0.16475937, 0.63501264, 0.39023638], 169 | [ 0.16579502, 0.63854196, 0.38584309], 170 | [ 0.16712921, 0.64205381, 0.38132023], 171 | [ 0.168779 , 0.64554661, 0.37666513], 172 | [ 0.17075915, 0.64901912, 0.37186962], 173 | [ 0.17308572, 0.65246934, 0.36693299], 174 | [ 0.1757732 , 0.65589512, 0.36185643], 175 | [ 0.17883344, 0.65929449, 0.3566372 ], 176 | [ 0.18227669, 0.66266536, 0.35127251], 177 | [ 0.18611159, 0.66600553, 0.34575959], 178 | [ 0.19034516, 0.66931265, 0.34009571], 179 | [ 0.19498285, 0.67258423, 0.3342782 ], 180 | [ 0.20002863, 0.67581761, 0.32830456], 181 | [ 0.20548509, 0.67900997, 0.3221725 ], 182 | [ 0.21135348, 0.68215834, 0.31587999], 183 | [ 0.2176339 , 0.68525954, 0.30942543], 184 | [ 0.22432532, 0.68831023, 0.30280771], 185 | [ 0.23142568, 0.69130688, 0.29602636], 186 | [ 0.23893914, 0.69424565, 0.28906643], 187 | [ 0.2468574 , 0.69712255, 0.28194103], 188 | [ 0.25517514, 0.69993351, 0.27465372], 189 | [ 0.26388625, 0.70267437, 0.26720869], 190 | [ 0.27298333, 0.70534087, 0.25961196], 191 | [ 0.28246016, 0.70792854, 0.25186761], 192 | [ 0.29232159, 0.71043184, 0.2439642 ], 193 | [ 0.30253943, 0.71284765, 0.23594089], 194 | [ 0.31309875, 0.71517209, 0.22781515], 195 | [ 0.32399522, 0.71740028, 0.21959115], 196 | [ 0.33520729, 0.71952906, 0.21129816], 197 | [ 0.3467003 , 0.72155723, 0.20298257], 198 | [ 0.35846225, 0.72348143, 0.19466318], 199 | [ 0.3704552 , 0.72530195, 0.18639333], 200 | [ 0.38264126, 0.72702007, 0.17822762], 201 | [ 0.39499483, 0.72863609, 0.17020921], 202 | [ 0.40746591, 0.73015499, 0.1624122 ], 203 | [ 0.42001969, 0.73158058, 0.15489659], 204 | [ 0.43261504, 0.73291878, 0.14773267], 205 | [ 0.44521378, 0.73417623, 0.14099043], 206 | [ 0.45777768, 0.73536072, 0.13474173], 207 | [ 0.47028295, 0.73647823, 0.1290455 ], 208 | [ 0.48268544, 0.73753985, 0.12397794], 209 | [ 0.49497773, 0.73854983, 0.11957878], 210 | [ 0.5071369 , 0.73951621, 0.11589589], 211 | [ 0.51913764, 0.74044827, 0.11296861], 212 | [ 0.53098624, 0.74134823, 0.11080237], 213 | [ 0.5426701 , 0.74222288, 0.10940411], 214 | [ 0.55417235, 0.74308049, 0.10876749], 215 | [ 0.56550904, 0.74392086, 0.10885609], 216 | [ 0.57667994, 0.74474781, 0.10963233], 217 | [ 0.58767906, 0.74556676, 0.11105089], 218 | [ 0.59850723, 0.74638125, 0.1130567 ], 219 | [ 0.609179 , 0.74719067, 0.11558918], 220 | [ 0.61969877, 0.74799703, 0.11859042], 221 | [ 0.63007148, 0.74880206, 0.12200388], 222 | [ 0.64030249, 0.74960714, 0.12577596], 223 | [ 0.65038997, 0.75041586, 0.12985641], 224 | [ 0.66034774, 0.75122659, 0.1342004 ], 225 | [ 0.67018264, 0.75203968, 0.13876817], 226 | [ 0.67990043, 0.75285567, 0.14352456], 227 | [ 0.68950682, 0.75367492, 0.14843886], 228 | [ 0.69900745, 0.75449768, 0.15348445], 229 | [ 0.70840781, 0.75532408, 0.15863839], 230 | [ 0.71771325, 0.75615416, 0.16388098], 231 | [ 0.72692898, 0.75698787, 0.1691954 ], 232 | [ 0.73606001, 0.75782508, 0.17456729], 233 | [ 0.74511119, 0.75866562, 0.17998443], 234 | [ 0.75408719, 0.75950924, 0.18543644], 235 | [ 0.76299247, 0.76035568, 0.19091446], 236 | [ 0.77183123, 0.76120466, 0.19641095], 237 | [ 0.78060815, 0.76205561, 0.20191973], 238 | [ 0.78932717, 0.76290815, 0.20743538], 239 | [ 0.79799213, 0.76376186, 0.21295324], 240 | [ 0.8066067 , 0.76461631, 0.21846931], 241 | [ 0.81517444, 0.76547101, 0.22398014], 242 | [ 0.82369877, 0.76632547, 0.2294827 ], 243 | [ 0.832183 , 0.7671792 , 0.2349743 ], 244 | [ 0.8406303 , 0.76803167, 0.24045248], 245 | [ 0.84904371, 0.76888236, 0.24591492], 246 | [ 0.85742615, 0.76973076, 0.25135935], 247 | [ 0.86578037, 0.77057636, 0.25678342], 248 | [ 0.87410891, 0.77141875, 0.2621846 ], 249 | [ 0.88241406, 0.77225757, 0.26755999], 250 | [ 0.89070781, 0.77308772, 0.27291122], 251 | [ 0.89898836, 0.77391069, 0.27823228], 252 | [ 0.90725475, 0.77472764, 0.28351668], 253 | [ 0.91550775, 0.77553893, 0.28875751], 254 | [ 0.92375722, 0.7763404 , 0.29395046], 255 | [ 0.9320227 , 0.77712286, 0.29909267], 256 | [ 0.94027715, 0.7779011 , 0.30415428], 257 | [ 0.94856742, 0.77865213, 0.3091325 ], 258 | [ 0.95686038, 0.7793949 , 0.31397459], 259 | [ 0.965222 , 0.7800975 , 0.31864342], 260 | [ 0.97365189, 0.78076521, 0.32301107], 261 | [ 0.98227405, 0.78134549, 0.32678728], 262 | [ 0.99136564, 0.78176999, 0.3281624 ], 263 | [ 0.99505988, 0.78542889, 0.32106514], 264 | [ 0.99594185, 0.79046888, 0.31648808], 265 | [ 0.99646635, 0.79566972, 0.31244662], 266 | [ 0.99681528, 0.80094905, 0.30858532], 267 | [ 0.9970578 , 0.80627441, 0.30479247], 268 | [ 0.99724883, 0.81161757, 0.30105328], 269 | [ 0.99736711, 0.81699344, 0.29725528], 270 | [ 0.99742254, 0.82239736, 0.29337235], 271 | [ 0.99744736, 0.82781159, 0.28943391], 272 | [ 0.99744951, 0.83323244, 0.28543062], 273 | [ 0.9973953 , 0.83867931, 0.2812767 ], 274 | [ 0.99727248, 0.84415897, 0.27692897], 275 | [ 0.99713953, 0.84963903, 0.27248698], 276 | [ 0.99698641, 0.85512544, 0.26791703], 277 | [ 0.99673736, 0.86065927, 0.26304767], 278 | [ 0.99652358, 0.86616957, 0.25813608], 279 | [ 0.99622774, 0.87171946, 0.25292044], 280 | [ 0.99590494, 0.87727931, 0.24750009], 281 | [ 0.99555225, 0.88285068, 0.2418514 ], 282 | [ 0.99513763, 0.8884501 , 0.23588062], 283 | [ 0.99471252, 0.89405076, 0.2296837 ], 284 | [ 0.99421873, 0.89968246, 0.2230963 ], 285 | [ 0.99370185, 0.90532165, 0.21619768], 286 | [ 0.99313786, 0.91098038, 0.2088926 ], 287 | [ 0.99250707, 0.91666811, 0.20108214], 288 | [ 0.99187888, 0.92235023, 0.19290417], 289 | [ 0.99110991, 0.92809686, 0.18387963], 290 | [ 0.99042108, 0.93379995, 0.17458127], 291 | [ 0.98958484, 0.93956962, 0.16420166], 292 | [ 0.98873988, 0.94533859, 0.15303117], 293 | [ 0.98784836, 0.95112482, 0.14074826], 294 | [ 0.98680727, 0.95697596, 0.12661626]] 295 | --------------------------------------------------------------------------------