Skip to content

Add Support for the GridCoverage2D Type - #1444

Closed
jamesmcclain wants to merge 17 commits into
locationtech:masterfrom
jamesmcclain:feature/jwm/geotools
Closed

jamesmcclain wants to merge 17 commits into
locationtech:masterfrom
jamesmcclain:feature/jwm/geotools

Conversation

@jamesmcclain

@jamesmcclain jamesmcclain commented Apr 12, 2016 •

Copy link
Copy Markdown
Member

In this pull request, support is added for converting the GeoTools GridCoverage2D to a Geotrellis Raster and vice-versa.

  • GridCoverage2D to Raster
  • Add proper support for NODATA
  • Add proper support for CRSs
  • Confirm Kryo serializability of GridCoverage2DMultibandTile and GridCoverage2DTile GridCoverage2D is not Kryo Serializable (See experimental diff below)
  • Raster to GridCoverage2D
  • Add Tests for GridCoverage2D to Raster
  • Add Tests for Raster to GridCoverage2D
  • Add ScalaDocs

Points of Interest

  • When converting form signed byte Geotrellis Rasters to GeoTools GridCoverage2Ds, the NODATA value must be non-negative. This is as a result of what seems to be a bug in Java's SamleDimension implementation.
  • Due to limitations on banked rasters and interleaved rasters, conversion form a float- or double-valued Geotrellis MultibandTile is not supported. I suggest converting the individual bands to separate GridCoverage2D objects.

To Do (2)

  • Take a pass over the ScalaDocs to make sure that linkages are legal

To Do (1)

  • NODATA Concerns
  • Thread Safety
  • ColorModel, SampleDimension correctness

More

The following diff can be applied to allow experimentation with GridCoverage2D vis-a-vis Spark

diff --git a/build.sbt b/build.sbt
index 05cfc79..dce90e1 100644
--- a/build.sbt
+++ b/build.sbt
@@ -131,7 +131,7 @@ lazy val slick = Project("slick", file("slick")).
   settings(commonSettings: _*)

 lazy val spark = Project("spark", file("spark")).
-  dependsOn(util, raster).
+  dependsOn(util, raster, geotools).
   settings(commonSettings: _*)

 lazy val sparkTestkit: Project = Project("spark-testkit", file("spark-testkit")).
diff --git a/spark/src/test/scala/geotrellis/spark/buffer/BufferTilesSpec.scala b/spark/src/test/scala/geotrellis/spark/buffer/BufferTilesSpec.scala
index 426dc80..d72b4bc 100644
--- a/spark/src/test/scala/geotrellis/spark/buffer/BufferTilesSpec.scala
+++ b/spark/src/test/scala/geotrellis/spark/buffer/BufferTilesSpec.scala
@@ -19,10 +19,51 @@ package geotrellis.spark.buffer
 import geotrellis.spark._
 import geotrellis.spark.io._
 import geotrellis.raster.io.geotiff.SinglebandGeoTiff
+import geotrellis.geotools._

+import org.geotools.coverage.grid._
+import org.geotools.coverage.grid.io._
+import org.geotools.gce.geotiff._
 import org.scalatest.FunSpec


