Skip to content

Commit

Permalink
[SEDONA-460] Add RS_Tile and RS_TileExplode (#1177)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kontinuation authored Jan 3, 2024
1 parent 1cc4c82 commit 21300bc
Show file tree
Hide file tree
Showing 9 changed files with 750 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import org.apache.sedona.common.FunctionsGeoTools;
import org.apache.sedona.common.raster.inputstream.ByteArrayImageInputStream;
import org.apache.sedona.common.raster.netcdf.NetCdfReader;
import org.apache.sedona.common.utils.ImageUtils;
import org.apache.sedona.common.utils.RasterUtils;
import org.geotools.coverage.GridSampleDimension;
import org.geotools.coverage.grid.GridCoverage2D;
import org.geotools.coverage.grid.GridEnvelope2D;
import org.geotools.coverage.grid.GridGeometry2D;
Expand All @@ -36,6 +38,7 @@
import org.locationtech.jts.geom.Geometry;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.metadata.spatial.PixelOrientation;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
Expand All @@ -44,6 +47,9 @@
import ucar.nc2.NetcdfFiles;

import javax.media.jai.RasterFactory;
import java.awt.Rectangle;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -380,4 +386,166 @@ public static GridCoverage2D makeNonEmptyRaster(int numBands, String bandDataTyp
transform, crs, null);
return RasterUtils.create(raster, gridGeometry, null);
}


public static class Tile {
private final int tileX;
private final int tileY;
private final GridCoverage2D coverage;

public Tile(int tileX, int tileY, GridCoverage2D coverage) {
this.tileX = tileX;
this.tileY = tileY;
this.coverage = coverage;
}

public int getTileX() {
return tileX;
}

public int getTileY() {
return tileY;
}

public GridCoverage2D getCoverage() {
return coverage;
}
}

/**
* Generate tiles from a grid coverage
* @param gridCoverage2D the grid coverage
* @param bandIndices the indices of the bands to select (1-based), can be null or empty to include all the bands.
* @param tileWidth the width of the tiles
* @param tileHeight the height of the tiles
* @param padWithNoData whether to pad the tiles with no data value
* @param padNoDataValue the no data value for padded tiles, only used when padWithNoData is true.
* If the value is NaN, the no data value of the original band will be used.
* @return the tiles
*/
public static Tile[] generateTiles(GridCoverage2D gridCoverage2D, int[] bandIndices, int tileWidth, int tileHeight,
boolean padWithNoData, double padNoDataValue) {
int numBands = gridCoverage2D.getNumSampleDimensions();
if (bandIndices == null || bandIndices.length == 0) {
// Select all the bands
bandIndices = new int[numBands];
for (int i = 0; i < numBands; i++) {
bandIndices[i] = i + 1;
}
} else {
// Check the band indices
for (int bandIndex : bandIndices) {
if (bandIndex <= 0 || bandIndex > numBands) {
throw new IllegalArgumentException(
String.format("Provided band index %d is not present in the raster", bandIndex));
}
}
}
return doGenerateTiles(gridCoverage2D, bandIndices, tileWidth, tileHeight, padWithNoData, padNoDataValue);
}

