OrtTensorRTProviderOptions trt_options{};
trt_options.device_id = 0;
trt_options.has_user_compute_stream = 1;
trt_options.trt_max_partition_iterations = 1000;
trt_options.trt_min_subgraph_size = 1;
trt_options.trt_engine_decryption_enable = false;
trt_options.trt_dla_core = 0;
trt_options.trt_dla_enable = 0;
trt_options.trt_fp16_enable = 1;
trt_options.trt_int8_enable = 0;
trt_options.trt_max_workspace_size = 2147483648;
trt_options.trt_int8_use_native_calibration_table = 1;
trt_options.trt_engine_cache_enable = 1;
trt_options.trt_engine_cache_path = "./trtcache";
trt_options.trt_dump_subgraphs = 1;
session_options.AppendExecutionProvider_TensorRT(trt_options);
可以使用Linux(x86_64)版本安装
可以通过 nvidia-smi
命令查看当前的驱动版本和最高支持的 CUDA 版本。
Note: Because of CUDA Minor Version Compatibility, Onnx Runtime built with CUDA 11.4 should be compatible with any CUDA 11.x version. Please reference Nvidia CUDA Minor Version Compatibility.
本文使用的模型如下图:
根据python的代码和环境,我是用的 opset 版本是 12。
parser.add_argument('--opset', type=int, default=12, help='ONNX: opset version')
onnx的版本是 1.12.0,onnxruntime在Python中使用1.13.1,这里也将使用1.13.1:
CUDA和TensorRT等的版本:(Windows 10)Yolov5-5.0模型的TensorRT加速+ C++部署 + VS2019封装 dll (CMake) + Qt调用
cuda 11.1
cudnn 8.5.0
TensorRT 8.2.1.8
Opencv 4.5.5
CMake 3.24.2
Python可以直接pip
安装onnxruntime,但是C++就需要自己设置,并且还要考虑兼容性
通过How to use ONNX Runtime的选项进入 Get Started,之后进入 Get Started / C++。可以通过 Builds 查看兼容性。
我这里主要是学习 Load and run the model with ONNX Runtime.
可以看到 C++ 版本的 ONNX Runtime 的使用,有两种选项:
Option 1: download a prebuilt package
我下载的版本:onnxruntime-win-x64-1.13.1.zip,下载好直接解压就可以使用了。
Option 2: build from source
根据上上图中的 Option 2 中的引导进行操作即可。
#include
#include
#include
#include
#include
#include
#include
using namespace cv;
Mat resize_image(Mat srcimg, int* newh, int* neww, int* top, int* left)
{
int srch = srcimg.rows, srcw = srcimg.cols;
int inpHeight = 640;
int inpWidth = 640;
*newh = inpHeight;
*neww = 640;
bool keep_ratio = true;
Mat dstimg;
if (keep_ratio && srch != srcw) {
float hw_scale = (float)srch / srcw;
if (hw_scale > 1) {
*newh = inpHeight;
*neww = int(inpWidth / hw_scale);
resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA);
*left = int((inpWidth - *neww) * 0.5);
copyMakeBorder(dstimg, dstimg, 0, 0, *left, inpWidth - *neww - *left, BORDER_CONSTANT, 114);
}
else {
*newh = (int)inpHeight * hw_scale;
*neww = inpWidth;
resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA);
*top = (int)(inpHeight - *newh) * 0.5;
copyMakeBorder(dstimg, dstimg, *top, inpHeight - *newh - *top, 0, 0, BORDER_CONSTANT, 114);
}
}
else {
resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA);
}
return dstimg;
}
int main(int argc, char* argv[])
{
//std::string imgpath = "images/bus.jpg";
std::string imgpath = "images/real.jpg";
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_ERROR);//设置OpenCV只输出错误日志
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "yolov5s-5.0");
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
#ifdef _WIN32
//const wchar_t* model_path = L"yolov5s.onnx";
const wchar_t* model_path = L"sim_best20221027.onnx";
#else
const char* model_path = "yolov5s.onnx";
#endif
std::vector<std::string> class_names;
//std::string classesFile = "class.names";
std::string classesFile = "myclass.txt";
std::ifstream ifs(classesFile.c_str());
std::string line;
while (getline(ifs, line)) class_names.push_back(line);
Ort::Session session(env, model_path, session_options);
// print model input layer (node names, types, shape etc.)
Ort::AllocatorWithDefaultOptions allocator;
// print number of model input nodes
size_t num_input_nodes = session.GetInputCount();
std::vector<const char*> input_node_names = { "images"};
std::vector<const char*> output_node_names = { "output0"};
size_t input_tensor_size = 3*640*640;
std::vector<float> input_tensor_values(input_tensor_size);
cv::Mat srcimg = cv::imread(imgpath);
int newh = 0, neww = 0, padh = 0, padw = 0;
Mat dstimg = resize_image(srcimg, &newh, &neww, &padh, &padw);//Padded resize
//resizedImage.convertTo(floatImage, CV_32FC3, 1 / 255.0);
for (int c = 0; c < 3; c++)
{
for (int i = 0; i < 640; i++)
{
for (int j = 0; j < 640; j++)
{
float pix = dstimg.ptr<uchar>(i)[j * 3 + 2 - c];
input_tensor_values[c * 640 * 640 + i * 640 + size_t(j)] = pix / 255.0;
}
}
}
// create input tensor object from data values
std::vector<int64_t> input_node_dims = { 1, 3, 640, 640 };
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), input_node_dims.size());
std::vector<Ort::Value> ort_inputs;
ort_inputs.push_back(std::move(input_tensor));
// score model & input tensor, get back output tensor
std::vector<Ort::Value> output_tensors = session.Run(Ort::RunOptions{ nullptr }, input_node_names.data(), ort_inputs.data(), input_node_names.size(), output_node_names.data(), output_node_names.size());
// Get pointer to output tensor float values
const float* rawOutput = output_tensors[0].GetTensorData<float>();
//generate proposals
std::vector<int64_t> outputShape = output_tensors[0].GetTensorTypeAndShapeInfo().GetShape();
size_t count = output_tensors[0].GetTensorTypeAndShapeInfo().GetElementCount();
std::vector<float> output(rawOutput, rawOutput + count);
std::vector<cv::Rect> boxes;
std::vector<float> confs;
std::vector<int> classIds;
int numClasses = (int)outputShape[2] - 5;
int elementsInBatch = (int)(outputShape[1] * outputShape[2]);
float confThreshold = 0.5;
for (auto it = output.begin(); it != output.begin() + elementsInBatch; it += outputShape[2])
{
float clsConf = *(it+4);//object scores
if (clsConf > confThreshold)
{
int centerX = (int)(*it);
int centerY = (int)(*(it + 1));
int width = (int)(*(it + 2));
int height = (int)(*(it + 3));
int x1 = centerX - width / 2;
int y1 = centerY - height / 2;
boxes.emplace_back(cv::Rect(x1, y1, width, height));
// first 5 element are x y w h and obj confidence
int bestClassId = -1;
float bestConf = 0.0;
for (int i = 5; i < numClasses + 5; i++)
{
if ((*(it + i)) > bestConf)
{
bestConf = it[i];
bestClassId = i - 5;
}
}
//confs.emplace_back(bestConf * clsConf);
confs.emplace_back(clsConf);
classIds.emplace_back(bestClassId);
}
}
float iouThreshold = 0.5;
std::vector<int> indices;
// Perform non maximum suppression to eliminate redundant overlapping boxes with
// lower confidences
cv::dnn::NMSBoxes(boxes, confs, confThreshold, iouThreshold, indices);
//随机数种子
RNG rng((unsigned)time(NULL));
for (size_t i = 0; i < indices.size(); ++i)
{
int index = indices[i];
int colorR = rng.uniform(0, 255);
int colorG = rng.uniform(0, 255);
int colorB = rng.uniform(0, 255);
//保留两位小数
float scores = round(confs[index] * 100) / 100;
std::ostringstream oss;
oss << scores;
rectangle(dstimg, Point(boxes[index].tl().x, boxes[index].tl().y), Point(boxes[index].br().x, boxes[index].br().y), Scalar(colorR, colorG, colorB), 1.5);
putText(dstimg, class_names[classIds[index]] + " " + oss.str(), Point(boxes[index].tl().x, boxes[index].tl().y - 5), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(colorR, colorG, colorB), 2);
}
imshow("检测结果", dstimg);
cv::waitKey();
}
这里有一个大坑
:原因是
直接赋值可以给 ONNX Runtime 的 API 使用
std::vector<const char*> input_node_names = { "images"};
std::vector<const char*> output_node_names = { "output0"};
这样用 ONNX Runtime 的API获得的 input_node_names 使用,会抛出异常,经过观察,两者的内容都是“images”的情况下区别就是 ‘i’
的地址 const char* _ptr64
类型的值不一样
// print number of model input nodes
size_t num_input_nodes = session.GetInputCount();
for (int i = 0; i < num_input_nodes; i++)
{
Ort::AllocatedStringPtr input_name_Ptr = session.GetInputNameAllocated(i, allocator);
input_node_names.push_back(input_name_Ptr.get());//"images" char * __ptr64
Ort::TypeInfo input_type_info = session.GetInputTypeInfo(i);
auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();//Get OrtTensorTypeAndShapeInfo from an OrtTypeInfo.
auto input_dims = input_tensor_info.GetShape();//Uses GetDimensionsCount & GetDimensions to return a std::vector of the shape.
input_node_dims.push_back(input_dims);//input_node_dims[0] = vector{1, 3, 640, 640}
}
InferenceSession
是 ONNX Runtime 的主要类。它用于加载和运行 ONNX 模型,以及指定环境和应用程序配置选项deprecated(强烈反对 / 强烈抨击)
的函数,不应该使用,应该使用其建议的替代版本 。CreateCpu()
static MemoryInfo Ort::MemoryInfo::CreateCpu(OrtAllocatorType type, OrtMemType mem_type1)
OrtStatus
that contains error details will be returned.OrtApi::ReleaseStatus
to free this pointer.GetTensorMutableData ()
或者 GetTensorData ()
,而且区别在于后者的输入和返回都是 const 。其 Wraps 的函数 GetTensorMutableData()如下:
获取指向张量内原始数据(row data)的指针。
用于直接读/写/修改内部张量数据。
size_t GetInputCount () const
size_t GetOutputCount () const
Run()
Value
的CreateTensor()
函数获得 Session()
GetInputNameAllocated()
和 GetOutputNameAllocated()
应该换成
GetInputNameAllocated()和GetOutputNameAllocated()。unique_ptr
)的实例,详细介绍见 2.3.1.8。TypeInfo GetInputTypeInfo (size_t index) const
TypeInfo GetOutputTypeInfo (size_t index) const
Env()
SessionOptions()
SetGraphOptimizationLevel()
SetIntraOpNumThreads()
Unowned< TensorTypeAndShapeInfo > GetTensorTypeAndShapeInfo () const
std::vector< int64_t > GetShape () const
size_t GetElementCount () const
GetTensorShapeElementCount()
,返回数据总数(all dimensions multiplied by each other),0 维返回 1,有小于0 的维度返回 -1。AllocatedStringPtr:注意这里有坑,AllocatedStringPtr 是智能指针,一定要注意生命周期的问题。
unique_ptr typedef 用于拥有 OrtAllocators 分配的字符串,并在作用域结束时释放它们。给定分配器的生命周期必须超过分配字符串 Ptr 实例的生命周期
要想对图像进行推理,就需要使用OpenCV对图像进行处理
cv::imread()
读取图片cv::imshow()
显示图片imshow()
,后面要搭配waitkey()
函数,否则无法正常显示。waitkey()
的解释:pollKey
无需等待即可轮询按键事件。它返回按下的键的代码,或者 -1 (如果自上次调用以来没有按下任何键)。waitKey
。waitKey
和pollKey
是HighGUI中唯一可以获取和处理GUI 事件的方法,因此需要定期调用其中之一以进行正常的事件处理,除非HighGUI 在负责事件处理的环境中使用。cv::dnn::NMSBoxes
进行非极大值抑制NMSBoxes() [1/3]
cv::rectangle()
画矩形框rectangle
cv::putText()
写文字putText()
官方文档:cv::Mat Class Reference
Mat(int rows, int cols, int type)
channels()
获取矩阵的通道数颜色通道的转换:cvtColor(cv_image, cv_image, cv::COLOR_BGR2RGB);
【OpenCV3】颜色空间转换——cv::cvtColor()详解
dims
获取矩阵的维数tips:dims 和 channels()的区别
size()
获取矩阵的尺寸convertTo()
转换矩阵的格式cv::Mat::convertTo(),可以实现数据类型的转换,通道的顺序转换,数据的位数转换等,具体实现自己搜索。
ptr() / at() / 地址
获取矩阵某个像素点的值官方文档: ptr() [1/20]
uchar* cv::Mat::ptr ( int i0 = 0 )
返回指向指定矩阵行的指针。
float pix = img.at(i, j)[c];
float pix = (int)(*(img.data + img.step[0] * i + img.step[1] * j + c));
float pix = img.ptr(i)[j * channel + channel-1 - c];//channel-1 是因为数组从0开始
cout<(i, j)<可输出三通道的值,如[230 222 102],而cout<(i, j)[c]< 输出的是uchar类型的乱码,需要数据类型转换才能输出值。
int,uchar,Vec3b
或者还是其他的,参考 opencv cv::Mat数据类型总结 的第二个表格。float pix = img.ptr(i)[j * 3 + 2 - c];
推理模型的步骤:
OpenCV读取图像—>对图像进行处理,获得合适尺寸的图像—>进行推理—>获得推理的结果
输入数据的处理最好采用yolov5提出的 Padded resize
方法来实现
Padded Resize
:可以保持图片的长宽比例,剩下的部分采用灰色填充,从而以填充边界(通常是灰色填充)的方式来保持原始图片的长宽比例,同时又满足模型正方形输入的需要。letterbox()
方法def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
# Resize and pad image while meeting stride-multiple constraints
shape = img.shape[:2] # current shape [height, width]
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
# Scale ratio (new / old)
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
if not scaleup: # only scale down, do not scale up (for better test mAP)
r = min(r, 1.0)
# Compute padding
ratio = r, r # width, height ratios
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
if auto: # minimum rectangle
dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding
elif scaleFill: # stretch
dw, dh = 0.0, 0.0
new_unpad = (new_shape[1], new_shape[0])
ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios
dw /= 2 # divide padding into 2 sides
dh /= 2
if shape[::-1] != new_unpad: # resize
img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border
return img, ratio, (dw, dh)
cv::resize()
函数参考:官方文档 cv::resize()
先看源码:位于imgproc.hpp
CV_EXPORTS_W void resize( InputArray src, OutputArray dst,
Size dsize, double fx = 0, double fy = 0,
int interpolation = INTER_LINEAR );
参数介绍:
dsize
and the same type as src . 输出图像,有着 dsize
的尺寸和 src
的类型。dsize = Size(round(fx*src.cols), round(fy*src.rows))
,C++的函数round()
和OpenCV的类Size
参考:C++:round函数用法 和 Opencv的Size类-尺寸类,注意Size
是先宽后高cv::copyMakeBorder()
函数参考:官方文档:copyMakeBorder(),作用是 Forms a border around an image.
CV_EXPORTS_W void copyMakeBorder(InputArray src, OutputArray dst,
int top, int bottom, int left, int right,
int borderType, const Scalar& value = Scalar() );
参数介绍:
dsize
and the same type as src . 输出图像,有着 dsize
的尺寸和 src
的类型。且尺寸为Size(src.cols+left+right, src.rows+top+bottom)
Scalar(B,G,R)
,如果直接写一个数字X,相当于Scalar(X,0,0)
,参考:opencv中的Scalar()函数。以及官方示例:typedef Scalar_ cv::Scalar需要了解的知识:[-Opencv中数据类型CV_8U, CV_16U, CV_16S, CV_32F 以及 CV_64F是什么?,我的应是从CV_8UC3
转换成CV_32FC3
。
此处需要注意:OpenCV读取的图像通道顺序是是BGR,归一化作为输入应该是RGB顺序,同时我的输入类型是float32,也要进行转换。
构造输入数据使用的函数:Ord::Value 的 公共成员函数 CreateTensor()
推理主要使用函数: Ord::Session 的 公共成员函数 Run
仔细研究函数的输入,将自己的数据构建成它需要的数据格式以及自己模型的输入数据类型和格式,然后进行输入就可以了。
对于我的模型,属于应当是 float32(1, 3, 640, 640)
typedef unsigned short wchar_t;
看 stdint.h
的源码,可以看到除了数据类型别名,定义了很多数据相关的宏,可以用来检测数据的大小,避免溢出等问题。
//
// stdint.h
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The C Standard Library header.
//
#pragma once
#define _STDINT
#include
#if _VCRT_COMPILER_PREPROCESSOR
#pragma warning(push)
#pragma warning(disable: _VCRUNTIME_DISABLED_WARNINGS)
typedef signed char int8_t;
typedef short int16_t;
typedef int int32_t;
typedef long long int64_t;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef signed char int_least8_t;
typedef short int_least16_t;
typedef int int_least32_t;
typedef long long int_least64_t;
typedef unsigned char uint_least8_t;
typedef unsigned short uint_least16_t;
typedef unsigned int uint_least32_t;
typedef unsigned long long uint_least64_t;
typedef signed char int_fast8_t;
typedef int int_fast16_t;
typedef int int_fast32_t;
typedef long long int_fast64_t;
typedef unsigned char uint_fast8_t;
typedef unsigned int uint_fast16_t;
typedef unsigned int uint_fast32_t;
typedef unsigned long long uint_fast64_t;
typedef long long intmax_t;
typedef unsigned long long uintmax_t;
// These macros must exactly match those in the Windows SDK's intsafe.h.
#define INT8_MIN (-127i8 - 1)
#define INT16_MIN (-32767i16 - 1)
#define INT32_MIN (-2147483647i32 - 1)
#define INT64_MIN (-9223372036854775807i64 - 1)
#define INT8_MAX 127i8
#define INT16_MAX 32767i16
#define INT32_MAX 2147483647i32
#define INT64_MAX 9223372036854775807i64
#define UINT8_MAX 0xffui8
#define UINT16_MAX 0xffffui16
#define UINT32_MAX 0xffffffffui32
#define UINT64_MAX 0xffffffffffffffffui64
#define INT_LEAST8_MIN INT8_MIN
#define INT_LEAST16_MIN INT16_MIN
#define INT_LEAST32_MIN INT32_MIN
#define INT_LEAST64_MIN INT64_MIN
#define INT_LEAST8_MAX INT8_MAX
#define INT_LEAST16_MAX INT16_MAX
#define INT_LEAST32_MAX INT32_MAX
#define INT_LEAST64_MAX INT64_MAX
#define UINT_LEAST8_MAX UINT8_MAX
#define UINT_LEAST16_MAX UINT16_MAX
#define UINT_LEAST32_MAX UINT32_MAX
#define UINT_LEAST64_MAX UINT64_MAX
#define INT_FAST8_MIN INT8_MIN
#define INT_FAST16_MIN INT32_MIN
#define INT_FAST32_MIN INT32_MIN
#define INT_FAST64_MIN INT64_MIN
#define INT_FAST8_MAX INT8_MAX
#define INT_FAST16_MAX INT32_MAX
#define INT_FAST32_MAX INT32_MAX
#define INT_FAST64_MAX INT64_MAX
#define UINT_FAST8_MAX UINT8_MAX
#define UINT_FAST16_MAX UINT32_MAX
#define UINT_FAST32_MAX UINT32_MAX
#define UINT_FAST64_MAX UINT64_MAX
#ifdef _WIN64
#define INTPTR_MIN INT64_MIN
#define INTPTR_MAX INT64_MAX
#define UINTPTR_MAX UINT64_MAX
#else
#define INTPTR_MIN INT32_MIN
#define INTPTR_MAX INT32_MAX
#define UINTPTR_MAX UINT32_MAX
#endif
#define INTMAX_MIN INT64_MIN
#define INTMAX_MAX INT64_MAX
#define UINTMAX_MAX UINT64_MAX
#define PTRDIFF_MIN INTPTR_MIN
#define PTRDIFF_MAX INTPTR_MAX
#ifndef SIZE_MAX
// SIZE_MAX definition must match exactly with limits.h for modules support.
#ifdef _WIN64
#define SIZE_MAX 0xffffffffffffffffui64
#else
#define SIZE_MAX 0xffffffffui32
#endif
#endif
#define SIG_ATOMIC_MIN INT32_MIN
#define SIG_ATOMIC_MAX INT32_MAX
#define WCHAR_MIN 0x0000
#define WCHAR_MAX 0xffff
#define WINT_MIN 0x0000
#define WINT_MAX 0xffff
#define INT8_C(x) (x)
#define INT16_C(x) (x)
#define INT32_C(x) (x)
#define INT64_C(x) (x ## LL)
#define UINT8_C(x) (x)
#define UINT16_C(x) (x)
#define UINT32_C(x) (x ## U)
#define UINT64_C(x) (x ## ULL)
#define INTMAX_C(x) INT64_C(x)
#define UINTMAX_C(x) UINT64_C(x)
#pragma warning(pop) // _VCRUNTIME_DISABLED_WARNINGS
#endif // _VCRT_COMPILER_PREPROCESSOR
根据选取的推理函数 inline std::vector
设置输入,我选择的是有三种形式,我选择的是第一种:Run() [1/3]
std::vector< Value > Ort::Session::Run ( const RunOptions & run_options,
const char *const * input_names,
const Value * input_values,
size_t input_count,
const char *const * output_names,
size_t output_count
)
调用者提供输入列表和要返回的所需输出列表。
std::vector output_values;
作为输出。vector input_names = { "images"};
那么 input_names.data()
(因为要使用指针),当然,多输入模型可以有多个参数。input_names.size()
。然后进入下面的函数:
inline void Session::Run(const RunOptions& run_options, const char* const* input_names, const Value* input_values, size_t input_count,
const char* const* output_names, Value* output_values, size_t output_count) {
static_assert(sizeof(Value) == sizeof(OrtValue*), "Value is really just an array of OrtValue* in memory, so we can reinterpret_cast safely");
auto ort_input_values = reinterpret_cast<const OrtValue**>(const_cast<Value*>(input_values));
auto ort_output_values = reinterpret_cast<OrtValue**>(output_values);
ThrowOnError(GetApi().Run(p_, run_options, input_names, ort_input_values, input_count, output_names, output_count, ort_output_values));
}
可以看到:
input_values
的操作auto ort_input_values = reinterpret_cast(const_cast(input_values))
const
属性,然后转换为const OrtValue**
的值output_values
进行 reinterpret_cast
操作,最终获得了 ort_input_values
和 ort_output_values
。OrtValue **
类型)进行了返回,该类型被封装。ONNXRuntime 是使用MSVC进行编译的,无法再MinGW编译器使用,参考如下:
error: unknown type name ‘Frees_ptr_opt’
error: ‘Frees_ptr_opt’ has not been declared