+class ExperimentSpec extends FunSpec with TestEnvironment {
+  def getImage(path: String): GridCoverage2D = {
+    val policy = AbstractGridFormat.OVERVIEW_POLICY.createValue
+    val gridSize = AbstractGridFormat.SUGGESTED_TILE_SIZE.createValue
+    val useJaiRead = AbstractGridFormat.USE_JAI_IMAGEREAD.createValue
+    val file = new java.io.File(path)
+    val reader = new GeoTiffReader(file)
+
+    policy.setValue(OverviewPolicy.IGNORE)
+    gridSize.setValue("1024,1024")
+    useJaiRead.setValue(true)
+
+    val image = reader.read(List(policy, gridSize, useJaiRead).toArray)
+    val renderedImage = image.getRenderedImage
+
+    require(renderedImage.getHeight <= 1024)
+    require(renderedImage.getWidth <= 1024)
+
+    image
+  }
+
+  describe("experiment") {
+    it("should") {
+      val list = List(
+        "./raster-test/data/geotiff-test-files/nex-pr-tile.tif",
+        "./raster-test/data/geotiff-test-files/3bands/uint32/3bands-tiled-band.tif")
+        .map({ path => GridCoverage2DToRaster(getImage(path)) })
+        .map({ raster => raster.tile.map({ (_, z) => z }) })
+        // .map({ path => getImage(path) })
+      val rdd = sc.parallelize(list)
+      val bands = rdd.map(_.bandCount).collect()
+      // val bands = rdd.map(_.getRenderedImage.getSampleModel.getNumBands).collect
+      bands should be (List(1,3))
+    }
+  }
+}
+
 class BufferTilesSpec extends FunSpec with TestEnvironment {

   describe("The BufferTiles functionality") {

@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch 4 times, most recently from 91827cf to f6be292 Compare April 16, 2016 02:00
@echeipesh

Copy link
Copy Markdown
Contributor

I would like to talk through the NoData handling. The way this reads is that each band in GridCoverage2D may have its own NoData value and in fact its own cell type. Is this common from what you've seen so far?

So then we have a problem if we try to map such a tile to a raster, which NoData value to pick? The way MultibandTile is specified our tiles have a single cell type and single nodata value.

We've had no end of trouble with incorrect interpretation of NoData values, so I don't think we should fudge it and pick the first one as GridCoverage2DToRaster.noData seems to do.
If more correct return type for GridCoverage2DToRaster.apply(gridCoverage: GridCoverage2D) would be is Array[Raster[Tile]] I think that's fine. Converting GridCoverage2D -> Raster[MultibandTile] with mismatched cellTypes and NoData fields should be an error.

What are your thoughts on this?

}
else {
val noData = noDataValue.get
typeEnum match {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What if the noDataValue matches our convention? Can we ever recover tile with DoubleConstantNoDataCellType for instance?

@jamesmcclain

Copy link
Copy Markdown
Member Author

What are your thoughts on this?

Sorry for not responding earlier. I am now working on a task that will need this code, so I will have a chance to do some more work with this.

@jamesmcclain

jamesmcclain commented May 11, 2016 •

Copy link
Copy Markdown
Member Author

I would like to talk through the NoData handling. The way this reads is that each band in GridCoverage2D may have its own NoData value and in fact its own cell type. Is this common from what you've seen so far?

I don't know how common different permutations are, but I agree that the closest Geotrellis analog is an array of tiles.

@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch from f6be292 to c56ac33 Compare May 12, 2016 02:06
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 12, 2016
It is no longer attempted to create a single MultibandTile to represent
all layers of a GridCoverage2D, now a sequence of Tiles is created.

Also, the converter now emits *ConstantNoDataCellType when it can.
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 12, 2016
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 12, 2016
It is no longer attempted to create a single MultibandTile to represent
all layers of a GridCoverage2D, now a sequence of Tiles is created.

Also, the converter now emits *ConstantNoDataCellType when it can.
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 12, 2016
@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch from c56ac33 to 79fddf2 Compare May 12, 2016 19:34
jamesmcclain added a commit to jamesmcclain/geotrellis that referenced this pull request May 13, 2016
Since all bands of Geotrellis MultibandTiles must all have the same
cellType and NODATA value, that particular type is not a good candidate
for representing the data in a GeoTools GridCoverage2D.  The best analog
of a GeoTools GridCoverage2D is an Array of Geotrellis Rasters.

Also, the converter now emits *ConstantNoDataCellType when it can.
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 13, 2016
@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch from 79fddf2 to 37c7806 Compare May 15, 2016 23:46
jamesmcclain added a commit to jamesmcclain/geotrellis that referenced this pull request May 15, 2016
Since all bands of Geotrellis MultibandTiles must all have the same
cellType and NODATA value, that particular type is not a good candidate
for representing the data in a GeoTools GridCoverage2D.  The best analog
of a GeoTools GridCoverage2D is an Array of Geotrellis Rasters.

Also, the converter now emits *ConstantNoDataCellType when it can.
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 15, 2016
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 15, 2016
The Raster[Tile] to GridCoverage2D transformation provides best-guess
ColorModels to the created GridCoverage2D objects.  The
Raster[MultibandTile] to GridCoverage2D transformation does not attempt
to provide ColorModel information.  NODATA metadata is now correct in
both the Tile case and MultibandTile case.
@jamesmcclain jamesmcclain changed the title Add Support for the GridCoverage2D Type [WIP] Add Support for the GridCoverage2D Type May 16, 2016
jamesmcclain added a commit to jamesmcclain/geotrellis that referenced this pull request May 16, 2016
Since all bands of Geotrellis MultibandTiles must all have the same
cellType and NODATA value, that particular type is not a good candidate
for representing the data in a GeoTools GridCoverage2D.  The best analog
of a GeoTools GridCoverage2D is an Array of Geotrellis Rasters.

Also, the converter now emits *ConstantNoDataCellType when it can.
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 16, 2016
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 16, 2016
The Raster[Tile] to GridCoverage2D transformation provides best-guess
ColorModels to the created GridCoverage2D objects.  The
Raster[MultibandTile] to GridCoverage2D transformation does not attempt
to provide ColorModel information.  NODATA metadata is now correct in
both the Tile case and MultibandTile case.
@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch from 37c7806 to a05483e Compare May 17, 2016 02:06
jamesmcclain added a commit to jamesmcclain/geotrellis that referenced this pull request May 17, 2016
Since all bands of Geotrellis MultibandTiles must all have the same
cellType and NODATA value, that particular type is not a good candidate
for representing the data in a GeoTools GridCoverage2D.  The best analog
of a GeoTools GridCoverage2D is an Array of Geotrellis Rasters.

Also, the converter now emits *ConstantNoDataCellType when it can.
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 17, 2016
jamesmcclain pushed a commit to jamesmcclain/geotrellis that referenced this pull request May 17, 2016
The Raster[Tile] to GridCoverage2D transformation provides best-guess
ColorModels to the created GridCoverage2D objects.  The
Raster[MultibandTile] to GridCoverage2D transformation does not attempt
to provide ColorModel information.  NODATA metadata is now correct in
both the Tile case and MultibandTile case.
@jamesmcclain jamesmcclain changed the title [WIP] Add Support for the GridCoverage2D Type Add Support for the GridCoverage2D Type May 17, 2016
@lossyrob

Copy link
Copy Markdown
Member

runs tests

[info] Total number of tests run: 274

* @param gridCoverage The GeoTools GridCoverage2D object
* @return A sequence of rasters of singleband Geotrellis [[Tile]]s
*/
def apply(gridCoverage: GridCoverage2D): Seq[Raster[Tile]] = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason why this should be Seq[Raster[Tile]] and not a Raster[MultibandTile]? Is it because the cell types can be different?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, that is the reason.

@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch from a05483e to 3df1f0e Compare May 20, 2016 18:46
@jamesmcclain

Copy link
Copy Markdown
Member Author

[info] Total number of tests run: 274

Is 274 a special number?

James McClain and others added 17 commits May 21, 2016 09:57
According to the documentation[1], DataBuffer.TYPE_BYTE corresponds to
"unsigned byte" not "byte".

1. https://docs.oracle.com/javase/7/docs/api/java/awt/image/DataBuffer.html
Since all bands of Geotrellis MultibandTiles must all have the same
cellType and NODATA value, that particular type is not a good candidate
for representing the data in a GeoTools GridCoverage2D.  The best analog
of a GeoTools GridCoverage2D is an Array of Geotrellis Rasters.

Also, the converter now emits *ConstantNoDataCellType when it can.
The Raster[Tile] to GridCoverage2D transformation provides best-guess
ColorModels to the created GridCoverage2D objects.  The
Raster[MultibandTile] to GridCoverage2D transformation does not attempt
to provide ColorModel information.  NODATA metadata is now correct in
both the Tile case and MultibandTile case.
@jamesmcclain
jamesmcclain force-pushed the feature/jwm/geotools branch from 3df1f0e to dfa9d59 Compare May 21, 2016 14:23
@lossyrob

Copy link
Copy Markdown
Member

Is 274 a special number?

It's just a good (large) number

@lossyrob

Copy link
Copy Markdown
Member

The GridCoverage2DTile does not handle NoData correctly. For instance, getDouble does not check to see if the pixel value corresponds to the "NoData" value saved off with the GridCoverage2D, and translate that to NODATA as the rest of the tiles do. Also, there was a technique I figured would be more performant, which also solves the NoData issue, by using the arrays of DataBuffers directly; spiking it shows it to be significantly faster:

class GridCoverage2DConvertersBenchmark extends Benchmarks with ConsoleReport {
  import GridCoverage2DConvertersBenchmark._
  val paths =
    List(
      BenchmarkUtil.dataPath("geotiff/SBN_inc_percap.tif"),
      BenchmarkUtil.dataPath("geotiff/elevation-comp.tif"),
      BenchmarkUtil.dataPath("geotiff/r-nir-wm-clipped.tif")
    )

  for(path <- paths) {
    benchmark(s"Converting a GeoTools GridCoverage2D to a GeoTrellis type: ${new java.io.File(path).getName}") {
      run("Foreach over GeoToolsTile") {
        new Benchmark {
          val p = path
          var gridCoverage: GridCoverage2D = _

          override def setUp(): Unit = {
            gridCoverage = getImage(p)
          }

          def run(): Unit = {
            // The previous way
            val Raster(tile, extent) = GridCoverage2DToRaster(gridCoverage).head
            var s = 0
            tile.foreach { z => if(isData(z)) s += z }
          }
        }
      }

      run("Foreach over GeoToolsTile Refactor") {
        new Benchmark {
          val p = path
          var gridCoverage: GridCoverage2D = _

          override def setUp(): Unit = {
            gridCoverage = getImage(p)
          }

          def run(): Unit = {
            // The refactored way
            val Raster(tile, extent) = gridCoverage.toRaster(0)
            var s = 0
            tile.foreach { z => if(isData(z)) s += z }
          }
        }
      }
    }
  }
}
Running benchmarks for Converting a GeoTools GridCoverage2D to a GeoTrellis type: SBN_inc_percap.tif...
  1 of 2: Foreach over GeoToolsTile     54130140.55 ns; σ=57058178.07221 ns @ 10 trials     
  2 of 2: Foreach over GeoToolsTile     4550679.99 ns; σ=4796837.89136 ns @ 10 trials               

  Name                                    ms  linear runtime
  Foreach over GeoToolsTile            54.13  ===========================
  Foreach over GeoToolsTile Refa        4.55  ==
Running benchmarks for Converting a GeoTools GridCoverage2D to a GeoTrellis type: elevation-comp.tif...
  1 of 2: Foreach over GeoToolsTile     36791265.45 ns; σ=38781398.93927 ns @ 10 trials       
  2 of 2: Foreach over GeoToolsTile     3368608.23 ns; σ=3550824.85014 ns @ 10 trials               

  Name                                    ms  linear runtime
  Foreach over GeoToolsTile            36.79  ============================
  Foreach over GeoToolsTile Refa        3.37  ==
Running benchmarks for Converting a GeoTools GridCoverage2D to a GeoTrellis type: r-nir-wm-clipped.tif...
  1 of 2: Foreach over GeoToolsTile     2954836000.00 ns; σ=3114670624.08710 ns @ 10 trials
  2 of 2: Foreach over GeoToolsTile     974585600.00 ns; σ=1027303423.60060 ns @ 10 trials

  Name                                     s  linear runtime
  Foreach over GeoToolsTile             2.95  =========================
  Foreach over GeoToolsTile Refa        0.97  ========

These are big changes to this PR, so it would be best to freeze work on this until my changes take place. Let me know if there's any significant difference to the branch I pulled yesterday vs this branch (I see a bunch of commits after my comment, but can't tell if those are actually newly pushed commits or old commits that I have).

@lossyrob

lossyrob commented May 21, 2016 •

Copy link
Copy Markdown
Member

It seems like NoData is handled in GeoTools differently then the GridSampleModel method: from what I have read, it is recommended to use the CoverageUtilities.getNoDataProperty(image) to get the NoData value(s) from a GridCoverage2D (and likewise set that property when creating a GridCoverage2D). Is there are reason to not use this method?

http://docs.geotools.org/stable/userguide/tutorial/raster/jaiext.html
https://github.com/geotools/geotools/blob/master/modules/library/coverage/src/main/java/org/geotools/resources/coverage/CoverageUtilities.java#L222

I'm including using this method to get/set NoData as part of the changes, so no action is necessary as of now.

Also, this is a NoData per GridCoverage2D, which would allow us to view GridCoverage2D as a Raster[MulitbandTile] rather than a Seq[Raster[Tile]], which is desirable.

@lossyrob

lossyrob commented Jun 1, 2016

Copy link
Copy Markdown
Member

Superseded by #1502

@lossyrob lossyrob closed this Jun 1, 2016
@jamesmcclain
jamesmcclain deleted the feature/jwm/geotools branch June 9, 2016 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants