C 怎么读取Cpp文件_用C++加速julia:BMP图片读取

最近写了一个程序,需要大批量地读取8位的BMP格式的灰度图,程序写完profiler一看,竟然读图和处理的时间不相上下,这是万万不可接受的。经过一番折腾,最后决定用C++来加速8位BMP图片的读取。目的很简单,读进来以后变成Float32类型就好了。

打个广告:julia什么语言都能call,C,C++,Fortran,python,R,AnyCall!

PyCall.jl + OpenCV方案

想想,读图是多么简单的事情啊,OpenCV走一波岂不美滋滋。然而julia没有OpenCV的封装,那就PyCall吧!于是请来Python轻松call了OpenCV。PyCall就是这么轻松愉快,所有python的库都直接call,你的是我的,他的也是我的,全是我的!

using PyCall
cv2=pyimport("cv2")
imreadcv(fname)=Float32.(cv2."imread"(fname,0))

选了张512×512的8位灰度图来测个速呗。快速计算一下大小。512×512=256KB,换成Float32也就1MB。

  memory estimate:  61.25 MiB
  allocs estimate:  2359352
  --------------
  minimum time:     483.473 ms (1.26% GC)
  median time:      495.222 ms (1.87% GC)
  mean time:        503.798 ms (3.19% GC)
  maximum time:     548.973 ms (15.07% GC)
  --------------
  samples:          10
  evals/sample:     1

然而测试结果大跌眼镜,竟然alloc了61.25M?WTF???来看看PyCall怎么说的:

Multidimensional NumPy arrays ( ndarray) are supported and can be converted to the native Julia Array type, which makes a copy of the data.

Copy,从Python到Julia的数组是Copy的,反之则不是。就算是Copy也不至于多吃60倍内存对不?果断放弃PyCall。

ImageMagick.jl方案

那要不用julia自己的库?ImageMagick.jl 给大名鼎鼎的ImageMagick库做了封装,速度应该不错。这里有个小坑,对于bmp格式的图,默认是3通道!bmp格式里明明有一位告诉了通道数,就这样被忽略了。无奈,必须先转灰度图,变成了0-1的8位浮点表示。不得不说这是Images.jl 的优秀设计,然而这个格式并不适合做图像处理啊,轻松溢出。。无奈,乘以255f0转成了Float32。

using Images,FileIO
imread(fname)=channelview(Gray.(load(fname)))*255f0

现在性能应该不错?毕竟ImageMagick,确实不错,内存直接降了17倍,速度也提升了20倍,简直强大。仔细一看,这个内存占用3.52M有点意思,分析一下。load进来是RGB表示需要256K*3,转成灰度需要256K,然后变成浮点需要1M,理论上只需要2M内存。另外1.5M可能是用来和库交换数据吧,copy一次0.75M,两次就1.5M了。嗯,就这么骗自己吧。

  memory estimate:  3.52 MiB
  allocs estimate:  291
  --------------
  minimum time:     21.443 ms (0.00% GC)
  median time:      24.656 ms (0.00% GC)
  mean time:        25.802 ms (1.93% GC)
  maximum time:     37.566 ms (13.16% GC)
  --------------
  samples:          194
  evals/sample:     1

TIF方案

还是不爽怎么办,明明直接就是灰度,非要读成RGB很难受啊。然而ImageMagick怎么读都是RGB,就算重新save一下灰度图读经来依然是RGB。就是bmp这个格式的锅!看看人家tif格式就没问题。1.78M内存,基本就是浮点的1M,灰度的256K,加上猜测的两次copy,正好。平均时间减少了8ms的快乐,嗯对,8ms的快乐。

imreadtif(fname)=channelview(load(fname))*255f0
julia> @benchmark imreadtif("ref.tif")
BenchmarkTools.Trial:
  memory estimate:  1.78 MiB
  allocs estimate:  346
  --------------
  minimum time:     14.877 ms (0.00% GC)
  median time:      17.154 ms (0.00% GC)
  mean time:        17.916 ms (1.49% GC)
  maximum time:     28.903 ms (11.29% GC)
  --------------
  samples:          279
  evals/sample:     1

然而手头全是bmp的图怎么办。。预处理转tif?可以是可以,但是过于暴力,不优雅。

CxxWrap.jl + C++方案

于是,想到了以前写的cpp版本的读取bmp格式的代码。bmp作为最简单的图像格式之一,基本可以无脑读取。其中有几个字段特别重要,列表如下:

offset  type    fieldName   
10      uint32  OffsetBits  数据的偏移
18      uint32  Width       宽
22      uint32  Height      高
28      uint16  BitCount    每像素位宽(8表示灰度,24表示RGB)

bmp里面每个像素的数据是倒着存的,即第一个数据为图像的右下角,从右往左,从下往上。如果是灰度图,则每次读一个char,如果是RGB,则每次读3个char,分别为RGB。还有一个规则,由于内存对齐的原因,bmp格式的每一行都需要4字节对齐,所以需要根据图像的宽度来计算一下对齐,将多余的数据扔掉即可。

那么代码就很好写了,先读几个重要的字段,计算出每行实际数据宽度,然后无脑读数据即可。只要记住julia是按列,cpp是按行就好了。让我们来愉快地使用C++读取bmp吧!

