26

使用Rust和WebAssembly在Node.js中进行人脸检测

 3 years ago
source link: https://segmentfault.com/a/1190000025183783
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文将介绍如何编写基于Node.js的AI即服务应用程序。

当今,用于AI的主流编程语言是Python。但是,用于Web的编程语言是JavaScript。为了将AI功能作为Web服务提供,我们需要将AI算法包装在JavaScript中,尤其是Node.js。

但是,Python和JavaScript本身都不适合计算密集型AI应用程序。它们是具有繁重运行时的高级(即慢速)语言。它们的易用性以降低性能为代价。 Python通过将AI计算包装在本机C / C ++模块中来解决此问题。 Node.js可以做同样的事情,但是我们有一个更好的方法 --WebAssembly。

WebAssembly VM提供与Node.js和其他JavaScript运行时的紧密集成。它们具有高性能,内存安全,默认情况下安全且可跨操作系统移植的特点。但是,我们的方法结合了WebAssembly和本机代码的最佳功能。

工作原理

基于Node.js的AI即服务应用程序由三部分组成。

Node.js应用程序提供Web服务并调用WebAssembly函数以执行计算密集型任务,例如AI推理。

数据准备,处理以及与其他系统的集成是通过WebAssembly 函数完成的。最初,我们支持Rust。应用程序开发人员必须编写此函数。

AI模型的实际执行是通过原生代码完成的,以最大限度地提高性能。该代码的这一部分非常简短,并经过了安全性和安全性审查。应用程序开发人员只需从WebAssembly函数调用此原生程序,就像今天在Python和Node.js中使用原生函数的方式一样。

接下来,我们看下示例程序。

人脸检测示例

人脸检测Web服务 允许用户上传照片,并显示绿色框标记的图像。

用于执行MTCNN人脸检测模型的Rust源代码基于Cetra的教程:使用Tensorflow Rust进行人脸检测。我们进行了更改以使Tensorflow库在WebAssembly中工作。

Node.js应用程序处理文件上传和响应。

app.post('/infer', function (req, res) {

let image_file = req.files.image_file;

var result_filename = uuidv4() + ".png";

// Call the infer() function from WebAssembly (SSVM)

var res = infer(req.body.detection_threshold, image_file.data);

fs.writeFileSync("public/" + result_filename, res);

res.send('![]()');

});

如您所见,JavaScript应用程序仅将图像数据和一个名为detection_threshold的参数传递给infer()函数,该参数指定要检测的最小脸部,然后将返回值保存到服务器上的图像文件中。 infer()函数用Rust编写,并编译成WebAssembly,以便可以从JavaScript调用它。

infer()函数将输入图像数据展平为一个数组。它建立了一个TensorFlow模型,并使用扁平化的图像数据作为模型的输入。 TensorFlow模型执行将返回一组数字,这些数字指示每个面框的四个角的坐标。然后,infer()函数在每个面孔周围绘制一个绿色框,然后将修改后的图像保存到Web服务器上的PNG文件中。

[wasm_bindgen]

pub fn infer(detection_threshold: &str, image_data: &[u8]) -> Vec<u8> {

let mut dt = detection_threshold;
... ...
let mut img = image::load_from_memory(image_data).unwrap();

// Run the tensorflow model using the face_detection_mtcnn native wrapper
let mut cmd = Command::new("face_detection_mtcnn");
// Pass in some arguments
cmd.arg(img.width().to_string())
    .arg(img.height().to_string())
    .arg(dt);
// The image bytes data is passed in via STDIN
for (_x, _y, rgb) in img.pixels() {
    cmd.stdin_u8(rgb[2] as u8)
        .stdin_u8(rgb[1] as u8)
        .stdin_u8(rgb[0] as u8);
}
let out = cmd.output();

// Draw boxes from the result JSON array
let line = Pixel::from_slice(&[0, 255, 0, 0]);
let stdout_json: Value = from_str(str::from_utf8(&out.stdout).expect("[]")).unwrap();
let stdout_vec = stdout_json.as_array().unwrap();
for i in 0..stdout_vec.len() {
    let xy = stdout_vec[i].as_array().unwrap();
    let x1: i32 = xy[0].as_f64().unwrap() as i32;
    let y1: i32 = xy[1].as_f64().unwrap() as i32;
    let x2: i32 = xy[2].as_f64().unwrap() as i32;
    let y2: i32 = xy[3].as_f64().unwrap() as i32;
    let rect = Rect::at(x1, y1).of_size((x2 - x1) as u32, (y2 - y1) as u32);
    draw_hollow_rect_mut(&mut img, rect, *line);
}   
let mut buf = Vec::new();
// Write the result image into STDOUT
img.write_to(&mut buf, image::ImageOutputFormat::Png).expect("Unable to write");
return buf;

}

