Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix usage of IntCachedColorMap in Indexed PNG encoding #2075

Merged
merged 4 commits into from Mar 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -113,6 +113,44 @@ class ColorMapSpec extends FunSpec with Matchers
}
}

it("should correctly map values to colors using IntCacheColorMap") {
val limits = Array(25,50,80,100)
val colors = Array(100,110,120,130)

val colorRamp: ColorRamp = colors
val breaksToColors: Map[Int, Int] = (limits zip colorRamp.stops(limits.size).colors).toMap
val colorMap1 = new IntColorMap(breaksToColors, ColorMap.Options(noDataColor = 0, classBoundaryType = LessThanOrEqualTo))
val arr = (0 until 90 by 5).toArray
val r = createTile(arr)
val h = r.histogram
val colorMap = colorMap1.cache(h)

val color: IndexedPngEncoding =
PngColorEncoding(colorMap.colors, colorMap.options.noDataColor, colorMap.options.fallbackColor) match {
case i @ IndexedPngEncoding(_, _) => i
case _ =>
withClue(s"Color should be Indexed") { sys.error("") }
}

// check that PNG will correctly convert raster to their color index values
val pngMap = color.convertColorMap(colorMap)
h.foreachValue { z =>
val color = colorMap.map(z)
val colorIndex = colorMap.colors.indexOf(color)
pngMap.map(z) should be(colorIndex)
}

// check that we can convert all raster values we have seen in our histogram
withClue(s"Cached colors: ${colorMap.colors.toList}") {
for (x <- arr) {
if (x <= 25) colorMap.map(x) should be(100)
else if (x <= 50) colorMap.map(x) should be(110)
// tile does not have any values past 75, since we didn't see them, we don't expect to color them
else if (x > 75) colorMap.options.noDataColor
}
}
}

it("should correctly map redundant values to colors") {
val limits = Array(25,42,60)
val colors = Array(10,20,30)
Expand Down
26 changes: 22 additions & 4 deletions raster/src/main/scala/geotrellis/raster/render/ColorMap.scala
Expand Up @@ -19,7 +19,7 @@ package geotrellis.raster.render
import geotrellis.raster._
import geotrellis.raster.histogram.Histogram
import geotrellis.util._

import spire.syntax.cfor._
import spire.std.any._

import scala.util.Try
Expand Down Expand Up @@ -179,6 +179,8 @@ trait ColorMap extends Serializable {
def mapDouble(v: Double): Int

def mapColors(f: Int => Int): ColorMap
/** Maps each color value to its index in the [[colors]] sequence.
* This is useful for table lookup encodings (ex: [[geotrellis.raster.render.png.IndexedPngEncoding]]) */
def mapColorsToIndex(): ColorMap

def withNoDataColor(color: Int): ColorMap
Expand Down Expand Up @@ -228,8 +230,15 @@ class IntColorMap(breaksToColors: Map[Int, Int], val options: Options = Options.

def cache(h: Histogram[Int]): ColorMap = {
val ch = h.mutable
h.foreachValue(z => ch.setItem(z, map(z)))
new IntCachedColorMap(colors, ch, options)
val cachedColors = h.values()
cfor(0)( _ < cachedColors.length, _ + 1) { i =>
Copy link
Member

@pomadchin pomadchin Mar 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is h.totalCount would be values count?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h.totalCount would give me the total number of times any value was seen bytes histogram across all buckets. Not sure if thats useful here. What are you thinking?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, nothing smart, that's why i approved it. I tried to avoid somehow calculating length for the second time (as it's already known in fact) and trying to avoid distinct function usage. But no; it's fine as it is.

val z = cachedColors(i)
val itemColor = map(z)
cachedColors(i) = itemColor
ch.setItem(z, itemColor)
}
// multiple values could have mapped to same color, use only distinct color values
new IntCachedColorMap(cachedColors.distinct.toVector, ch, options)
}

def breaksString: String = {
Expand All @@ -243,6 +252,14 @@ class IntColorMap(breaksToColors: Map[Int, Int], val options: Options = Options.
/** Caches a color ramp based on a histogram of values. This is an optimization, since
* often times we create a histogram while classifying, and can reuse that computed
* information in the color mapping.
*
* In order for this class to work correctly the histogram acts as a map from values to their colors
* and must contain a value for each pixel value that we expect to encounter.
*
* The performance benefit is `eC` lookup cost instead of `Log` provided by [[IntColorMap]]
*
* @param colors All color values that can be encountered
* @param h Histogram where counts of values have been replaced cached RGBA color
*/
class IntCachedColorMap(val colors: Vector[Int], h: Histogram[Int], val options: Options)
extends ColorMap {
Expand All @@ -259,8 +276,9 @@ class IntCachedColorMap(val colors: Vector[Int], h: Histogram[Int], val options:
}

def mapColorsToIndex(): ColorMap = {
val colorIndexMap = colors.zipWithIndex.map { case (c, i) => (i, c) }.toMap
val colorIndexMap = colors.zipWithIndex.toMap
val ch = h.mutable

h.foreachValue(z => ch.setItem(z, colorIndexMap(h.itemCount(z).toInt)))
new IntCachedColorMap((0 to colors.length).toVector, ch, options)
}
Expand Down
Expand Up @@ -18,6 +18,7 @@ package geotrellis.raster.render.png

import geotrellis.raster.render._

/** Captures the conversion strategies from RGBA color space to PNG pixels. */
sealed abstract class PngColorEncoding(val n: Byte, val depth: Int) {
def convertColorMap(colorMap: ColorMap): ColorMap
}
Expand Down