One direction linear blur?

I’m wondering if it was possible to implement one direction linear blur.

#@cli blur_linear : amplitude1[%],_amplitude2[%],_angle,_boundary_conditions={ 0=dirichlet | 1=neumann }
#@cli : Apply linear blur on selected images, with specified angle and amplitudes.
#@cli : Default values: 'amplitude2=0', 'angle=0' and 'boundary_conditions=1'.
#@cli : $ image.jpg blur_linear 10,0,45
#@cli : $$
blur_linear : skip ${2=0},${3=0},${4=1}
e[^-1] "Apply linear blur on image$?, with angle $3 deg. and amplitudes ($1,$2)."
std1={if(${is_percent\ $1},$1*max(w,h),$1)}
std2={if(${is_percent\ $2},$2*max(w,h),$2)}
stdM={round(1.25*max($std1,$std2))}
if $stdM<=0 return fi
repeat $! l[$>]
expand_xy $stdM,{$4!=0}
{2*$stdM},{2*$stdM} gaussian. $1,$2,$3 normalize_sum.
convolve_fft[0] [1] rm. shrink_xy $stdM
endl done

I’m reading this code, and I think it may has to do with convolve_fft that would make it possible or gaussian? I want one direction linear blur because I would like to replicate Splinter Paint.NET plugin, and the evidence I have seen from using the plugin reveals one direction motion blur with radial duplicates.

EDIT:
Hmm, it appears that I can in theory have only one direction motion blur by taking half of the image depending on the angle. Then use convolve_fft. Would this interpretation be correct?

I recall us having this discussion before. Could you simulate what you mean by a one direction blur? Could you show an example image of the splinter effect?

Well, here comes images of what I"m trying to achieve.

Original Image:

image

The goal:

image

Observation from just 1 splinter

image

Standard Motion Blur

image

I can honestly only deduce that splinter use one direction motion blur. I do not know the specifics of convolution other than it goes in one direction only, but knowing the specific isn’t that necessary. As you see in my g’mic thread, that one was done with 2-direction motion blur.

That being said, I don’t like how gaussian command use 11 and 12 as variable. That makes things confusing.

Would using half kernels work? I explored that a year or so ago but don’t remember the result.

Splinter looks more like crystal to me…

What is 11 and 12?

@afre

See this code made by @David_Tschumperle

gaussian : skip ${1=3},${2=$1},${3=0}
e[^-1] "Draw centered gaussian on image$? with standard deviations ($1,$2) and angle $3 deg."
u={cos($3*pi/180)}
v={sin($3*pi/180)}
dmax={max(w,h)}
if isnum($1) l1=$1 else l1={${1}10000*$dmax/100} fi
if isnum($2) l2=$2 else l2={${2}10000*$dmax/100} fi
l1={1/(2*max(1/3,$l1)^2)}
l2={1/(2*max(1/3,$l2)^2)}
A={$l1*$u*$u+$l2*$v*$v}
B={($l1-$l2)*$u*$v}
C={$l1*$v*$v+$l2*$u*$u}
repeat $! l[$>] nm={0,n}
w={w} h={h} ds={d},{s} rm
$w,$h,1,1,'X=x-{($w-1)/2};Y=y-{($h-1)/2};$A*X*X+2*$B*X*Y+$C*Y*Y'
* -1 exp r $w,$h,$ds
nm $nm endl done

I do not know what 11 and 12 means and why is it number.

It is a lowercase L: for length?

Oh, I did not see that. I think I might be able to find a theoretical solution without modifying gaussian now coming to think of it. Probably won’t look good as a gaussian version of it with smooth interpolation between stretched and non-stretched pixel. I can use the distance away from center to determine value between 1 and 0. Then use convolve_fft. At center it is 1, and when the distance threshold is reached, it is 0.

From the image it looks like smears in different directions, seeding from bright loci. The brighter they are the later the set of directional smear is drawn. The interesting part is that in the goal image, the smears aren’t just single lines in every direction. They are parallel sets in 5 directions.

I don’t know if this observation helps in your planning. Hope it does.

Compare with this image

The value are almost right, however the 2 direction motion blur is what makes it look off. Hence why I compared motion blur with splinter with 1 splinter.

I think I just solved it. Using the code as blur_linear code as base. I may be able to do splinter plugin for PDN.

Behold, one direction linear blur.

Good: half kernel does work. Don’t know why I didn’t keep that snippet. Oh yes, that data loss event. :blush: Ahem, anyway, I would like to see how you modified the stdlib code because I didn’t go that route myself. Bet it is much more efficient because it is based on David’s code.

Well, here’s the code