/**
* Generate tiles from an in-db grid coverage. The generated tiles are also in-db grid coverages. Pixel data will be
* copied into the tiles.
* @param gridCoverage2D the in-db grid coverage
* @param bandIndices the indices of the bands to select (1-based)
* @param tileWidth the width of the tiles
* @param tileHeight the height of the tiles
* @param padWithNoData whether to pad the tiles with no data value
* @param padNoDataValue the no data value for padded tiles, only used when padWithNoData is true.
* If the value is NaN, the no data value of the original band will be used.
* @return the tiles
*/
private static Tile[] doGenerateTiles(GridCoverage2D gridCoverage2D, int[] bandIndices, int tileWidth,
int tileHeight, boolean padWithNoData, double padNoDataValue) {
AffineTransform2D affine = RasterUtils.getAffineTransform(gridCoverage2D, PixelOrientation.CENTER);
RenderedImage image = gridCoverage2D.getRenderedImage();
double[] noDataValues = new double[bandIndices.length];
for (int i = 0; i < bandIndices.length; i++) {
noDataValues[i] = RasterUtils.getNoDataValue(gridCoverage2D.getSampleDimension(bandIndices[i] - 1));
}
int width = image.getWidth();
int height = image.getHeight();
int numTileX = (int) Math.ceil((double) width / tileWidth);
int numTileY = (int) Math.ceil((double) height / tileHeight);
Tile[] tiles = new Tile[numTileX * numTileY];
for (int tileY = 0; tileY < numTileY; tileY++) {
for (int tileX = 0; tileX < numTileX; tileX++) {
int x0 = tileX * tileWidth;
int y0 = tileY * tileHeight;

// Rect to copy from the original image
int rectWidth = Math.min(tileWidth, width - x0);
int rectHeight = Math.min(tileHeight, height - y0);

// If we don't pad with no data, the tiles on the boundary may have a different size
int currentTileWidth = padWithNoData? tileWidth: rectWidth;
int currentTileHeight = padWithNoData? tileHeight: rectHeight;
boolean needPadding = padWithNoData && (rectWidth < tileWidth || rectHeight < tileHeight);

// Create a new affine transformation for this tile
AffineTransform2D tileAffine = RasterUtils.translateAffineTransform(affine, x0, y0);
GridGeometry2D gridGeometry2D = new GridGeometry2D(
new GridEnvelope2D(0, 0, currentTileWidth, currentTileHeight),
PixelInCell.CELL_CENTER,
tileAffine, gridCoverage2D.getCoordinateReferenceSystem(), null);

// Prepare a new image for this tile, and copy the data from the original image
WritableRaster raster = RasterFactory.createBandedRaster(
image.getSampleModel().getDataType(), currentTileWidth, currentTileHeight,
bandIndices.length, null);
GridSampleDimension[] sampleDimensions = new GridSampleDimension[bandIndices.length];
Raster sourceRaster = image.getData(new Rectangle(x0, y0, rectWidth, rectHeight));
for (int k = 0; k < bandIndices.length; k++) {
int bandIndex = bandIndices[k] - 1;

// Copy sample dimensions from source bands, and pad with no data value if necessary
GridSampleDimension sampleDimension = gridCoverage2D.getSampleDimension(bandIndex);
double noDataValue = noDataValues[k];
if (needPadding && !Double.isNaN(padNoDataValue)) {
sampleDimension = RasterUtils.createSampleDimensionWithNoDataValue(sampleDimension, padNoDataValue);
noDataValue = padNoDataValue;
}
sampleDimensions[k] = sampleDimension;

// Copy data from original image to tile image
ImageUtils.copyRasterWithPadding(sourceRaster, bandIndex, raster, k, noDataValue);
}

GridCoverage2D tile = RasterUtils.create(raster, gridGeometry2D, sampleDimensions);
tiles[tileY * numTileX + tileX] = new Tile(tileX, tileY, tile);
}
}

return tiles;
}

public static GridCoverage2D[] rsTile(GridCoverage2D gridCoverage2D, int[] bandIndices, int tileWidth, int tileHeight,
boolean padWithNoData, Double padNoDataValue) {
if (gridCoverage2D == null) {
return null;
}
if (padNoDataValue == null) {
padNoDataValue = Double.NaN;
}
Tile[] tiles = generateTiles(gridCoverage2D, bandIndices, tileWidth, tileHeight, padWithNoData, padNoDataValue);
GridCoverage2D[] result = new GridCoverage2D[tiles.length];
for (int i = 0; i < tiles.length; i++) {
result[i] = tiles[i].getCoverage();
}
return result;
}

