Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala - idiomatic way to calculate sums of interleaved array?

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.

like image 245
I82Much Avatar asked Jul 17 '10 17:07

I82Much


3 Answers

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

like image 156
Alexey Romanov Avatar answered Nov 12 '22 04:11

Alexey Romanov


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.

like image 29
Dave Griffith Avatar answered Nov 12 '22 02:11

Dave Griffith


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/method:

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)
}
like image 2
olle kullberg Avatar answered Nov 12 '22 03:11

olle kullberg