#@cli rep_splinter_blur: _length,_thickness,_duplicates,_angle,_sharpen_multiplier,-1<=_balance<=1,_boundary={ 0=None | 1=Neumann | 2=Periodic | 3=Mirror },_bisided={ 0=one-line | two-line }
rep_splinter_blur:
skip ${1=10%},${2=5%},${3=5},${4=0},${5=0},${6=0},${7=1},${8=0}

start_ang={$4}
angs_per_dups={360/$3}

m "average_output: ti=$! add / $ti"
if $6==-1 m "output_splinter : min"
elif $6<0&&$6>-1 m "output_splinter : +average_output +min[^-1] f. lerp(i#-2,i,abs($6)) k."
elif $6==0 m "output_splinter : average_output"
elif $6>0&&$6<1 m "output_splinter : +average_output +max[^-1] f. lerp(i#-2,i,$6) k."
elif $6==1 m "output_splinter : max"
else error (-1<=\$\6<=1)=F
fi



repeat $! l[$>]
 hypo={(sqrt(w^2+h^2)/2)}
 if ${is_percent\ $1} length={round($1*$hypo)} 
 else length={round($1)} 
 fi
 
 if ${is_percent\ $2} thickness={round($2*$hypo)} 
 else thickness={round($2)} 
 fi
 
 ds={d},{s}
 
 if $7!=2 expand_xy $length,$7 fi
 repeat $3
  ang={$start_ang+$angs_per_dups*$>}
  +rep_splinter_blur_convolve_map. $length,$thickness,$ang,$5,$8
  +convolve_fft[0] [-1]
  rm..
 done
 rm[0]
 output_splinter
 if $7!=2 shrink_xy $length fi
endl done

