convert.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. #! /usr/bin/env python
  2. """
  3. Reads Darknet config and weights and creates Keras model with TF backend.
  4. """
  5. import argparse
  6. import configparser
  7. import io
  8. import os
  9. from collections import defaultdict
  10. import numpy as np
  11. from keras import backend as K
  12. from keras.layers import (Conv2D, Input, ZeroPadding2D, Add,
  13. UpSampling2D, MaxPooling2D, Concatenate)
  14. from keras.layers.advanced_activations import LeakyReLU
  15. from keras.layers import BatchNormalization
  16. from keras.models import Model
  17. from keras.regularizers import l2
  18. from keras.utils.vis_utils import plot_model as plot
  19. parser = argparse.ArgumentParser(description='Darknet To Keras Converter.')
  20. parser.add_argument('config_path', help='Path to Darknet cfg file.')
  21. parser.add_argument('weights_path', help='Path to Darknet weights file.')
  22. parser.add_argument('output_path', help='Path to output Keras model file.')
  23. parser.add_argument(
  24. '-p',
  25. '--plot_model',
  26. help='Plot generated Keras model and save as image.',
  27. action='store_true')
  28. parser.add_argument(
  29. '-w',
  30. '--weights_only',
  31. help='Save as Keras weights file instead of model file.',
  32. action='store_true')
  33. def unique_config_sections(config_file):
  34. """Convert all config sections to have unique names.
  35. Adds unique suffixes to config sections for compability with configparser.
  36. """
  37. section_counters = defaultdict(int)
  38. output_stream = io.StringIO()
  39. with open(config_file) as fin:
  40. for line in fin:
  41. if line.startswith('['):
  42. section = line.strip().strip('[]')
  43. _section = section + '_' + str(section_counters[section])
  44. section_counters[section] += 1
  45. line = line.replace(section, _section)
  46. output_stream.write(line)
  47. output_stream.seek(0)
  48. return output_stream
  49. # %%
  50. def _main(args):
  51. config_path = os.path.expanduser(args.config_path)
  52. weights_path = os.path.expanduser(args.weights_path)
  53. assert config_path.endswith('.cfg'), '{} is not a .cfg file'.format(
  54. config_path)
  55. assert weights_path.endswith(
  56. '.weights'), '{} is not a .weights file'.format(weights_path)
  57. output_path = os.path.expanduser(args.output_path)
  58. assert output_path.endswith(
  59. '.h5'), 'output path {} is not a .h5 file'.format(output_path)
  60. output_root = os.path.splitext(output_path)[0]
  61. # Load weights and config.
  62. print('Loading weights.')
  63. weights_file = open(weights_path, 'rb')
  64. major, minor, revision = np.ndarray(
  65. shape=(3, ), dtype='int32', buffer=weights_file.read(12))
  66. if (major*10+minor)>=2 and major<1000 and minor<1000:
  67. seen = np.ndarray(shape=(1,), dtype='int64', buffer=weights_file.read(8))
  68. else:
  69. seen = np.ndarray(shape=(1,), dtype='int32', buffer=weights_file.read(4))
  70. print('Weights Header: ', major, minor, revision, seen)
  71. print('Parsing Darknet config.')
  72. unique_config_file = unique_config_sections(config_path)
  73. cfg_parser = configparser.ConfigParser()
  74. cfg_parser.read_file(unique_config_file)
  75. print('Creating Keras model.')
  76. input_layer = Input(shape=(None, None, 3))
  77. prev_layer = input_layer
  78. all_layers = []
  79. weight_decay = float(cfg_parser['net_0']['decay']
  80. ) if 'net_0' in cfg_parser.sections() else 5e-4
  81. count = 0
  82. out_index = []
  83. for section in cfg_parser.sections():
  84. print('Parsing section {}'.format(section))
  85. if section.startswith('convolutional'):
  86. filters = int(cfg_parser[section]['filters'])
  87. size = int(cfg_parser[section]['size'])
  88. stride = int(cfg_parser[section]['stride'])
  89. pad = int(cfg_parser[section]['pad'])
  90. activation = cfg_parser[section]['activation']
  91. batch_normalize = 'batch_normalize' in cfg_parser[section]
  92. padding = 'same' if pad == 1 and stride == 1 else 'valid'
  93. # Setting weights.
  94. # Darknet serializes convolutional weights as:
  95. # [bias/beta, [gamma, mean, variance], conv_weights]
  96. prev_layer_shape = K.int_shape(prev_layer)
  97. weights_shape = (size, size, prev_layer_shape[-1], filters)
  98. darknet_w_shape = (filters, weights_shape[2], size, size)
  99. weights_size = np.product(weights_shape)
  100. print('conv2d', 'bn'
  101. if batch_normalize else ' ', activation, weights_shape)
  102. conv_bias = np.ndarray(
  103. shape=(filters, ),
  104. dtype='float32',
  105. buffer=weights_file.read(filters * 4))
  106. count += filters
  107. if batch_normalize:
  108. bn_weights = np.ndarray(
  109. shape=(3, filters),
  110. dtype='float32',
  111. buffer=weights_file.read(filters * 12))
  112. count += 3 * filters
  113. bn_weight_list = [
  114. bn_weights[0], # scale gamma
  115. conv_bias, # shift beta
  116. bn_weights[1], # running mean
  117. bn_weights[2] # running var
  118. ]
  119. conv_weights = np.ndarray(
  120. shape=darknet_w_shape,
  121. dtype='float32',
  122. buffer=weights_file.read(weights_size * 4))
  123. count += weights_size
  124. # DarkNet conv_weights are serialized Caffe-style:
  125. # (out_dim, in_dim, height, width)
  126. # We would like to set these to Tensorflow order:
  127. # (height, width, in_dim, out_dim)
  128. conv_weights = np.transpose(conv_weights, [2, 3, 1, 0])
  129. conv_weights = [conv_weights] if batch_normalize else [
  130. conv_weights, conv_bias
  131. ]
  132. # Handle activation.
  133. act_fn = None
  134. if activation == 'leaky':
  135. pass # Add advanced activation later.
  136. elif activation != 'linear':
  137. raise ValueError(
  138. 'Unknown activation function `{}` in section {}'.format(
  139. activation, section))
  140. # Create Conv2D layer
  141. if stride>1:
  142. # Darknet uses left and top padding instead of 'same' mode
  143. prev_layer = ZeroPadding2D(((1,0),(1,0)))(prev_layer)
  144. conv_layer = (Conv2D(
  145. filters, (size, size),
  146. strides=(stride, stride),
  147. kernel_regularizer=l2(weight_decay),
  148. use_bias=not batch_normalize,
  149. weights=conv_weights,
  150. activation=act_fn,
  151. padding=padding))(prev_layer)
  152. if batch_normalize:
  153. conv_layer = (BatchNormalization(
  154. weights=bn_weight_list))(conv_layer)
  155. prev_layer = conv_layer
  156. if activation == 'linear':
  157. all_layers.append(prev_layer)
  158. elif activation == 'leaky':
  159. act_layer = LeakyReLU(alpha=0.1)(prev_layer)
  160. prev_layer = act_layer
  161. all_layers.append(act_layer)
  162. elif section.startswith('route'):
  163. ids = [int(i) for i in cfg_parser[section]['layers'].split(',')]
  164. layers = [all_layers[i] for i in ids]
  165. if len(layers) > 1:
  166. print('Concatenating route layers:', layers)
  167. concatenate_layer = Concatenate()(layers)
  168. all_layers.append(concatenate_layer)
  169. prev_layer = concatenate_layer
  170. else:
  171. skip_layer = layers[0] # only one layer to route
  172. all_layers.append(skip_layer)
  173. prev_layer = skip_layer
  174. elif section.startswith('maxpool'):
  175. size = int(cfg_parser[section]['size'])
  176. stride = int(cfg_parser[section]['stride'])
  177. all_layers.append(
  178. MaxPooling2D(
  179. pool_size=(size, size),
  180. strides=(stride, stride),
  181. padding='same')(prev_layer))
  182. prev_layer = all_layers[-1]
  183. elif section.startswith('shortcut'):
  184. index = int(cfg_parser[section]['from'])
  185. activation = cfg_parser[section]['activation']
  186. assert activation == 'linear', 'Only linear activation supported.'
  187. all_layers.append(Add()([all_layers[index], prev_layer]))
  188. prev_layer = all_layers[-1]
  189. elif section.startswith('upsample'):
  190. stride = int(cfg_parser[section]['stride'])
  191. assert stride == 2, 'Only stride=2 supported.'
  192. all_layers.append(UpSampling2D(stride)(prev_layer))
  193. prev_layer = all_layers[-1]
  194. elif section.startswith('yolo'):
  195. out_index.append(len(all_layers)-1)
  196. all_layers.append(None)
  197. prev_layer = all_layers[-1]
  198. elif section.startswith('net'):
  199. pass
  200. else:
  201. raise ValueError(
  202. 'Unsupported section header type: {}'.format(section))
  203. # Create and save model.
  204. if len(out_index)==0: out_index.append(len(all_layers)-1)
  205. model = Model(inputs=input_layer, outputs=[all_layers[i] for i in out_index])
  206. print(model.summary())
  207. if args.weights_only:
  208. model.save_weights('{}'.format(output_path))
  209. print('Saved Keras weights to {}'.format(output_path))
  210. else:
  211. model.save('{}'.format(output_path))
  212. print('Saved Keras model to {}'.format(output_path))
  213. # Check to see if all weights have been read.
  214. remaining_weights = len(weights_file.read()) / 4
  215. weights_file.close()
  216. print('Read {} of {} from Darknet weights.'.format(count, count +
  217. remaining_weights))
  218. if remaining_weights > 0:
  219. print('Warning: {} unused weights'.format(remaining_weights))
  220. if args.plot_model:
  221. plot(model, to_file='{}.png'.format(output_root), show_shapes=True)
  222. print('Saved model plot to {}.png'.format(output_root))
  223. if __name__ == '__main__':
  224. _main(parser.parse_args())