oxedyne/fe2o3/fe2o3_infer/README.md
5.1 KiB, 1 run
created by r1870400018:19767, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | # oxedyne_fe2o3_infer |
| 2 | |
| 3 | Convolutional inference on the CPU, in safe Rust, with no dependency beyond |
| 4 | `oxedyne_fe2o3_core`. |
| 5 | |
| 6 | The crate carries the numerics for two small networks -- a face detector and a |
| 7 | face embedder -- and the pieces around them: an ONNX subset loader, the |
| 8 | anchor-free decode and suppression a detector needs, the similarity transform |
| 9 | and warp an embedder's preprocessing needs, and a hundred and twenty-eight |
| 10 | dimensional unit vector at the end that two faces can be compared with. |
| 11 | |
| 12 | It owns nothing. Weights arrive as `&[u8]`, images arrive as pixels, and where |
| 13 | they came from and how the work is spread across cores are the caller's |
| 14 | business. |
| 15 | |
| 16 | ## Using it |
| 17 | |
| 18 | ```rust |
| 19 | use oxedyne_fe2o3_infer::prelude::*; |
| 20 | |
| 21 | let cpu = Cpu::detect(); |
| 22 | let det = res!(Detector::load(&detector_onnx)); |
| 23 | let emb = res!(Embedder::load(&embedder_onnx)); |
| 24 | |
| 25 | // Fit the photograph into the detector's canvas. |
| 26 | let img = res!(Image::new(&rgb, width, height, 3)); |
| 27 | let (canvas, lb) = res!(letterbox(&img, 640, 640)); |
| 28 | let view = res!(Image::new(&canvas, 640, 640, 3)); |
| 29 | |
| 30 | // Detect, then embed each face out of the original photograph. |
| 31 | for d in res!(det.detect(cpu, &view, &DetectorOptions::default())) { |
| 32 | let d = d.unletterbox(&lb); |
| 33 | let e = res!(emb.embed(cpu, &img, &d.landmarks)); |
| 34 | // `cosine(&e, &other)` compares two faces. |
| 35 | } |
| 36 | ``` |
| 37 | |
| 38 | ## The two networks |
| 39 | |
| 40 | Neither is in this repository. Weights are not source, and the pair comes to |
| 41 | thirty-nine megabytes. |
| 42 | |
| 43 | | | Detector | Embedder | |
| 44 | |---|---|---| |
| 45 | | File | `face_detection_yunet_2023mar.onnx` | `face_recognition_sface_2021dec.onnx` | |
| 46 | | Bytes | 232,589 | 38,696,353 | |
| 47 | | SHA-256 | `8f2383e4dd3cfbb4553ea8718107fc0423210dc964f9f4280604804ed2552fa4` | `0ba9fbfa01b5270c96627c4ef784da859931e02f04419c829e83484087c34e79` | |
| 48 | | Licence | MIT | Apache-2.0 | |
| 49 | | Parameters | 53,121 | 9,671,000 | |
| 50 | | Output | boxes, scores, five landmarks | 128 dimensions | |
| 51 | |
| 52 | Both come from the OpenCV model zoo, over git-lfs media URLs: |
| 53 | |
| 54 | ``` |
| 55 | https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models/face_detection_yunet/face_detection_yunet_2023mar.onnx |
| 56 | https://media.githubusercontent.com/media/opencv/opencv_zoo/main/models/face_recognition_sface/face_recognition_sface_2021dec.onnx |
| 57 | ``` |
| 58 | |
| 59 | The plain `raw.githubusercontent.com` URL answers a git-lfs pointer, not the |
| 60 | model. Each hash above is also the `oid` in that pointer, so fetching both and |
| 61 | comparing is a provenance check as well as an integrity one. |
| 62 | |
| 63 | Redistributing either is permitted, with the licence text and notices carried |
| 64 | along -- Apache-2.0 §4 for the embedder, the copyright notice for the detector. |
| 65 | Note separately that both were trained on corpora whose own terms are |
| 66 | research-only; the distributor's grant is what a licence question turns on, but |
| 67 | the provenance is worth knowing. |
| 68 | |
| 69 | **Channel order differs between them and neither says so.** The detector was |
| 70 | exported against blue-green-red input and the embedder against red-green-blue. |
| 71 | Both entry points here take ordinary red-green-blue pixels and reorder for the |
| 72 | network, so a caller never has to know, but anyone feeding the graph directly |
| 73 | does. |
| 74 | |
| 75 | ## Testing |
| 76 | |
| 77 | ```bash |
| 78 | CARGO_TARGET_DIR=~/.cache/cargo-targets/fe2o3_infer_target \ |
| 79 | FE2O3_INFER_MODELS=/path/to/the/onnx/files \ |
| 80 | cargo test --release -p oxedyne_fe2o3_infer |
| 81 | ``` |
| 82 | |
| 83 | Without `FE2O3_INFER_MODELS` the tests that want a model report that they were |
| 84 | skipped and pass. `tests/models.rs` carries a hundred and twenty-eight floats |
| 85 | and seventy-two summary values recorded from an independent implementation |
| 86 | (tract 0.23.4) on a fixed input, so the check is against something other than |
| 87 | this crate's own answer. |
| 88 | |
| 89 | `tests/guard.rs` is a performance guard rather than a correctness one, and it |
| 90 | is not optional. The register tile in the matrix kernel sits on a cliff: |
| 91 | whether the accumulator lives in vector registers or spills to the stack is an |
| 92 | all-or-nothing decision the code generator makes, adjacent tile heights differ |
| 93 | by a factor of thirty-two, and a compiler upgrade can move the boundary without |
| 94 | changing a line. The second guard catches `mul_add` reaching the path that has |
| 95 | no fused multiply-add, where it becomes a library call and costs the same |
| 96 | thirty times. |
| 97 | |
| 98 | ## Measured |
| 99 | |
| 100 | AMD Ryzen 7 6800H, one core, stock `x86-64` target with no `-C target-cpu`. |
| 101 | |
| 102 | | | this crate | tract 0.23.4 | |
| 103 | |---|---|---| |
| 104 | | Detector, 640×640 | 37.4 ms | 99.4 ms | |
| 105 | | Embedder, 112×112 | 16.8 ms | 46.7 ms | |
| 106 | | Matrix kernel, weighted over the embedder's layers | 39.4 GMAC/s | 12.3 GMAC/s | |
| 107 | | The same on the baseline path | 11.0 GMAC/s | -- | |
| 108 | |
| 109 | Agreement with tract over seventy-eight photographs: largest absolute |
| 110 | difference in any detector head, 1.9 × 10⁻⁵; every decoded box identical to |
| 111 | five decimal places of intersection over union. Over ninety-three faces: cosine |
| 112 | similarity between the two embeddings 1.000000000, largest difference in any |
| 113 | component of the unit vector 2.1 × 10⁻⁶. |
| 114 | |
| 115 | ## Safety |
| 116 | |
| 117 | The crate is `#![deny(unsafe_code)]` with exactly one documented exception, in |
| 118 | `kern::run`, where calling a `#[target_feature]` function from an unfeatured |
| 119 | context requires the token even though the body is entirely safe. There are no |
| 120 | raw pointers, no intrinsics and no hand-written assembly. There is no |
| 121 | `unwrap()`, no `expect()`, and no `?`. |