├── README.md
├── config.pickle
├── doc
├── fit_web.jpg
├── img-fit.jpg
└── mian_web.jpg
├── frcnn_api.py
├── issue_template.md
├── keras_frcnn
├── FixedBatchNormalization.py
├── RoiPoolingConv.py
├── __init__.py
├── __pycache__
│ ├── FixedBatchNormalization.cpython-35.pyc
│ ├── RoiPoolingConv.cpython-35.pyc
│ ├── __init__.cpython-35.pyc
│ ├── config.cpython-35.pyc
│ ├── data_augment.cpython-35.pyc
│ ├── data_generators.cpython-35.pyc
│ ├── losses.cpython-35.pyc
│ ├── pascal_voc_parser.cpython-35.pyc
│ ├── resnet.cpython-35.pyc
│ └── roi_helpers.cpython-35.pyc
├── config.py
├── data_augment.py
├── data_generators.py
├── losses.py
├── pascal_voc_parser.py
├── resnet.py
├── roi_helpers.py
├── simple_parser.py
└── vgg.py
├── measure_map.py
├── pic_tmp
├── 001.jpg
├── 002.jpg
├── 003.jpg
├── 006.jpg
└── 007.jpg
├── static
├── css
│ └── user_form.css
├── img
│ └── logo.png
└── js
│ └── jquery-1.6.2.js
├── templates
└── img_fit.html
├── test_frcnn.py
└── train_frcnn.py
/README.md:
--------------------------------------------------------------------------------
1 | # keras-frcnn-web
2 |
3 | 利用keras建立 faster rcnn网络,训练完成后,将检测api释放为rest接口,方便后端调用
4 |
5 | # linux 环境搭建
6 | ```Bash
7 | 1. wget https://www.python.org/ftp/python/3.5.2/Python-3.5.2.tar.xz
8 | 2. tar Jxvf Python-3.5.2.tar.xz
9 | 3. cd Python-3.5.2
10 | 4. ./configure --prefix=/usr/local/python3
11 | 5. make && make install # 成功提示:Ignoring ensurepip failure: pip 7.1.2 requires SSL/TLS
12 | 6. ln -s /usr/local/python3/bin/pip3.5 /usr/local/bin/pip3.5
13 | 7. ln -s /usr/local/python3/bin/python3.5 /usr/local/bin/python3
14 | 8. pip3.5 install --upgrade pip
15 | 9. pip3.5 install keras
16 | 10. pip3.5 install tensorflow
17 | 11. pip3.5 install flask
18 | 12. pip3.5 install flask_httpauth
19 | 13. pip3.5 install werkzeug
20 | 14.pip install opencv-python
21 | ```
22 |
23 | # 数据集格式
24 |
25 | 数据集为VOC2007,数据格式如下:
26 |
27 | 顶层目录
28 |
29 | Annotations —目标真值区域
30 |
31 | ImageSets —-类别标签
32 |
33 | JPEGImages —–图像
34 |
35 |
36 | 生成数据集,可参考博客
37 |
38 | https://blog.csdn.net/xvshu/article/details/81298625
39 |
40 | # 训练命令
41 |
42 | >python train_frcnn.py -p $your_train_voc2007_path
43 |
44 |
45 | # 测试命令
46 |
47 | >python test_frcnn.py -p $your_test_img_path
48 |
49 |
50 | 测试结果:
51 |
52 |
53 |
54 | # 发布api命令
55 |
56 | >python frcnn_api.py
57 |
58 |
59 | # 测试页面
60 | ## 传入图片
61 |
62 |
63 |
64 | ## 检测结果
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/config.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/config.pickle
--------------------------------------------------------------------------------
/doc/fit_web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/doc/fit_web.jpg
--------------------------------------------------------------------------------
/doc/img-fit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/doc/img-fit.jpg
--------------------------------------------------------------------------------
/doc/mian_web.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/doc/mian_web.jpg
--------------------------------------------------------------------------------
/frcnn_api.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, jsonify, abort, make_response, request, url_for,render_template
2 | from flask_httpauth import HTTPBasicAuth
3 | import test_frcnn
4 | import os
5 | from werkzeug.utils import secure_filename
6 | import cv2
7 | from scipy import misc
8 |
9 | app = Flask(__name__)
10 | # 图片最大为16M
11 | app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
12 | auth = HTTPBasicAuth()
13 |
14 | #设置post请求中获取的图片保存的路径
15 | UPLOAD_FOLDER = 'pic_tmp/'
16 | if not os.path.exists(UPLOAD_FOLDER):
17 | os.makedirs(UPLOAD_FOLDER)
18 | else:
19 | pass
20 | ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg'])
21 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
22 |
23 |
24 | @app.route('/')
25 | def index():
26 | return render_template("img_fit.html")
27 |
28 | @app.route('/img/fit', methods=['POST'])
29 | def face_insert():
30 | #分别获取post请求中的图片信息
31 | upload_files = request.files['imagefile']
32 | #从post请求图片保存到本地路径中
33 | file = upload_files
34 | filename = secure_filename(file.filename)
35 | file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
36 | image_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
37 | print(image_path)
38 | img = cv2.imread(os.path.expanduser(image_path))
39 | # img = misc.imread(os.path.expanduser(image_path), mode='RGB')
40 |
41 | return test_frcnn.tf_fit_img(img)
42 |
43 |
44 | @auth.get_password
45 | def get_password(username):
46 | if username == 'root':
47 | return 'root'
48 | return None
49 |
50 |
51 | @auth.error_handler
52 | def unauthorized():
53 | return make_response(jsonify({'error': 'Unauthorized access'}), 401)
54 |
55 |
56 | @app.errorhandler(400)
57 | def not_found(error):
58 | return make_response(jsonify({'error': 'Invalid data!'}), 400)
59 |
60 | if __name__ == '__main__':
61 | app.run(host='172.30.53.250', port=8099)
62 |
63 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 | NOTE: this repo is now deprecated. Instead, I recommend keras users use https://github.com/delftrobotics/keras-retinanet which runs faster and is more accurate.
2 |
--------------------------------------------------------------------------------
/keras_frcnn/FixedBatchNormalization.py:
--------------------------------------------------------------------------------
1 | from keras.engine import Layer, InputSpec
2 | from keras import initializers, regularizers
3 | from keras import backend as K
4 |
5 |
6 | class FixedBatchNormalization(Layer):
7 |
8 | def __init__(self, epsilon=1e-3, axis=-1,
9 | weights=None, beta_init='zero', gamma_init='one',
10 | gamma_regularizer=None, beta_regularizer=None, **kwargs):
11 |
12 | self.supports_masking = True
13 | self.beta_init = initializers.get(beta_init)
14 | self.gamma_init = initializers.get(gamma_init)
15 | self.epsilon = epsilon
16 | self.axis = axis
17 | self.gamma_regularizer = regularizers.get(gamma_regularizer)
18 | self.beta_regularizer = regularizers.get(beta_regularizer)
19 | self.initial_weights = weights
20 | super(FixedBatchNormalization, self).__init__(**kwargs)
21 |
22 | def build(self, input_shape):
23 | self.input_spec = [InputSpec(shape=input_shape)]
24 | shape = (input_shape[self.axis],)
25 |
26 | self.gamma = self.add_weight(shape,
27 | initializer=self.gamma_init,
28 | regularizer=self.gamma_regularizer,
29 | name='{}_gamma'.format(self.name),
30 | trainable=False)
31 | self.beta = self.add_weight(shape,
32 | initializer=self.beta_init,
33 | regularizer=self.beta_regularizer,
34 | name='{}_beta'.format(self.name),
35 | trainable=False)
36 | self.running_mean = self.add_weight(shape, initializer='zero',
37 | name='{}_running_mean'.format(self.name),
38 | trainable=False)
39 | self.running_std = self.add_weight(shape, initializer='one',
40 | name='{}_running_std'.format(self.name),
41 | trainable=False)
42 |
43 | if self.initial_weights is not None:
44 | self.set_weights(self.initial_weights)
45 | del self.initial_weights
46 |
47 | self.built = True
48 |
49 | def call(self, x, mask=None):
50 |
51 | assert self.built, 'Layer must be built before being called'
52 | input_shape = K.int_shape(x)
53 |
54 | reduction_axes = list(range(len(input_shape)))
55 | del reduction_axes[self.axis]
56 | broadcast_shape = [1] * len(input_shape)
57 | broadcast_shape[self.axis] = input_shape[self.axis]
58 |
59 | if sorted(reduction_axes) == range(K.ndim(x))[:-1]:
60 | x_normed = K.batch_normalization(
61 | x, self.running_mean, self.running_std,
62 | self.beta, self.gamma,
63 | epsilon=self.epsilon)
64 | else:
65 | # need broadcasting
66 | broadcast_running_mean = K.reshape(self.running_mean, broadcast_shape)
67 | broadcast_running_std = K.reshape(self.running_std, broadcast_shape)
68 | broadcast_beta = K.reshape(self.beta, broadcast_shape)
69 | broadcast_gamma = K.reshape(self.gamma, broadcast_shape)
70 | x_normed = K.batch_normalization(
71 | x, broadcast_running_mean, broadcast_running_std,
72 | broadcast_beta, broadcast_gamma,
73 | epsilon=self.epsilon)
74 |
75 | return x_normed
76 |
77 | def get_config(self):
78 | config = {'epsilon': self.epsilon,
79 | 'axis': self.axis,
80 | 'gamma_regularizer': self.gamma_regularizer.get_config() if self.gamma_regularizer else None,
81 | 'beta_regularizer': self.beta_regularizer.get_config() if self.beta_regularizer else None}
82 | base_config = super(FixedBatchNormalization, self).get_config()
83 | return dict(list(base_config.items()) + list(config.items()))
--------------------------------------------------------------------------------
/keras_frcnn/RoiPoolingConv.py:
--------------------------------------------------------------------------------
1 | from keras.engine.topology import Layer
2 | import keras.backend as K
3 |
4 | if K.backend() == 'tensorflow':
5 | import tensorflow as tf
6 |
7 | class RoiPoolingConv(Layer):
8 | '''ROI pooling layer for 2D inputs.
9 | See Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition,
10 | K. He, X. Zhang, S. Ren, J. Sun
11 | # Arguments
12 | pool_size: int
13 | Size of pooling region to use. pool_size = 7 will result in a 7x7 region.
14 | num_rois: number of regions of interest to be used
15 | # Input shape
16 | list of two 4D tensors [X_img,X_roi] with shape:
17 | X_img:
18 | `(1, channels, rows, cols)` if dim_ordering='th'
19 | or 4D tensor with shape:
20 | `(1, rows, cols, channels)` if dim_ordering='tf'.
21 | X_roi:
22 | `(1,num_rois,4)` list of rois, with ordering (x,y,w,h)
23 | # Output shape
24 | 3D tensor with shape:
25 | `(1, num_rois, channels, pool_size, pool_size)`
26 | '''
27 | def __init__(self, pool_size, num_rois, **kwargs):
28 |
29 | self.dim_ordering = K.image_dim_ordering()
30 | assert self.dim_ordering in {'tf', 'th'}, 'dim_ordering must be in {tf, th}'
31 |
32 | self.pool_size = pool_size
33 | self.num_rois = num_rois
34 |
35 | super(RoiPoolingConv, self).__init__(**kwargs)
36 |
37 | def build(self, input_shape):
38 | if self.dim_ordering == 'th':
39 | self.nb_channels = input_shape[0][1]
40 | elif self.dim_ordering == 'tf':
41 | self.nb_channels = input_shape[0][3]
42 |
43 | def compute_output_shape(self, input_shape):
44 | if self.dim_ordering == 'th':
45 | return None, self.num_rois, self.nb_channels, self.pool_size, self.pool_size
46 | else:
47 | return None, self.num_rois, self.pool_size, self.pool_size, self.nb_channels
48 |
49 | def call(self, x, mask=None):
50 |
51 | assert(len(x) == 2)
52 |
53 | img = x[0]
54 | rois = x[1]
55 |
56 | input_shape = K.shape(img)
57 |
58 | outputs = []
59 |
60 | for roi_idx in range(self.num_rois):
61 |
62 | x = rois[0, roi_idx, 0]
63 | y = rois[0, roi_idx, 1]
64 | w = rois[0, roi_idx, 2]
65 | h = rois[0, roi_idx, 3]
66 |
67 | row_length = w / float(self.pool_size)
68 | col_length = h / float(self.pool_size)
69 |
70 | num_pool_regions = self.pool_size
71 |
72 | #NOTE: the RoiPooling implementation differs between theano and tensorflow due to the lack of a resize op
73 | # in theano. The theano implementation is much less efficient and leads to long compile times
74 |
75 | if self.dim_ordering == 'th':
76 | for jy in range(num_pool_regions):
77 | for ix in range(num_pool_regions):
78 | x1 = x + ix * row_length
79 | x2 = x1 + row_length
80 | y1 = y + jy * col_length
81 | y2 = y1 + col_length
82 |
83 | x1 = K.cast(x1, 'int32')
84 | x2 = K.cast(x2, 'int32')
85 | y1 = K.cast(y1, 'int32')
86 | y2 = K.cast(y2, 'int32')
87 |
88 | x2 = x1 + K.maximum(1,x2-x1)
89 | y2 = y1 + K.maximum(1,y2-y1)
90 |
91 | new_shape = [input_shape[0], input_shape[1],
92 | y2 - y1, x2 - x1]
93 |
94 | x_crop = img[:, :, y1:y2, x1:x2]
95 | xm = K.reshape(x_crop, new_shape)
96 | pooled_val = K.max(xm, axis=(2, 3))
97 | outputs.append(pooled_val)
98 |
99 | elif self.dim_ordering == 'tf':
100 | x = K.cast(x, 'int32')
101 | y = K.cast(y, 'int32')
102 | w = K.cast(w, 'int32')
103 | h = K.cast(h, 'int32')
104 |
105 | rs = tf.image.resize_images(img[:, y:y+h, x:x+w, :], (self.pool_size, self.pool_size))
106 | outputs.append(rs)
107 |
108 | final_output = K.concatenate(outputs, axis=0)
109 | final_output = K.reshape(final_output, (1, self.num_rois, self.pool_size, self.pool_size, self.nb_channels))
110 |
111 | if self.dim_ordering == 'th':
112 | final_output = K.permute_dimensions(final_output, (0, 1, 4, 2, 3))
113 | else:
114 | final_output = K.permute_dimensions(final_output, (0, 1, 2, 3, 4))
115 |
116 | return final_output
117 |
118 |
119 | def get_config(self):
120 | config = {'pool_size': self.pool_size,
121 | 'num_rois': self.num_rois}
122 | base_config = super(RoiPoolingConv, self).get_config()
123 | return dict(list(base_config.items()) + list(config.items()))
124 |
--------------------------------------------------------------------------------
/keras_frcnn/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__init__.py
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/FixedBatchNormalization.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/FixedBatchNormalization.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/RoiPoolingConv.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/RoiPoolingConv.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/__init__.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/__init__.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/config.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/config.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/data_augment.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/data_augment.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/data_generators.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/data_generators.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/losses.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/losses.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/pascal_voc_parser.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/pascal_voc_parser.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/resnet.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/resnet.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/__pycache__/roi_helpers.cpython-35.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/keras_frcnn/__pycache__/roi_helpers.cpython-35.pyc
--------------------------------------------------------------------------------
/keras_frcnn/config.py:
--------------------------------------------------------------------------------
1 | from keras import backend as K
2 | import math
3 |
4 | class Config:
5 |
6 | def __init__(self):
7 |
8 | self.verbose = True
9 |
10 | self.network = 'resnet50'
11 |
12 | # setting for data augmentation
13 | self.use_horizontal_flips = False
14 | self.use_vertical_flips = False
15 | self.rot_90 = False
16 |
17 | # anchor box scales
18 | self.anchor_box_scales = [128, 256, 512]
19 |
20 | # anchor box ratios
21 | self.anchor_box_ratios = [[1, 1], [1./math.sqrt(2), 2./math.sqrt(2)], [2./math.sqrt(2), 1./math.sqrt(2)]]
22 |
23 | # size to resize the smallest side of the image
24 | self.im_size = 600
25 |
26 | # image channel-wise mean to subtract
27 | self.img_channel_mean = [103.939, 116.779, 123.68]
28 | self.img_scaling_factor = 1.0
29 |
30 | # number of ROIs at once
31 | self.num_rois = 4
32 |
33 | # stride at the RPN (this depends on the network configuration)
34 | self.rpn_stride = 16
35 |
36 | self.balanced_classes = False
37 |
38 | # scaling the stdev
39 | self.std_scaling = 4.0
40 | self.classifier_regr_std = [8.0, 8.0, 4.0, 4.0]
41 |
42 | # overlaps for RPN
43 | self.rpn_min_overlap = 0.3
44 | self.rpn_max_overlap = 0.7
45 |
46 | # overlaps for classifier ROIs
47 | self.classifier_min_overlap = 0.1
48 | self.classifier_max_overlap = 0.5
49 |
50 | # placeholder for the class mapping, automatically generated by the parser
51 | self.class_mapping = None
52 |
53 | #location of pretrained weights for the base network
54 | # weight files can be found at:
55 | # https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_th_dim_ordering_th_kernels_notop.h5
56 | # https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels_notop.h5
57 |
58 | self.model_path = 'model_frcnn.vgg.hdf5'
59 |
--------------------------------------------------------------------------------
/keras_frcnn/data_augment.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 | import copy
4 |
5 |
6 | def augment(img_data, config, augment=True):
7 | assert 'filepath' in img_data
8 | assert 'bboxes' in img_data
9 | assert 'width' in img_data
10 | assert 'height' in img_data
11 |
12 | img_data_aug = copy.deepcopy(img_data)
13 |
14 | img = cv2.imread(img_data_aug['filepath'])
15 |
16 | if augment:
17 | rows, cols = img.shape[:2]
18 |
19 | if config.use_horizontal_flips and np.random.randint(0, 2) == 0:
20 | img = cv2.flip(img, 1)
21 | for bbox in img_data_aug['bboxes']:
22 | x1 = bbox['x1']
23 | x2 = bbox['x2']
24 | bbox['x2'] = cols - x1
25 | bbox['x1'] = cols - x2
26 |
27 | if config.use_vertical_flips and np.random.randint(0, 2) == 0:
28 | img = cv2.flip(img, 0)
29 | for bbox in img_data_aug['bboxes']:
30 | y1 = bbox['y1']
31 | y2 = bbox['y2']
32 | bbox['y2'] = rows - y1
33 | bbox['y1'] = rows - y2
34 |
35 | if config.rot_90:
36 | angle = np.random.choice([0,90,180,270],1)[0]
37 | if angle == 270:
38 | img = np.transpose(img, (1,0,2))
39 | img = cv2.flip(img, 0)
40 | elif angle == 180:
41 | img = cv2.flip(img, -1)
42 | elif angle == 90:
43 | img = np.transpose(img, (1,0,2))
44 | img = cv2.flip(img, 1)
45 | elif angle == 0:
46 | pass
47 |
48 | for bbox in img_data_aug['bboxes']:
49 | x1 = bbox['x1']
50 | x2 = bbox['x2']
51 | y1 = bbox['y1']
52 | y2 = bbox['y2']
53 | if angle == 270:
54 | bbox['x1'] = y1
55 | bbox['x2'] = y2
56 | bbox['y1'] = cols - x2
57 | bbox['y2'] = cols - x1
58 | elif angle == 180:
59 | bbox['x2'] = cols - x1
60 | bbox['x1'] = cols - x2
61 | bbox['y2'] = rows - y1
62 | bbox['y1'] = rows - y2
63 | elif angle == 90:
64 | bbox['x1'] = rows - y2
65 | bbox['x2'] = rows - y1
66 | bbox['y1'] = x1
67 | bbox['y2'] = x2
68 | elif angle == 0:
69 | pass
70 |
71 | img_data_aug['width'] = img.shape[1]
72 | img_data_aug['height'] = img.shape[0]
73 | return img_data_aug, img
74 |
--------------------------------------------------------------------------------
/keras_frcnn/data_generators.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import
2 | import numpy as np
3 | import cv2
4 | import random
5 | import copy
6 | from . import data_augment
7 | import threading
8 | import itertools
9 |
10 |
11 | def union(au, bu, area_intersection):
12 | area_a = (au[2] - au[0]) * (au[3] - au[1])
13 | area_b = (bu[2] - bu[0]) * (bu[3] - bu[1])
14 | area_union = area_a + area_b - area_intersection
15 | return area_union
16 |
17 |
18 | def intersection(ai, bi):
19 | x = max(ai[0], bi[0])
20 | y = max(ai[1], bi[1])
21 | w = min(ai[2], bi[2]) - x
22 | h = min(ai[3], bi[3]) - y
23 | if w < 0 or h < 0:
24 | return 0
25 | return w*h
26 |
27 |
28 | def iou(a, b):
29 | # a and b should be (x1,y1,x2,y2)
30 |
31 | if a[0] >= a[2] or a[1] >= a[3] or b[0] >= b[2] or b[1] >= b[3]:
32 | return 0.0
33 |
34 | area_i = intersection(a, b)
35 | area_u = union(a, b, area_i)
36 |
37 | return float(area_i) / float(area_u + 1e-6)
38 |
39 |
40 | def get_new_img_size(width, height, img_min_side=600):
41 | if width <= height:
42 | f = float(img_min_side) / width
43 | resized_height = int(f * height)
44 | resized_width = img_min_side
45 | else:
46 | f = float(img_min_side) / height
47 | resized_width = int(f * width)
48 | resized_height = img_min_side
49 |
50 | return resized_width, resized_height
51 |
52 |
53 | class SampleSelector:
54 | def __init__(self, class_count):
55 | # ignore classes that have zero samples
56 | self.classes = [b for b in class_count.keys() if class_count[b] > 0]
57 | self.class_cycle = itertools.cycle(self.classes)
58 | self.curr_class = next(self.class_cycle)
59 |
60 | def skip_sample_for_balanced_class(self, img_data):
61 |
62 | class_in_img = False
63 |
64 | for bbox in img_data['bboxes']:
65 |
66 | cls_name = bbox['class']
67 |
68 | if cls_name == self.curr_class:
69 | class_in_img = True
70 | self.curr_class = next(self.class_cycle)
71 | break
72 |
73 | if class_in_img:
74 | return False
75 | else:
76 | return True
77 |
78 |
79 | def calc_rpn(C, img_data, width, height, resized_width, resized_height, img_length_calc_function):
80 |
81 | downscale = float(C.rpn_stride)
82 | anchor_sizes = C.anchor_box_scales
83 | anchor_ratios = C.anchor_box_ratios
84 | num_anchors = len(anchor_sizes) * len(anchor_ratios)
85 |
86 | # calculate the output map size based on the network architecture
87 |
88 | (output_width, output_height) = img_length_calc_function(resized_width, resized_height)
89 |
90 | n_anchratios = len(anchor_ratios)
91 |
92 | # initialise empty output objectives
93 | y_rpn_overlap = np.zeros((output_height, output_width, num_anchors))
94 | y_is_box_valid = np.zeros((output_height, output_width, num_anchors))
95 | y_rpn_regr = np.zeros((output_height, output_width, num_anchors * 4))
96 |
97 | num_bboxes = len(img_data['bboxes'])
98 |
99 | num_anchors_for_bbox = np.zeros(num_bboxes).astype(int)
100 | best_anchor_for_bbox = -1*np.ones((num_bboxes, 4)).astype(int)
101 | best_iou_for_bbox = np.zeros(num_bboxes).astype(np.float32)
102 | best_x_for_bbox = np.zeros((num_bboxes, 4)).astype(int)
103 | best_dx_for_bbox = np.zeros((num_bboxes, 4)).astype(np.float32)
104 |
105 | # get the GT box coordinates, and resize to account for image resizing
106 | gta = np.zeros((num_bboxes, 4))
107 | for bbox_num, bbox in enumerate(img_data['bboxes']):
108 | # get the GT box coordinates, and resize to account for image resizing
109 | gta[bbox_num, 0] = bbox['x1'] * (resized_width / float(width))
110 | gta[bbox_num, 1] = bbox['x2'] * (resized_width / float(width))
111 | gta[bbox_num, 2] = bbox['y1'] * (resized_height / float(height))
112 | gta[bbox_num, 3] = bbox['y2'] * (resized_height / float(height))
113 |
114 | # rpn ground truth
115 |
116 | for anchor_size_idx in range(len(anchor_sizes)):
117 | for anchor_ratio_idx in range(n_anchratios):
118 | anchor_x = anchor_sizes[anchor_size_idx] * anchor_ratios[anchor_ratio_idx][0]
119 | anchor_y = anchor_sizes[anchor_size_idx] * anchor_ratios[anchor_ratio_idx][1]
120 |
121 | for ix in range(output_width):
122 | # x-coordinates of the current anchor box
123 | x1_anc = downscale * (ix + 0.5) - anchor_x / 2
124 | x2_anc = downscale * (ix + 0.5) + anchor_x / 2
125 |
126 | # ignore boxes that go across image boundaries
127 | if x1_anc < 0 or x2_anc > resized_width:
128 | continue
129 |
130 | for jy in range(output_height):
131 |
132 | # y-coordinates of the current anchor box
133 | y1_anc = downscale * (jy + 0.5) - anchor_y / 2
134 | y2_anc = downscale * (jy + 0.5) + anchor_y / 2
135 |
136 | # ignore boxes that go across image boundaries
137 | if y1_anc < 0 or y2_anc > resized_height:
138 | continue
139 |
140 | # bbox_type indicates whether an anchor should be a target
141 | bbox_type = 'neg'
142 |
143 | # this is the best IOU for the (x,y) coord and the current anchor
144 | # note that this is different from the best IOU for a GT bbox
145 | best_iou_for_loc = 0.0
146 |
147 | for bbox_num in range(num_bboxes):
148 |
149 | # get IOU of the current GT box and the current anchor box
150 | curr_iou = iou([gta[bbox_num, 0], gta[bbox_num, 2], gta[bbox_num, 1], gta[bbox_num, 3]], [x1_anc, y1_anc, x2_anc, y2_anc])
151 | # calculate the regression targets if they will be needed
152 | if curr_iou > best_iou_for_bbox[bbox_num] or curr_iou > C.rpn_max_overlap:
153 | cx = (gta[bbox_num, 0] + gta[bbox_num, 1]) / 2.0
154 | cy = (gta[bbox_num, 2] + gta[bbox_num, 3]) / 2.0
155 | cxa = (x1_anc + x2_anc)/2.0
156 | cya = (y1_anc + y2_anc)/2.0
157 |
158 | tx = (cx - cxa) / (x2_anc - x1_anc)
159 | ty = (cy - cya) / (y2_anc - y1_anc)
160 | tw = np.log((gta[bbox_num, 1] - gta[bbox_num, 0]) / (x2_anc - x1_anc))
161 | th = np.log((gta[bbox_num, 3] - gta[bbox_num, 2]) / (y2_anc - y1_anc))
162 |
163 | if img_data['bboxes'][bbox_num]['class'] != 'bg':
164 |
165 | # all GT boxes should be mapped to an anchor box, so we keep track of which anchor box was best
166 | if curr_iou > best_iou_for_bbox[bbox_num]:
167 | best_anchor_for_bbox[bbox_num] = [jy, ix, anchor_ratio_idx, anchor_size_idx]
168 | best_iou_for_bbox[bbox_num] = curr_iou
169 | best_x_for_bbox[bbox_num,:] = [x1_anc, x2_anc, y1_anc, y2_anc]
170 | best_dx_for_bbox[bbox_num,:] = [tx, ty, tw, th]
171 |
172 | # we set the anchor to positive if the IOU is >0.7 (it does not matter if there was another better box, it just indicates overlap)
173 | if curr_iou > C.rpn_max_overlap:
174 | bbox_type = 'pos'
175 | num_anchors_for_bbox[bbox_num] += 1
176 | # we update the regression layer target if this IOU is the best for the current (x,y) and anchor position
177 | if curr_iou > best_iou_for_loc:
178 | best_iou_for_loc = curr_iou
179 | best_regr = (tx, ty, tw, th)
180 |
181 | # if the IOU is >0.3 and <0.7, it is ambiguous and no included in the objective
182 | if C.rpn_min_overlap < curr_iou < C.rpn_max_overlap:
183 | # gray zone between neg and pos
184 | if bbox_type != 'pos':
185 | bbox_type = 'neutral'
186 |
187 | # turn on or off outputs depending on IOUs
188 | if bbox_type == 'neg':
189 | y_is_box_valid[jy, ix, anchor_ratio_idx + n_anchratios * anchor_size_idx] = 1
190 | y_rpn_overlap[jy, ix, anchor_ratio_idx + n_anchratios * anchor_size_idx] = 0
191 | elif bbox_type == 'neutral':
192 | y_is_box_valid[jy, ix, anchor_ratio_idx + n_anchratios * anchor_size_idx] = 0
193 | y_rpn_overlap[jy, ix, anchor_ratio_idx + n_anchratios * anchor_size_idx] = 0
194 | elif bbox_type == 'pos':
195 | y_is_box_valid[jy, ix, anchor_ratio_idx + n_anchratios * anchor_size_idx] = 1
196 | y_rpn_overlap[jy, ix, anchor_ratio_idx + n_anchratios * anchor_size_idx] = 1
197 | start = 4 * (anchor_ratio_idx + n_anchratios * anchor_size_idx)
198 | y_rpn_regr[jy, ix, start:start+4] = best_regr
199 |
200 | # we ensure that every bbox has at least one positive RPN region
201 |
202 | for idx in range(num_anchors_for_bbox.shape[0]):
203 | if num_anchors_for_bbox[idx] == 0:
204 | # no box with an IOU greater than zero ...
205 | if best_anchor_for_bbox[idx, 0] == -1:
206 | continue
207 | y_is_box_valid[
208 | best_anchor_for_bbox[idx,0], best_anchor_for_bbox[idx,1], best_anchor_for_bbox[idx,2] + n_anchratios *
209 | best_anchor_for_bbox[idx,3]] = 1
210 | y_rpn_overlap[
211 | best_anchor_for_bbox[idx,0], best_anchor_for_bbox[idx,1], best_anchor_for_bbox[idx,2] + n_anchratios *
212 | best_anchor_for_bbox[idx,3]] = 1
213 | start = 4 * (best_anchor_for_bbox[idx,2] + n_anchratios * best_anchor_for_bbox[idx,3])
214 | y_rpn_regr[
215 | best_anchor_for_bbox[idx,0], best_anchor_for_bbox[idx,1], start:start+4] = best_dx_for_bbox[idx, :]
216 |
217 | y_rpn_overlap = np.transpose(y_rpn_overlap, (2, 0, 1))
218 | y_rpn_overlap = np.expand_dims(y_rpn_overlap, axis=0)
219 |
220 | y_is_box_valid = np.transpose(y_is_box_valid, (2, 0, 1))
221 | y_is_box_valid = np.expand_dims(y_is_box_valid, axis=0)
222 |
223 | y_rpn_regr = np.transpose(y_rpn_regr, (2, 0, 1))
224 | y_rpn_regr = np.expand_dims(y_rpn_regr, axis=0)
225 |
226 | pos_locs = np.where(np.logical_and(y_rpn_overlap[0, :, :, :] == 1, y_is_box_valid[0, :, :, :] == 1))
227 | neg_locs = np.where(np.logical_and(y_rpn_overlap[0, :, :, :] == 0, y_is_box_valid[0, :, :, :] == 1))
228 |
229 | num_pos = len(pos_locs[0])
230 |
231 | # one issue is that the RPN has many more negative than positive regions, so we turn off some of the negative
232 | # regions. We also limit it to 256 regions.
233 | num_regions = 256
234 |
235 | if len(pos_locs[0]) > num_regions/2:
236 | val_locs = random.sample(range(len(pos_locs[0])), len(pos_locs[0]) - num_regions/2)
237 | y_is_box_valid[0, pos_locs[0][val_locs], pos_locs[1][val_locs], pos_locs[2][val_locs]] = 0
238 | num_pos = num_regions/2
239 |
240 | if len(neg_locs[0]) + num_pos > num_regions:
241 | val_locs = random.sample(range(len(neg_locs[0])), len(neg_locs[0]) - num_pos)
242 | y_is_box_valid[0, neg_locs[0][val_locs], neg_locs[1][val_locs], neg_locs[2][val_locs]] = 0
243 |
244 | y_rpn_cls = np.concatenate([y_is_box_valid, y_rpn_overlap], axis=1)
245 | y_rpn_regr = np.concatenate([np.repeat(y_rpn_overlap, 4, axis=1), y_rpn_regr], axis=1)
246 |
247 | return np.copy(y_rpn_cls), np.copy(y_rpn_regr)
248 |
249 |
250 | class threadsafe_iter:
251 | """Takes an iterator/generator and makes it thread-safe by
252 | serializing call to the `next` method of given iterator/generator.
253 | """
254 | def __init__(self, it):
255 | self.it = it
256 | self.lock = threading.Lock()
257 |
258 | def __iter__(self):
259 | return self
260 |
261 | def next(self):
262 | with self.lock:
263 | return next(self.it)
264 |
265 |
266 | def threadsafe_generator(f):
267 | """A decorator that takes a generator function and makes it thread-safe.
268 | """
269 | def g(*a, **kw):
270 | return threadsafe_iter(f(*a, **kw))
271 | return g
272 |
273 | def get_anchor_gt(all_img_data, class_count, C, img_length_calc_function, backend, mode='train'):
274 |
275 | # The following line is not useful with Python 3.5, it is kept for the legacy
276 | # all_img_data = sorted(all_img_data)
277 |
278 | sample_selector = SampleSelector(class_count)
279 |
280 | while True:
281 | if mode == 'train':
282 | np.random.shuffle(all_img_data)
283 |
284 | for img_data in all_img_data:
285 | try:
286 |
287 | if C.balanced_classes and sample_selector.skip_sample_for_balanced_class(img_data):
288 | continue
289 |
290 | # read in image, and optionally add augmentation
291 |
292 | if mode == 'train':
293 | img_data_aug, x_img = data_augment.augment(img_data, C, augment=True)
294 | else:
295 | img_data_aug, x_img = data_augment.augment(img_data, C, augment=False)
296 |
297 | (width, height) = (img_data_aug['width'], img_data_aug['height'])
298 | (rows, cols, _) = x_img.shape
299 |
300 | assert cols == width
301 | assert rows == height
302 |
303 | # get image dimensions for resizing
304 | (resized_width, resized_height) = get_new_img_size(width, height, C.im_size)
305 |
306 | # resize the image so that smalles side is length = 600px
307 | x_img = cv2.resize(x_img, (resized_width, resized_height), interpolation=cv2.INTER_CUBIC)
308 |
309 | try:
310 | y_rpn_cls, y_rpn_regr = calc_rpn(C, img_data_aug, width, height, resized_width, resized_height, img_length_calc_function)
311 | except:
312 | continue
313 |
314 | # Zero-center by mean pixel, and preprocess image
315 |
316 | x_img = x_img[:,:, (2, 1, 0)] # BGR -> RGB
317 | x_img = x_img.astype(np.float32)
318 | x_img[:, :, 0] -= C.img_channel_mean[0]
319 | x_img[:, :, 1] -= C.img_channel_mean[1]
320 | x_img[:, :, 2] -= C.img_channel_mean[2]
321 | x_img /= C.img_scaling_factor
322 |
323 | x_img = np.transpose(x_img, (2, 0, 1))
324 | x_img = np.expand_dims(x_img, axis=0)
325 |
326 | y_rpn_regr[:, y_rpn_regr.shape[1]//2:, :, :] *= C.std_scaling
327 |
328 | if backend == 'tf':
329 | x_img = np.transpose(x_img, (0, 2, 3, 1))
330 | y_rpn_cls = np.transpose(y_rpn_cls, (0, 2, 3, 1))
331 | y_rpn_regr = np.transpose(y_rpn_regr, (0, 2, 3, 1))
332 |
333 | yield np.copy(x_img), [np.copy(y_rpn_cls), np.copy(y_rpn_regr)], img_data_aug
334 |
335 | except Exception as e:
336 | print(e)
337 | continue
338 |
--------------------------------------------------------------------------------
/keras_frcnn/losses.py:
--------------------------------------------------------------------------------
1 | from keras import backend as K
2 | from keras.objectives import categorical_crossentropy
3 |
4 | if K.image_dim_ordering() == 'tf':
5 | import tensorflow as tf
6 |
7 | lambda_rpn_regr = 1.0
8 | lambda_rpn_class = 1.0
9 |
10 | lambda_cls_regr = 1.0
11 | lambda_cls_class = 1.0
12 |
13 | epsilon = 1e-4
14 |
15 |
16 | def rpn_loss_regr(num_anchors):
17 | def rpn_loss_regr_fixed_num(y_true, y_pred):
18 | if K.image_dim_ordering() == 'th':
19 | x = y_true[:, 4 * num_anchors:, :, :] - y_pred
20 | x_abs = K.abs(x)
21 | x_bool = K.less_equal(x_abs, 1.0)
22 | return lambda_rpn_regr * K.sum(
23 | y_true[:, :4 * num_anchors, :, :] * (x_bool * (0.5 * x * x) + (1 - x_bool) * (x_abs - 0.5))) / K.sum(epsilon + y_true[:, :4 * num_anchors, :, :])
24 | else:
25 | x = y_true[:, :, :, 4 * num_anchors:] - y_pred
26 | x_abs = K.abs(x)
27 | x_bool = K.cast(K.less_equal(x_abs, 1.0), tf.float32)
28 |
29 | return lambda_rpn_regr * K.sum(
30 | y_true[:, :, :, :4 * num_anchors] * (x_bool * (0.5 * x * x) + (1 - x_bool) * (x_abs - 0.5))) / K.sum(epsilon + y_true[:, :, :, :4 * num_anchors])
31 |
32 | return rpn_loss_regr_fixed_num
33 |
34 |
35 | def rpn_loss_cls(num_anchors):
36 | def rpn_loss_cls_fixed_num(y_true, y_pred):
37 | if K.image_dim_ordering() == 'tf':
38 | return lambda_rpn_class * K.sum(y_true[:, :, :, :num_anchors] * K.binary_crossentropy(y_pred[:, :, :, :], y_true[:, :, :, num_anchors:])) / K.sum(epsilon + y_true[:, :, :, :num_anchors])
39 | else:
40 | return lambda_rpn_class * K.sum(y_true[:, :num_anchors, :, :] * K.binary_crossentropy(y_pred[:, :, :, :], y_true[:, num_anchors:, :, :])) / K.sum(epsilon + y_true[:, :num_anchors, :, :])
41 |
42 | return rpn_loss_cls_fixed_num
43 |
44 |
45 | def class_loss_regr(num_classes):
46 | def class_loss_regr_fixed_num(y_true, y_pred):
47 | x = y_true[:, :, 4*num_classes:] - y_pred
48 | x_abs = K.abs(x)
49 | x_bool = K.cast(K.less_equal(x_abs, 1.0), 'float32')
50 | return lambda_cls_regr * K.sum(y_true[:, :, :4*num_classes] * (x_bool * (0.5 * x * x) + (1 - x_bool) * (x_abs - 0.5))) / K.sum(epsilon + y_true[:, :, :4*num_classes])
51 | return class_loss_regr_fixed_num
52 |
53 |
54 | def class_loss_cls(y_true, y_pred):
55 | return lambda_cls_class * K.mean(categorical_crossentropy(y_true[0, :, :], y_pred[0, :, :]))
56 |
--------------------------------------------------------------------------------
/keras_frcnn/pascal_voc_parser.py:
--------------------------------------------------------------------------------
1 | import os
2 | import cv2
3 | import xml.etree.ElementTree as ET
4 | import numpy as np
5 | def get_data(input_path):
6 | all_imgs = []
7 |
8 | classes_count = {}
9 |
10 | class_mapping = {}
11 |
12 | visualise = False
13 |
14 | data_paths = [os.path.join(input_path,s) for s in ['VOC2007']]
15 |
16 |
17 | print('Parsing annotation files')
18 |
19 | for data_path in data_paths:
20 |
21 | annot_path = os.path.join(data_path, 'Annotations')
22 | imgs_path = os.path.join(data_path, 'JPEGImages')
23 | imgsets_path_trainval = os.path.join(data_path, 'ImageSets','Main','trainval.txt')
24 | imgsets_path_test = os.path.join(data_path, 'ImageSets','Main','test.txt')
25 |
26 | trainval_files = []
27 | test_files = []
28 | try:
29 | with open(imgsets_path_trainval) as f:
30 | for line in f:
31 | trainval_files.append(line.strip() + '.jpg')
32 | except Exception as e:
33 | print(e)
34 |
35 | try:
36 | with open(imgsets_path_test) as f:
37 | for line in f:
38 | test_files.append(line.strip() + '.jpg')
39 | except Exception as e:
40 | if data_path[-7:] == 'VOC2012':
41 | # this is expected, most pascal voc distibutions dont have the test.txt file
42 | pass
43 | else:
44 | print(e)
45 |
46 | annots = [os.path.join(annot_path, s) for s in os.listdir(annot_path)]
47 | idx = 0
48 | for annot in annots:
49 | try:
50 | idx += 1
51 |
52 | et = ET.parse(annot)
53 | element = et.getroot()
54 |
55 | element_objs = element.findall('object')
56 | element_filename = element.find('filename').text
57 | element_width = int(element.find('size').find('width').text)
58 | element_height = int(element.find('size').find('height').text)
59 |
60 | if len(element_objs) > 0:
61 | annotation_data = {'filepath': os.path.join(imgs_path, element_filename), 'width': element_width,
62 | 'height': element_height, 'bboxes': []}
63 |
64 | if element_filename in trainval_files:
65 | annotation_data['imageset'] = 'trainval'
66 | elif element_filename in test_files:
67 | annotation_data['imageset'] = 'test'
68 | else:
69 | annotation_data['imageset'] = 'trainval'
70 |
71 | for element_obj in element_objs:
72 | class_name = element_obj.find('name').text
73 | if class_name not in classes_count:
74 | classes_count[class_name] = 1
75 | else:
76 | classes_count[class_name] += 1
77 |
78 | if class_name not in class_mapping:
79 | class_mapping[class_name] = len(class_mapping)
80 |
81 | obj_bbox = element_obj.find('bndbox')
82 | x1 = int(round(float(obj_bbox.find('xmin').text)))
83 | y1 = int(round(float(obj_bbox.find('ymin').text)))
84 | x2 = int(round(float(obj_bbox.find('xmax').text)))
85 | y2 = int(round(float(obj_bbox.find('ymax').text)))
86 | difficulty = int(element_obj.find('difficult').text) == 1
87 | annotation_data['bboxes'].append(
88 | {'class': class_name, 'x1': x1, 'x2': x2, 'y1': y1, 'y2': y2, 'difficult': difficulty})
89 | all_imgs.append(annotation_data)
90 |
91 | if visualise:
92 | img = cv2.imread(annotation_data['filepath'])
93 | for bbox in annotation_data['bboxes']:
94 | cv2.rectangle(img, (bbox['x1'], bbox['y1']), (bbox[
95 | 'x2'], bbox['y2']), (0, 0, 255))
96 | cv2.imshow('img', img)
97 | cv2.waitKey(0)
98 |
99 | except Exception as e:
100 | print(e)
101 | continue
102 | return all_imgs, classes_count, class_mapping
103 |
--------------------------------------------------------------------------------
/keras_frcnn/resnet.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | '''ResNet50 model for Keras.
3 | # Reference:
4 | - [Deep Residual Learning for Image Recognition](https://arxiv.org/abs/1512.03385)
5 | Adapted from code contributed by BigMoyan.
6 | '''
7 |
8 | from __future__ import print_function
9 | from __future__ import absolute_import
10 |
11 | from keras.layers import Input, Add, Dense, Activation, Flatten, Convolution2D, MaxPooling2D, ZeroPadding2D, \
12 | AveragePooling2D, TimeDistributed
13 |
14 | from keras import backend as K
15 |
16 | from keras_frcnn.RoiPoolingConv import RoiPoolingConv
17 | from keras_frcnn.FixedBatchNormalization import FixedBatchNormalization
18 |
19 | def get_weight_path():
20 | if K.image_dim_ordering() == 'th':
21 | return 'resnet50_weights_th_dim_ordering_th_kernels_notop.h5'
22 | else:
23 | return 'resnet50_weights_tf_dim_ordering_tf_kernels.h5'
24 |
25 | def get_img_output_length(width, height):
26 | def get_output_length(input_length):
27 | # zero_pad
28 | input_length += 6
29 | # apply 4 strided convolutions
30 | filter_sizes = [7, 3, 1, 1]
31 | stride = 2
32 | for filter_size in filter_sizes:
33 | input_length = (input_length - filter_size + stride) // stride
34 | return input_length
35 |
36 | return get_output_length(width), get_output_length(height)
37 |
38 | def identity_block(input_tensor, kernel_size, filters, stage, block, trainable=True):
39 |
40 | nb_filter1, nb_filter2, nb_filter3 = filters
41 |
42 | if K.image_dim_ordering() == 'tf':
43 | bn_axis = 3
44 | else:
45 | bn_axis = 1
46 |
47 | conv_name_base = 'res' + str(stage) + block + '_branch'
48 | bn_name_base = 'bn' + str(stage) + block + '_branch'
49 |
50 | x = Convolution2D(nb_filter1, (1, 1), name=conv_name_base + '2a', trainable=trainable)(input_tensor)
51 | x = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '2a')(x)
52 | x = Activation('relu')(x)
53 |
54 | x = Convolution2D(nb_filter2, (kernel_size, kernel_size), padding='same', name=conv_name_base + '2b', trainable=trainable)(x)
55 | x = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '2b')(x)
56 | x = Activation('relu')(x)
57 |
58 | x = Convolution2D(nb_filter3, (1, 1), name=conv_name_base + '2c', trainable=trainable)(x)
59 | x = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '2c')(x)
60 |
61 | x = Add()([x, input_tensor])
62 | x = Activation('relu')(x)
63 | return x
64 |
65 |
66 | def identity_block_td(input_tensor, kernel_size, filters, stage, block, trainable=True):
67 |
68 | # identity block time distributed
69 |
70 | nb_filter1, nb_filter2, nb_filter3 = filters
71 | if K.image_dim_ordering() == 'tf':
72 | bn_axis = 3
73 | else:
74 | bn_axis = 1
75 |
76 | conv_name_base = 'res' + str(stage) + block + '_branch'
77 | bn_name_base = 'bn' + str(stage) + block + '_branch'
78 |
79 | x = TimeDistributed(Convolution2D(nb_filter1, (1, 1), trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '2a')(input_tensor)
80 | x = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '2a')(x)
81 | x = Activation('relu')(x)
82 |
83 | x = TimeDistributed(Convolution2D(nb_filter2, (kernel_size, kernel_size), trainable=trainable, kernel_initializer='normal',padding='same'), name=conv_name_base + '2b')(x)
84 | x = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '2b')(x)
85 | x = Activation('relu')(x)
86 |
87 | x = TimeDistributed(Convolution2D(nb_filter3, (1, 1), trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '2c')(x)
88 | x = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '2c')(x)
89 |
90 | x = Add()([x, input_tensor])
91 | x = Activation('relu')(x)
92 |
93 | return x
94 |
95 | def conv_block(input_tensor, kernel_size, filters, stage, block, strides=(2, 2), trainable=True):
96 |
97 | nb_filter1, nb_filter2, nb_filter3 = filters
98 | if K.image_dim_ordering() == 'tf':
99 | bn_axis = 3
100 | else:
101 | bn_axis = 1
102 |
103 | conv_name_base = 'res' + str(stage) + block + '_branch'
104 | bn_name_base = 'bn' + str(stage) + block + '_branch'
105 |
106 | x = Convolution2D(nb_filter1, (1, 1), strides=strides, name=conv_name_base + '2a', trainable=trainable)(input_tensor)
107 | x = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '2a')(x)
108 | x = Activation('relu')(x)
109 |
110 | x = Convolution2D(nb_filter2, (kernel_size, kernel_size), padding='same', name=conv_name_base + '2b', trainable=trainable)(x)
111 | x = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '2b')(x)
112 | x = Activation('relu')(x)
113 |
114 | x = Convolution2D(nb_filter3, (1, 1), name=conv_name_base + '2c', trainable=trainable)(x)
115 | x = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '2c')(x)
116 |
117 | shortcut = Convolution2D(nb_filter3, (1, 1), strides=strides, name=conv_name_base + '1', trainable=trainable)(input_tensor)
118 | shortcut = FixedBatchNormalization(axis=bn_axis, name=bn_name_base + '1')(shortcut)
119 |
120 | x = Add()([x, shortcut])
121 | x = Activation('relu')(x)
122 | return x
123 |
124 |
125 | def conv_block_td(input_tensor, kernel_size, filters, stage, block, input_shape, strides=(2, 2), trainable=True):
126 |
127 | # conv block time distributed
128 |
129 | nb_filter1, nb_filter2, nb_filter3 = filters
130 | if K.image_dim_ordering() == 'tf':
131 | bn_axis = 3
132 | else:
133 | bn_axis = 1
134 |
135 | conv_name_base = 'res' + str(stage) + block + '_branch'
136 | bn_name_base = 'bn' + str(stage) + block + '_branch'
137 |
138 | x = TimeDistributed(Convolution2D(nb_filter1, (1, 1), strides=strides, trainable=trainable, kernel_initializer='normal'), input_shape=input_shape, name=conv_name_base + '2a')(input_tensor)
139 | x = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '2a')(x)
140 | x = Activation('relu')(x)
141 |
142 | x = TimeDistributed(Convolution2D(nb_filter2, (kernel_size, kernel_size), padding='same', trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '2b')(x)
143 | x = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '2b')(x)
144 | x = Activation('relu')(x)
145 |
146 | x = TimeDistributed(Convolution2D(nb_filter3, (1, 1), kernel_initializer='normal'), name=conv_name_base + '2c', trainable=trainable)(x)
147 | x = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '2c')(x)
148 |
149 | shortcut = TimeDistributed(Convolution2D(nb_filter3, (1, 1), strides=strides, trainable=trainable, kernel_initializer='normal'), name=conv_name_base + '1')(input_tensor)
150 | shortcut = TimeDistributed(FixedBatchNormalization(axis=bn_axis), name=bn_name_base + '1')(shortcut)
151 |
152 | x = Add()([x, shortcut])
153 | x = Activation('relu')(x)
154 | return x
155 |
156 | def nn_base(input_tensor=None, trainable=False):
157 |
158 | # Determine proper input shape
159 | if K.image_dim_ordering() == 'th':
160 | input_shape = (3, None, None)
161 | else:
162 | input_shape = (None, None, 3)
163 |
164 | if input_tensor is None:
165 | img_input = Input(shape=input_shape)
166 | else:
167 | if not K.is_keras_tensor(input_tensor):
168 | img_input = Input(tensor=input_tensor, shape=input_shape)
169 | else:
170 | img_input = input_tensor
171 |
172 | if K.image_dim_ordering() == 'tf':
173 | bn_axis = 3
174 | else:
175 | bn_axis = 1
176 |
177 | x = ZeroPadding2D((3, 3))(img_input)
178 |
179 | x = Convolution2D(64, (7, 7), strides=(2, 2), name='conv1', trainable = trainable)(x)
180 | x = FixedBatchNormalization(axis=bn_axis, name='bn_conv1')(x)
181 | x = Activation('relu')(x)
182 | x = MaxPooling2D((3, 3), strides=(2, 2))(x)
183 |
184 | x = conv_block(x, 3, [64, 64, 256], stage=2, block='a', strides=(1, 1), trainable = trainable)
185 | x = identity_block(x, 3, [64, 64, 256], stage=2, block='b', trainable = trainable)
186 | x = identity_block(x, 3, [64, 64, 256], stage=2, block='c', trainable = trainable)
187 |
188 | x = conv_block(x, 3, [128, 128, 512], stage=3, block='a', trainable = trainable)
189 | x = identity_block(x, 3, [128, 128, 512], stage=3, block='b', trainable = trainable)
190 | x = identity_block(x, 3, [128, 128, 512], stage=3, block='c', trainable = trainable)
191 | x = identity_block(x, 3, [128, 128, 512], stage=3, block='d', trainable = trainable)
192 |
193 | x = conv_block(x, 3, [256, 256, 1024], stage=4, block='a', trainable = trainable)
194 | x = identity_block(x, 3, [256, 256, 1024], stage=4, block='b', trainable = trainable)
195 | x = identity_block(x, 3, [256, 256, 1024], stage=4, block='c', trainable = trainable)
196 | x = identity_block(x, 3, [256, 256, 1024], stage=4, block='d', trainable = trainable)
197 | x = identity_block(x, 3, [256, 256, 1024], stage=4, block='e', trainable = trainable)
198 | x = identity_block(x, 3, [256, 256, 1024], stage=4, block='f', trainable = trainable)
199 |
200 | return x
201 |
202 |
203 | def classifier_layers(x, input_shape, trainable=False):
204 |
205 | # compile times on theano tend to be very high, so we use smaller ROI pooling regions to workaround
206 | # (hence a smaller stride in the region that follows the ROI pool)
207 | if K.backend() == 'tensorflow':
208 | x = conv_block_td(x, 3, [512, 512, 2048], stage=5, block='a', input_shape=input_shape, strides=(2, 2), trainable=trainable)
209 | elif K.backend() == 'theano':
210 | x = conv_block_td(x, 3, [512, 512, 2048], stage=5, block='a', input_shape=input_shape, strides=(1, 1), trainable=trainable)
211 |
212 | x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='b', trainable=trainable)
213 | x = identity_block_td(x, 3, [512, 512, 2048], stage=5, block='c', trainable=trainable)
214 | x = TimeDistributed(AveragePooling2D((7, 7)), name='avg_pool')(x)
215 |
216 | return x
217 |
218 |
219 | def rpn(base_layers,num_anchors):
220 |
221 | x = Convolution2D(512, (3, 3), padding='same', activation='relu', kernel_initializer='normal', name='rpn_conv1')(base_layers)
222 |
223 | x_class = Convolution2D(num_anchors, (1, 1), activation='sigmoid', kernel_initializer='uniform', name='rpn_out_class')(x)
224 | x_regr = Convolution2D(num_anchors * 4, (1, 1), activation='linear', kernel_initializer='zero', name='rpn_out_regress')(x)
225 |
226 | return [x_class, x_regr, base_layers]
227 |
228 | def classifier(base_layers, input_rois, num_rois, nb_classes = 21, trainable=False):
229 |
230 | # compile times on theano tend to be very high, so we use smaller ROI pooling regions to workaround
231 |
232 | if K.backend() == 'tensorflow':
233 | pooling_regions = 14
234 | input_shape = (num_rois,14,14,1024)
235 | elif K.backend() == 'theano':
236 | pooling_regions = 7
237 | input_shape = (num_rois,1024,7,7)
238 |
239 | out_roi_pool = RoiPoolingConv(pooling_regions, num_rois)([base_layers, input_rois])
240 | out = classifier_layers(out_roi_pool, input_shape=input_shape, trainable=True)
241 |
242 | out = TimeDistributed(Flatten())(out)
243 |
244 | out_class = TimeDistributed(Dense(nb_classes, activation='softmax', kernel_initializer='zero'), name='dense_class_{}'.format(nb_classes))(out)
245 | # note: no regression target for bg class
246 | out_regr = TimeDistributed(Dense(4 * (nb_classes-1), activation='linear', kernel_initializer='zero'), name='dense_regress_{}'.format(nb_classes))(out)
247 | return [out_class, out_regr]
248 |
249 |
--------------------------------------------------------------------------------
/keras_frcnn/roi_helpers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pdb
3 | import math
4 | from . import data_generators
5 | import copy
6 |
7 |
8 | def calc_iou(R, img_data, C, class_mapping):
9 |
10 | bboxes = img_data['bboxes']
11 | (width, height) = (img_data['width'], img_data['height'])
12 | # get image dimensions for resizing
13 | (resized_width, resized_height) = data_generators.get_new_img_size(width, height, C.im_size)
14 |
15 | gta = np.zeros((len(bboxes), 4))
16 |
17 | for bbox_num, bbox in enumerate(bboxes):
18 | # get the GT box coordinates, and resize to account for image resizing
19 | gta[bbox_num, 0] = int(round(bbox['x1'] * (resized_width / float(width))/C.rpn_stride))
20 | gta[bbox_num, 1] = int(round(bbox['x2'] * (resized_width / float(width))/C.rpn_stride))
21 | gta[bbox_num, 2] = int(round(bbox['y1'] * (resized_height / float(height))/C.rpn_stride))
22 | gta[bbox_num, 3] = int(round(bbox['y2'] * (resized_height / float(height))/C.rpn_stride))
23 |
24 | x_roi = []
25 | y_class_num = []
26 | y_class_regr_coords = []
27 | y_class_regr_label = []
28 | IoUs = [] # for debugging only
29 |
30 | for ix in range(R.shape[0]):
31 | (x1, y1, x2, y2) = R[ix, :]
32 | x1 = int(round(x1))
33 | y1 = int(round(y1))
34 | x2 = int(round(x2))
35 | y2 = int(round(y2))
36 |
37 | best_iou = 0.0
38 | best_bbox = -1
39 | for bbox_num in range(len(bboxes)):
40 | curr_iou = data_generators.iou([gta[bbox_num, 0], gta[bbox_num, 2], gta[bbox_num, 1], gta[bbox_num, 3]], [x1, y1, x2, y2])
41 | if curr_iou > best_iou:
42 | best_iou = curr_iou
43 | best_bbox = bbox_num
44 |
45 | if best_iou < C.classifier_min_overlap:
46 | continue
47 | else:
48 | w = x2 - x1
49 | h = y2 - y1
50 | x_roi.append([x1, y1, w, h])
51 | IoUs.append(best_iou)
52 |
53 | if C.classifier_min_overlap <= best_iou < C.classifier_max_overlap:
54 | # hard negative example
55 | cls_name = 'bg'
56 | elif C.classifier_max_overlap <= best_iou:
57 | cls_name = bboxes[best_bbox]['class']
58 | cxg = (gta[best_bbox, 0] + gta[best_bbox, 1]) / 2.0
59 | cyg = (gta[best_bbox, 2] + gta[best_bbox, 3]) / 2.0
60 |
61 | cx = x1 + w / 2.0
62 | cy = y1 + h / 2.0
63 |
64 | tx = (cxg - cx) / float(w)
65 | ty = (cyg - cy) / float(h)
66 | tw = np.log((gta[best_bbox, 1] - gta[best_bbox, 0]) / float(w))
67 | th = np.log((gta[best_bbox, 3] - gta[best_bbox, 2]) / float(h))
68 | else:
69 | print('roi = {}'.format(best_iou))
70 | raise RuntimeError
71 |
72 | class_num = class_mapping[cls_name]
73 | class_label = len(class_mapping) * [0]
74 | class_label[class_num] = 1
75 | y_class_num.append(copy.deepcopy(class_label))
76 | coords = [0] * 4 * (len(class_mapping) - 1)
77 | labels = [0] * 4 * (len(class_mapping) - 1)
78 | if cls_name != 'bg':
79 | label_pos = 4 * class_num
80 | sx, sy, sw, sh = C.classifier_regr_std
81 | coords[label_pos:4+label_pos] = [sx*tx, sy*ty, sw*tw, sh*th]
82 | labels[label_pos:4+label_pos] = [1, 1, 1, 1]
83 | y_class_regr_coords.append(copy.deepcopy(coords))
84 | y_class_regr_label.append(copy.deepcopy(labels))
85 | else:
86 | y_class_regr_coords.append(copy.deepcopy(coords))
87 | y_class_regr_label.append(copy.deepcopy(labels))
88 |
89 | if len(x_roi) == 0:
90 | return None, None, None, None
91 |
92 | X = np.array(x_roi)
93 | Y1 = np.array(y_class_num)
94 | Y2 = np.concatenate([np.array(y_class_regr_label),np.array(y_class_regr_coords)],axis=1)
95 |
96 | return np.expand_dims(X, axis=0), np.expand_dims(Y1, axis=0), np.expand_dims(Y2, axis=0), IoUs
97 |
98 | def apply_regr(x, y, w, h, tx, ty, tw, th):
99 | try:
100 | cx = x + w/2.
101 | cy = y + h/2.
102 | cx1 = tx * w + cx
103 | cy1 = ty * h + cy
104 | w1 = math.exp(tw) * w
105 | h1 = math.exp(th) * h
106 | x1 = cx1 - w1/2.
107 | y1 = cy1 - h1/2.
108 | x1 = int(round(x1))
109 | y1 = int(round(y1))
110 | w1 = int(round(w1))
111 | h1 = int(round(h1))
112 |
113 | return x1, y1, w1, h1
114 |
115 | except ValueError:
116 | return x, y, w, h
117 | except OverflowError:
118 | return x, y, w, h
119 | except Exception as e:
120 | print(e)
121 | return x, y, w, h
122 |
123 | def apply_regr_np(X, T):
124 | try:
125 | x = X[0, :, :]
126 | y = X[1, :, :]
127 | w = X[2, :, :]
128 | h = X[3, :, :]
129 |
130 | tx = T[0, :, :]
131 | ty = T[1, :, :]
132 | tw = T[2, :, :]
133 | th = T[3, :, :]
134 |
135 | cx = x + w/2.
136 | cy = y + h/2.
137 | cx1 = tx * w + cx
138 | cy1 = ty * h + cy
139 |
140 | w1 = np.exp(tw.astype(np.float64)) * w
141 | h1 = np.exp(th.astype(np.float64)) * h
142 | x1 = cx1 - w1/2.
143 | y1 = cy1 - h1/2.
144 |
145 | x1 = np.round(x1)
146 | y1 = np.round(y1)
147 | w1 = np.round(w1)
148 | h1 = np.round(h1)
149 | return np.stack([x1, y1, w1, h1])
150 | except Exception as e:
151 | print(e)
152 | return X
153 |
154 | def non_max_suppression_fast(boxes, probs, overlap_thresh=0.9, max_boxes=300):
155 | # code used from here: http://www.pyimagesearch.com/2015/02/16/faster-non-maximum-suppression-python/
156 | # if there are no boxes, return an empty list
157 | if len(boxes) == 0:
158 | return []
159 |
160 | # grab the coordinates of the bounding boxes
161 | x1 = boxes[:, 0]
162 | y1 = boxes[:, 1]
163 | x2 = boxes[:, 2]
164 | y2 = boxes[:, 3]
165 |
166 | np.testing.assert_array_less(x1, x2)
167 | np.testing.assert_array_less(y1, y2)
168 |
169 | # if the bounding boxes integers, convert them to floats --
170 | # this is important since we'll be doing a bunch of divisions
171 | if boxes.dtype.kind == "i":
172 | boxes = boxes.astype("float")
173 |
174 | # initialize the list of picked indexes
175 | pick = []
176 |
177 | # calculate the areas
178 | area = (x2 - x1) * (y2 - y1)
179 |
180 | # sort the bounding boxes
181 | idxs = np.argsort(probs)
182 |
183 | # keep looping while some indexes still remain in the indexes
184 | # list
185 | while len(idxs) > 0:
186 | # grab the last index in the indexes list and add the
187 | # index value to the list of picked indexes
188 | last = len(idxs) - 1
189 | i = idxs[last]
190 | pick.append(i)
191 |
192 | # find the intersection
193 |
194 | xx1_int = np.maximum(x1[i], x1[idxs[:last]])
195 | yy1_int = np.maximum(y1[i], y1[idxs[:last]])
196 | xx2_int = np.minimum(x2[i], x2[idxs[:last]])
197 | yy2_int = np.minimum(y2[i], y2[idxs[:last]])
198 |
199 | ww_int = np.maximum(0, xx2_int - xx1_int)
200 | hh_int = np.maximum(0, yy2_int - yy1_int)
201 |
202 | area_int = ww_int * hh_int
203 |
204 | # find the union
205 | area_union = area[i] + area[idxs[:last]] - area_int
206 |
207 | # compute the ratio of overlap
208 | overlap = area_int/(area_union + 1e-6)
209 |
210 | # delete all indexes from the index list that have
211 | idxs = np.delete(idxs, np.concatenate(([last],
212 | np.where(overlap > overlap_thresh)[0])))
213 |
214 | if len(pick) >= max_boxes:
215 | break
216 |
217 | # return only the bounding boxes that were picked using the integer data type
218 | boxes = boxes[pick].astype("int")
219 | probs = probs[pick]
220 | return boxes, probs
221 |
222 | import time
223 | def rpn_to_roi(rpn_layer, regr_layer, C, dim_ordering, use_regr=True, max_boxes=300,overlap_thresh=0.9):
224 |
225 | regr_layer = regr_layer / C.std_scaling
226 |
227 | anchor_sizes = C.anchor_box_scales
228 | anchor_ratios = C.anchor_box_ratios
229 |
230 | assert rpn_layer.shape[0] == 1
231 |
232 | if dim_ordering == 'th':
233 | (rows,cols) = rpn_layer.shape[2:]
234 |
235 | elif dim_ordering == 'tf':
236 | (rows, cols) = rpn_layer.shape[1:3]
237 |
238 | curr_layer = 0
239 | if dim_ordering == 'tf':
240 | A = np.zeros((4, rpn_layer.shape[1], rpn_layer.shape[2], rpn_layer.shape[3]))
241 | elif dim_ordering == 'th':
242 | A = np.zeros((4, rpn_layer.shape[2], rpn_layer.shape[3], rpn_layer.shape[1]))
243 |
244 | for anchor_size in anchor_sizes:
245 | for anchor_ratio in anchor_ratios:
246 |
247 | anchor_x = (anchor_size * anchor_ratio[0])/C.rpn_stride
248 | anchor_y = (anchor_size * anchor_ratio[1])/C.rpn_stride
249 | if dim_ordering == 'th':
250 | regr = regr_layer[0, 4 * curr_layer:4 * curr_layer + 4, :, :]
251 | else:
252 | regr = regr_layer[0, :, :, 4 * curr_layer:4 * curr_layer + 4]
253 | regr = np.transpose(regr, (2, 0, 1))
254 |
255 | X, Y = np.meshgrid(np.arange(cols),np. arange(rows))
256 |
257 | A[0, :, :, curr_layer] = X - anchor_x/2
258 | A[1, :, :, curr_layer] = Y - anchor_y/2
259 | A[2, :, :, curr_layer] = anchor_x
260 | A[3, :, :, curr_layer] = anchor_y
261 |
262 | if use_regr:
263 | A[:, :, :, curr_layer] = apply_regr_np(A[:, :, :, curr_layer], regr)
264 |
265 | A[2, :, :, curr_layer] = np.maximum(1, A[2, :, :, curr_layer])
266 | A[3, :, :, curr_layer] = np.maximum(1, A[3, :, :, curr_layer])
267 | A[2, :, :, curr_layer] += A[0, :, :, curr_layer]
268 | A[3, :, :, curr_layer] += A[1, :, :, curr_layer]
269 |
270 | A[0, :, :, curr_layer] = np.maximum(0, A[0, :, :, curr_layer])
271 | A[1, :, :, curr_layer] = np.maximum(0, A[1, :, :, curr_layer])
272 | A[2, :, :, curr_layer] = np.minimum(cols-1, A[2, :, :, curr_layer])
273 | A[3, :, :, curr_layer] = np.minimum(rows-1, A[3, :, :, curr_layer])
274 |
275 | curr_layer += 1
276 |
277 | all_boxes = np.reshape(A.transpose((0, 3, 1,2)), (4, -1)).transpose((1, 0))
278 | all_probs = rpn_layer.transpose((0, 3, 1, 2)).reshape((-1))
279 |
280 | x1 = all_boxes[:, 0]
281 | y1 = all_boxes[:, 1]
282 | x2 = all_boxes[:, 2]
283 | y2 = all_boxes[:, 3]
284 |
285 | idxs = np.where((x1 - x2 >= 0) | (y1 - y2 >= 0))
286 |
287 | all_boxes = np.delete(all_boxes, idxs, 0)
288 | all_probs = np.delete(all_probs, idxs, 0)
289 |
290 | result = non_max_suppression_fast(all_boxes, all_probs, overlap_thresh=overlap_thresh, max_boxes=max_boxes)[0]
291 |
292 | return result
293 |
--------------------------------------------------------------------------------
/keras_frcnn/simple_parser.py:
--------------------------------------------------------------------------------
1 | import cv2
2 | import numpy as np
3 |
4 | def get_data(input_path):
5 | found_bg = False
6 | all_imgs = {}
7 |
8 | classes_count = {}
9 |
10 | class_mapping = {}
11 |
12 | visualise = True
13 |
14 | with open(input_path,'r') as f:
15 |
16 | print('Parsing annotation files')
17 |
18 | for line in f:
19 | line_split = line.strip().split(',')
20 | (filename,x1,y1,x2,y2,class_name) = line_split
21 |
22 | if class_name not in classes_count:
23 | classes_count[class_name] = 1
24 | else:
25 | classes_count[class_name] += 1
26 |
27 | if class_name not in class_mapping:
28 | if class_name == 'bg' and found_bg == False:
29 | print('Found class name with special name bg. Will be treated as a background region (this is usually for hard negative mining).')
30 | found_bg = True
31 | class_mapping[class_name] = len(class_mapping)
32 |
33 | if filename not in all_imgs:
34 | all_imgs[filename] = {}
35 |
36 | img = cv2.imread(filename)
37 | (rows,cols) = img.shape[:2]
38 | all_imgs[filename]['filepath'] = filename
39 | all_imgs[filename]['width'] = cols
40 | all_imgs[filename]['height'] = rows
41 | all_imgs[filename]['bboxes'] = []
42 | if np.random.randint(0,6) > 0:
43 | all_imgs[filename]['imageset'] = 'trainval'
44 | else:
45 | all_imgs[filename]['imageset'] = 'test'
46 |
47 | all_imgs[filename]['bboxes'].append({'class': class_name, 'x1': int(x1), 'x2': int(x2), 'y1': int(y1), 'y2': int(y2)})
48 |
49 |
50 | all_data = []
51 | for key in all_imgs:
52 | all_data.append(all_imgs[key])
53 |
54 | # make sure the bg class is last in the list
55 | if found_bg:
56 | if class_mapping['bg'] != len(class_mapping) - 1:
57 | key_to_switch = [key for key in class_mapping.keys() if class_mapping[key] == len(class_mapping)-1][0]
58 | val_to_switch = class_mapping['bg']
59 | class_mapping['bg'] = len(class_mapping) - 1
60 | class_mapping[key_to_switch] = val_to_switch
61 |
62 | return all_data, classes_count, class_mapping
63 |
64 |
65 |
--------------------------------------------------------------------------------
/keras_frcnn/vgg.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """VGG16 model for Keras.
3 | # Reference
4 | - [Very Deep Convolutional Networks for Large-Scale Image Recognition](https://arxiv.org/abs/1409.1556)
5 | """
6 | from __future__ import print_function
7 | from __future__ import absolute_import
8 | from __future__ import division
9 |
10 | import warnings
11 |
12 | from keras.models import Model
13 | from keras.layers import Flatten, Dense, Input, Conv2D, MaxPooling2D, Dropout
14 | from keras.layers import GlobalAveragePooling2D, GlobalMaxPooling2D, TimeDistributed
15 | from keras.engine.topology import get_source_inputs
16 | from keras.utils import layer_utils
17 | from keras.utils.data_utils import get_file
18 | from keras import backend as K
19 | from keras_frcnn.RoiPoolingConv import RoiPoolingConv
20 |
21 |
22 | def get_weight_path():
23 | if K.image_dim_ordering() == 'th':
24 | print('pretrained weights not available for VGG with theano backend')
25 | return
26 | else:
27 | return 'vgg16_weights_tf_dim_ordering_tf_kernels.h5'
28 |
29 |
30 | def get_img_output_length(width, height):
31 | def get_output_length(input_length):
32 | return input_length//16
33 |
34 | return get_output_length(width), get_output_length(height)
35 |
36 | def nn_base(input_tensor=None, trainable=False):
37 |
38 |
39 | # Determine proper input shape
40 | if K.image_dim_ordering() == 'th':
41 | input_shape = (3, None, None)
42 | else:
43 | input_shape = (None, None, 3)
44 |
45 | if input_tensor is None:
46 | img_input = Input(shape=input_shape)
47 | else:
48 | if not K.is_keras_tensor(input_tensor):
49 | img_input = Input(tensor=input_tensor, shape=input_shape)
50 | else:
51 | img_input = input_tensor
52 |
53 | if K.image_dim_ordering() == 'tf':
54 | bn_axis = 3
55 | else:
56 | bn_axis = 1
57 |
58 | # Block 1
59 | x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv1')(img_input)
60 | x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv2')(x)
61 | x = MaxPooling2D((2, 2), strides=(2, 2), name='block1_pool')(x)
62 |
63 | # Block 2
64 | x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
65 | x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv2')(x)
66 | x = MaxPooling2D((2, 2), strides=(2, 2), name='block2_pool')(x)
67 |
68 | # Block 3
69 | x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv1')(x)
70 | x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv2')(x)
71 | x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv3')(x)
72 | x = MaxPooling2D((2, 2), strides=(2, 2), name='block3_pool')(x)
73 |
74 | # Block 4
75 | x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv1')(x)
76 | x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv2')(x)
77 | x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv3')(x)
78 | x = MaxPooling2D((2, 2), strides=(2, 2), name='block4_pool')(x)
79 |
80 | # Block 5
81 | x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1')(x)
82 | x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2')(x)
83 | x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3')(x)
84 | # x = MaxPooling2D((2, 2), strides=(2, 2), name='block5_pool')(x)
85 |
86 | return x
87 |
88 | def rpn(base_layers, num_anchors):
89 |
90 | x = Conv2D(512, (3, 3), padding='same', activation='relu', kernel_initializer='normal', name='rpn_conv1')(base_layers)
91 |
92 | x_class = Conv2D(num_anchors, (1, 1), activation='sigmoid', kernel_initializer='uniform', name='rpn_out_class')(x)
93 | x_regr = Conv2D(num_anchors * 4, (1, 1), activation='linear', kernel_initializer='zero', name='rpn_out_regress')(x)
94 |
95 | return [x_class, x_regr, base_layers]
96 |
97 |
98 | def classifier(base_layers, input_rois, num_rois, nb_classes = 21, trainable=False):
99 |
100 | # compile times on theano tend to be very high, so we use smaller ROI pooling regions to workaround
101 |
102 | if K.backend() == 'tensorflow':
103 | pooling_regions = 7
104 | input_shape = (num_rois,7,7,512)
105 | elif K.backend() == 'theano':
106 | pooling_regions = 7
107 | input_shape = (num_rois,512,7,7)
108 |
109 | out_roi_pool = RoiPoolingConv(pooling_regions, num_rois)([base_layers, input_rois])
110 |
111 | out = TimeDistributed(Flatten(name='flatten'))(out_roi_pool)
112 | out = TimeDistributed(Dense(4096, activation='relu', name='fc1'))(out)
113 | out = TimeDistributed(Dropout(0.5))(out)
114 | out = TimeDistributed(Dense(4096, activation='relu', name='fc2'))(out)
115 | out = TimeDistributed(Dropout(0.5))(out)
116 |
117 | out_class = TimeDistributed(Dense(nb_classes, activation='softmax', kernel_initializer='zero'), name='dense_class_{}'.format(nb_classes))(out)
118 | # note: no regression target for bg class
119 | out_regr = TimeDistributed(Dense(4 * (nb_classes-1), activation='linear', kernel_initializer='zero'), name='dense_regress_{}'.format(nb_classes))(out)
120 |
121 | return [out_class, out_regr]
122 |
123 |
124 |
--------------------------------------------------------------------------------
/measure_map.py:
--------------------------------------------------------------------------------
1 | import os
2 | import cv2
3 | import numpy as np
4 | import sys
5 | import pickle
6 | from optparse import OptionParser
7 | import time
8 | from keras_frcnn import config
9 | import keras_frcnn.resnet as nn
10 | from keras import backend as K
11 | from keras.layers import Input
12 | from keras.models import Model
13 | from keras_frcnn import roi_helpers
14 | from keras_frcnn import data_generators
15 | from sklearn.metrics import average_precision_score
16 |
17 |
18 | def get_map(pred, gt, f):
19 | T = {}
20 | P = {}
21 | fx, fy = f
22 |
23 | for bbox in gt:
24 | bbox['bbox_matched'] = False
25 |
26 | pred_probs = np.array([s['prob'] for s in pred])
27 | box_idx_sorted_by_prob = np.argsort(pred_probs)[::-1]
28 |
29 | for box_idx in box_idx_sorted_by_prob:
30 | pred_box = pred[box_idx]
31 | pred_class = pred_box['class']
32 | pred_x1 = pred_box['x1']
33 | pred_x2 = pred_box['x2']
34 | pred_y1 = pred_box['y1']
35 | pred_y2 = pred_box['y2']
36 | pred_prob = pred_box['prob']
37 | if pred_class not in P:
38 | P[pred_class] = []
39 | T[pred_class] = []
40 | P[pred_class].append(pred_prob)
41 | found_match = False
42 |
43 | for gt_box in gt:
44 | gt_class = gt_box['class']
45 | gt_x1 = gt_box['x1']/fx
46 | gt_x2 = gt_box['x2']/fx
47 | gt_y1 = gt_box['y1']/fy
48 | gt_y2 = gt_box['y2']/fy
49 | gt_seen = gt_box['bbox_matched']
50 | if gt_class != pred_class:
51 | continue
52 | if gt_seen:
53 | continue
54 | iou = data_generators.iou((pred_x1, pred_y1, pred_x2, pred_y2), (gt_x1, gt_y1, gt_x2, gt_y2))
55 | if iou >= 0.5:
56 | found_match = True
57 | gt_box['bbox_matched'] = True
58 | break
59 | else:
60 | continue
61 |
62 | T[pred_class].append(int(found_match))
63 |
64 | for gt_box in gt:
65 | if not gt_box['bbox_matched'] and not gt_box['difficult']:
66 | if gt_box['class'] not in P:
67 | P[gt_box['class']] = []
68 | T[gt_box['class']] = []
69 |
70 | T[gt_box['class']].append(1)
71 | P[gt_box['class']].append(0)
72 |
73 | #import pdb
74 | #pdb.set_trace()
75 | return T, P
76 |
77 | sys.setrecursionlimit(40000)
78 |
79 | parser = OptionParser()
80 |
81 | parser.add_option("-p", "--path", dest="test_path", help="Path to test data.")
82 | parser.add_option("-n", "--num_rois", dest="num_rois",
83 | help="Number of ROIs per iteration. Higher means more memory use.", default=32)
84 | parser.add_option("--config_filename", dest="config_filename", help=
85 | "Location to read the metadata related to the training (generated when training).",
86 | default="config.pickle")
87 | parser.add_option("-o", "--parser", dest="parser", help="Parser to use. One of simple or pascal_voc",
88 | default="pascal_voc"),
89 |
90 | (options, args) = parser.parse_args()
91 |
92 | if not options.test_path: # if filename is not given
93 | parser.error('Error: path to test data must be specified. Pass --path to command line')
94 |
95 |
96 | if options.parser == 'pascal_voc':
97 | from keras_frcnn.pascal_voc_parser import get_data
98 | elif options.parser == 'simple':
99 | from keras_frcnn.simple_parser import get_data
100 | else:
101 | raise ValueError("Command line option parser must be one of 'pascal_voc' or 'simple'")
102 |
103 | config_output_filename = options.config_filename
104 |
105 | with open(config_output_filename, 'r') as f_in:
106 | C = pickle.load(f_in)
107 |
108 | # turn off any data augmentation at test time
109 | C.use_horizontal_flips = False
110 | C.use_vertical_flips = False
111 | C.rot_90 = False
112 |
113 | img_path = options.test_path
114 |
115 |
116 | def format_img(img, C):
117 | img_min_side = float(C.im_size)
118 | (height,width,_) = img.shape
119 |
120 | if width <= height:
121 | f = img_min_side/width
122 | new_height = int(f * height)
123 | new_width = int(img_min_side)
124 | else:
125 | f = img_min_side/height
126 | new_width = int(f * width)
127 | new_height = int(img_min_side)
128 | fx = width/float(new_width)
129 | fy = height/float(new_height)
130 | img = cv2.resize(img, (new_width, new_height), interpolation=cv2.INTER_CUBIC)
131 | img = img[:, :, (2, 1, 0)]
132 | img = img.astype(np.float32)
133 | img[:, :, 0] -= C.img_channel_mean[0]
134 | img[:, :, 1] -= C.img_channel_mean[1]
135 | img[:, :, 2] -= C.img_channel_mean[2]
136 | img /= C.img_scaling_factor
137 | img = np.transpose(img, (2, 0, 1))
138 | img = np.expand_dims(img, axis=0)
139 | return img, fx, fy
140 |
141 |
142 | class_mapping = C.class_mapping
143 |
144 | if 'bg' not in class_mapping:
145 | class_mapping['bg'] = len(class_mapping)
146 |
147 | class_mapping = {v: k for k, v in class_mapping.iteritems()}
148 | print(class_mapping)
149 | class_to_color = {class_mapping[v]: np.random.randint(0, 255, 3) for v in class_mapping}
150 | C.num_rois = int(options.num_rois)
151 |
152 | if K.image_dim_ordering() == 'th':
153 | input_shape_img = (3, None, None)
154 | input_shape_features = (1024, None, None)
155 | else:
156 | input_shape_img = (None, None, 3)
157 | input_shape_features = (None, None, 1024)
158 |
159 |
160 | img_input = Input(shape=input_shape_img)
161 | roi_input = Input(shape=(C.num_rois, 4))
162 | feature_map_input = Input(shape=input_shape_features)
163 |
164 | # define the base network (resnet here, can be VGG, Inception, etc)
165 | shared_layers = nn.nn_base(img_input, trainable=True)
166 |
167 | # define the RPN, built on the base layers
168 | num_anchors = len(C.anchor_box_scales) * len(C.anchor_box_ratios)
169 | rpn_layers = nn.rpn(shared_layers, num_anchors)
170 |
171 | classifier = nn.classifier(feature_map_input, roi_input, C.num_rois, nb_classes=len(class_mapping), trainable=True)
172 |
173 | model_rpn = Model(img_input, rpn_layers)
174 | model_classifier_only = Model([feature_map_input, roi_input], classifier)
175 |
176 | model_classifier = Model([feature_map_input, roi_input], classifier)
177 |
178 | model_rpn.load_weights(C.model_path, by_name=True)
179 | model_classifier.load_weights(C.model_path, by_name=True)
180 |
181 | model_rpn.compile(optimizer='sgd', loss='mse')
182 | model_classifier.compile(optimizer='sgd', loss='mse')
183 |
184 | all_imgs, _, _ = get_data(options.test_path)
185 | test_imgs = [s for s in all_imgs if s['imageset'] == 'test']
186 |
187 |
188 | T = {}
189 | P = {}
190 | for idx, img_data in enumerate(test_imgs):
191 | print('{}/{}'.format(idx,len(test_imgs)))
192 | st = time.time()
193 | filepath = img_data['filepath']
194 |
195 | img = cv2.imread(filepath)
196 |
197 | X, fx, fy = format_img(img, C)
198 |
199 | if K.image_dim_ordering() == 'tf':
200 | X = np.transpose(X, (0, 2, 3, 1))
201 |
202 | # get the feature maps and output from the RPN
203 | [Y1, Y2, F] = model_rpn.predict(X)
204 |
205 | R = roi_helpers.rpn_to_roi(Y1, Y2, C, K.image_dim_ordering(), overlap_thresh=0.7)
206 |
207 | # convert from (x1,y1,x2,y2) to (x,y,w,h)
208 | R[:, 2] -= R[:, 0]
209 | R[:, 3] -= R[:, 1]
210 |
211 | # apply the spatial pyramid pooling to the proposed regions
212 | bboxes = {}
213 | probs = {}
214 |
215 | for jk in range(R.shape[0] // C.num_rois + 1):
216 | ROIs = np.expand_dims(R[C.num_rois * jk:C.num_rois * (jk + 1), :], axis=0)
217 | if ROIs.shape[1] == 0:
218 | break
219 |
220 | if jk == R.shape[0] // C.num_rois:
221 | # pad R
222 | curr_shape = ROIs.shape
223 | target_shape = (curr_shape[0], C.num_rois, curr_shape[2])
224 | ROIs_padded = np.zeros(target_shape).astype(ROIs.dtype)
225 | ROIs_padded[:, :curr_shape[1], :] = ROIs
226 | ROIs_padded[0, curr_shape[1]:, :] = ROIs[0, 0, :]
227 | ROIs = ROIs_padded
228 |
229 | [P_cls, P_regr] = model_classifier_only.predict([F, ROIs])
230 |
231 | for ii in range(P_cls.shape[1]):
232 |
233 | if np.argmax(P_cls[0, ii, :]) == (P_cls.shape[2] - 1):
234 | continue
235 |
236 | cls_name = class_mapping[np.argmax(P_cls[0, ii, :])]
237 |
238 | if cls_name not in bboxes:
239 | bboxes[cls_name] = []
240 | probs[cls_name] = []
241 |
242 | (x, y, w, h) = ROIs[0, ii, :]
243 |
244 | cls_num = np.argmax(P_cls[0, ii, :])
245 | try:
246 | (tx, ty, tw, th) = P_regr[0, ii, 4 * cls_num:4 * (cls_num + 1)]
247 | tx /= C.classifier_regr_std[0]
248 | ty /= C.classifier_regr_std[1]
249 | tw /= C.classifier_regr_std[2]
250 | th /= C.classifier_regr_std[3]
251 | x, y, w, h = roi_helpers.apply_regr(x, y, w, h, tx, ty, tw, th)
252 | except:
253 | pass
254 | bboxes[cls_name].append([16 * x, 16 * y, 16 * (x + w), 16 * (y + h)])
255 | probs[cls_name].append(np.max(P_cls[0, ii, :]))
256 |
257 | all_dets = []
258 |
259 | for key in bboxes:
260 | bbox = np.array(bboxes[key])
261 |
262 | new_boxes, new_probs = roi_helpers.non_max_suppression_fast(bbox, np.array(probs[key]), overlap_thresh=0.5)
263 | for jk in range(new_boxes.shape[0]):
264 | (x1, y1, x2, y2) = new_boxes[jk, :]
265 | det = {'x1': x1, 'x2': x2, 'y1': y1, 'y2': y2, 'class': key, 'prob': new_probs[jk]}
266 | all_dets.append(det)
267 |
268 |
269 | print('Elapsed time = {}'.format(time.time() - st))
270 | t, p = get_map(all_dets, img_data['bboxes'], (fx, fy))
271 | for key in t.keys():
272 | if key not in T:
273 | T[key] = []
274 | P[key] = []
275 | T[key].extend(t[key])
276 | P[key].extend(p[key])
277 | all_aps = []
278 | for key in T.keys():
279 | ap = average_precision_score(T[key], P[key])
280 | print('{} AP: {}'.format(key, ap))
281 | all_aps.append(ap)
282 | print('mAP = {}'.format(np.mean(np.array(all_aps))))
283 | #print(T)
284 | #print(P)
--------------------------------------------------------------------------------
/pic_tmp/001.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/pic_tmp/001.jpg
--------------------------------------------------------------------------------
/pic_tmp/002.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/pic_tmp/002.jpg
--------------------------------------------------------------------------------
/pic_tmp/003.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/pic_tmp/003.jpg
--------------------------------------------------------------------------------
/pic_tmp/006.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/pic_tmp/006.jpg
--------------------------------------------------------------------------------
/pic_tmp/007.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/pic_tmp/007.jpg
--------------------------------------------------------------------------------
/static/css/user_form.css:
--------------------------------------------------------------------------------
1 | /*重置样式*/
2 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr,
3 | acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub
4 | , sup, tt, var, b, u, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody,
5 | tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header,
6 | hgroup, menu, nav, output, section, summary, time, mark, audio, video {
7 | margin: 0;
8 | padding: 0;
9 | border: 0
10 | }
11 | body {
12 | font-family: "微软雅黑";
13 | background: #f4f4f4;
14 | }
15 |
16 | /*header*/
17 | .header-line {
18 | width: 100%;
19 | height: 4px;
20 | background: #0dbfdd;
21 | }
22 |
23 | /*content*/
24 | .content {
25 | width: 28%;
26 | margin: 70px auto 0;
27 | text-align: center;
28 | }
29 | .content-logo {
30 | width: 80px;
31 | height: 80px;
32 | }
33 | .content-title {
34 | margin: 10px 0 25px 0;
35 | font-size: 2em;
36 | color: #747474;
37 | font-weight: normal;
38 | }
39 | .content-form {
40 | width: 100%;
41 | padding: 36px 0 20px;
42 | border: 1px solid #dedede;
43 | text-align: center;
44 | background: #fff;
45 | }
46 | .content-form form div {
47 | margin-bottom: 19px;
48 | }
49 | .content-form form .user,
50 | .content-form form .password {
51 | width: 77%;
52 | height: 20px;
53 | padding: 10px;
54 | font-size: 1em;
55 | border: 1px solid #cccbcb;
56 | border-radius: 7px;
57 | letter-spacing: 1px;
58 | }
59 | .content-form form input:focus {
60 | outline: none;
61 | -webkit-box-shadow: 0 0 5px #0dbfdd;
62 | box-shadow: 0 0 5px #0dbfdd;
63 | }
64 | .content-form-signup {
65 | width: 84%;
66 | margin: 0 auto;
67 | padding: 10px;
68 | border: 1px solid #cccbcb;
69 | border-radius: 7px;
70 | font-size: 1em;
71 | font-weight: bold;
72 | color: #fff;
73 | background: #0dbfdd;
74 | cursor: pointer;
75 | }
76 | .content-form-signup:hover {
77 | background: #0cb3d0;
78 | }
79 | .content-form-signup:focus {
80 | outline: none;
81 | border: 1px solid #0cb3d0;
82 | }
83 | .content-login-description {
84 | margin-top: 25px;
85 | line-height: 1.63636364;
86 | color: #747474;
87 | font-size: .91666667rem;
88 | }
89 | .content-login-link {
90 | font-size: 16px;
91 | color: #0dbfdd;
92 | text-decoration: none;
93 | }
94 |
95 | /*输入框无内容便提示*/
96 | #remind_1,
97 | #remind_2 {
98 | width: 76%;
99 | margin: 0 auto 2px;
100 | text-align: left;
101 | font-size: .2em;
102 | color: #f00;
103 | }
--------------------------------------------------------------------------------
/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xvshu/keras-frcnn-web/a828e7091f0fb894d4e31d997cb3b9c3b29b61a6/static/img/logo.png
--------------------------------------------------------------------------------
/templates/img_fit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |