Julia: Calling C Module
23 Dec 2019In one specific task, I need to extract the DCT coefficients from a JPEG image and export them into Julia. Since there are a number of existing C libraries that are capable of doing this, it is more convenient to call them directly with Julia.
The library
In order to extract DCT coefficients from JPEG images, we need to
call libjpeg APIs. Fortunately, there has
been existing wrappers on GitHub that encapsulates the complicated libjpeg
function
calls. To download the package, we can clone the repository to the Julia source code’s
directory.
git clone https://github.com/klauscc/jpeg_toolbox_python.git
Since we are not building the Python bindings, we can comment out the last line in
CMakeLists.txt
:
#add_subdirectory(python)
And then we build the dynamic library inside the build
folder.
$ mkdir build $ cd build $ cmake .. $ make
If everything is successful, we should be able to see libextract_dct.so
in build
directory.
$ ls CMakeCache.txt cmake_install.cmake Makefile CMakeFiles libextract_dct.so test_jpeg_read
Exposing C structures and functions to Julia
There are two functions provided by the C package, namely read_jpeg
and freeJpegObj
.
We need to export these two functions to Julia so that we can use its results. Firstly,
we define a Julia structure that is identical to jpegobj
in C.
struct CJpegObj
image_width::Int32
image_height::Int32
image_components::Int32
image_color_space::Int32
quant_nums::Int32
coef_array_shape::NTuple{4, NTuple{2, Int32}}
quant_tables::Ptr{Float64}
coef_arrays::Ptr{Ptr{Float64}}
end
Then, we load the functions from the dynamic library into Julia.
using Libdl
jpegTool = Libdl.dlopen("jpeg_toolbox_python/build/libextract_dct.so")
read_jpeg = Libdl.dlsym(jpegTool, :read_jpeg)
free_jpeg_obj = Libdl.dlsym(jpegTool, :freeJpegObj)
We can create a Julia type to store the data from C library and use a function to copy data from C to Julia in order to properly manage resource.
mutable struct JpegObj
image_width::Int32
image_height::Int32
image_components::Int32
image_color_space::Int32
quant_nums::Int32
coef_array_shape::Array{Int32, 2}
quant_tables::Array{Array{Float64, 2}, 1}
coef_arrays::Array{Array{Float64, 2}, 1}
end
using Base.Filesystem
# this function can create potential memory leak when there is an exception!
function read_dct_coef(imgname::String)
@assert isfile(imgname)
# get object
cJpegObj = ccall(read_jpeg, CJpegObj, (Cstring,), imgname)
# copy the data from C to julia
jpegObj = JpegObj(
cJpegObj.image_width,
cJpegObj.image_height,
cJpegObj.image_components,
cJpegObj.image_color_space,
cJpegObj.quant_nums,
Array{Int32, 2}(undef, 3, 2),
Array{Array{Float64, 2}, 1}(undef, 0),
Array{Array{Float64, 2}, 1}(undef, 3)
)
println(cJpegObj)
# copy dct array shapes
for i in 1:3, j in 1:2
jpegObj.coef_array_shape[i, j] = cJpegObj.coef_array_shape[i][j]
end
# copy quantization tables
# always assume dct size is 8
@assert jpegObj.quant_nums >= 1
jpegObj.quant_tables = fill(Array{Float64, 2}(undef, 8, 8), jpegObj.quant_nums)
for i in 1:jpegObj.quant_nums, x in 1:8, y in 1:8
offset = (i - 1) * 64 + (x - 1) * 8 + y
jpegObj.quant_tables[i][x, y] = unsafe_load(cJpegObj.quant_tables, offset)
end
# copy dct arrays
for i in 1:3
rows = jpegObj.coef_array_shape[i, 1]
cols = jpegObj.coef_array_shape[i, 2]
jpegObj.coef_arrays[i] = Array{Float64, 2}(undef, rows, cols)
dataPtr = unsafe_load(cJpegObj.coef_arrays, i)
for x in 1:rows, y in 1:cols
offset = (x-1) * cols + y
jpegObj.coef_arrays[i][x, y] = unsafe_load(dataPtr, offset)
end
end
# free object
ccall(free_jpeg_obj,Ptr{Cvoid} ,(CJpegObj,), cJpegObj)
jpegObj
end