views:

178

answers:

3

Hi all,

I'm attempting to calculate the average color of an image in Scala, where "average" is defined as the redSum/numpixels, greenSum/numpixels, blueSum/numpixels .

Here is the code I am using to calculate the average color in a rectangular region of an image (the Raster).

// A raster is an abstraction of a piece of an image and the underlying
// pixel data.
// For instance, we can get a raster than is of the upper left twenty
// pixel square of an image
def calculateColorFromRaster(raster:Raster): Color = {
  var redSum = 0
  var greenSum = 0
  var blueSum = 0

  val minX = raster.getMinX()
  val minY = raster.getMinY()

  val height = raster.getHeight()
  val width = raster.getWidth()
  val numPixels = height * width

  val numChannels = raster.getNumBands() 

  val pixelBuffer = new Array[Int](width*height*numChannels)
  val pixels = raster.getPixels(minX,minY,width,height,pixelBuffer)

  // pixelBuffer now filled with r1,g1,b1,r2,g2,b2,...
  // If there's an alpha channel, it will be r1,g1,b1,a1,r2,... but we skip the alpha
  for (i <- 0 until numPixels) {
    val redOffset = numChannels * i
    val red = pixels(redOffset)
    val green = pixels(redOffset+1)
    val blue = pixels(redOffset+2)

    redSum+=red
    greenSum+=green
    blueSum+=blue
  }
  new Color(redSum / numPixels, greenSum / numPixels, blueSum / numPixels)
}

Is there a more idiomatic Scala way of summing up over the different interleaved arrays? Some way to get a projection over the array that iterates over every 4th element? I'm interested in any expertise the Stack Overflow community can provide.

+10  A: 

pixels.grouped(3) will return an Iterator[Array[Int]] of 3-element arrays. So

val pixelRGBs = pixels.grouped(3)

val (redSum, greenSum, blueSum) = 
  pixelRGBs.foldLeft((0, 0, 0)) {case ((rSum, gSum, bSum), Array(r, g, b)) => (rSum + r, gSum + g, bSum + b)}

new Color(redSum / numPixels, greenSum / numPixels, blueSum / numPixels)

UPDATE: To deal with both 3 and 4 channels, I would write

pixels.grouped(numChannels).foldLeft((0, 0, 0)) {case ((rSum, gSum, bSum), Array(r, g, b, _*)) => (rSum + r, gSum + g, bSum + b)}

_* here basically means "0 or more elements". See "Matching on Sequences" in http://programming-scala.labs.oreilly.com/ch03.html

Alexey Romanov
Perfect answer. This answer has also helped me better understand how foldLeft works. The use of the case statement also makes it very readable.Bravo
I82Much
What's the best way to handle the fact that the grouped value is not always 3, but is instead equal to the number of channels, which could be 3 or 4 depending on the alpha value? I always want 0,1,2 indices, but in that case I imagine I couldn't do a case Array(r,g,b) anymore since I don't always have a 3 element array.pixels.grouped(numChannels).foldLeft((0,0,0)) {case ((rsum,gsum,bsum), colors:Array[Int]) => (rsum+colors(0), gsum+colors(1), bsum+colors(2)) }
I82Much
@Sandor: I don't see why you want the cumulative results (i.e. sums over only pixels in the beginning).
Alexey Romanov
@Alexey: you are right, I did not read it carefully enough :-(. ScanLeft really does not make sense in this case. My comment was deleted.
Sandor Murakozi
+5  A: 

This is insane overkill for this problem, but I do a lot of partitioned reductions over datasets, and have built some utility functions for it. The most general of them is reduceBy, which takes a collection (actually a Traversable), a partition function, a mapping function, and a reduction function, and produces a map from partitions to reduced/mapped values.

  def reduceBy[A, B, C](t: Traversable[A], f: A => B, g: A => C, reducer: (C, C) => C): Map[B, C] = {
    def reduceInto(map: Map[B, C], key: B, value: C): Map[B, C] =
      if (map.contains(key)) {
        map + (key -> reducer(map(key), value))
      }
      else {
        map + (key -> value)
      }
    t.foldLeft(Map.empty[B, C])((m, x) => reduceInto(m, f(x), g(x)))
  }

Given that heavy machinery, your problem becomes

val sumByColor:Map[Int, Int] = reduceBy(1 until numPixels, (i => i%numChannels), (i=>pixel(i)), (_+_))
return Color(sumByColor(0)/numPixels, sumByColor(1)/numPixels, sumByColor(2)/numPixels)

Stand mute before the awesome power of higher order programming.

Dave Griffith
+2  A: 

This is a great question, since I think the solution you have provided is the idiomatic solution! The imperative model really fits this problem. I tried to find a simple functional solution that reads well, but I could not do it.

I think the one with pixels.grouped(3) is pretty good, but I am not sure it is better than the one you have.

My own "non imperative" solution involves defining a case class with the + operator/mehtod:

import java.awt.image.Raster
import java.awt.Color

def calculateColorFromRaster(raster:Raster): Color = {
  val minX = raster.getMinX()
  val minY = raster.getMinY()

  val height = raster.getHeight()
  val width = raster.getWidth()
  val numPixels = height * width

  val numChannels = raster.getNumBands()

  val pixelBuffer = new Array[Int](width*height*numChannels)
  val pixels = raster.getPixels(minX,minY,width,height,pixelBuffer)

  // pixelBuffer now filled with r1,g1,b1,r2,g2,b2,...
  // If there's an alpha channel, it will be r1,g1,b1,a1,r2,... but we skip the alpha

  // This case class is only used to sum the pixels, a real waste of CPU!
  case class MyPixelSum(r: Int, g: Int, b: Int){
    def +(sum: MyPixelSum) = MyPixelSum(sum.r +r, sum.g + g, sum.b + b)
  }

  val pixSumSeq= 0 until numPixels map((i: Int) => {
    val redOffset = numChannels * i
    MyPixelSum(pixels(redOffset), pixels(redOffset+1),pixels(redOffset+2))
  })
  val s = pixSumSeq.reduceLeft(_ + _)

  new Color(s.r / numPixels, s.g / numPixels, s.b / numPixels)
}
olle kullberg