2010-07-17 11 views
9

Estoy intentando calcular el color promedio de una imagen en Scala, donde "promedio" se define como rojoSum/numpixels, greenSum/numpixels, blueSum/numpixels.Scala: ¿forma idiomática de calcular sumas de matriz intercalada?

Aquí está el código que estoy usando para calcular el color promedio en una región rectangular de una imagen (el 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) 
} 

¿Hay una manera Scala más idiomática de resumir sobre las diferentes matrices intercaladas? ¿Alguna manera de obtener una proyección sobre la matriz que itera sobre cada 4º elemento? Estoy interesado en cualquier experiencia que la comunidad de Stack Overflow pueda brindar.

Respuesta

10

pixels.grouped(3) devolverá un Iterator[Array[Int]] de matrices de 3 elementos. Así

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) 

ACTUALIZACIÓN: Para hacer frente a ambos canales 3 y 4, que escribiría

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

_* aquí significa básicamente "0 o más elementos". Consulte "Coincidencia en secuencias" en http://programming-scala.labs.oreilly.com/ch03.html

+0

respuesta perfecta. Esta respuesta también me ayudó a comprender mejor cómo funciona foldLeft. El uso de la declaración de caso también lo hace muy legible. Bravo – I82Much

+0

¿Cuál es la mejor manera de manejar el hecho de que el valor agrupado no siempre es 3, sino que es igual al número de canales, que podría ser 3 o 4 según el valor alfa? Siempre quiero índices de 0,1,2, pero en ese caso me imagino que no podría hacer un Arreglo de caso (r, g, b) ya que no siempre tengo una matriz de 3 elementos. pixels.grouped (numChannels) .foldLeft ((0,0,0)) {case ((rsum, gsum, bsum), colores: Array [Int]) => (rsum + colors (0), gsum + colors (1), bsum + colors (2))} – I82Much

+0

@Sandor: No veo por qué quiere los resultados acumulativos (es decir, sumas sobre solo píxeles al principio). –

6

Esto es una exageración insana para este problema, pero hago una gran cantidad de reducciones particionadas sobre los conjuntos de datos, y he creado algunas funciones de utilidad para él. El más general de ellos es reductionBy, que toma una colección (en realidad, un Traversable), una función de partición, una función de mapeo y una función de reducción, y produce un mapa de particiones a valores reducidos/mapeados.

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))) 
    } 

Dado que la maquinaria pesada, el problema se convierte en

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) 

Soporte muda ante el impresionante poder de la programación de orden superior.

2

Esta es una gran pregunta, ya que creo que la solución que ha proporcionado es la solución idiomática. El modelo imperativo realmente se ajusta a este problema. Traté de encontrar una solución funcional simple que se lea bien, pero no pude hacerlo.

Creo que el que tiene pixels.grouped (3) es bastante bueno, pero no estoy seguro de que sea mejor que el que tiene.

Mi propia solución "no imperativa" implica la definición de una clase de caso con el operador/+ método:

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) 
} 
Cuestiones relacionadas