public static GridCoverage2D[] rsTile(GridCoverage2D gridCoverage2D, int[] bandIndices, int tileWidth, int tileHeight,
boolean padWithNoData) {
return rsTile(gridCoverage2D, bandIndices, tileWidth, tileHeight, padWithNoData, Double.NaN);
}

public static GridCoverage2D[] rsTile(GridCoverage2D gridCoverage2D, int[] bandIndices, int tileWidth, int tileHeight) {
return rsTile(gridCoverage2D, bandIndices, tileWidth, tileHeight, false);
}

public static GridCoverage2D[] rsTile(GridCoverage2D gridCoverage2D, int tileWidth, int tileHeight) {
return rsTile(gridCoverage2D, null, tileWidth, tileHeight);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sedona.common.utils;

import java.awt.image.Raster;
import java.awt.image.WritableRaster;

/**
* Utility functions for image processing.
*/
public class ImageUtils {
private ImageUtils() {}

/**
* Copy a raster to another raster, with padding if necessary.
* @param sourceRaster the source raster
* @param sourceBand the source band
* @param destRaster the destination raster, which must not be smaller than the source raster
* @param destBand the destination band
* @param padValue the padding value, or NaN if no padding is needed
*/
public static void copyRasterWithPadding(Raster sourceRaster, int sourceBand, WritableRaster destRaster, int destBand, double padValue) {
int destWidth = destRaster.getWidth();
int destHeight = destRaster.getHeight();
int destMinX = destRaster.getMinX();
int destMinY = destRaster.getMinY();
int sourceWidth = sourceRaster.getWidth();
int sourceHeight = sourceRaster.getHeight();
int sourceMinX = sourceRaster.getMinX();
int sourceMinY = sourceRaster.getMinY();
if (sourceWidth > destWidth || sourceHeight > destHeight) {
throw new IllegalArgumentException("Source raster is larger than destination raster");
}

// Copy the source raster to the destination raster
double[] samples = sourceRaster.getSamples(sourceMinX, sourceMinY, sourceWidth, sourceHeight, sourceBand, (double[]) null);
destRaster.setSamples(destMinX, destMinY, sourceWidth, sourceHeight, destBand, samples);

// Pad the right edge
for (int y = destMinY; y < sourceHeight + destMinY; y++) {
for (int x = sourceWidth + destMinX; x < destWidth + destMinX; x++) {
destRaster.setSample(x, y, destBand, padValue);
}
}
// Pad the bottom edge
for (int y = sourceHeight + destMinY; y < destHeight + destMinY; y++) {
for (int x = destMinX; x < destWidth + destMinX; x++) {
destRaster.setSample(x, y, destBand, padValue);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,27 @@ public static AffineTransform2D getAffineTransform(GridCoverage2D raster, PixelO
return (AffineTransform2D) crsTransform;
}

/**
* Translate an affine transformation by a given offset.
* @param affine the affine transformation
* @param offsetX the offset in x direction
* @param offsetY the offset in y direction
* @return the translated affine transformation
*/
public static AffineTransform2D translateAffineTransform(AffineTransform2D affine, int offsetX, int offsetY) {
double ipX = affine.getTranslateX();
double ipY = affine.getTranslateY();
double scaleX = affine.getScaleX();
double scaleY = affine.getScaleY();
double skewX = affine.getShearX();
double skewY = affine.getShearY();

// Move the origin using the affine transformation, and leave scale and skew unchanged.
double newIpX = ipX + offsetX * scaleX + offsetY * skewX;
double newIpY = ipY + offsetX * skewY + offsetY * scaleY;
return new AffineTransform2D(scaleX, skewY, skewX, scaleY, newIpX, newIpY);
}

public static Point2D getWorldCornerCoordinates(GridCoverage2D raster, int colX, int rowY) throws TransformException {
return raster.getGridGeometry().getGridToCRS2D(PixelOrientation.UPPER_LEFT).transform(new GridCoordinates2D(colX - 1, rowY - 1), null);
}
Expand Down
Loading

0 comments on commit 21300bc

Please sign in to comment.