julia本身可以非常轻松的ccall,调用 C 和 Fortran 无压力。但是我就想要写C++。于是使用了CxxWrap.jl,这个库对大部分cpp特性进行了封装,可以非常轻松地和julia交换数据。在这里我直接在堆上new了一块内存,然后把这个指针交给julia。julia的gc就能自动帮我释放了,delete都不用写有没有!!

#include "jlcxx/jlcxx.hpp"
#include <fstream>

auto readBMP(std::string filename) {
    std::ifstream file(filename, std::ifstream::binary);

    file.seekg(10, std::ios::beg);
    uint32_t OffBits;
    file.read((char *) &OffBits, sizeof(uint32_t));

    file.seekg(18, std::ios::beg);
    uint32_t Width;
    file.read((char *) &Width, sizeof(uint32_t));

    file.seekg(22, std::ios::beg);
    uint32_t Height;
    file.read((char *) &Height, sizeof(uint32_t));

    file.seekg(28, std::ios::beg);
    uint16_t BitCount;
    file.read((char *) &BitCount, sizeof(uint16_t));

    uint32_t BitDepth = BitCount >> 3;
    uint32_t RowWidth = (Width * BitDepth - 1 >> 2) + 1 << 2;

    float* data=new float[Height*Width];
    float *dp=data+Height*Width-1;
    if (BitDepth == 1) {
        uint8_t bit;
        for (uint32_t r = 0; r < Height; r++) {
            file.seekg(OffBits + r * RowWidth, std::ios::beg);
            for (uint32_t c = 0; c < Width; c++) {
                file.read((char *) &bit, sizeof(bit));
                data[Height - 1 - r + c * Height] = static_cast<float>(bit);
            }
        }
    } else if (BitDepth == 3) {
        uint8_t RGB[3];
        for (uint32_t r = 0; r < Height; r++) {
            file.seekg(OffBits + r * RowWidth, std::ios::beg);
            for (uint32_t c = 0; c < Width; c++) {
                file.read((char *) &RGB, sizeof(char) * 3);
                data[Height - 1 - r + c * Height] = 0.299f * RGB[0] + 0.587f * RGB[1] + 0.114f * RGB[2];
            }
        }
    }
    file.close();
    return jlcxx::make_julia_array(data,Height,Width);
}

JLCXX_MODULE define_julia_module(jlcxx::Module& mod){
    mod.method("load",&readBMP);
}

逻辑非常的C++,只是最后返回时,把指针给了julia托管。感谢C++14,我能用auto做返回值,毕竟我真的不知道也不care返回了什么对不?最后一行轻松创建一个module,服从FileIO.jl的规则,起名为load,把cpp的函数指针给他就好了,参数为string,返回一个julia的array,轻松愉快。

当然,这个是要编译成动态库的,相应的cmake规则如下。需要link julia和cxxwrap_julia两个库。我这里用的是windows+msys2上的mingw,gcc8.3,就是这么潮。linux上也一样,文件路径换一下就好了。

set(CMAKE_CXX_STANDARD 14)

include_directories(D:/Julia/include/julia)
link_directories(D:/Julia/lib)
include_directories(D:/JuliaPkg/Pkgs/packages/CxxWrap/KcmSi/deps/usr/include)
link_directories(D:/JuliaPkg/Pkgs/packages/CxxWrap/KcmSi/deps/usr/lib)

add_definitions(-DJULIA_ENABLE_THREADING)

add_library(testCxx SHARED library.cpp)
target_link_libraries(testCxx cxxwrap_julia julia)

编译结束后,我们得到了一个libtestCxx.dll。为了在julia端调用它,我们还需要稍微打个包,创建一个module BMP,@wrapmodule 和 @initcxx 两个宏会为我们做好一切,只需要改一下库的名字即可。然后就可以用BMP.load()来进行读取啦,把这个文件存为BMP.jl。

module BMP
    using CxxWrap
    @wrapmodule(joinpath(@__DIR__,"libtestCxx"))
    function __init__()
        @initcxx
    end
end

做完了这一切,终于到了紧张而轻松的时刻,见证C++的时刻到了。

include("BMP.jl")
imreadcpp(file)=BMP.load(file)
julia> @benchmark imreadcpp("ref.bmp")
BenchmarkTools.Trial:
  memory estimate:  1.00 MiB
  allocs estimate:  4
  --------------
  minimum time:     9.562 ms (0.00% GC)
  median time:      10.709 ms (0.00% GC)
  mean time:        11.074 ms (0.00% GC)
  maximum time:     33.798 ms (0.00% GC)
  --------------
  samples:          444
  evals/sample:     1

非常完美,内存就是整整的1M,没有任何多余的开销,包括GC。julia不愧是万能call,而且基本没有call的开销啊啊啊,太香了!平均速度11ms,比tif方案又低了6ms,啊,6ms的快乐。没有GC的C++好香甜~

经过一番折腾,终于舒服了,接口优雅,快乐,香甜,具有极佳的食用体验 。大家快来使用万能call机julia,啥都别说了,快上车,call起来!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值