# Sobel edge detector

Your task is to write a program that takes an input image and run it through edge-detection to become an output image.

The edge-detection works as follows (if unclear, see sobel edge detection):

• The value for a pixel is the total brightness of a pixel, so if it is in color, you will need to convert it to grayscale first (to keep things simple and golf-able, you can take the average value for R, G and B).
• The formulae for Gx and Gy for pixel p(i,j) are:
• Gx = -1 * p(i-1, j-1) - 2 * p(i-1, j) - 1 * p(i-1, j+1) + 1 * p(i+1, j-1) + 2 * p(i+1, j) + 1 * p(i+1, j+1)
• Gy = -1 * p(i-1, j-1) - 2 * p(i, j-1) - 1 * p(i+1, j-1) + 1 * p(i-1, j+1) + 2 * p(i, j+1) + 1 * p(i+1, j+1)
• The value for the size of the edge at that pixel is then: √(Gx2 + Gy2)

The output image is for each pixel the size of the edge √(Gx2 + Gy2) as greyscale.

Bonuses:

• Perform a gaussian blur to smooth out the image before edge-detection kicks in, to omit any smaller edges. This gives a bonus of -30% on the end result.
• Take the angle of the edge in account. You give the output pixel some color, by taking the same greyscale value and adding color from a color wheel using the angle obtained from the formula arctan(Gy/Gx). This gives another bonus of -30% on the end result.

Rules:

• You may omit the value for the edgepixels, and set them to black, or you may use 0 for any pixel outside the image.
• Your ouput image must be in an image format that can be opened on most computers.
• Output must be written to disk or be pipeable to a file.
• Input is given as a commandline argument, in the form of a relative path to the image, or piped in from the commandline.
• This is code golf, so shortest code in bytes wins!
• Can you exactly specify the gaussian blur? Is the input grayscale as well, if no, how should we apply this edge detection to coloured images? Is it correct that the output image has the exact same size as the input, but the input is only performed on the inner pixels (not the one we've set to zero)? – flawr Nov 11 '15 at 19:43
• Have you seen the videos about edge detection from Computerphile? I can smell a connection there :) – GiantTree Nov 11 '15 at 20:08
• @flawr I have to test what gaussian blur is good for edge detection, so I don't really know what is a good value. more on Gaussian blur here. The input image is in color, and you'll need to convert it to grayscale first if you want to perform the edge detection. The edge detection is performed either A: on the inner pixels, and you set the outer 1px border of the output image to black, or B: on all pixels, and you take 0 as the value for any pixels outside the image. – vrwim Nov 11 '15 at 20:11
• @GiantTree nooooooo the video is totally not related :) – vrwim Nov 11 '15 at 20:12
• Why has this been down voted? It seems to be a perfectly valid question. – Addison Crump Nov 12 '15 at 11:20

# J, 166 164 161 154 150 144 143 bytes.

Not golfed too much; I mostly collapsed my longer implementation (see below), so there's probably lots of room for improvement. Uses BMP library. Saves result in file o. I handled edgepixels by only using full 3x3 cells, so the final image has width and height smaller by 2 pixels.

