Skip to content

Commit eb51d66

Browse files
authored
Merge pull request #245 from larzeitlin/geotiff-dtype-fixes-rerende
re-render .qmd following changes in #221
2 parents 10abfec + b211e00 commit eb51d66

File tree

1 file changed

+91
-66
lines changed

1 file changed

+91
-66
lines changed

site/gis/geotiff.qmd

Lines changed: 91 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
---
2-
author: luke-zeitlin
2+
author:
3+
name: Luke Zeitlin
4+
url: https://github.com/larzeitlin
5+
image: https://github.com/larzeitlin.png
6+
affiliation: []
7+
image: tiff_geotiff_cog.png
38
type: post
49
date: '2025-12-07'
510
category: gis
@@ -16,7 +21,7 @@ format:
1621
.clay-side-by-side .sourceCode {margin: 0}
1722
.clay-side-by-side {margin: 1em 0}
1823
</style>
19-
<script src="geotiff_files/md-default5.js" type="text/javascript"></script><script src="geotiff_files/md-default6.js" type="text/javascript"></script>
24+
<script src="https://code.jquery.com/jquery-3.6.0.min.js" type="text/javascript"></script><script src="https://code.jquery.com/ui/1.13.1/jquery-ui.min.js" type="text/javascript"></script>
2025

2126
# Cloud Optimized GeoTIFFs in JVM Clojure
2227

@@ -111,6 +116,14 @@ javax.imageio.stream.FileImageInputStream
111116

112117

113118

119+
::: {.sourceClojure}
120+
```clojure
121+
(require '[tech.v3.datatype :as dtype])
122+
```
123+
:::
124+
125+
126+
114127
::: {.sourceClojure}
115128
```clojure
116129
(defn get-pixel [^BufferedImage image x y]
@@ -120,7 +133,8 @@ javax.imageio.stream.FileImageInputStream
120133
num-bands (.getNumBands raster)
121134
pixel (double-array num-bands)]
122135
(.getPixel raster x y pixel)
123-
(vec pixel)))
136+
;; A dtype-next read-only buffer as a view of the Java array:
137+
(dtype/as-reader pixel)))
124138
```
125139
:::
126140

@@ -150,7 +164,8 @@ Now we use the above helper functions to handle our locally held TIFF.
150164

151165
::: {.printedClojure}
152166
```clojure
153-
[45.0]
167+
#buffer<float64>[1]
168+
[45.00]
154169

155170
```
156171
:::
@@ -196,7 +211,8 @@ This also works for a GeoTIFF:
196211

197212
::: {.printedClojure}
198213
```clojure
199-
[42.0 76.0 32.0]
214+
#buffer<float64>[3]
215+
[42.00, 76.00, 32.00]
200216

201217
```
202218
:::
@@ -219,7 +235,7 @@ to work with.
219235

220236
::: {.printedClojure}
221237
```clojure
222-
#object[it.geosolutions.imageioimpl.plugins.tiff.TIFFImageMetadata 0x66719488 "it.geosolutions.imageioimpl.plugins.tiff.TIFFImageMetadata@66719488"]
238+
#object[com.sun.media.imageioimpl.plugins.tiff.TIFFImageMetadata 0x4b46954f "com.sun.media.imageioimpl.plugins.tiff.TIFFImageMetadata@4b46954f"]
223239

224240
```
225241
:::
@@ -232,6 +248,7 @@ into formats handled by Clojure's dataset stack. Here I have done that via `Buff
232248
There may well be a more convenient way - please let me know!
233249

234250
There are several interconnected libraries for datasets in Clojure:
251+
235252
- `tablecloth` -> high level API for interacting with `tech.ml.dataset`
236253
- `tech.ml.dataset` -> tabular datasets in Clojure. This is the core library.
237254
- `dtype-next` -> the lowest level of this stack. array operations, buffer management, etc.
@@ -242,7 +259,9 @@ bring along the rest of the stack.
242259

243260
::: {.sourceClojure}
244261
```clojure
245-
(require '[tablecloth.api :as tc])
262+
(require '[tablecloth.api :as tc]
263+
'[tech.v3.tensor :as tensor]
264+
'[tech.v3.dataset.tensor :as ds-tensor])
246265
```
247266
:::
248267

