Hey! I am applying a series of filters/operations from JuliaImages to images that are initially of type Array{UInt8,2}. What would be the most efficient image type for this kind of computation?

Should I always convert back to Array{UInt8,2} after application of a filter? Should I rather find a specific type that could be used for all filters without conversion? To me, the issue here is that type conversion makes the overall computation slow.

Filters I use: some keep the image type unchanged, other do change it. I use for instance ImageMorphology.dilate, Images.fastcorners, ImageFiltering.imfilter, Images.canny, Images.imedge. I also use logical operations between pairs of images, such as:

The operators with their output type are summarized in the table below, assuming that the input (or pair of inputs) is of type Array{UInt8,2}:

Name

Kernel (if needed)

Output type

ImageFiltering.imfilter

Images.Kernel.gaussian

Array{Float64,2}

ImageFiltering.imfilter

Images.Kernel.Laplacian

Array{Int16,2}

ImageFiltering.imfilter

Images.Kernel.sobel

Array{Float64,2}

ImageFiltering.imfilter

[-1 1 -1; 1 0 1; -1 1 -1]

Array{Int64,2}

Images.canny

Array{Bool,2}

Images.imedge

Array{Float64,2}

ImageMorphology.dilate

Array{UInt8,2}

ImageMorphology.erode

Array{UInt8,2}

bitwise_and

Array{UInt8,2}

bitwise_or

Array{UInt8,2}

bitwise_and

Array{UInt8,2}

label_components

Array{Int64,2}

I am also interested in an image type that would be efficient for a subset of the operators above.

Filters ordering: I assume that the operators are applied randomly, there is no specific rule as to which operator should be applied after another one.

In JuliaImages we deliberately use N0f8 from FixedPointNumbers to replace UInt8 because otherwise, it’s often very confusing what is white in image processing (1 or 255?).

Converting Array{UInt8, 2} to Array{N0f8, 2} can be done efficiently via reinterpret(N0f8, img_uint8). And the reverse if rawview(img_n0f8)

Many operations like bitwise_and are actually a pointwise operations, and thus it’s better to write the scalar version, e.g.,:

bitwise_and(x::T, y::T) where T<:Number = 0xff * (x & y)

and use broadcasting

C = bitwise_and.(A, B)
# or in-place version
C .= bitwise_and.(A, B)

Didn’t check if the code runs, but I think you can get the idea here.

Thanks! About sequentially applying random filters, are you suggesting to first convert to N0f8 image and then never convert any output / input of any filter? This would mean that input type of a filter could be many different things. For instance, applying imfilter with gaussian kernel to an N0f8 image produces an Array{Float64,2} image while applying canny produces an Array{Bool,2} image.

I could not see any incompability so far with the filters I suggested but I wonder if this is a good practice and if this is efficient.

In general, allowing the element type to be computed via the algebraic operations makes sense. There’s no penalty for changing output types as long as they are predictable (to Julia) from the input types.

This won’t happen if all filters are applied in an in-place manner:

function rand_fill!(X)
for i in 1:length(X)
X[i] = rand(Bool)
end
return X
end
X = Array{N0f8, 2}(undef, 4, 4)
# X will still of eltype N0f8 because there
# will be an implicit type conversion here
rand_fill!(X)