Generating more interesting image previews using imagemagick
I was recently talking to a client who wanted to sell digital images. In the gallery the free ones would be shown but the ones for sale would be blurred out entirely.
Even though I ended up not doing the project I was curious about how to generate image previews that are better then just blurring the entire image. After all, users would need to get a sense of what it looks like before they’d want to buy it.
The two most common solutions are to show a version with poor resolution or to overlay a watermark on it. Both are easy to implement and work fairly well.
I wanted to see if there was a way to selectively hide parts of a higher resolution image while hiding it more than a watermark.
I wanted the end-result to be a teaser image of sorts. The user should “see” what it is. It should look interesting. But it shouldn’t be usable. The lead image about shows the end result that I’m quite happy with. Below is the process I went through to achieve it.
I mostly used mini_magick
in order to replicate how a web application might execute these transformations. But running the commands directly is entirely doable and in fact I do so in a couple of places. mini_magick
after all is just a wrapper around that helps to build the commands.
Blurring the image as a control
imagemagick has three ways of blurring. The normal blur
, gaussian-blur
and adaptive-blur
.
require 'mini_magick'
INPUT_FILE = "image-previews-input-1.jpg"
convert = MiniMagick::Tool::Convert.new
convert << INPUT_FILE
convert.blur("0x100")
convert << "plain-blur-output.jpg"
convert.call
convert = MiniMagick::Tool::Convert.new
convert << INPUT_FILE
convert.gaussian_blur("0x100")
convert << "gaussian-blur-output.jpg"
convert.call
Original | Blur | Gaussian |
---|---|---|
The regular blur and the gaussian looked nearly the same. The regular blur ran much quicker.
Blurring only the center of the image
The documentation for adaptive blur sounded like it was exactly what I had in mind: it blurs less towards the edges of the image.
require 'mini_magick'
INPUT_FILE = "image-previews-input-1.jpg"
convert = MiniMagick::Tool::Convert.new
convert << INPUT_FILE
convert.adaptive_blur("0x100")
convert << "adaptive-blur-output.jpg"
convert.call
Blur | Adaptive |
---|---|
However, upon closer reading I understood that it blurs the edges in the image less.
Blur images, except close to the edges as defined by an edge detection on the image. Eg make the colors smoother, but don’t destroy any sharp edges the image may have.
From: https://imagemagick.org/script/command-line-options.php#adaptive-blur
Blurring the center more can be accomplished by generating a circular gradient to serve as a map for the blur.
convert -size 500x750 radial-gradient:white-black image-previews-circle-map.png
convert image-previews-input-1.jpg image-previews-circle-map.png \
-compose blur -set option:compose:args 10x20 -composite \
image-previews-circle-map-blur-output.png
Original | Composite blur | Regular Blur |
---|---|---|
That already looks better then just blurring the entire image. It’s however still quite revealing.
Cover the image in black triangles
Next, I wanted to try to cover the image in black triangles.
require 'mini_magick'
INPUT_FILE = "image-previews-input-1.jpg"
image = MiniMagick::Image.open(INPUT_FILE)
size = image.dimensions.map{|x| x}
width = size[0]
height = size[1]
density = 40 # to control the size of the triangles
sh = height / density
sw = width / density
convert = MiniMagick::Tool::Convert.new
convert << INPUT_FILE
density.times do |w|
density.times do |h|
convert.draw("polygon #{sw * w},#{sh * h} #{sw * (w+1)},#{sh * h} #{sw * (w+1)},#{sh * (h + 1)}")
end
end
convert << "output-with-triangles.png"
convert.call
Original | With Triangles |
---|---|
This looks quite good. The image isn’t really usable for anything but at the same time users can quite clearly “see” what the image is about.
The script above doesn’t take into account the spacing left over on the right and bottom due to integer division, but that’d be trivial to fix.
Increase probability of triangles towards the center
Next, I wanted to see if the results would be better if there were fewer triangles generated near the edges of the image.
require 'mini_magick'
INPUT_FILE = "image-previews-input-1.jpg"
image = MiniMagick::Image.open(INPUT_FILE)
size = image.dimensions.map{|x| x}
width = size[0]
height = size[1]
density = 40
sh = height / density
sw = width / density
hh = height / 2
hw = width / 2
convert = MiniMagick::Tool::Convert.new
convert << INPUT_FILE
density.times do |w|
density.times do |h|
c = rand
prob = 1 - ((((hw.to_f - (w*sw).to_f).abs / hw.to_f) + (hh.to_f - (h*sh).to_f).abs / hh.to_f) / 2) + 0.1
if c < prob
convert.draw("polygon #{sw * w},#{sh * h} #{sw * (w+1)},#{sh * h} #{sw * (w+1)},#{sh * (h + 1)}")
end
end
end
convert << "image-previews-output-with-probability-triangles.png"
convert.call
Original | With probabilistic triangles |
---|---|
Slightly more interesting. Not a big improvement on just covering the entire image however.
Note that the idea here is that this would be run when the image is uploaded and the preview saved. Firstly, this takes quite a while to run so generating it on the fly is not feasible. Secondly, since it uses rand
it theoretically means that a user could keep refreshing the page till they got an unblurred version of each polygon.
Make the triangles transparent
Then I tried blurring the triangles instead of just filling them in.
For this example I had to use a larger input image. With a smaller image the transparency wouldn’t be applied as well and there would be tearing when they triangles are composed back.
I also reduced the density and the blur since it was taking a long time to run.
Since the density was reduced I changed the triangles to polygons in order to blur a larger part of the image.
require 'mini_magick'
INPUT_FILE = "input-1-lg.jpg"
image = MiniMagick::Image.open(INPUT_FILE)
size = image.dimensions.map{|x| x}
height = size[0]
width = size[1]
density = 10
sh = height / density
sw = width / density
fragments = []
hh = height / 2
hw = width / 2
density.times do |w|
density.times do |h|
c = rand
prob = 1 - ((((hw.to_f - (w*sw).to_f).abs / hw.to_f) + (hh.to_f - (h*sh).to_f).abs / hh.to_f) / 2)
if c < prob
fragments.push({w: w*sw, h: h*sh})
convert = MiniMagick::Tool::Convert.new
convert << INPUT_FILE
convert.crop("#{sh}x#{sw}+#{h*sh}+#{w*sw}")
convert.blur("0x200")
convert.fill("black")
convert.draw("polygon #{sh/2},#{sw} #{sh},#{sw} #{sh},0") # Use sh/2 instead of just sh so that the "triangle" starts in the middle rather then in the corner to cover more of the image
convert.fuzz("15%")
convert.define('png:color-type=6')
convert.transparent("black")
convert << "output-crop-with-triangle-#{w*sw}-#{h*sh}.png"
convert.call
end
end
end
image = MiniMagick::Image.open(INPUT_FILE)
fragments.each do |x|
cropped = MiniMagick::Image.new("output-crop-with-triangle-#{x[:w]}-#{x[:h]}.png")
result = image.composite(cropped) do |c|
c.compose "Over"
c.geometry "+#{x[:h]}+#{x[:w]}"
end
result.write "image-previews-output-with-probability-transparent-triangles.jpg"
File.delete("output-crop-with-triangle-#{x[:w]}-#{x[:h]}.png")
if fragments.last != x
image = MiniMagick::Image.open("image-previews-output-with-probability-transparent-triangles.jpg")
end
end
Original | With probabilistic transparent polygons |
---|---|
I think this looks quite good and is what I imagined.
Applying a ripple effect
In the imagemagick
docs I found an example for composing an image with a ripple effect so ended up trying that as well.
First, a ripple mask is created:
convert -size 667x1000 gradient: -evaluate sin 60 image-previews-wave-gradient.jpg
The 667x1000
should correspond to the widthxheight
of the image.
The mask is then used to displace the pixels of the original image.
INPUT_FILE = "image-previews-input-1.jpg"
gradient = MiniMagick::Image.open("image-previews-wave-gradient.jpg")
image = MiniMagick::Image.open(INPUT_FILE)
result = image.composite(gradient) do |c|
c.displace("5x20")
end
result.write "image-previews-rippled-output.jpg"
Original | Rippled |
---|---|
This also looks like an interesting result that might be useful but I think it obfuscates too much of the image.
Conclusion
I think blurring random polygons near the center of the image creates a more interesting image preview then just blurring the entire image, showing a low resolution or applying a watermark.
Original | Blur | With probabilistic transparent polygons |
---|---|---|
HackerNews feedback
A couple of interesting points from the HackerNews comment thread.
- Would not be suitable for purchasing art assets because of the need to clearly see the full image.
- Changing the feel of the image
- Hiding too much of it
- Size comparison would have been useful. (Run time as well probably.)
- A/B test different versions for purchases
Sources
- https://github.com/minimagick/minimagick#composite
- https://stackoverflow.com/questions/8894194/retrieving-the-hex-code-of-the-color-of-a-given-pixel>
- https://www.imagemagick.org/script/command-line-options.php#draw
- https://legacy.imagemagick.org/Usage/crop/
- https://imagemagick.org/script/command-line-options.php#adaptive-blur
- https://legacy.imagemagick.org/Usage/draw/#circles
- https://legacy.imagemagick.org/Usage/crop/
- https://legacy.imagemagick.org/Usage/blur/#blur_channel
- https://im.snibgo.com/selblur.htm
- https://news.ycombinator.com/item?id=27047352