um average_output,output_splinter,splinter_convolve
#@cli rep_splinter_blur_convolve_map: _length,_thickness,_angle,_sharpen_multiplier,_bisided={ 0=one-line | two-line }
#@cli : Create a convolve map for directional blur. This enables one to create a convolve map for one-direction motion blur.
#@cli : Default values: '_length=10%','_thickness=5%','_angle=0','_bisided=1'
rep_splinter_blur_convolve_map : skip ${1=10%},${2=5%},${3=0},${5=1}
repeat $! l[$>]
 hypo={(sqrt(w^2+h^2)/2)}
 if ${is_percent\ $1} length={round($1*$hypo)} else length={round($1)} fi
 if ${is_percent\ $2} thickness={round($2*$hypo)} else thickness={round($2)} fi
 ds={d},{s} 
 rm
 {$length},{$length},1,1
 f "begin(
  const sides=$5;
  const thickness="$thickness";
  const hw=(w-1)/2;
  const hh=(h-1)/2;
  const ang=($3/180)*pi*-1;
  const cos_ang=cos(ang);
  const sin_ang=sin(ang);
  rot_x(a,b)=a*cos_ang-b*sin_ang;
  rot_y(a,b)=a*sin_ang+b*cos_ang;
  cutval(v)=v<0?0:v;
  maxcutval(v)=v>1?1:v;
 );
 xx=x/w-.5;
 yy=y/h-.5;
 lx=x-hw;
 ly=y-hh;
 radial_grad=1-sqrt(xx^2+yy^2)*2;
 radial_grad=cutval(radial_grad);
 line=1-maxcutval(abs(rot_x(lx,ly))/thickness);
 sides?(line?radial_grad*line):(rot_y(lx,ly)<=0?(line?radial_grad*line));
 "
 / {is}
 if $4
  avgstat={ia}
  +f (i*2-$avgstat)
  f.. lerp(i,i#1,min(1,$4))
  k[0]
 fi
 r 100%,100%,$ds,0,1
endl done

It looks like this.

The second one is more convincing, but targeting the dark regions.

Yep, now all I have to do is to rearrange variables until it is comfortable for cli users. Thickness to 1 looks the best.

The efficiency comes from the fact it uses convolve_fft to convolve the image. It’s always faster to use this when convolution kernels are large (convolution is just a complex multiplication in Fourier space).

When to use convolve_fft vs regular convolve?

I’d say that for masks larger than 9x9 or 11x11, convolve_fft is probably faster.
Anyway, dealing with FFT has the property to consider periodic boundary conditions, which may not be desired. It’s still possible to simulate different boundary conditions by padding the original image, but if the mask is really large, this can be expensive (you have to add borders that are large as the half-size of the convolution kernel).

Is there a way to make the script faster? Duplicates of 50 takes forever for a convolution of 100 px. PDN plugin Splinter Blur version is a lot faster, but that may has to do with applied multi-threading, and I can’t find out about that because it’s not open source.

Since I changed variables, here’s the current code.

Splinter Blur

The part I want to make faster is the part close to ang={} line.

#@cli rep_splinter_blur: _length,_duplicates,_angle,_thickness,_sharpen_multiplier,-1<=_balance<=1,_boundary={ 0=None | 1=Neumann | 2=Periodic | 3=Mirror },_bisided={ 0=one-line | two-line }
#@cli : Apply Splinter Blur to Image. Based off observation from using Splinter Blur plugin within Paint.NET made by Ed Harvey. Note that convolution result is different.
rep_splinter_blur:
skip ${1=10%},${2=5},${3=0},${4=0},${5=0},${6=0},${7=1},${8=0}

start_ang={$3}
angs_per_dups={360/$2}

m "average_output: ti=$! add / $ti"
if $6==-1 m "output_splinter : min"
elif $6<0&&$6>-1 m "output_splinter : +average_output +min[^-1] f. lerp(i#-2,i,abs($6)) k."
elif $6==0 m "output_splinter : average_output"
elif $6>0&&$6<1 m "output_splinter : +average_output +max[^-1] f. lerp(i#-2,i,$6) k."
elif $6==1 m "output_splinter : max"
else error (-1<=\$\6<=1)=F
fi



repeat $! l[$>]
 cutval={im},{iM}
 hypo={(sqrt(w^2+h^2)/2)}
 if ${is_percent\ $1} length={round($1*$hypo)} 
 else length={round($1)} 
 fi
 
 if ${is_percent\ $4} thickness={round($4*$hypo)} 
 else thickness={round($4)} 
 fi
 
 ds={d},{s}
 
 if $7!=2 expand_xy {round($length/2)},$7 fi
 repeat $2
  ang={$start_ang+$angs_per_dups*$>}
  +rep_splinter_blur_convolve_map. $length,$thickness,$ang,$5,$8
  +convolve_fft[0] [-1]
  rm..
 done
 rm[0]
 output_splinter
 cut $cutval
 if $7!=2 shrink_xy {round($length/2)} fi
endl done

um average_output,output_splinter,splinter_convolve
#@cli rep_splinter_blur_convolve_map: _length,_thickness,_angle,_sharpen_multiplier,_bisided={ 0=one-line | two-line }
#@cli : Create a convolve map for directional blur. This enables one to create a convolve map for one-direction motion blur.
#@cli : Default values: '_length=10%','_thickness=5%','_angle=0','_bisided=1'
rep_splinter_blur_convolve_map : skip ${1=10%},${2=5%},${3=0},${5=1}
repeat $! l[$>]
 hypo={(sqrt(w^2+h^2)/2)}
 if ${is_percent\ $1} length={round($1*$hypo)} else length={round($1)} fi
 if ${is_percent\ $2} thickness={max(round($2*$hypo),1)} else thickness={max(round($2),1)} fi
 ds={d},{s} 
 rm
 {$length},{$length},1,1
 f "begin(
  const sides=$5;
  const thickness="$thickness";
  const hw=(w-1)/2;
  const hh=(h-1)/2;
  const ang=($3/180)*pi*-1;
  const cos_ang=cos(ang);
  const sin_ang=sin(ang);
  rot_x(a,b)=a*cos_ang-b*sin_ang;
  rot_y(a,b)=a*sin_ang+b*cos_ang;
  cutval(v)=v<0?0:v;
  maxcutval(v)=v>1?1:v;
 );
 xx=x/w-.5;
 yy=y/h-.5;
 lx=x-hw;
 ly=y-hh;
 radial_grad=1-sqrt(xx^2+yy^2)*2;
 radial_grad=cutval(radial_grad);
 line=1-maxcutval(abs(rot_x(lx,ly))/thickness);
 sides?(line?radial_grad*line):(rot_y(lx,ly)<=0?(line?radial_grad*line));
 "
 / {is}
 if $4
  avgstat={ia}
  +f (i*2-$avgstat)
  f.. lerp(i,i#1,min(1,$4))
  k[0]
 fi
 r 100%,100%,$ds,0,1
endl done

Note to self, try to create radial duplicates on the fill block. That might be the solution.

EDIT: Just tested the note to self idea. Nope.

Also if you can factorize your kernel and separate the 2D convolution into a 1D vertical and a 1D horizontal one, it’s better to avoid FFT.

But then, trying to limit the number of operations is not always good, it depends on how the memory is accessed vs. cache misses vs. I/O speed vs. computation speed. I have seen GPU benchmarks where FFT starts being worth it only for 64×64 kernels and up. It’s difficult to predict the performance without benchmarking.

Some Python libs actually run various convolutions for various kernels and images sizes, and cache the runtimes, so the code later switch to the fastest path depending on sizes.

Thanks for your input. I am doing that already.