@@ -253,26 +272,43 @@ bring along the rest of the stack.
253272
(defn buffered-image->dataset [^BufferedImage img]
254273
(let [width (.getWidth img)
255274
height (.getHeight img)
256-
raster (.getRaster img)
257-
num-bands (.getNumBands raster)
258-
259-
;; Pre-allocate arrays for each band's pixel data
260-
band-arrays (vec (repeatedly num-bands #(double-array (* width height))))
261-
262-
;; Read each band's data
263-
_ (doseq [band (range num-bands)]
264-
(.getSamples raster 0 0 width height band (nth band-arrays band)))
265-
266-
;; Create coordinate arrays
267-
xs (vec (for [_y (range height) x (range width)] x))
268-
ys (vec (for [y (range height) _x (range width)] y))
275+
num-bands (.getNumBands (.getRaster img))
276+
277+
;; The following will be easier after dtype-next's
278+
;; [#133](https://github.com/cnuernber/dtype-next/issues/133)
279+
;; is resolved.
280+
tensor (-> img
281+
dtype/->array-buffer
282+
;; height x width x bands
283+
(tensor/construct-tensor (dims/dimensions
284+
[width height num-bands]))
285+
;; bands x height x width
286+
(tensor/transpose [2 0 1]))
287+
288+
xs (dtype/as-reader
289+
(tensor/compute-tensor [height width]
290+
(fn [y x] x)))
291+
ys (dtype/as-reader
292+
(tensor/compute-tensor [height width]
293+
(fn [y x] y)))
269294

270295
;; Build dataset
271-
ds-map (into {:x xs :y ys}
272-
(map-indexed
273-
(fn [i arr] [(keyword (str "band-" i)) (vec arr)])
274-
band-arrays))]
275-
(tc/dataset ds-map)))
296+
ds-map (into {:x xs :y ys}
297+
(for [i (range num-bands)]
298+
[(keyword (str "band-" i))
299+
(dtype/as-reader
300+
(tensor i))]))]
301+
(-> tensor
302+
(tensor/reshape [(* height width)
303+
num-bands])
304+
ds-tensor/tensor->dataset
305+
(tc/add-columns {:x xs
306+
:y ys})
307+
(tc/select-columns (concat [:x :y]
308+
(range num-bands)))
309+
(tc/rename-columns (into {}
310+
(for [i (range num-bands)]
311+
[i (keyword (str "band-" i))]))))))
276312
```
277313
:::
278314

@@ -298,15 +334,15 @@ bring along the rest of the stack.
298334

299335

300336
::: {.clay-dataset}
301-
_unnamed: descriptive-stats [5 11]:
337+
:_unnamed: descriptive-stats [5 11]:
302338

303-
| :col-name | :datatype | :n-valid | :n-missing | :min | :mean | :max | :standard-deviation | :skew | :first | :last |
304-
|-----------|-----------|---------:|-----------:|-----:|--------------:|-------:|--------------------:|----------------:|-------:|-------:|
305-
| :x | :int64 | 29989001 | 0 | 0.0 | 2999.00000000 | 5998.0 | 1731.76213725 | 0.00000000E+00 | 0.0 | 5998.0 |
306-
| :y | :int64 | 29989001 | 0 | 0.0 | 2499.00000000 | 4998.0 | 1443.08699303 | -1.10958482E-17 | 0.0 | 4998.0 |
307-
| :band-0 | :float64 | 29989001 | 0 | 0.0 | 68.21786911 | 255.0 | 60.51839822 | 1.33067394E+00 | 42.0 | 9.0 |
308-
| :band-1 | :float64 | 29989001 | 0 | 0.0 | 88.75898750 | 255.0 | 38.37694129 | 1.53742117E+00 | 76.0 | 54.0 |
309-
| :band-2 | :float64 | 29989001 | 0 | 0.0 | 73.80655098 | 255.0 | 41.20030938 | 1.29732261E+00 | 32.0 | 71.0 |
339+
| :col-name | :datatype | :n-valid | :n-missing | :min | :mean | :max | :standard-deviation | :skew | :first | :last |
340+
|-----------|-----------|---------:|-----------:|-----:|--------------:|-------:|--------------------:|----------------:|-------:|------:|
341+
| :x | :int64 | 29989001 | 0 | 0.0 | 2999.00000000 | 5998.0 | 1731.76213725 | 0.00000000E+00 | 0 | 5998 |
342+
| :y | :int64 | 29989001 | 0 | 0.0 | 2499.00000000 | 4998.0 | 1443.08699303 | -1.10958482E-17 | 0 | 4998 |
343+
| :band-0 | :uint8 | 29989001 | 0 | 0.0 | 76.92727144 | 255.0 | 48.50581256 | 1.20979977E+00 | 42 | 72 |
344+
| :band-1 | :uint8 | 29989001 | 0 | 0.0 | 76.92790493 | 255.0 | 48.50363602 | 1.20979051E+00 | 33 | 73 |
345+
| :band-2 | :uint8 | 29989001 | 0 | 0.0 | 76.92823122 | 255.0 | 48.50667146 | 1.20996005E+00 | 38 | 71 |
310346

311347

312348
:::
@@ -349,7 +385,6 @@ a multispectral image.
349385
::: {.sourceClojure}
350386
```clojure
351387
(require '[tech.v3.dataset :as ds]
352-
'[tech.v3.datatype :as dtype]
353388
'[tech.v3.datatype.statistics :as dstats])
354389
```
355390
:::
@@ -406,35 +441,22 @@ java.awt.Color
406441
::: {.sourceClojure}
407442
```clojure
408443
(defn dataset->image
409-
[dataset {:keys [r g b]}]
410-
(let [xs (ds/column dataset :x)
411-
ys (ds/column dataset :y)
412-
rs (ds/column dataset r)
413-
gs (ds/column dataset g)
414-
bs (ds/column dataset b)
415-
416-
max-x (-> xs last int)
417-
min-x (-> xs first int)
418-
max-y (-> ys last int)
419-
min-y (-> ys first int)
420-
421-
width (inc (- max-x min-x))
422-
height (inc (- max-y min-y))
423-
424-
;; Create the image
425-
img (BufferedImage. width height BufferedImage/TYPE_INT_RGB)]
426-
427-
;; Set pixels
428-
(dotimes [i (ds/row-count dataset)]
429-
(let [x (- (int (dtype/get-value xs i)) min-x)
430-
y (- (int (dtype/get-value ys i)) min-y)
431-
r (int (dtype/get-value rs i))
432-
g (int (dtype/get-value gs i))
433-
b (int (dtype/get-value bs i))
434-
color (Color. r g b)]
435-
(.setRGB img x y (.getRGB color))))
436-
437-
img))
444+
[dataset {:as columns-mapping
445+
:keys [r g b]}]
446+
(let [height (->> dataset
447+
:y
448+
((juxt dstats/max dstats/min))
449+
(apply -))
450+
width (->> dataset
451+
:x
452+
((juxt dstats/max dstats/min))
453+
(apply -))]
454+
(-> columns-mapping
455+
(update-vals dataset)
456+
tc/dataset
457+
ds-tensor/dataset->tensor
458+
(tensor/reshape [height width 3])
459+
bufimg/tensor->image)))
438460
```
439461
:::
440462

@@ -472,8 +494,10 @@ such a way that makes it not straightforward
472494
to read using only built-in Java libraries. This metadata contains useful information
473495
such as coordinate system, location, number of channels and so on.
474496
If we want to use it, we have a number of options:
497+
475498
- Roll your own GeoTIFF metadata reader using `javax.imageio.metadata` and use
476499
field accessors and walk the metadata tree to find the information that you want.
500+
477501
- Apache SIS
478502
- GeoTools
479503
- Python Interop (with Rasterio)
@@ -488,6 +512,7 @@ GIS operations such as getting raster values at specific geographic
488512
coordinates.
489513
Here are [developer docs](https://sis.apache.org/book/en/developer-guide.html) for Apache SIS.
490514
It's worth a scan to be aware of some terminology used in this library, for example:
515+
491516
- [Coverage](https://sis.apache.org/book/en/developer-guide.html#Coverage): a function that returns an attribute value for a given coordinate
492517
(this includes raster images).
493518

@@ -545,7 +570,7 @@ We can view the metadata in this interesting output format that SIS provides.
545570
::: {.callout-note}
546571
## OUT
547572
```
548-
#object[org.apache.sis.metadata.iso.DefaultMetadata 0x4af9f150 Metadata
573+
#object[org.apache.sis.metadata.iso.DefaultMetadata 0x78245dd0 Metadata
549574
  ├─Metadata identifier……………………………………………………………… example_geotiff_medium
550575
  ├─Metadata standard (1 of 2)…………………………………………… Geographic Information — Metadata Part 1: Fundamentals
551576
  │   ├─Edition…………………………………………………………………………………… ISO 19115-1:2014
@@ -817,7 +842,7 @@ View COG dimensions without downloading the full image
817842
{:x [499980.0 609780.0],
818843
:y [1790220.0 1900020.0],
819844
:crs
820-
#object[org.apache.sis.referencing.crs.DefaultProjectedCRS 0x70bbb286 "ProjectedCRS[\"WGS 84 / UTM zone 36N\",\n BaseGeogCRS[\"WGS 84\",\n Datum[\"World Geodetic System 1984\",\n Ellipsoid[\"WGS 1984\", 6378137.0, 298.257223563]],\n Unit[\"degree\", 0.017453292519943295]],\n Conversion[\"UTM zone 36N\",\n Method[\"Transverse Mercator\"],\n Parameter[\"Latitude of natural origin\", 0.0],\n Parameter[\"Longitude of natural origin\", 33.0],\n Parameter[\"Scale factor at natural origin\", 0.9996],\n Parameter[\"False easting\", 500000.0],\n Parameter[\"False northing\", 0.0]],\n CS[Cartesian, 2],\n Axis[\"Easting (E)\", east],\n Axis[\"Northing (N)\", north],\n Unit[\"metre\", 1],\n Id[\"EPSG\", 32636, \"9.9.1\", Citation[\"Subset of EPSG\"], URI[\"urn:ogc:def:crs:EPSG:9.9.1:32636\"]],\n Remark[\"Definitions from public sources. When a definition corresponds to an EPSG object (ignoring metadata), the EPSG code is provided as a reference where to find the complete definition.\"]]"]}
845+
#object[org.apache.sis.referencing.crs.DefaultProjectedCRS 0x37992194 "ProjectedCRS[\"WGS 84 / UTM zone 36N\",\n BaseGeogCRS[\"WGS 84\",\n Datum[\"World Geodetic System 1984\",\n Ellipsoid[\"WGS 1984\", 6378137.0, 298.257223563]],\n Unit[\"degree\", 0.017453292519943295]],\n Conversion[\"UTM zone 36N\",\n Method[\"Transverse Mercator\"],\n Parameter[\"Latitude of natural origin\", 0.0],\n Parameter[\"Longitude of natural origin\", 33.0],\n Parameter[\"Scale factor at natural origin\", 0.9996],\n Parameter[\"False easting\", 500000.0],\n Parameter[\"False northing\", 0.0]],\n CS[Cartesian, 2],\n Axis[\"Easting (E)\", east],\n Axis[\"Northing (N)\", north],\n Unit[\"metre\", 1],\n Id[\"EPSG\", 32636, \"9.9.1\", Citation[\"Subset of EPSG\"], URI[\"urn:ogc:def:crs:EPSG:9.9.1:32636\"]],\n Remark[\"Definitions from public sources. When a definition corresponds to an EPSG object (ignoring metadata), the EPSG code is provided as a reference where to find the complete definition.\"]]"]}
821846

822847
```
823848
:::
@@ -975,4 +1000,4 @@ Pull requests are welcome. Please refer to `src/gis/geotiff.clj` at
9751000

9761001
```{=html}
9771002
<div><pre><small><small>source: <a href="https://github.com/ClojureCivitas/clojurecivitas.github.io/blob/main/src/gis/geotiff.clj">src/gis/geotiff.clj</a></small></small></pre></div>
978-
```
1003+
```

0 commit comments

Comments
 (0)