face_detection_mtcnn命令以原生代码运行MTCNN TensorFlow模型。它包含三个参数:图像宽度,图像高度和检测阈值。从WebAssembly infer() 通过STDIN传入RGB值的实际图像数据。模型的结果以JSON编码,并通过STDOUT返回。

请注意,我们如何传递模型参数detection_threshold名为min_size的模型张量,然后使用input张量传递输入图像数据。box张量用于从模型中检索结果。

fn main() -> Result<(), Box<dyn Error>> {

// Get the arguments passed in from WebAssembly
let args: Vec<String> = env::args().collect();
let img_width: u64 = args[1].parse::<u64>().unwrap();
let img_height: u64 = args[2].parse::<u64>().unwrap();
let detection_threshold: f32 = args[3].parse::<f32>().unwrap();
let mut buffer: Vec<u8> = Vec::new();
let mut flattened: Vec<f32> = Vec::new();

// The image bytes are read from STDIN
io::stdin().read_to_end(&mut buffer)?;
for num in buffer {
    flattened.push(num.into());
}

// Load up the graph as a byte array and create a tensorflow graph.
let model = include_bytes!("mtcnn.pb");
let mut graph = Graph::new();
graph.import_graph_def(&*model, &ImportGraphDefOptions::new())?;

let mut args = SessionRunArgs::new();
// The `input` tensor expects BGR pixel data from the input image
let input = Tensor::new(&[img_height, img_width, 3]).with_values(&flattened)?;
args.add_feed(&graph.operation_by_name_required("input")?, 0, &input);

// The `min_size` tensor takes the detection_threshold argument
let min_size = Tensor::new(&[]).with_values(&[detection_threshold])?;
args.add_feed(&graph.operation_by_name_required("min_size")?, 0, &min_size);

// Default input params for the model
let thresholds = Tensor::new(&[3]).with_values(&[0.6f32, 0.7f32, 0.7f32])?;
args.add_feed(&graph.operation_by_name_required("thresholds")?, 0, &thresholds);
let factor = Tensor::new(&[]).with_values(&[0.709f32])?;
args.add_feed(&graph.operation_by_name_required("factor")?, 0, &factor);

// Request the following outputs after the session runs.
let bbox = args.request_fetch(&graph.operation_by_name_required("box")?, 0);

let session = Session::new(&SessionOptions::new(), &graph)?;
session.run(&mut args)?;

// Get the bounding boxes
let bbox_res: Tensor<f32> = args.fetch(bbox)?;
let mut iter = 0;
let mut json_vec: Vec<[f32; 4]> = Vec::new();
while (iter * 4) < bbox_res.len() {
    json_vec.push([
        bbox_res[4 * iter + 1], // x1
        bbox_res[4 * iter],     // y1
        bbox_res[4 * iter + 3], // x2
        bbox_res[4 * iter + 2], // y2
    ]);
    iter += 1;
}
let json_obj = json!(json_vec);
// Return result JSON in STDOUT
println!("{}", json_obj.to_string()); 
Ok(())

}

我们的目标是为常见的AI模型创建原生执行包装,以便开发人员可以将它们用作库。

部署人脸检测示例

作为前提条件,您将需要安装Rust,Node.js,Second State WebAssembly VM 和 ssvmup 工具。查看有关步骤的说明,或仅使用我们的Docker镜像。您还需要TensorFlow库。

$ wget https://storage.googleapis.co...

$ sudo tar -C /usr/ -xzf libtensorflow-gpu-linux-x86_64-1.15.0.tar.gz

