diff --git a/models/cv/face/facenet/ixrt/README.md b/models/cv/face/facenet/ixrt/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5d230fa054f9261f399cd223539265d75716a81a --- /dev/null +++ b/models/cv/face/facenet/ixrt/README.md @@ -0,0 +1,80 @@ +# FaceNet + +## Description + +Facenet is a facial recognition system originally proposed and developed by Google. It utilizes deep learning techniques, specifically convolutional neural networks (CNNs), to transform facial images into high-dimensional feature vectors. These feature vectors possess high discriminative power, enabling comparison and identification of different faces. The core idea of Facenet is to map faces into a multi-dimensional space of feature vectors, achieving efficient representation and recognition of faces. + +## Setup + +### Install + +```bash +pip3 install tensorflow +pip3 install onnxsim +pip3 install scikit-learn +pip3 install tf_slim +pip3 install tqdm +pip3 install pycuda +pip3 install onnx +pip3 install tabulate +pip3 install cv2 +pip3 install scipy==1.8.0 +pip3 install pycocotools +pip3 install opencv-python==4.6.0.66 +``` + +### Download + +Pretrained model: + +Dataset: to download the lfw dataset. + +### Model Conversion + +```bash +mkdir checkpoints +git clone https://github.com/timesler/facenet-pytorch +mv /Path/facenet/ixrt/tensorflow2pytorch.py facenet-pytorch +python3 /facenet-pytorch/tensorflow2pytorch.py \ + --facenet_weights_path ${CHECKPOINTS_DIR} \ + --facenet_pb_path ${FACENET_PB_DIR} \ + --onnx_save_name facenet_export.onnx +mv facenet_export.onnx ${CHECKPOINTS_DIR} +``` + +### Data preprocessing + +We need to adjust the image resolution of the original dataset to 160x160. For details, please refer to the following link: . This code relies on tensorflow 1.xx; If you encounter problems with TensorFlow version incompatibility during dataset processing, you can also download the preprocessed dataset from here: + +```bash +unzip facenet_datasets.zip +``` + +## Inference + +### FP16 + +Because there are differences in model export, it is necessary to verify the following information before executing inference: In deploy.py, "/last_bn/BatchNormalization_output_0" refers to the output name of the BatchNormalization node in the exported ONNX model, such as "1187". "/avgpool_1a/GlobalAveragePool_output_0" refers to the output name of the GlobalAveragePool node, such as "1178". Additionally, make sure to update "/last_bn/BatchNormalization_output_0" in build_engine.py to the corresponding name, such as "1187". + +```bash +# Accuracy +bash scripts/infer_facenet_fp16_accuracy.sh +# Performance +bash scripts/infer_facenet_fp16_performance.sh +``` + +### INT8 + +```bash +# Accuracy +bash scripts/infer_facenet_int8_accuracy.sh +# Performance +bash scripts/infer_facenet_int8_performance.sh +``` + +## Results + +Model |BatchSize |Precision |FPS |AUC |ACC +--------|-----------|----------|----------|----------|------------ +FaceNet | 64 | FP16 | 8751.15 | 0.999 | 0.986 +FaceNet | 64 | INT8 | 13505.33 | 0.999 | 0.986 diff --git a/models/cv/face/facenet/ixrt/build_engine.py b/models/cv/face/facenet/ixrt/build_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..74a62202defa50397cc4227da2181eebe10ab3e9 --- /dev/null +++ b/models/cv/face/facenet/ixrt/build_engine.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import cv2 +import argparse +import numpy as np + +import torch +import tensorrt + +import onnx +from onnx import helper +from onnx import TensorProto,numpy_helper +from load_ixrt_plugin import load_ixrt_plugin +load_ixrt_plugin() + +def add_facenet_norm(onnx_model): + norm = helper.make_node('FacenetNorm_IxRT', inputs=['/last_bn/BatchNormalization_output_0'] , outputs=['/Pow_1_output_0'], name='facenet_norm_1', size=512) + + onnx_model = onnx.load(onnx_model) + graph = onnx_model.graph + nodes = graph.node + graph.node.append(norm) + output = onnx.helper.make_tensor_value_info('/Pow_1_output_0', TensorProto.FLOAT, [64, 512, 1, 1]) + graph = onnx.helper.make_graph( + graph.node, + "facenet model", + graph.input, + [output], + graph.initializer + ) + info_model = onnx.helper.make_model(graph, producer_name="facenet") + info_model.opset_import[0].version = 11 + onnx.save(info_model, "tmp4.onnx") + +def main(config): + IXRT_LOGGER = tensorrt.Logger(tensorrt.Logger.WARNING) + builder = tensorrt.Builder(IXRT_LOGGER) + EXPLICIT_BATCH = 1 << (int)(tensorrt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) + network = builder.create_network(EXPLICIT_BATCH) + build_config = builder.create_builder_config() + print("start prepare...") + add_facenet_norm(config.model) + parser = tensorrt.OnnxParser(network, IXRT_LOGGER) + parser.parse_from_file("tmp4.onnx") + + precision = tensorrt.BuilderFlag.INT8 if config.precision == "int8" else tensorrt.BuilderFlag.FP16 + # print("precision : ", precision) + build_config.set_flag(precision) + + plan = builder.build_serialized_network(network, build_config) + engine_file_path = config.engine + with open(engine_file_path, "wb") as f: + f.write(plan) + os.remove("tmp4.onnx") + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--model", type=str) + parser.add_argument("--precision", type=str, choices=["float16", "int8", "float32"], default="int8", + help="The precision of datatype") + parser.add_argument("--engine", type=str, default=None) + args = parser.parse_args() + return args + +if __name__ == "__main__": + args = parse_args() + main(args) \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/common.py b/models/cv/face/facenet/ixrt/common.py new file mode 100644 index 0000000000000000000000000000000000000000..9db1327ad1531c452fb38182d747c81fc6f8eccf --- /dev/null +++ b/models/cv/face/facenet/ixrt/common.py @@ -0,0 +1,116 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import cv2 +import glob +import torch +import tensorrt +import numpy as np +import pycuda.driver as cuda + +from torch.utils.data import DataLoader, SubsetRandomSampler, SequentialSampler +from torchvision import datasets, transforms + +def create_engine_context(engine_path, logger): + with open(engine_path, "rb") as f: + runtime = tensorrt.Runtime(logger) + assert runtime + engine = runtime.deserialize_cuda_engine(f.read()) + assert engine + context = engine.create_execution_context() + assert context + + return engine, context + +def get_io_bindings(engine): + # Setup I/O bindings + inputs = [] + outputs = [] + allocations = [] + + for i in range(engine.num_bindings): + is_input = False + if engine.binding_is_input(i): + is_input = True + name = engine.get_binding_name(i) + dtype = engine.get_binding_dtype(i) + shape = engine.get_binding_shape(i) + if is_input: + batch_size = shape[0] + size = np.dtype(tensorrt.nptype(dtype)).itemsize + for s in shape: + size *= s + allocation = cuda.mem_alloc(size) + binding = { + "index": i, + "name": name, + "dtype": np.dtype(tensorrt.nptype(dtype)), + "shape": list(shape), + "allocation": allocation, + } + print(f"binding {i}, name : {name} dtype : {np.dtype(tensorrt.nptype(dtype))} shape : {list(shape)}") + allocations.append(allocation) + if engine.binding_is_input(i): + inputs.append(binding) + else: + outputs.append(binding) + return inputs, outputs, allocations + +def fixed_image_standardization(image_tensor): + processed_tensor = (image_tensor - 127.5) / 128.0 + return processed_tensor + +def collate_pil(x): + out_x, out_y = [], [] + for xx, yy in x: + out_x.append(xx) + out_y.append(yy) + return out_x, out_y + +def getdataloader(datasets_dir, step=20, batch_size=64, image_size=160): + orig_img_ds = datasets.ImageFolder(datasets_dir + 'lfw', transform=None) + orig_img_ds.samples = [ + (p, p) + for p, _ in orig_img_ds.samples + ] + loader = DataLoader( + orig_img_ds, + num_workers=16, + batch_size=batch_size, + collate_fn=collate_pil + ) + crop_paths = [] + box_probs = [] + for i, (x, b_paths) in enumerate(loader): + crops = [p for p in b_paths] + crop_paths.extend(crops) + # print('\rBatch {} of {}'.format(i + 1, len(loader)), end='') + + trans = transforms.Compose([ + np.float32, + transforms.ToTensor(), + fixed_image_standardization + ]) + + dataset = datasets.ImageFolder(datasets_dir + 'lfw', transform=trans) + embed_loader = DataLoader( + dataset, + num_workers=16, + batch_size=batch_size, + sampler=SequentialSampler(dataset) + ) + + return embed_loader, crop_paths diff --git a/models/cv/face/facenet/ixrt/config/FACENET_CONFIG b/models/cv/face/facenet/ixrt/config/FACENET_CONFIG new file mode 100644 index 0000000000000000000000000000000000000000..3b3282eff772fa4a2d46d2cc2aace1570ad0f1bb --- /dev/null +++ b/models/cv/face/facenet/ixrt/config/FACENET_CONFIG @@ -0,0 +1,34 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# IMGSIZE : 模型输入hw大小 +# MODEL_NAME : 生成onnx/engine的basename +# ORIGINE_MODEL : 原始onnx文件名称 +IMGSIZE=160 +MODEL_NAME=facenet +ORIGINE_MODEL=facenet_export.onnx + +# QUANT CONFIG (仅PRECISION为int8时生效) + # QUANT_OBSERVER : 量化策略,可选 [hist_percentile, percentile, minmax, entropy, ema] + # QUANT_BATCHSIZE : 量化时组dataloader的batchsize, 最好和onnx中的batchsize保持一致,有些op可能推导shape错误(比如Reshape) + # QUANT_STEP : 量化步数 + # QUANT_SEED : 随机种子 保证量化结果可复现 + # QUANT_EXIST_ONNX : 如果有其他来源的量化模型则填写 +QUANT_OBSERVER=hist_percentile +QUANT_BATCHSIZE=64 +QUANT_STEP=32 +QUANT_SEED=42 +DISABLE_QUANT_LIST= +QUANT_EXIST_ONNX= diff --git a/models/cv/face/facenet/ixrt/deploy.py b/models/cv/face/facenet/ixrt/deploy.py new file mode 100644 index 0000000000000000000000000000000000000000..79f4ce5880bb50f78127a923e09c446547ac3fd2 --- /dev/null +++ b/models/cv/face/facenet/ixrt/deploy.py @@ -0,0 +1,445 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import onnx +import os +import simplejson as json +import argparse +from onnxsim import simplify +import numpy as np +import shutil +from onnx import numpy_helper +from onnx import AttributeProto, TensorProto, GraphProto +from load_ixrt_plugin import load_ixrt_plugin +load_ixrt_plugin() + +def onnx_sim(onnx_name, save_name): + # simplify onnx + cmd = "onnxsim {} {}".format(onnx_name, save_name) + os.system(cmd) + print("[info] onnxsim done!") + + +def cut_model(onnx_name): + input_names = ["input"] + output_names = ["/last_bn/BatchNormalization_output_0"] + onnx.utils.extract_model(onnx_name, onnx_name, input_names, output_names) + +def fuse_matmul(onnx_name, save_onnx_name): + find_matmul = 0 + + onnx_model = onnx.load(onnx_name) + + graph = onnx_model.graph + nodes = graph.node + + conv_weights = None + conv_bias = None + bn_weights = None + bn_bias = None + conv_weights_new = None + conv_bias_new = None + + pre_node = None + for i, node in enumerate(nodes): + if (node.op_type == "Conv"): + pass + if (node.op_type == "MatMul"): + for k, ten in enumerate(graph.initializer): + if ten.name == node.input[1]: + H , W = ten.dims + weights = np.fromstring(ten.raw_data, dtype=np.float32) + weights = weights.reshape(ten.dims) + conv_weights = weights.transpose() + if (node.op_type == "BatchNormalization" and pre_node.op_type == "MatMul"): + find_matmul=1 + weights = None + bias = None + mean = None + var = None + + for k, ten in enumerate(graph.initializer): + if ten.name == node.input[1]: + weights = np.fromstring(ten.raw_data, dtype=np.float32) + if ten.name == node.input[2]: + bias = np.fromstring(ten.raw_data, dtype=np.float32) + if ten.name == node.input[3]: + mean = np.fromstring(ten.raw_data, dtype=np.float32) + if ten.name == node.input[4]: + var = np.fromstring(ten.raw_data, dtype=np.float32) + + bn_weights = np.diag(weights / np.sqrt(var + 1e-8)) + bn_bias = bias - weights * mean / np.sqrt(var + 1e-8) + + conv_weights_new = np.matmul(bn_weights, conv_weights) + a, b = conv_weights_new.shape + conv_weights_new = conv_weights_new.reshape((a,b,1,1)) + # conv_bias_new = bn_weights * conv_bias + bn_bias + conv_bias_new = 0 + bn_bias + conv_weights_new_initializer = onnx.numpy_helper.from_array(conv_weights_new, name='conv_weights_new') + graph.initializer.append(conv_weights_new_initializer) + conv_bias_new_initializer = onnx.numpy_helper.from_array(conv_bias_new, name='conv_bias_new') + graph.initializer.append(conv_bias_new_initializer) + + pre_node.op_type = "Conv" + pre_node.input[0] = "/avgpool_1a/GlobalAveragePool_output_0" + pre_node.input[1] = "conv_weights_new" + pre_node.input.append("conv_bias_new") + pre_node.output[0] = "/last_bn/BatchNormalization_output_0" + dilations = onnx.helper.make_attribute("dilations", [1,1]) + group = onnx.helper.make_attribute("group", 1) + kernel_shape = onnx.helper.make_attribute("kernel_shape", [1,1]) + pads = onnx.helper.make_attribute("pads", [0,0,0,0]) + strides = onnx.helper.make_attribute("strides", [1,1]) + + pre_node.attribute.append(dilations) + pre_node.attribute.append(group) + pre_node.attribute.append(kernel_shape) + pre_node.attribute.append(pads) + pre_node.attribute.append(strides) + graph.node.remove(node) + + pre_node = node + + for i, node in enumerate(nodes): + if (node.name == "Reshape_353"): + # print("[reshape] : ", node.name) + graph.node.remove(node) + + if find_matmul==1: + output = onnx.helper.make_tensor_value_info('/last_bn/BatchNormalization_output_0', TensorProto.FLOAT, [64, 512, 1, 1]) + graph = onnx.helper.make_graph( + graph.node, + "facenet model", + graph.input, + [output], + graph.initializer + ) + + info_model = onnx.helper.make_model(graph, producer_name="facenet") + info_model.opset_import[0].version = 11 + onnx_model = onnx.shape_inference.infer_shapes(info_model) + + onnx.checker.check_model(onnx_model) + onnx.save(onnx_model, save_onnx_name) + +def fuse_mul(onnx_name, save_onnx_name): + onnx_model = onnx.load(onnx_name) + + graph = onnx_model.graph + nodes = graph.node + pre_node = None + + for i, node in enumerate(nodes): + if (node.op_type == "Constant"): + pass + + if (node.op_type == "Mul" and pre_node.op_type == "Conv" ): + for ten in graph.initializer: + if ten.name == node.input[1]: + scale_name = ten.name + scale = np.fromstring(ten.raw_data, dtype=np.float32) + + for k, ten in enumerate(graph.initializer): + # print(ten.name) + if ten.name == pre_node.input[1]: + weights_name = ten.name + weights = np.fromstring(ten.raw_data, dtype=np.float32) + weights *= scale + graph.initializer[k].raw_data = weights.tobytes() + + if ten.name == pre_node.input[2]: + bias_name = ten.name + bias = np.fromstring(ten.raw_data, dtype=np.float32) + # print("bias len: ",len(da)) + bias *= scale + graph.initializer[k].raw_data = bias.tobytes() + + new_conv = pre_node + new_conv.output[0] = node.output[0] + graph.node.remove(node) + pre_node = node + + onnx.checker.check_model(onnx_model) + onnx.save(onnx_model, save_onnx_name) + +def create_graph_json(onnx_name): + # create graph json and weights + graph_path = onnx_name[0:-5] + "_graph.json" + weight_path = onnx_name[0:-5] + ".weights" + + model = onnx.load(onnx_name) + graph = model.graph + nodes = graph.node + initializer = graph.initializer + value_info = graph.value_info # Infer shape info + + model_inputs = [tensor.name for tensor in graph.input] + model_outputs = [tensor.name for tensor in graph.output] + + model = {} + model["nodes"] = {} + model["tensors"] = {} + model["edges"] = {} + model["output"] = {} + data_type_table = { + 1: "float32", + 2: "uint8", + 3: "int8", + 4: "uint16", + 5: "int16", + 6: "int32", + 7: "int64", + 9: "bool", + 10: "float16", + 11: "double", + 12: "uint32", + 13: "uint64", + } + input_cache = [] + for item in graph.input: + if item.type.tensor_type.elem_type in data_type_table: + cache = { + "name": item.name, + "type": data_type_table[item.type.tensor_type.elem_type], + } + else: + cache = {"name": item.name} + input_cache.append(cache) + model["input"] = input_cache + + output_cache = [] + for item in graph.output: + if item.type.tensor_type.elem_type in data_type_table: + cache = { + "name": item.name, + "type": data_type_table[item.type.tensor_type.elem_type], + } + else: + cache = {"name": item.name} + output_cache.append(cache) + model["output"] = output_cache + + # find cast dict + input_cast_dict = {} + output_cast_dict = {} + for i, item in enumerate(nodes): + node_name = item.name + input_edge_list = list(item.input) + output_edge_list = list(item.output) + # remove input and output cast op + if item.op_type == "Cast": + if input_edge_list[0] in model_inputs: + input_cast_dict[output_edge_list[0]] = input_edge_list[0] + if output_edge_list[0] in model_outputs: + output_cast_dict[input_edge_list[0]] = output_edge_list[0] + + for i, item in enumerate(nodes): + node_name = item.name + input_edge_list = list(item.input) + output_edge_list = list(item.output) + # remove input and output cast op + if item.op_type == "Cast": + if input_edge_list[0] in model_inputs: + continue + if output_edge_list[0] in model_outputs: + continue + + for idx, edge_name in enumerate(input_edge_list): + if edge_name in input_cast_dict.keys(): + input_edge_list[idx] = input_cast_dict[edge_name] + + for idx, edge_name in enumerate(output_edge_list): + if edge_name in output_cast_dict.keys(): + output_edge_list[idx] = output_cast_dict[edge_name] + + # remove mask in EmbedLayerNormalization + if item.op_type == "EmbedLayerNormalization": + no_attention_mask_in_Embed = True + for input_edge in input_edge_list: + if "attention_mask" in input_edge: + input_edge_list.remove(input_edge) + no_attention_mask_in_Embed = False + if no_attention_mask_in_Embed: + for tensor_name in model_inputs: + if "attention_mask" in tensor_name: + output_edge_list[1] = tensor_name + + node_dict = {"inputs": input_edge_list, "outputs": output_edge_list} + node_dict["op_type"] = item.op_type + attribute_dict = {} + + if node_name == "": + for input_edge in input_edge_list: + node_name += input_edge + "_" + node_name += "to" + for output_edge in output_edge_list: + node_name += "_" + output_edge + + for attr in item.attribute: + + if attr.type == onnx.AttributeProto().AttributeType.FLOAT: + attribute_dict[attr.name] = attr.f + if attr.type == onnx.AttributeProto().AttributeType.FLOATS: + attribute_dict[attr.name] = [x for x in attr.floats] + if attr.type == onnx.AttributeProto().AttributeType.INT: + attribute_dict[attr.name] = attr.i + if attr.type == onnx.AttributeProto().AttributeType.INTS: + attribute_dict[attr.name] = [x for x in attr.ints] + if attr.type == onnx.AttributeProto().AttributeType.STRING: + attribute_dict[attr.name] = str(attr.s.decode("UTF-8")) + if attr.type == onnx.AttributeProto().AttributeType.STRINGS: + attribute_dict[attr.name] = [str(x.decode("UTF-8")) for x in attr.strings] + + node_dict["attrbiute"] = attribute_dict + model["nodes"][node_name] = node_dict + + for i, item in enumerate(initializer): + tensor_name = item.name + tensor_dict = {} + if item.data_type in data_type_table: + tensor_dict["data_type"] = data_type_table[item.data_type] + else: + print( + tensor_name, + " use unsupport data type: ", + item.data_type, + ", data info will not be saved", + ) + continue + tensor_dict["dims"] = list(item.dims) + + model["tensors"][tensor_name] = tensor_dict + + with open(graph_path, "w") as fh: + json.dump(model, fh, indent=4) + + + """ + Export weight + """ + byte_string = "".encode() + + weight_file_postfix = ".weights" + for item in initializer: + tensor_name = item.name + + np_data = None + if len(item.raw_data): + np_data = np.frombuffer(item.raw_data, dtype=np.byte) + elif item.data_type == 1 and len(item.float_data): + np_data = np.array(list(item.float_data), dtype=np.float32) + elif item.data_type == 2 and len(item.int32_data): + np_data = np.array(list(item.int32_data), dtype=np.uint8) + elif item.data_type == 6 and len(item.int32_data): + np_data = np.array(list(item.int32_data), dtype=np.int32) + elif item.data_type == 7 and len(item.int64_data): + np_data = np.array(list(item.int64_data), dtype=np.int64) + elif item.data_type == 10 and len(item.int32_data): + np_data = ( + np.asarray(item.int32_data, dtype=np.uint16) + .reshape(item.dims) + .view(np.float16) + ) + else: + print( + "tensor name: ", + tensor_name, + ", type: ", + item.data_type, + ", len: ", + len(item.raw_data), + len(item.float_data), + len(item.int32_data), + len(item.int64_data), + ", will not save into weights file", + ) + + if np_data is not None: + byte_string += np.uint64(len(tensor_name)).tobytes() + byte_string += tensor_name.encode() + np_bytes = np_data.tobytes() + byte_string += np.uint64(len(np_bytes)).tobytes() + byte_string += np_bytes + + + # Export weight values as bin file + with open(weight_path, "wb") as fh: + fh.write(byte_string) + print("----------------------------") + print("[OK] graph and weights file save at :") + print(graph_path) + print(weight_path) + return graph_path, weight_path + +def add_facenet_norm(cfg_name): + graph_json = json.load(open(cfg_name)) + + graph_json["nodes"]["facenet_norm_1"] = { + "inputs": [ + "/last_bn/BatchNormalization_output_0" + ], + "outputs": [ + "/Pow_1_output_0" + ], + "op_type": "FacenetNorm", + "attrbiute": { + "size": 512 + } + } + graph_json["output"] = [] + graph_json["output"].append({"name":"/Pow_1_output_0", "type":"float32"}) + + with open(cfg_name, "w") as fh: + json.dump(graph_json, fh, indent=4) + + +def main(args): + print("[info] input onnx name :", args.onnx_name) + # onnxsim + onnx_sim(args.onnx_name, "tmp1.onnx") + # cut model + cut_model("tmp1.onnx") + # fuse matmul bn + fuse_matmul("tmp1.onnx", "tmp2.onnx") + # fuse mul + fuse_mul("tmp2.onnx", "facenet_weights/facenet.onnx") + # generate cfg weights + # graph_path, weight_path = create_graph_json("facenet_weights/facenet.onnx") + # add facenet norm + # add_facenet_norm(graph_path) + + os.remove("tmp1.onnx") + os.remove("tmp2.onnx") + print("\n[info] facenet deploy done!!!") + + +def parse_args(): + parser = argparse.ArgumentParser("deploy facenet") + parser.add_argument("--model_name", default="facenet", help="model name") + parser.add_argument("--onnx_name", default="facenet_weights/facenet_export.onnx", help="onnx filepath") + parser.add_argument("--save_name", default="facenet_weights/facenet.onnx", help="onnx filepath") + parser.add_argument("--data_type", default="int8", type=str, choices=["float16", "int8"], help="int8 float16") + parser.add_argument("--batch_size", default="64", type=int, help="batch_size") + parser.add_argument("--quant_file", default="", type=str, help="quant file") + parser.add_argument("--img_size", default="160", type=int, help="image size") + parser.add_argument("--device", default=0, type=int, help="cuda device 0 1 3 ...") + + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + main(args) \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/inference.py b/models/cv/face/facenet/ixrt/inference.py new file mode 100644 index 0000000000000000000000000000000000000000..ec9876e33c800206003d4d5e2c2d165929ba6591 --- /dev/null +++ b/models/cv/face/facenet/ixrt/inference.py @@ -0,0 +1,169 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import argparse +import json +import os +import re +import time +from tqdm import tqdm + +import cv2 +import numpy as np +import pycuda.autoinit +import pycuda.driver as cuda +import torch +import tensorrt +from tensorrt.utils import topk +from sklearn import metrics +from scipy.optimize import brentq +from sklearn.model_selection import KFold +from scipy import interpolate + +from utils import read_pairs, get_paths, evaluate +from common import getdataloader, create_engine_context, get_io_bindings +from load_ixrt_plugin import load_ixrt_plugin +load_ixrt_plugin() + +def main(config): + embed_loader, crop_paths = getdataloader(config.datasets_dir, config.loop_count, config.bsz, config.imgsz) + + host_mem = tensorrt.IHostMemory + logger = tensorrt.Logger(tensorrt.Logger.ERROR) + + # Load Engine && I/O bindings + engine, context = create_engine_context(config.engine_file, logger) + inputs, outputs, allocations = get_io_bindings(engine) + + # Warm up + if config.warm_up > 0: + print("\nWarm Start.") + for i in range(config.warm_up): + context.execute_v2(allocations) + print("Warm Done.") + + # Inference + if config.test_mode == "FPS": + torch.cuda.synchronize() + start_time = time.time() + + for i in range(config.loop_count): + context.execute_v2(allocations) + + torch.cuda.synchronize() + end_time = time.time() + forward_time = end_time - start_time + + fps = config.loop_count * config.bsz / forward_time + + print("FPS : ", fps) + print(f"Performance Check : Test {fps} >= target {config.fps_target}") + if fps >= config.fps_target: + print("pass!") + exit() + else: + print("failed!") + exit(1) + + elif config.test_mode == "ACC": + + classes = [] + embeddings = [] + + for xb, yb in tqdm(embed_loader): + + output = np.zeros(outputs[0]["shape"], outputs[0]["dtype"]) + current_imgs_num = xb.numpy().shape[0] + xb = xb.numpy() + xb = np.ascontiguousarray(xb) + + cuda.memcpy_htod(inputs[0]["allocation"], xb) + context.execute_v2(allocations) + cuda.memcpy_dtoh(output, outputs[0]["allocation"]) + + output = output.reshape(output.shape[0],output.shape[1]) + #print("output shape ",output.shape) + + classes.extend(yb[0:current_imgs_num].numpy()) + embeddings.extend(output) + + + embeddings_dict = dict(zip(crop_paths,embeddings)) + + pairs = read_pairs(config.datasets_dir + config.pairs_name) + path_list, issame_list = get_paths(config.datasets_dir + 'lfw', pairs) + # embeddings = np.array([embeddings_dict[path.replace(".png",".jpg")] for path in path_list]) + embeddings = np.array([embeddings_dict[path] for path in path_list]) + tpr, fpr, accuracy, val, val_std, far, fp, fn = evaluate(embeddings, issame_list) + + print('\nAccuracy: %2.5f+-%2.5f' % (np.mean(accuracy), np.std(accuracy))) + print('Validation rate: %2.5f+-%2.5f @ FAR=%2.5f' % (val, val_std, far)) + + auc = metrics.auc(fpr, tpr) + print('Area Under Curve (AUC): %1.3f' % auc) + #eer = brentq(lambda x: 1. - x - interpolate.interp1d(fpr, tpr, fill_value="extrapolate")(x), 0., 1.) + #print('Equal Error Rate (EER): %1.3f' % eer) + + acc = np.mean(accuracy) + print(f"Accuracy Check : Test {acc} >= target {config.acc_target}") + if acc >= config.acc_target: + print("pass!") + exit() + else: + print("failed!") + exit(1) + +def parse_config(): + parser = argparse.ArgumentParser() + parser.add_argument("--test_mode", type=str, default="FPS", help="FPS MAP") + parser.add_argument( + "--engine_file", + type=str, + help="engine file path" + ) + parser.add_argument( + "--datasets_dir", + type=str, + default="", + help="ImageNet dir", + ) + parser.add_argument("--pairs_name", type=str, default="pairs.txt", help="binary weights file name") + parser.add_argument("--warm_up", type=int, default=-1, help="warm_up times") + parser.add_argument("--bsz", type=int, default=32, help="test batch size") + parser.add_argument( + "--imgsz", + "--img", + "--img-size", + type=int, + default=160, + help="inference size h,w", + ) + parser.add_argument("--use_async", action="store_true") + parser.add_argument( + "--device", type=int, default=0, help="cuda device, i.e. 0 or 0,1,2,3,4" + ) + parser.add_argument("--fps_target", type=float, default=-1.0) + parser.add_argument("--acc_target", type=float, default=-1.0) + parser.add_argument("--loop_count", type=int, default=-1) + + config = parser.parse_args() + return config + +if __name__ == "__main__": + config = parse_config() + main(config) \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/load_ixrt_plugin.py b/models/cv/face/facenet/ixrt/load_ixrt_plugin.py new file mode 100644 index 0000000000000000000000000000000000000000..ae47dc8e854b6bea1f768e65c4dd481048bfebce --- /dev/null +++ b/models/cv/face/facenet/ixrt/load_ixrt_plugin.py @@ -0,0 +1,27 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ctypes +import tensorrt +from os.path import join, dirname, exists +def load_ixrt_plugin(logger=tensorrt.Logger(tensorrt.Logger.INFO), namespace="", dynamic_path=""): + if not dynamic_path: + dynamic_path = join(dirname(tensorrt.__file__), "lib", "libixrt_plugin.so") + if not exists(dynamic_path): + raise FileNotFoundError( + f"The ixrt_plugin lib {dynamic_path} is not existed, please provided effective plugin path!") + ctypes.CDLL(dynamic_path) + tensorrt.init_libnvinfer_plugins(logger, namespace) + print(f"Loaded plugin from {dynamic_path}") \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/quant.py b/models/cv/face/facenet/ixrt/quant.py new file mode 100644 index 0000000000000000000000000000000000000000..26413e3e0f58f219cce2bd78804de288cba1fd1a --- /dev/null +++ b/models/cv/face/facenet/ixrt/quant.py @@ -0,0 +1,133 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import torch +from tensorrt.deploy.api import * +from tensorrt.deploy.utils.seed import manual_seed +from torchvision import models +from argparse import ArgumentParser +from torch.utils.data import DataLoader +from torch.utils.data.dataset import Dataset +from torchvision import datasets, transforms +import json +import cv2 +import numpy as np +import math +import simplejson as json +from tensorrt.deploy import static_quantize + + +# manual_seed(43) +device = 0 if torch.cuda.is_available() else "cpu" + + +def fixed_image_standardization(image_tensor): + processed_tensor = (image_tensor - 127.5) / 128.0 + return processed_tensor + +def create_dataloader(args): + image_dir_path = os.path.join(args.data_path, "lfw") + + trans = transforms.Compose([ + np.float32, + transforms.ToTensor(), + fixed_image_standardization + ]) + + dataset = datasets.ImageFolder(args.data_path + 'lfw', transform=trans) + + calibration_dataset = dataset + print("image folder total images : ", len(dataset)) + if args.num_samples is not None: + indices = np.random.permutation(len(dataset))[:args.num_samples] + calibration_dataset = torch.utils.data.Subset( + dataset, indices=indices + ) + print("calibration_dataset images : ", len(calibration_dataset)) + + assert len(dataset), f"data size is 0, check data path please" + calibration_dataloader = DataLoader( + calibration_dataset, + batch_size=args.batch_size, + shuffle=True, + num_workers=args.workers, + ) + verify_dataloader = DataLoader( + dataset, + batch_size=args.batch_size, + shuffle=True, + num_workers=args.workers, + ) + + return calibration_dataloader, verify_dataloader + + +@torch.no_grad() +def quantize_model(args, model_name, model, dataloader): + + calibration_dataloader, verify_dataloader = dataloader + print("calibration dataset length: ", len(calibration_dataloader)) + + if isinstance(model, torch.nn.Module): + model = model.to(device) + model.eval() + + static_quantize(args.model, + calibration_dataloader=calibration_dataloader, + save_quant_onnx_path=os.path.join("./facenet_weights", f"{model_name}-quant.onnx"), + observer=args.observer, + data_preprocess=lambda x: x[0].to("cuda"), + quant_format="qdq", + disable_quant_names=None) + +def create_argparser(*args, **kwargs): + parser = ArgumentParser(*args, **kwargs) + parser.add_argument("--batch_size", type=int, default=64) + parser.add_argument("--img_size", type=int, default=160) + parser.add_argument("-j", "--workers", type=int, default=4) + parser.add_argument("--model", type=str, default="./facenet_weights/facenet.onnx") + parser.add_argument("--num_samples", type=int, default=1000) + parser.add_argument("--data_path", type=str, default="./facenet_datasets/") + parser.add_argument("--analyze", action="store_true") + parser.add_argument("--observer", type=str, default="hist_percentile") + parser.add_argument("--fp32_acc", action="store_true") + parser.add_argument("--use_ixrt", action="store_true") + parser.add_argument("--quant_params", type=str, default=None) + parser.add_argument("--disable_bias_correction", action="store_true") + return parser + +def parse_args(): + parser = create_argparser("PTQ Quantization") + args = parser.parse_args() + args.use_ixquant = not args.use_ixrt + return args + + +def main(): + args = parse_args() + print(args) + dataloader = create_dataloader(args) + + if args.model.endswith(".onnx"): + model_name = os.path.basename(args.model) + model_name = model_name.rsplit(".", maxsplit=1)[0] + model = args.model + else: + print("[Error] file name not correct ", args.model) + quantize_model(args, model_name, model, dataloader) + +if __name__ == "__main__": + main() diff --git a/models/cv/face/facenet/ixrt/scripts/infer_facenet_fp16_accuracy.sh b/models/cv/face/facenet/ixrt/scripts/infer_facenet_fp16_accuracy.sh new file mode 100644 index 0000000000000000000000000000000000000000..27e5e8ad859d95c86dfc9b29fdc78150b0c60c95 --- /dev/null +++ b/models/cv/face/facenet/ixrt/scripts/infer_facenet_fp16_accuracy.sh @@ -0,0 +1,136 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +#!/bin/bash + +EXIT_STATUS=0 +check_status() +{ + if ((${PIPESTATUS[0]} != 0));then + EXIT_STATUS=1 + fi +} + +# Run paraments +BSZ=64 +TGT=-1 +WARM_UP=0 +LOOP_COUNT=-1 +RUN_MODE=ACC +PRECISION=float16 + +# Update arguments +index=0 +options=$@ +arguments=($options) +for argument in $options +do + index=`expr $index + 1` + case $argument in + --bs) BSZ=${arguments[index]};; + --tgt) TGT=${arguments[index]};; + esac +done +PROJ_DIR=$(cd $(dirname $0);cd ../../; pwd) +echo PROJ_DIR : ${PROJ_DIR} +RUN_DIR="${PROJ_DIR}/ixrt/" +DATASETS_DIR="${RUN_DIR}/facenet_datasets/" +CHECKPOINTS_DIR="${RUN_DIR}/facenet_weights/" +CONFIG_DIR="${PROJ_DIR}/ixrt/config/FACENET_CONFIG" +source ${CONFIG_DIR} +ORIGINE_MODEL=${CHECKPOINTS_DIR}/${ORIGINE_MODEL} + +echo CHECKPOINTS_DIR : ${CHECKPOINTS_DIR} +echo DATASETS_DIR : ${DATASETS_DIR} +echo RUN_DIR : ${RUN_DIR} +echo CONFIG_DIR : ${CONFIG_DIR} +echo ====================== Model Info ====================== +echo Model Name : ${MODEL_NAME} +echo Model Input Name : ${MODEL_INPUT_NAME} +echo Model Output Name : ${MODEL_OUTPUT_NAME} +echo Onnx Path : ${ORIGINE_MODEL} + +step=0 +SIM_MODEL=${CHECKPOINTS_DIR}/${MODEL_NAME}.onnx + +# Simplify Model +let step++ +echo; +echo [STEP ${step}] : Simplify Model +if [ -f ${SIM_MODEL} ];then + echo " "Simplify Model, ${SIM_MODEL} has been existed +else + cd $RUN_DIR + python3 ${RUN_DIR}/deploy.py \ + --onnx_name ${CHECKPOINTS_DIR}/facenet_export.onnx + echo " "Generate ${SIM_MODEL} +fi + +# Quant Model +if [ $PRECISION == "int8" ];then + let step++ + echo; + echo [STEP ${step}] : Quant Model + if [[ -z ${QUANT_EXIST_ONNX} ]];then + QUANT_EXIST_ONNX=$CHECKPOINTS_DIR/${MODEL_NAME}-quant.onnx + fi + if [[ -f ${QUANT_EXIST_ONNX} ]];then + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Quant Model Skip, ${QUANT_EXIST_ONNX} has been existed + else + python3 ${RUN_DIR}/quant.py \ + --model ${SIM_MODEL} \ + --batch_size ${QUANT_BATCHSIZE} \ + --img_size ${IMGSIZE} \ + --num_samples 6400 \ + --observer ${QUANT_OBSERVER} \ + --disable_bias_correction + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Generate ${SIM_MODEL} + fi +fi + + +# Build Engine +let step++ +echo; +echo [STEP ${step}] : Build Engine +ENGINE_FILE=${CHECKPOINTS_DIR}/${MODEL_NAME}_${PRECISION}_bs${BSZ}.engine +FINAL_MODEL=${SIM_MODEL} +if [ -f $ENGINE_FILE ];then + echo " "Build Engine Skip, $ENGINE_FILE has been existed +else + python3 ${RUN_DIR}/build_engine.py \ + --precision ${PRECISION} \ + --model ${FINAL_MODEL} \ + --engine ${ENGINE_FILE} + echo " "Generate Engine ${ENGINE_FILE} +fi + +# Inference +let step++ +echo; +echo [STEP ${step}] : Inference +python3 ${RUN_DIR}/inference.py \ + --engine_file=${ENGINE_FILE} \ + --datasets_dir=${DATASETS_DIR} \ + --imgsz=${IMGSIZE} \ + --warm_up=${WARM_UP} \ + --loop_count ${LOOP_COUNT} \ + --test_mode ${RUN_MODE} \ + --fps_target ${TGT} \ + --bsz ${BSZ}; check_status + +exit ${EXIT_STATUS} \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/scripts/infer_facenet_fp16_performance.sh b/models/cv/face/facenet/ixrt/scripts/infer_facenet_fp16_performance.sh new file mode 100644 index 0000000000000000000000000000000000000000..401658cafd85297b9d98f7febb9e7c88746062ef --- /dev/null +++ b/models/cv/face/facenet/ixrt/scripts/infer_facenet_fp16_performance.sh @@ -0,0 +1,136 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +#!/bin/bash + +EXIT_STATUS=0 +check_status() +{ + if ((${PIPESTATUS[0]} != 0));then + EXIT_STATUS=1 + fi +} + +# Run paraments +BSZ=64 +TGT=-1 +WARM_UP=3 +LOOP_COUNT=20 +RUN_MODE=FPS +PRECISION=float16 + +# Update arguments +index=0 +options=$@ +arguments=($options) +for argument in $options +do + index=`expr $index + 1` + case $argument in + --bs) BSZ=${arguments[index]};; + --tgt) TGT=${arguments[index]};; + esac +done +PROJ_DIR=$(cd $(dirname $0);cd ../../; pwd) +echo PROJ_DIR : ${PROJ_DIR} +RUN_DIR="${PROJ_DIR}/ixrt/" +DATASETS_DIR="${RUN_DIR}/facenet_datasets/" +CHECKPOINTS_DIR="${RUN_DIR}/facenet_weights/" +CONFIG_DIR="${PROJ_DIR}/ixrt/config/FACENET_CONFIG" +source ${CONFIG_DIR} +ORIGINE_MODEL=${CHECKPOINTS_DIR}/${ORIGINE_MODEL} + +echo CHECKPOINTS_DIR : ${CHECKPOINTS_DIR} +echo DATASETS_DIR : ${DATASETS_DIR} +echo RUN_DIR : ${RUN_DIR} +echo CONFIG_DIR : ${CONFIG_DIR} +echo ====================== Model Info ====================== +echo Model Name : ${MODEL_NAME} +echo Model Input Name : ${MODEL_INPUT_NAME} +echo Model Output Name : ${MODEL_OUTPUT_NAME} +echo Onnx Path : ${ORIGINE_MODEL} + +step=0 +SIM_MODEL=${CHECKPOINTS_DIR}/${MODEL_NAME}.onnx + +# Simplify Model +let step++ +echo; +echo [STEP ${step}] : Simplify Model +if [ -f ${SIM_MODEL} ];then + echo " "Simplify Model, ${SIM_MODEL} has been existed +else + cd $RUN_DIR + python3 ${RUN_DIR}/deploy.py \ + --onnx_name ${CHECKPOINTS_DIR}/facenet_export.onnx + echo " "Generate ${SIM_MODEL} +fi + +# Quant Model +if [ $PRECISION == "int8" ];then + let step++ + echo; + echo [STEP ${step}] : Quant Model + if [[ -z ${QUANT_EXIST_ONNX} ]];then + QUANT_EXIST_ONNX=$CHECKPOINTS_DIR/${MODEL_NAME}-quant.onnx + fi + if [[ -f ${QUANT_EXIST_ONNX} ]];then + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Quant Model Skip, ${QUANT_EXIST_ONNX} has been existed + else + python3 ${RUN_DIR}/quant.py \ + --model ${SIM_MODEL} \ + --batch_size ${QUANT_BATCHSIZE} \ + --img_size ${IMGSIZE} \ + --num_samples 6400 \ + --observer ${QUANT_OBSERVER} \ + --disable_bias_correction + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Generate ${SIM_MODEL} + fi +fi + + +# Build Engine +let step++ +echo; +echo [STEP ${step}] : Build Engine +ENGINE_FILE=${CHECKPOINTS_DIR}/${MODEL_NAME}_${PRECISION}_bs${BSZ}.engine +FINAL_MODEL=${SIM_MODEL} +if [ -f $ENGINE_FILE ];then + echo " "Build Engine Skip, $ENGINE_FILE has been existed +else + python3 ${RUN_DIR}/build_engine.py \ + --precision ${PRECISION} \ + --model ${FINAL_MODEL} \ + --engine ${ENGINE_FILE} + echo " "Generate Engine ${ENGINE_FILE} +fi + +# Inference +let step++ +echo; +echo [STEP ${step}] : Inference +python3 ${RUN_DIR}/inference.py \ + --engine_file=${ENGINE_FILE} \ + --datasets_dir=${DATASETS_DIR} \ + --imgsz=${IMGSIZE} \ + --warm_up=${WARM_UP} \ + --loop_count ${LOOP_COUNT} \ + --test_mode ${RUN_MODE} \ + --fps_target ${TGT} \ + --bsz ${BSZ}; check_status + +exit ${EXIT_STATUS} \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/scripts/infer_facenet_int8_accuracy.sh b/models/cv/face/facenet/ixrt/scripts/infer_facenet_int8_accuracy.sh new file mode 100644 index 0000000000000000000000000000000000000000..c2c2f176bcd0ea6bb00acedb6fbda80b47456a08 --- /dev/null +++ b/models/cv/face/facenet/ixrt/scripts/infer_facenet_int8_accuracy.sh @@ -0,0 +1,138 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +#!/bin/bash + +EXIT_STATUS=0 +check_status() +{ + if ((${PIPESTATUS[0]} != 0));then + EXIT_STATUS=1 + fi +} + +# Run paraments +BSZ=64 +TGT=-1 +WARM_UP=0 +LOOP_COUNT=-1 +RUN_MODE=ACC +PRECISION=int8 + +# Update arguments +index=0 +options=$@ +arguments=($options) +for argument in $options +do + index=`expr $index + 1` + case $argument in + --bs) BSZ=${arguments[index]};; + --tgt) TGT=${arguments[index]};; + esac +done +PROJ_DIR=$(cd $(dirname $0);cd ../../; pwd) +echo PROJ_DIR : ${PROJ_DIR} +RUN_DIR="${PROJ_DIR}/ixrt/" +DATASETS_DIR="${RUN_DIR}/facenet_datasets/" +CHECKPOINTS_DIR="${RUN_DIR}/facenet_weights/" +CONFIG_DIR="${PROJ_DIR}/ixrt/config/FACENET_CONFIG" +source ${CONFIG_DIR} +ORIGINE_MODEL=${CHECKPOINTS_DIR}/${ORIGINE_MODEL} + +echo CHECKPOINTS_DIR : ${CHECKPOINTS_DIR} +echo DATASETS_DIR : ${DATASETS_DIR} +echo RUN_DIR : ${RUN_DIR} +echo CONFIG_DIR : ${CONFIG_DIR} +echo ====================== Model Info ====================== +echo Model Name : ${MODEL_NAME} +echo Model Input Name : ${MODEL_INPUT_NAME} +echo Model Output Name : ${MODEL_OUTPUT_NAME} +echo Onnx Path : ${ORIGINE_MODEL} + +step=0 +SIM_MODEL=${CHECKPOINTS_DIR}/${MODEL_NAME}.onnx + +# Simplify Model +let step++ +echo; +echo [STEP ${step}] : Simplify Model +if [ -f ${SIM_MODEL} ];then + echo " "Simplify Model, ${SIM_MODEL} has been existed +else + cd $RUN_DIR + python3 ${RUN_DIR}/deploy.py \ + --onnx_name ${CHECKPOINTS_DIR}/facenet_export.onnx + echo " "Generate ${SIM_MODEL} +fi + +# Quant Model +if [ $PRECISION == "int8" ];then + let step++ + echo; + echo [STEP ${step}] : Quant Model + if [[ -z ${QUANT_EXIST_ONNX} ]];then + QUANT_EXIST_ONNX=$CHECKPOINTS_DIR/${MODEL_NAME}-quant.onnx + fi + if [[ -f ${QUANT_EXIST_ONNX} ]];then + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Quant Model Skip, ${QUANT_EXIST_ONNX} has been existed + else + cd $RUN_DIR + python3 ${RUN_DIR}/quant.py \ + --model ${SIM_MODEL} \ + --batch_size ${QUANT_BATCHSIZE} \ + --data_path ${DATASETS_DIR} \ + --img_size ${IMGSIZE} \ + --num_samples 6400 \ + --observer ${QUANT_OBSERVER} \ + --disable_bias_correction + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Generate ${SIM_MODEL} + fi +fi + + +# Build Engine +let step++ +echo; +echo [STEP ${step}] : Build Engine +ENGINE_FILE=${CHECKPOINTS_DIR}/${MODEL_NAME}_${PRECISION}_bs${BSZ}.engine +FINAL_MODEL=${SIM_MODEL} +if [ -f $ENGINE_FILE ];then + echo " "Build Engine Skip, $ENGINE_FILE has been existed +else + python3 ${RUN_DIR}/build_engine.py \ + --precision ${PRECISION} \ + --model ${FINAL_MODEL} \ + --engine ${ENGINE_FILE} + echo " "Generate Engine ${ENGINE_FILE} +fi + +# Inference +let step++ +echo; +echo [STEP ${step}] : Inference +python3 ${RUN_DIR}/inference.py \ + --engine_file=${ENGINE_FILE} \ + --datasets_dir=${DATASETS_DIR} \ + --imgsz=${IMGSIZE} \ + --warm_up=${WARM_UP} \ + --loop_count ${LOOP_COUNT} \ + --test_mode ${RUN_MODE} \ + --fps_target ${TGT} \ + --bsz ${BSZ}; check_status + +exit ${EXIT_STATUS} \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/scripts/infer_facenet_int8_performance.sh b/models/cv/face/facenet/ixrt/scripts/infer_facenet_int8_performance.sh new file mode 100644 index 0000000000000000000000000000000000000000..7574347c028dfdb28e3b06016d4c61fb6d3e1328 --- /dev/null +++ b/models/cv/face/facenet/ixrt/scripts/infer_facenet_int8_performance.sh @@ -0,0 +1,138 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +#!/bin/bash + +EXIT_STATUS=0 +check_status() +{ + if ((${PIPESTATUS[0]} != 0));then + EXIT_STATUS=1 + fi +} + +# Run paraments +BSZ=64 +TGT=-1 +WARM_UP=3 +LOOP_COUNT=20 +RUN_MODE=FPS +PRECISION=int8 + +# Update arguments +index=0 +options=$@ +arguments=($options) +for argument in $options +do + index=`expr $index + 1` + case $argument in + --bs) BSZ=${arguments[index]};; + --tgt) TGT=${arguments[index]};; + esac +done +PROJ_DIR=$(cd $(dirname $0);cd ../../; pwd) +echo PROJ_DIR : ${PROJ_DIR} +RUN_DIR="${PROJ_DIR}/ixrt/" +DATASETS_DIR="${RUN_DIR}/facenet_datasets/" +CHECKPOINTS_DIR="${RUN_DIR}/facenet_weights/" +CONFIG_DIR="${PROJ_DIR}/ixrt/config/FACENET_CONFIG" +source ${CONFIG_DIR} +ORIGINE_MODEL=${CHECKPOINTS_DIR}/${ORIGINE_MODEL} + +echo CHECKPOINTS_DIR : ${CHECKPOINTS_DIR} +echo DATASETS_DIR : ${DATASETS_DIR} +echo RUN_DIR : ${RUN_DIR} +echo CONFIG_DIR : ${CONFIG_DIR} +echo ====================== Model Info ====================== +echo Model Name : ${MODEL_NAME} +echo Model Input Name : ${MODEL_INPUT_NAME} +echo Model Output Name : ${MODEL_OUTPUT_NAME} +echo Onnx Path : ${ORIGINE_MODEL} + +step=0 +SIM_MODEL=${CHECKPOINTS_DIR}/${MODEL_NAME}.onnx + +# Simplify Model +let step++ +echo; +echo [STEP ${step}] : Simplify Model +if [ -f ${SIM_MODEL} ];then + echo " "Simplify Model, ${SIM_MODEL} has been existed +else + cd $RUN_DIR + python3 ${RUN_DIR}/deploy.py \ + --onnx_name ${CHECKPOINTS_DIR}/facenet_export.onnx + echo " "Generate ${SIM_MODEL} +fi + +# Quant Model +if [ $PRECISION == "int8" ];then + let step++ + echo; + echo [STEP ${step}] : Quant Model + if [[ -z ${QUANT_EXIST_ONNX} ]];then + QUANT_EXIST_ONNX=$CHECKPOINTS_DIR/${MODEL_NAME}-quant.onnx + fi + if [[ -f ${QUANT_EXIST_ONNX} ]];then + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Quant Model Skip, ${QUANT_EXIST_ONNX} has been existed + else + cd $RUN_DIR + python3 ${RUN_DIR}/quant.py \ + --model ${SIM_MODEL} \ + --batch_size ${QUANT_BATCHSIZE} \ + --data_path ${DATASETS_DIR} \ + --img_size ${IMGSIZE} \ + --num_samples 6400 \ + --observer ${QUANT_OBSERVER} \ + --disable_bias_correction + SIM_MODEL=${QUANT_EXIST_ONNX} + echo " "Generate ${SIM_MODEL} + fi +fi + + +# Build Engine +let step++ +echo; +echo [STEP ${step}] : Build Engine +ENGINE_FILE=${CHECKPOINTS_DIR}/${MODEL_NAME}_${PRECISION}_bs${BSZ}.engine +FINAL_MODEL=${SIM_MODEL} +if [ -f $ENGINE_FILE ];then + echo " "Build Engine Skip, $ENGINE_FILE has been existed +else + python3 ${RUN_DIR}/build_engine.py \ + --precision ${PRECISION} \ + --model ${FINAL_MODEL} \ + --engine ${ENGINE_FILE} + echo " "Generate Engine ${ENGINE_FILE} +fi + +# Inference +let step++ +echo; +echo [STEP ${step}] : Inference +python3 ${RUN_DIR}/inference.py \ + --engine_file=${ENGINE_FILE} \ + --datasets_dir=${DATASETS_DIR} \ + --imgsz=${IMGSIZE} \ + --warm_up=${WARM_UP} \ + --loop_count ${LOOP_COUNT} \ + --test_mode ${RUN_MODE} \ + --fps_target ${TGT} \ + --bsz ${BSZ}; check_status + +exit ${EXIT_STATUS} \ No newline at end of file diff --git a/models/cv/face/facenet/ixrt/tensorflow2pytorch.py b/models/cv/face/facenet/ixrt/tensorflow2pytorch.py new file mode 100644 index 0000000000000000000000000000000000000000..f76ba0fff91ae1ac334c2babbc10f0d65139b711 --- /dev/null +++ b/models/cv/face/facenet/ixrt/tensorflow2pytorch.py @@ -0,0 +1,387 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import tensorflow.compat.v1 as tf +tf.disable_v2_behavior() +import torch +import json +import os, sys + +from dependencies.facenet.src import facenet +from dependencies.facenet.src.models import inception_resnet_v1 as tf_mdl +from dependencies.facenet.src.align import detect_face + +from models.inception_resnet_v1 import InceptionResnetV1 +from models.mtcnn import PNet, RNet, ONet + + +def import_tf_params(tf_mdl_dir, sess): + """Import tensorflow model from save directory. + + Arguments: + tf_mdl_dir {str} -- Location of protobuf, checkpoint, meta files. + sess {tensorflow.Session} -- Tensorflow session object. + + Returns: + (list, list, list) -- Tuple of lists containing the layer names, + parameter arrays as numpy ndarrays, parameter shapes. + """ + print('\nLoading tensorflow model\n') + if callable(tf_mdl_dir): + tf_mdl_dir(sess) + else: + facenet.load_model(tf_mdl_dir) + + print('\nGetting model weights\n') + images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0") + print(images_placeholder) + tf_layers = tf.trainable_variables() + tf_params = sess.run(tf_layers) + print(tf.get_default_graph()) + + tf_shapes = [p.shape for p in tf_params] + tf_layers = [l.name for l in tf_layers] + + print(tf_shapes) + print(tf_shapes) + + if not callable(tf_mdl_dir): + path = os.path.join(tf_mdl_dir, 'layer_description.json') + else: + path = 'data/layer_description.json' + with open(path, 'w') as f: + json.dump({l: s for l, s in zip(tf_layers, tf_shapes)}, f) + + return tf_layers, tf_params, tf_shapes + + +def get_layer_indices(layer_lookup, tf_layers): + """Giving a lookup of model layer attribute names and tensorflow variable names, + find matching parameters. + + Arguments: + layer_lookup {dict} -- Dictionary mapping pytorch attribute names to (partial) + tensorflow variable names. Expects dict of the form {'attr': ['tf_name', ...]} + where the '...'s are ignored. + tf_layers {list} -- List of tensorflow variable names. + + Returns: + list -- The input dictionary with the list of matching inds appended to each item. + """ + layer_inds = {} + for name, value in layer_lookup.items(): + layer_inds[name] = value + [[i for i, n in enumerate(tf_layers) if value[0] in n]] + return layer_inds + + +def load_tf_batchNorm(weights, layer): + """Load tensorflow weights into nn.BatchNorm object. + + Arguments: + weights {list} -- Tensorflow parameters. + layer {torch.nn.Module} -- nn.BatchNorm. + """ + layer.bias.data = torch.tensor(weights[0]).view(layer.bias.data.shape) + layer.weight.data = torch.ones_like(layer.weight.data) + layer.running_mean = torch.tensor(weights[1]).view(layer.running_mean.shape) + layer.running_var = torch.tensor(weights[2]).view(layer.running_var.shape) + + +def load_tf_conv2d(weights, layer, transpose=False): + """Load tensorflow weights into nn.Conv2d object. + + Arguments: + weights {list} -- Tensorflow parameters. + layer {torch.nn.Module} -- nn.Conv2d. + """ + if isinstance(weights, list): + if len(weights) == 2: + layer.bias.data = ( + torch.tensor(weights[1]) + .view(layer.bias.data.shape) + ) + weights = weights[0] + + if transpose: + dim_order = (3, 2, 1, 0) + else: + dim_order = (3, 2, 0, 1) + + layer.weight.data = ( + torch.tensor(weights) + .permute(dim_order) + .view(layer.weight.data.shape) + ) + + +def load_tf_conv2d_trans(weights, layer): + return load_tf_conv2d(weights, layer, transpose=True) + + +def load_tf_basicConv2d(weights, layer): + """Load tensorflow weights into grouped Conv2d+BatchNorm object. + + Arguments: + weights {list} -- Tensorflow parameters. + layer {torch.nn.Module} -- Object containing Conv2d+BatchNorm. + """ + load_tf_conv2d(weights[0], layer.conv) + load_tf_batchNorm(weights[1:], layer.bn) + + +def load_tf_linear(weights, layer): + """Load tensorflow weights into nn.Linear object. + + Arguments: + weights {list} -- Tensorflow parameters. + layer {torch.nn.Module} -- nn.Linear. + """ + if isinstance(weights, list): + if len(weights) == 2: + layer.bias.data = ( + torch.tensor(weights[1]) + .view(layer.bias.data.shape) + ) + weights = weights[0] + layer.weight.data = ( + torch.tensor(weights) + .transpose(-1, 0) + .view(layer.weight.data.shape) + ) + + +# High-level parameter-loading functions: + +def load_tf_block35(weights, layer): + load_tf_basicConv2d(weights[:4], layer.branch0) + load_tf_basicConv2d(weights[4:8], layer.branch1[0]) + load_tf_basicConv2d(weights[8:12], layer.branch1[1]) + load_tf_basicConv2d(weights[12:16], layer.branch2[0]) + load_tf_basicConv2d(weights[16:20], layer.branch2[1]) + load_tf_basicConv2d(weights[20:24], layer.branch2[2]) + load_tf_conv2d(weights[24:26], layer.conv2d) + + +def load_tf_block17_8(weights, layer): + load_tf_basicConv2d(weights[:4], layer.branch0) + load_tf_basicConv2d(weights[4:8], layer.branch1[0]) + load_tf_basicConv2d(weights[8:12], layer.branch1[1]) + load_tf_basicConv2d(weights[12:16], layer.branch1[2]) + load_tf_conv2d(weights[16:18], layer.conv2d) + + +def load_tf_mixed6a(weights, layer): + if len(weights) != 16: + raise ValueError(f'Number of weight arrays ({len(weights)}) not equal to 16') + load_tf_basicConv2d(weights[:4], layer.branch0) + load_tf_basicConv2d(weights[4:8], layer.branch1[0]) + load_tf_basicConv2d(weights[8:12], layer.branch1[1]) + load_tf_basicConv2d(weights[12:16], layer.branch1[2]) + + +def load_tf_mixed7a(weights, layer): + if len(weights) != 28: + raise ValueError(f'Number of weight arrays ({len(weights)}) not equal to 28') + load_tf_basicConv2d(weights[:4], layer.branch0[0]) + load_tf_basicConv2d(weights[4:8], layer.branch0[1]) + load_tf_basicConv2d(weights[8:12], layer.branch1[0]) + load_tf_basicConv2d(weights[12:16], layer.branch1[1]) + load_tf_basicConv2d(weights[16:20], layer.branch2[0]) + load_tf_basicConv2d(weights[20:24], layer.branch2[1]) + load_tf_basicConv2d(weights[24:28], layer.branch2[2]) + + +def load_tf_repeats(weights, layer, rptlen, subfun): + if len(weights) % rptlen != 0: + raise ValueError(f'Number of weight arrays ({len(weights)}) not divisible by {rptlen}') + weights_split = [weights[i:i+rptlen] for i in range(0, len(weights), rptlen)] + for i, w in enumerate(weights_split): + subfun(w, getattr(layer, str(i))) + + +def load_tf_repeat_1(weights, layer): + load_tf_repeats(weights, layer, 26, load_tf_block35) + + +def load_tf_repeat_2(weights, layer): + load_tf_repeats(weights, layer, 18, load_tf_block17_8) + + +def load_tf_repeat_3(weights, layer): + load_tf_repeats(weights, layer, 18, load_tf_block17_8) + + +def test_loaded_params(mdl, tf_params, tf_layers): + """Check each parameter in a pytorch model for an equivalent parameter + in a list of tensorflow variables. + + Arguments: + mdl {torch.nn.Module} -- Pytorch model. + tf_params {list} -- List of ndarrays representing tensorflow variables. + tf_layers {list} -- Corresponding list of tensorflow variable names. + """ + tf_means = torch.stack([torch.tensor(p).mean() for p in tf_params]) + for name, param in mdl.named_parameters(): + pt_mean = param.data.mean() + matching_inds = ((tf_means - pt_mean).abs() < 1e-8).nonzero() + print(f'{name} equivalent to {[tf_layers[i] for i in matching_inds]}') + + +def compare_model_outputs(pt_mdl, sess, test_data): + """Given some testing data, compare the output of pytorch and tensorflow models. + + Arguments: + pt_mdl {torch.nn.Module} -- Pytorch model. + sess {tensorflow.Session} -- Tensorflow session object. + test_data {torch.Tensor} -- Pytorch tensor. + """ + print('\nPassing test data through TF model\n') + if isinstance(sess, tf.Session): + images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0") + phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0") + embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0") + feed_dict = {images_placeholder: test_data.numpy(), phase_train_placeholder: False} + tf_output = torch.tensor(sess.run(embeddings, feed_dict=feed_dict)) + else: + tf_output = sess(test_data) + + print(tf_output.shape, tf_output) + + print('\nPassing test data through PT model\n') + pt_output = pt_mdl(test_data.permute(0, 3, 1, 2)) + print(pt_output.shape, pt_output) + + distance = (tf_output - pt_output).norm() + print(f'\nDistance {distance}\n') + + +def compare_mtcnn(pt_mdl, tf_fun, sess, ind, test_data): + tf_mdls = tf_fun(sess) + tf_mdl = tf_mdls[ind] + + print('\nPassing test data through TF model\n') + tf_output = tf_mdl(test_data.numpy()) + tf_output = [torch.tensor(out) for out in tf_output] + print('\n'.join([str(o.view(-1)[:10]) for o in tf_output])) + + print('\nPassing test data through PT model\n') + with torch.no_grad(): + pt_output = pt_mdl(test_data.permute(0, 3, 2, 1)) + pt_output = [torch.tensor(out) for out in pt_output] + for i in range(len(pt_output)): + if len(pt_output[i].shape) == 4: + pt_output[i] = pt_output[i].permute(0, 3, 2, 1).contiguous() + print('\n'.join([str(o.view(-1)[:10]) for o in pt_output])) + + distance = [(tf_o - pt_o).norm() for tf_o, pt_o in zip(tf_output, pt_output)] + print(f'\nDistance {distance}\n') + + +def load_tf_model_weights(mdl, layer_lookup, tf_mdl_dir, is_resnet=True, arg_num=None): + """Load tensorflow parameters into a pytorch model. + + Arguments: + mdl {torch.nn.Module} -- Pytorch model. + layer_lookup {[type]} -- Dictionary mapping pytorch attribute names to (partial) + tensorflow variable names, and a function suitable for loading weights. + Expects dict of the form {'attr': ['tf_name', function]}. + tf_mdl_dir {str} -- Location of protobuf, checkpoint, meta files. + """ + tf.reset_default_graph() + with tf.Session() as sess: + tf_layers, tf_params, tf_shapes = import_tf_params(tf_mdl_dir, sess) + layer_info = get_layer_indices(layer_lookup, tf_layers) + + for layer_name, info in layer_info.items(): + print(f'Loading {info[0]}/* into {layer_name}') + weights = [tf_params[i] for i in info[2]] + layer = getattr(mdl, layer_name) + info[1](weights, layer) + + test_loaded_params(mdl, tf_params, tf_layers) + + if is_resnet: + compare_model_outputs(mdl, sess, torch.randn(5, 160, 160, 3).detach()) + + +def tensorflow2pytorch(args): + lookup_inception_resnet_v1 = { + 'conv2d_1a': ['InceptionResnetV1/Conv2d_1a_3x3', load_tf_basicConv2d], + 'conv2d_2a': ['InceptionResnetV1/Conv2d_2a_3x3', load_tf_basicConv2d], + 'conv2d_2b': ['InceptionResnetV1/Conv2d_2b_3x3', load_tf_basicConv2d], + 'conv2d_3b': ['InceptionResnetV1/Conv2d_3b_1x1', load_tf_basicConv2d], + 'conv2d_4a': ['InceptionResnetV1/Conv2d_4a_3x3', load_tf_basicConv2d], + 'conv2d_4b': ['InceptionResnetV1/Conv2d_4b_3x3', load_tf_basicConv2d], + 'repeat_1': ['InceptionResnetV1/Repeat/block35', load_tf_repeat_1], + 'mixed_6a': ['InceptionResnetV1/Mixed_6a', load_tf_mixed6a], + 'repeat_2': ['InceptionResnetV1/Repeat_1/block17', load_tf_repeat_2], + 'mixed_7a': ['InceptionResnetV1/Mixed_7a', load_tf_mixed7a], + 'repeat_3': ['InceptionResnetV1/Repeat_2/block8', load_tf_repeat_3], + 'block8': ['InceptionResnetV1/Block8', load_tf_block17_8], + 'last_linear': ['InceptionResnetV1/Bottleneck/weights', load_tf_linear], + 'last_bn': ['InceptionResnetV1/Bottleneck/BatchNorm', load_tf_batchNorm], + # 'logits': ['Logits', load_tf_linear], + } + + print('\nLoad CASIA-Webface-trained weights and save\n') + mdl = InceptionResnetV1(num_classes=10575).eval() + tf_mdl_dir = args.facenet_pb_path + + load_tf_model_weights(mdl, lookup_inception_resnet_v1, tf_mdl_dir) + # print(f'????????') + # data_name = 'casia-webfacexxxxxxx' + # state_dict = mdl.state_dict() + # torch.save(state_dict, f'{tf_mdl_dir}-{data_name}.pt') + + x = torch.rand(64, 3, 160, 160)#.cuda() + # y = resnet(x) + # print(y.shape) + + + f = f"{args.facenet_weights_path}/{args.onnx_save_name}" + torch.onnx.export(mdl, x, f, verbose=False, opset_version=11, + input_names=['input'], output_names=['output'], dynamic_axes=None) + + + +import argparse +def parse_args(): + parser = argparse.ArgumentParser("deploy facenet") + parser.add_argument("--facenet_weights_path", default="", help="onnx model path") + parser.add_argument("--facenet_pb_path", default="", help="") + parser.add_argument("--onnx_save_name", default="", help="") + + return parser.parse_args() +args = parse_args() + +tensorflow2pytorch(args) + + +# device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') +# print('Running on device: {}'.format(device)) + +# # Load pretrained resnet model +# resnet = InceptionResnetV1( +# classify=False, +# pretrained='casia-webface' +# )#.to(device) + +# x = torch.rand(64, 3, 160, 160)#.cuda() +# y = resnet(x) +# print(y.shape) + + +# f = f"{args.facenet_weights_path}/{args.onnx_save_name}" +# torch.onnx.export(resnet, x, f, verbose=False, opset_version=11, input_names=['input'], output_names=['output'], dynamic_axes=None) diff --git a/models/cv/face/facenet/ixrt/utils.py b/models/cv/face/facenet/ixrt/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ab8f213bf6bf629ad073140f4ab886760c707759 --- /dev/null +++ b/models/cv/face/facenet/ixrt/utils.py @@ -0,0 +1,192 @@ +# Copyright (c) 2024, Shanghai Iluvatar CoreX Semiconductor Co., Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import math + +from sklearn.model_selection import KFold +from scipy import interpolate +import numpy as np + + +# LFW functions taken from David Sandberg's FaceNet implementation +def distance(embeddings1, embeddings2, distance_metric=0): + if distance_metric==0: + # Euclidian distance + diff = np.subtract(embeddings1, embeddings2) + dist = np.sum(np.square(diff),1) + elif distance_metric==1: + # Distance based on cosine similarity + dot = np.sum(np.multiply(embeddings1, embeddings2), axis=1) + norm = np.linalg.norm(embeddings1, axis=1) * np.linalg.norm(embeddings2, axis=1) + similarity = dot / norm + dist = np.arccos(similarity) / math.pi + else: + raise 'Undefined distance metric %d' % distance_metric + + return dist + +def calculate_roc(thresholds, embeddings1, embeddings2, actual_issame, nrof_folds=10, distance_metric=0, subtract_mean=False): + assert(embeddings1.shape[0] == embeddings2.shape[0]) + assert(embeddings1.shape[1] == embeddings2.shape[1]) + nrof_pairs = min(len(actual_issame), embeddings1.shape[0]) + nrof_thresholds = len(thresholds) + k_fold = KFold(n_splits=nrof_folds, shuffle=False) + + tprs = np.zeros((nrof_folds,nrof_thresholds)) + fprs = np.zeros((nrof_folds,nrof_thresholds)) + accuracy = np.zeros((nrof_folds)) + + is_false_positive = [] + is_false_negative = [] + + indices = np.arange(nrof_pairs) + + for fold_idx, (train_set, test_set) in enumerate(k_fold.split(indices)): + if subtract_mean: + mean = np.mean(np.concatenate([embeddings1[train_set], embeddings2[train_set]]), axis=0) + else: + mean = 0.0 + dist = distance(embeddings1-mean, embeddings2-mean, distance_metric) + + # Find the best threshold for the fold + acc_train = np.zeros((nrof_thresholds)) + for threshold_idx, threshold in enumerate(thresholds): + _, _, acc_train[threshold_idx], _ ,_ = calculate_accuracy(threshold, dist[train_set], actual_issame[train_set]) + best_threshold_index = np.argmax(acc_train) + for threshold_idx, threshold in enumerate(thresholds): + tprs[fold_idx,threshold_idx], fprs[fold_idx,threshold_idx], _, _, _ = calculate_accuracy(threshold, dist[test_set], actual_issame[test_set]) + _, _, accuracy[fold_idx], is_fp, is_fn = calculate_accuracy(thresholds[best_threshold_index], dist[test_set], actual_issame[test_set]) + + tpr = np.mean(tprs,0) + fpr = np.mean(fprs,0) + is_false_positive.extend(is_fp) + is_false_negative.extend(is_fn) + + return tpr, fpr, accuracy, is_false_positive, is_false_negative + +def calculate_accuracy(threshold, dist, actual_issame): + predict_issame = np.less(dist, threshold) + tp = np.sum(np.logical_and(predict_issame, actual_issame)) + fp = np.sum(np.logical_and(predict_issame, np.logical_not(actual_issame))) + tn = np.sum(np.logical_and(np.logical_not(predict_issame), np.logical_not(actual_issame))) + fn = np.sum(np.logical_and(np.logical_not(predict_issame), actual_issame)) + + is_fp = np.logical_and(predict_issame, np.logical_not(actual_issame)) + is_fn = np.logical_and(np.logical_not(predict_issame), actual_issame) + + tpr = 0 if (tp+fn==0) else float(tp) / float(tp+fn) + fpr = 0 if (fp+tn==0) else float(fp) / float(fp+tn) + acc = float(tp+tn)/dist.size + return tpr, fpr, acc, is_fp, is_fn + +def calculate_val(thresholds, embeddings1, embeddings2, actual_issame, far_target, nrof_folds=10, distance_metric=0, subtract_mean=False): + assert(embeddings1.shape[0] == embeddings2.shape[0]) + assert(embeddings1.shape[1] == embeddings2.shape[1]) + nrof_pairs = min(len(actual_issame), embeddings1.shape[0]) + nrof_thresholds = len(thresholds) + k_fold = KFold(n_splits=nrof_folds, shuffle=False) + + val = np.zeros(nrof_folds) + far = np.zeros(nrof_folds) + + indices = np.arange(nrof_pairs) + + for fold_idx, (train_set, test_set) in enumerate(k_fold.split(indices)): + if subtract_mean: + mean = np.mean(np.concatenate([embeddings1[train_set], embeddings2[train_set]]), axis=0) + else: + mean = 0.0 + dist = distance(embeddings1-mean, embeddings2-mean, distance_metric) + + # Find the threshold that gives FAR = far_target + far_train = np.zeros(nrof_thresholds) + for threshold_idx, threshold in enumerate(thresholds): + _, far_train[threshold_idx] = calculate_val_far(threshold, dist[train_set], actual_issame[train_set]) + if np.max(far_train)>=far_target: + f = interpolate.interp1d(far_train, thresholds, kind='slinear') + threshold = f(far_target) + else: + threshold = 0.0 + + val[fold_idx], far[fold_idx] = calculate_val_far(threshold, dist[test_set], actual_issame[test_set]) + + val_mean = np.mean(val) + far_mean = np.mean(far) + val_std = np.std(val) + return val_mean, val_std, far_mean + +def calculate_val_far(threshold, dist, actual_issame): + predict_issame = np.less(dist, threshold) + true_accept = np.sum(np.logical_and(predict_issame, actual_issame)) + false_accept = np.sum(np.logical_and(predict_issame, np.logical_not(actual_issame))) + n_same = np.sum(actual_issame) + n_diff = np.sum(np.logical_not(actual_issame)) + val = float(true_accept) / float(n_same) + far = float(false_accept) / float(n_diff) + return val, far + + + +def evaluate(embeddings, actual_issame, nrof_folds=10, distance_metric=0, subtract_mean=False): + # Calculate evaluation metrics + thresholds = np.arange(0, 4, 0.01) + embeddings1 = embeddings[0::2] + embeddings2 = embeddings[1::2] + tpr, fpr, accuracy, fp, fn = calculate_roc(thresholds, embeddings1, embeddings2, + np.asarray(actual_issame), nrof_folds=nrof_folds, distance_metric=distance_metric, subtract_mean=subtract_mean) + thresholds = np.arange(0, 4, 0.001) + val, val_std, far = calculate_val(thresholds, embeddings1, embeddings2, + np.asarray(actual_issame), 1e-3, nrof_folds=nrof_folds, distance_metric=distance_metric, subtract_mean=subtract_mean) + return tpr, fpr, accuracy, val, val_std, far, fp, fn + +def add_extension(path): + if os.path.exists(path+'.jpg'): + return path+'.jpg' + elif os.path.exists(path+'.png'): + return path+'.png' + else: + raise RuntimeError('No file "%s" with extension png or jpg.' % path) + +def get_paths(lfw_dir, pairs): + nrof_skipped_pairs = 0 + path_list = [] + issame_list = [] + for pair in pairs: + if len(pair) == 3: + path0 = add_extension(os.path.join(lfw_dir, pair[0], pair[0] + '_' + '%04d' % int(pair[1]))) + path1 = add_extension(os.path.join(lfw_dir, pair[0], pair[0] + '_' + '%04d' % int(pair[2]))) + issame = True + elif len(pair) == 4: + path0 = add_extension(os.path.join(lfw_dir, pair[0], pair[0] + '_' + '%04d' % int(pair[1]))) + path1 = add_extension(os.path.join(lfw_dir, pair[2], pair[2] + '_' + '%04d' % int(pair[3]))) + issame = False + if os.path.exists(path0) and os.path.exists(path1): # Only add the pair if both paths exist + path_list += (path0,path1) + issame_list.append(issame) + else: + nrof_skipped_pairs += 1 + if nrof_skipped_pairs>0: + print('Skipped %d image pairs' % nrof_skipped_pairs) + + return path_list, issame_list + +def read_pairs(pairs_filename): + pairs = [] + with open(pairs_filename, 'r') as f: + for line in f.readlines()[1:]: + pair = line.strip().split() + pairs.append(pair) + return np.array(pairs, dtype=object) \ No newline at end of file