-
Notifications
You must be signed in to change notification settings - Fork 49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds optimised algorithm for Median Filter #34
base: master
Are you sure you want to change the base?
Conversation
Cool! I'd be excited to have a more efficient algorithm for large kernels. Before I do a detailed review, maybe a couple of high-level comments. First, it looks like you've changed the generic implementation to use a histogram- and mode-based approach. This is clearly useful for median-filtering, but I doubt this will be helpful for, e.g, uses_histogram(::typeof(median!)) = true
uses_histogram(::Any) = false and then branch to a different implementation whenever Second, it sure would be nice to have this work in more than 2d, but I think this implementation is quite specific to 2d, right? If so we should also limit this to just that case (or generalize it to higher dimensions). Third, perhaps @tejus-gupta might chime in? See JuliaImages/Images.jl#639 (comment) |
Having a trait function would be definitely better. I'll make those changes and update the pull request. Yes, for now it is specific to 2D. I'll see if I can generalise it to nD. |
If you haven't read this blog post, it might help. Seems that if you just use an |
I have updated the commit with support for N-Dimensional inputs also added the uses_histogram function to maintain the generic implementation for mapwindow.
|
@hari-sikchi You should also read this paper. I think it would be best to write separate functions for cases where the color values are discrete and when they are continuous. For discrete color levels, the paper you cited works in O(n) per pixel while this paper works in O(1) per pixel. A 2D implementation is simple but there are some trade-offs involved with n-D extensions. With constant time median filtering algorithm, the memory requirement grows exponentially with number of dimensions. For both the algorithms, we can't use histograms with continuous values images and need to store the values in a BST. This will increase the time complexity to O(nlogn) and O(logn). As far as I understand, you are primarily facing difficulty in iterating in zig-zag manner in higher dimensions. You may want to benchmark if there is any benefit of doing this rather than simply re-initializing the histogram for the next row. |
I agree, I am converting the code to column major for now. Let's see whether reinitializing the histogram can make it faster. |
@timholy Are there functions to compute union and difference for CartesianRange? I think it would be easier to keep track of pixels to add/subtract if we could simply compute union/difference of CartesianRange as we move to next pixel. Wouldn't it be cleaner(and easier to debug) to write a separate median_filter function? |
@tejus-gupta I have made changes in the code to be column major and obtained significant time reduction. Also, zig-zag traversal increases the time complexity. In the paper, for constant time median filter per pixel we don't need to have BST or union data structures. Only requirement is more memory which increases with dimension largely as:dim[2] x dim[3] x dim[4] x .... x dim[n] . The point of inflection is around 9 x 9 filter size for 512 x 512 input, after that the new implementation runs faster. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking promising! Still work to do, I'm afraid. I've tried to be pretty explicit about some design changes that will hopefully make testing robust.
I agree with @tejus-gupta that we may need separate implementations for continuous values (Float32
and Float64
) vs discrete values. We may want to restrict this signature to Union{N0f8,Gray{N0f8}}
eltypes, for example. (In which case my comments about "what about values outside the 0 to 1 range?" are not relevant anymore.)
src/mapwindow.jl
Outdated
function mapwindow{T,N}(f, | ||
img::AbstractArray{T,N}, | ||
window::Indices{N}, | ||
border::BorderSpecAny; | ||
callmode=:copy!) | ||
_mapwindow(replace_function(f), img, window, border, default_shape(f); callmode=callmode) | ||
if(is_medianfilter(f)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of introducing is_medianfilter
, a more "Julian" approach would be to change the signature of your median_filter
function below to be the following method:
function mapwindow{T,N}(f::typeof(median!),
But I actually have a slightly different suggestion, see below.
src/mapwindow.jl
Outdated
border::BorderSpecAny, | ||
shape=default_shape(f); | ||
callmode=:copy!) | ||
inds = indices(img) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the original algorithm is faster for smaller window sizes, you could insert something like this:
if prod(map(length, window)) < 81
return _mapwindow!(...)
end
and keep the original implementation as well.
However, this makes testing a little harder: you'd like to check, for example, that the two algorithms give the same result as one another, and that means you need to have a way of calling each one separately.
For that reason, I recommend something like the following:
# Let's change the syntax of `replace_function`. First, fallbacks in case anyone has already made a local extension.
replace_function(f, window) = replace_function(f)
replace_function(f) = f
# Now the one you care about
function replace_function(::typeof(median!), window)
if prod(map(length, window)) < 81
return median_direct!
end
median_histogram
end
Then change all callers of replace_function(f)
to replace_function(f, window)
, define
function median_direct!(v::AbstractVector)
inds = indices(v,1)
Base.middle(Base.select!(v, (first(inds)+last(inds))÷2, Base.Order.ForwardOrdering()))
end
and define median_histogram
to be your definition. Then you can call this method
function _mapwindow{T,N}(f::typeof(median_histogram),
Finally, in tests you can do this:
med1 = ImageFiltering._mapwindow(ImageFiltering.median_direct!, args...)
med2 = ImageFiltering._mapwindow(ImageFiltering.median_histogram, args...)
@test med1 == med2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have created new dispatches for different methods and separated implementations for discrete and continuous inputs. Do suggest, if there can be a better dispatch method.
Presently:
- Two new functions are added
a. median_direct!
b. median_histogram! - Only for img::Union{AbstractArray{N0f8,N},AbstractArray{ColorTypes.Gray{N0f8},N}} is a choice made between median_histogram! and median_direct!.
src/mapwindow.jl
Outdated
else | ||
|
||
window_size=size(v,1) | ||
dims = map(x->x.stop-x.start+1,window) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to use the "official interface" of ranges rather than accessing fields directly---use first(x)
and last(x)
rather than x.start
and x.stop
. (If someone renames the fields or redefines the implementation, they can't deprecate your direct access of the fields whereas they can make sure that first
and last
continue to work properly. This actually happened not so long ago with LinSpace
and StepRangeLen
.)
However, in this particular case I think this is equivalent to map(length, window)
, and that's much clearer for readers of your code.
src/mapwindow.jl
Outdated
width=window[1].stop-window[1].start+1 | ||
inds = indices(v,1) | ||
if mode == 0 | ||
# Update the histogram according to new entries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indent comment too
src/mapwindow.jl
Outdated
if mode == 0 | ||
# Update the histogram according to new entries | ||
for i = first(inds):last(inds) | ||
id= trunc(Int64,(v[i]*255))+1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if id
turns out to be < 1 or > 256? (You'll get a boundserror.) See a similar discussion here (click to expand the comments, and check the diff to see how @tejus-gupta solved the issue by calling extrema
).
Would be good to add a test where the image has one value outside the 0-to-1 range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the median_histogram method is restricted to N0f8, that won't be a problem, right?
src/mapwindow.jl
Outdated
# Compute the median | ||
tempsum = 0 | ||
m_index=-1 | ||
for i = 1:256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of this hard-codes the fact that histogram
has length 256, but note that histogram
is not even allocated in this function so people changing code in one place might cause bugs in a different place.
for in in linearindices(histogram)
would be more general.
src/mapwindow.jl
Outdated
for i =1:256 | ||
counter+=m_histogram[i] | ||
end | ||
# Compute the median |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this should be a standalone function, since you do the same thing again below.
src/mapwindow.jl
Outdated
id= trunc(Int64,(v[j]*255))+1 | ||
m_histogram[id]-=1 | ||
if(m_histogram[id]<0) | ||
println("stop") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this just for debugging? How about @assert m_histogram[id] >= 0
? I think julia -O3
disables assertions, so it's possible to be both safe and have it not hurt your runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, pushed old commit by mistake
src/mapwindow.jl
Outdated
|
||
elseif mode == 1 | ||
# Update the histogram according to new entries | ||
for i = width:width:dims[1]*dims[2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still 2d, right? How about prod(dims)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this isn't 2D. The iterator 'i' iterates through all the new values added in all dimensions .
for i = first(inds):width:dims[1]*dims[2] [This iterates over a 2D slice]
for j= i: dims[1]*dims[2]:last(inds) [This iterates over all such 2D slices in nD]
src/mapwindow.jl
Outdated
prev_col=curr_col | ||
|
||
Rwin = CartesianRange(map(+, window, I.I)) | ||
copy!(buf, Rbuf, img, Rwin) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm missing something, but won't this alone keep your method O(n^2)
in 2d? You're copying all the values even if you only "use" some of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that is true. Sadly, when I am trying to break Rwin in :
Rwin_add= CartesianRange(map(+, (window_array_add...), I.I)) [ Side column to add]
Rwin_subtract = CartesianRange(map(+, (window_array_subtract...), I.I)) [Side column to remove]
is leading to more running time. I can't quite figure out why that is happening. I am working on reducing the time complexity more.
I am on a flight, will fix this as soon as I reach back. |
Let me know if you're still working on this or ready for a re-review. |
Sorry, I hadn't noticed your responses to my review comments since they got folded. Since the file structure has been reorganized GitHub doesn't let me add on to the existing threads, so I'll just reply by quoting:
|
I have changed the mapwindow dispatch function to work for types img::Union{AbstractArray{N0f8,N},AbstractArray{ColorTypes.Gray{N0f8},N}} and removed type incompatibilities. The code for complete O(n) complexity[after breaking the copy function] looks somewhat like this : (https://gist.github.com/hari-sikchi/b965eec72d45c43392512b2b9d970b82) |
Fast Median filter based on algorithm as explained in paper: http://ieeexplore.ieee.org/document/1163188/?reload=true
Algorithm in short:
Algorithm : Huang’s O(n) median filtering algorithm.
Input: Image X of size m × n, kernel radius r
Output: Image Y of the same size as X
Initialize kernel histogram H
for i = 1 to m do
for j = 1 to n do
for k = −r to r do
Remove X[i+k,j−r−1] from H
Add X[i+k,j+r] to H
end for
Y[i,j] ← median(H)
end for
end for
Improves the time complexity from O((ImageDims) x (kernelSize x kernelSize x log(kernelSize))) to constant(k) x O((ImageDims) x (kernelSize)
Currently, the filter is running slow for small filters than the original one and I have observed for 512x512 images, the new filter runs faster for kernels greater than 7x7 size. I think the code if written in a more optimized way can help make it run faster also for smaller kernels.
I am fairly new to Julia and would be grateful for help to make it faster.