为了部署人脸检测示例,我们从本机TensorFlow模型驱动程序开始。您可以从该项目中的Rust源代码进行编译。

in the native_model_zoo/face_detection_mtcnn directory

$ cargo install --path .

接下来,转到Web应用程序项目。运行ssvmup命令以从Rust构建WebAssembly函数。回想一下,此WebAssembly函数为Web应用程序执行数据准备逻辑。

in the nodejs/face_detection_service directory

$ ssvmup build

借助构建的WebAssembly功能,您现在可以启动Node.js应用程序。

$ npm i express express-fileupload uuid

$ cd node

$ node server.js

现在可以在计算机的端口8080上使用Web服务。尝试使用自己的自拍照或家人和集体照!

TensorFlow 模型动物园

原生Rust 包 face_detection_mtcnn是基于TensorFlow库的简单包装器。它加载经过训练的TensorFlow模型(称为冻结保存的模型),设置模型的输入,执行模型,并从模型中检索输出值。

实际上,我们的包装器仅检索检测到的脸部周围的盒子坐标。该模型实际上为每个检测到的脸部以及每个脸部的眼睛,嘴巴和鼻子的位置提供了置信度。通过更改模型中的检索张量名称,包装器可以获取此信息并返回到WASM函数。

如果您想使用其他模型,则遵循该示例并为您自己的模型创建包装器应该相当容易。您只需要了解张量名称及其数据类型的输入和输出即可。

为了实现此目标,我们创建了一个名为“原生模型动物园”的项目,以为尽可能多的TensorFlow模型开发现成的Rust包装器。

总结

在本文中,我们演示了如何使用Rust和WebAssembly在Node.js中实现真实的AI即服务用例。我们的方法为整个社区提供了一个为“模型动物园”做出贡献的框架,该模型可以用作更多应用程序开发人员的AI库。

PS: 本文属于翻译,原文

7FZR7rY.png!mobile

本文将介绍如何编写基于Node.js的AI即服务应用程序。

当今,用于AI的主流编程语言是Python。但是,用于Web的编程语言是JavaScript。为了将AI功能作为Web服务提供,我们需要将AI算法包装在JavaScript中,尤其是Node.js。

但是,Python和JavaScript本身都不适合计算密集型AI应用程序。它们是具有繁重运行时的高级(即慢速)语言。它们的易用性以降低性能为代价。 Python通过将AI计算包装在本机C / C ++模块中来解决此问题。 Node.js可以做同样的事情,但是我们有一个更好的方法 --WebAssembly。

WebAssembly VM提供与Node.js和其他JavaScript运行时的紧密集成。它们具有高性能,内存安全,默认情况下安全且可跨操作系统移植的特点。但是,我们的方法结合了WebAssembly和本机代码的最佳功能。

工作原理

基于Node.js的AI即服务应用程序由三部分组成。

  • Node.js应用程序提供Web服务并调用WebAssembly函数以执行计算密集型任务,例如AI推理。
  • 数据准备,处理以及与其他系统的集成是通过WebAssembly 函数完成的。最初,我们支持Rust。应用程序开发人员必须编写此函数。
  • AI模型的实际执行是通过原生代码完成的,以最大限度地提高性能。该代码的这一部分非常简短,并经过了安全性和安全性审查。应用程序开发人员只需从WebAssembly函数调用此原生程序,就像今天在Python和Node.js中使用原生函数的方式一样。

ruqYjqa.png!mobile

接下来,我们看下示例程序。

人脸检测示例

人脸检测Web服务 允许用户上传照片,并显示绿色框标记的图像。

用于执行MTCNN人脸检测模型的Rust源代码基于Cetra的教程: 使用Tensorflow Rust进行人脸检测 。我们进行了更改以使Tensorflow库在WebAssembly中工作。

yMrQJ3R.jpg!mobile

Node.js应用程序 处理文件上传和响应。

app.post('/infer', function (req, res) {
  let image_file = req.files.image_file;
  var result_filename = uuidv4() + ".png";

  // Call the infer() function from WebAssembly (SSVM)
  var res = infer(req.body.detection_threshold, image_file.data);

  fs.writeFileSync("public/" + result_filename, res);
  res.send('<img src="' +  result_filename + '"/>');
});

