geoai 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -3
- package/README.md +47 -3
- package/geoai.js +590 -137
- package/index.d.ts +118 -3
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.7] - 2026-07-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- OpenAerialMap (OAM) map provider — HOT Imagery STAC + TiTiler (`provider: "oam"`)
|
|
14
|
+
- `examples/07-oam-quickstart` — Rome orthophoto draw-to-detect demo
|
|
15
|
+
- OAM option in `live-examples-nextjs` (flies to Rome; mosaic tiles)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Docs Vercel prebuild: ship `remark` deps with docs and run `generate-llms-txt` from `docs/scripts/` so module resolution works when the Vercel root is `docs/`
|
|
19
|
+
|
|
20
|
+
## [1.0.6] - 2026-07-16
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- ChangeStar ViT-B building footprint model (`geobase/changestar-building-segmentation-vitb`)
|
|
24
|
+
- Building segmentation factory to route Hub `modelId` to the correct runner
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `building-footprint-segmentation` now defaults to ChangeStar; pass
|
|
28
|
+
`modelId: "geobase/building-footprint-segmentation"` for the lighter model
|
|
29
|
+
|
|
10
30
|
## [1.0.5] - 2026-07-15
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -16,9 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
36
|
- WMS documentation (`wms.mdx`), map-providers table update, and 17 WMS tests (including pipeline inference)
|
|
17
37
|
- Project roadmap (`_docs4devs/ROADMAP.md`) for use-case guides and quantization pipeline backlog
|
|
18
38
|
|
|
19
|
-
### Changed
|
|
20
|
-
- `live-examples-nextjs` pins `geoai` to local `build/` for WMS/TMS provider development
|
|
21
|
-
|
|
22
39
|
## [1.0.4] - 2026-07-11
|
|
23
40
|
|
|
24
41
|
### Added
|
package/README.md
CHANGED
|
@@ -40,12 +40,12 @@ You can also use GeoAI directly in the browser via CDN:
|
|
|
40
40
|
|
|
41
41
|
#### Unpkg CDN
|
|
42
42
|
```html
|
|
43
|
-
<script src="https://unpkg.com/geoai@1.0.
|
|
43
|
+
<script src="https://unpkg.com/geoai@1.0.7/geoai.js"></script>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
#### jsDelivr CDN
|
|
47
47
|
```html
|
|
48
|
-
<script src="https://cdn.jsdelivr.net/npm/geoai@1.0.
|
|
48
|
+
<script src="https://cdn.jsdelivr.net/npm/geoai@1.0.7/geoai.min.js"></script>
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
When using CDN, the library is available globally as `geoai`.
|
|
@@ -186,6 +186,17 @@ const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
|
186
186
|
});
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
+
### OpenAerialMap (OAM)
|
|
190
|
+
Community aerial imagery via HOT Imagery STAC + TiTiler — no API key.
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
194
|
+
provider: "oam",
|
|
195
|
+
// itemId: "67826781a07cc20001818cdb", // optional pin
|
|
196
|
+
// mosaic: true, // optional: collection mosaic only
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
189
200
|
### ESRI
|
|
190
201
|
ESRI World Imagery - no API key required.
|
|
191
202
|
|
|
@@ -231,6 +242,39 @@ const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
|
|
|
231
242
|
- [Slides (PDF)](https://geobase-docs.s3.amazonaws.com/geobase-ai-assets/talks/lightning-talk-webgpu-geoai.pdf)
|
|
232
243
|
- [Slide source (Marp)](talks/lightning-talk-webgpu-geoai.md)
|
|
233
244
|
|
|
245
|
+
## Agents & LLMs
|
|
246
|
+
|
|
247
|
+
### Install the GeoAI agent skill
|
|
248
|
+
|
|
249
|
+
Use the [Skills CLI](https://skills.sh/) so coding agents (Cursor, Claude Code, Codex, etc.) load GeoAI.js integration guidance automatically:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
# Install into the current project (recommended)
|
|
253
|
+
npx skills add decision-labs/geoai.js --skill geoai -y
|
|
254
|
+
|
|
255
|
+
# Or install globally for all projects
|
|
256
|
+
npx skills add decision-labs/geoai.js --skill geoai -g -y
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
List what you have installed:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npx skills list
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Use the skill once without installing (pipes a prompt into an agent):
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
npx skills use decision-labs/geoai.js@geoai
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Source: [`skills/geoai`](skills/geoai) — pipeline API, task picker, providers, and Web Worker patterns for background inference.
|
|
272
|
+
|
|
273
|
+
### LLM-readable docs
|
|
274
|
+
|
|
275
|
+
- **Index**: [docs.geobase.app/geoai/llms.txt](https://docs.geobase.app/geoai/llms.txt)
|
|
276
|
+
- **Full corpus**: [docs.geobase.app/geoai/llms-full.txt](https://docs.geobase.app/geoai/llms-full.txt) — regenerate with `pnpm docs:llms`
|
|
277
|
+
|
|
234
278
|
## Links
|
|
235
279
|
|
|
236
280
|
- **Documentation**: [docs.geobase.app/geoai](https://docs.geobase.app/geoai) - Comprehensive documentation, examples, and API reference
|
|
@@ -248,4 +292,4 @@ We welcome contributions! Please see our [contributing guide](CONTRIBUTING.md) f
|
|
|
248
292
|
|
|
249
293
|
MIT License - see [LICENSE.md](LICENSE.md) for details.
|
|
250
294
|
|
|
251
|
-
[//]: <> (Toggle CI
|
|
295
|
+
[//]: <> (Toggle CI off)
|
package/geoai.js
CHANGED
|
@@ -390,7 +390,7 @@ const postProcessYoloOutput = (model_outputs, pixel_values, rawImage, id2label)
|
|
|
390
390
|
return model_output;
|
|
391
391
|
};
|
|
392
392
|
const parametersChanged = (instance, model_id, providerParams, modelParams) => {
|
|
393
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
393
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
|
|
394
394
|
if (instance.model_id !== model_id) {
|
|
395
395
|
return true;
|
|
396
396
|
}
|
|
@@ -425,6 +425,11 @@ const parametersChanged = (instance, model_id, providerParams, modelParams) => {
|
|
|
425
425
|
return true;
|
|
426
426
|
}
|
|
427
427
|
break;
|
|
428
|
+
case "oam":
|
|
429
|
+
if (((_m = instance.providerParams) == null ? void 0 : _m.stacUrl) !== (providerParams == null ? void 0 : providerParams.stacUrl) || ((_n = instance.providerParams) == null ? void 0 : _n.rasterUrl) !== (providerParams == null ? void 0 : providerParams.rasterUrl) || ((_o = instance.providerParams) == null ? void 0 : _o.collection) !== (providerParams == null ? void 0 : providerParams.collection) || ((_p = instance.providerParams) == null ? void 0 : _p.asset) !== (providerParams == null ? void 0 : providerParams.asset) || ((_q = instance.providerParams) == null ? void 0 : _q.itemId) !== (providerParams == null ? void 0 : providerParams.itemId) || ((_r = instance.providerParams) == null ? void 0 : _r.mosaic) !== (providerParams == null ? void 0 : providerParams.mosaic)) {
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
428
433
|
}
|
|
429
434
|
if (modelParams) {
|
|
430
435
|
const instanceModelParams = instance.modelParams || {};
|
|
@@ -1380,6 +1385,175 @@ class Wms extends MapSource {
|
|
|
1380
1385
|
);
|
|
1381
1386
|
}
|
|
1382
1387
|
}
|
|
1388
|
+
const DEFAULT_OAM_STAC_URL = "https://api.imagery.hotosm.org/stac";
|
|
1389
|
+
const DEFAULT_OAM_RASTER_URL = "https://api.imagery.hotosm.org/raster";
|
|
1390
|
+
const DEFAULT_OAM_COLLECTION = "openaerialmap";
|
|
1391
|
+
const DEFAULT_OAM_ASSET = "visual";
|
|
1392
|
+
function buildOamMosaicTileUrlTemplate(rasterUrl = DEFAULT_OAM_RASTER_URL, collection = DEFAULT_OAM_COLLECTION, asset = DEFAULT_OAM_ASSET) {
|
|
1393
|
+
const base = rasterUrl.replace(/\/$/, "");
|
|
1394
|
+
return `${base}/collections/${encodeURIComponent(collection)}/tiles/WebMercatorQuad/{z}/{x}/{y}?assets=${encodeURIComponent(asset)}`;
|
|
1395
|
+
}
|
|
1396
|
+
function buildOamItemTileUrlTemplate(itemId, rasterUrl = DEFAULT_OAM_RASTER_URL, collection = DEFAULT_OAM_COLLECTION, asset = DEFAULT_OAM_ASSET) {
|
|
1397
|
+
const base = rasterUrl.replace(/\/$/, "");
|
|
1398
|
+
return `${base}/collections/${encodeURIComponent(collection)}/items/${encodeURIComponent(itemId)}/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?assets=${encodeURIComponent(asset)}`;
|
|
1399
|
+
}
|
|
1400
|
+
function pickBestOamItem(features) {
|
|
1401
|
+
if (!features.length) return void 0;
|
|
1402
|
+
return features.slice().sort((a, b) => {
|
|
1403
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1404
|
+
const gsdA = typeof ((_a = a.properties) == null ? void 0 : _a.gsd) === "number" ? a.properties.gsd : Number.POSITIVE_INFINITY;
|
|
1405
|
+
const gsdB = typeof ((_b = b.properties) == null ? void 0 : _b.gsd) === "number" ? b.properties.gsd : Number.POSITIVE_INFINITY;
|
|
1406
|
+
if (gsdA !== gsdB) return gsdA - gsdB;
|
|
1407
|
+
const dateA = Date.parse(
|
|
1408
|
+
String(((_c = a.properties) == null ? void 0 : _c.created) || ((_d = a.properties) == null ? void 0 : _d.datetime) || 0)
|
|
1409
|
+
);
|
|
1410
|
+
const dateB = Date.parse(
|
|
1411
|
+
String(((_e = b.properties) == null ? void 0 : _e.created) || ((_f = b.properties) == null ? void 0 : _f.datetime) || 0)
|
|
1412
|
+
);
|
|
1413
|
+
return dateB - dateA;
|
|
1414
|
+
})[0];
|
|
1415
|
+
}
|
|
1416
|
+
async function searchOamItems(bbox2, options = {}) {
|
|
1417
|
+
const stacUrl = (options.stacUrl || DEFAULT_OAM_STAC_URL).replace(/\/$/, "");
|
|
1418
|
+
const collection = options.collection || DEFAULT_OAM_COLLECTION;
|
|
1419
|
+
const limit = options.limit ?? 10;
|
|
1420
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
1421
|
+
const response = await fetchImpl(`${stacUrl}/search`, {
|
|
1422
|
+
method: "POST",
|
|
1423
|
+
headers: {
|
|
1424
|
+
"Content-Type": "application/json",
|
|
1425
|
+
Accept: "application/geo+json",
|
|
1426
|
+
...options.headers || {}
|
|
1427
|
+
},
|
|
1428
|
+
body: JSON.stringify({
|
|
1429
|
+
collections: [collection],
|
|
1430
|
+
bbox: bbox2,
|
|
1431
|
+
limit
|
|
1432
|
+
})
|
|
1433
|
+
});
|
|
1434
|
+
if (!response.ok) {
|
|
1435
|
+
const body = await response.text().catch(() => "");
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
`OAM STAC search failed (${response.status}): ${body || response.statusText}`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
const data = await response.json();
|
|
1441
|
+
return data.features || [];
|
|
1442
|
+
}
|
|
1443
|
+
class Oam extends MapSource {
|
|
1444
|
+
constructor(config = {}) {
|
|
1445
|
+
super();
|
|
1446
|
+
__publicField(this, "stacUrl");
|
|
1447
|
+
__publicField(this, "rasterUrl");
|
|
1448
|
+
__publicField(this, "collection");
|
|
1449
|
+
__publicField(this, "asset");
|
|
1450
|
+
__publicField(this, "itemId");
|
|
1451
|
+
__publicField(this, "mosaic");
|
|
1452
|
+
__publicField(this, "attribution");
|
|
1453
|
+
__publicField(this, "tileSize");
|
|
1454
|
+
__publicField(this, "headers");
|
|
1455
|
+
/** Resolved XYZ template with `{z}/{x}/{y}` placeholders. */
|
|
1456
|
+
__publicField(this, "tileUrlTemplate");
|
|
1457
|
+
__publicField(this, "resolvedForBboxKey");
|
|
1458
|
+
this.stacUrl = (config.stacUrl || DEFAULT_OAM_STAC_URL).replace(/\/$/, "");
|
|
1459
|
+
this.rasterUrl = (config.rasterUrl || DEFAULT_OAM_RASTER_URL).replace(
|
|
1460
|
+
/\/$/,
|
|
1461
|
+
""
|
|
1462
|
+
);
|
|
1463
|
+
this.collection = config.collection || DEFAULT_OAM_COLLECTION;
|
|
1464
|
+
this.asset = config.asset || DEFAULT_OAM_ASSET;
|
|
1465
|
+
this.itemId = config.itemId;
|
|
1466
|
+
this.mosaic = config.mosaic === true;
|
|
1467
|
+
this.attribution = config.attribution || "OpenAerialMap / HOT Imagery";
|
|
1468
|
+
this.tileSize = config.tileSize || 256;
|
|
1469
|
+
this.headers = config.headers;
|
|
1470
|
+
if (this.itemId) {
|
|
1471
|
+
this.tileUrlTemplate = buildOamItemTileUrlTemplate(
|
|
1472
|
+
this.itemId,
|
|
1473
|
+
this.rasterUrl,
|
|
1474
|
+
this.collection,
|
|
1475
|
+
this.asset
|
|
1476
|
+
);
|
|
1477
|
+
} else if (this.mosaic) {
|
|
1478
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1479
|
+
this.rasterUrl,
|
|
1480
|
+
this.collection,
|
|
1481
|
+
this.asset
|
|
1482
|
+
);
|
|
1483
|
+
} else {
|
|
1484
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1485
|
+
this.rasterUrl,
|
|
1486
|
+
this.collection,
|
|
1487
|
+
this.asset
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Resolve tile URL template for an AOI via STAC (unless mosaic/itemId pin).
|
|
1493
|
+
*/
|
|
1494
|
+
async resolveTileTemplate(polygon2) {
|
|
1495
|
+
if (this.itemId) {
|
|
1496
|
+
this.tileUrlTemplate = buildOamItemTileUrlTemplate(
|
|
1497
|
+
this.itemId,
|
|
1498
|
+
this.rasterUrl,
|
|
1499
|
+
this.collection,
|
|
1500
|
+
this.asset
|
|
1501
|
+
);
|
|
1502
|
+
return this.tileUrlTemplate;
|
|
1503
|
+
}
|
|
1504
|
+
if (this.mosaic) {
|
|
1505
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1506
|
+
this.rasterUrl,
|
|
1507
|
+
this.collection,
|
|
1508
|
+
this.asset
|
|
1509
|
+
);
|
|
1510
|
+
return this.tileUrlTemplate;
|
|
1511
|
+
}
|
|
1512
|
+
const bbox$1 = bbox(polygon2);
|
|
1513
|
+
const bboxKey = bbox$1.map((n) => n.toFixed(6)).join(",");
|
|
1514
|
+
if (this.resolvedForBboxKey === bboxKey && this.tileUrlTemplate) {
|
|
1515
|
+
return this.tileUrlTemplate;
|
|
1516
|
+
}
|
|
1517
|
+
const features = await searchOamItems(bbox$1, {
|
|
1518
|
+
stacUrl: this.stacUrl,
|
|
1519
|
+
collection: this.collection,
|
|
1520
|
+
headers: this.headers
|
|
1521
|
+
});
|
|
1522
|
+
const best = pickBestOamItem(features);
|
|
1523
|
+
if (best == null ? void 0 : best.id) {
|
|
1524
|
+
this.itemId = best.id;
|
|
1525
|
+
this.tileUrlTemplate = buildOamItemTileUrlTemplate(
|
|
1526
|
+
best.id,
|
|
1527
|
+
this.rasterUrl,
|
|
1528
|
+
this.collection,
|
|
1529
|
+
this.asset
|
|
1530
|
+
);
|
|
1531
|
+
} else {
|
|
1532
|
+
this.tileUrlTemplate = buildOamMosaicTileUrlTemplate(
|
|
1533
|
+
this.rasterUrl,
|
|
1534
|
+
this.collection,
|
|
1535
|
+
this.asset
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
this.resolvedForBboxKey = bboxKey;
|
|
1539
|
+
return this.tileUrlTemplate;
|
|
1540
|
+
}
|
|
1541
|
+
async getImage(polygon2, bands, expression, zoomLevel, requiresSquare = false, stitch = true) {
|
|
1542
|
+
await this.resolveTileTemplate(polygon2);
|
|
1543
|
+
return super.getImage(
|
|
1544
|
+
polygon2,
|
|
1545
|
+
bands,
|
|
1546
|
+
expression,
|
|
1547
|
+
zoomLevel,
|
|
1548
|
+
requiresSquare,
|
|
1549
|
+
stitch
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
getTileUrlFromTileCoords(tileCoords, instance) {
|
|
1553
|
+
const [x, y, z] = tileCoords;
|
|
1554
|
+
return instance.tileUrlTemplate.replaceAll("{z}", z.toString()).replaceAll("{x}", x.toString()).replaceAll("{y}", y.toString());
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1383
1557
|
class BaseModel {
|
|
1384
1558
|
constructor(model_id, providerParams, modelParams) {
|
|
1385
1559
|
__publicField(this, "providerParams");
|
|
@@ -1449,6 +1623,19 @@ class BaseModel {
|
|
|
1449
1623
|
extraParams: this.providerParams.extraParams
|
|
1450
1624
|
});
|
|
1451
1625
|
break;
|
|
1626
|
+
case "oam":
|
|
1627
|
+
this.dataProvider = new Oam({
|
|
1628
|
+
stacUrl: this.providerParams.stacUrl,
|
|
1629
|
+
rasterUrl: this.providerParams.rasterUrl,
|
|
1630
|
+
collection: this.providerParams.collection,
|
|
1631
|
+
asset: this.providerParams.asset,
|
|
1632
|
+
itemId: this.providerParams.itemId,
|
|
1633
|
+
mosaic: this.providerParams.mosaic,
|
|
1634
|
+
attribution: this.providerParams.attribution,
|
|
1635
|
+
tileSize: this.providerParams.tileSize,
|
|
1636
|
+
headers: this.providerParams.headers
|
|
1637
|
+
});
|
|
1638
|
+
break;
|
|
1452
1639
|
case "sentinel":
|
|
1453
1640
|
throw new Error("Sentinel provider not implemented yet");
|
|
1454
1641
|
default:
|
|
@@ -3109,6 +3296,377 @@ const _OilStorageTankDetection = class _OilStorageTankDetection extends BaseMode
|
|
|
3109
3296
|
};
|
|
3110
3297
|
__publicField(_OilStorageTankDetection, "instance", null);
|
|
3111
3298
|
let OilStorageTankDetection = _OilStorageTankDetection;
|
|
3299
|
+
class BaseDenseSegmentationModel extends BaseModel {
|
|
3300
|
+
constructor(model_id, providerParams, modelParams) {
|
|
3301
|
+
super(model_id, providerParams, modelParams);
|
|
3302
|
+
__publicField(this, "model");
|
|
3303
|
+
__publicField(this, "processor");
|
|
3304
|
+
/** Tile edge length in pixels. */
|
|
3305
|
+
__publicField(this, "tileSize", 1024);
|
|
3306
|
+
/** Overlap between adjacent tiles for feather blending. */
|
|
3307
|
+
__publicField(this, "tileOverlap", 64);
|
|
3308
|
+
/** Default probability threshold for binarization. */
|
|
3309
|
+
__publicField(this, "defaultThreshold", 0.5);
|
|
3310
|
+
}
|
|
3311
|
+
async initializeModel() {
|
|
3312
|
+
if (this.model) return;
|
|
3313
|
+
this.processor = await ImageProcessor.from_pretrained(this.model_id);
|
|
3314
|
+
const pretrainedModel = await PreTrainedModel.from_pretrained(
|
|
3315
|
+
this.model_id,
|
|
3316
|
+
this.modelParams
|
|
3317
|
+
);
|
|
3318
|
+
this.model = pretrainedModel.sessions.model;
|
|
3319
|
+
}
|
|
3320
|
+
resolveProbabilityOutput(outputs) {
|
|
3321
|
+
const tensor = outputs[this.outputName];
|
|
3322
|
+
if (!tensor) {
|
|
3323
|
+
throw new Error(
|
|
3324
|
+
`Model output "${this.outputName}" not found; got [${Object.keys(outputs).join(", ")}]`
|
|
3325
|
+
);
|
|
3326
|
+
}
|
|
3327
|
+
const dims = tensor.dims;
|
|
3328
|
+
const raw = tensor.data ?? tensor.cpuData;
|
|
3329
|
+
return {
|
|
3330
|
+
type: tensor.type,
|
|
3331
|
+
dims,
|
|
3332
|
+
size: tensor.size,
|
|
3333
|
+
data: raw
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
featherWeight(size, overlap) {
|
|
3337
|
+
const ramp = new Float32Array(size);
|
|
3338
|
+
ramp.fill(1);
|
|
3339
|
+
if (overlap > 0) {
|
|
3340
|
+
for (let i = 0; i < overlap; i++) {
|
|
3341
|
+
const t = i / (overlap - 1 || 1);
|
|
3342
|
+
ramp[i] = t;
|
|
3343
|
+
ramp[size - 1 - i] = t;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
const weights = new Float32Array(size * size);
|
|
3347
|
+
for (let y = 0; y < size; y++) {
|
|
3348
|
+
for (let x = 0; x < size; x++) {
|
|
3349
|
+
weights[y * size + x] = ramp[y] * ramp[x];
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
return weights;
|
|
3353
|
+
}
|
|
3354
|
+
async runTile(tileImage) {
|
|
3355
|
+
if (!this.processor || !this.model) {
|
|
3356
|
+
throw new Error("Model or processor not initialized");
|
|
3357
|
+
}
|
|
3358
|
+
const inputs = await this.processor(tileImage);
|
|
3359
|
+
const pixelValues = inputs.pixel_values;
|
|
3360
|
+
const dataCopy = pixelValues.data instanceof Float32Array ? new Float32Array(pixelValues.data) : Float32Array.from(pixelValues.data);
|
|
3361
|
+
const feed = new Tensor("float32", dataCopy, pixelValues.dims);
|
|
3362
|
+
let outputs;
|
|
3363
|
+
try {
|
|
3364
|
+
outputs = await this.model.run({
|
|
3365
|
+
[this.inputName]: feed
|
|
3366
|
+
});
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
const detail = err instanceof Error ? err.message : typeof err === "number" ? `ONNX Runtime abort (${err}) — often WASM out-of-memory for this ${this.tileSize}px fp32 model. Try WebGPU (Chrome/Edge) or a smaller AOI.` : String(err);
|
|
3369
|
+
throw new Error(`Dense segmentation tile inference failed: ${detail}`);
|
|
3370
|
+
}
|
|
3371
|
+
const { data } = this.resolveProbabilityOutput(outputs);
|
|
3372
|
+
return {
|
|
3373
|
+
data,
|
|
3374
|
+
height: this.tileSize,
|
|
3375
|
+
width: this.tileSize
|
|
3376
|
+
};
|
|
3377
|
+
}
|
|
3378
|
+
/**
|
|
3379
|
+
* Overlapped tiled inference with feather blending, matching the
|
|
3380
|
+
* ChangeStar pipeline (tile_size / overlap / threshold).
|
|
3381
|
+
*/
|
|
3382
|
+
async inferDenseMask(geoRawImage, threshold) {
|
|
3383
|
+
const { height, width } = geoRawImage;
|
|
3384
|
+
const tileSize = this.tileSize;
|
|
3385
|
+
const overlap = Math.min(this.tileOverlap, tileSize - 1);
|
|
3386
|
+
const stride = tileSize - overlap;
|
|
3387
|
+
const feather = this.featherWeight(tileSize, overlap);
|
|
3388
|
+
const accum = new Float32Array(height * width);
|
|
3389
|
+
const weightSum = new Float32Array(height * width);
|
|
3390
|
+
for (let rowOff = 0; rowOff < height; rowOff += stride) {
|
|
3391
|
+
for (let colOff = 0; colOff < width; colOff += stride) {
|
|
3392
|
+
const winH = Math.min(tileSize, height - rowOff);
|
|
3393
|
+
const winW = Math.min(tileSize, width - colOff);
|
|
3394
|
+
const croppedRaw = await geoRawImage.crop([
|
|
3395
|
+
colOff,
|
|
3396
|
+
rowOff,
|
|
3397
|
+
colOff + winW - 1,
|
|
3398
|
+
rowOff + winH - 1
|
|
3399
|
+
]);
|
|
3400
|
+
const padH = tileSize - winH;
|
|
3401
|
+
const padW = tileSize - winW;
|
|
3402
|
+
const paddedRaw = padH > 0 || padW > 0 ? await croppedRaw.pad([0, padW, 0, padH]) : croppedRaw;
|
|
3403
|
+
const tileImage = GeoRawImage.fromRawImage(
|
|
3404
|
+
paddedRaw,
|
|
3405
|
+
geoRawImage.getBounds(),
|
|
3406
|
+
geoRawImage.getCRS()
|
|
3407
|
+
);
|
|
3408
|
+
const { data: prob } = await this.runTile(tileImage);
|
|
3409
|
+
for (let y = 0; y < winH; y++) {
|
|
3410
|
+
for (let x = 0; x < winW; x++) {
|
|
3411
|
+
const srcIdx = y * tileSize + x;
|
|
3412
|
+
const dstIdx = (rowOff + y) * width + (colOff + x);
|
|
3413
|
+
const w = feather[y * tileSize + x];
|
|
3414
|
+
accum[dstIdx] += prob[srcIdx] * w;
|
|
3415
|
+
weightSum[dstIdx] += w;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
const binary = new Uint8Array(height * width);
|
|
3421
|
+
for (let i = 0; i < height * width; i++) {
|
|
3422
|
+
const w = weightSum[i] === 0 ? 1 : weightSum[i];
|
|
3423
|
+
binary[i] = accum[i] / w > threshold ? 255 : 0;
|
|
3424
|
+
}
|
|
3425
|
+
const scale = height > 512 || width > 512 ? 2 : 1;
|
|
3426
|
+
const contourH = Math.max(1, Math.floor(height / scale));
|
|
3427
|
+
const contourW = Math.max(1, Math.floor(width / scale));
|
|
3428
|
+
let contourBinary = binary;
|
|
3429
|
+
let contourImage = geoRawImage;
|
|
3430
|
+
if (scale > 1) {
|
|
3431
|
+
contourBinary = new Uint8Array(contourH * contourW);
|
|
3432
|
+
for (let y = 0; y < contourH; y++) {
|
|
3433
|
+
for (let x = 0; x < contourW; x++) {
|
|
3434
|
+
contourBinary[y * contourW + x] = binary[y * scale * width + x * scale];
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
contourImage = new GeoRawImage(
|
|
3438
|
+
contourBinary,
|
|
3439
|
+
contourW,
|
|
3440
|
+
contourH,
|
|
3441
|
+
1,
|
|
3442
|
+
geoRawImage.getBounds(),
|
|
3443
|
+
geoRawImage.getCRS()
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
const maskTensor = new Tensor("uint8", contourBinary, [
|
|
3447
|
+
1,
|
|
3448
|
+
1,
|
|
3449
|
+
contourH,
|
|
3450
|
+
contourW
|
|
3451
|
+
]);
|
|
3452
|
+
const masksToFC = maskToGeoJSON({ mask: [maskTensor] }, contourImage);
|
|
3453
|
+
const features = [];
|
|
3454
|
+
masksToFC.forEach((fc) => {
|
|
3455
|
+
fc.features.forEach((feature2) => {
|
|
3456
|
+
feature2.properties = {
|
|
3457
|
+
...feature2.properties || {},
|
|
3458
|
+
class: "building"
|
|
3459
|
+
};
|
|
3460
|
+
features.push(feature2);
|
|
3461
|
+
});
|
|
3462
|
+
});
|
|
3463
|
+
return {
|
|
3464
|
+
type: "FeatureCollection",
|
|
3465
|
+
features
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
async inference(params) {
|
|
3469
|
+
const {
|
|
3470
|
+
inputs: { polygon: polygon2 },
|
|
3471
|
+
postProcessingParams: { confidenceThreshold } = {},
|
|
3472
|
+
mapSourceParams
|
|
3473
|
+
} = params;
|
|
3474
|
+
if (!polygon2) {
|
|
3475
|
+
throw new Error("Polygon input is required for dense segmentation");
|
|
3476
|
+
}
|
|
3477
|
+
if (!polygon2.geometry || polygon2.geometry.type !== "Polygon") {
|
|
3478
|
+
throw new Error("Input must be a valid GeoJSON Polygon feature");
|
|
3479
|
+
}
|
|
3480
|
+
if (!this.initialized) {
|
|
3481
|
+
await this.initialize();
|
|
3482
|
+
}
|
|
3483
|
+
if (!this.model || !this.processor || !this.dataProvider) {
|
|
3484
|
+
throw new Error("Model or data provider not initialized");
|
|
3485
|
+
}
|
|
3486
|
+
const geoRawImage = await this.polygonToImage(
|
|
3487
|
+
polygon2,
|
|
3488
|
+
mapSourceParams == null ? void 0 : mapSourceParams.zoomLevel,
|
|
3489
|
+
mapSourceParams == null ? void 0 : mapSourceParams.bands,
|
|
3490
|
+
mapSourceParams == null ? void 0 : mapSourceParams.expression
|
|
3491
|
+
);
|
|
3492
|
+
const threshold = typeof confidenceThreshold === "number" ? confidenceThreshold : this.defaultThreshold;
|
|
3493
|
+
const taskName = this.model_id.split("/").pop() ?? "dense-segmentation";
|
|
3494
|
+
const inferenceStartTime = performance.now();
|
|
3495
|
+
console.log(`[${taskName}] starting dense segmentation inference...`);
|
|
3496
|
+
const detections = await this.inferDenseMask(geoRawImage, threshold);
|
|
3497
|
+
const inferenceEndTime = performance.now();
|
|
3498
|
+
console.log(
|
|
3499
|
+
`[${taskName}] inference completed. Time taken: ${(inferenceEndTime - inferenceStartTime).toFixed(2)}ms`
|
|
3500
|
+
);
|
|
3501
|
+
return {
|
|
3502
|
+
detections,
|
|
3503
|
+
geoRawImage
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
const _ChangeStarBuildingSegmentation = class _ChangeStarBuildingSegmentation extends BaseDenseSegmentationModel {
|
|
3508
|
+
constructor(model_id, providerParams, modelParams) {
|
|
3509
|
+
super(model_id, providerParams, modelParams);
|
|
3510
|
+
__publicField(this, "inputName", "image");
|
|
3511
|
+
__publicField(this, "outputName", "building_prob");
|
|
3512
|
+
__publicField(this, "tileSize", 1024);
|
|
3513
|
+
__publicField(this, "tileOverlap", 64);
|
|
3514
|
+
__publicField(this, "defaultThreshold", 0.5);
|
|
3515
|
+
}
|
|
3516
|
+
static async getInstance(model_id, providerParams, modelParams) {
|
|
3517
|
+
if (!_ChangeStarBuildingSegmentation.instanceRef || parametersChanged(
|
|
3518
|
+
_ChangeStarBuildingSegmentation.instanceRef,
|
|
3519
|
+
model_id,
|
|
3520
|
+
providerParams,
|
|
3521
|
+
modelParams
|
|
3522
|
+
)) {
|
|
3523
|
+
_ChangeStarBuildingSegmentation.instanceRef = new _ChangeStarBuildingSegmentation(
|
|
3524
|
+
model_id,
|
|
3525
|
+
providerParams,
|
|
3526
|
+
modelParams
|
|
3527
|
+
);
|
|
3528
|
+
await _ChangeStarBuildingSegmentation.instanceRef.initialize();
|
|
3529
|
+
}
|
|
3530
|
+
return { instance: _ChangeStarBuildingSegmentation.instanceRef };
|
|
3531
|
+
}
|
|
3532
|
+
};
|
|
3533
|
+
__publicField(_ChangeStarBuildingSegmentation, "instanceRef", null);
|
|
3534
|
+
__publicField(_ChangeStarBuildingSegmentation, "default_huggingface_id", "geobase/changestar-building-segmentation-vitb");
|
|
3535
|
+
let ChangeStarBuildingSegmentation = _ChangeStarBuildingSegmentation;
|
|
3536
|
+
const _ImageFeatureExtraction = class _ImageFeatureExtraction extends BaseModel {
|
|
3537
|
+
constructor(model_id, providerParams, modelParams) {
|
|
3538
|
+
super(model_id, providerParams, modelParams);
|
|
3539
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3540
|
+
__publicField(this, "extractor");
|
|
3541
|
+
__publicField(this, "patchSize");
|
|
3542
|
+
}
|
|
3543
|
+
static async getInstance(model_id, providerParams, modelParams) {
|
|
3544
|
+
if (!_ImageFeatureExtraction.instance || parametersChanged(
|
|
3545
|
+
_ImageFeatureExtraction.instance,
|
|
3546
|
+
model_id,
|
|
3547
|
+
providerParams,
|
|
3548
|
+
modelParams
|
|
3549
|
+
)) {
|
|
3550
|
+
_ImageFeatureExtraction.instance = new _ImageFeatureExtraction(
|
|
3551
|
+
model_id,
|
|
3552
|
+
providerParams,
|
|
3553
|
+
modelParams
|
|
3554
|
+
);
|
|
3555
|
+
await _ImageFeatureExtraction.instance.initialize();
|
|
3556
|
+
}
|
|
3557
|
+
return { instance: _ImageFeatureExtraction.instance };
|
|
3558
|
+
}
|
|
3559
|
+
async initializeModel() {
|
|
3560
|
+
this.extractor = await pipeline(
|
|
3561
|
+
"image-feature-extraction",
|
|
3562
|
+
this.model_id,
|
|
3563
|
+
this.modelParams
|
|
3564
|
+
);
|
|
3565
|
+
this.extractor.processor.image_processor.do_resize = false;
|
|
3566
|
+
this.patchSize = this.extractor.model.config.patch_size;
|
|
3567
|
+
}
|
|
3568
|
+
/**
|
|
3569
|
+
* Performs image feature extraction on a geographic area specified by a GeoJSON polygon.
|
|
3570
|
+
* Extracts dense feature representations using DINOv3 and computes patch-to-patch similarities.
|
|
3571
|
+
*
|
|
3572
|
+
* @param params - Inference parameters containing polygon and processing options
|
|
3573
|
+
* @returns Promise<ImageFeatureExtractionResults> containing feature vectors, similarity matrix, and metadata
|
|
3574
|
+
* @throws {Error} If data provider, model or processor are not properly initialized
|
|
3575
|
+
*/
|
|
3576
|
+
async inference(params) {
|
|
3577
|
+
var _a, _b;
|
|
3578
|
+
const {
|
|
3579
|
+
inputs: { polygon: polygon2 },
|
|
3580
|
+
postProcessingParams: { similarityThreshold = 0.5 } = {},
|
|
3581
|
+
mapSourceParams
|
|
3582
|
+
} = params;
|
|
3583
|
+
if (!polygon2) {
|
|
3584
|
+
throw new Error("Polygon input is required for feature extraction");
|
|
3585
|
+
}
|
|
3586
|
+
if (!polygon2.geometry || polygon2.geometry.type !== "Polygon") {
|
|
3587
|
+
throw new Error("Input must be a valid GeoJSON Polygon feature");
|
|
3588
|
+
}
|
|
3589
|
+
if (!this.initialized) {
|
|
3590
|
+
await this.initialize();
|
|
3591
|
+
}
|
|
3592
|
+
if (!this.extractor || !this.patchSize) {
|
|
3593
|
+
throw new Error("Feature extractor not properly initialized");
|
|
3594
|
+
}
|
|
3595
|
+
const geoRawImage = await this.polygonToImage(
|
|
3596
|
+
polygon2,
|
|
3597
|
+
mapSourceParams == null ? void 0 : mapSourceParams.zoomLevel,
|
|
3598
|
+
mapSourceParams == null ? void 0 : mapSourceParams.bands,
|
|
3599
|
+
mapSourceParams == null ? void 0 : mapSourceParams.expression
|
|
3600
|
+
);
|
|
3601
|
+
const inferenceStartTime = performance.now();
|
|
3602
|
+
console.log("[image-feature-extraction] starting inference...");
|
|
3603
|
+
try {
|
|
3604
|
+
const features = await this.extractor(geoRawImage, {
|
|
3605
|
+
pooling: "none"
|
|
3606
|
+
});
|
|
3607
|
+
const numRegisterTokens = this.extractor.model.config.num_register_tokens ?? 0;
|
|
3608
|
+
const startIndex = 1 + numRegisterTokens;
|
|
3609
|
+
const patchFeatures = features.slice(null, [startIndex, null]);
|
|
3610
|
+
const normalizedFeatures = patchFeatures.normalize(2, -1);
|
|
3611
|
+
const scores = await matmul(
|
|
3612
|
+
normalizedFeatures,
|
|
3613
|
+
normalizedFeatures.permute(0, 2, 1)
|
|
3614
|
+
);
|
|
3615
|
+
const similarityMatrix = (await scores.tolist())[0];
|
|
3616
|
+
const featureVectors = (await patchFeatures.tolist())[0];
|
|
3617
|
+
let filteredFeatures = featureVectors;
|
|
3618
|
+
let filteredSimilarityMatrix = similarityMatrix;
|
|
3619
|
+
if (similarityThreshold > 0) {
|
|
3620
|
+
const highSimilarityPatches = /* @__PURE__ */ new Set();
|
|
3621
|
+
for (let i = 0; i < similarityMatrix.length; i++) {
|
|
3622
|
+
for (let j = 0; j < similarityMatrix[i].length; j++) {
|
|
3623
|
+
if (i !== j && similarityMatrix[i][j] >= similarityThreshold) {
|
|
3624
|
+
highSimilarityPatches.add(i);
|
|
3625
|
+
highSimilarityPatches.add(j);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
const patchIndices = Array.from(highSimilarityPatches).sort(
|
|
3630
|
+
(a, b) => a - b
|
|
3631
|
+
);
|
|
3632
|
+
filteredFeatures = patchIndices.map((i) => featureVectors[i]);
|
|
3633
|
+
filteredSimilarityMatrix = patchIndices.map(
|
|
3634
|
+
(i) => patchIndices.map((j) => similarityMatrix[i][j])
|
|
3635
|
+
);
|
|
3636
|
+
}
|
|
3637
|
+
const inferenceEndTime = performance.now();
|
|
3638
|
+
console.log(
|
|
3639
|
+
`[image-feature-extraction] inference completed. Time taken: ${(inferenceEndTime - inferenceStartTime).toFixed(2)}ms`
|
|
3640
|
+
);
|
|
3641
|
+
const result = {
|
|
3642
|
+
features: filteredFeatures,
|
|
3643
|
+
similarityMatrix: filteredSimilarityMatrix,
|
|
3644
|
+
patchSize: this.patchSize,
|
|
3645
|
+
geoRawImage,
|
|
3646
|
+
metadata: {
|
|
3647
|
+
numPatches: filteredFeatures.length,
|
|
3648
|
+
featureDimensions: ((_a = filteredFeatures[0]) == null ? void 0 : _a.length) || 0,
|
|
3649
|
+
modelId: this.model_id
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
console.log("[image-feature-extraction] Result structure:", {
|
|
3653
|
+
featuresLength: result.features.length,
|
|
3654
|
+
similarityMatrixLength: result.similarityMatrix.length,
|
|
3655
|
+
patchSize: result.patchSize,
|
|
3656
|
+
geoRawImageBounds: (_b = result.geoRawImage) == null ? void 0 : _b.getBounds(),
|
|
3657
|
+
metadata: result.metadata
|
|
3658
|
+
});
|
|
3659
|
+
return result;
|
|
3660
|
+
} catch (error) {
|
|
3661
|
+
console.error("Feature extraction error:", error);
|
|
3662
|
+
throw new Error(
|
|
3663
|
+
`Feature extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
};
|
|
3668
|
+
__publicField(_ImageFeatureExtraction, "instance", null);
|
|
3669
|
+
let ImageFeatureExtraction = _ImageFeatureExtraction;
|
|
3112
3670
|
const _BuildingFootPrintSegmentation = class _BuildingFootPrintSegmentation extends BaseModel {
|
|
3113
3671
|
constructor(model_id, providerParams, modelParams) {
|
|
3114
3672
|
super(model_id, providerParams, modelParams);
|
|
@@ -3278,141 +3836,42 @@ const _BuildingFootPrintSegmentation = class _BuildingFootPrintSegmentation exte
|
|
|
3278
3836
|
}
|
|
3279
3837
|
};
|
|
3280
3838
|
__publicField(_BuildingFootPrintSegmentation, "instance", null);
|
|
3839
|
+
__publicField(_BuildingFootPrintSegmentation, "default_huggingface_id", "geobase/building-footprint-segmentation");
|
|
3281
3840
|
let BuildingFootPrintSegmentation = _BuildingFootPrintSegmentation;
|
|
3282
|
-
const
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3841
|
+
const buildingFootprintFactory = (params, modelId, modelParams) => BuildingFootPrintSegmentation.getInstance(modelId, params, modelParams);
|
|
3842
|
+
const changeStarFactory = (params, modelId, modelParams) => ChangeStarBuildingSegmentation.getInstance(
|
|
3843
|
+
modelId,
|
|
3844
|
+
params,
|
|
3845
|
+
modelParams ?? { dtype: "fp32" }
|
|
3846
|
+
);
|
|
3847
|
+
const buildingSegmentationFactories = {
|
|
3848
|
+
[BuildingFootPrintSegmentation.default_huggingface_id]: buildingFootprintFactory,
|
|
3849
|
+
[ChangeStarBuildingSegmentation.default_huggingface_id]: changeStarFactory
|
|
3850
|
+
};
|
|
3851
|
+
const BuildingSegmentationFactory = {
|
|
3852
|
+
get knownModelIds() {
|
|
3853
|
+
return Object.keys(buildingSegmentationFactories);
|
|
3854
|
+
},
|
|
3855
|
+
getInstance(params, modelId, modelParams) {
|
|
3856
|
+
const resolvedId = modelId == null ? void 0 : modelId.trim();
|
|
3857
|
+
if (!resolvedId) {
|
|
3858
|
+
return Promise.reject(
|
|
3859
|
+
new Error(
|
|
3860
|
+
`Building segmentation modelId is required. Known ids: ${this.knownModelIds.join(", ")}`
|
|
3861
|
+
)
|
|
3300
3862
|
);
|
|
3301
|
-
await _ImageFeatureExtraction.instance.initialize();
|
|
3302
3863
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
this.modelParams
|
|
3310
|
-
);
|
|
3311
|
-
this.extractor.processor.image_processor.do_resize = false;
|
|
3312
|
-
this.patchSize = this.extractor.model.config.patch_size;
|
|
3313
|
-
}
|
|
3314
|
-
/**
|
|
3315
|
-
* Performs image feature extraction on a geographic area specified by a GeoJSON polygon.
|
|
3316
|
-
* Extracts dense feature representations using DINOv3 and computes patch-to-patch similarities.
|
|
3317
|
-
*
|
|
3318
|
-
* @param params - Inference parameters containing polygon and processing options
|
|
3319
|
-
* @returns Promise<ImageFeatureExtractionResults> containing feature vectors, similarity matrix, and metadata
|
|
3320
|
-
* @throws {Error} If data provider, model or processor are not properly initialized
|
|
3321
|
-
*/
|
|
3322
|
-
async inference(params) {
|
|
3323
|
-
var _a, _b;
|
|
3324
|
-
const {
|
|
3325
|
-
inputs: { polygon: polygon2 },
|
|
3326
|
-
postProcessingParams: { similarityThreshold = 0.5 } = {},
|
|
3327
|
-
mapSourceParams
|
|
3328
|
-
} = params;
|
|
3329
|
-
if (!polygon2) {
|
|
3330
|
-
throw new Error("Polygon input is required for feature extraction");
|
|
3331
|
-
}
|
|
3332
|
-
if (!polygon2.geometry || polygon2.geometry.type !== "Polygon") {
|
|
3333
|
-
throw new Error("Input must be a valid GeoJSON Polygon feature");
|
|
3334
|
-
}
|
|
3335
|
-
if (!this.initialized) {
|
|
3336
|
-
await this.initialize();
|
|
3337
|
-
}
|
|
3338
|
-
if (!this.extractor || !this.patchSize) {
|
|
3339
|
-
throw new Error("Feature extractor not properly initialized");
|
|
3340
|
-
}
|
|
3341
|
-
const geoRawImage = await this.polygonToImage(
|
|
3342
|
-
polygon2,
|
|
3343
|
-
mapSourceParams == null ? void 0 : mapSourceParams.zoomLevel,
|
|
3344
|
-
mapSourceParams == null ? void 0 : mapSourceParams.bands,
|
|
3345
|
-
mapSourceParams == null ? void 0 : mapSourceParams.expression
|
|
3346
|
-
);
|
|
3347
|
-
const inferenceStartTime = performance.now();
|
|
3348
|
-
console.log("[image-feature-extraction] starting inference...");
|
|
3349
|
-
try {
|
|
3350
|
-
const features = await this.extractor(geoRawImage, {
|
|
3351
|
-
pooling: "none"
|
|
3352
|
-
});
|
|
3353
|
-
const numRegisterTokens = this.extractor.model.config.num_register_tokens ?? 0;
|
|
3354
|
-
const startIndex = 1 + numRegisterTokens;
|
|
3355
|
-
const patchFeatures = features.slice(null, [startIndex, null]);
|
|
3356
|
-
const normalizedFeatures = patchFeatures.normalize(2, -1);
|
|
3357
|
-
const scores = await matmul(
|
|
3358
|
-
normalizedFeatures,
|
|
3359
|
-
normalizedFeatures.permute(0, 2, 1)
|
|
3360
|
-
);
|
|
3361
|
-
const similarityMatrix = (await scores.tolist())[0];
|
|
3362
|
-
const featureVectors = (await patchFeatures.tolist())[0];
|
|
3363
|
-
let filteredFeatures = featureVectors;
|
|
3364
|
-
let filteredSimilarityMatrix = similarityMatrix;
|
|
3365
|
-
if (similarityThreshold > 0) {
|
|
3366
|
-
const highSimilarityPatches = /* @__PURE__ */ new Set();
|
|
3367
|
-
for (let i = 0; i < similarityMatrix.length; i++) {
|
|
3368
|
-
for (let j = 0; j < similarityMatrix[i].length; j++) {
|
|
3369
|
-
if (i !== j && similarityMatrix[i][j] >= similarityThreshold) {
|
|
3370
|
-
highSimilarityPatches.add(i);
|
|
3371
|
-
highSimilarityPatches.add(j);
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
const patchIndices = Array.from(highSimilarityPatches).sort(
|
|
3376
|
-
(a, b) => a - b
|
|
3377
|
-
);
|
|
3378
|
-
filteredFeatures = patchIndices.map((i) => featureVectors[i]);
|
|
3379
|
-
filteredSimilarityMatrix = patchIndices.map(
|
|
3380
|
-
(i) => patchIndices.map((j) => similarityMatrix[i][j])
|
|
3381
|
-
);
|
|
3382
|
-
}
|
|
3383
|
-
const inferenceEndTime = performance.now();
|
|
3384
|
-
console.log(
|
|
3385
|
-
`[image-feature-extraction] inference completed. Time taken: ${(inferenceEndTime - inferenceStartTime).toFixed(2)}ms`
|
|
3386
|
-
);
|
|
3387
|
-
const result = {
|
|
3388
|
-
features: filteredFeatures,
|
|
3389
|
-
similarityMatrix: filteredSimilarityMatrix,
|
|
3390
|
-
patchSize: this.patchSize,
|
|
3391
|
-
geoRawImage,
|
|
3392
|
-
metadata: {
|
|
3393
|
-
numPatches: filteredFeatures.length,
|
|
3394
|
-
featureDimensions: ((_a = filteredFeatures[0]) == null ? void 0 : _a.length) || 0,
|
|
3395
|
-
modelId: this.model_id
|
|
3396
|
-
}
|
|
3397
|
-
};
|
|
3398
|
-
console.log("[image-feature-extraction] Result structure:", {
|
|
3399
|
-
featuresLength: result.features.length,
|
|
3400
|
-
similarityMatrixLength: result.similarityMatrix.length,
|
|
3401
|
-
patchSize: result.patchSize,
|
|
3402
|
-
geoRawImageBounds: (_b = result.geoRawImage) == null ? void 0 : _b.getBounds(),
|
|
3403
|
-
metadata: result.metadata
|
|
3404
|
-
});
|
|
3405
|
-
return result;
|
|
3406
|
-
} catch (error) {
|
|
3407
|
-
console.error("Feature extraction error:", error);
|
|
3408
|
-
throw new Error(
|
|
3409
|
-
`Feature extraction failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3864
|
+
const create = buildingSegmentationFactories[resolvedId];
|
|
3865
|
+
if (!create) {
|
|
3866
|
+
return Promise.reject(
|
|
3867
|
+
new Error(
|
|
3868
|
+
`Unknown building segmentation modelId "${resolvedId}". Known ids: ${this.knownModelIds.join(", ")}`
|
|
3869
|
+
)
|
|
3410
3870
|
);
|
|
3411
3871
|
}
|
|
3872
|
+
return create(params, resolvedId, modelParams);
|
|
3412
3873
|
}
|
|
3413
3874
|
};
|
|
3414
|
-
__publicField(_ImageFeatureExtraction, "instance", null);
|
|
3415
|
-
let ImageFeatureExtraction = _ImageFeatureExtraction;
|
|
3416
3875
|
const modelRegistry = [
|
|
3417
3876
|
{
|
|
3418
3877
|
task: "zero-shot-object-detection",
|
|
@@ -3597,20 +4056,14 @@ const modelRegistry = [
|
|
|
3597
4056
|
{
|
|
3598
4057
|
task: "building-footprint-segmentation",
|
|
3599
4058
|
library: "geoai",
|
|
3600
|
-
description: "Segments the precise outlines (footprints) of buildings in imagery. Useful for mapping, urban planning, or disaster assessment.",
|
|
4059
|
+
description: "Segments the precise outlines (footprints) of buildings in imagery. Defaults to ChangeStar ViT-B; pass modelId for the lighter footprint model. Useful for mapping, urban planning, or disaster assessment.",
|
|
3601
4060
|
examples: [
|
|
3602
4061
|
"Segment building footprints in this city block.",
|
|
3603
4062
|
"Identify the outlines of all buildings in this image.",
|
|
3604
4063
|
"Find building perimeters in this urban area."
|
|
3605
4064
|
],
|
|
3606
4065
|
ioConfig: {},
|
|
3607
|
-
geobase_ai_pipeline: (params, modelId =
|
|
3608
|
-
return BuildingFootPrintSegmentation.getInstance(
|
|
3609
|
-
modelId,
|
|
3610
|
-
params,
|
|
3611
|
-
modelParams
|
|
3612
|
-
);
|
|
3613
|
-
}
|
|
4066
|
+
geobase_ai_pipeline: (params, modelId = ChangeStarBuildingSegmentation.default_huggingface_id, modelParams) => BuildingSegmentationFactory.getInstance(params, modelId, modelParams)
|
|
3614
4067
|
},
|
|
3615
4068
|
{
|
|
3616
4069
|
task: "image-feature-extraction",
|
package/index.d.ts
CHANGED
|
@@ -198,10 +198,59 @@ declare class Wms extends MapSource {
|
|
|
198
198
|
number
|
|
199
199
|
], instance: Wms): string;
|
|
200
200
|
}
|
|
201
|
+
export interface OamConfig {
|
|
202
|
+
/** STAC API root (default HOT Imagery STAC). */
|
|
203
|
+
stacUrl?: string;
|
|
204
|
+
/** TiTiler / raster API root. */
|
|
205
|
+
rasterUrl?: string;
|
|
206
|
+
/** STAC collection id. */
|
|
207
|
+
collection?: string;
|
|
208
|
+
/** Asset key used for RGB tiles (default `visual`). */
|
|
209
|
+
asset?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Pin to a specific STAC item id. Skips search.
|
|
212
|
+
* Item tiles: `/collections/{collection}/items/{itemId}/tiles/...`
|
|
213
|
+
*/
|
|
214
|
+
itemId?: string;
|
|
215
|
+
/**
|
|
216
|
+
* When true, use the collection mosaic tile endpoint (no STAC search).
|
|
217
|
+
* When false (default) and `itemId` is unset, search STAC for the AOI and
|
|
218
|
+
* use the best matching item's tiles (fallback to mosaic if none found).
|
|
219
|
+
*/
|
|
220
|
+
mosaic?: boolean;
|
|
221
|
+
attribution?: string;
|
|
222
|
+
tileSize?: number;
|
|
223
|
+
headers?: Record<string, string>;
|
|
224
|
+
}
|
|
225
|
+
declare class Oam extends MapSource {
|
|
226
|
+
stacUrl: string;
|
|
227
|
+
rasterUrl: string;
|
|
228
|
+
collection: string;
|
|
229
|
+
asset: string;
|
|
230
|
+
itemId?: string;
|
|
231
|
+
mosaic: boolean;
|
|
232
|
+
attribution: string;
|
|
233
|
+
tileSize: number;
|
|
234
|
+
headers?: Record<string, string>;
|
|
235
|
+
/** Resolved XYZ template with `{z}/{x}/{y}` placeholders. */
|
|
236
|
+
tileUrlTemplate: string;
|
|
237
|
+
private resolvedForBboxKey?;
|
|
238
|
+
constructor(config?: OamConfig);
|
|
239
|
+
/**
|
|
240
|
+
* Resolve tile URL template for an AOI via STAC (unless mosaic/itemId pin).
|
|
241
|
+
*/
|
|
242
|
+
resolveTileTemplate(polygon: GeoJSON.Feature): Promise<string>;
|
|
243
|
+
getImage(polygon: any, bands?: number[], expression?: string, zoomLevel?: number, requiresSquare?: boolean, stitch?: boolean): Promise<GeoRawImage | GeoRawImage[][]>;
|
|
244
|
+
protected getTileUrlFromTileCoords(tileCoords: [
|
|
245
|
+
number,
|
|
246
|
+
number,
|
|
247
|
+
number
|
|
248
|
+
], instance: Oam): string;
|
|
249
|
+
}
|
|
201
250
|
declare abstract class BaseModel {
|
|
202
251
|
protected static instance: BaseModel | null;
|
|
203
252
|
protected providerParams: ProviderParams;
|
|
204
|
-
protected dataProvider: Mapbox | Geobase | Esri | Tms | Wms | undefined;
|
|
253
|
+
protected dataProvider: Mapbox | Geobase | Esri | Tms | Wms | Oam | undefined;
|
|
205
254
|
protected model_id: string;
|
|
206
255
|
protected initialized: boolean;
|
|
207
256
|
protected modelParams?: PretrainedModelOptions;
|
|
@@ -447,6 +496,7 @@ declare class BuildingFootPrintSegmentation extends BaseModel {
|
|
|
447
496
|
protected static instance: BuildingFootPrintSegmentation | null;
|
|
448
497
|
protected model: ort.InferenceSession | undefined;
|
|
449
498
|
protected processor: ImageProcessor | undefined;
|
|
499
|
+
static readonly default_huggingface_id = "geobase/building-footprint-segmentation";
|
|
450
500
|
private constructor();
|
|
451
501
|
static getInstance(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions): Promise<{
|
|
452
502
|
instance: BuildingFootPrintSegmentation;
|
|
@@ -474,6 +524,53 @@ declare class ImageFeatureExtraction extends BaseModel {
|
|
|
474
524
|
*/
|
|
475
525
|
inference(params: InferenceParams): Promise<ImageFeatureExtractionResults>;
|
|
476
526
|
}
|
|
527
|
+
declare abstract class BaseDenseSegmentationModel extends BaseModel {
|
|
528
|
+
protected model: ort.InferenceSession | undefined;
|
|
529
|
+
protected processor: ImageProcessor | undefined;
|
|
530
|
+
/** ONNX input feed name (e.g. "image"). */
|
|
531
|
+
protected abstract readonly inputName: string;
|
|
532
|
+
/** Soft-probability output tensor name (e.g. "building_prob"). */
|
|
533
|
+
protected abstract readonly outputName: string;
|
|
534
|
+
/** Tile edge length in pixels. */
|
|
535
|
+
protected readonly tileSize: number;
|
|
536
|
+
/** Overlap between adjacent tiles for feather blending. */
|
|
537
|
+
protected readonly tileOverlap: number;
|
|
538
|
+
/** Default probability threshold for binarization. */
|
|
539
|
+
protected readonly defaultThreshold: number;
|
|
540
|
+
protected constructor(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions);
|
|
541
|
+
protected initializeModel(): Promise<void>;
|
|
542
|
+
protected resolveProbabilityOutput(outputs: Record<string, any>): {
|
|
543
|
+
type: string;
|
|
544
|
+
dims: number[];
|
|
545
|
+
size: number;
|
|
546
|
+
data: Float32Array;
|
|
547
|
+
};
|
|
548
|
+
protected featherWeight(size: number, overlap: number): Float32Array;
|
|
549
|
+
protected runTile(tileImage: GeoRawImage): Promise<{
|
|
550
|
+
data: Float32Array;
|
|
551
|
+
height: number;
|
|
552
|
+
width: number;
|
|
553
|
+
}>;
|
|
554
|
+
/**
|
|
555
|
+
* Overlapped tiled inference with feather blending, matching the
|
|
556
|
+
* ChangeStar pipeline (tile_size / overlap / threshold).
|
|
557
|
+
*/
|
|
558
|
+
protected inferDenseMask(geoRawImage: GeoRawImage, threshold: number): Promise<GeoJSON.FeatureCollection>;
|
|
559
|
+
inference(params: InferenceParams): Promise<ObjectDetectionResults>;
|
|
560
|
+
}
|
|
561
|
+
declare class ChangeStarBuildingSegmentation extends BaseDenseSegmentationModel {
|
|
562
|
+
protected static instanceRef: ChangeStarBuildingSegmentation | null;
|
|
563
|
+
static readonly default_huggingface_id = "geobase/changestar-building-segmentation-vitb";
|
|
564
|
+
protected readonly inputName = "image";
|
|
565
|
+
protected readonly outputName = "building_prob";
|
|
566
|
+
protected readonly tileSize = 1024;
|
|
567
|
+
protected readonly tileOverlap = 64;
|
|
568
|
+
protected readonly defaultThreshold = 0.5;
|
|
569
|
+
private constructor();
|
|
570
|
+
static getInstance(model_id: string, providerParams: ProviderParams, modelParams?: PretrainedModelOptions): Promise<{
|
|
571
|
+
instance: ChangeStarBuildingSegmentation;
|
|
572
|
+
}>;
|
|
573
|
+
}
|
|
477
574
|
export type MapboxParams = {
|
|
478
575
|
provider: "mapbox";
|
|
479
576
|
apiKey: string;
|
|
@@ -520,6 +617,24 @@ export type WmsParams = {
|
|
|
520
617
|
headers?: Record<string, string>;
|
|
521
618
|
extraParams?: Record<string, string>;
|
|
522
619
|
};
|
|
620
|
+
export type OamParams = {
|
|
621
|
+
provider: "oam";
|
|
622
|
+
/** STAC API root (default https://api.imagery.hotosm.org/stac). */
|
|
623
|
+
stacUrl?: string;
|
|
624
|
+
/** Raster/TiTiler API root (default https://api.imagery.hotosm.org/raster). */
|
|
625
|
+
rasterUrl?: string;
|
|
626
|
+
/** STAC collection id (default openaerialmap). */
|
|
627
|
+
collection?: string;
|
|
628
|
+
/** Asset key for RGB tiles (default visual). */
|
|
629
|
+
asset?: string;
|
|
630
|
+
/** Pin to a specific STAC item id (skips search). */
|
|
631
|
+
itemId?: string;
|
|
632
|
+
/** Use collection mosaic tiles instead of per-item STAC search. */
|
|
633
|
+
mosaic?: boolean;
|
|
634
|
+
attribution?: string;
|
|
635
|
+
tileSize?: number;
|
|
636
|
+
headers?: Record<string, string>;
|
|
637
|
+
};
|
|
523
638
|
export interface InferenceInputs {
|
|
524
639
|
polygon: GeoJSON.Feature;
|
|
525
640
|
classLabel?: string;
|
|
@@ -538,10 +653,10 @@ export interface InferenceParams {
|
|
|
538
653
|
postProcessingParams?: PostProcessingParams;
|
|
539
654
|
mapSourceParams?: MapSourceParams;
|
|
540
655
|
}
|
|
541
|
-
export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams | WmsParams;
|
|
656
|
+
export type ProviderParams = MapboxParams | SentinelParams | GeobaseParams | EsriParams | TmsParams | WmsParams | OamParams;
|
|
542
657
|
export type HuggingFaceModelTask = "mask-generation" | "zero-shot-object-detection" | "zero-shot-image-classification" | "object-detection" | "oriented-object-detection" | "image-feature-extraction";
|
|
543
658
|
export type GeobaseAiModelTask = "damage-assessment" | "vegetation-classification" | "land-cover-classification" | "land-use-classification" | "land-cover-change-detection" | "land-use-change-detection" | "solar-panel-detection" | "ship-detection" | "car-detection" | "wetland-segmentation" | "building-detection" | "oil-storage-tank-detection" | "building-footprint-segmentation";
|
|
544
|
-
export type ModelInstance = MaskGeneration | ZeroShotObjectDetection | ObjectDetection | OrientedObjectDetection | LandCoverClassification | SolarPanelDetection | ShipDetection | CarDetection | WetLandSegmentation | BuildingDetection | OilStorageTankDetection | BuildingFootPrintSegmentation | ImageFeatureExtraction;
|
|
659
|
+
export type ModelInstance = MaskGeneration | ZeroShotObjectDetection | ObjectDetection | OrientedObjectDetection | LandCoverClassification | SolarPanelDetection | ShipDetection | CarDetection | WetLandSegmentation | BuildingDetection | ChangeStarBuildingSegmentation | OilStorageTankDetection | BuildingFootPrintSegmentation | ImageFeatureExtraction;
|
|
545
660
|
export type ModelConfig = {
|
|
546
661
|
task: HuggingFaceModelTask | GeobaseAiModelTask;
|
|
547
662
|
library: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "geoai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "A JavaScript library for running Geo AI models in frontend applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"geoai",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"test": "vitest",
|
|
64
64
|
"test:coverage": "vitest --coverage",
|
|
65
65
|
"test:build": "pnpm run build && vitest run test/build.test.ts",
|
|
66
|
+
"docs:llms": "node ./docs/scripts/generate-llms-txt.mjs",
|
|
66
67
|
"lint:scripts": "eslint . --ext .ts",
|
|
67
68
|
"lint:styles": "stylelint ./**/*.{css,scss}",
|
|
68
69
|
"format:scripts": "prettier . --write",
|