Skip to content

Commit 2c9733a

Browse files
authored
Merge pull request #70 from locationtech/fix/68
Partial fix for 68
2 parents d05d6c6 + d5cdae7 commit 2c9733a

File tree

11 files changed

+159
-61
lines changed

11 files changed

+159
-61
lines changed

core/src/main/scala/astraea/spark/rasterframes/util/MultibandRender.scala

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
package astraea.spark.rasterframes.util
2222

2323
import geotrellis.raster._
24-
import geotrellis.raster.render.Png
24+
import geotrellis.raster.render.{ColorRamp, Png}
25+
26+
import scala.util.Try
2527

2628
/**
2729
* Rework of process courtesy of @lossyrob for creating natural color RGB images in GeoTrellis.
@@ -55,6 +57,11 @@ object MultibandRender {
5557
}
5658
import CellTransforms._
5759

60+
/** Create an RGB composite PNG file from the given MultibandTile and color profile. */
61+
@deprecated("Use Profile.render instead", "0.7.0")
62+
def rgbComposite(tile: MultibandTile, profile: Profile): Png = profile.render(tile)
63+
64+
/** Base type for Rendering profiles. */
5865
trait Profile {
5966
/** Value from -255 to 255 */
6067
def brightness: Int = 0
@@ -73,8 +80,10 @@ object MultibandRender {
7380
/** Convert the tile to an Int-based cell type. */
7481
def normalizeCellType(tile: Tile): Tile = tile.convert(IntCellType)
7582

76-
/** Convert tile such that cells values fall between 0 and 255. */
77-
def compressRange(tile: Tile): Tile = tile
83+
/** Convert tile such that cells values fall between 0 and 255, if desired. */
84+
def compressRange(tile: Tile): Tile =
85+
// `Try` below is due to https://github.com/locationtech/geotrellis/issues/2621
86+
Try(tile.rescale(0, 255)).getOrElse(tile)
7887

7988
/** Apply color correction so it "looks nice". */
8089
def colorAdjust(tile: Tile): Tile = {
@@ -89,7 +98,15 @@ object MultibandRender {
8998
normalizeCellType(tile).map(pipeline)
9099
}
91100

92-
val applyAdjustment = compressRange _ andThen colorAdjust
101+
val applyAdjustment: Tile Tile =
102+
compressRange _ andThen colorAdjust
103+
104+
def render(tile: MultibandTile) = {
105+
val r = applyAdjustment(red(tile))
106+
val g = applyAdjustment(green(tile))
107+
val b = applyAdjustment(blue(tile))
108+
ArrayMultibandTile(r, g, b).renderPng
109+
}
93110
}
94111
case object Default extends Profile
95112

@@ -108,13 +125,11 @@ object MultibandRender {
108125

109126
case object NAIPNaturalColor extends Profile {
110127
override val gamma = 1.4
111-
override def compressRange(tile: Tile): Tile = tile.rescale(0, 255)
112128
}
113129

114-
def rgbComposite(tile: MultibandTile, profile: Profile): Png = {
115-
val red = profile.applyAdjustment(profile.red(tile))
116-
val green = profile.applyAdjustment(profile.green(tile))
117-
val blue = profile.applyAdjustment(profile.blue(tile))
118-
ArrayMultibandTile(red, green, blue).renderPng
130+
case class ColorRampProfile(ramp: ColorRamp) extends Profile {
131+
// Are there other ways to use the other bands?
132+
override def render(tile: MultibandTile): Png =
133+
colorAdjust(tile.band(0)).renderPng(ramp)
119134
}
120135
}

core/src/main/scala/astraea/spark/rasterframes/util/debug/package.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
package astraea.spark.rasterframes.util
2222

2323
import astraea.spark.rasterframes._
24+
import geotrellis.proj4.LatLng
25+
import geotrellis.vector.{Feature, Geometry}
26+
import geotrellis.vector.io.json.JsonFeatureCollection
27+
import spray.json.JsValue
2428

2529
/**
2630
* Additional debugging routines. No guarantees these are or will remain stable.
@@ -34,5 +38,19 @@ package object debug {
3438
def describeFullSchema: String = {
3539
self.schema.prettyJson
3640
}
41+
42+
/** Renders all the extents in this RasterFrame as GeoJSON in EPSG:4326. This does a full
43+
* table scan and collects **all** the geometry into the driver, and then converts it into a
44+
* Spray JSON data structure. Not performant, and for debugging only. */
45+
def geoJsonExtents: JsValue = {
46+
import spray.json.DefaultJsonProtocol._
47+
48+
val features = self
49+
.select(BOUNDS_COLUMN, SPATIAL_KEY_COLUMN)
50+
.collect()
51+
.map{ case (p, s) Feature(Geometry(p).reproject(self.crs, LatLng), Map("col" -> s.col, "row" -> s.row)) }
52+
53+
JsonFeatureCollection(features).toJson
54+
}
3755
}
3856
}

core/src/test/scala/astraea/spark/rasterframes/RasterFrameSpec.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ class RasterFrameSpec extends TestEnvironment with MetadataKeys
301301

302302
noException shouldBe thrownBy {
303303
val raster = joined.toMultibandRaster(Seq($"red", $"green", $"blue"), 128, 128)
304-
val png = MultibandRender.rgbComposite(raster.tile, MultibandRender.Landsat8NaturalColor)
304+
val png = MultibandRender.Landsat8NaturalColor.render(raster.tile)
305305
//png.write(s"target/${getClass.getSimpleName}.png")
306306
}
307307
}
@@ -313,7 +313,7 @@ class RasterFrameSpec extends TestEnvironment with MetadataKeys
313313

