Ognjen Regoje bio photo

Ognjen Regoje
But you can call me Oggy


I make things that run on the web (mostly).
More ABOUT me and my PROJECTS.

me@ognjen.io LinkedIn

Generating more interesting image previews using imagemagick

#imagemagick #mini_magick #technical

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
Input images Input images Input images

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
Input images Input images

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
Input images Input images Input images

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
Input images Input images

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
Input images Input images

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
Input images Input images

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

Input images

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
Input images Input images

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
Input images Input images Input images

HackerNews feedback

A couple of interesting points from the HackerNews comment thread.

  1. 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
  1. Size comparison would have been useful. (Run time as well probably.)
  2. A/B test different versions for purchases