load'bmp'
S=:s,.0,.-s=:1 2 1
p=:([:*:[:+/[:,*)"2
exit''


Usage:

echo 'image.bmp' | jconsole golf.ijs


Expanded:

load 'bmp'

sobel1 =: 3 3 \$ 1 0 _1 2 0 _2 1 0 _1
NB. transposed
sobel2 =: |: sobel1
NB. convert default representation to R,G,B arrays
rgbimage =: (3 # 256) #: image
NB. convert to grayscale
greyimage =: 3 %~ (+/"1) rgbimage
NB. 3x3 cells around each pixel
cells =: 3 3 ,.;._3 greyimage
NB. multiply 3x3 cell by 3x3 sobel, then sum all values in it
partial =: 4 : '+/"1 +/"1 x *"2 y'
NB. square partial (vertical and horizontal) results, sum and root
combine =: [: %: *:@[ + *:@]
NB. limit RGB values to 255
limit =: 255 <. ]
newimage =: limit (sobel1&partial combine sobel2&partial) cells
NB. convert back to J-friendly representation
to_save =: 256 #. 3 #"0 <. newimage
to_save writebmp 'out.bmp'
NB. jconsole stays open by default
exit''


Sample input and output:

• This is a nice example of the ;._3 subarray operator. I noticed you defined a verb p with rank 2 to operate on the subarrays after you create them. You could instead operate on each subarray when you cut. My try at implementing it based on your work is 256#.3#"0<.255<.3 3((|:S)&*+&.*:&(+/)&,S&*);._3%&3(3#256)+/@#:. That should take it down to 126 bytes total. – miles Jul 15 '16 at 14:49
• I've got it down to 119 bytes with 'o'writebmp~256#.3#"0<.255<.3 3(*+&.*:&(+/)&,(*|:))&((-,.0,.])1 2 1);._3%&3(3#256)+/@#:readbmp]stdin'' assuming that only the filename is input on stdin. You can perform this using echo -n so that an extra newline is not included in stdin. On my computer, the script exits automatically when using a piped input to a script which means I don't have to include the exit'' and can save an extra 6 bytes, but I'm not sure if this is true for all. – miles Jul 15 '16 at 16:37

# Python, 161*0.7=112.7 bytes

With the Gaussian Blur bonus.

As you did not explicitly forbid built-in methods, here is OpenCV:

from cv2 import*
from numpy import*
x,y=Sobel(g,5,1,0),Sobel(g,5,0,1)
imwrite('s.png',sqrt(x*x+y*y))


### Without bonus, 136 bytes

from cv2 import*
from numpy import*
x,y=Sobel(g,5,1,0),Sobel(g,5,0,1)
imwrite('s.png',sqrt(x*x+y*y))

• Edit1: Replaced the named constans by their values.

• Could you possibly give a sample input and output image? – R. Kap Jul 17 '16 at 1:38
• @R.Kap better late than never. – Karl Napf Oct 15 '16 at 23:34

# MATLAB, 212*0.4=84.8 bytes

Using the filter toolbox and the HSV colorspace

function f(x);f=@(i,x)imfilter(i,x);s=@(x)fspecial(x);S=s('sobel');A=f(double(rgb2gray(imread(x)))/255,s('gaussian'));X=f(A,S);Y=f(A,S');imwrite(hsv2rgb(cat(3,atan2(Y,X)/pi/2+0.5,0*A+1,sqrt(X.^2+Y.^2))),'t.png')


or ungolfed

function f(x)
f=@(i,x)imfilter(i,x);
s=@(x)fspecial(x);
S=s('sobel');
X=f(A,S);
Y=f(A,S');
imwrite(hsv2rgb(cat(3,atan2(Y,X)/pi/2+0.5,0*A+1,sqrt(X.^2+Y.^2))),'t.png')


# Love2D Lua, 466 Bytes

A=arg[2]i=love.image.newImageData q=math t=i(A)g=i(t:getWidth()-2,t:getHeight()-2)m={{-1,-2,-1},{0,0,0},{1,2,1}}M={{-1,0,1},{-2,0,2},{-1,0,1}}t:mapPixel(function(_,_,r,g,b)a=(r+g+b)/3 return a,a,a end)g:mapPixel(function(x,y)v=0 for Y=0,2 do for X=0,2 do v=v+(t:getPixel(x+X,y+Y)*m[Y+1][X+1])end end V=0 for Y=0,2 do for X=0,2 do V=V+(t:getPixel(x+X,y+Y)*M[Y+1][X+1])end end v=q.max(q.min(q.sqrt(V^2+v^2),255),0)return v,v,v end)g:encode('png',"o")love.event.quit()


Takes command line input, outputs to a file called "o" under your Love2D appsdata folder. Love2D Wont let you save files anywhere else.

Just about as golfed as I could get it, probably could be golfed further.

## Explained

-- Assign the Input to A
A=arg[2]

-- Assign some macros to save FUTURE BYTES™
i=love.image.newImageData
q=math

-- t is the original image, g is the new output image. g is two pixels smaller, which is easier and better looking than a border.
t = i(A)
g = i(t:getWidth()-2,t:getHeight()-2)

-- m and M are our two sobel kernals. Fairly self explanitary.
m = {{-1,-2,-1}
,{0,0,0}
,{1,2,1}}

M = {{-1,0,1}
,{-2,0,2}
,{-1,0,1}}

-- Convert t to grayscale, to save doing this math later.
t:mapPixel(function(_,_,r,g,b)a=(r+g+b)/3 return a,a,a end)

-- Execute our kernals
g:mapPixel(function(x,y)
-- v refers to the VERTICAL output of the Kernel m.
v=0
for Y=0,2 do
for X=0,2 do
v=v+(t:getPixel(x+X,y+Y)*m[Y+1][X+1])
end
end

-- V is the HORIZONTAL of M
V=0
for Y=0,2 do
for X=0,2 do
V=V+(t:getPixel(x+X,y+Y)*M[Y+1][X+1])
end
end

-- Clamp the values and sum them.
v = q.max(q.min(q.sqrt(V^2 + v^2),255),0)
-- Return the grayscale.
return v,v,v
end)

-- Save, renaming the file. The golfed version just outputs as 'o'
g:encode('png',"S_".. A:gsub("(.*)%....","%1.png"))

-- Quit. Not needed, but I'm a sucker for self contained LOVE2D
love.event.quit()


## And...

Although it doesn't actually improve my score (Makes it worse infact), here is the version with the colour wheel implemented.

900 - 270 = 630 Bytes

A=arg[2]i=love.image.newImageData q=math t=i(A)g=i(t:getWidth()-2,t:getHeight()-2)m={{-1,-2,-1},{0,0,0},{1,2,1}}M={{-1,0,1},{-2,0,2},{-1,0,1}}function T(h,s,v)if s <=0 then return v,v,v end h,s,v=h*6,s,v/255 local c=v*s local x=(1-q.abs((h%2)-1))*c local m,r,g,b=(v-c),0,0,0 if h < 1 then r,g,b=c,x,0 elseif h < 2 then r,g,b=x,c,0 elseif h < 3 then r,g,b=0,c,x elseif h < 4 then r,g,b=0,x,c elseif h < 5 then r,g,b=x,0,c else r,g,b=c,0,x end return(r+m)*255,(g+m)*255,(b+m)*255 end t:mapPixel(function(_,_,r,g,b)a=(r+g+b)/3 return a,a,a end)g:mapPixel(function(x,y)v=0 for Y=0,2 do for X=0,2 do v=v+(t:getPixel(x+X,y+Y)*m[Y+1][X+1])end end V=0 for Y=0,2 do for X=0,2 do V=V+(t:getPixel(x+X,y+Y)*M[Y+1][X+1])end end h=v H=V v=q.max(q.min(q.sqrt(V^2+v^2),255),0)h=q.atan2(H,h)/q.pi*2 return T(h,1,v,255)end)g:encode('png',"S_".. A:gsub("(.*)%....","%1.png"))G=love.graphics.newImage(g)love.event.quit()