314314
noException shouldBe thrownBy {
315315
val raster = joined.toMultibandRaster(Seq($"red", $"green", $"blue"), 256, 256)
316-
val png = MultibandRender.rgbComposite(raster.tile, MultibandRender.NAIPNaturalColor)
316+
val png = MultibandRender.NAIPNaturalColor.render(raster.tile)
317317
png.write(s"target/${getClass.getSimpleName}.png")
318318
}
319319
}

core/src/test/scala/astraea/spark/rasterframes/TestData.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import com.vividsolutions.jts.geom.{Coordinate, GeometryFactory}
2525
import geotrellis.proj4.LatLng
2626
import geotrellis.raster
2727
import geotrellis.raster._
28-
import geotrellis.raster.io.geotiff.SinglebandGeoTiff
28+
import geotrellis.raster.io.geotiff.{MultibandGeoTiff, SinglebandGeoTiff}
2929
import geotrellis.spark._
3030
import geotrellis.spark.testkit.TileLayerRDDBuilders
3131
import geotrellis.spark.tiling.LayoutDefinition
@@ -89,6 +89,7 @@ trait TestData {
8989
}
9090

9191
def readSingleband(name: String) = SinglebandGeoTiff(IOUtils.toByteArray(getClass.getResourceAsStream("/" + name)))
92+
def readMultiband(name: String) = MultibandGeoTiff(IOUtils.toByteArray(getClass.getResourceAsStream("/" + name)))
9293

9394
/** 774 x 500 GeoTiff */
9495
def sampleGeoTiff = readSingleband("L8-B8-Robinson-IL.tiff")
@@ -106,6 +107,8 @@ trait TestData {
106107
readSingleband(s"NAIP-VA-b$band.tiff")
107108
}
108109

