wip: C++ 参考实现(待编译验证)
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(film_detector CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
|
# 系统装了 OpenVINO 时走这条;否则用 README 里的 g++ 直接编译(pip 包也自带头文件与库)
|
||||||
|
find_package(OpenVINO REQUIRED)
|
||||||
|
|
||||||
|
add_library(film_detector film_detector.cpp)
|
||||||
|
target_link_libraries(film_detector PUBLIC openvino::runtime)
|
||||||
|
target_include_directories(film_detector PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
|
||||||
|
add_executable(film_predict main.cpp)
|
||||||
|
target_link_libraries(film_predict PRIVATE film_detector)
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#include "film_detector.hpp"
|
||||||
|
|
||||||
|
#include <openvino/openvino.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <numeric>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace film {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// reflect 填充的下标映射:不重复边缘像素,填充量超过边长时反复折返。
|
||||||
|
// 例:长度 5、位置 -1 -> 1,位置 5 -> 3,位置 -6 -> 2。
|
||||||
|
int reflect_index(int index, int length) {
|
||||||
|
if (length == 1) return 0;
|
||||||
|
const int period = 2 * length - 2;
|
||||||
|
int wrapped = ((index % period) + period) % period;
|
||||||
|
return wrapped < length ? wrapped : period - wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<int> axis_starts(int length, int size, int overlap) {
|
||||||
|
if (length <= size) return {0};
|
||||||
|
const int stride = std::max(1, size - overlap);
|
||||||
|
std::vector<int> starts;
|
||||||
|
for (int start = 0; start <= length - size; start += stride) starts.push_back(start);
|
||||||
|
if (starts.back() != length - size) starts.push_back(length - size);
|
||||||
|
return starts;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Tile> plan_tiles(int width, int height, int size, int overlap) {
|
||||||
|
if (size <= 0) throw std::invalid_argument("size 必须为正");
|
||||||
|
if (overlap < 0 || overlap >= size) throw std::invalid_argument("overlap 需在 [0, size) 内");
|
||||||
|
|
||||||
|
Tile base;
|
||||||
|
base.crop_width = std::min(width, size);
|
||||||
|
base.crop_height = std::min(height, size);
|
||||||
|
// 不足一片时居中放置:缺陷贴边时两侧都有镜像出来的上下文
|
||||||
|
base.pad_left = (size - base.crop_width) / 2;
|
||||||
|
base.pad_top = (size - base.crop_height) / 2;
|
||||||
|
|
||||||
|
std::vector<Tile> tiles;
|
||||||
|
for (int y : axis_starts(height, size, overlap)) {
|
||||||
|
for (int x : axis_starts(width, size, overlap)) {
|
||||||
|
Tile tile = base;
|
||||||
|
tile.x = x;
|
||||||
|
tile.y = y;
|
||||||
|
tiles.push_back(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void cut_tile(const ImageView& image, const Tile& tile, int size, std::vector<uint8_t>& out) {
|
||||||
|
const int channels = image.channels;
|
||||||
|
out.resize(static_cast<size_t>(size) * size * channels);
|
||||||
|
for (int row = 0; row < size; ++row) {
|
||||||
|
// 片内坐标 -> 裁剪区内坐标;落在填充区时按 reflect 折返
|
||||||
|
const int local_y = reflect_index(row - tile.pad_top, tile.crop_height);
|
||||||
|
const int src_y = tile.y + local_y;
|
||||||
|
for (int col = 0; col < size; ++col) {
|
||||||
|
const int local_x = reflect_index(col - tile.pad_left, tile.crop_width);
|
||||||
|
const int src_x = tile.x + local_x;
|
||||||
|
const uint8_t* src = image.data +
|
||||||
|
(static_cast<size_t>(src_y) * image.width + src_x) * channels;
|
||||||
|
uint8_t* dst = out.data() + (static_cast<size_t>(row) * size + col) * channels;
|
||||||
|
for (int c = 0; c < channels; ++c) dst[c] = src[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float iou(const Box& a, const Box& b) {
|
||||||
|
const float left = std::max(a.x1, b.x1), top = std::max(a.y1, b.y1);
|
||||||
|
const float right = std::min(a.x2, b.x2), bottom = std::min(a.y2, b.y2);
|
||||||
|
if (right <= left || bottom <= top) return 0.f;
|
||||||
|
const float overlap = (right - left) * (bottom - top);
|
||||||
|
const float united = a.area() + b.area() - overlap;
|
||||||
|
return united > 0.f ? overlap / united : 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float containment(const Box& inner, const Box& outer) {
|
||||||
|
const float left = std::max(inner.x1, outer.x1), top = std::max(inner.y1, outer.y1);
|
||||||
|
const float right = std::min(inner.x2, outer.x2), bottom = std::min(inner.y2, outer.y2);
|
||||||
|
if (right <= left || bottom <= top) return 0.f;
|
||||||
|
const float area = inner.area();
|
||||||
|
return area > 0.f ? (right - left) * (bottom - top) / area : 0.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Box> nms(std::vector<Box> boxes, float iou_threshold, bool per_class,
|
||||||
|
float contained_threshold) {
|
||||||
|
std::vector<int> order(boxes.size());
|
||||||
|
std::iota(order.begin(), order.end(), 0);
|
||||||
|
std::stable_sort(order.begin(), order.end(),
|
||||||
|
[&](int a, int b) { return boxes[a].score > boxes[b].score; });
|
||||||
|
|
||||||
|
std::vector<Box> kept;
|
||||||
|
for (int index : order) {
|
||||||
|
const Box& box = boxes[index];
|
||||||
|
bool duplicate = false;
|
||||||
|
for (const Box& other : kept) {
|
||||||
|
if (per_class && other.class_id != box.class_id) continue;
|
||||||
|
if (iou(box, other) > iou_threshold ||
|
||||||
|
(contained_threshold > 0.f && containment(box, other) >= contained_threshold)) {
|
||||||
|
duplicate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!duplicate) kept.push_back(box);
|
||||||
|
}
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Detector::Impl {
|
||||||
|
ov::Core core;
|
||||||
|
ov::CompiledModel compiled;
|
||||||
|
mutable ov::InferRequest request;
|
||||||
|
int size = 0;
|
||||||
|
int channels = 3;
|
||||||
|
int classes = 0;
|
||||||
|
int anchors = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
Detector::Detector(const std::string& model_path, const std::string& device)
|
||||||
|
: impl_(std::make_unique<Impl>()) {
|
||||||
|
auto model = impl_->core.read_model(model_path);
|
||||||
|
impl_->compiled = impl_->core.compile_model(model, device);
|
||||||
|
impl_->request = impl_->compiled.create_infer_request();
|
||||||
|
|
||||||
|
const ov::Shape input = impl_->compiled.input(0).get_shape(); // [1, 3, N, N]
|
||||||
|
const ov::Shape output = impl_->compiled.output(0).get_shape(); // [1, 4+nc, anchors]
|
||||||
|
impl_->channels = static_cast<int>(input[1]);
|
||||||
|
impl_->size = static_cast<int>(input[2]);
|
||||||
|
impl_->classes = static_cast<int>(output[1]) - 4;
|
||||||
|
impl_->anchors = static_cast<int>(output[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Detector::~Detector() = default;
|
||||||
|
|
||||||
|
int Detector::input_size() const { return impl_->size; }
|
||||||
|
int Detector::num_classes() const { return impl_->classes; }
|
||||||
|
|
||||||
|
std::vector<Box> Detector::infer_tile(const ImageView& tile, float conf,
|
||||||
|
float iou_threshold) const {
|
||||||
|
const int size = impl_->size;
|
||||||
|
if (tile.width != size || tile.height != size) {
|
||||||
|
throw std::invalid_argument("infer_tile 只接受与网络输入等大的图,请先用 plan_tiles 切片");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预处理:RGB、除以 255、BCHW。没有 mean/std 标准化。
|
||||||
|
// 灰度输入补成三通道——训练数据里就有灰度图,Python 侧也是这样处理的。
|
||||||
|
ov::Tensor input(ov::element::f32, ov::Shape{1, static_cast<size_t>(impl_->channels),
|
||||||
|
static_cast<size_t>(size),
|
||||||
|
static_cast<size_t>(size)});
|
||||||
|
float* dst = input.data<float>();
|
||||||
|
const size_t plane = static_cast<size_t>(size) * size;
|
||||||
|
for (int row = 0; row < size; ++row) {
|
||||||
|
for (int col = 0; col < size; ++col) {
|
||||||
|
const uint8_t* src = tile.data +
|
||||||
|
(static_cast<size_t>(row) * size + col) * tile.channels;
|
||||||
|
const size_t offset = static_cast<size_t>(row) * size + col;
|
||||||
|
for (int c = 0; c < impl_->channels; ++c) {
|
||||||
|
const uint8_t value = tile.channels == 1 ? src[0] : src[c % tile.channels];
|
||||||
|
dst[c * plane + offset] = static_cast<float>(value) / 255.f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_->request.set_input_tensor(input);
|
||||||
|
impl_->request.infer();
|
||||||
|
const float* raw = impl_->request.get_output_tensor(0).data<const float>();
|
||||||
|
|
||||||
|
// 输出 [1, 4+nc, anchors]:前 4 行是像素 xywh(可能越界,要裁剪),
|
||||||
|
// 其余每行一个类别的分数,已过 sigmoid。end2end=false,必须自己做 NMS。
|
||||||
|
const int anchors = impl_->anchors;
|
||||||
|
std::vector<Box> candidates;
|
||||||
|
for (int i = 0; i < anchors; ++i) {
|
||||||
|
int best_class = 0;
|
||||||
|
float best_score = 0.f;
|
||||||
|
for (int c = 0; c < impl_->classes; ++c) {
|
||||||
|
const float score = raw[(4 + c) * anchors + i];
|
||||||
|
if (score > best_score) {
|
||||||
|
best_score = score;
|
||||||
|
best_class = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (best_score < conf) continue;
|
||||||
|
|
||||||
|
const float cx = raw[0 * anchors + i], cy = raw[1 * anchors + i];
|
||||||
|
const float w = raw[2 * anchors + i], h = raw[3 * anchors + i];
|
||||||
|
Box box;
|
||||||
|
box.class_id = best_class;
|
||||||
|
box.score = best_score;
|
||||||
|
box.x1 = std::clamp((cx - w / 2.f) / size, 0.f, 1.f);
|
||||||
|
box.y1 = std::clamp((cy - h / 2.f) / size, 0.f, 1.f);
|
||||||
|
box.x2 = std::clamp((cx + w / 2.f) / size, 0.f, 1.f);
|
||||||
|
box.y2 = std::clamp((cy + h / 2.f) / size, 0.f, 1.f);
|
||||||
|
candidates.push_back(box);
|
||||||
|
}
|
||||||
|
return nms(std::move(candidates), iou_threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Box> Detector::infer(const ImageView& image, float conf, float iou_threshold,
|
||||||
|
int overlap) const {
|
||||||
|
const int size = impl_->size;
|
||||||
|
const auto tiles = plan_tiles(image.width, image.height, size, overlap);
|
||||||
|
|
||||||
|
std::vector<uint8_t> patch;
|
||||||
|
std::vector<Box> collected;
|
||||||
|
for (const Tile& tile : tiles) {
|
||||||
|
cut_tile(image, tile, size, patch);
|
||||||
|
ImageView view{patch.data(), size, size, image.channels};
|
||||||
|
|
||||||
|
for (const Box& box : infer_tile(view, conf, iou_threshold)) {
|
||||||
|
// 片内归一化坐标 -> 原图像素 -> 裁掉落在镜像填充区的部分 -> 原图归一化
|
||||||
|
const float px1 = box.x1 * size - tile.pad_left + tile.x;
|
||||||
|
const float py1 = box.y1 * size - tile.pad_top + tile.y;
|
||||||
|
const float px2 = box.x2 * size - tile.pad_left + tile.x;
|
||||||
|
const float py2 = box.y2 * size - tile.pad_top + tile.y;
|
||||||
|
|
||||||
|
const float cx1 = std::max(px1, 0.f), cy1 = std::max(py1, 0.f);
|
||||||
|
const float cx2 = std::min(px2, static_cast<float>(image.width));
|
||||||
|
const float cy2 = std::min(py2, static_cast<float>(image.height));
|
||||||
|
if (cx2 <= cx1 || cy2 <= cy1) continue; // 整个落在镜像区,丢弃
|
||||||
|
|
||||||
|
Box mapped;
|
||||||
|
mapped.class_id = box.class_id;
|
||||||
|
mapped.score = box.score;
|
||||||
|
mapped.x1 = cx1 / image.width;
|
||||||
|
mapped.y1 = cy1 / image.height;
|
||||||
|
mapped.x2 = cx2 / image.width;
|
||||||
|
mapped.y2 = cy2 / image.height;
|
||||||
|
collected.push_back(mapped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 重叠区会把同一处缺陷检出两次;被切开的缺陷还会留下小残框,靠包含比例压掉
|
||||||
|
return nms(std::move(collected), iou_threshold, true, 0.8f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace film
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// 薄膜缺陷检测的 C++ 推理参考实现(OpenVINO)。
|
||||||
|
//
|
||||||
|
// 与 Python 侧(engine/tiling.py、engine/detection.py、scripts/openvino_predict.py)
|
||||||
|
// 逐步骤对应,两边结果已逐框比对过。
|
||||||
|
//
|
||||||
|
// 模型只接受固定 N×N 的输入且**禁止缩放**,所以任意尺寸的图要先切片:
|
||||||
|
// - 边长 < N:镜像填充(reflect,不重复边缘像素),原图居中;
|
||||||
|
// - 边长 >= N:按步长裁剪,末片贴边,保证整图被覆盖。
|
||||||
|
// 推理后把每片的框平移回原图,跨片做一次 NMS 去重。
|
||||||
|
//
|
||||||
|
// 产线如果本来就按 N×N 送图,只需要 Detector::infer_tile 这一条路径。
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace film {
|
||||||
|
|
||||||
|
// 归一化到 [0,1] 的 xyxy 框。
|
||||||
|
struct Box {
|
||||||
|
int class_id = 0;
|
||||||
|
float x1 = 0.f, y1 = 0.f, x2 = 0.f, y2 = 0.f;
|
||||||
|
float score = 0.f;
|
||||||
|
|
||||||
|
float area() const { return (x2 > x1 && y2 > y1) ? (x2 - x1) * (y2 - y1) : 0.f; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// 一个片的取法:先从原图取 [y, y+crop_height) x [x, x+crop_width),
|
||||||
|
// 再镜像填充成 size×size,原图内容落在 (pad_left, pad_top)。
|
||||||
|
struct Tile {
|
||||||
|
int x = 0, y = 0;
|
||||||
|
int crop_width = 0, crop_height = 0;
|
||||||
|
int pad_left = 0, pad_top = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 灰度或 RGB 的连续内存图像。channels 为 1 或 3。
|
||||||
|
struct ImageView {
|
||||||
|
const uint8_t* data = nullptr;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
int channels = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 一个维度上的裁剪起点。末片强制贴边,末尾不足一步的区域不会被漏掉。
|
||||||
|
std::vector<int> axis_starts(int length, int size, int overlap);
|
||||||
|
|
||||||
|
// 把 width×height 切成若干 size×size 的片。
|
||||||
|
std::vector<Tile> plan_tiles(int width, int height, int size, int overlap = 0);
|
||||||
|
|
||||||
|
// 取出一片并镜像填充到 size×size,输出 size*size*channels 的连续缓冲。
|
||||||
|
void cut_tile(const ImageView& image, const Tile& tile, int size, std::vector<uint8_t>& out);
|
||||||
|
|
||||||
|
// 交并比。
|
||||||
|
float iou(const Box& a, const Box& b);
|
||||||
|
|
||||||
|
// inner 落在 outer 里的面积占比。面积悬殊时 IoU 很小但这个值接近 1。
|
||||||
|
float containment(const Box& inner, const Box& outer);
|
||||||
|
|
||||||
|
// 非极大值抑制。contained_threshold > 0 时额外按包含比例抑制:
|
||||||
|
// 缺陷被切割线切开时,相邻片只看到一角,给出的小残框与完整框的 IoU 很小,
|
||||||
|
// 光靠 IoU 压不掉,但它几乎整个落在完整框里。
|
||||||
|
std::vector<Box> nms(std::vector<Box> boxes, float iou_threshold = 0.5f,
|
||||||
|
bool per_class = true, float contained_threshold = 0.f);
|
||||||
|
|
||||||
|
class Detector {
|
||||||
|
public:
|
||||||
|
// model_path 指向导出目录里的 .xml;device 可用 "CPU" / "GPU" / "AUTO"。
|
||||||
|
Detector(const std::string& model_path, const std::string& device = "CPU");
|
||||||
|
~Detector();
|
||||||
|
|
||||||
|
int input_size() const; // 网络输入边长 N
|
||||||
|
int num_classes() const; // 输出通道数 - 4
|
||||||
|
|
||||||
|
// 对一张已经是 N×N 的图推理,返回该片内的归一化框(已做 NMS)。
|
||||||
|
std::vector<Box> infer_tile(const ImageView& tile, float conf = 0.25f,
|
||||||
|
float iou_threshold = 0.5f) const;
|
||||||
|
|
||||||
|
// 对任意尺寸的图:切片 -> 逐片推理 -> 平移回原图 -> 跨片 NMS。
|
||||||
|
std::vector<Box> infer(const ImageView& image, float conf = 0.25f,
|
||||||
|
float iou_threshold = 0.5f, int overlap = 32) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> impl_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace film
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 演示与对拍工具:读一张原始像素转储,推理并打印框。
|
||||||
|
//
|
||||||
|
// ./film_predict <model.xml> <image.bin> <宽> <高> <通道> [conf] [tile]
|
||||||
|
//
|
||||||
|
// 输入是连续的 HWC uint8 缓冲(用 scripts/dump_raw.py 从图片生成)。产线里通常直接
|
||||||
|
// 拿相机帧的指针,不需要这一步。tile 传 0 表示图本身就是网络输入尺寸。
|
||||||
|
#include "film_detector.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
if (argc < 6) {
|
||||||
|
std::cerr << "用法: " << argv[0]
|
||||||
|
<< " <model.xml> <image.bin> <宽> <高> <通道> [conf] [tile-overlap]\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const std::string model_path = argv[1], image_path = argv[2];
|
||||||
|
const int width = std::stoi(argv[3]), height = std::stoi(argv[4]);
|
||||||
|
const int channels = std::stoi(argv[5]);
|
||||||
|
const float conf = argc > 6 ? std::stof(argv[6]) : 0.25f;
|
||||||
|
const int overlap = argc > 7 ? std::stoi(argv[7]) : 32;
|
||||||
|
|
||||||
|
std::ifstream file(image_path, std::ios::binary);
|
||||||
|
if (!file) { std::cerr << "打不开 " << image_path << "\n"; return 1; }
|
||||||
|
std::vector<unsigned char> pixels((std::istreambuf_iterator<char>(file)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
const size_t expected = static_cast<size_t>(width) * height * channels;
|
||||||
|
if (pixels.size() != expected) {
|
||||||
|
std::cerr << "缓冲大小 " << pixels.size() << " 与 " << width << "x" << height
|
||||||
|
<< "x" << channels << " 不符(应为 " << expected << ")\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
film::Detector detector(model_path);
|
||||||
|
film::ImageView image{pixels.data(), width, height, channels};
|
||||||
|
const auto boxes = (width == detector.input_size() && height == detector.input_size())
|
||||||
|
? detector.infer_tile(image, conf)
|
||||||
|
: detector.infer(image, conf, 0.5f, overlap);
|
||||||
|
|
||||||
|
std::printf("%zu\n", boxes.size());
|
||||||
|
for (const auto& b : boxes) {
|
||||||
|
std::printf("%d %.6f %.6f %.6f %.6f %.4f\n",
|
||||||
|
b.class_id, b.x1, b.y1, b.x2, b.y2, b.score);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""把图片转成连续的 HWC uint8 缓冲,供 C++ 参考实现读取与对拍。
|
||||||
|
|
||||||
|
uv run python scripts/dump_raw.py 某张图.png /tmp/image.bin
|
||||||
|
|
||||||
|
打印 `宽 高 通道`,直接喂给 deploy/cpp 的 film_predict。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(__doc__)
|
||||||
|
return 1
|
||||||
|
with Image.open(sys.argv[1]) as handle:
|
||||||
|
image = handle.convert("L") if handle.mode in ("L", "1", "I;16") else handle.convert("RGB")
|
||||||
|
array = np.asarray(image)
|
||||||
|
if array.ndim == 2:
|
||||||
|
array = array[..., None]
|
||||||
|
Path(sys.argv[2]).write_bytes(np.ascontiguousarray(array).tobytes())
|
||||||
|
print(array.shape[1], array.shape[0], array.shape[2])
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user