소스 검색

Add ncnn prototype (#296)

Lutz Roeder 6 년 전
부모
커밋
640326ce6a
10개의 변경된 파일1230개의 추가작업 그리고 4개의 파일을 삭제
  1. 1 0
      Makefile
  2. 4 2
      electron-builder.yml
  3. 1 0
      setup.py
  4. 1 0
      src/app.js
  5. 1 1
      src/index.html
  6. 322 0
      src/ncnn-metadata.json
  7. 755 0
      src/ncnn.js
  8. 2 1
      src/view.js
  9. 91 0
      test/models.json
  10. 52 0
      tools/ncnn

+ 1 - 0
Makefile

@@ -27,6 +27,7 @@ update:
 	@./tools/darknet sync
 	@./tools/keras sync install metadata
 	@./tools/mxnet sync metadata
+	@./tools/ncnn sync
 	@./tools/onnx sync install schema metadata
 	@./tools/paddle sync schema
 	@./tools/pytorch sync install schema metadata

+ 4 - 2
electron-builder.yml

@@ -30,8 +30,6 @@ fileAssociations:
     ext: cmf
   - name: "CNTK Model"
     ext: dnn
-  - name: "Caffe Model"
-    ext: caffemodel
   - name: "TensorFlow Meta Graph"
     ext: meta
   - name: "TensorFlow Graph"
@@ -52,6 +50,8 @@ fileAssociations:
     ext: pbtxt
   - name: "Caffe Model"
     ext: pbtxt
+  - name: "Caffe Model"
+    ext: caffemodel
   - name: "TensorFlow Graph"
     ext: prototxt
   - name: "TensorFlow Saved Model"
@@ -62,6 +62,8 @@ fileAssociations:
     ext: prototxt
   - name: "Caffe Model"
     ext: prototxt
+  - name: "NCNN Model"
+    ext: param
   - name: "PyTorch Model"
     ext: pth
   - name: "PyTorch Model"

+ 1 - 0
setup.py

@@ -95,6 +95,7 @@ setuptools.setup(
             'darknet.js', 'darknet-metadata.json',
             'keras.js', 'keras-metadata.json', 'hdf5.js',
             'mxnet.js', 'mxnet-metadata.json',
+            'ncnn.js', 'ncnn-metadata.json',
             'openvino.js', 'openvino-metadata.json', 'openvino-parser.js',
             'paddle.js', 'paddle-metadata.json', 'paddle-proto.js',
             'pytorch.js', 'pytorch-metadata.json', 'pickle.js',

+ 1 - 0
src/app.js

@@ -126,6 +126,7 @@ class Application {
                     'mar', 'params',
                     'meta',
                     'tflite', 'lite', 'tfl', 'bin',
+                    'param',
                     'pt', 'pth', 't7',
                     'pkl', 'joblib',
                     'pbtxt', 'prototxt',

+ 1 - 1
src/index.html

@@ -68,7 +68,7 @@
         </svg>
     </a>
     <button id="open-file-button" class="center" style="top: 200px; width: 125px; opacity: 0;">Open Model...</button>
-    <input type="file" id="open-file-dialog" style="display:none" multiple="false" accept=".onnx, .pb, .meta, .tflite, .lite, .tfl, .bin, .keras, .h5, .hdf5, .json, .model, .mar, .params, .dnn, .cmf, .mlmodel, .caffemodel, .pbtxt, .prototxt, .pkl, .pt, .pth, .t7, .joblib, .cfg, .xml">
+    <input type="file" id="open-file-dialog" style="display:none" multiple="false" accept=".onnx, .pb, .meta, .tflite, .lite, .tfl, .bin, .keras, .h5, .hdf5, .json, .model, .mar, .params, .param, .dnn, .cmf, .mlmodel, .caffemodel, .pbtxt, .prototxt, .pkl, .pt, .pth, .t7, .joblib, .cfg, .xml">
     <!-- Preload fonts to workaround Chrome SVG layout issue -->
     <div style="font-weight: normal; color: rgba(0, 0, 0, 0.01); user-select: none;">.</div>
     <div style="font-weight: bold; color: rgba(0, 0, 0, 0.01); user-select: none;">.</div>

+ 322 - 0
src/ncnn-metadata.json

@@ -0,0 +1,322 @@
+[
+  {
+    "name": "InnerProduct",
+    "schema": {
+      "category": "Layer",
+      "attributes": [
+        { "name": "num_output", "type": "int32", "default": 0 },
+        { "name": "bias_term", "default": 0, "visible": false },
+        { "name": "weight_data_size", "default": 0, "visible": false },
+        { "name": "int8_scale_term", "default": 0, "id": "8" },
+        { "name": "activation_type", "default": 0, "id": "9" },
+        { "name": "activation_params", "default": 0, "id": "10" }
+      ]
+    }
+  },
+  {
+    "name": "Convolution",
+    "schema": {
+      "category": "Layer",
+      "attributes": [
+        { "name": "num_output", "default": 0 },
+        { "name": "kernel_w", "default": 0 },
+        { "name": "dilation_w", "default": 1 },
+        { "name": "stride_w", "default": 1 },
+        { "name": "pad_w", "default": 0 },
+        { "name": "bias_term", "default": 0, "visible": false },
+        { "name": "weight_data_size", "default": 0, "visible": false },
+        { "name": "int8_scale_term", "default": 0 },
+        { "name": "activation_type", "default": 0 },
+        { "name": "activation_params", "default": [] },
+        { "name": "kernel_h", "default": 0 },
+        { "name": "dilation_h", "default": 1 },
+        { "name": "stride_h", "default": 1 },
+        { "name": "pad_h", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "ConvolutionDepthWise",
+    "schema": {
+      "category": "Layer",
+      "attributes": [
+        { "name": "num_output", "default": 0 },
+        { "name": "kernel_w", "default": 0 },
+        { "name": "dilation_w", "default": 1 },
+        { "name": "stride_w", "default": 1 },
+        { "name": "pad_w", "default": 0 },
+        { "name": "bias_term", "default": 0, "visible": false },
+        { "name": "weight_data_size", "default": 0, "visible": false },
+        { "name": "int8_scale_term", "default": 0 },
+        { "name": "activation_type", "default": 0 },
+        { "name": "activation_params", "default": [] },
+        { "name": "kernel_h", "default": 0 },
+        { "name": "dilation_h", "default": 1 },
+        { "name": "stride_h", "default": 1 },
+        { "name": "pad_h", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "DeconvolutionDepthWise",
+    "schema": {
+      "category": "Layer",
+      "attributes": [
+        { "name": "num_output", "default": 0 },
+        { "name": "kernel_w", "default": 0 },
+        { "name": "dilation_w", "default": 1 },
+        { "name": "stride_w", "default": 1 },
+        { "name": "pad_w", "default": 0 },
+        { "name": "bias_term", "default": 0, "visible": false },
+        { "name": "weight_data_size", "default": 0, "visible": false },
+        { "name": "int8_scale_term", "default": 0 },
+        { "name": "activation_type", "default": 0 },
+        { "name": "activation_params", "default": [] },
+        { "name": "kernel_h", "default": 0 },
+        { "name": "dilation_h", "default": 1 },
+        { "name": "stride_h", "default": 1 },
+        { "name": "pad_h", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "ReLU",
+    "schema": {
+      "category": "Activation"
+    }
+  },
+  {
+    "name": "ReLU6",
+    "schema": {
+      "category": "Activation"
+    }
+  },
+  {
+    "name": "TanH",
+    "schema": {
+      "category": "Activation"
+    }
+  },
+  {
+    "name": "BatchNorm",
+    "schema": {
+      "category": "Normalization",
+      "attributes": [
+        { "name": "channels", "type": "int32", "default": 0 },
+        { "name": "eps", "type": "float32", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "Concat",
+    "schema": {
+      "category": "Tensor",
+      "attributes": [
+        { "name": "axis", "type": "int32", "default": 0 }
+      ],
+      "inputs": [
+        { "name": "input", "option": "variadic" }
+      ],
+      "outputs": [
+        { "name": "output" }
+      ]
+    }
+  },
+  {
+    "name": "Split",
+    "schema": {
+      "category": "Tensor",
+      "inputs": [
+        { "name": "input" }
+      ],
+      "outputs": [
+        { "name": "output", "option": "variadic" }
+      ]
+    }
+  },
+  {
+    "name": "Pooling",
+    "schema": {
+      "category": "Pool",
+      "attributes": [
+        { "name": "pooling_type", "default": 0 },
+        { "name": "kernel_w", "default": 0 },
+        { "name": "stride_w", "default": 1 },
+        { "name": "pad_left", "default": 0 },
+        { "name": "global_pooling", "default": 0 },
+        { "name": "pad_mode", "default": 0 },
+        { "name": "kernel_h", "default": 0, "id": 11 },
+        { "name": "stride_h", "default": 1, "id": 12 },
+        { "name": "pad_top",	"default": 0 , "id": 13 },
+        { "name": "pad_right",	"default": 0, "id": 14 },
+        { "name": "pad_bottom",	"default": 0, "id": 15 }
+      ]
+    }
+  },
+  {
+    "name": "Flatten",
+    "schema": {
+      "category": "Shape"
+    }
+  },
+  {
+    "name": "Reshape",
+    "schema": {
+      "category": "Shape",
+      "attributes": [
+        { "name": "w", "default": -233 },
+        { "name": "h", "default": -233 },
+        { "name": "c", "default": -233 },
+        { "name": "permute", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "Scale",
+    "schema": {
+      "category": "Layer",
+      "attributes": [
+        { "name": "scale_data_size", "default": 0, "visible": false },
+        { "name": "bias_term", "default": 0, "visible": false }
+      ]
+    }
+  },
+  {
+    "name": "Permute",
+    "schema": {
+      "category": "Shape",
+      "attributes": [
+        { "name": "order_type", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "Softmax",
+    "schema": {
+      "category": "Activation",
+      "attributes": [
+        { "name": "axis", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "Sigmoid",
+    "schema": {
+      "category": "Activation"
+    }
+  },
+  {
+    "name": "Eltwise",
+    "schema": {
+      "attributes": [
+        { "name": "op_type", "default": 0 },
+        { "name": "coeffs", "default": [] }
+      ]
+    }
+  },
+  {
+    "name": "LRN",
+    "schema": {
+      "category": "Normalization",
+      "attributes": [
+        { "name": "region_type", "default": 0 },
+        { "name": "local_size", "default": 5 },
+        { "name": "alpha", "default": 1 },
+        { "name": "beta", "default": 0.75 },
+        { "name": "bias", "default": 1 }
+      ]
+    }
+  },
+  {
+    "name": "PriorBox",
+    "schema": {
+      "attributes": [
+        { "name": "min_sizes", "default": [] },
+        { "name": "max_sizes", "default": [] },
+        { "name": "aspect_ratios", "default": [] },
+        { "name": "varainces0", "default": 0 },
+        { "name": "varainces1", "default": 0 },
+        { "name": "varainces2", "default": 0 },
+        { "name": "varainces3", "default": 0 },
+        { "name": "flip", "default": 1 },
+        { "name": "clip", "default": 0 },
+        { "name": "image_width", "default": 0 },
+        { "name": "image_height", "default": 0 },
+        { "name": "step_width", "default": -233 },
+        { "name": "step_height", "default": -233 },
+        { "name": "offset", "default": 0 }
+      ]
+    }
+  },
+  {
+    "name": "DetectionOutput",
+    "schema": {
+      "attributes": [
+        { "name": "num_class", "default": 0 },
+        { "name": "nms_threshold", "default": 0.05 },
+        { "name": "nms_top_k", "default": 300 },
+        { "name": "keep_top_k", "default": 100 },
+        { "name": "confidence_threshold", "default": 0.5 },
+        { "name": "varainces0", "default": 0.1 },
+        { "name": "varainces1", "default": 0.1 },
+        { "name": "varainces2", "default": 0.2 },
+        { "name": "varainces3", "default": 0.2 }
+      ]
+    }
+  },
+  {
+    "name": "ShuffleChannel",
+    "schema": {
+      "attributes": [
+        { "name": "group", "default": 1 }
+      ]
+    }
+  },
+  { 
+    "name": "Bias",
+    "schema": {
+      "category": "Layer",
+      "attributes": [
+        { "name": "bias_data_size", "default": 0, "visible": false }
+      ]
+    }
+  },
+  { 
+    "name": "PReLU",
+    "schema": {
+      "category": "Activation",
+      "attributes": [
+        { "name": "num_slope", "type": "int32", "default": 0, "visible": false }
+      ]
+    }
+  },
+  { 
+    "name": "Dropout",
+    "schema": {
+      "category": "Dropout",
+      "attributes": [
+        { "name": "scale", "type": "float32", "default": 1 }
+      ]
+    }
+  },
+  { 
+    "name": "Slice",
+    "schema": {
+      "category": "Tensor",
+      "attributes": [
+        { "name": "slices", "default": [] },
+        { "name": "axis", "default": 0 }
+      ]
+    }
+  },
+  { 
+    "name": "BinaryOp",
+    "schema": {
+      "attributes": [
+        { "name": "op_type", "type": "int32", "default": 0 },
+        { "name": "with_scalar", "type": "int32", "default": 0 },
+        { "name": "b", "type": "float32", "default": 0 }
+      ]
+    }
+  }
+]

+ 755 - 0
src/ncnn.js

@@ -0,0 +1,755 @@
+/* jshint esversion: 6 */
+/* eslint "indent": [ "error", 4, { "SwitchCase": 1 } ] */
+
+var ncnn = ncnn || {};
+
+// https://github.com/Tencent/ncnn/wiki/param-and-model-file-structure
+// https://github.com/Tencent/ncnn/wiki/operation-param-weight-table
+
+ncnn.ModelFactory = class {
+
+    match(context) {
+        var identifier = context.identifier.toLowerCase();
+        if (identifier.endsWith('param') || identifier.endsWith('.cfg.ncnn')) {
+            var text = context.text;
+            text = text.substring(0, Math.min(text.length, 32));
+            var signature = text.split('\n').shift().trim();
+            if (signature === '7767517') {
+                return true;
+            }
+        }
+        if (identifier.endsWith('bin') || identifier.endsWith('.weights.ncnn')) {
+            var buffer = context.buffer;
+            if (buffer.length > 4) {
+                var type = buffer[0] | buffer[1] << 8 | buffer[2] | buffer [3];
+                if (type === 0x00000000 || type === 0x00000001 || 
+                    type === 0x01306B47 || type === 0x000D4B38 || type === 0x0002C056) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    open(context, host) {
+        return ncnn.Metadata.open(host).then((metadata) => {
+            var identifier = context.identifier.toLowerCase();
+            var param = (text, bin) => {
+                try {
+                    return new ncnn.Model(metadata, text, bin);
+                }
+                catch (error) {
+                    var message = error && error.message ? error.message : error.toString();
+                    message = message.endsWith('.') ? message.substring(0, message.length - 1) : message;
+                    throw new ncnn.Error(message + " in '" + identifier + "'.");
+                }
+            };
+            if (identifier.endsWith('param') || identifier.endsWith('.cfg.ncnn')) {
+                var bin = null;
+                if (identifier.endsWith('param')) {
+                    bin = context.identifier.substring(0, context.identifier.length - 6) + '.bin';
+                }
+                else if (identifier.endsWith('.cfg.ncnn')) {
+                    bin = context.identifier.substring(0, context.identifier.length - 9) + '.weights.ncnn';
+                }
+                return context.request(bin, null).then((bin) => {
+                    return param(context.text, bin);
+                }).catch(() => {
+                    return param(context.text, null);
+                });
+            }
+            if (identifier.endsWith('bin') || identifier.endsWith('.weights.ncnn')) {
+                var text = null;
+                if (identifier.endsWith('bin')) {
+                    text = context.identifier.substring(0, context.identifier.length - 4) + '.param';
+                }
+                else if (identifier.endsWith('.weights.ncnn')) {
+                    text = context.identifier.substring(0, context.identifier.length - 13) + '.cfg.ncnn';
+                }
+                return context.request(text, 'utf-8').then((text) => {
+                    return param(text, context.buffer);
+                }).catch(() => {
+                    throw new ncnn.Error("Failed to load '" + text + "' in '" + identifier + "'.");
+                });
+            }
+        });
+    }
+}
+
+ncnn.Model = class {
+
+    constructor(metadata, param, bin) {
+        this._format = 'NCNN'
+        this._graphs = [];
+        this._graphs.push(new ncnn.Graph(metadata, param, bin));
+    }
+
+    get format() {
+        return this._format;
+    }
+
+    get graphs() {
+        return this._graphs;
+    }
+}
+
+ncnn.Graph = class {
+
+    constructor(metadata, param, bin) {
+        this._inputs = [];
+        this._outputs = [];
+        this._nodes = [];
+
+        var reader = new ncnn.BlobReader(bin);
+
+        var lines = param.split('\n');
+        var signature = lines.shift();
+        if (signature !== '7767517') {
+            throw new ncnn.Error('Invalid signature.')
+        }
+        var header = lines.shift().split(' ');
+        if (header.length !== 2) {
+            throw new ncnn.Error('Invalid header count.');
+        }
+
+        var layers = [];
+        var layer;
+        while (lines.length > 0) {
+            var line = lines.shift().trim();
+            if (line.length > 0) {
+                var columns = line.split(' ').filter((s) => s.length != 0);
+                layer = {};
+                layer.type = columns.shift();
+                layer.name = columns.shift();
+                var inputCount = parseInt(columns.shift(), 10);
+                var outputCount = parseInt(columns.shift(), 10);
+                layer.inputs = columns.splice(0, inputCount);
+                layer.outputs = columns.splice(0, outputCount);
+                layer.attr = {};
+                layer.attributes = columns.map((attribute) => {
+                    var list = attribute.split('=');
+                    var key = list[0].trim();
+                    var value = list[1].trim();
+                    var keyInt = parseInt(key, 10);
+                    if (key < 0) {
+                        value = value.split(',').map((v) => v.trim());
+                        value.shift();
+                        key = (-(keyInt + 23300)).toString();
+                    }
+                    layer.attr[key] = value;
+                    return { key: key, value: value };
+                });
+                layers.push(layer);
+            }
+        }
+
+        for (layer of layers) {
+            if (layer.type == 'Input') {
+                var dimensions = layer.attributes.map((a) => parseInt(a.value, 10));
+                var shape = new ncnn.TensorShape(dimensions);
+                var type = new ncnn.TensorType('float32', shape);
+                this._inputs.push(new ncnn.Parameter(layer.name, true, layer.outputs.map((output) => new ncnn.Argument(output, type, null))));
+            }
+            else {
+                this._nodes.push(new ncnn.Node(metadata, reader, layer));
+            }
+        }
+    }
+
+    get inputs() {
+        return this._inputs;
+    }
+
+    get outputs() {
+        return this._outputs;
+    }
+
+    get nodes() {
+        return this._nodes;
+    }
+}
+
+ncnn.Parameter = class {
+
+    constructor(name, visible, args) {
+        this._name = name;
+        this._visible = visible;
+        this._arguments = args;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get visible() {
+        return this._visible;
+    }
+
+    get arguments() {
+        return this._arguments;
+    }
+};
+
+ncnn.Argument = class {
+    constructor(id, type, initializer) {
+        this._id = id;
+        this._type = type || null;
+        this._initializer = initializer || null;
+    }
+
+    get id() {
+        return this._id;
+    }
+
+    get type() {
+        if (this._initializer) {
+            return this._initializer.type;
+        }
+        return this._type;
+    }
+
+    get initializer() {
+        return this._initializer;
+    }
+};
+
+ncnn.Node = class {
+
+    constructor(metadata, reader, layer) {
+        this._metadata = metadata;
+        this._inputs = [];
+        this._outputs = [];
+        this._attributes = [];
+        this._operator = layer.type;
+        this._name = layer.name;
+
+        var schema = metadata.getSchema(this._operator);
+
+        var attributeMetadata = {};
+        if (schema && schema.attributes) {
+            for (var i = 0; i < schema.attributes.length; i++) {
+                var id = schema.attributes[i].id || i.toString();
+                attributeMetadata[id] = schema.attributes[i];
+            }
+        }
+        for (var attribute of layer.attributes) {
+            var attributeSchema = attributeMetadata[attribute.key];
+            this._attributes.push(new ncnn.Attribute(attributeSchema, attribute.key, attribute.value));
+        }
+
+        var inputs = layer.inputs;
+        var inputIndex = 0;
+        if (schema && schema.inputs) {
+            for (var inputDef of schema.inputs) {
+                if (inputIndex < inputs.length || inputDef.option != 'optional') {
+                    var inputCount = (inputDef.option == 'variadic') ? (inputs.length - inputIndex) : 1;
+                    var inputArguments = inputs.slice(inputIndex, inputIndex + inputCount).filter((id) => id != '' || inputDef.option != 'optional').map((id) => {
+                        return new ncnn.Argument(id, null, null);
+                    });
+                    this._inputs.push(new ncnn.Parameter(inputDef.name, true, inputArguments));
+                    inputIndex += inputCount;
+                }
+            }
+        }
+        else {
+            this._inputs = this._inputs.concat(inputs.slice(inputIndex).map((input, index) => {
+                var inputName = ((inputIndex + index) == 0) ? 'input' : (inputIndex + index).toString();
+                return new ncnn.Parameter(inputName, true, [
+                    new ncnn.Argument(input, null, null)
+                ]);
+            }));
+        }
+
+        var outputs = layer.outputs;
+        var outputIndex = 0;
+        if (schema && schema.outputs) {
+            for (var outputDef of schema.outputs) {
+                if (outputIndex < outputs.length || outputDef.option != 'optional') {
+                    var outputCount = (outputDef.option == 'variadic') ? (outputs.length - outputIndex) : 1;
+                    var outputArguments = outputs.slice(outputIndex, outputIndex + outputCount).map((id) => {
+                        return new ncnn.Argument(id, null, null)
+                    });
+                    this._outputs.push(new ncnn.Parameter(outputDef.name, true, outputArguments));
+                    outputIndex += outputCount;
+                }
+            }
+        }
+        else {
+            this._outputs = this._outputs.concat(outputs.slice(outputIndex).map((output, index) => {
+                var outputName = ((outputIndex + index) == 0) ? 'output' : (outputIndex + index).toString();
+                return new ncnn.Parameter(outputName, true, [
+                    new ncnn.Argument(output, null, null)
+                ]);
+            }));
+        }
+
+        var num_output;
+        var weight_data_size;
+        var channels;
+        var scale_data_size;
+        var bias_data_size;
+        switch (this._operator) {
+            case 'BatchNorm':
+                channels = parseInt(layer.attr['0'] || 0, 10);
+                this._weight(reader, 'slope', [ channels ], 'float32');
+                this._weight(reader, 'mean', [ channels ], 'float32');
+                this._weight(reader, 'variance', [ channels ], 'float32');
+                this._weight(reader, 'bias', [ channels ], 'float32');
+                reader.next();
+                break;
+            case 'InnerProduct':
+                num_output = parseInt(layer.attr['0'] || 0, 10);
+                weight_data_size = parseInt(layer.attr['2'] || 0, 10);
+                this._weight(reader, 'weight', [ num_output, weight_data_size / num_output ]);
+                if (layer.attr['1'] == '1') {
+                    this._weight(reader, 'bias', [ num_output ], 'float32');
+                }
+                reader.next();
+                break;
+            case 'Bias':
+                bias_data_size = parseInt(layer.attr['0'] || 0, 10);
+                this._weight(reader, 'bias', [ bias_data_size ], 'float32');
+                reader.next();
+                break;
+            case 'Embed':
+                this._weight('weight');
+                this._weight('bias');
+                reader.next();
+                break;
+            case 'Convolution':
+            case 'ConvolutionDepthWise':
+            case 'Deconvolution':
+            case 'DeconvolutionDepthWise':
+                num_output = parseInt(layer.attr['0'] || 0, 10);
+                var kernel_w = parseInt(layer.attr['1'] || 0, 10);
+                var kernel_h = parseInt(layer.attr['1'] || kernel_w, 10);
+                weight_data_size = parseInt(layer.attr['6'] || 0, 10);
+                this._weight(reader, 'weight', [ num_output, weight_data_size / ( num_output * kernel_w * kernel_h), kernel_w, kernel_h ]);
+                if (layer.attr['5'] == '1') {
+                    this._weight(reader, 'bias', [ num_output ], 'float32');
+                }
+                reader.next();
+                break;
+            case 'Dequantize':
+                if (layer.attr['1'] == '1') {
+                    bias_data_size = parseInt(layer.attr['2'] || 0, 10);
+                    this._weight(reader, 'bias', [ bias_data_size ], 'float32');
+                }
+                reader.next();
+                break;
+            case 'Requantize':
+                if (layer.attr['2'] == '1') {
+                    bias_data_size = parseInt(layer.attr['3'] || 0, 10);
+                    this._weight(reader, 'bias', [ bias_data_size ], 'float32');
+                }
+                reader.next();
+                break;
+            case 'InstanceNorm':
+                channels = parseInt(layer.attr['0'] || 0, 10);
+                this._weight(reader, 'gamma', [ channels ], 'float32');
+                this._weight(reader, 'beta', [ channels ], 'float32');
+                reader.next();
+                break;
+            case 'Scale':
+                scale_data_size = parseInt(layer.attr['0'] || 0, 10);
+                if (scale_data_size != -233) {
+                    this._weight(reader, 'scale', [ scale_data_size], 'float32');
+                    if (layer.attr['1'] == '1') {
+                        this._weight(reader, 'bias', [ scale_data_size ], 'float32');
+                    }
+                    reader.next();
+                }
+                break;
+            case 'Normalize':
+                scale_data_size = parseInt(layer.attr['3'] || 0, 10);
+                this._weight(reader, 'scale', [ scale_data_size ], 'float32');
+                reader.next();
+                break;
+            case 'PReLU':
+                var num_slope = parseInt(layer.attr['0'] || 0, 10);
+                this._weight(reader, 'slope', [ num_slope ], 'float32');
+                reader.next();
+                break;
+        }
+    }
+
+    get operator() {
+        return this._operator;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get category() {
+        var schema = this._metadata.getSchema(this._operator);
+        return (schema && schema.category) ? schema.category : '';
+    }
+
+    get documentation() {
+        return '';
+    }
+
+    get attributes() {
+        return this._attributes;
+    }
+
+    get inputs() {
+        return this._inputs;
+    }
+
+    get outputs() {
+        return this._outputs;
+    }
+
+    _weight(reader, name, dimensions, dataType) {
+        dimensions = dimensions || null;
+        var data = null;
+        if (dimensions) {
+            var size = 1;
+            for (var dimension of dimensions) {
+                size *= dimension;
+            }
+            if (!dataType) {
+                dataType = reader.dataType;
+            }
+            data = reader.read(size, dataType);
+        }
+        else {
+            dataType = dataType || '?';
+            reader.dispose();
+        }
+        this._inputs.push(new ncnn.Parameter(name, true, [
+            new ncnn.Argument('', null, new ncnn.Tensor(new ncnn.TensorType(dataType, new ncnn.TensorShape(dimensions)), data))
+        ]));
+    }
+}
+
+ncnn.Attribute = class {
+
+    constructor(schema, key, value) {
+        this._type = '';
+        this._name = key;
+        this._value = value;
+        if (schema) {
+            this._name = schema.name;
+            if (schema.type) {
+                this._type = schema.type;
+            }
+            switch (this._type) {
+                case 'int32':
+                    this._value = parseInt(this._value, 10);
+                    break;
+                case 'float32':
+                    this._value = parseFloat(this._value);
+                    break;
+            }
+            if (Object.prototype.hasOwnProperty.call(schema, 'visible') && !schema.visible) {
+                this._visible = false;
+            }
+            else if (Object.prototype.hasOwnProperty.call(schema, 'default')) {
+                if (this._value == schema.default || (this._value && this._value.toString() == schema.default.toString())) {
+                    this._visible = false;
+                }
+            }
+        }
+    }
+
+    get type() {
+        return this._type;
+    }
+
+    get name() {
+        return this._name;
+    }
+
+    get value() {
+        return this._value;
+    }
+
+    get visible() {
+        return this._visible == false ? false : true;
+    }
+}
+
+ncnn.Tensor = class {
+
+    constructor(type, data) {
+        this._type = type;
+        this._data = data;
+    }
+
+    get kind() {
+        return 'Weight';
+    }
+
+    get type() {
+        return this._type;
+    }
+
+    get state() {
+        return this._context().state || null;
+    }
+
+    get value() {
+        var context = this._context();
+        if (context.state) {
+            return null;
+        }
+        context.limit = Number.MAX_SAFE_INTEGER;
+        return this._decode(context, 0);
+    }
+
+    toString() {
+        var context = this._context();
+        if (context.state) {
+            return '';
+        }
+        context.limit = 10000;
+        var value = this._decode(context, 0);
+        return JSON.stringify(value, null, 4);
+    }
+
+    _context() {
+        var context = {};
+        context.index = 0;
+        context.count = 0;
+        context.state = null;
+
+        if (this._type.dataType == '?') {
+            context.state = 'Tensor has unknown data type.';
+            return context;
+        }
+        if (!this._type.shape) {
+            context.state = 'Tensor has no dimensions.';
+            return context;
+        }
+
+        if (!this._data) {
+            context.state = 'Tensor data is empty.';
+            return context;
+        }
+
+        switch (this._type.dataType) {
+            case 'float32':
+                context.data = new DataView(this._data.buffer, this._data.byteOffset, this._data.byteLength);
+                break;
+            default:
+                context.state = 'Tensor data type is not implemented.';
+                break;
+        }
+
+        context.dataType = this._type.dataType;
+        context.shape = this._type.shape.dimensions;
+        return context;
+    }
+
+    _decode(context, dimension) {
+        var shape = context.shape;
+        if (context.shape.length == 0) {
+            shape = [ 1 ];
+        }
+        var results = [];
+        var size = shape[dimension];
+        if (dimension == shape.length - 1) {
+            for (var i = 0; i < size; i++) {
+                if (context.count > context.limit) {
+                    results.push('...');
+                    return results;
+                }
+                switch (this._type.dataType)
+                {
+                    case 'float32':
+                        results.push(context.data.getFloat32(context.index, true));
+                        context.index += 4;
+                        context.count++;
+                        break;
+                }
+            }
+        }
+        else {
+            for (var j = 0; j < size; j++) {
+                if (context.count > context.limit) {
+                    results.push('...');
+                    return results;
+                }
+                results.push(this._decode(context, dimension + 1));
+            }
+        }
+        if (context.shape.length == 0) {
+            return results[0];
+        }
+        return results;
+    }
+
+}
+
+ncnn.TensorType = class {
+
+    constructor(dataType, shape) {
+        this._dataType = dataType || '?';
+        this._shape = shape;
+    }
+
+    get dataType() {
+        return this._dataType;
+    }
+
+    get shape()
+    {
+        return this._shape;
+    }
+
+    toString() {
+        return this._dataType + this._shape.toString();
+    }
+}
+
+ncnn.TensorShape = class {
+
+    constructor(dimensions) {
+        this._dimensions = dimensions;
+    }
+
+    get dimensions() {
+        return this._dimensions;
+    }
+
+    toString() {
+        return this._dimensions ? ('[' + this._dimensions.map((dimension) => dimension ? dimension.toString() : '?').join(',') + ']') : '';
+    }
+};
+
+ncnn.Metadata = class {
+
+    static open(host) {
+        if (ncnn.Metadata._metadata) {
+            return Promise.resolve(ncnn.Metadata._metadata);
+        }
+        return host.request(null, 'ncnn-metadata.json', 'utf-8').then((data) => {
+            ncnn.Metadata._metadata = new ncnn.Metadata(data);
+            return ncnn.Metadata._metadata;
+        }).catch(() => {
+            ncnn.Metadata._metadata = new ncnn.Metadata(null);
+            return ncnn.Metadata._metadatas;
+        });
+    }
+
+    constructor(data) {
+        this._map = {};
+        this._attributeCache = {};
+        if (data) {
+            var items = JSON.parse(data);
+            if (items) {
+                for (var item of items) {
+                    if (item.name && item.schema) {
+                        this._map[item.name] = item.schema;
+                    }
+                }
+            }
+        }
+    }
+
+    getSchema(operator) {
+        return this._map[operator] || null;
+    }
+
+    getAttributeSchema(operator, name) {
+        var map = this._attributeCache[operator];
+        if (!map) {
+            map = {};
+            var schema = this.getSchema(operator);
+            if (schema && schema.attributes && schema.attributes.length > 0) {
+                for (var attribute of schema.attributes) {
+                    map[attribute.name] = attribute;
+                }
+            }
+            this._attributeCache[operator] = map;
+        }
+        return map[name] || null;
+    }
+};
+
+ncnn.BlobReader = class {
+
+    constructor(buffer) {
+        this._buffer = buffer;
+        this._position = 0;
+    }
+
+    get dataType() {
+        if (!this._dataType && this._buffer && this._position + 4 < this._buffer.length) {
+            var f0 = this._buffer[this._position++];
+            var f1 = this._buffer[this._position++];
+            var f2 = this._buffer[this._position++];
+            var f3 = this._buffer[this._position++];
+            var type = f0 | f1 << 8 | f2 << 16 | f3 << 24;
+            switch (type) {
+                case 0x00000000:
+                    this._dataType = 'float32';
+                    break;
+                case 0x01306B47:
+                    this._dataType = 'float16';
+                    break;
+                case 0x000D4B38:
+                    this._dataType = 'int8';
+                    break;
+                case 0x00000001:
+                    this._dataType = 'qint8';
+                    break;
+                case 0x0002C056: // size * sizeof(float) - raw data with extra scaling
+                default:
+                    throw new ncnn.Error("Unknown weight type '" + type + "'.");
+            }
+        }
+        return this._dataType || '?';
+    }
+
+    read(size, dataType) {
+        if (this._buffer) {
+            dataType = dataType || this.dataType;
+            var position = this._position
+            switch (dataType) {
+                case 'float32': 
+                    size *= 4;
+                    this._position += size;
+                    return this._buffer.subarray(position, this._position);
+                case 'float16': 
+                    size *= 2;
+                    this._position += size;
+                    return this._buffer.subarray(position, this._position);
+                case 'int8': 
+                    this._position += size;
+                    return this._buffer.subarray(position, this._position);
+                case 'qint8':
+                    this._position += size + 1024;
+                    return null;
+                default:
+                    this.dispose();
+                    break;
+            }
+        }
+        return null;
+    }
+
+    next() {
+        this._dataType = null;
+    }
+
+    dispose() {
+        this._dataType = null;
+        this._buffer = null;
+    }
+}
+
+ncnn.Error = class extends Error {
+
+    constructor(message) {
+        super(message);
+        this.name = 'Error loading ncnn model.';
+    }
+};
+
+if (typeof module !== 'undefined' && typeof module.exports === 'object') {
+    module.exports.ModelFactory = ncnn.ModelFactory;
+}

+ 2 - 1
src/view.js

@@ -507,7 +507,7 @@ view.View = class {
                                     type.shape && 
                                     type.shape.dimensions && 
                                     Object.prototype.hasOwnProperty.call(type.shape.dimensions, 'length')) {
-                                    shape = '\u3008' + type.shape.dimensions.join('\u00D7') + '\u3009';
+                                    shape = '\u3008' + type.shape.dimensions.map((d) => d ? d : '?').join('\u00D7') + '\u3009';
                                     if (type.shape.dimensions.length == 0 && argument.initializer && !argument.initializer.state) {
                                         shape = argument.initializer.toString();
                                         if (shape && shape.length > 10) {
@@ -1120,6 +1120,7 @@ view.ModelFactoryService = class {
         this.register('./openvino', [ '.xml' ]);
         this.register('./darknet', [ '.cfg' ]);
         this.register('./paddle', [ '.paddle', '__model__' ]);
+        this.register('./ncnn', [ '.param', '.bin', '.cfg.ncnn', '.weights.ncnn' ]);
     }
 
     register(id, extensions) {

+ 91 - 0
test/models.json

@@ -2376,6 +2376,97 @@
     "target": "vgg19_bn.model",
     "source": "https://s3.amazonaws.com/mxnet-model-server/onnx-vgg19_bn/vgg19_bn.model"
   },
+  {
+    "type":   "ncnn",
+    "target": "darknet_yolov2/darknet_yolov2.cfg.ncnn,darknet_yolov2/darknet_yolov2.weights.ncnn",
+    "source": "https://github.com/00liujj/gen-ncnn-models/raw/master/tests/darknet_yolov2.cfg.ncnn,https://github.com/00liujj/gen-ncnn-models/raw/master/tests/darknet_yolov2.weights.ncnn",
+    "format": "NCNN",
+    "link":   "https://github.com/00liujj/gen-ncnn-models"
+  },
+  {
+    "type":   "ncnn",
+    "target": "frozen_pb/frozen.pb.weights.ncnn,frozen_pb/frozen.pb.cfg.ncnn",
+    "source": "https://github.com/00liujj/gen-ncnn-models/raw/master/tests/frozen.pb.cfg.ncnn,https://github.com/00liujj/gen-ncnn-models/raw/master/tests/frozen.pb.weights.ncnn",
+    "format": "NCNN",
+    "link":   "https://github.com/00liujj/gen-ncnn-models"
+  },
+  {
+    "type":   "ncnn",
+    "target": "googlenet/googlenet.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/googlenet.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "mobilefacenet/mobilefacenet.bin,mobilefacenet/mobilefacenet.param",
+    "source": "https://github.com/GRAYKEY/mobilefacenet_ncnn/blob/master/models/mobilefacenet.bin?raw=true,https://raw.githubusercontent.com/GRAYKEY/mobilefacenet_ncnn/master/models/mobilefacenet.param",
+    "format": "NCNN",
+    "link":   "https://github.com/GRAYKEY/mobilefacenet_ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "mobilenet_yolov3/mobilenet_yolov3.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/mobilenet_yolov3.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "mobilenet_yolov3/mobilenet_yolov3.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/mobilenet_yolov3.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "ncnn_yolo2/ncnn_yolo2_det3.bin,ncnn_yolo2/ncnn_yolo2_det3.param",
+    "source": "https://raw.githubusercontent.com/kyo055/ncnn_yolo2/master/models/det3.bin,https://raw.githubusercontent.com/kyo055/ncnn_yolo2/master/models/det3.param",
+    "format": "NCNN",
+    "link":   "https://github.com/kyo055/ncnn_yolo2"
+  },
+  {
+    "type":   "ncnn",
+    "target": "resnet18_int8/resnet18_int8.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/resnet18_int8.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "shufflenet/shufflenet.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/shufflenet.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "squeezenet/squeezenet.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/squeezenet.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "squeezenet_ssd_faces_ncnn/faces_wider_squeezenet.param,squeezenet_ssd_faces_ncnn/faces_wider_squeezenet.bin",
+    "source": "https://deepdetect.com/models/init/embedded/images/detection/squeezenet_ssd_faces_ncnn.tar.gz[faces_wider_squeezenet.param,faces_wider_squeezenet.bin]",
+    "format": "NCNN",
+    "link":   "https://www.deepdetect.com/models/faces_embedded_ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "squeezenet_v1.1/squeezenet_v1.1.bin",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/examples/squeezenet_v1.1.bin",
+    "error":  "Failed to load 'squeezenet_v1.1.param' in 'squeezenet_v1.1.bin'.",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
+  {
+    "type":   "ncnn",
+    "target": "vgg16/vgg16.param",
+    "source": "https://raw.githubusercontent.com/Tencent/ncnn/master/benchmark/vgg16.param",
+    "format": "NCNN",
+    "link":   "https://github.com/Tencent/ncnn"
+  },
   {
     "type":   "onnx",
     "target": "arcface-resnet100.onnx",

+ 52 - 0
tools/ncnn

@@ -0,0 +1,52 @@
+#!/bin/bash
+
+set -e
+
+root=$(cd $(dirname ${0})/..; pwd)
+src=${root}/src
+third_party=${root}/third_party
+tools=${root}/tools
+
+if [ $(which python3) ] && [ $(which pip3) ]; then
+    python="python3"
+    pip="pip3"
+else
+    python="python"
+    pip="pip"
+fi
+
+identifier=ncnn
+
+bold() {
+    echo "$(tty -s && tput bold)$1$(tty -s && tput sgr0)" 
+}
+
+git_sync() {
+    mkdir -p "${third_party}"
+    if [ -d "${third_party}/${1}" ]; then
+        git -C "${third_party}/${1}" fetch --quiet -p
+        git -C "${third_party}/${1}" reset --quiet --hard origin/master
+    else
+        git -C "${third_party}" clone --quiet --recursive ${2} ${1}
+    fi
+    git -C "${third_party}" submodule update --quiet --init
+}
+
+clean() {
+    bold "ncnn clean"
+    rm -rf ${third_party}/${identifier}
+}
+
+sync() {
+    bold "ncnn sync"
+    git_sync ncnn https://github.com/Tencent/ncnn.git
+}
+
+while [ "$#" != 0 ]; do
+    command="$1" && shift
+    case "${command}" in
+        "clean") clean;;
+        "sync") sync;;
+        "metadata") metadata;;
+    esac
+done