如您所见,JavaScript应用程序仅将图像数据和一个名为 detection_threshold 的参数传递给 infer() 函数,该参数指定要检测的最小脸部,然后将返回值保存到服务器上的图像文件中。 infer() 函数用Rust编写,并编译成WebAssembly,以便可以从JavaScript调用它。

infer() 函数将输入图像数据展平为一个数组。它建立了一个TensorFlow模型,并使用扁平化的图像数据作为模型的输入。 TensorFlow模型执行将返回一组数字,这些数字指示每个面框的四个角的坐标。然后, infer() 函数在每个面孔周围绘制一个绿色框,然后将修改后的图像保存到Web服务器上的PNG文件中。

#[wasm_bindgen]
pub fn infer(detection_threshold: &str, image_data: &[u8]) -> Vec<u8> {
    let mut dt = detection_threshold;
    ... ...
    let mut img = image::load_from_memory(image_data).unwrap();

    // Run the tensorflow model using the face_detection_mtcnn native wrapper
    let mut cmd = Command::new("face_detection_mtcnn");
    // Pass in some arguments
    cmd.arg(img.width().to_string())
        .arg(img.height().to_string())
        .arg(dt);
    // The image bytes data is passed in via STDIN
    for (_x, _y, rgb) in img.pixels() {
        cmd.stdin_u8(rgb[2] as u8)
            .stdin_u8(rgb[1] as u8)
            .stdin_u8(rgb[0] as u8);
    }
    let out = cmd.output();

    // Draw boxes from the result JSON array
    let line = Pixel::from_slice(&[0, 255, 0, 0]);
    let stdout_json: Value = from_str(str::from_utf8(&out.stdout).expect("[]")).unwrap();
    let stdout_vec = stdout_json.as_array().unwrap();
    for i in 0..stdout_vec.len() {
        let xy = stdout_vec[i].as_array().unwrap();
        let x1: i32 = xy[0].as_f64().unwrap() as i32;
        let y1: i32 = xy[1].as_f64().unwrap() as i32;
        let x2: i32 = xy[2].as_f64().unwrap() as i32;
        let y2: i32 = xy[3].as_f64().unwrap() as i32;
        let rect = Rect::at(x1, y1).of_size((x2 - x1) as u32, (y2 - y1) as u32);
        draw_hollow_rect_mut(&mut img, rect, *line);
    }   
    let mut buf = Vec::new();
    // Write the result image into STDOUT
    img.write_to(&mut buf, image::ImageOutputFormat::Png).expect("Unable to write");
    return buf;
}

face_detection_mtcnn 命令以原生代码运行MTCNN TensorFlow模型。它包含三个参数:图像宽度,图像高度和检测阈值。从WebAssembly infer() 通过 STDIN 传入RGB值的实际图像数据。模型的结果以JSON编码,并通过 STDOUT 返回。

请注意,我们如何传递模型参数 detection_threshold 名为 min_size 的模型张量,然后使用 input 张量传递输入图像数据。 box 张量用于从模型中检索结果。

