How to get perfect pixel alignment - Sentinel 2 data download

Image pixels from adjacent (but overlapping) AOIs do not align when the data is downloaded, and I would like to know how I can ensure two requests for image pixels share the same alignment?
image

I am currently using the Python SDK, but to recreate the issue I have used the request builder, and here is the generated code for 2 adjacent AOIs, for S2 data on the same date.

AOI 1:

# This is script may only work with sentinelhub.__version__ >= '3.4.0'from sentinelhub import SentinelHubRequest, DataCollection, MimeType, CRS, BBox, SHConfig, Geometry# Credentialsconfig = SHConfig()config.sh_client_id = '<your client id here>'config.sh_client_secret = '<your client secret here>'evalscript = """//VERSION=3function setup() {  return {    input: ["B02", "B03", "B04"],    output: { bands: 3 }  };}function evaluatePixel(sample) {  return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];}"""bbox = BBox(bbox=[30.55303014474393, -29.444193007640393, 30.554029904282658, -29.44325944105369], crs=CRS.WGS84)request = SentinelHubRequest(    evalscript=evalscript,    input_data=[        SentinelHubRequest.input_data(            data_collection=DataCollection.SENTINEL2_L2A,                      time_interval=('2022-07-02', '2022-07-02'),                  ),    ],    responses=[        SentinelHubRequest.output_response('default', MimeType.TIFF),    ],    bbox=bbox,    resolution=(0.00017699, 0.00017699),    config=config)response = request.get_data()

AOI 2:

# This is script may only work with sentinelhub.__version__ >= '3.4.0'from sentinelhub import SentinelHubRequest, DataCollection, MimeType, CRS, BBox, SHConfig, Geometry# Credentialsconfig = SHConfig()config.sh_client_id = '<your client id here>'config.sh_client_secret = '<your client secret here>'evalscript = """//VERSION=3function setup() {  return {    input: ["B02", "B03", "B04"],    output: { bands: 3 }  };}function evaluatePixel(sample) {  return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];}"""bbox = BBox(bbox=[30.55374081718668, -29.443881819731814, 30.554732546528953, -29.442930767626954], crs=CRS.WGS84)request = SentinelHubRequest(    evalscript=evalscript,    input_data=[        SentinelHubRequest.input_data(            data_collection=DataCollection.SENTINEL2_L2A,                      time_interval=('2022-07-02', '2022-07-02'),                  ),    ],    responses=[        SentinelHubRequest.output_response('default', MimeType.TIFF),    ],    bbox=bbox,    resolution=(0.00017699, 0.00017699),    config=config)response = request.get_data()

e27fea03-e3e7-4f17-bacd-113202777076.tiff (466 Bytes)
09585002-b6cb-4a23-9634-c4a6af560851.tiff (462 Bytes)

Hi @jens.hiestermann ,

To make pixels aligned perfectly, you should align your bounding box coordinates to 10m using UTM.

In your case, you can convert your AOI to EPSG:32736 and round it to 10 meters.

aoi_1 = BBox(bbox=[30.55303014474393, -29.444193007640393, 30.554029904282658, -29.44325944105369], crs=CRS.WGS84)
aoi_2 = BBox(bbox=[30.55374081718668, -29.443881819731814, 30.554732546528953, -29.442930767626954], crs=CRS.WGS84)

new_aoi_1 = np.round(list(aoi_1.transform(CRS(32736))), -1)
new_aoi_2 = np.round(list(aoi_2.transform(CRS(32736))), -1)

After aligning bounding box to 10 meters, the image pixels from the adjacent AOI should be perfectly aligned.

Below are the example requests for your AOIs.
AOI 1:

curl -X POST https://services.sentinel-hub.com/api/v1/process \
 -H 'Content-Type: application/json' \
 -H 'Authorization: Bearer ' \
 -d '{
  "input": {
    "bounds": {
      "bbox": [
        262650,
        6740310,
        262750,
        6740410
      ],
      "properties": {
        "crs": "http://www.opengis.net/def/crs/EPSG/0/32736"
      }
    },
    "data": [
      {
        "dataFilter": {
          "timeRange": {
            "from": "2022-09-01T00:00:00Z",
            "to": "2022-11-10T23:59:59Z"
          },
          "mosaickingOrder": "leastCC"
        },
        "type": "sentinel-2-l2a"
      }
    ]
  },
  "output": {
    "width": 10,
    "height": 10,
    "responses": [
      {
        "identifier": "default",
        "format": {
          "type": "image/tiff"
        }
      }
    ]
  },
  "evalscript": "//VERSION=3\n\nfunction setup() {\n  return {\n    input: [\"B02\", \"B03\", \"B04\"],\n    output: { bands: 3 }\n  };\n}\n\nfunction evaluatePixel(sample) {\n  return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];\n}"
}'

AOI 2:

curl -X POST https://services.sentinel-hub.com/api/v1/process \
 -H 'Content-Type: application/json' \
 -H 'Authorization: Bearer ' \
 -d '{
  "input": {
    "bounds": {
      "bbox": [
        262720,
        6740340,
        262820,
        6740450
      ],
      "properties": {
        "crs": "http://www.opengis.net/def/crs/EPSG/0/32736"
      }
    },
    "data": [
      {
        "dataFilter": {
          "timeRange": {
            "from": "2022-09-01T00:00:00Z",
            "to": "2022-11-10T23:59:59Z"
          },
          "mosaickingOrder": "leastCC"
        },
        "type": "sentinel-2-l2a"
      }
    ]
  },
  "output": {
    "width": 10,
    "height": 11,
    "responses": [
      {
        "identifier": "default",
        "format": {
          "type": "image/tiff"
        }
      }
    ]
  },
  "evalscript": "//VERSION=3\n\nfunction setup() {\n  return {\n    input: [\"B02\", \"B03\", \"B04\"],\n    output: { bands: 3 }\n  };\n}\n\nfunction evaluatePixel(sample) {\n  return [2.5 * sample.B04, 2.5 * sample.B03, 2.5 * sample.B02];\n}"
}'

Thank you for the response @chung.horng.

Follow-on comments, after implementing Chung’s solution:

  1. Below I adapted the ‘to_utm_bbox’ function in the geo_utils library (included in the sentinel hub python sdk) to include the ability to align pixels given a resolution.
# A function to round to a multiple
def round_to_multiple(number, multiple):
    return multiple * round(number / multiple)

def to_utm_bbox(bbox: BBox, resolution: int = 10, align_pixel: bool = False) -> BBox:
    """Transform bbox into UTM CRS

    :param bbox: bounding box
    :param align_pixel: do pixels need to be aligned to a similar upper limits (default: False)
    :return: bounding box in UTM CRS
    """
    if not CRS.is_utm(bbox.crs):
        lng, lat = bbox.middle
        utm_crs = get_utm_crs(lng, lat, source_crs=bbox.crs)
        bbox = bbox.transform(CRS(utm_crs))
    if align_pixel:
        bbox_bounds = tuple([round_to_multiple(coord, resolution)for coord in list(bbox)])
        bbox = BBox(bbox=bbox_bounds, crs=CRS(utm_crs))
    return bbox
bbox = BBox(bbox=bounds, crs=CRS.WGS84)
# Convert the bounding box to UTM so that pixels are snapped and aligned.
bbox_utm = to_utm_bbox(bbox=bbox,resolution=resolution, align_pixel=True)
  1. It is also valuable to note that you can use ‘get_utm_crs’ to locate the correct utm epsg code for the projection.
1 Like