img = pilimage.open('breugel-the-harvesters.jpg')
img_np=np.array(img)
img_np.shape
(552, 750, 3)
display(img)
This is equivalent to taking a particular weighted avg of the channels from pillow docs:
When translating a color image to greyscale (mode “L”), the library uses the ITU-R 601-2 luma transform:
$L = \frac{299}{1000}R + \frac{587}{1000}G + \frac{114}{1000}B$
kx.q('greyscale:4h$.299 .587 .114 wsum/:/:')
grey_img_np=np.hstack(kx.q('greyscale')(img_np).np()).reshape(img_np.shape[0],img_np.shape[1])
display(pilimage.fromarray(grey_img_np))
We can use convolutions to do this:
we look at nearby pixels and see if things are changing
q)0^1 0 -1 xprev\: 1+til 8
0 1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
2 3 4 5 6 7 8 0
q)show m:2 4#1+til 8
1 2 3 4
5 6 7 8
I am filling nulls with 0, but we can fill (0^) with anything
q)0^1 xprev/: m
0 1 2 3
0 5 6 7
q)m
1 2 3 4
5 6 7 8
q)0^1 xprev/: m
0 1 2 3
0 5 6 7
q)shape 1 0 -1 xprev/:\: m
3 2 4
Which means we have 3 copies of this 2x4 matrix,
We can visualize this as follows, we have 3 tables of 2x4 matrices
---------
|0 1 2 3|
|0 5 6 7|
---------
|1 2 3 4|
|5 6 7 8|
---------
|2 3 4 0|
|6 7 8 0|
---------
Now suppose we shift just the first matrix 1 prev
---------
|0 1 2 3|
|0 5 6 7|
---------
q) 1 xprev first -1 0 1 xprev/:\: m
`long$()
0 1 2 3
We remedy this by taking the right number of elements (4) for each row then 0 fill(^)
---------
|0 1 2 3|
|0 5 6 7|
---------
So that it looks like this:
q)0^4#/:1 xprev first -1 0 1 xprev/:\: m
0 0 0 0
0 1 2 3
---------
|0 0 0 0|
|0 1 2 3|
---------
|0 1 2 3|
|0 5 6 7|
---------
|0 5 6 7|
|0 0 0 0|
---------
-------------------------
|0 0 0 0|0 0 0 0|0 0 0 0|
|0 1 2 3|1 2 3 4|2 3 4 0|
-------------------------
|0 1 2 3|1 2 3 4|2 3 4 0|
|0 5 6 7|5 6 7 8|6 7 8 0|
-------------------------
|0 5 6 7|5 6 7 8|6 7 8 0|
|0 0 0 0|0 0 0 0|0 0 0 0|
-------------------------
q)shape 1 0 -1 xprev/:\: -1 0 1 xprev/:\: 2 4#1+til 8
3 3 2 4
q)shifter:1 0 -1 xprev/:\:
q)shape 2 shifter/ 2 4#1+til 8
3 3 2 4
q)shape 4#/:/:raze 2 shifter/ 2 4#1+til 8
9 2 4
To finish the preparation for the convolution we simply flatten the last two dimensions
q)shape raze each 4#/:/:raze 2(1 0 -1 xprev/:\:)/ 2 4#1+til 8
9 8
Original Matrix
1 2 3 4
5 6 7 8
We can fill this 9x8 matrix with zeros
first column corresponds to the upper left most cell whose neighbors are all missing(0)
except 1(itself), 2(right),5(below),6(right&below)
q)show 0^raze each 4#/:/:raze 2(1 0 -1 xprev/:\:)/ 2 4#1+til 8
0 0 0 0 0 1 2 3
0 0 0 0 1 2 3 4
0 0 0 0 2 3 4 0
0 1 2 3 0 5 6 7
|1| 2 3 4 5 6 7 8
|2| 3 4 0 6 7 8 0
0 5 6 7 0 0 0 0
|5| 6 7 8 0 0 0 0
|6| 7 8 0 0 0 0 0
conv9:{raze each count[first x]#/:/:raze 2(1 0 -1 xprev/:\:)/x}
the kernel looks at the neighboring pixels and sums to 0
if all the pixels are the same the sum will be 0 otherwise there will be some difference
differences toward the center count more
q)show sobel_kern:(1 0 -1;2 0 -2;1 0 -1)
1 0 -1
2 0 -2
1 0 -1
edge_vertical=kx.q('4h$abs raze[sobel_kern] wsum conv9::',grey_img_np).np().reshape(grey_img_np.shape)
display(pilimage.fromarray(edge_vertical))
edge_horizontal=kx.q('4h$abs raze[flip sobel_kern] wsum conv9::',grey_img_np).np().reshape(grey_img_np.shape)
display(pilimage.fromarray(edge_horizontal))
edge_both=np.sqrt(edge_horizontal.astype('float32')**2+edge_vertical.astype('float32')**2).astype('uint8')
display(pilimage.fromarray(edge_both))
edge_both_rgb=kx.q('4h$raze sobel_rgb::',img_np).np().reshape(img_np.shape[0],img_np.shape[1])
display(pilimage.fromarray(edge_both_rgb))
l:(9 9 9 0 9;
9 9 0 9 6;
9 9 9 9 0;
9 1 9 3 9;
9 9 0 9 9)
It's easy enough to see that we take the path down from 0,6,0,3,0 giving 9
A greedy path would take 0,0,9,1,0 which gives 10
q){y+min 0W^1 0 -1 xprev\:x} scan l
9 9 9 0 9
18 18 0 9 6
27 9 9 9 6
18 10 18 9 15
19 19 9 18 18
Usually in a recursive solution we need to add backpointers
so we can retrace our steps so we can implement the best path
But in Q we can simply reverse the matrix and then follow a greedy path
then reverse the path
q)reverse imin[r 0]{x imin y x:x+-1 0 1}\r:reverse {y+min 0W^1 0 -1 xprev\:x} scan l
3 4 4 3 2
imin:{x?min x}
shape:-1 _ count each first\
conv9:{raze each count[first x]#/:/:raze 2(1 0 -1 xprev/:\:)/x}
sobel_kern:(1 0 -1;2 0 -2;1 0 -1)
sobel:{shape[y]#sqrt sum{x*x}raze'[(x;flip x)]wsum\:conv9 y}[sobel_kern]
sobel_rgb:avg sobel peach til[3]{y[;;x]}[;]\:
minseam:{reverse imin[r 0]{x imin y x:x+-1 0 1}\r:reverse{y+min 0W^1 0 -1 xprev\:x}\[x]}
removeseam:{x@'til[count first x]except/:minseam sobel_rgb x}
### Let's remove a third of our image
display(pilimage.fromarray(img_np))
twothirds_img_q=kx.q('{raze over removeseam/[x;y]}',250,img_np)
twothirds_img=twothirds_img_q.np().reshape(img_np.shape[0],-1,3)
display(pilimage.fromarray(twothirds_img))
class ImageWithResize(object):
def __init__(self,img):
self.img=img
self.m=np.array(self.img)
self.indexes=m_new=kx.q('raze getallseams ::',self.m)\
.np().reshape(self.m.shape[0],self.m.shape[1])
self.mask=np.ones_like(self.m,bool)
def remove_n_seams(self,n):
self.mask[:,:]=True
self.mask[np.arange(self.indexes.shape[0])[:, None],
self.indexes[:,:n]]=False
return pilimage.fromarray(self.m[self.mask]\
.reshape(self.m.shape[0],-1,3))
img_w_resize=ImageWithResize(img)
resize=lambda this,n_pixels: ImageWithResize.remove_n_seams(this, n_pixels)
pn.interact(resize,this=img_w_resize,n_pixels=(0,img_w_resize.m.shape[1],10))
display(canvases[5])
display(canvases[4])
display(canvases[1])