fn main() -> Result<(), Box<dyn Error>> {
    // Get the arguments passed in from WebAssembly
    let args: Vec<String> = env::args().collect();
    let img_width: u64 = args[1].parse::<u64>().unwrap();
    let img_height: u64 = args[2].parse::<u64>().unwrap();
    let detection_threshold: f32 = args[3].parse::<f32>().unwrap();
    let mut buffer: Vec<u8> = Vec::new();
    let mut flattened: Vec<f32> = Vec::new();

    // The image bytes are read from STDIN
    io::stdin().read_to_end(&mut buffer)?;
    for num in buffer {
        flattened.push(num.into());
    }

    // Load up the graph as a byte array and create a tensorflow graph.
    let model = include_bytes!("mtcnn.pb");
    let mut graph = Graph::new();
    graph.import_graph_def(&*model, &ImportGraphDefOptions::new())?;

    let mut args = SessionRunArgs::new();
    // The `input` tensor expects BGR pixel data from the input image
    let input = Tensor::new(&[img_height, img_width, 3]).with_values(&flattened)?;
    args.add_feed(&graph.operation_by_name_required("input")?, 0, &input);

    // The `min_size` tensor takes the detection_threshold argument
    let min_size = Tensor::new(&[]).with_values(&[detection_threshold])?;
    args.add_feed(&graph.operation_by_name_required("min_size")?, 0, &min_size);

    // Default input params for the model
    let thresholds = Tensor::new(&[3]).with_values(&[0.6f32, 0.7f32, 0.7f32])?;
    args.add_feed(&graph.operation_by_name_required("thresholds")?, 0, &thresholds);
    let factor = Tensor::new(&[]).with_values(&[0.709f32])?;
    args.add_feed(&graph.operation_by_name_required("factor")?, 0, &factor);

    // Request the following outputs after the session runs.
    let bbox = args.request_fetch(&graph.operation_by_name_required("box")?, 0);

    let session = Session::new(&SessionOptions::new(), &graph)?;
    session.run(&mut args)?;

    // Get the bounding boxes
    let bbox_res: Tensor<f32> = args.fetch(bbox)?;
    let mut iter = 0;
    let mut json_vec: Vec<[f32; 4]> = Vec::new();
    while (iter * 4) < bbox_res.len() {
        json_vec.push([
            bbox_res[4 * iter + 1], // x1
            bbox_res[4 * iter],     // y1
            bbox_res[4 * iter + 3], // x2
            bbox_res[4 * iter + 2], // y2
        ]);
        iter += 1;
    }
    let json_obj = json!(json_vec);
    // Return result JSON in STDOUT
    println!("{}", json_obj.to_string()); 
    Ok(())
}

我们的目标是为常见的AI模型创建原生执行包装,以便开发人员可以将它们用作库。

部署人脸检测示例

作为前提条件,您将需要安装Rust,Node.js, Second State WebAssembly VMssvmup 工具。查看有关步骤的说明,或仅使用我们的Docker镜像。您还需要TensorFlow库。

$ wget https://storage.googleapis.com/tensorflow/libtensorflow/libtensorflow-gpu-linux-x86_64-1.15.0.tar.gz
$ sudo tar -C /usr/ -xzf libtensorflow-gpu-linux-x86_64-1.15.0.tar.gz

为了部署人脸检测示例,我们从本机TensorFlow模型驱动程序开始。您可以从该项目中的 Rust源代码 进行编译。

# in the native_model_zoo/face_detection_mtcnn directory
$ cargo install --path .

接下来,转到 Web应用程序项目 。运行ssvmup命令以从Rust构建WebAssembly函数。回想一下,此WebAssembly函数为Web应用程序执行数据准备逻辑。

# in the nodejs/face_detection_service directory
$ ssvmup build

借助构建的WebAssembly功能,您现在可以启动Node.js应用程序。

$ npm i express express-fileupload uuid

$ cd node
$ node server.js

现在可以在计算机的端口8080上使用Web服务。尝试使用自己的自拍照或家人和集体照!

TensorFlow 模型动物园

原生Rust 包 face_detection_mtcnn是基于TensorFlow库的简单包装器。它加载经过训练的TensorFlow模型(称为冻结保存的模型),设置模型的输入,执行模型,并从模型中检索输出值。

实际上,我们的包装器仅检索检测到的脸部周围的盒子坐标。该模型实际上为每个检测到的脸部以及每个脸部的眼睛,嘴巴和鼻子的位置提供了置信度。通过更改模型中的检索张量名称,包装器可以获取此信息并返回到WASM函数。

如果您想使用其他模型,则遵循该示例并为您自己的模型创建包装器应该相当容易。您只需要了解张量名称及其数据类型的输入和输出即可。

为了实现此目标,我们创建了一个名为“ 原生模型动物园 ”的项目,以为尽可能多的TensorFlow模型开发现成的Rust包装器。

总结

在本文中,我们演示了如何使用Rust和WebAssembly在Node.js中实现真实的AI即服务用例。我们的方法为整个社区提供了一个为“模型动物园”做出贡献的框架,该模型可以用作更多应用程序开发人员的AI库。

PS: 本文属于翻译, 原文


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK