Note
Go to the end to download the full example code
Insert RasterΒΆ
The RasterElement objects store the Raster data in WKB form. This WKB format is usually fetched from the database but when the data comes from another source it can be hard to format it as as a WKB. This example shows a method to convert input data into a WKB in order to insert it. This example uses SQLAlchemy ORM queries.
Warning
The PixelType values are not always properly translated by the Rasterio library, so exporting a raster and re-importing it using this method will properly import the values but might not keep the same internal types.
16 import struct
17 from sys import byteorder
18
19 import numpy as np
20 import pytest
21 import rasterio
22 from sqlalchemy import Column
23 from sqlalchemy import Integer
24 from sqlalchemy import MetaData
25 from sqlalchemy import text
26 from sqlalchemy.orm import declarative_base
27
28 from geoalchemy2 import Raster
29 from geoalchemy2 import RasterElement
30
31 # Tests imports
32 from tests import test_only_with_dialects
33
34 metadata = MetaData()
35 Base = declarative_base(metadata=metadata)
36
37
38 class Ocean(Base): # type: ignore
39 __tablename__ = "ocean"
40 id = Column(Integer, primary_key=True)
41 rast = Column(Raster)
42
43 def __init__(self, rast):
44 self.rast = rast
45
46
47 _DTYPE = {
48 "?": [0, "?", 1],
49 "u1": [2, "B", 1],
50 "i1": [3, "b", 1],
51 "B": [4, "B", 1],
52 "i2": [5, "h", 2],
53 "u2": [6, "H", 2],
54 "i4": [7, "i", 4],
55 "u4": [8, "I", 4],
56 "f4": [10, "f", 4],
57 "f8": [11, "d", 8],
58 }
59
60
61 def write_wkb_raster(dataset):
62 """Creates a WKB raster from the given raster file with rasterio.
63 :dataset: Rasterio dataset
64 :returns: binary: Binary raster in WKB format
65
66 This function was imported from
67 https://github.com/nathancahill/wkb-raster/blob/master/wkb_raster.py
68 and slightly adapted.
69 """
70
71 # Define format, see https://docs.python.org/3/library/struct.html
72 format_string = "bHHddddddIHH"
73
74 if byteorder == "big":
75 endian = ">"
76 endian_byte = 0
77 elif byteorder == "little":
78 endian = "<"
79 endian_byte = 1
80
81 # Write the raster header data.
82 header = bytes()
83
84 transform = dataset.transform.to_gdal()
85
86 version = 0
87 nBands = int(dataset.count)
88 scaleX = transform[1]
89 scaleY = transform[5]
90 ipX = transform[0]
91 ipY = transform[3]
92 skewX = 0
93 skewY = 0
94 srid = int(dataset.crs.to_string().split("EPSG:")[1])
95 width = int(dataset.meta.get("width"))
96 height = int(dataset.meta.get("height"))
97
98 fmt = f"{endian}{format_string}"
99
100 header = struct.pack(
101 fmt,
102 endian_byte,
103 version,
104 nBands,
105 scaleX,
106 scaleY,
107 ipX,
108 ipY,
109 skewX,
110 skewY,
111 srid,
112 width,
113 height,
114 )
115
116 bands = []
117
118 # Create band header data
119
120 # not used - always False
121 isOffline = False
122 hasNodataValue = False
123
124 if "nodata" in dataset.meta:
125 hasNodataValue = True
126
127 # not used - always False
128 isNodataValue = False
129
130 # unset
131 reserved = False
132
133 # # Based on the pixel type, determine the struct format, byte size and
134 # # numpy dtype
135 rasterio_dtype = dataset.meta.get("dtype")
136 dt_short = np.dtype(rasterio_dtype).str[1:]
137 pixtype, nodata_fmt, _ = _DTYPE[dt_short]
138
139 # format binary -> :b
140 binary_str = f"{isOffline:b}{hasNodataValue:b}{isNodataValue:b}{reserved:b}{pixtype:b}"
141 # convert to int
142 binary_decimal = int(binary_str, 2)
143
144 # pack to 1 byte
145 # 4 bits for ifOffline, hasNodataValue, isNodataValue, reserved
146 # 4 bit for pixtype
147 # -> 8 bit = 1 byte
148 band_header = struct.pack("<b", binary_decimal)
149
150 # Write the nodata value
151 nodata = struct.pack(nodata_fmt, int(dataset.meta.get("nodata") or 0))
152
153 for i in range(1, nBands + 1):
154 band_array = dataset.read(i)
155
156 # # Write the pixel values: width * height * size
157
158 # numpy tobytes() method instead of packing with struct.pack()
159 band_binary = band_array.reshape(width * height).tobytes()
160
161 bands.append(band_header + nodata + band_binary)
162
163 # join all bands
164 allbands = bytes()
165 for b in bands:
166 allbands += b
167
168 wkb = header + allbands
169
170 return wkb
171
172
173 @test_only_with_dialects("postgresql")
174 class TestInsertRaster:
175 @pytest.fixture(
176 params=[
177 "1BB",
178 "2BUI",
179 "4BUI",
180 "8BSI",
181 "8BUI",
182 "16BSI",
183 "16BUI",
184 "32BSI",
185 "32BUI",
186 "32BF",
187 "64BF",
188 ]
189 )
190 def input_img(self, conn, tmpdir, request):
191 """Create a TIFF image that will be imported as Raster."""
192 pixel_type = request.param
193 conn.execute(text("SET postgis.gdal_enabled_drivers = 'ENABLE_ALL';"))
194 data = conn.execute(
195 text(
196 """SELECT
197 ST_AsTIFF(
198 ST_AsRaster(
199 ST_GeomFromText('POLYGON((0 0,1 1,0 1,0 0))'),
200 5,
201 6,
202 '{}'
203 ),
204 'GTiff'
205 );""".format(
206 pixel_type
207 )
208 )
209 ).scalar()
210 filename = tmpdir / "image.tiff"
211 with open(filename, "wb") as f:
212 f.write(data.tobytes())
213 return filename, pixel_type
214
215 def test_insert_raster(self, session, conn, input_img):
216 """Insert a TIFF image into a raster column."""
217 filename, pixel_type = input_img
218 metadata.drop_all(conn, checkfirst=True)
219 metadata.create_all(conn)
220
221 # Load the image and transform it into a WKB
222 with rasterio.open(str(filename), "r+") as dataset:
223 dataset.crs = rasterio.crs.CRS.from_epsg(4326)
224 expected_values = dataset.read()[0]
225 raw_wkb = write_wkb_raster(dataset)
226
227 # Insert a new raster element
228 polygon_raster = RasterElement(raw_wkb)
229 o = Ocean(polygon_raster)
230 session.add(o)
231 session.flush()
232
233 # Check inserted values
234 new_values = conn.execute(text("SELECT ST_DumpValues(rast, 1, false) FROM ocean;")).scalar()
235 np.testing.assert_array_equal(
236 np.array(new_values, dtype=expected_values.dtype), expected_values
237 )