tensorflow c++ API

问题描述:

解决方法:参考http://blog.sina.com.cn/s/blog_5d5a19fe0102yvfc.html

logging.h报错 ”error C2589: “(”: “::”右边的非法标记“问题,需要:

项目属性   ——> C/C++ ——> 预处理器 ——> 预处理器定义 (此处添加预定义编译开关   NOMINMAX)

结果如下:

源代码如下:


/*
* test tensorflow_cc c++ successfully
* load mnist.pb model successfully
* 2019.6.28
* wang dan
* conference:https://github.com/tensorflow/tensorflow/blob/master/tensorflow/examples/label_image
* */

#include <fstream>
#include <utility>
#include <vector>
#include <Eigen/Core>
#include <Eigen/Dense>

#include "tensorflow/cc/ops/const_op.h"
#include "tensorflow/cc/ops/image_ops.h"
#include "tensorflow/cc/ops/standard_ops.h"
#include "tensorflow/core/framework/graph.pb.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/core/graph/default_device.h"
#include "tensorflow/core/graph/graph_def_builder.h"
#include "tensorflow/core/lib/core/errors.h"
#include "tensorflow/core/lib/core/stringpiece.h"
#include "tensorflow/core/lib/core/threadpool.h"
#include "tensorflow/core/lib/io/path.h"
#include "tensorflow/core/lib/strings/stringprintf.h"
#include "tensorflow/core/platform/env.h"
#include "tensorflow/core/platform/init_main.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/types.h"
#include "tensorflow/core/public/session.h"
#include "tensorflow/core/util/command_line_flags.h"

using namespace std;
using namespace tensorflow;
using namespace tensorflow::ops;
using tensorflow::Flag;
using tensorflow::Tensor;
using tensorflow::Status;
using tensorflow::string;
using tensorflow::int32;

static Status ReadEntireFile(tensorflow::Env* env, const string& filename,
    Tensor* output) {
    tensorflow::uint64 file_size = 0;
    TF_RETURN_IF_ERROR(env->GetFileSize(filename, &file_size));

    string contents;
    contents.resize(file_size);

    std::unique_ptr<tensorflow::RandomAccessFile> file;
    TF_RETURN_IF_ERROR(env->NewRandomAccessFile(filename, &file));

    tensorflow::StringPiece data;
    TF_RETURN_IF_ERROR(file->Read(0, file_size, &data, &(contents)[0]));
    if (data.size() != file_size) {
        return tensorflow::errors::DataLoss("Truncated read of '", filename,
            "' expected ", file_size, " got ",
            data.size());
    }
    //  output->scalar<string>()() = data.ToString();
    output->scalar<string>()() = string(data);
    return Status::OK();
}

Status ReadTensorFromImageFile(const string& file_name, const int input_height,
    const int input_width, const float input_mean,
    const float input_std,
    std::vector<Tensor>* out_tensors) {
    auto root = tensorflow::Scope::NewRootScope();
    using namespace ::tensorflow::ops;

    string input_name = "file_reader";
    string output_name = "normalized";

    // read file_name into a tensor named input
    Tensor input(tensorflow::DT_STRING, tensorflow::TensorShape());
    TF_RETURN_IF_ERROR(
        ReadEntireFile(tensorflow::Env::Default(), file_name, &input));

    // use a placeholder to read input data
    auto file_reader =
        Placeholder(root.WithOpName("input"), tensorflow::DataType::DT_STRING);

    std::vector<std::pair<string, tensorflow::Tensor>> inputs = {
        { "input", input },
    };

    // Now try to figure out what kind of file it is and decode it.
    const int wanted_channels = 1;
    //  tensorflow::Output image_reader;
    //  if (tensorflow::StringPiece(file_name).ends_with(".png")) {
    //    image_reader = DecodePng(root.WithOpName("png_reader"), file_reader,
    //                             DecodePng::Channels(wanted_channels));
    //  } else if (tensorflow::StringPiece(file_name).ends_with(".gif")) {
    //    // gif decoder returns 4-D tensor, remove the first dim
    //    image_reader =
    //        Squeeze(root.WithOpName("squeeze_first_dim"),
    //                DecodeGif(root.WithOpName("gif_reader"), file_reader));
    //  } else if (tensorflow::StringPiece(file_name).ends_with(".bmp")) {
    //    image_reader = DecodeBmp(root.WithOpName("bmp_reader"), file_reader);
    //  } else {
    //    // Assume if it's neither a PNG nor a GIF then it must be a JPEG.
    //    image_reader = DecodeJpeg(root.WithOpName("jpeg_reader"), file_reader,
    //                              DecodeJpeg::Channels(wanted_channels));
    //  }

    tensorflow::Output image_reader;
    if (tensorflow::str_util::EndsWith(file_name, ".png")) {
        image_reader = DecodePng(root.WithOpName("png_reader"), file_reader,
            DecodePng::Channels(wanted_channels));
    }
    else if (tensorflow::str_util::EndsWith(file_name, ".gif")) {
        // gif decoder returns 4-D tensor, remove the first dim
        image_reader =
            Squeeze(root.WithOpName("squeeze_first_dim"),
                DecodeGif(root.WithOpName("gif_reader"), file_reader));
    }
    else if (tensorflow::str_util::EndsWith(file_name, ".bmp")) {
        image_reader = DecodeBmp(root.WithOpName("bmp_reader"), file_reader);
    }
    else {
        // Assume if it's neither a PNG nor a GIF then it must be a JPEG.
        image_reader = DecodeJpeg(root.WithOpName("jpeg_reader"), file_reader,
            DecodeJpeg::Channels(wanted_channels));
    }
    // Now cast the image data to float so we can do normal math on it.
    auto float_caster =
        Cast(root.WithOpName("float_caster"), image_reader, tensorflow::DT_FLOAT);

    auto dims_expander = ExpandDims(root.WithOpName("expand"), float_caster, 0);

    float input_max = 255;
    Div(root.WithOpName("div"), dims_expander, input_max);

    tensorflow::GraphDef graph;
    TF_RETURN_IF_ERROR(root.ToGraphDef(&graph));

    std::unique_ptr<tensorflow::Session> session(
        tensorflow::NewSession(tensorflow::SessionOptions()));
    TF_RETURN_IF_ERROR(session->Create(graph));
    //  std::vector<Tensor> out_tensors;
    //  TF_RETURN_IF_ERROR(session->Run({}, {output_name + ":0", output_name + ":1"},
    //                                    {}, &out_tensors));

    TF_RETURN_IF_ERROR(session->Run({ inputs }, { "div" }, {}, out_tensors));
    return Status::OK();
}


int main()
{
    Session* session;
    Status status = NewSession(SessionOptions(), &session);//创建新会话Session

    string model_path = "../frozen_inference_graph.pb";
    GraphDef graphdef; //Graph Definition for current model

    Status status_load = ReadBinaryProto(Env::Default(), model_path, &graphdef); //从pb文件中读取图模型;
    if (!status_load.ok()) {
        std::cout << "ERROR: Loading model failed..." << model_path << std::endl;
        std::cout << status_load.ToString() << "\n";
        return -1;
    }
    Status status_create = session->Create(graphdef); //将模型导入会话Session中;
    if (!status_create.ok()) {
        std::cout << "ERROR: Creating graph in session failed..." << status_create.ToString() << std::endl;
        return -1;
    }
    cout << "Session successfully created." << endl;
    string image_path = "../image1.jpg";
    int input_height = 28;
    int input_width = 28;
    int input_mean = 0;
    int input_std = 1;
    std::vector<Tensor> resized_tensors;
    Status read_tensor_status =
        ReadTensorFromImageFile(image_path, input_height, input_width, input_mean,
            input_std, &resized_tensors);
    if (!read_tensor_status.ok()) {
        LOG(ERROR) << read_tensor_status;
        cout << "resing error" << endl;
        return -1;
    }

    const Tensor& resized_tensor = resized_tensors[0];
    std::cout << resized_tensor.DebugString() << endl;

    vector<tensorflow::Tensor> outputs;
    string output_node = "softmax";
    Status status_run = session->Run({ { "inputs", resized_tensor } }, { output_node }, {}, &outputs);

    if (!status_run.ok()) {
        std::cout << "ERROR: RUN failed..." << std::endl;
        std::cout << status_run.ToString() << "\n";
        return -1;
    }
    //Fetch output value
    std::cout << "Output tensor size:" << outputs.size() << std::endl;
    for (std::size_t i = 0; i < outputs.size(); i++) {
        std::cout << outputs[i].DebugString() << endl;
    }

    Tensor t = outputs[0];                   // Fetch the first tensor
    int ndim2 = t.shape().dims();             // Get the dimension of the tensor
    auto tmap = t.tensor<float, 2>();        // Tensor Shape: [batch_size, target_class_num]
    int output_dim = t.shape().dim_size(1);  // Get the target_class_num from 1st dimension
    std::vector<double> tout;

    // Argmax: Get Final Prediction Label and Probability
    int output_class_id = -1;
    double output_prob = 0.0;
    for (int j = 0; j < output_dim; j++)
    {
        std::cout << "Class " << j << " prob:" << tmap(0, j) << "," << std::endl;
        if (tmap(0, j) >= output_prob) {
            output_class_id = j;
            output_prob = tmap(0, j);
        }
    }

    std::cout << "Final class id: " << output_class_id << std::endl;
    std::cout << "Final class prob: " << output_prob << std::endl;

    return 0;
}
 

输出为下,具体有何错误,或者如何识别目标……接下来继续奋斗吧

换了一个程序,程序如下:

代码来自 https://blog.csdn.net/daydayup_668819/article/details/61196640


//一个最小的但有用的C++的例子展示了如何加载一个ImageNet式目标识别tensorflow模型,准备输入图像,通过图运行和解释结果。
//它的目的是有尽可能少的依赖和尽可能清楚,所以它比它在产品代码中更详细。特别是,自动使用
//TensorFlow的大量返回值的类型能删除大量的样板,但我发现在样本代码中,显式类型是有用的,使其查找相关的类变得简单。
//要使用它,在工作目录中learning/brain/tutorials/label_image /data/文件夹下面编译并运行,你应该能看到Lena图像这个例子输出的前五个标签。然后你可以自定义它使用您自己的模型或图像,通过在main()函数中改变文件名。
//默认包括的googlenet_graph.pb文件是创建自Inception。
#include <fstream>
#include <vector>

#include "tensorflow/cc/ops/const_op.h"
#include "tensorflow/cc/ops/image_ops.h"
#include "tensorflow/cc/ops/standard_ops.h"
#include "tensorflow/core/framework/graph.pb.h"
#include "tensorflow/core/framework/tensor.h"
#include "tensorflow/core/graph/default_device.h"
#include "tensorflow/core/graph/graph_def_builder.h"
#include "tensorflow/core/lib/core/errors.h"
#include "tensorflow/core/lib/core/stringpiece.h"
#include "tensorflow/core/lib/core/threadpool.h"
#include "tensorflow/core/lib/io/path.h"
#include "tensorflow/core/lib/strings/stringprintf.h"
#include "tensorflow/core/platform/init_main.h"
#include "tensorflow/core/platform/logging.h"
#include "tensorflow/core/platform/types.h"
#include "tensorflow/core/public/session.h"
#include "tensorflow/core/util/command_line_flags.h"

// 这些都是公共类,它很方便的引用没有命名空间。
using tensorflow::Flag;
using tensorflow::Tensor;
using tensorflow::Status;
using tensorflow::string;
using tensorflow::int32;

// 取一个文件名,从它加载一个标签列表,每行一个,返回一个字符串的向量。它填充空字符串,所以结果的长度是16的倍数,因为我们的模型期望这样。

Status ReadLabelsFile(string file_name, std::vector<string>* result,
    size_t* found_label_count) {
    std::ifstream file(file_name);
    if (!file) {
        return tensorflow::errors::NotFound("Labels file ", file_name,
            " not found.");
    }
    result->clear();
    string line;
    while (std::getline(file, line)) {
        result->push_back(line);
    }
    *found_label_count = result->size();
    const int padding = 16;
    while (result->size() % padding) {
        result->emplace_back();
    }
    return Status::OK();
}

// 给定一个图像文件名,读入数据,尝试将其解码为图像,将它调整到所要求的大小,然后按需要缩放值。
Status ReadTensorFromImageFile(string file_name, const int input_height,
    const int input_width, const float input_mean,
    const float input_std,
    std::vector<Tensor>* out_tensors) {
    auto root = tensorflow::Scope::NewRootScope();
    using namespace ::tensorflow::ops;  // NOLINT(build/namespaces)

    string input_name = "file_reader";
    string output_name = "normalized";
    auto file_reader = tensorflow::ops::ReadFile(root.WithOpName(input_name),
        file_name);
    // 现在尝试找出它是什么样的文件,并解码它。
    const int wanted_channels = 3;
    tensorflow::Output image_reader;
    //if (tensorflow::StringPiece(file_name).ends_with(".png")) {
    //    image_reader = DecodePng(root.WithOpName("png_reader"), file_reader,
    //        DecodePng::Channels(wanted_channels));
    //}
    //else if (tensorflow::StringPiece(file_name).ends_with(".gif")) {
    //    image_reader = DecodeGif(root.WithOpName("gif_reader"), file_reader);
    //}
    //else {
    //    // 假设图像不是 PNG 或是 GIF 格式,就一定是JPEG格式.
    //    image_reader = DecodeJpeg(root.WithOpName("jpeg_reader"), file_reader,
    //        DecodeJpeg::Channels(wanted_channels));
    //}
    // 现在把图像数据cast为float类型,这样我们可以做常规的数学计算.
    auto float_caster =
        Cast(root.WithOpName("float_caster"), image_reader, tensorflow::DT_FLOAT);
    // 在TensorFlow里,图像ops的约定是所有图像都能批处理,以使它们是具有[批,高度,宽度,通道]索引的四维数组。因为我们只有一个图像,我们
    //用expanddims()函数开始添加1维的批尺寸。

    auto dims_expander = ExpandDims(root, float_caster, 0);
    // 双线调整图像大小以适合其要求的尺寸。
    auto resized = ResizeBilinear(
        root, dims_expander,
        Const(root.WithOpName("size"), { input_height, input_width }));
    // 减去mean,除以scale
    Div(root.WithOpName(output_name), Sub(root, resized, { input_mean }),
    { input_std });

    // 这个运行我们刚刚建的GraphDef网络模型的定义,返回输出张量的结果
    tensorflow::GraphDef graph;
    TF_RETURN_IF_ERROR(root.ToGraphDef(&graph));

    std::unique_ptr<tensorflow::Session> session(
        tensorflow::NewSession(tensorflow::SessionOptions()));
    TF_RETURN_IF_ERROR(session->Create(graph));
    TF_RETURN_IF_ERROR(session->Run({}, { output_name }, {}, out_tensors));
    return Status::OK();
}

// 从磁盘读取一个模型图像的定义,创建session对象,你可以使用它来运行
Status LoadGraph(string graph_file_name,
    std::unique_ptr<tensorflow::Session>* session) {
    tensorflow::GraphDef graph_def;
    Status load_graph_status =
        ReadBinaryProto(tensorflow::Env::Default(), graph_file_name, &graph_def);
    if (!load_graph_status.ok()) {
        return tensorflow::errors::NotFound("Failed to load compute graph at '",
            graph_file_name, "'");
    }
    session->reset(tensorflow::NewSession(tensorflow::SessionOptions()));
    Status session_create_status = (*session)->Create(graph_def);
    if (!session_create_status.ok()) {
        return session_create_status;
    }
    return Status::OK();
}

// 分析Inception图像的输出,以检索最高分数和它们在张量中的位置,这些位置对应于类别。
Status GetTopLabels(const std::vector<Tensor>& outputs, int how_many_labels,
    Tensor* indices, Tensor* scores) {
    auto root = tensorflow::Scope::NewRootScope();
    using namespace ::tensorflow::ops;  // NOLINT(build/namespaces)

    string output_name = "top_k";
    TopK(root.WithOpName(output_name), outputs[0], how_many_labels);
    // 这个运行我们刚刚建的GraphDef网络模型的定义,返回输出张量的结果
    tensorflow::GraphDef graph;
    TF_RETURN_IF_ERROR(root.ToGraphDef(&graph));

    std::unique_ptr<tensorflow::Session> session(
        tensorflow::NewSession(tensorflow::SessionOptions()));
    TF_RETURN_IF_ERROR(session->Create(graph));
    // TopK节点返回两个输出,分数和原来的索引,因此,我们必须追加:0和:1指定两者。
    std::vector<Tensor> out_tensors;
    TF_RETURN_IF_ERROR(session->Run({}, { output_name + ":0", output_name + ":1" },
    {}, &out_tensors));
    *scores = out_tensors[0];
    *indices = out_tensors[1];
    return Status::OK();
}

// 给定一个模型运行的输出,以及包含该标签的文件的名称打印出得分最高值的前五名。
Status PrintTopLabels(const std::vector<Tensor>& outputs,
    string labels_file_name) {
    std::vector<string> labels;
    size_t label_count;
    Status read_labels_status =
        ReadLabelsFile(labels_file_name, &labels, &label_count);
    if (!read_labels_status.ok()) {
        LOG(ERROR) << read_labels_status;
        return read_labels_status;
    }
    const int how_many_labels = std::min(5, static_cast<int>(label_count));
    Tensor indices;
    Tensor scores;
    TF_RETURN_IF_ERROR(GetTopLabels(outputs, how_many_labels, &indices, &scores));
    tensorflow::TTypes<float>::Flat scores_flat = scores.flat<float>();
    tensorflow::TTypes<int32>::Flat indices_flat = indices.flat<int32>();
    for (int pos = 0; pos < how_many_labels; ++pos) {
        const int label_index = indices_flat(pos);
        const float score = scores_flat(pos);
        LOG(INFO) << labels[label_index] << " (" << label_index << "): " << score;
    }
    return Status::OK();
}

// 这是一个测试函数,返回最顶上的标签索引是否为预期的。
Status CheckTopLabel(const std::vector<Tensor>& outputs, int expected,
    bool* is_expected) {
    *is_expected = false;
    Tensor indices;
    Tensor scores;
    const int how_many_labels = 1;
    TF_RETURN_IF_ERROR(GetTopLabels(outputs, how_many_labels, &indices, &scores));
    tensorflow::TTypes<int32>::Flat indices_flat = indices.flat<int32>();
    if (indices_flat(0) != expected) {
        LOG(ERROR) << "Expected label #" << expected << " but got #"
            << indices_flat(0);
        *is_expected = false;
    }
    else {
        *is_expected = true;
    }
    return Status::OK();
}

int main(int argc, char* argv[]) {
    // 他们定义图形和输入数据的位置,以及什么样的输入模型是期望的。
    //如果你训练自己的模型,或使用GoogLeNet以外的模型,你需要更新这些。

    string image = "../car.jpg";
    string graph = "../car_frozen_model.pb";
    string labels ="../all.pbtxt";
    int32 input_width = 299;
    int32 input_height = 299;
    int32 input_mean = 128;
    int32 input_std = 128;
    string input_layer = "Mul";
    string output_layer = "softmax";
    bool self_test = false;
    string root_dir = "";
    std::vector<Flag> flag_list = {
        Flag("image", &image, "image to be processed"),
        Flag("graph", &graph, "graph to be executed"),
        Flag("labels", &labels, "name of file containing labels"),
        Flag("input_width", &input_width, "resize image to this width in pixels"),
        Flag("input_height", &input_height,"resize image to this height in pixels"),
        Flag("input_mean", &input_mean, "scale pixel values to this mean"),
        Flag("input_std", &input_std, "scale pixel values to this std deviation"),
        Flag("input_layer", &input_layer, "name of input layer"),
        Flag("output_layer", &output_layer, "name of output layer"),
        Flag("self_test", &self_test, "run a self test"),
        Flag("root_dir", &root_dir,
        "interpret image and graph file names relative to this directory"),
    };
    string usage = tensorflow::Flags::Usage(argv[0], flag_list);
    const bool parse_result = tensorflow::Flags::Parse(&argc, argv, flag_list);
    if (!parse_result) {
        LOG(ERROR) << usage;
        return -1;
    }

    // 我们需要调用这个函数建立Tensorflow的通用状态
    tensorflow::port::InitMain(argv[0], &argc, &argv);
    if (argc > 1) {
        LOG(ERROR) << "Unknown argument " << argv[1] << "\n" << usage;
        return -1;
    }

    // 首先加载和初始化模型
    std::unique_ptr<tensorflow::Session> session;
    string graph_path = tensorflow::io::JoinPath(root_dir, graph);
    Status load_graph_status = LoadGraph(graph_path, &session);
    if (!load_graph_status.ok()) {
        LOG(ERROR) << load_graph_status;
        return -1;
    }

    // 用float数组的个数从硬盘获取图像,调整大小和归一化到主图像期望的规格
    std::vector<Tensor> resized_tensors;
    string image_path = tensorflow::io::JoinPath(root_dir, image);
    Status read_tensor_status =
        ReadTensorFromImageFile(image_path, input_height, input_width, input_mean,
            input_std, &resized_tensors);
    if (!read_tensor_status.ok()) {
        LOG(ERROR) << read_tensor_status;
        return -1;
    }
    const Tensor& resized_tensor = resized_tensors[0];

    // 通过model运行图像
    std::vector<Tensor> outputs;
    Status run_status = session->Run({ { input_layer, resized_tensor } },
    { output_layer }, {}, &outputs);
    if (!run_status.ok()) {
        LOG(ERROR) << "Running model failed: " << run_status;
        return -1;
    }

    // 这是用于自动化测试,以确保我们由默认设置得到预期的结果。我们知道标签866(军装)应该是Asmiral Hopper图像的置顶标签。
    if (self_test) {
        bool expected_matches;
        Status check_status = CheckTopLabel(outputs, 866, &expected_matches);
        if (!check_status.ok()) {
            LOG(ERROR) << "Running check failed: " << check_status;
            return -1;
        }
        if (!expected_matches) {
            LOG(ERROR) << "Self-test failed!";
            return -1;
        }
    }

    // 对我们产生的结果做一些有意思的事情
    Status print_status = PrintTopLabels(outputs, labels);
    if (!print_status.ok()) {
        LOG(ERROR) << "Running print failed: " << print_status;
        return -1;
    }

    return 0;
}

出现以下错误:

2019-10-16 10:34:45.393635: E test.cpp:258] Invalid argument: Shape must be rank 4 but is rank 2 for 'ResizeBilinear' (op: 'ResizeBilinear') with input shapes: [1,0], [2].

