k47.cz
mastodon twitter RSS
bandcamp explorer

Content-aware image cropping with Scala

— k47 (CC by)

Port skriptu Content-aware image cropping with ChunkyPNG z Ruby do Scaly. Zajímavé je, že struktura kódu je skoro identická.

Více info.


import javax.imageio.ImageIO
import java.io.File
import java.awt.image.BufferedImage
import java.awt.{ RenderingHints, AlphaComposite }

// Content-aware image cropping with ChunkyPNG
// https://gist.github.com/a54cd41137b678935c91

final class Cropper(file: String) {
  val image = ImageIO.read(new File(file))

  def cropAndScale(newWidth: Int = 100, newHeight: Int = 100) = {
    val size = math.min(image.getHeight, image.getWidth)
    resize(crop(size, size), newWidth, newHeight)
  }

  def crop(cropWidth: Int = 100, cropHeight: Int = 100) = {
    var (x, y, width, height) = (0, 0, image.getWidth, image.getHeight)
    val sliceLength = 16

    while ((width - x) > cropWidth) {
      val sliceWidth = math.min(width - x - cropWidth, sliceLength)

      val left  = image.getSubimage(x, 0, sliceWidth, image.getHeight)
      val right = image.getSubimage(width - sliceWidth, 0, sliceWidth, image.getHeight)

      if (entropy(left) < entropy(right))
        x += sliceWidth
      else
        width -= sliceWidth
    }

    while ((height - y) > cropHeight) {
      val sliceHeight = math.min(height - y - cropHeight, sliceLength)

      val top    = image.getSubimage(0, y, image.getWidth, sliceHeight)
      val bottom = image.getSubimage(0, height - sliceHeight, image.getWidth, sliceHeight)

      if (entropy(top) < entropy(bottom))
        y += sliceHeight
      else
        height -= sliceHeight
    }

    image.getSubimage(x, y, cropWidth, cropHeight)
  }

  private def histogram(image: BufferedImage) = {
    val hist = new Array[Int](256)
    for (d <- grayscaleData(image)) hist(d) = hist(d) + 1
    hist
  }

  /** http://www.mathworks.com/help/toolbox/images/ref/entropy.html */
  private def entropy(image: BufferedImage) = {
    val hist = histogram(grayscale(image))
    val area = (image.getWidth * image.getHeight).toDouble

    -hist.view.filter(_ > 0).foldLeft(0.0) { (e, freq) =>
      val p = freq / area
      e + p * math.log(p) // log2
    }
  }

  private def resize(img: BufferedImage, w: Int, h: Int): BufferedImage = {
    val resized = new BufferedImage(w, h, img.getType)
    val g = resized.createGraphics()

    //g.setComposite(AlphaComposite.Src)
    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)

    g.drawImage(img, 0, 0, w, h, null)
    g.dispose()

    resized
  }

  private def grayscale(img: BufferedImage) = {
    val gray = new BufferedImage(img.getWidth, img.getHeight, BufferedImage.TYPE_BYTE_GRAY)
    val g = gray.getGraphics()
    g.drawImage(img, 0, 0, null)
    g.dispose()
    gray
  }

  private def grayscaleData(img: BufferedImage) =
    img.getData.getSamples(0, 0, img.getWidth, img.getHeight, 0, new Array[Int](img.getWidth * img.getHeight))
}

val r = new Cropper("dog.png").cropAndScale(100, 100)
ImageIO.write(r, "jpg", new File("duck-cropped.jpg"))
píše k47, ascii@k47.cz