Skip to content

Commit d5cdae7

Browse files
committed
Fixes #68
Added better natural color rendering to slippy map export. Signed-off-by: Simeon H.K. Fitch <[email protected]>
1 parent 2373c8b commit d5cdae7

File tree

10 files changed

+93
-75
lines changed

10 files changed

+93
-75
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/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: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +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}
2827
import org.apache.spark.sql.types.LongType
2928
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
3032

3133
/**
3234
* Spark SQL data source over GeoTIFF files.
@@ -88,10 +90,25 @@ class DefaultSource extends DataSourceRegister with RelationProvider with Creata
8890
if(cols.toDouble * rows * 64.0 > Runtime.getRuntime.totalMemory() * 0.5)
8991
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.")
9092

91-
logger.debug(s"Writing DataFrame to GeoTIFF ($cols by $rows) at ${pathO.get}")
92-
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)
9395

94-
GeoTiff(raster).write(pathO.get.getPath)
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)
109+
110+
logger.debug(s"Writing DataFrame to GeoTIFF ($cols x $rows) at ${pathO.get}")
111+
geotiff.write(pathO.get.getPath)
95112
GeoTiffRelation(sqlContext, pathO.get)
96113
}
97114
}
@@ -101,4 +118,5 @@ object DefaultSource {
101118
final val PATH_PARAM = "path"
102119
final val IMAGE_WIDTH_PARAM = "imageWidth"
103120
final val IMAGE_HEIGHT_PARAM = "imageWidth"
121+
final val COMPRESS = "compress"
104122
}

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ trait GeoTiffInfoSupport {
5959
// * `info.rasterExtent.{cellwidth|cellheight}` is the per-pixel spatial resolution
6060
// * `info.extent` and `info.rasterExtent.extent` are the same thing
6161

62-
6362
val tlm = {
6463
val tileLayout = if(info.segmentLayout.isTiled) {
6564
info.segmentLayout.tileLayout
@@ -77,17 +76,9 @@ trait GeoTiffInfoSupport {
7776
SpatialKey(tileLayout.layoutCols - 1, tileLayout.layoutRows - 1)
7877
)
7978

80-
val layoutExtentWidth = tileLayout.totalCols * info.rasterExtent.cellwidth
81-
val layoutExtentHeight = tileLayout.totalRows * info.rasterExtent.cellheight
82-
83-
val layoutExtent = Extent(
84-
extent.xmin,
85-
extent.ymin,
86-
extent.xmin + layoutExtentWidth,
87-
extent.ymin + layoutExtentHeight
88-
)
89-
90-
TileLayerMetadata(cellType, LayoutDefinition(layoutExtent, tileLayout), extent, crs, bounds)
79+
TileLayerMetadata(cellType,
80+
LayoutDefinition(info.rasterExtent, tileLayout.tileCols, tileLayout.tileRows),
81+
extent, crs, bounds)
9182
}
9283

9384
(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: 4 additions & 17 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,33 +106,17 @@ class GeoTiffDataSourceSpec
103106
}
104107

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

111113
logger.info(s"Read extent: ${rf.tileLayerMetadata.merge.extent}")
112114

113-
val cellsize = rf.groupBy(
114-
tileDimensions(col("tile_1"))("rows") as "tileRows",
115-
tileDimensions(col("tile_1"))("cols") as "tileCols"
116-
).count()
117-
118-
cellsize.show(false)
119-
120-
rf.withBounds().select(
121-
tileDimensions(col("tile_1"))("rows") as "tileRows",
122-
tileDimensions(col("tile_1"))("cols") as "tileCols",
123-
BOUNDS_COLUMN)
124-
.show(false)
125-
126-
val out = Paths.get(outputLocalPath, "example-geotiff.tiff")
115+
val out = Paths.get("target", "example-geotiff.tiff")
127116
logger.info(s"Writing to $out")
128-
//val out = Paths.get("target", "example-geotiff.tiff")
129117
noException shouldBe thrownBy {
130118
rf.write.geotiff.save(out.toString)
131119
}
132-
logger.info("Done!")
133120
}
134121
}
135122
}

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">

experimental/src/main/scala/astraea/spark/rasterframes/experimental/slippy/SlippyExport.scala

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@
2020

2121
package astraea.spark.rasterframes.experimental.slippy
2222

23+
import java.io.PrintStream
2324
import java.net.URI
2425

2526
import astraea.spark.rasterframes._
27+
import astraea.spark.rasterframes.util.MultibandRender.ColorRampProfile
2628
import astraea.spark.rasterframes.util._
2729
import geotrellis.proj4.{LatLng, WebMercator}
2830
import geotrellis.raster._
2931
import geotrellis.raster.io.geotiff._
3032
import geotrellis.raster.io.geotiff.tags.codes.ColorSpace
31-
import geotrellis.raster.render.{ColorMap, ColorRamps}
3233
import geotrellis.raster.resample.Bilinear
3334
import geotrellis.spark._
3435
import geotrellis.spark.io.slippy.HadoopSlippyTileWriter
@@ -41,7 +42,6 @@ import org.apache.hadoop.fs.{FileSystem, Path}
4142
import org.apache.spark.annotation.Experimental
4243

4344
import scala.io.Source
44-
import scala.util.Try
4545

4646
/**
4747
* Experimental support for exporting a RasterFrame into Slippy map format.
@@ -94,7 +94,7 @@ trait SlippyExport extends MethodExtensions[RasterFrame]{
9494
* @param colorMap Optional color map to use for rendering tiles in non-RGB RasterFrames.
9595
*/
9696
@Experimental
97-
def exportSlippyMap(dest: URI, colorMap: Option[ColorMap] = None): Unit = {
97+
def exportSlippyMap(dest: URI, renderer: MultibandRender.Profile = MultibandRender.Default): Unit = {
9898
val spark = self.sparkSession
9999
implicit val sc = spark.sparkContext
100100

@@ -116,15 +116,9 @@ trait SlippyExport extends MethodExtensions[RasterFrame]{
116116

117117
val (zoom, reprojected) = inputRDD.reproject(WebMercator, layoutScheme, Bilinear)
118118
val writer = new HadoopSlippyTileWriter[MultibandTile](dest.toASCIIString + "/" + tileDirName, "png")({ (_, tile) =>
119-
val png = if(colorMap.isEmpty && tile.bands.lengthCompare(3) == 0) {
120-
// `Try` below is due to https://github.com/locationtech/geotrellis/issues/2621
121-
tile.mapBands((_, t) Try(t.rescale(0, 255)).getOrElse(t)).renderPng()
122-
}
123-
else {
124-
// Are there other ways to use the other bands?
125-
val selected = tile.bands.head
126-
colorMap.map(m selected.renderPng(m)).getOrElse(selected.renderPng(ColorRamps.greyscale(256)))
127-
}
119+
require(tile.bandCount >= 3 || renderer.isInstanceOf[ColorRampProfile],
120+
"Single-band and dual-band RasterFrames require a ColorRampProfile for rendering")
121+
val png = renderer.render(tile)
128122
png.bytes
129123
})
130124

@@ -153,10 +147,10 @@ object SlippyExport {
153147
val subst = new StrSubstitutor(subs.asJava)
154148

155149
val fs = FileSystem.get(dest, conf)
156-
withResource(fs.create(new Path(new Path(dest), "index.html"), true)) { out
150+
withResource(fs.create(new Path(new Path(dest), "index.html"), true)) { hout
151+
val out = new PrintStream(hout, true ,"UTF-8")
157152
for(line rawLines) {
158-
out.writeBytes(subst.replace(line))
159-
out.writeChar('\n')
153+
out.println(subst.replace(line))
160154
}
161155
}
162156
}

0 commit comments

Comments
 (0)