请按任意键继续. . .

错误!!!!:

2019-10-17 21:39:52.716390: E test.cpp:325] Running model failed: Invalid argument: You must feed a value for placeholder tensor 'Placeholder_1' with dtype float and shape [3]
         [[Node: Placeholder_1 = Placeholder[dtype=DT_FLOAT, shape=[3], _device="/job:localhost/replica:0/task:0/device:CPU:0"]()]]
请按任意键继续. . .

解决:

将Placeholder改为Placeholder_1,结果如下:

2019-10-17 21:44:53.676842: W c:\users\user\source\repos\tensorflow\tensorflow\core\framework\allocator.cc:108] Allocation of 411041792 exceeds 10% of system memory.
2019-10-17 21:44:53.984972: W c:\users\user\source\repos\tensorflow\tensorflow\core\framework\allocator.cc:108] Allocation of 411041792 exceeds 10% of system memory.
2019-10-17 21:44:54.527466: W c:\users\user\source\repos\tensorflow\tensorflow\core\framework\allocator.cc:108] Allocation of 411041792 exceeds 10% of system memory.
2019-10-17 21:44:55.460414: W c:\users\user\source\repos\tensorflow\tensorflow\core\framework\allocator.cc:108] Allocation of 411041792 exceeds 10% of system memory.
2019-10-17 21:45:24.729464: W c:\users\user\source\repos\tensorflow\tensorflow\core\framework\op_kernel.cc:1275] OP_REQUIRES failed at strided_slice_op.cc:105 : Invalid argument: slice index 1 of dimension 0 out of bounds.
2019-10-17 21:45:24.729435: W c:\users\user\source\repos\tensorflow\tensorflow\core\framework\op_kernel.cc:1275] OP_REQUIRES failed at strided_slice_op.cc:105 : Invalid argument: slice index 1 of dimension 0 out of bounds.
2019-10-17 21:45:24.751219: E test.cpp:325] Running model failed: Invalid argument: You must feed a value for placeholder tensor 'Placeholder' with dtype float and shape [1,?,?,3]
         [[Node: Placeholder = Placeholder[dtype=DT_FLOAT, shape=[1,?,?,3], _device="/job:localhost/replica:0/task:0/device:CPU:0"]()]]
请按任意键继续. . .

 

 

理解源代码,看https://blog.csdn.net/yuejisuo1948/article/details/84197534

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值