110+
def rgbCogSample = readMultiband("LC08_RGB_Norfolk_COG.tiff")
111+
109112
def sampleTileLayerRDD(implicit spark: SparkSession): TileLayerRDD[SpatialKey] = {
110113
val rf = sampleGeoTiff.projectedRaster.toRF(128, 128)
111114
rf.toTileLayerRDD(rf.tileColumns.head).left.get

datasource/src/main/scala/astraea/spark/rasterframes/datasource/geotiff/DefaultSource.scala

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@
1919

2020
package astraea.spark.rasterframes.datasource.geotiff
2121

22-
import _root_.geotrellis.raster.io.geotiff.GeoTiff
2322
import astraea.spark.rasterframes._
2423
import astraea.spark.rasterframes.datasource._
2524
import astraea.spark.rasterframes.util._
2625
import com.typesafe.scalalogging.LazyLogging
2726
import org.apache.spark.sql.sources.{BaseRelation, CreatableRelationProvider, DataSourceRegister, RelationProvider}
28-
import org.apache.spark.sql.{DataFrame, SQLContext, SaveMode}
27+
import org.apache.spark.sql.types.LongType
28+
import org.apache.spark.sql.{DataFrame, SQLContext, SaveMode, functions F}
29+
import _root_.geotrellis.raster.io.geotiff.{GeoTiffOptions, MultibandGeoTiff, Tags, Tiled}
30+
import _root_.geotrellis.raster.io.geotiff.compression._
31+
import _root_.geotrellis.raster.io.geotiff.tags.codes.ColorSpace
2932

3033
/**
3134
* Spark SQL data source over GeoTIFF files.
@@ -50,25 +53,62 @@ class DefaultSource extends DataSourceRegister with RelationProvider with Creata
5053
require(pathO.get.getScheme == "file" || pathO.get.getScheme == null, "Currently only 'file://' destinations are supported")
5154
sqlContext.withRasterFrames
5255

53-
5456
require(data.isRF, "GeoTIFF can only be constructed from a RasterFrame")
5557
val rf = data.certify
5658

57-
val tl = rf.tileLayerMetadata.merge.layout.tileLayout
59+
// If no desired image size is given, write at full size.
60+
lazy val (fullResCols, fullResRows) = {
61+
// get the layout size given that the tiles may be heterogenously sized
62+
// first get any valid row and column in the spatial key structure
63+
val sk = rf.select(SPATIAL_KEY_COLUMN).first()
64+
65+
val tc = rf.tileColumns.head
66+
67+
val c = rf
68+
.where(SPATIAL_KEY_COLUMN("row") === sk.row)
69+
.agg(
70+
F.sum(tileDimensions(tc)("cols") cast(LongType))
71+
).first()
72+
.getLong(0)
73+
74+
val r = rf
75+
.where(SPATIAL_KEY_COLUMN("col") === sk.col)
76+
.agg(
77+
F.sum(tileDimensions(tc)("rows") cast(LongType))
78+
).first()
79+
.getLong(0)
5880

59-
val cols = numParam(DefaultSource.IMAGE_WIDTH_PARAM, parameters).getOrElse(tl.totalCols)
60-
val rows = numParam(DefaultSource.IMAGE_HEIGHT_PARAM, parameters).getOrElse(tl.totalRows)
81+
(c, r)
82+
}
83+
84+
val cols = numParam(DefaultSource.IMAGE_WIDTH_PARAM, parameters).getOrElse(fullResCols)
85+
val rows = numParam(DefaultSource.IMAGE_HEIGHT_PARAM, parameters).getOrElse(fullResRows)
6186

6287
require(cols <= Int.MaxValue && rows <= Int.MaxValue, s"Can't construct a GeoTIFF of size $cols x $rows. (Too big!)")
6388

6489
// Should we really play traffic cop here?
6590
if(cols.toDouble * rows * 64.0 > Runtime.getRuntime.totalMemory() * 0.5)
6691
logger.warn(s"You've asked for the construction of a very large image ($cols x $rows), destined for ${pathO.get}. Out of memory error likely.")
6792

68-
println()
69-
val raster = rf.toMultibandRaster(rf.tileColumns, cols.toInt, rows.toInt)
93+
val tcols = rf.tileColumns
94+
val raster = rf.toMultibandRaster(tcols, cols.toInt, rows.toInt)
95+
96+
// We make some assumptions here.... eventually have column metadata encode this.
97+
val colorSpace = tcols.size match {
98+
case 3 | 4 ColorSpace.RGB
99+
case _ ColorSpace.BlackIsZero
100+
}
101+
102+
val compress = parameters.get(DefaultSource.COMPRESS).map(_.toBoolean).getOrElse(false)
103+
val options = GeoTiffOptions(Tiled, if (compress) DeflateCompression else NoCompression, colorSpace)
104+
val tags = Tags(
105+
RFBuildInfo.toMap.filter(_._1.startsWith("rf")).mapValues(_.toString),
106+
tcols.map(c Map("RF_COL" -> c.columnName)).toList
107+
)
108+
val geotiff = new MultibandGeoTiff(raster.tile, raster.extent, raster.crs, tags, options)
70109

71-
GeoTiff(raster).write(pathO.get.getPath)
110+
logger.debug(s"Writing DataFrame to GeoTIFF ($cols x $rows) at ${pathO.get}")
111+
geotiff.write(pathO.get.getPath)
72112
GeoTiffRelation(sqlContext, pathO.get)
73113
}
74114
}
@@ -78,4 +118,5 @@ object DefaultSource {
78118
final val PATH_PARAM = "path"
79119
final val IMAGE_WIDTH_PARAM = "imageWidth"
80120
final val IMAGE_HEIGHT_PARAM = "imageWidth"
121+
final val COMPRESS = "compress"
81122
}

datasource/src/main/scala/astraea/spark/rasterframes/datasource/geotiff/GeoTiffInfoSupport.scala

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ import geotrellis.raster.io.geotiff.reader.GeoTiffReader.GeoTiffInfo
2727
import geotrellis.spark.{KeyBounds, SpatialKey, TileLayerMetadata}
2828
import geotrellis.spark.tiling.LayoutDefinition
2929
import geotrellis.util.ByteReader
30+
import geotrellis.vector.Extent
3031

3132
/**
32-
* Utility mix in for generating a tlm from GeoTiff headers.
33+
* Utility mix-in for generating a tlm from GeoTiff headers.
3334
*
3435
* @since 5/4/18
3536
*/
@@ -49,23 +50,35 @@ trait GeoTiffInfoSupport {
4950

5051
def extractGeoTiffLayout(reader: ByteReader): (GeoTiffReader.GeoTiffInfo, TileLayerMetadata[SpatialKey]) = {
5152
val info: GeoTiffInfo = Shims.readGeoTiffInfo(reader, false, true)
53+
// Some notes on GeoTiffInfo properties:
54+
// * `info.extent` is the actual geotiff extent
55+
// * `info.segmentLayout.tileLayout` contains the internal, regularized gridding of a tiled GeoTIFF
56+
// * `info.segmentLayout.tileLayout.{totalCols|totalRows}` is the largest number of possible cells in the internal gridding
57+
// * `info.segmentLayout.{totalCols|totalRows}` are the real dimensions of the GeoTIFF. This is likely smaller than
58+
// the total size of `info.segmentLayout.tileLayout.{totalCols|totalRows}`
59+
// * `info.rasterExtent.{cellwidth|cellheight}` is the per-pixel spatial resolution
60+
// * `info.extent` and `info.rasterExtent.extent` are the same thing
61+
5262
val tlm = {
53-
val layout = if(!info.segmentLayout.isTiled) {
63+
val tileLayout = if(info.segmentLayout.isTiled) {
64+
info.segmentLayout.tileLayout
65+
}
66+
else {
5467
val width = info.segmentLayout.totalCols
5568
val height = info.segmentLayout.totalRows
5669
defaultLayout(width, height)
5770
}
58-
else {
59-
info.segmentLayout.tileLayout
60-
}
6171
val extent = info.extent
6272
val crs = info.crs
6373
val cellType = info.cellType
6474
val bounds = KeyBounds(
6575
SpatialKey(0, 0),
66-
SpatialKey(layout.layoutCols - 1, layout.layoutRows - 1)
76+
SpatialKey(tileLayout.layoutCols - 1, tileLayout.layoutRows - 1)
6777
)
68-
TileLayerMetadata(cellType, LayoutDefinition(extent, layout), extent, crs, bounds)
78+
79+
TileLayerMetadata(cellType,
80+
LayoutDefinition(info.rasterExtent, tileLayout.tileCols, tileLayout.tileRows),
81+
extent, crs, bounds)
6982
}
7083

7184
(info, tlm)

datasource/src/main/scala/astraea/spark/rasterframes/datasource/geotiff/GeoTiffRelation.scala

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,9 @@ import java.net.URI
2323

2424
import astraea.spark.rasterframes._
2525
import astraea.spark.rasterframes.util._
26-
import geotrellis.raster.{MultibandTile, Tile, TileLayout}
27-
import geotrellis.raster.io.geotiff.reader.GeoTiffReader
2826
import geotrellis.spark._
2927
import geotrellis.spark.io._
3028
import geotrellis.spark.io.hadoop._
31-
import geotrellis.spark.tiling.LayoutDefinition
3229
import geotrellis.util._
3330
import org.apache.hadoop.fs.Path
3431
import org.apache.spark.rdd.RDD

datasource/src/test/scala/astraea/spark/rasterframes/datasource/geotiff/GeoTiffDataSourceSpec.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ package astraea.spark.rasterframes.datasource.geotiff
2121
import java.nio.file.Paths
2222

2323
import astraea.spark.rasterframes._
24+
import geotrellis.proj4.LatLng
2425
import org.apache.spark.sql.functions._
26+
import geotrellis.vector._
27+
import geotrellis.vector.io.json.JsonFeatureCollection
2528

2629
/**
2730
* @since 1/14/18
@@ -103,13 +106,14 @@ class GeoTiffDataSourceSpec
103106
}
104107

105108
it("should write GeoTIFF") {
106-
107109
val rf = spark.read
108110
.geotiff
109111
.loadRF(cogPath)
110112

111-
val out = Paths.get(outputLocalPath, "example-geotiff.tiff")
112-
//val out = Paths.get("target", "example-geotiff.tiff")
113+
logger.info(s"Read extent: ${rf.tileLayerMetadata.merge.extent}")
114+
115+
val out = Paths.get("target", "example-geotiff.tiff")
116+
logger.info(s"Writing to $out")
113117
noException shouldBe thrownBy {
114118
rf.write.geotiff.save(out.toString)
115119
}

experimental/src/it/scala/astraea/spark/rasterframes/experimental/slippy/SlippyExportDriver.scala

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import geotrellis.raster._
2727
import geotrellis.raster.io.geotiff.SinglebandGeoTiff
2828
import org.apache.spark.sql.SparkSession
2929
import SlippyExport._
30+
import astraea.spark.rasterframes.util.MultibandRender
3031

3132
object SlippyExportDriver {
3233
def main(args: Array[String]): Unit = {
@@ -38,21 +39,34 @@ object SlippyExportDriver {
3839
.getOrCreate()
3940
.withRasterFrames
4041

41-
val bands: Seq[SinglebandGeoTiff] = for (i 1 to 3) yield {
42-
TestData.readSingleband(s"NAIP-VA-b$i.tiff")
43-
//s"L8-B$i-Elkton-VA.tiff")
44-
}
42+
def mergeBands = {
43+
val bands: Seq[SinglebandGeoTiff] = for (i 1 to 3) yield {
44+
TestData.l8Sample(i)
45+
}
46+
47+
val mtile = MultibandTile(bands.map(_.tile))
4548

46-
val mtile = MultibandTile(bands.map(_.tile))
49+
val pr = ProjectedRaster(mtile, bands.head.extent, bands.head.crs)
4750

48-
val pr = ProjectedRaster(mtile, bands.head.extent, bands.head.crs)
51+
implicit val bandCount = PairRDDConverter.forSpatialMultiband(bands.length)
4952

50-
implicit val bandCount = PairRDDConverter.forSpatialMultiband(bands.length)
53+
val rf = pr.toRF(64, 64)
5154

52-
val rf = pr.toRF(64, 64)
55+
//rf.exportGeoTiffTiles(new File("target/slippy-tiff").toURI)
5356

54-
//rf.exportGeoTiffTiles(new File("target/slippy-tiff").toURI)
57+
rf.exportSlippyMap(new File("target/slippy-1/").toURI)
58+
}
59+
60+
def multiband = {
61+
implicit val bandCount = PairRDDConverter.forSpatialMultiband(3)
62+
val rf = TestData.rgbCogSample.projectedRaster.toRF(256, 256)
63+
import astraea.spark.rasterframes.datasource.geotiff._
64+
//val rf = spark.read.geotiff.loadRF(getClass.getResource("/LC08_RGB_Norfolk_COG.tiff").toURI)
65+
println(rf.tileLayerMetadata.merge.toString)
66+
rf.exportSlippyMap(new File("target/slippy-2/").toURI, MultibandRender.Landsat8NaturalColor)
67+
}
5568

56-
rf.exportSlippyMap(new File("target/slippy-png/").toURI)
69+
multiband
70+
//mergeBands
5771
}
5872
}

experimental/src/main/resources/slippy.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
<html lang="en">
2323
<head>
24-
<meta charset="UTF-8">
2524
<title>RasterFrames</title>
2625
<meta charset="utf-8" />
2726
<meta name="viewport" content="width=device-width, initial-scale=1.0">

0 commit comments

Comments
 (0)