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

[SEDONA-460] Add RS_Tile and RS_TileExplode #1177

Merged
merged 1 commit into from
Jan 3, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading